Skip to content

Commit

Permalink
feat: improve backtesting report
Browse files Browse the repository at this point in the history
  • Loading branch information
bludnic committed May 23, 2024
1 parent b557775 commit 1e98b25
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 63 deletions.
3 changes: 2 additions & 1 deletion packages/backtesting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@opentrader/exchanges": "workspace:*",
"@opentrader/logger": "workspace:*",
"@opentrader/tools": "workspace:*",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"table": "^6.8.2"
}
}
107 changes: 97 additions & 10 deletions packages/backtesting/src/backtesting-report.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,107 @@
import type { SmartTrade } from "@opentrader/bot-processor";
import { OrderStatusEnum } from "@opentrader/types";
import { table } from "table";
import type {
BotTemplate,
IBotConfiguration,
Order,
SmartTrade,
} from "@opentrader/bot-processor";
import { ICandlestick, OrderStatusEnum } from "@opentrader/types";
import { format, logger } from "@opentrader/logger";
import { buyOrder } from "./report/buyOrder";
import { buyTransaction } from "./report/buyTransaction";
import { sellOrder } from "./report/sellOrder";
import { sellTransaction } from "./report/sellTransaction";
import type { ActiveOrder, ReportResult, Transaction } from "./types";

type OrderInfo = Order & {
side: "buy" | "sell";
trade: SmartTrade;
};

export class BacktestingReport {
constructor(private smartTrades: SmartTrade[]) {}

create(): ReportResult {
return {
transactions: this.getTransactions(),
activeOrders: this.getActiveOrders(),
totalProfit: this.calcTotalProfit(),
};
constructor(
private candlesticks: ICandlestick[],
private smartTrades: SmartTrade[],
private botConfig: IBotConfiguration,
private template: BotTemplate<any>,
) {}

create(): string {
const startDate = format.datetime(this.candlesticks[0].timestamp);
const endDate = format.datetime(
this.candlesticks[this.candlesticks.length - 1].timestamp,
);

const strategyParams = JSON.stringify(this.botConfig.settings, null, 2);
const strategyName = this.template.name;

const exchange = this.botConfig.exchangeCode;
const baseCurrency = this.botConfig.baseCurrency;
const quoteCurrency = this.botConfig.quoteCurrency;
const pair = `${baseCurrency}/${quoteCurrency}`;

const backtestData: Array<any[]> = [
["Date", "Action", "Price", "Quantity", "Profit"],
];

const trades = this.getOrders().map((order) => {
return [
format.datetime(order.updatedAt),
order.side.toUpperCase(),
order.filledPrice,
order.trade.quantity,
order.side === "sell" && order.trade.sell
? order.trade.sell.filledPrice! - order.trade.buy.filledPrice!
: "-",
];
});
const tradesTable = table(backtestData.concat(trades));
const totalProfit = this.calcTotalProfit();

return `Backtesting done.
+------------------------+
| Backtesting Report |
+------------------------+
Strategy: ${strategyName}
Strategy params:
${strategyParams}
Exchange: ${exchange}
Pair: ${pair}
Start date: ${startDate}
End date: ${endDate}
Trades:
${tradesTable}
Total Trades: ${this.smartTrades.length}
Total Profit: ${totalProfit} ${quoteCurrency}
`;
}

getOrders(): Array<OrderInfo> {
const orders: Array<OrderInfo> = [];

for (const trade of this.getFinishedSmartTrades()) {
orders.push({
...trade.buy,
side: "buy",
trade,
});

if (trade.sell) {
orders.push({
...trade.sell,
side: "sell",
trade,
});
}
}

return orders.sort((a, b) => a.updatedAt - b.updatedAt);
}

getTransactions(): Transaction[] {
Expand Down
34 changes: 21 additions & 13 deletions packages/backtesting/src/backtesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
IBotConfiguration,
StrategyRunner,
BotTemplate,
BotState,
} from "@opentrader/bot-processor";
import { createStrategyRunner } from "@opentrader/bot-processor";
import type { ICandlestick } from "@opentrader/types";
Expand All @@ -18,13 +19,18 @@ export class Backtesting<T extends IBotConfiguration<T>> {
private store: MemoryStore;
private exchange: MemoryExchange;
private processor: StrategyRunner<T>;
private botState: BotState = {};
private botTemplate: BotTemplate<T>;
private botConfig: T;

constructor(options: { botConfig: T; botTemplate: BotTemplate<T> }) {
const { botConfig, botTemplate } = options;

this.marketSimulator = new MarketSimulator();
this.store = new MemoryStore(this.marketSimulator);
this.exchange = new MemoryExchange(this.marketSimulator);
this.botTemplate = botTemplate;
this.botConfig = botConfig;

this.processor = createStrategyRunner({
store: this.store,
Expand All @@ -34,7 +40,7 @@ export class Backtesting<T extends IBotConfiguration<T>> {
});
}

async run(candlesticks: ICandlestick[]): Promise<ReportResult> {
async run(candlesticks: ICandlestick[]): Promise<string> {
for (const [index, candle] of candlesticks.entries()) {
this.marketSimulator.nextCandle(candle);

Expand All @@ -46,33 +52,35 @@ export class Backtesting<T extends IBotConfiguration<T>> {

const anyOrderFulfilled = this.marketSimulator.fulfillOrders();

if (anyOrderFulfilled) {
console.log("Fulfilled Table");
console.table(fulfilledTable(this.store.getSmartTrades()));
}
// if (anyOrderFulfilled) {
// console.log("Fulfilled Table");
// console.table(fulfilledTable(this.store.getSmartTrades()));
// }

const botState = {};
if (index === 0) {
await this.processor.start(botState);
await this.processor.start(this.botState);
} else if (index === candlesticks.length - 1) {
// last candle
await this.processor.stop(botState);
await this.processor.stop(this.botState);
} else {
await this.processor.process(botState, {
await this.processor.process(this.botState, {
candle,
candles: candlesticks.slice(0, index + 1),
});
}

const anyOrderPlaced = this.marketSimulator.placeOrders();
if (anyOrderPlaced) {
console.log("Placed Table");
console.table(gridTable(this.store.getSmartTrades()));
}
// if (anyOrderPlaced) {
// console.log("Placed Table");
// console.table(gridTable(this.store.getSmartTrades()));
// }
}

const report = new BacktestingReport(
candlesticks,
this.marketSimulator.smartTrades,
this.botConfig,
this.botTemplate,
).create();

return report;
Expand Down
29 changes: 19 additions & 10 deletions packages/backtesting/src/market-simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from "@opentrader/bot-processor";
import type { ICandlestick } from "@opentrader/types";
import { OrderStatusEnum } from "@opentrader/types";
import { format, logger } from "@opentrader/logger";

export class MarketSimulator {
/**
Expand Down Expand Up @@ -42,6 +43,16 @@ export class MarketSimulator {
this.smartTrades.push(smartTrade);
}

editSmartTrade(newSmartTrade: SmartTrade, ref: string) {
this.smartTrades = this.smartTrades.map((smartTrade) => {
if (smartTrade.ref === ref) {
return newSmartTrade;
}

return smartTrade;
});
}

/**
* Changes the order status from: `idle -> placed`
* Return `true` if any order was placed
Expand Down Expand Up @@ -113,8 +124,8 @@ export class MarketSimulator {
};
smartTrade.buy = filledOrder;

console.log(
`[TestingDb] ST# ${smartTrade.id} buy marked order filled with ${filledOrder.filledPrice}, updated at ${updatedAt}`,
logger.info(
`[MarketSimulator] Market BUY order was filled at ${filledOrder.filledPrice} on ${format.datetime(updatedAt)}`,
);

return true;
Expand All @@ -128,10 +139,9 @@ export class MarketSimulator {
};
smartTrade.buy = filledOrder;

console.log(
`[TestingDb] ST# ${smartTrade.id} buy order filled, updated at ${updatedAt}`,
logger.info(
`[MarketSimulator] Limit BUY order was filled at ${filledOrder.filledPrice} on ${format.datetime(updatedAt)}`,
);
console.log(smartTrade);
return true;
}
}
Expand All @@ -147,8 +157,8 @@ export class MarketSimulator {
};
smartTrade.sell = filledOrder;

console.log(
`[TestingDb] ST# ${smartTrade.id} sell marked order filled with ${filledOrder.filledPrice}, updated at ${updatedAt}`,
logger.info(
`[MarketSimulator] Market SELL order was filled at ${filledOrder.filledPrice} on ${format.datetime(updatedAt)}`,
);
return true;
} else if (smartTrade.sell.type === "Limit") {
Expand All @@ -162,10 +172,9 @@ export class MarketSimulator {

smartTrade.sell = filledOrder;

console.log(
`[TestingDb] ST# ${smartTrade.id} sell order filled, updated at ${updatedAt}`,
logger.info(
`[MarketSimulator] Limit SELL order was filled at ${filledOrder.filledPrice} on ${format.datetime(updatedAt)}`,
);
console.log(smartTrade);
return true;
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/backtesting/src/store/memory-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,14 @@ export class MemoryStore implements IStore {
}
}

return {
const newSmartTrade: SmartTrade = {
...smartTrade,
sell: order,
};

this.marketSimulator.editSmartTrade(newSmartTrade, ref);

return newSmartTrade;
}

async cancelSmartTrade(_ref: string, _botId: number): Promise<boolean> {
Expand Down
6 changes: 5 additions & 1 deletion packages/bot-templates/src/templates/grid-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
} from "@opentrader/bot-processor";
import { computeGridLevelsFromCurrentAssetPrice } from "@opentrader/tools";
import type { IGetMarketPriceResponse } from "@opentrader/types";
import { logger } from "@opentrader/logger";

export function* gridBot(ctx: TBotContext<GridBotConfig>) {
const { config: bot, onStart, onStop } = ctx;
const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`;

const exchange: IExchange = yield useExchange();

Expand All @@ -25,7 +27,9 @@ export function* gridBot(ctx: TBotContext<GridBotConfig>) {
symbol: `${bot.baseCurrency}/${bot.quoteCurrency}`,
});
price = markPrice;
console.log(`[GridBotTemple] Bot started [markPrice: ${price}]`);
logger.info(
`[Grid] Bot strategy started on ${symbol} pair. Current price is ${price} ${bot.quoteCurrency}`,
);
}

const gridLevels = computeGridLevelsFromCurrentAssetPrice(
Expand Down
21 changes: 7 additions & 14 deletions packages/bot/src/market-data/ccxt-candles.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
import { type Exchange, type OHLCV } from "ccxt";
import type { BarSize, ICandlestick } from "@opentrader/types";
import type { ICandlesProvider } from "./candles-provider.interface";
import { logger, format } from "@opentrader/logger";

export class CCXTCandlesProvider
extends EventEmitter
Expand Down Expand Up @@ -76,11 +77,8 @@ export class CCXTCandlesProvider
this.since = startDate.getTime();

this.on("start", () => {
console.log(
`CandleProvider: Start fetching ${this.symbol} candles from`,
this.startDate,
"to",
this.endDate,
logger.info(
`Start fetching ${this.symbol} candles from ${format.datetime(startDate)} to ${format.datetime(endDate)}`,
);
void this.start();
});
Expand All @@ -102,20 +100,15 @@ export class CCXTCandlesProvider
});

if (filteredCandles.length === 0) {
console.log("No more candles");
logger.debug("No more candles");
this.emit("done");
return;
}

const firstCandle = filteredCandles[0];
const lastCandle = filteredCandles[filteredCandles.length - 1];
console.log(
"Fetched",
filteredCandles.length,
"candles from",
new Date(firstCandle.timestamp).toISOString(),
"to",
new Date(lastCandle.timestamp).toISOString(),
logger.info(
`Fetched ${filteredCandles.length} candles from ${format.datetime(firstCandle.timestamp)} to ${format.datetime(lastCandle.timestamp)}`,
);

filteredCandles.forEach((candle) => {
Expand All @@ -125,7 +118,7 @@ export class CCXTCandlesProvider
this.since = lastCandle.timestamp + 60000;

if (this.since >= this.endDate.getTime()) {
console.log("Reached the end");
logger.debug("Reached the end");
this.emit("done");
return;
}
Expand Down
Loading

0 comments on commit 1e98b25

Please sign in to comment.