Skip to content

Commit

Permalink
Merge pull request #55 from bludnic/feat/trades
Browse files Browse the repository at this point in the history
New bot context for cross exchange trading
  • Loading branch information
bludnic authored Aug 19, 2024
2 parents 1237ed5 + 1e65022 commit f9e5a8d
Show file tree
Hide file tree
Showing 83 changed files with 2,403 additions and 878 deletions.
15 changes: 3 additions & 12 deletions packages/backtesting/src/backtesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@
*
* Repository URL: https://github.com/bludnic/opentrader
*/
import type {
IBotConfiguration,
StrategyRunner,
BotTemplate,
BotState,
} from "@opentrader/bot-processor";
import type { IBotConfiguration, StrategyRunner, BotTemplate, BotState } from "@opentrader/bot-processor";
import { createStrategyRunner } from "@opentrader/bot-processor";
import type { ICandlestick } from "@opentrader/types";
import { logger, format } from "@opentrader/logger";
Expand Down Expand Up @@ -59,11 +54,7 @@ export class Backtesting<T extends IBotConfiguration<T>> {
for (const [index, candle] of candlesticks.entries()) {
this.marketSimulator.nextCandle(candle);

logger.info(
`Process candle ${format.candletime(candle.timestamp)}: ${format.candle(
candle,
)}`,
);
logger.info(`Process candle ${format.candletime(candle.timestamp)}: ${format.candle(candle)}`);

// const anyOrderFulfilled = this.marketSimulator.fulfillOrders();

Expand All @@ -78,7 +69,7 @@ export class Backtesting<T extends IBotConfiguration<T>> {
// last candle
await this.processor.stop(this.botState);
} else {
await this.processor.process(this.botState, {
await this.processor.process(this.botState, undefined, {
candle,
candles: candlesticks.slice(0, index + 1),
});
Expand Down
66 changes: 30 additions & 36 deletions packages/backtesting/src/exchange/memory-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,25 @@ import type {
IWatchCandlesResponse,
IPlaceMarketOrderRequest,
IPlaceMarketOrderResponse,
ITrade,
IOrderbook,
ITicker,
} from "@opentrader/types";
import { ExchangeCode } from "@opentrader/types";
import type { MarketSimulator } from "../market-simulator.js";

export class MemoryExchange implements IExchange {
ccxt = {} as any;
exchangeCode = ExchangeCode.OKX;
isPaper = false;

/**
* @internal
*/
constructor(private marketSimulator: MarketSimulator) {}

async destroy() {}

async loadMarkets() {
return {};
}
Expand All @@ -44,9 +50,7 @@ export class MemoryExchange implements IExchange {
return [];
}

async getLimitOrder(
_body: IGetLimitOrderRequest,
): Promise<IGetLimitOrderResponse> {
async getLimitOrder(_body: IGetLimitOrderRequest): Promise<IGetLimitOrderResponse> {
return {
exchangeOrderId: "",
clientOrderId: "",
Expand All @@ -61,44 +65,34 @@ export class MemoryExchange implements IExchange {
};
}

async placeLimitOrder(
_body: IPlaceLimitOrderRequest,
): Promise<IPlaceLimitOrderResponse> {
async placeLimitOrder(_body: IPlaceLimitOrderRequest): Promise<IPlaceLimitOrderResponse> {
return {
orderId: "",
clientOrderId: "",
};
}

async placeMarketOrder(
_body: IPlaceMarketOrderRequest,
): Promise<IPlaceMarketOrderResponse> {
async placeMarketOrder(_body: IPlaceMarketOrderRequest): Promise<IPlaceMarketOrderResponse> {
return {
orderId: "",
clientOrderId: "",
};
}

async placeStopOrder(
_body: IPlaceStopOrderRequest,
): Promise<IPlaceStopOrderResponse> {
async placeStopOrder(_body: IPlaceStopOrderRequest): Promise<IPlaceStopOrderResponse> {
return {
orderId: "",
clientOrderId: "",
};
}

async cancelLimitOrder(
_body: ICancelLimitOrderRequest,
): Promise<ICancelLimitOrderResponse> {
async cancelLimitOrder(_body: ICancelLimitOrderRequest): Promise<ICancelLimitOrderResponse> {
return {
orderId: "",
};
}

async getMarketPrice(
params: IGetMarketPriceRequest,
): Promise<IGetMarketPriceResponse> {
async getMarketPrice(params: IGetMarketPriceRequest): Promise<IGetMarketPriceResponse> {
const candlestick = this.marketSimulator.currentCandle;
const assetPrice = candlestick.close;
const { symbol } = params;
Expand All @@ -110,15 +104,11 @@ export class MemoryExchange implements IExchange {
};
}

async getCandlesticks(
_params: IGetCandlesticksRequest,
): Promise<ICandlestick[]> {
async getCandlesticks(_params: IGetCandlesticksRequest): Promise<ICandlestick[]> {
return [];
}

async getTradingFeeRates(
_params: IGetTradingFeeRatesRequest,
): Promise<IGetTradingFeeRatesResponse> {
async getTradingFeeRates(_params: IGetTradingFeeRatesRequest): Promise<IGetTradingFeeRatesResponse> {
return {
makerFee: 0,
takerFee: 0,
Expand Down Expand Up @@ -176,19 +166,23 @@ export class MemoryExchange implements IExchange {
return [];
}

async watchOrders(
_params?: IWatchOrdersRequest,
): Promise<IWatchOrdersResponse> {
throw new Error(
"Not implemented. Backtesting doesn't require this method.",
);
async watchOrders(_params?: IWatchOrdersRequest): Promise<IWatchOrdersResponse> {
throw new Error("Not implemented. Backtesting doesn't require this method.");
}

async watchCandles(_params?: IWatchCandlesRequest): Promise<IWatchCandlesResponse> {
throw new Error("Not implemented. Backtesting doesn't require this method.");
}

async watchTrades(): Promise<ITrade[]> {
throw new Error("Not implemented. Backtesting doesn't require this method.");
}

async watchOrderbook(): Promise<IOrderbook> {
throw new Error("Not implemented. Backtesting doesn't require this method.");
}

async watchCandles(
_params?: IWatchCandlesRequest,
): Promise<IWatchCandlesResponse> {
throw new Error(
"Not implemented. Backtesting doesn't require this method.",
);
async watchTicker(): Promise<ITicker> {
throw new Error("Not implemented. Backtesting doesn't require this method.");
}
}
20 changes: 17 additions & 3 deletions packages/bot-processor/src/strategy-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* Repository URL: https://github.com/bludnic/opentrader
*/
import type { IExchange } from "@opentrader/exchanges";
import type { MarketData } from "@opentrader/types";
import type { MarketData, MarketId, MarketEventType } from "@opentrader/types";
import { BotControl } from "./bot-control.js";
import { effectRunnerMap } from "./effect-runner.js";
import { isEffect } from "./effects/index.js";
Expand All @@ -43,8 +43,22 @@ export class StrategyRunner<T extends IBotConfiguration> {
await this.runTemplate(context);
}

async process(state: BotState, market?: MarketData) {
const context = createContext(this.control, this.botConfig, this.exchange, "process", state, market);
async process(
state: BotState,
event?: MarketEventType,
market?: MarketData,
markets: Record<MarketId, MarketData> = {},
) {
const context = createContext(
this.control,
this.botConfig,
this.exchange,
"process",
state,
market,
markets,
event,
);

await this.runTemplate(context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ExchangeCode } from "@opentrader/types";
import type { BarSize, ExchangeCode } from '@opentrader/types';

export type IBotConfiguration<T = any> = {
id: number;
baseCurrency: string;
quoteCurrency: string;
exchangeCode: ExchangeCode;
settings: T;
timeframe?: BarSize | null;
};
9 changes: 7 additions & 2 deletions packages/bot-processor/src/types/bot/bot-context.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IExchange } from "@opentrader/exchanges";
import type { MarketData } from "@opentrader/types";
import type { MarketData, MarketId, MarketEventType } from "@opentrader/types";
import type { IBotControl } from "./bot-control.interface.js";
import type { IBotConfiguration } from "./bot-configuration.interface.js";
import type { BotState } from "./bot.state.js";
Expand All @@ -25,11 +25,16 @@ export type TBotContext<T extends IBotConfiguration, S extends BotState = BotSta
* Event
*/
command: "start" | "stop" | "process";
event?: MarketEventType;
onStart: boolean;
onStop: boolean;
onProcess: boolean;
/**
* Marked data
* Default market from `bot.symbol`
*/
market: MarketData;
/**
* Additional markets
*/
markets: Record<MarketId, MarketData>;
};
69 changes: 66 additions & 3 deletions packages/bot-processor/src/types/bot/bot-template.type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import type { ZodObject } from "zod";
import { BarSize, MarketEventType } from "@opentrader/types";
import type { TBotContext } from "./bot-context.type.js";
import type { IBotConfiguration } from "./bot-configuration.interface.js";

export type WatchCondition<T extends IBotConfiguration> = string | string[] | ((botConfig: T) => string | string[]);

// @todo move to types
export const Watcher = {
watchTrades: "watchTrades",
watchOrderbook: "watchOrderbook",
watchTicker: "watchTicker",
watchCandles: "watchCandles",
} as const;
export type Watcher = (typeof Watcher)[keyof typeof Watcher];

export interface BotTemplate<T extends IBotConfiguration> {
(ctx: TBotContext<T>): Generator<unknown, unknown>;
/**
Expand All @@ -13,6 +25,11 @@ export interface BotTemplate<T extends IBotConfiguration> {
* When the bot starts, it will download the required number of candles.
*/
requiredHistory?: number;
/**
* Used to aggregate 1m candles to a higher timeframe, when using candles watcher.
* If not provided, the timeframe from the bot config will be used.
*/
timeframe?: BarSize | null | ((botConfig: T) => BarSize | null | undefined);
/**
* Strategy params schema.
*/
Expand All @@ -23,12 +40,58 @@ export interface BotTemplate<T extends IBotConfiguration> {
*/
hidden?: boolean;
/**
* Run policy for the bot.
* List of pairs to watch for trades.
*
* @example Watch trades on BTC/USDT pair. The default exchange from bot config will be used.
* ```ts
* strategy.watchers = {
* watchTrades: "BTC/USDT",
* }
* ```
*
* @example Watch trades on specific exchange.
* ```ts
* strategy.watchers = {
* watchTrades: "OKX:BTC/USDT"
* }
* ```
*
* @example Watch trades on multiple pairs.
* ```ts
* strategy.watchers = {
* watchTrades: ["BTC/USDT", "ETH/USDT"],
* }
* ```
*
* @example Watch trades on different exchanges.
* ```ts
* strategy.watchers = {
* watchTrades: ["OKX:BTC/USDT", "BINANCE:BTC/USDT"]
* }
* ```
*
* @example Watch trades on a computed pairs list.
* ```ts
* strategy.watchers = {
* watchTrades: (botConfig) => botConfig.symbol
* }
* ```
*/
watchers?: {
[Watcher.watchTrades]?: WatchCondition<T>;
[Watcher.watchOrderbook]?: WatchCondition<T>;
[Watcher.watchTicker]?: WatchCondition<T>;
[Watcher.watchCandles]?: WatchCondition<T>;
};
runPolicy?: {
/**
* List of pairs to watch for trades.
* The size of the candle is determined by `timeframe` property above.
* If not provided, the channel will listen to 1m candles.
*/
watchTrades?: string | string[] | ((botConfig: T) => string | string[]);
[MarketEventType.onCandleClosed]?: boolean | ((botConfig: T) => boolean);
[MarketEventType.onPublicTrade]?: boolean | ((botConfig: T) => boolean);
[MarketEventType.onOrderbookChange]?: boolean | ((botConfig: T) => boolean);
[MarketEventType.onTickerChange]?: boolean | ((botConfig: T) => boolean);
[MarketEventType.onOrderFilled]?: boolean | ((botConfig: T) => boolean);
};
}
6 changes: 5 additions & 1 deletion packages/bot-processor/src/utils/createContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IExchange } from "@opentrader/exchanges";
import type { MarketData } from "@opentrader/types";
import type { MarketData, MarketId, MarketEventType } from "@opentrader/types";
import type { BotState, IBotConfiguration, IBotControl, TBotContext } from "../types/index.js";

export function createContext<T extends IBotConfiguration>(
Expand All @@ -11,6 +11,8 @@ export function createContext<T extends IBotConfiguration>(
market: MarketData = {
candles: [],
},
markets: Record<MarketId, MarketData> = {},
event?: MarketEventType,
): TBotContext<T> {
return {
control,
Expand All @@ -22,5 +24,7 @@ export function createContext<T extends IBotConfiguration>(
onProcess: command === "process",
state,
market,
markets,
event,
};
}
2 changes: 2 additions & 0 deletions packages/bot-store/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
Loading

0 comments on commit f9e5a8d

Please sign in to comment.