betfair-emulator
Version:
289 lines (265 loc) • 10.4 kB
JavaScript
'use strict';
let _ = require('lodash');
let utils = require('./utils.js');
let EmulatorBet = require('./emulator_bet.js');
class EmulatorMarket {
constructor(logger, marketId) {
this.log = logger;
this.marketId = marketId;
this.initialized = false;
this.runners = {};
this.matchedBets = new Map();
this.unmatchedBets = new Map();
}
// match bets using new price data
_matchBets() {
this.log.debug('try match bets');
let matchedBetIds = [];
this.unmatchedBets.forEach((bet, betId) => {
this.log.info('_matchBets betId=', betId, bet.toString());
let runner = this.runners[bet.selectionId];
switch (bet.side) {
case 'LAY':
let toLay = runner.layAvailability;
if(toLay.length==0) {
// no offers
return;
}
let worstLay = _.last(toLay);
console.log(worstLay, toLay);
if (bet.price > worstLay.price) {
bet.match(worstLay.price,
`marketWorstLayPrice=${worstLay.price}, price=${bet.price}`);
matchedBetIds.push(bet.betId);
}
_.forEach(toLay, (offer) => {
if (bet.price >= offer.price && bet.size < offer.size) {
bet.match(offer.price,
`marketPrice=${offer.price}, marketSize=${offer.size} price=${bet.price}`);
matchedBetIds.push(bet.betId);
return false;
}
return true;
});
break;
case 'BACK':
let toBack = runner.backAvailability;
if(toBack.length==0) {
// no offers
return;
}
let worstBack = _.last(toBack);
if (bet.price < worstBack.price) {
bet.match(worstBack.price,
`marketWorstBackPrice=${worstBack.price}, price=${bet.price}`);
matchedBetIds.push(bet.betId);
}
_.forEach(toBack, (offer) => {
if (bet.price <= offer.price && bet.size < offer.size) {
bet.match(offer.price,
`marketPrice=${offer.price}, marketSize=${offer.size} price=${bet.price}`);
matchedBetIds.push(bet.betId);
return false;
}
return true;
});
break;
}
});
// place matched bets to this.matchedBets, remove from this.unmatchedBets
_.forEach(matchedBetIds, (betId) => {
let bet = this.unmatchedBets.get(betId);
this.matchedBets.set(betId, bet);
this.unmatchedBets.delete(betId);
});
return;
}
// handle market go inplay
_onGoInplay() {
this.log.debug('marketId: ' + this.marketId + ' go inplay');
// cancel all the bets that has persistence LAPSE
let lapsedIds = [];
for (let tuple of this.unmatchedBets) {
let [betId, bet] = tuple;
if (bet.limitOrder.persistenceType == 'LAPSE') {
bet.lapse();
lapsedIds.push(betId);
}
}
_.each(lapsedIds, (id) => {this.unmatchedBets.delete(id);})
}
// update listMarketBook with emulator orders
_updateOrders(runner, orders) {
this.log.debug('update orders for runnerId' + runner.selectionId);
if (!_.isArray(runner.orders)) {
runner.orders = [];
}
let selectionId = runner.selectionId;
for (let tuple of orders) {
let [betId, bet] = tuple;
if (selectionId == bet.selectionId) {
runner.orders.push(bet.getOrder());
}
}
}
onListMarketBook(params, marketBook) {
if (marketBook.marketId != this.marketId) {
throw new Error('onListMarketBook marketId mismatch');
}
this.log.debug('onListMarketBook for market: ' + this.marketId, params);
if (!this.inplay && marketBook.inplay) {
this._onGoInplay();
}
this.status = marketBook.status;
this.betDelay = marketBook.betDelay;
this.inplay = marketBook.inplay;
this.version = marketBook.version;
// update prices
_.each(marketBook.runners, (runner) => {
// avaliable to BACK
let availableToBack = _.cloneDeep(runner.ex.availableToBack);
let backAvailability = [];
_.reduce(availableToBack, (acc, item) => {
acc += item.size;
item.size = utils.normalizeSize(acc);
backAvailability.push(item);
return acc;
}, 0);
this.log.debug('back availability', backAvailability);
// avaliable to lAY
let availableToLay = _.cloneDeep(runner.ex.availableToLay);
let layAvailability = [];
_.reduce(availableToLay, (acc, item) => {
acc += item.size;
item.size = utils.normalizeSize(acc);
layAvailability.push(item);
return acc;
}, 0);
this.log.debug('lay availability', layAvailability);
// store
this.runners[runner.selectionId] = {
selectionId: runner.selectionId,
backAvailability,
layAvailability
}
});
// try match bets
this._matchBets();
// put orders into response
_.each(marketBook.runners, (runner) => {
// update orders/matches
if (params.orderProjection == 'ALL' || params.orderProjection == 'EXECUTABLE') {
this.log.debug('update unmatched orders');
this._updateOrders(runner, this.unmatchedBets);
}
if (params.orderProjection == 'ALL' || params.orderProjection == 'EXECUTION_COMPLETE') {
this.log.debug('update matched orders');
this._updateOrders(runner, this.matchedBets);
}
});
this.initialized = true;
}
placeOrders(params, cb = () => {}) {
if (params.marketId != this.marketId) {
throw new Error('placeOrders marketId mismatch');
}
if (!_.isArray(params.instructions) || _.isEmpty(params.instructions)) {
throw new Error('placeOrders: bad or empty instructions');
}
this.log.debug('placeOrders params:', params);
// TODO - limitOrder is hardcoded
let marketId = params.marketId;
let bets = [];
_.each(params.instructions, (instruction) => {
let bet = new EmulatorBet(
this.log,
instruction.selectionId,
instruction.side,
instruction.limitOrder.price,
instruction.limitOrder.size,
{
orderType: instruction.orderType,
persistenceType: instruction.limitOrder.persistenceType
}
);
this.unmatchedBets.set(bet.betId, bet);
bets.push(bet);
});
// result
let result = {
customerRef: params.customerRef,
status: "SUCCESS",
marketId: marketId,
id: 2, // UNDOCUMENTED parameter
instructionReports: _.map(bets, (bet) => {
return {
status: "SUCCESS",
instruction: bet.getInstruction(),
betId: bet.betId,
placedDate: bet.placedDate,
averagePriceMatched: bet.averagePriceMatched,
sizeMatched: bet.sizeMatched,
isEmulatorBet: true
}
})
};
this.log.debug('placeOrders result:', result);
cb(null, result);
}
cancelOrders(params, cb = () => {}) {
if (params.marketId != this.marketId) {
throw new Error('cancelOrders marketId mismatch');
}
let instructions = _.cloneDeep(params.instructions);
if(!instructions) {
let ids = Array.from(this.unmatchedBets.keys());
instructions = ids.map((id)=>{return {betId:id}});
}
if(instructions.length==0) {
// no bets to cancel
cb(null, {
customerRef: params.customerRef,
status: 'SUCCESS',
marketId: params.marketId,
instructionReports: []
});
return;
}
let allErrors = true;
let hasErrors = false;
let result = [];
_.each(instructions, (instruction) => {
let bet = this.unmatchedBets.get(instruction.betId);
if(!bet) {
result.push({
status: "FAILURE",
errorCode: "BET_TAKEN_OR_LAPSED",
instruction
});
hasErrors = true;
return;
}
let sizeCancelled = bet.cancel(instruction.sizeReduction);
result.push({
status: "SUCCESS",
instruction,
sizeCancelled,
cancelledDate: new Date()
});
allErrors = false;
if(bet.sizeRemaining==0) {
this.unmatchedBets.delete(bet.betId);
}
});
let status = allErrors ? 'FAILURE' : (hasErrors ? "PROCESSED_WITH_ERRORS" : "SUCCESS");
let errorCode = allErrors ? 'BET_ACTION_ERROR' : (hasErrors ? "PROCESSED_WITH_ERRORS" : undefined);
cb(null, {
customerRef: params.customerRef,
status,
errorCode,
marketId: params.marketId,
instructionReports: result
});
}
}
module.exports = EmulatorMarket;