turingtrader.js
Version:
A backtesting engine for Node.js
419 lines (335 loc) • 15.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createSimulator = void 0;
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _tradingCalendarUs = require("./trading-calendar-us");
var _data2 = require("../data");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
var createSimulator = function createSimulator(algo) {
var _algo$calendar, _internalInterface;
var data = {
tradingCalendar: (_algo$calendar = algo.calendar) !== null && _algo$calendar !== void 0 ? _algo$calendar : (0, _tradingCalendarUs.createTradingCalendarUS)(),
cache: {}
};
var state = {
tradingDays: null,
tradingDayIndex: null,
cash: 0.0,
positions: {}
}; //========== internal interface: methods available to algorithms
var internalInterface = (_internalInterface = {
//----- properties
get startDate() {
return data.tradingCalendar.startDate;
},
set startDate(d) {
data.tradingCalendar.startDate = d;
},
get endDate() {
return data.tradingCalendar.endDate;
},
set endDate(d) {
data.tradingCalendar.endDate = d;
},
get tradingCalendar() {
return data.tradingCalendar;
},
get cache() {
return data.cache;
},
orderTypes: {
// NOTE: the values correspond to the
// sequence of order execution
mktThisClose: 0,
mktNextOpen: 1,
limNextBar: 2,
stpNextBar: 3
},
get nav() {},
get result() {
return getProperty("result");
},
//----- methods
info: function info() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return console.log("INFO: ", args);
},
asset: function asset(name) {
return (0, _data2.loadAsset)(internalInterface, name, algo.data);
}
}, (0, _defineProperty2["default"])(_internalInterface, "cache", function cache(id, fn) {
var _data$cache$id;
return (_data$cache$id = data.cache[id]) !== null && _data$cache$id !== void 0 ? _data$cache$id : data.cache[id] = fn();
}), (0, _defineProperty2["default"])(_internalInterface, "loop", function () {
var _loop = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(fn) {
var result, i, isLastTradingDay, orders, oi, o, ticker, _loop2, ti;
return _regenerator["default"].wrap(function _callee$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
state.tradingDays = internalInterface.tradingCalendar.tradingDays;
state.nextTradingDay = internalInterface.tradingCalendar.nextTradingDay;
result = {
t: [],
// we populate the next bar's open on this bar's close
// therefore, we never get to populate the first bar's open
o: [state.cash],
// we can't produce meaningful high and low values.
// therefore, we just skip them.
c: [],
// these are the asset allocations on open and close
// limit and stop orders are reflected in the close
oAlloc: [{
ticker: [],
weight: []
}],
cAlloc: [],
// this flag indicates rebalancing dates. mktNextOpen
// orders are reflected on the previous close because
// that's when they've been submitted
fAlloc: []
};
_context2.t0 = _regenerator["default"].keys(state.tradingDays);
case 4:
if ((_context2.t1 = _context2.t0()).done) {
_context2.next = 31;
break;
}
i = _context2.t1.value;
// NOTE: this loop is processed strictly
// in order to avoid any issues w/ the
// simulator's state
state.tradingDayIndex = i;
isLastTradingDay = Number(i) === state.tradingDays.length - 1;
_context2.next = 10;
return fn();
case 10:
orders = _context2.sent;
// set flag to indicate rebalancing dates
result.fAlloc.push((orders === null || orders === void 0 ? void 0 : orders.length) > 0 ? 1 : 0); // make sure we have a position for each
// asset affected by the order basket
_context2.t2 = _regenerator["default"].keys(orders);
case 13:
if ((_context2.t3 = _context2.t2()).done) {
_context2.next = 22;
break;
}
oi = _context2.t3.value;
o = orders[oi];
_context2.next = 18;
return o.ticker;
case 18:
ticker = _context2.sent;
if (!(ticker in state.positions)) {
state.positions[ticker] = {
qty: 0.0,
data: o.data
};
}
_context2.next = 13;
break;
case 22:
_loop2 = /*#__PURE__*/_regenerator["default"].mark(function _loop2(ti) {
var prices, _ticker, nav, _oi, _o, _ticker2, qtyCurrent, qtyNew, cashFlow, alloc, _ticker3, _alloc, _ticker4;
return _regenerator["default"].wrap(function _loop2$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
// find the relevant asset prices
prices = {};
_context.t0 = _regenerator["default"].keys(state.positions);
case 2:
if ((_context.t1 = _context.t0()).done) {
_context.next = 16;
break;
}
_ticker = _context.t1.value;
if (!(internalInterface.orderTypes[ti] === internalInterface.orderTypes.mktThisClose)) {
_context.next = 10;
break;
}
_context.next = 7;
return state.positions[_ticker].data.close.t(0);
case 7:
_context.t2 = _context.sent;
_context.next = 13;
break;
case 10:
_context.next = 12;
return state.positions[_ticker].data.open.t(-1);
case 12:
_context.t2 = _context.sent;
case 13:
prices[_ticker] = _context.t2;
_context.next = 2;
break;
case 16:
// calculate NAV
nav = Object.keys(state.positions).reduce(function (acc, ticker) {
return acc + state.positions[ticker].qty * prices[ticker];
}, state.cash); // process orders
_context.t3 = _regenerator["default"].keys(orders);
case 18:
if ((_context.t4 = _context.t3()).done) {
_context.next = 33;
break;
}
_oi = _context.t4.value;
_o = _objectSpread({}, orders[_oi]); // on last bar, we execute mktNextOpen as mktThisClose
if (isLastTradingDay === true && _o.type === internalInterface.orderTypes.mktNextOpen) {
_o.type = internalInterface.orderTypes.mktThisClose;
} // only execute orders of type we are currently processing
if (!(_o.type !== internalInterface.orderTypes[ti])) {
_context.next = 24;
break;
}
return _context.abrupt("continue", 18);
case 24:
_context.next = 26;
return _o.ticker;
case 26:
_ticker2 = _context.sent;
qtyCurrent = state.positions[_ticker2].qty;
qtyNew = _o.alloc * nav / prices[_ticker2];
cashFlow = (qtyNew - qtyCurrent) * prices[_ticker2]; // ignore orders smaller than 0.1% of NAV
if (Math.abs(cashFlow / nav) > 1e-3) {
// TODO: consider commissions here
state.cash -= cashFlow;
state.positions[_ticker2].qty = qtyNew;
}
_context.next = 18;
break;
case 33:
// TODO: remove positions < 0.1% of NAV
// save NAV & allocations
if (internalInterface.orderTypes[ti] === internalInterface.orderTypes.mktThisClose) {
result.t.push(internalInterface.t(0));
result.c.push(nav);
alloc = {
ticker: [],
weight: []
};
for (_ticker3 in state.positions) {
alloc.ticker.push(_ticker3);
alloc.weight.push(state.positions[_ticker3].qty * prices[_ticker3] / nav);
}
result.cAlloc.push(alloc);
} else if (isLastTradingDay !== true && internalInterface.orderTypes[ti] === internalInterface.orderTypes.mktNextOpen) {
// simData.t added while processing mktThisClose
result.o.push(nav);
_alloc = {
ticker: [],
weight: []
};
for (_ticker4 in state.positions) {
_alloc.ticker.push(_ticker4);
_alloc.weight.push(state.positions[_ticker4].qty * prices[_ticker4] / nav);
}
result.oAlloc.push(_alloc);
} //internalInterface.info(`${internalInterface.t(0)}: ${nav}`)
case 34:
case "end":
return _context.stop();
}
}
}, _loop2);
});
_context2.t4 = _regenerator["default"].keys(internalInterface.orderTypes);
case 24:
if ((_context2.t5 = _context2.t4()).done) {
_context2.next = 29;
break;
}
ti = _context2.t5.value;
return _context2.delegateYield(_loop2(ti), "t6", 27);
case 27:
_context2.next = 24;
break;
case 29:
_context2.next = 4;
break;
case 31:
return _context2.abrupt("return", result);
case 32:
case "end":
return _context2.stop();
}
}
}, _callee);
}));
function loop(_x) {
return _loop.apply(this, arguments);
}
return loop;
}()), (0, _defineProperty2["default"])(_internalInterface, "t", function t(offset) {
// this is the raw index into the tradingDays array
var rawIndex = state.tradingDayIndex - offset; // if the index is -1, we return the
// next trading day *after* the sim range
if (rawIndex === state.tradingDays.length) return state.nextTradingDay;
var index = Math.min(state.tradingDays.length - 1, Math.max(0, rawIndex));
return state.tradingDays[index];
}), (0, _defineProperty2["default"])(_internalInterface, "deposit", function deposit(amount) {
state.cash += amount;
}), _internalInterface); //========== external interface: methods called on the simulator instance
var externalInterface = {
run: function () {
var _run = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2() {
var result, key, ticker, _data;
return _regenerator["default"].wrap(function _callee2$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return algo.run(internalInterface);
case 2:
result = _context3.sent;
_context3.t0 = _regenerator["default"].keys(algo.extra);
case 4:
if ((_context3.t1 = _context3.t0()).done) {
_context3.next = 13;
break;
}
key = _context3.t1.value;
ticker = algo.extra[key];
_context3.next = 9;
return (0, _data2.loadAsset)(internalInterface, ticker, algo.data).data;
case 9:
_data = _context3.sent;
result[key] = {
meta: {
ticker: _data.meta.ticker
},
c: _data.c
};
_context3.next = 4;
break;
case 13:
return _context3.abrupt("return", result);
case 14:
case "end":
return _context3.stop();
}
}
}, _callee2);
}));
function run() {
return _run.apply(this, arguments);
}
return run;
}(),
report: function report() {
return algo.report(internalInterface);
}
};
return externalInterface;
}; //==============================================================================
// end of file
exports.createSimulator = createSimulator;