/* * Copyright Reiryoku Technologies and its contributors, www.reiryoku.com, www.mida.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import { MidaTradingAccount, } from "#accounts/MidaTradingAccount"; import { date, MidaDate, } from "#dates/MidaDate"; import { MidaDateConvertible, } from "#dates/MidaDateConvertible"; import { decimal, MidaDecimal, } from "#decimals/MidaDecimal"; import { MidaDecimalConvertible, } from "#decimals/MidaDecimalConvertible"; import { MidaEvent, } from "#events/MidaEvent"; import { MidaEventListener, } from "#events/MidaEventListener"; import { logger, } from "#loggers/MidaLogger"; import { MidaOrder, } from "#orders/MidaOrder"; import { MidaOrderDirection, } from "#orders/MidaOrderDirection"; import { MidaOrderDirectives, } from "#orders/MidaOrderDirectives"; import { MidaOrderExecutionType, } from "#orders/MidaOrderExecutionType"; import { MidaOrderPurpose, } from "#orders/MidaOrderPurpose"; import { MidaOrderRejection, } from "#orders/MidaOrderRejection"; import { MidaOrderStatus, } from "#orders/MidaOrderStatus"; import { MidaOrderTimeInForce, } from "#orders/MidaOrderTimeInForce"; import { MidaPeriod, } from "#periods/MidaPeriod"; import { MidaPosition, } from "#positions/MidaPosition"; import { MidaPositionDirection, } from "#positions/MidaPositionDirection"; import { MidaPositionStatus, } from "#positions/MidaPositionStatus"; import { MidaProtection, } from "#protections/MidaProtection"; import { MidaProtectionDirectives, } from "#protections/MidaProtectionDirectives"; import { MidaSymbol, } from "#symbols/MidaSymbol"; import { MidaTick, } from "#ticks/MidaTick"; import { MidaTimeframe, } from "#timeframes/MidaTimeframe"; import { MidaTrade, } from "#trades/MidaTrade"; import { MidaTradeDirection, } from "#trades/MidaTradeDirection"; import { MidaTradePurpose, } from "#trades/MidaTradePurpose"; import { MidaTradeStatus, } from "#trades/MidaTradeStatus"; import { MidaEmitter, } from "#utilities/emitters/MidaEmitter"; import { createOrderResolver, uuid, } from "#utilities/MidaUtilities"; import { MidaPlaygroundAccount, } from "!/src/playground/accounts/MidaPlaygroundAccount"; import { MidaPlaygroundAccountConfiguration, } from "!/src/playground/accounts/MidaPlaygroundAccountConfiguration"; import { MidaPlaygroundCommissionCustomizer, } from "!/src/playground/customizers/MidaPlaygroundCommissionCustomizer"; import { MidaPlayground, } from "!/src/playground/MidaPlayground"; import { MidaPlaygroundEngineElapsedData, } from "!/src/playground/MidaPlaygroundEngineElapsedData"; import { MidaPlaygroundEngineParameters, } from "!/src/playground/MidaPlaygroundEngineParameters"; import { tickFromPeriod, } from "!/src/playground/MidaPlaygroundUtilities"; import { MidaPlaygroundOrder, } from "!/src/playground/orders/MidaPlaygroundOrder"; import { MidaPlaygroundPosition, } from "!/src/playground/positions/MidaPlaygroundPosition"; import { MidaPlaygroundTrade, } from "!/src/playground/trades/MidaPlaygroundTrade"; /* * 5W (Who, What, Where, When, Why) * This is a trading simulator created by Vasile Pește, Reiryoku Technologies and its contributors */ /** * *** *** *** Parameters and Features Documentation *** *** *** * * Feed Confirmation: * The parameter allowing to stop the engine from emitting new data until your logic * has completed processing the current data. * The Problem: when backtesting with Trading Systems, the engine might emit ticks * when the current tick is still being processed by the trading system logic, since * the engine and trading system are asynchronous and don't know each other, this can * lead to unexpected behaviour for example executing an order at an unexpected price. * * If waitFeedConfirmation is set to true and elapseTime is used, nextFeed() must * be invoked by the trading system when its data processing has finished */ export class MidaPlaygroundEngine { #localDate: MidaDate; // readonly #ticksGenerators: Record>; readonly #periodsGenerators: Record>>; // // #savedTicksLimit: number; readonly #localTicks: Record; readonly #lastTicks: Record; // // #savedPeriodsLimit: number; readonly #localPeriods: Record>; // readonly #orders: Record; // For faster lookups readonly #ordersArray: MidaPlaygroundOrder[]; // For faster iterations readonly #trades: Record; // For faster lookups readonly #tradesArray: MidaPlaygroundTrade[]; // For faster iterations readonly #positions: Record; // For faster lookups readonly #positionsArray: MidaPlaygroundPosition[]; // For faster iterations readonly #tradingAccounts: Record; #commissionCustomizer?: MidaPlaygroundCommissionCustomizer; #waitFeedConfirmation: boolean; #feedResolver?: () => void; #feedResolverPromise: Promise | undefined; readonly #emitter: MidaEmitter; readonly #protectedEmitter: MidaEmitter; public constructor ({ localDate, commissionCustomizer, savedTicksLimit, savedPeriodsLimit, }: MidaPlaygroundEngineParameters = {}) { this.#localDate = date(localDate ?? 0); this.#ticksGenerators = {}; this.#periodsGenerators = {}; this.#savedTicksLimit = savedTicksLimit ?? 1000; this.#localTicks = {}; this.#lastTicks = {}; this.#savedPeriodsLimit = savedPeriodsLimit ?? 1000; this.#localPeriods = {}; this.#orders = {}; this.#ordersArray = []; this.#trades = {}; this.#tradesArray = []; this.#positions = {}; this.#positionsArray = []; this.#tradingAccounts = {}; this.#commissionCustomizer = commissionCustomizer; this.#waitFeedConfirmation = false; this.#feedResolver = undefined; this.#feedResolverPromise = undefined; this.#emitter = new MidaEmitter(); this.#protectedEmitter = new MidaEmitter(); } public get localDate (): MidaDate { return this.#localDate; } public get orders (): MidaOrder[] { return this.#ordersArray; } public get trades (): MidaTrade[] { return this.#tradesArray; } public get positions (): MidaPosition[] { return this.#positionsArray; } public get savedTicksLimit (): number { return this.#savedTicksLimit; } public set savedTicksLimit (limit: number) { this.#savedTicksLimit = limit; } public get savedPeriodsLimit (): number { return this.#savedPeriodsLimit; } public set savedPeriodsLimit (limit: number) { this.#savedPeriodsLimit = limit; } public get waitFeedConfirmation (): boolean { return this.#waitFeedConfirmation; } public set waitFeedConfirmation (waitFeedConfirmation: boolean) { this.#waitFeedConfirmation = waitFeedConfirmation; } public setLocalDate (date: MidaDateConvertible): void { this.#localDate = new MidaDate(date); } public setTicksGenerator (symbol: string, generator: AsyncGenerator): void { this.#ticksGenerators[symbol] = generator; } public setPeriodsGenerator (symbol: string, timeframe: MidaTimeframe, generator: AsyncGenerator): void { if (!this.#periodsGenerators[symbol]) { this.#periodsGenerators[symbol] = {}; } this.#periodsGenerators[symbol][timeframe] = generator; } public setCommissionCustomizer (customizer?: MidaPlaygroundCommissionCustomizer): void { this.#commissionCustomizer = customizer; } public async getSymbolExchangeRate (symbol: string): Promise { let lastTick: MidaTick | undefined = this.#lastTicks[symbol]; if (!lastTick) { for (const tick of this.#localTicks[symbol] ?? []) { if (tick.date.timestamp <= this.#localDate.timestamp) { lastTick = tick; this.#lastTicks[symbol] = tick; } } } if (!lastTick) { throw new Error("No quotes available"); } return [ lastTick.bid, lastTick.ask, ]; } public async getSymbolBid (symbol: string): Promise { return (await this.getSymbolExchangeRate(symbol))[0]; } public async getSymbolAsk (symbol: string): Promise { return (await this.getSymbolExchangeRate(symbol))[1]; } public async getSymbolPeriods (symbol: string, timeframe: MidaTimeframe): Promise { return this.#localPeriods[symbol][timeframe] ?? []; } // eslint-disable-next-line max-lines-per-function public async placeOrder (tradingAccount: MidaPlaygroundAccount, directives: MidaOrderDirectives): Promise { const positionId: string | undefined = directives.positionId; let symbol: string; let purpose: MidaOrderPurpose; if (positionId) { const position: MidaPosition | undefined = await this.getOpenPositionById(positionId); if (!position) { throw new Error("Position not found"); } symbol = position.symbol; if ( directives.direction === MidaOrderDirection.BUY && position.direction === MidaPositionDirection.LONG || directives.direction === MidaOrderDirection.SELL && position.direction === MidaPositionDirection.SHORT ) { purpose = MidaOrderPurpose.OPEN; } else { purpose = MidaOrderPurpose.CLOSE; } } else { symbol = directives.symbol as string; purpose = MidaOrderPurpose.OPEN; // Hedged account, always open a position if no specific position is impacted } const creationDate: MidaDate = this.#localDate; const order: MidaPlaygroundOrder = new MidaPlaygroundOrder({ id: uuid(), tradingAccount, symbol, requestedVolume: decimal(directives.volume), direction: directives.direction, purpose, limitPrice: directives.limit !== undefined ? decimal(directives.limit) : undefined, stopPrice: directives.stop !== undefined ? decimal(directives.stop) : undefined, status: MidaOrderStatus.REQUESTED, creationDate, lastUpdateDate: creationDate, positionId, trades: [], timeInForce: directives.timeInForce ?? MidaOrderTimeInForce.GOOD_TILL_CANCEL, isStopOut: false, engineEmitter: this.#protectedEmitter, requestedProtection: directives.protection, }); this.#orders[order.id] = order; this.#ordersArray.push(order); const resolver: Promise = createOrderResolver(order, directives.resolverEvents) as Promise; const listeners: { [eventType: string]: MidaEventListener } = directives.listeners ?? {}; for (const eventType of Object.keys(listeners)) { order.on(eventType, listeners[eventType]); } this.acceptOrder(order.id); if (order.executionType === MidaOrderExecutionType.MARKET) { this.tryExecuteOrder(order); // Not necessary to await because of resolver } else { this.moveOrderToPending(order.id); // Used to check if the pending order can be executed at the current tick this.#updatePendingOrder(order, this.#lastTicks[symbol]); // Not necessary to await because of resolver } return resolver; } /** * Elapses a given amount of time (triggering the respective market data) * @param seconds Amount of seconds to elapse */ // eslint-disable-next-line max-lines-per-function public async elapseTime (seconds: number): Promise { if (seconds <= 0) { return { elapsedTicks: [], elapsedPeriods: [], }; } const previousDate: MidaDate = this.#localDate; const currentDate: MidaDate = previousDate.addSeconds(seconds); // const elapsedTicks: MidaTick[] = []; // Assumption: generated ticks are ordered by time for (const [ symbol, generator, ] of Object.entries(this.#ticksGenerators)) { while (true) { const tick: MidaTick | undefined = (await generator.next()).value; if (!tick) { logger.info(`Playground | ${symbol} ticks generator has reached its end`); break; } // Terminate generation if a tick of the future is encountered // (assume that generated ticks are ordered by time) if (tick.date.timestamp > currentDate.timestamp) { break; } if (tick.date.timestamp > previousDate.timestamp) { elapsedTicks.push(tick); } } } // // const elapsedPeriods: MidaPeriod[] = []; for (const [ symbol, timeframesMap, ] of Object.entries(this.#periodsGenerators)) { for (const [ timeframe, generator, ] of Object.entries(timeframesMap)) { while (true) { const period: MidaPeriod | undefined = (await generator.next()).value; if (!period) { logger.info(`Playground | ${symbol} ${timeframe} periods generator has reached its end`); break; } // TODO: Trigger also opened candles, not only closed // Terminate generation if a period of the future is encountered // (assume that generated periods are ordered by time) if (period.endDate.timestamp > currentDate.timestamp) { break; } if (period.endDate.timestamp > previousDate.timestamp) { elapsedPeriods.push(period); } } } } // const elapsedData: (MidaTick | MidaPeriod)[] = [ ...elapsedTicks, ...elapsedPeriods, ]; elapsedData.sort((a: MidaTick | MidaPeriod, b: MidaTick | MidaPeriod): number => { const left: number = a instanceof MidaTick ? a.date.timestamp : a.endDate.timestamp; const right: number = b instanceof MidaTick ? b.date.timestamp : b.endDate.timestamp; return left - right; }); logger.info(`Playground | Preparing to process ${elapsedTicks.length} ticks and ${elapsedPeriods.length} periods`); for (let i = 0, length = elapsedData.length; i < length; ++i) { const data: MidaTick | MidaPeriod = elapsedData[i]; if (data instanceof MidaTick) { await this.#processTick(data); } else { await this.#processPeriod(data); } } this.#localDate = currentDate; logger.info("Playground | Elapse completed"); return { elapsedTicks, elapsedPeriods, }; } public async elapseTicks (quantity: number = 1): Promise { if (quantity <= 0) { return { elapsedTicks: [], elapsedPeriods: [], }; } const currentTimestamp = this.#localDate.timestamp; // const elapsedTicks: MidaTick[] = []; // Assumption: generated ticks are ordered by time for (const [ symbol, generator, ] of Object.entries(this.#ticksGenerators)) { while (true) { const tick: MidaTick | undefined = (await generator.next()).value; if (!tick) { logger.info(`Playground | ${symbol} ticks generator has reached its end`); break; } if (tick.date.timestamp > currentTimestamp) { elapsedTicks.push(tick); } if (elapsedTicks.length >= quantity) { break; } } } // for (let i = 0, length = elapsedTicks.length; i < length; ++i) { await this.#processTick(elapsedTicks[i]); } return { elapsedTicks, elapsedPeriods: [], }; } // public nextFeed (): void { if (this.#feedResolver) { this.#feedResolver(); this.#feedResolver = undefined; } } // public addSymbolTicks (symbol: string, ticks: MidaTick[]): void { const localTicks: MidaTick[] = this.getSymbolTicks(symbol); const updatedTicks: MidaTick[] = [ ...localTicks, ...ticks, ]; updatedTicks.sort((a: MidaTick, b: MidaTick): number => a.date.timestamp - b.date.timestamp); this.#localTicks[symbol] = this.savedTicksLimit > 0 ? updatedTicks.slice(-this.savedTicksLimit) : updatedTicks; } public addSymbolPeriods (symbol: string, periods: MidaPeriod[]): void { const timeframe: MidaTimeframe = periods[0].timeframe; const localPeriods: MidaPeriod[] = this.#localPeriods[symbol]?.[timeframe] ?? []; const updatedPeriods: MidaPeriod[] = localPeriods.concat(periods); updatedPeriods.sort((a: MidaPeriod, b: MidaPeriod): number => a.startDate.timestamp - b.startDate.timestamp); const cappedPeriods: MidaPeriod[] = this.savedPeriodsLimit > 0 ? updatedPeriods.slice(-this.savedPeriodsLimit) : updatedPeriods; if (!this.#localPeriods[symbol]) { this.#localPeriods[symbol] = {}; } this.#localPeriods[symbol][timeframe] = cappedPeriods; } public getSymbolTicks (symbol: string): MidaTick[] { return this.#localTicks[symbol] ?? []; } public getOrdersByAccount (tradingAccount: MidaPlaygroundAccount): MidaPlaygroundOrder[] { return this.#ordersArray .filter((order: MidaOrder) => tradingAccount === order.tradingAccount); } public getTradesByAccount (tradingAccount: MidaPlaygroundAccount): MidaPlaygroundTrade[] { return this.#tradesArray .filter((trade: MidaTrade) => tradingAccount === trade.tradingAccount); } public async getPendingOrders (): Promise { const pendingOrders: MidaPlaygroundOrder[] = []; for (const account of Object.values(this.#tradingAccounts)) { pendingOrders.push(...await account.getPendingOrders()); } return pendingOrders; } public async getOpenPositions (): Promise { const openPositions = []; for (let i = 0, length = this.#positionsArray.length; i < length; ++i) { const position: MidaPlaygroundPosition = this.#positionsArray[i]; if (position.status === MidaPositionStatus.OPEN) { openPositions.push(position); } } return openPositions; } public async getOpenPositionById (id: string): Promise { const openPositions: MidaPlaygroundPosition[] = await this.getOpenPositions(); for (const position of openPositions) { if (position.id === id) { return position; } } return undefined; } public async getOpenPositionsByAccount (tradingAccount: MidaPlaygroundAccount): Promise { return [ ...await this.getOpenPositions(), ] .filter((position: MidaPosition) => tradingAccount === position.tradingAccount) as MidaPlaygroundPosition[]; } // eslint-disable-next-line max-lines-per-function, complexity protected async tryExecuteOrder (order: MidaPlaygroundOrder): Promise { const tradingAccount: MidaPlaygroundAccount = order.tradingAccount; const executedVolume: MidaDecimal = order.requestedVolume; const symbol = order.symbol; const completeSymbol: MidaSymbol | undefined = await tradingAccount.getSymbol(symbol); if (!completeSymbol) { this.rejectOrder(order.id, MidaOrderRejection.SYMBOL_NOT_FOUND); return order; } // let positionId: string | undefined = order.positionId; let position: MidaPlaygroundPosition | undefined = undefined; if (positionId) { position = this.#positions[positionId]; if (!position || position.status !== MidaPositionStatus.OPEN) { this.rejectOrder(order.id, MidaOrderRejection.POSITION_NOT_FOUND); return order; } } // // const [ bid, ask, ] = await Promise.all([ this.getSymbolBid(symbol), this.getSymbolAsk(symbol), ]); const executionPrice: MidaDecimal = order.direction === MidaOrderDirection.SELL ? bid : ask; const executionDate: MidaDate = this.#localDate; // // const requestedProtection: MidaProtectionDirectives = order.requestedProtection ?? {}; if (order.direction === MidaOrderDirection.BUY) { if ("stopLoss" in requestedProtection && decimal(requestedProtection.stopLoss).greaterThanOrEqual(bid)) { this.rejectOrder(order.id, MidaOrderRejection.INVALID_STOP_LOSS); return order; } if ("takeProfit" in requestedProtection && decimal(requestedProtection.takeProfit).lessThanOrEqual(bid)) { this.rejectOrder(order.id, MidaOrderRejection.INVALID_TAKE_PROFIT); return order; } } else { if ("stopLoss" in requestedProtection && decimal(requestedProtection.stopLoss).lessThanOrEqual(ask)) { this.rejectOrder(order.id, MidaOrderRejection.INVALID_STOP_LOSS); return order; } if ("takeProfit" in requestedProtection && decimal(requestedProtection.takeProfit).greaterThanOrEqual(ask)) { this.rejectOrder(order.id, MidaOrderRejection.INVALID_TAKE_PROFIT); return order; } } // let grossProfit: MidaDecimal = executedVolume; let grossProfitAsset: string = completeSymbol.baseAsset; if (order.direction === MidaOrderDirection.SELL) { grossProfit = grossProfit.multiply(executionPrice); grossProfitAsset = completeSymbol.quoteAsset; } // let assetToWithdraw: string = completeSymbol.quoteAsset; let volumeToWithdraw: MidaDecimal = grossProfit.multiply(executionPrice); let assetToDeposit: string = completeSymbol.baseAsset; let volumeToDeposit: MidaDecimal = grossProfit; if (order.direction === MidaOrderDirection.SELL) { assetToWithdraw = completeSymbol.baseAsset; volumeToWithdraw = executedVolume; assetToDeposit = completeSymbol.quoteAsset; volumeToDeposit = grossProfit; } if (!await this.accountHasFunds(tradingAccount, assetToWithdraw, volumeToWithdraw)) { this.rejectOrder(order.id, MidaOrderRejection.NOT_ENOUGH_MONEY); return order; } await tradingAccount.withdraw(assetToWithdraw, volumeToWithdraw); await tradingAccount.deposit(assetToDeposit, volumeToDeposit); // const [ commissionAsset, commission, ] = await this.#commissionCustomizer?.(order, { volume: executedVolume, executionPrice, executionDate, }) ?? [ tradingAccount.primaryAsset, decimal(0), ]; await tradingAccount.withdraw(commissionAsset, commission); // // const swap: MidaDecimal = decimal(0); const swapAsset: string = tradingAccount.primaryAsset; await tradingAccount.deposit(swapAsset, swap); // if (!position) { const protection: MidaProtection = {}; if ("stopLoss" in requestedProtection) { protection.stopLoss = decimal(requestedProtection.stopLoss); } if ("takeProfit" in requestedProtection) { protection.takeProfit = decimal(requestedProtection.takeProfit); } position = new MidaPlaygroundPosition({ id: uuid(), symbol: order.symbol, volume: decimal(0), // Automatically updated after execution direction: order.direction === MidaOrderDirection.BUY ? MidaPositionDirection.LONG : MidaPositionDirection.SHORT, protection, tradingAccount: order.tradingAccount, engineEmitter: this.#protectedEmitter, }); this.#positions[position.id] = position; this.#positionsArray.push(position); } positionId = position.id; let accountPrimaryGrossProfit: MidaDecimal = decimal(0); if (position && order.purpose === MidaOrderPurpose.CLOSE) { accountPrimaryGrossProfit = executedVolume.div(position.volume).mul(await position.getUnrealizedGrossProfit()); } const trade: MidaPlaygroundTrade = new MidaPlaygroundTrade({ id: uuid(), orderId: order.id, positionId, symbol: order.symbol, volume: executedVolume, direction: order.direction === MidaOrderDirection.BUY ? MidaTradeDirection.BUY : MidaTradeDirection.SELL, status: MidaTradeStatus.EXECUTED, purpose: order.purpose === MidaOrderPurpose.OPEN ? MidaTradePurpose.OPEN : MidaTradePurpose.CLOSE, executionDate, executionPrice, grossProfit: accountPrimaryGrossProfit, commission, swap, commissionAsset, grossProfitAsset: tradingAccount.primaryAsset, swapAsset, tradingAccount, }); this.#trades[trade.id] = trade; this.#tradesArray.push(trade); this.#emitter.notifyListeners("trade", { trade, }); this.#protectedEmitter.notifyListeners("trade", { trade, }); // this.#protectedEmitter.notifyListeners("order-execute", { order, trades: [ trade, ], }); return order; } public async createAccount (configuration: MidaPlaygroundAccountConfiguration = {}): Promise { const id: string = configuration.id ?? uuid(); const account: MidaPlaygroundAccount = new MidaPlaygroundAccount({ id, ownerName: configuration.ownerName ?? "", platform: MidaPlayground.instance, primaryAsset: configuration.primaryAsset ?? "USD", engine: this, }); // const balanceSheet: Record = configuration.balanceSheet ?? {}; for (const asset of Object.keys(balanceSheet)) { if (balanceSheet.hasOwnProperty(asset)) { await account.deposit(asset, balanceSheet[asset]); } } // MidaPlayground.addTradingAccount(id, account); this.#tradingAccounts[id] = account; return account; } public on (type: string): Promise; public on (type: string, listener: MidaEventListener): string; public on (type: string, listener?: MidaEventListener): Promise | string { if (!listener) { return this.#emitter.on(type); } return this.#emitter.on(type, listener); } public removeEventListener (uuid: string): void { this.#emitter.removeEventListener(uuid); } protected notifyListeners (type: string, descriptor?: Record): void { this.#emitter.notifyListeners(type, descriptor); } async #processTick (tick: MidaTick): Promise { const symbol = tick.symbol; this.#localDate = tick.date; this.#lastTicks[symbol] = tick; // await this.addSymbolTicks(symbol, [ tick, ]); await this.#onTick(tick); } async #onTick (tick: MidaTick): Promise { // if (this.#waitFeedConfirmation) { this.#feedResolverPromise = new Promise((resolve) => { this.#feedResolver = (): void => resolve(); }); } // await this.#updatePendingOrders(tick); await this.#updateOpenPositions(tick); this.#emitter.notifyListeners("tick", { tick, }); /* for (const account of this.#tradingAccounts) { // const marginLevel: MidaDecimal | undefined = await account.getMarginLevel(); if (marginLevel?.lessThanOrEqual(account.marginCallLevel)) { this.notifyListeners("margin-call", { marginLevel, }); } // } */ // if (this.#waitFeedConfirmation) { await this.#feedResolverPromise; } // } async #processPeriod (period: MidaPeriod): Promise { await this.addSymbolPeriods(period.symbol, [ period, ]); await this.#onPeriodUpdate(period); if (period.isClosed) { await this.#onPeriodClose(period); const elapsedTicks: MidaTick[] = [ tickFromPeriod(period, "close"), ]; /* for (let i: number = 0, length: number = elapsedTicks.length; i < length; ++i) { await this.#processTick(elapsedTicks[i]); } */ } } async #onPeriodUpdate (period: MidaPeriod): Promise { // if (this.#waitFeedConfirmation) { this.#feedResolverPromise = new Promise((resolve) => { this.#feedResolver = (): void => resolve(); }); } // this.#emitter.notifyListeners("period-update", { period, }); // if (this.#waitFeedConfirmation) { await this.#feedResolverPromise; } // } async #onPeriodClose (period: MidaPeriod): Promise { // if (this.#waitFeedConfirmation) { this.#feedResolverPromise = new Promise((resolve) => { this.#feedResolver = (): void => resolve(); }); } // this.#emitter.notifyListeners("period-close", { period, }); // if (this.#waitFeedConfirmation) { await this.#feedResolverPromise; } // } async #updatePendingOrders (tick: MidaTick): Promise { const orders: MidaPlaygroundOrder[] = await this.getPendingOrders(); for (const order of orders) { await this.#updatePendingOrder(order, tick); } } async #updatePendingOrder (order: MidaPlaygroundOrder, tick: MidaTick): Promise { const bid: MidaDecimal = tick.bid; const ask: MidaDecimal = tick.ask; const limitPrice: MidaDecimal | undefined = order.limitPrice; const stopPrice: MidaDecimal | undefined = order.stopPrice; // if (limitPrice) { if ( order.direction === MidaOrderDirection.SELL && bid.greaterThanOrEqual(limitPrice) || order.direction === MidaOrderDirection.BUY && ask.lessThanOrEqual(limitPrice) ) { logger.info(`Playground | Pending Order ${order.id} hit limit`); await this.tryExecuteOrder(order); } } // // if (stopPrice) { if ( order.direction === MidaOrderDirection.SELL && bid.lessThanOrEqual(stopPrice) || order.direction === MidaOrderDirection.BUY && ask.greaterThanOrEqual(stopPrice) ) { logger.info(`Playground | Pending Order ${order.id} hit stop`); await this.tryExecuteOrder(order); } } // } async #updateOpenPositions (tick: MidaTick): Promise { const openPositions: MidaPosition[] = await this.getOpenPositions(); for (let i = 0, length = openPositions.length; i < length; ++i) { await this.#updateOpenPosition(openPositions[i], tick); } } async #updateOpenPosition (position: MidaPosition, tick: MidaTick): Promise { const tradingAccount: MidaTradingAccount = position.tradingAccount; const bid: MidaDecimal = tick.bid; const ask: MidaDecimal = tick.ask; const stopLoss: MidaDecimal | undefined = position.stopLoss; const takeProfit: MidaDecimal | undefined = position.takeProfit; // if (stopLoss) { if ( position.direction === MidaPositionDirection.SHORT && ask.greaterThanOrEqual(stopLoss) || position.direction === MidaPositionDirection.LONG && bid.lessThanOrEqual(stopLoss) ) { logger.info(`Playground | Position ${position.id} hit stop loss`); await position.close(); } } // // if (takeProfit) { if ( position.direction === MidaPositionDirection.SHORT && ask.lessThanOrEqual(takeProfit) || position.direction === MidaPositionDirection.LONG && bid.greaterThanOrEqual(takeProfit) ) { logger.info(`Playground | Position ${position.id} hit take profit`); await position.close(); } } // /* // const marginLevel: MidaDecimal | undefined = await tradingAccount.getMarginLevel(); if (marginLevel?.lessThanOrEqual(account.stopOutLevel)) { await position.close(); this.notifyListeners("stop-out", { positionId: position.id, marginLevel, }); } // */ // const equity: MidaDecimal = await tradingAccount.getEquity(); if (equity.lessThanOrEqual(0)) { await position.close(); } // } public cancelOrder (orderId: string): void { this.#protectedEmitter.notifyListeners("order-cancel", { orderId, cancelDate: this.#localDate, }); logger.warn(`Playground | Order ${orderId} canceled`); } protected rejectOrder (orderId: string, rejection: MidaOrderRejection): void { this.#protectedEmitter.notifyListeners("order-reject", { orderId, rejectionDate: this.#localDate, rejection, }); logger.warn(`Playground | Order ${orderId} rejected: ${rejection}`); } protected acceptOrder (orderId: string): void { this.#protectedEmitter.notifyListeners("order-accept", { orderId, acceptDate: this.#localDate, }); } protected moveOrderToPending (orderId: string): void { this.#protectedEmitter.notifyListeners("order-pending", { orderId, pendingDate: this.#localDate, }); } protected async accountHasFunds (tradingAccount: MidaPlaygroundAccount, asset: string, volume: MidaDecimalConvertible): Promise { const { freeVolume, } = await tradingAccount.getAssetBalance(asset); return freeVolume.greaterThanOrEqual(volume); } }