From 4f50386f58c354d4ebb906413540b3a34839d1aa Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 12 Aug 2024 23:23:07 +0100 Subject: [PATCH 01/16] feat: trades consumer draft --- .../bot-templates/src/templates/test/index.ts | 2 + .../bot-templates/src/templates/test/okx.ts | 30 +++ .../src/templates/test/trades.ts | 15 ++ packages/bot/src/consumers/trades.consumer.ts | 181 ++++++++++++++++++ packages/bot/src/platform.ts | 19 +- 5 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 packages/bot-templates/src/templates/test/okx.ts create mode 100644 packages/bot-templates/src/templates/test/trades.ts create mode 100644 packages/bot/src/consumers/trades.consumer.ts diff --git a/packages/bot-templates/src/templates/test/index.ts b/packages/bot-templates/src/templates/test/index.ts index 328d98a0..91f02620 100644 --- a/packages/bot-templates/src/templates/test/index.ts +++ b/packages/bot-templates/src/templates/test/index.ts @@ -4,3 +4,5 @@ export * from "./debug.js"; export * from "./candle.js"; export * from "./rsi.js"; export * from "./state.js"; +export * from "./okx.js"; +export * from "./trades.js"; diff --git a/packages/bot-templates/src/templates/test/okx.ts b/packages/bot-templates/src/templates/test/okx.ts new file mode 100644 index 00000000..ab4dcbff --- /dev/null +++ b/packages/bot-templates/src/templates/test/okx.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { logger } from "@opentrader/logger"; +import { buy, sell, TBotContext, useExchange } from "@opentrader/bot-processor"; +import { IExchange } from "@opentrader/exchanges"; +import { IGetMarketPriceResponse } from "@opentrader/types"; + +export function* okx(ctx: TBotContext) { + logger.info("[OKX STRATEGY]: Strategy exec"); + + const exchange: IExchange = yield useExchange(); + + const { price }: IGetMarketPriceResponse = yield exchange.getMarketPrice({ symbol: "BTC/USDT" }); + logger.info(`[OKX STRATEGY]: Market price: ${price}`); + + const result = yield sell({ + pair: "BTC/USDT", + quantity: 0.0001, + price: 10000, // much lower than current price + orderType: "Limit", + }); + + console.log("[OKX STRATEGY]: Sell result", result); +} + +okx.displayName = "OKX Strategy"; +okx.hidden = true; +okx.schema = z.object({}); +okx.runPolicy = { + watchTrades: "BTC/USDT", +}; diff --git a/packages/bot-templates/src/templates/test/trades.ts b/packages/bot-templates/src/templates/test/trades.ts new file mode 100644 index 00000000..7e518089 --- /dev/null +++ b/packages/bot-templates/src/templates/test/trades.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { logger } from "@opentrader/logger"; +import { buy, sell, TBotContext, useExchange } from "@opentrader/bot-processor"; + +export function* testTrades(ctx: TBotContext) { + logger.info("[TRADES]: Strategy exec"); + console.log("[TRADE]", ctx.market?.trade); +} + +testTrades.displayName = "Trades Strategy"; +testTrades.hidden = true; +testTrades.schema = z.object({}); +testTrades.runPolicy = { + watchTrades: ["BTC/USDT", "ETH/USDT"], +}; diff --git a/packages/bot/src/consumers/trades.consumer.ts b/packages/bot/src/consumers/trades.consumer.ts new file mode 100644 index 00000000..2d1dfddb --- /dev/null +++ b/packages/bot/src/consumers/trades.consumer.ts @@ -0,0 +1,181 @@ +import { exchangeProvider } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; +import type { TBot } from "@opentrader/db"; +import { xprisma } from "@opentrader/db"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import type { TradeEvent } from "../channels/index.js"; +import { TradesChannel } from "../channels/index.js"; +import { processingQueue } from "../queue/index.js"; +import { BotTemplate } from "../../../bot-processor/src/index.js"; + +function getSymbolsToWatch(runPolicy: BotTemplate["runPolicy"], bot: TBot): string[] { + if (!runPolicy?.watchTrades) { + return []; + } + + const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; + + let symbols: string[] | string = []; + + if (typeof runPolicy.watchTrades === "string" || Array.isArray(runPolicy.watchTrades)) { + symbols = runPolicy.watchTrades; + } else if (typeof runPolicy.watchTrades === "function") { + symbols = runPolicy.watchTrades(bot); + } + + if (Array.isArray(symbols)) { + return symbols; + } else { + return [symbols]; + } +} + +export class TradesConsumer { + private channels: TradesChannel[] = []; + private bots: TBot[] = []; + + constructor(bots: TBot[]) { + this.bots = bots; + } + + async create() { + logger.info(`[TradesConsumer] Creating trades channel for ${this.bots.length} bots`); + + for (const bot of this.bots) { + await this.addBot(bot); + } + } + + /** + * Subscribes the bot to the trades channel. + * It will create the channel if necessary or reusing it if it already exists. + * @param bot Bot to add + * @returns + */ + async addBot(bot: TBot) { + const exchangeAccount = await xprisma.exchangeAccount.findUniqueOrThrow({ + where: { + id: bot.exchangeAccountId, + }, + }); + const exchange = exchangeProvider.fromAccount(exchangeAccount); + const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; + + const { strategyFn } = await findStrategy(bot.template); + if (!isWatchingSymbol(strategyFn.runPolicy, bot)) { + logger.warn( + `[TradesConsumer]: Skip adding bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel. Reason: No runPolicty for watchTrades.`, + ); + return; + } + + let channel = this.channels.find((channel) => channel.exchangeCode === exchange.exchangeCode); + if (!channel) { + channel = new TradesChannel(exchange); + this.channels.push(channel); + + logger.info(`[TradesConsumer] Created ${exchange.exchangeCode}:${symbol} channel`); + + // @todo type + channel.on("trade", this.handleTrade); + } + + await channel.add(symbol); + logger.info( + `[TradesConsumer]: Subscribed bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel`, + ); + } + + /** + * Remove unused channels that are no longer used by any bots. + * Triggered when any bot was stopped. + */ + async cleanStaleChannels() { + const bots = await xprisma.bot.findMany({ + where: { + timeframe: { not: null }, + enabled: true, + }, + include: { + exchangeAccount: true, + }, + }); + + for (const channel of this.channels) { + // Clean stale channels + const isChannelUsedByAnyBot = bots.some((bot) => bot.exchangeAccount.exchangeCode === channel.exchangeCode); + if (!isChannelUsedByAnyBot) { + logger.info(`[TradesConsumer] Removing stale channel ${channel.exchangeCode}`); + this.removeChannel(channel); + continue; // no need to check watchers + } + + // Clean up stale watchers + for (const watcher of channel.getWatchers()) { + const isWatcherUsedByAnyBot = bots.some( + (bot) => + bot.exchangeAccount.exchangeCode === channel.exchangeCode && + `${bot.baseCurrency}/${bot.quoteCurrency}` === watcher.symbol, + ); + + if (!isWatcherUsedByAnyBot) { + logger.info(`[TradesConsumer] Removing stale watcher ${channel.exchangeCode}:${watcher.symbol}`); + channel.removeWatcher(watcher); + } + } + } + } + + private handleTrade = async (data: TradeEvent) => { + const { trade, symbol } = data; + + logger.info(`[TradesConsumer] New trade: ${trade.side} ${trade.amount} of ${symbol}. Start processing.`); + + const enabledBots = await xprisma.bot.custom.findMany({ + where: { + enabled: true, + }, + }); + const targetBots: TBot[] = []; + + for (const bot of enabledBots) { + const { strategyFn } = await findStrategy(bot.template); + + if (getSymbolsToWatch(strategyFn.runPolicy, bot)) { + targetBots.push(bot); + } + } + + logger.info(`[TradesConsumer]: Targeted ${targetBots.length} bots`); + + for (const bot of targetBots) { + if (!bot.enabled) { + logger.warn("❗ Cannot run bot process when the bot is disabled"); + continue; + } + + processingQueue.push({ + type: "onPublicTrade", + bot, + trade, + }); + } + }; + + /** + * Destroy and remove the channel from the list. + * @param exchangeCode + */ + private removeChannel(channel: TradesChannel) { + channel.off("trade", this.handleTrade); + channel.destroy(); + + this.channels = this.channels.filter((c) => c !== channel); + } + + destroy() { + for (const channel of this.channels) { + channel.destroy(); + } + } +} diff --git a/packages/bot/src/platform.ts b/packages/bot/src/platform.ts index 07a30792..b1ef957a 100644 --- a/packages/bot/src/platform.ts +++ b/packages/bot/src/platform.ts @@ -5,17 +5,20 @@ import { BotProcessing } from "@opentrader/processing"; import { eventBus } from "@opentrader/event-bus"; import { CandlesConsumer } from "./consumers/candles.consumer.js"; +import { TradesConsumer } from "./consumers/trades.consumer.js"; import { OrdersConsumer } from "./consumers/orders.consumer.js"; export class Platform { private ordersConsumer: OrdersConsumer; private candlesConsumer: CandlesConsumer; + private tradesConsumer: TradesConsumer; private unsubscribeFromEventBus = () => {}; constructor(exchangeAccounts: ExchangeAccountWithCredentials[], bots: TBot[]) { this.ordersConsumer = new OrdersConsumer(exchangeAccounts); this.candlesConsumer = new CandlesConsumer(bots); + this.tradesConsumer = new TradesConsumer(bots); } async bootstrap() { @@ -30,6 +33,9 @@ export class Platform { logger.info("[Processor] CandlesProcessor created"); await this.candlesConsumer.create(); + logger.info("[Processor] TradesProcessor created"); + await this.tradesConsumer.create(); + this.unsubscribeFromEventBus = this.subscribeToEventBus(); } @@ -45,6 +51,9 @@ export class Platform { logger.info("[Processor] CandlesProcessor destroyed"); this.candlesConsumer.destroy(); + logger.info("[Processor] TradesProcessor destroyed"); + this.tradesConsumer.destroy(); + this.unsubscribeFromEventBus(); } @@ -99,8 +108,14 @@ export class Platform { * - When an exchange account was updated → Resubcribe to orders channel with new credentials */ private subscribeToEventBus() { - const onBotStarted = async (bot: TBot) => await this.candlesConsumer.addBot(bot); - const onBotStopped = async (bot: TBot) => await this.candlesConsumer.cleanStaleChannels(); + const onBotStarted = async (bot: TBot) => { + await this.candlesConsumer.addBot(bot); + await this.tradesConsumer.addBot(bot); + }; + const onBotStopped = async (bot: TBot) => { + await this.candlesConsumer.cleanStaleChannels(); + await this.tradesConsumer.cleanStaleChannels(); + }; const addExchangeAccount = async (exchangeAccount: ExchangeAccountWithCredentials) => await this.ordersConsumer.addExchangeAccount(exchangeAccount); From 89ced64d3fe6ff074bba1e4a0792b18eb2d9a336 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 15 Aug 2024 17:07:08 +0100 Subject: [PATCH 02/16] feat(exchanges): add `watchOrderbook` and `watchTicker` methods --- .../exchanges/src/exchanges/ccxt/exchange.ts | 16 +++++++++ .../exchanges/src/exchanges/ccxt/normalize.ts | 33 +++++++++++++++++++ .../exchanges/src/types/exchange.interface.ts | 6 +++- .../src/types/normalize.interface.ts | 14 +++++++- packages/types/src/exchange/index.ts | 2 ++ .../src/exchange/market-data/get-orderbook.ts | 22 +++++++++++++ .../src/exchange/market-data/get-ticker.ts | 16 +++++++++ 7 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/exchange/market-data/get-orderbook.ts create mode 100644 packages/types/src/exchange/market-data/get-ticker.ts diff --git a/packages/exchanges/src/exchanges/ccxt/exchange.ts b/packages/exchanges/src/exchanges/ccxt/exchange.ts index 1f9bd63a..66c49067 100644 --- a/packages/exchanges/src/exchanges/ccxt/exchange.ts +++ b/packages/exchanges/src/exchanges/ccxt/exchange.ts @@ -43,6 +43,8 @@ import type { ExchangeCode, IWatchTradesRequest, IWatchTradesResponse, + IOrderbook, + ITicker, } from "@opentrader/types"; import { pro } from "ccxt"; import type { Dictionary, Market, Exchange } from "ccxt"; @@ -219,4 +221,18 @@ export class CCXTExchange implements IExchange { return normalize.watchTrades.response(data); } + + async watchOrderbook(symbol: string): Promise { + const args = normalize.watchOrderbook.request(symbol); + const data = await this.ccxt.watchOrderBook(...args); + + return normalize.watchOrderbook.response(data); + } + + async watchTicker(symbol: string): Promise { + const args = normalize.watchTicker.request(symbol); + const data = await this.ccxt.watchTicker(...args); + + return normalize.watchTicker.response(data); + } } diff --git a/packages/exchanges/src/exchanges/ccxt/normalize.ts b/packages/exchanges/src/exchanges/ccxt/normalize.ts index 0a273077..67471559 100644 --- a/packages/exchanges/src/exchanges/ccxt/normalize.ts +++ b/packages/exchanges/src/exchanges/ccxt/normalize.ts @@ -198,6 +198,37 @@ const watchTrades: Normalize["watchTrades"] = { })), }; +const watchOrderbook: Normalize["watchOrderbook"] = { + request: (symbol) => [symbol], + response: (orderbook) => ({ + symbol: orderbook.symbol!, + timestamp: orderbook.timestamp!, + + bids: orderbook.bids.map(([price, quantity]) => ({ price: price!, quantity: quantity! })), + asks: orderbook.asks.map(([price, quantity]) => ({ price: price!, quantity: quantity! })), + }), +}; + +const watchTicker: Normalize["watchTicker"] = { + request: (symbol) => [symbol], + response: (ticker) => ({ + symbol: ticker.symbol!, + timestamp: ticker.timestamp!, + + bid: ticker.bid!, + ask: ticker.ask!, + last: ticker.last!, + + open: ticker.open, + high: ticker.high, + low: ticker.low, + close: ticker.close, + + baseVolume: ticker.baseVolume!, + quoteVolume: ticker.quoteVolume!, + }), +}; + export const normalize: Normalize = { accountAssets, getLimitOrder, @@ -214,4 +245,6 @@ export const normalize: Normalize = { watchOrders, watchCandles, watchTrades, + watchOrderbook, + watchTicker, }; diff --git a/packages/exchanges/src/types/exchange.interface.ts b/packages/exchanges/src/types/exchange.interface.ts index 04c57556..2fbbdf95 100644 --- a/packages/exchanges/src/types/exchange.interface.ts +++ b/packages/exchanges/src/types/exchange.interface.ts @@ -1,4 +1,4 @@ -import type { +import { IAccountAsset, IGetCandlesticksRequest, ICandlestick, @@ -26,6 +26,8 @@ import type { IPlaceMarketOrderResponse, IWatchTradesRequest, IWatchTradesResponse, + IOrderbook, + ITicker, } from "@opentrader/types"; import type { Dictionary, Market, Exchange } from "ccxt"; @@ -54,4 +56,6 @@ export interface IExchange { watchOrders: (params?: IWatchOrdersRequest) => Promise; watchCandles: (symbol: IWatchCandlesRequest) => Promise; watchTrades: (symbol: IWatchTradesRequest) => Promise; + watchOrderbook: (symbol: string) => Promise; + watchTicker: (symbol: string) => Promise; } diff --git a/packages/exchanges/src/types/normalize.interface.ts b/packages/exchanges/src/types/normalize.interface.ts index a08312f5..ecf4ec23 100644 --- a/packages/exchanges/src/types/normalize.interface.ts +++ b/packages/exchanges/src/types/normalize.interface.ts @@ -27,8 +27,10 @@ import type { ExchangeCode, IWatchTradesRequest, IWatchTradesResponse, + IOrderbook, + ITicker, } from "@opentrader/types"; -import type { Balances, Exchange, Order, Dictionary, Market, OHLCV, Ticker, Trade } from "ccxt"; +import type { Balances, Exchange, Order, Dictionary, Market, OHLCV, Ticker, Trade, OrderBook } from "ccxt"; export type Normalize = { accountAssets: { @@ -107,4 +109,14 @@ export type Normalize = { request: (params: IWatchTradesRequest) => Parameters; response: (data: Trade[]) => IWatchTradesResponse; }; + + watchOrderbook: { + request: (symbol: string) => Parameters; + response: (data: OrderBook) => IOrderbook; + }; + + watchTicker: { + request: (symbol: string) => Parameters; + response: (data: Ticker) => ITicker; + }; }; diff --git a/packages/types/src/exchange/index.ts b/packages/types/src/exchange/index.ts index 7849b5b7..aa350a59 100644 --- a/packages/types/src/exchange/index.ts +++ b/packages/types/src/exchange/index.ts @@ -2,6 +2,8 @@ export * from "./account/common.js"; export * from "./account/get-trading-fee-rates.js"; export * from "./market-data/get-candlesticks.js"; export * from "./market-data/get-trades.js"; +export * from "./market-data/get-orderbook.js"; +export * from "./market-data/get-ticker.js"; export * from "./public-data/get-market-price.js"; export * from "./public-data/get-symbols-info.js"; export * from "./trade/common/enums.js"; diff --git a/packages/types/src/exchange/market-data/get-orderbook.ts b/packages/types/src/exchange/market-data/get-orderbook.ts new file mode 100644 index 00000000..868a6e73 --- /dev/null +++ b/packages/types/src/exchange/market-data/get-orderbook.ts @@ -0,0 +1,22 @@ +export type IBid = { + price: number; + quantity: number; +}; + +export type IAsk = { + price: number; + quantity: number; +}; + +export type IOrderbook = { + asks: IAsk[]; + bids: IBid[]; + /** + * Timestamp of the orderbook in milliseconds + */ + timestamp: number; + /** + * Marget symbol as BTC/USDT + */ + symbol: string; +}; diff --git a/packages/types/src/exchange/market-data/get-ticker.ts b/packages/types/src/exchange/market-data/get-ticker.ts new file mode 100644 index 00000000..3a3f5868 --- /dev/null +++ b/packages/types/src/exchange/market-data/get-ticker.ts @@ -0,0 +1,16 @@ +export type ITicker = { + symbol: string; + timestamp: number; + + bid: number; + ask: number; + last: number; + + open?: number; + high?: number; + low?: number; + close?: number; + + baseVolume: number; + quoteVolume: number; +}; From 712db2f30084745ba74cd74c1347c5f80e270322 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 15 Aug 2024 17:43:39 +0100 Subject: [PATCH 03/16] feat(channels): add Orderbook channel --- packages/bot/src/channels/index.ts | 1 + packages/bot/src/channels/orderbook/index.ts | 2 + .../channels/orderbook/orderbook.channel.ts | 84 +++++++++++++++++++ .../channels/orderbook/orderbook.watcher.ts | 77 +++++++++++++++++ packages/bot/src/channels/orderbook/types.ts | 6 ++ 5 files changed, 170 insertions(+) create mode 100644 packages/bot/src/channels/orderbook/index.ts create mode 100644 packages/bot/src/channels/orderbook/orderbook.channel.ts create mode 100644 packages/bot/src/channels/orderbook/orderbook.watcher.ts create mode 100644 packages/bot/src/channels/orderbook/types.ts diff --git a/packages/bot/src/channels/index.ts b/packages/bot/src/channels/index.ts index c751de52..d85aa48e 100644 --- a/packages/bot/src/channels/index.ts +++ b/packages/bot/src/channels/index.ts @@ -1,3 +1,4 @@ export * from "./candles/index.js"; export * from "./trades/index.js"; export * from "./orders/index.js"; +export * from "./orderbook/index.js"; diff --git a/packages/bot/src/channels/orderbook/index.ts b/packages/bot/src/channels/orderbook/index.ts new file mode 100644 index 00000000..6a8d9402 --- /dev/null +++ b/packages/bot/src/channels/orderbook/index.ts @@ -0,0 +1,2 @@ +export * from "./orderbook.channel.js"; +export * from "./types.js"; diff --git a/packages/bot/src/channels/orderbook/orderbook.channel.ts b/packages/bot/src/channels/orderbook/orderbook.channel.ts new file mode 100644 index 00000000..ca15ca62 --- /dev/null +++ b/packages/bot/src/channels/orderbook/orderbook.channel.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from "node:events"; +import type { IExchange } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; +import { IOrderbook } from "@opentrader/types"; +import type { OrderbookEvent } from "./types.js"; +import { OrderbookWatcher } from "./orderbook.watcher.js"; + +/** + * Channel that subscribes to the orderbook on specific symbol. + * + * Emits: + * - orderbook: `OrderbookEvent` + * + * @example + * ```ts + * const exchange = exchangeProvider.fromCode(ExchangeCode.OKX); + * + * const channel = new OrderbookChannel(exchange); + * channel.add("BTC/USDT"); + * channel.add("ETH/USDT"); + * channel.add("ETH/USDT"); + * + * channel.on("orderbook", (orderbook) => { + * logger.info(orderbook, "New orderbook snapshot"); + * }); + * ``` + */ +export class OrderbookChannel extends EventEmitter { + private readonly exchange: IExchange; + private watchers: OrderbookWatcher[] = []; + + constructor(exchange: IExchange) { + super(); + + this.exchange = exchange; + } + + async add(symbol: string) { + let watcher = this.watchers.find((watcher) => watcher.symbol === symbol); + if (!watcher) { + watcher = new OrderbookWatcher(symbol, this.exchange); + watcher.on("orderbook", this.handleOrderbook); + + this.watchers.push(watcher); + } else { + logger.info(`[OrderbookChannel] Watcher on ${this.exchange.exchangeCode}:${symbol} already exists. Reusing it.`); + } + + watcher.enable(); + } + + handleOrderbook = (orderbook: IOrderbook) => { + const event: OrderbookEvent = { + symbol: orderbook.symbol, + orderbook, + }; + + this.emit("orderbook", event); + }; + + destroy() { + for (const watcher of this.watchers) { + watcher.off("orderbook", this.handleOrderbook); + watcher.disable(); + } + this.watchers = []; + + logger.info(`[OrderbookChannel] Orderbook channel for ${this.exchange.exchangeCode} destroyed`); + } + + getWatchers() { + return this.watchers; + } + + removeWatcher(watcher: OrderbookWatcher) { + watcher.disable(); + + this.watchers = this.watchers.filter((w) => w !== watcher); + } + + get exchangeCode() { + return this.exchange.exchangeCode; + } +} diff --git a/packages/bot/src/channels/orderbook/orderbook.watcher.ts b/packages/bot/src/channels/orderbook/orderbook.watcher.ts new file mode 100644 index 00000000..0cd2d6b2 --- /dev/null +++ b/packages/bot/src/channels/orderbook/orderbook.watcher.ts @@ -0,0 +1,77 @@ +import { EventEmitter } from "node:events"; +import { ExchangeClosedByUser, NetworkError, RequestTimeout } from "ccxt"; +import { type IExchange } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; + +/** + * Watcher for orderbook changes on specific symbol. + * + * Emits: + * - orderbook: `IOrderbook` + */ +export class OrderbookWatcher extends EventEmitter { + public symbol: string; + private exchange: IExchange; + private enabled = false; + + constructor(symbol: string, exchange: IExchange) { + super(); + this.symbol = symbol; + this.exchange = exchange; + } + + enable() { + if (this.enabled) { + logger.warn(`[OrderbookWatcher] Watcher on ${this.exchange.exchangeCode}:${this.symbol} is already enabled`); + return; + } + + this.enabled = true; + logger.info(`[OrderbookWatcher] Watcher on ${this.exchange.exchangeCode}:${this.symbol} was enabled`); + void this.watch(); + } + + disable() { + this.enabled = false; + } + + /** + * Watch orderbook on specific symbol. + */ + private async watch() { + while (this.enabled) { + try { + const orderbook = await this.exchange.watchOrderbook(this.symbol); + logger.debug( + orderbook, + `OrderbookWatcher: Received ${orderbook.asks.length} ASKs and ${orderbook.bids.length} BIDs for ${this.exchange.exchangeCode}:${this.symbol}`, + ); + + this.emit("orderbook", orderbook); + } catch (err) { + if (err instanceof NetworkError) { + logger.warn( + `[OrderbookWatcher] NetworkError occurred on ${this.exchange.exchangeCode}:${this.symbol}. Error: ${err.message}. Reconnecting in 3s...`, + ); + await new Promise((resolve) => setTimeout(resolve, 3000)); // prevents infinite cycle + } else if (err instanceof RequestTimeout) { + logger.warn( + err, + `[OrderbookWatcher] RequestTimeout occurred on ${this.exchange.exchangeCode}:${this.symbol}.`, + ); + } else if (err instanceof ExchangeClosedByUser) { + // This is an expected error when shutting down the daemon by running disable() method + logger.info("[OrderbookWatcher] ExchangeClosedByUser"); + break; // is it necessary, when `this.enabled` is already `false`? + } else { + logger.error( + err, + `[OrderbookWatcher] Unhandled error occurred on ${this.exchange.exchangeCode}:${this.symbol}. Watcher stopped.`, + ); + this.disable(); + break; + } + } + } + } +} diff --git a/packages/bot/src/channels/orderbook/types.ts b/packages/bot/src/channels/orderbook/types.ts new file mode 100644 index 00000000..a7bfb59f --- /dev/null +++ b/packages/bot/src/channels/orderbook/types.ts @@ -0,0 +1,6 @@ +import type { IOrderbook } from "@opentrader/types"; + +export type OrderbookEvent = { + symbol: string; + orderbook: IOrderbook; +}; From 1999400675f0b02ec4718d2393b23e72e635a188 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 15 Aug 2024 17:48:35 +0100 Subject: [PATCH 04/16] feat(channels): add Ticker channel --- packages/bot/src/channels/index.ts | 1 + packages/bot/src/channels/ticker/index.ts | 2 + .../bot/src/channels/ticker/ticker.channel.ts | 84 +++++++++++++++++++ .../bot/src/channels/ticker/ticker.watcher.ts | 74 ++++++++++++++++ packages/bot/src/channels/ticker/types.ts | 6 ++ 5 files changed, 167 insertions(+) create mode 100644 packages/bot/src/channels/ticker/index.ts create mode 100644 packages/bot/src/channels/ticker/ticker.channel.ts create mode 100644 packages/bot/src/channels/ticker/ticker.watcher.ts create mode 100644 packages/bot/src/channels/ticker/types.ts diff --git a/packages/bot/src/channels/index.ts b/packages/bot/src/channels/index.ts index d85aa48e..a301d72b 100644 --- a/packages/bot/src/channels/index.ts +++ b/packages/bot/src/channels/index.ts @@ -2,3 +2,4 @@ export * from "./candles/index.js"; export * from "./trades/index.js"; export * from "./orders/index.js"; export * from "./orderbook/index.js"; +export * from "./ticker/index.js"; diff --git a/packages/bot/src/channels/ticker/index.ts b/packages/bot/src/channels/ticker/index.ts new file mode 100644 index 00000000..26a3dd31 --- /dev/null +++ b/packages/bot/src/channels/ticker/index.ts @@ -0,0 +1,2 @@ +export * from "./ticker.channel.js"; +export * from "./types.js"; diff --git a/packages/bot/src/channels/ticker/ticker.channel.ts b/packages/bot/src/channels/ticker/ticker.channel.ts new file mode 100644 index 00000000..828c00d4 --- /dev/null +++ b/packages/bot/src/channels/ticker/ticker.channel.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from "node:events"; +import type { IExchange } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; +import { ITicker } from "@opentrader/types"; +import type { TickerEvent } from "./types.js"; +import { TickerWatcher } from "./ticker.watcher.js"; + +/** + * Channel that subscribes to the ticker on specific symbol. + * + * Emits: + * - ticker: `TickerEvent` + * + * @example + * ```ts + * const exchange = exchangeProvider.fromCode(ExchangeCode.OKX); + * + * const channel = new TickerChannel(exchange); + * channel.add("BTC/USDT"); + * channel.add("ETH/USDT"); + * channel.add("ETH/USDT"); + * + * channel.on("ticker", (ticker) => { + * logger.info(ticker, "Ticker event received"); + * }); + * ``` + */ +export class TickerChannel extends EventEmitter { + private readonly exchange: IExchange; + private watchers: TickerWatcher[] = []; + + constructor(exchange: IExchange) { + super(); + + this.exchange = exchange; + } + + async add(symbol: string) { + let watcher = this.watchers.find((watcher) => watcher.symbol === symbol); + if (!watcher) { + watcher = new TickerWatcher(symbol, this.exchange); + watcher.on("ticker", this.handleTicker); + + this.watchers.push(watcher); + } else { + logger.info(`[TickerChannel] Watcher on ${this.exchange.exchangeCode}:${symbol} already exists. Reusing it.`); + } + + watcher.enable(); + } + + handleTicker = (ticker: ITicker) => { + const event: TickerEvent = { + symbol: ticker.symbol, + ticker, + }; + + this.emit("ticker", event); + }; + + destroy() { + for (const watcher of this.watchers) { + watcher.off("ticker", this.handleTicker); + watcher.disable(); + } + this.watchers = []; + + logger.info(`[TickerChannel] Channel for ${this.exchange.exchangeCode} destroyed`); + } + + getWatchers() { + return this.watchers; + } + + removeWatcher(watcher: TickerWatcher) { + watcher.disable(); + + this.watchers = this.watchers.filter((w) => w !== watcher); + } + + get exchangeCode() { + return this.exchange.exchangeCode; + } +} diff --git a/packages/bot/src/channels/ticker/ticker.watcher.ts b/packages/bot/src/channels/ticker/ticker.watcher.ts new file mode 100644 index 00000000..fe12d393 --- /dev/null +++ b/packages/bot/src/channels/ticker/ticker.watcher.ts @@ -0,0 +1,74 @@ +import { EventEmitter } from "node:events"; +import { ExchangeClosedByUser, NetworkError, RequestTimeout } from "ccxt"; +import { type IExchange } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; + +/** + * Watcher for ticker on specific symbol. + * + * Emits: + * - ticker: `ITicker` + */ +export class TickerWatcher extends EventEmitter { + public symbol: string; + private exchange: IExchange; + private enabled = false; + + constructor(symbol: string, exchange: IExchange) { + super(); + this.symbol = symbol; + this.exchange = exchange; + } + + enable() { + if (this.enabled) { + logger.warn(`[TickerWatcher] Watcher on ${this.exchange.exchangeCode}:${this.symbol} is already enabled`); + return; + } + + this.enabled = true; + logger.info(`[TickerWatcher] Watcher on ${this.exchange.exchangeCode}:${this.symbol} was enabled`); + void this.watch(); + } + + disable() { + this.enabled = false; + } + + /** + * Watch ticker on specific symbol. + */ + private async watch() { + while (this.enabled) { + try { + const ticker = await this.exchange.watchTicker(this.symbol); + logger.debug( + ticker, + `TickerWatcher: Received ticker with bid ${ticker.bid} and ask ${ticker.ask} for ${this.exchange.exchangeCode}:${this.symbol}`, + ); + + this.emit("ticker", ticker); + } catch (err) { + if (err instanceof NetworkError) { + logger.warn( + `[TickerWatcher] NetworkError occurred on ${this.exchange.exchangeCode}:${this.symbol}. Error: ${err.message}. Reconnecting in 3s...`, + ); + await new Promise((resolve) => setTimeout(resolve, 3000)); // prevents infinite cycle + } else if (err instanceof RequestTimeout) { + logger.warn(err, `[TickerWatcher] RequestTimeout occurred on ${this.exchange.exchangeCode}:${this.symbol}.`); + } else if (err instanceof ExchangeClosedByUser) { + // This is an expected error when shutting down the daemon by running disable() method + logger.info("[TickerWatcher] ExchangeClosedByUser"); + break; // is it necessary, when `this.enabled` is already `false`? + } else { + logger.error( + err, + `[TickerWatcher] Unhandled error occurred on ${this.exchange.exchangeCode}:${this.symbol}. Watcher stopped.`, + ); + this.disable(); + break; + } + } + } + } +} diff --git a/packages/bot/src/channels/ticker/types.ts b/packages/bot/src/channels/ticker/types.ts new file mode 100644 index 00000000..5e721dff --- /dev/null +++ b/packages/bot/src/channels/ticker/types.ts @@ -0,0 +1,6 @@ +import type { ITicker } from "@opentrader/types"; + +export type TickerEvent = { + symbol: string; + ticker: ITicker; +}; From c8a564a069823f044a1ced7a0c8a33c53c4691d3 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 15 Aug 2024 21:35:07 +0100 Subject: [PATCH 05/16] fix(tools, isValidSymbolId): allow quote currencies starting with a digit --- packages/tools/src/symbolId/isValidSymbolId.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/src/symbolId/isValidSymbolId.ts b/packages/tools/src/symbolId/isValidSymbolId.ts index 9f0e9279..8dc31a2e 100644 --- a/packages/tools/src/symbolId/isValidSymbolId.ts +++ b/packages/tools/src/symbolId/isValidSymbolId.ts @@ -5,7 +5,7 @@ export function isValidSymbolId(symbolId: string) { const exchangeCodes = Object.keys(ExchangeCode); const symbolPattern = `^(${exchangeCodes.join( "|", - )})${EXCHANGE_CODE_DELIMITER}[A-Z0-9]+${CURRENCY_PAIR_DELIMITER}[A-Z]+$`; + )})${EXCHANGE_CODE_DELIMITER}[A-Z0-9]+${CURRENCY_PAIR_DELIMITER}[A-Z0-9]+$`; return new RegExp(symbolPattern).test(symbolId); } From 99c55d4cc63d20b7dc7d74a1ebb939ae38b86582 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 15 Aug 2024 21:35:25 +0100 Subject: [PATCH 06/16] feat(tools): add `isValidSymbol` util --- packages/tools/src/symbolId/index.ts | 1 + packages/tools/src/symbolId/isValidSymbol.spec.ts | 15 +++++++++++++++ packages/tools/src/symbolId/isValidSymbol.ts | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 packages/tools/src/symbolId/isValidSymbol.spec.ts create mode 100644 packages/tools/src/symbolId/isValidSymbol.ts diff --git a/packages/tools/src/symbolId/index.ts b/packages/tools/src/symbolId/index.ts index c856f0d8..92f3fb14 100644 --- a/packages/tools/src/symbolId/index.ts +++ b/packages/tools/src/symbolId/index.ts @@ -2,4 +2,5 @@ export * from "./composeSymbolId.js"; export * from "./composeSymbolIdFromPair.js"; export * from "./decomposeSymbolId.js"; export * from "./isValidSymbolId.js"; +export * from "./isValidSymbol.js"; export * from "./isValidExchangeCode.js"; diff --git a/packages/tools/src/symbolId/isValidSymbol.spec.ts b/packages/tools/src/symbolId/isValidSymbol.spec.ts new file mode 100644 index 00000000..e85d4012 --- /dev/null +++ b/packages/tools/src/symbolId/isValidSymbol.spec.ts @@ -0,0 +1,15 @@ +import { isValidSymbol } from "./isValidSymbol.js"; + +describe("isValidSymbol", () => { + it("test existing exchange with a valid currency pair", () => { + expect(isValidSymbol("BTC/USDT")).toBe(true); + }); + + it("test existing exchange with a non-valid currency pair", () => { + expect(isValidSymbol("BTCUSDT")).toBe(false); + }); + + it("test existing exchange with a symbol starting with a number", () => { + expect(isValidSymbol("1INCH/USDT")).toBe(true); + }); +}); diff --git a/packages/tools/src/symbolId/isValidSymbol.ts b/packages/tools/src/symbolId/isValidSymbol.ts new file mode 100644 index 00000000..fd7afc96 --- /dev/null +++ b/packages/tools/src/symbolId/isValidSymbol.ts @@ -0,0 +1,7 @@ +import { CURRENCY_PAIR_DELIMITER } from "./constants.js"; + +export function isValidSymbol(symbol: string) { + const symbolPattern = `^[A-Z0-9]+${CURRENCY_PAIR_DELIMITER}[A-Z0-9]+$`; + + return new RegExp(symbolPattern).test(symbol); +} From d3d46e09db8e130be51122dbb7ce37b47c227761 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 16 Aug 2024 02:40:53 +0100 Subject: [PATCH 07/16] feat: improve bot context for later supporting cross-exchange strategies (#24, #26) --- packages/bot-processor/src/strategy-runner.ts | 6 +- .../types/bot/bot-configuration.interface.ts | 3 +- .../src/types/bot/bot-context.type.ts | 3 +- .../src/types/bot/bot-template.type.ts | 69 +- .../bot-processor/src/utils/createContext.ts | 4 +- .../bot-templates/src/templates/grid-bot.ts | 3 + packages/bot-templates/src/templates/grid.ts | 8 +- packages/bot-templates/src/templates/rsi.ts | 44 +- .../bot-templates/src/templates/test/index.ts | 1 - .../bot-templates/src/templates/test/okx.ts | 30 - .../src/templates/test/trades.ts | 4 +- .../bot/src/consumers/candles.consumer.ts | 120 +-- .../bot/src/consumers/orderbook.consumer.ts | 126 +++ packages/bot/src/consumers/orders.consumer.ts | 23 +- packages/bot/src/consumers/ticker.consumer.ts | 131 +++ packages/bot/src/consumers/timeframe.cron.ts | 63 -- packages/bot/src/consumers/trades.consumer.ts | 108 +- packages/bot/src/platform.ts | 93 +- packages/bot/src/queue/queue.ts | 76 +- packages/bot/src/queue/types.ts | 24 +- packages/daemon/src/platform.ts | 1 + .../types/bot/bot-with-exchange-account.ts | 8 + packages/db/src/types/bot/index.ts | 1 + packages/event-bus/src/index.ts | 10 +- packages/processing/package.json | 1 + packages/processing/src/bot/bot.processing.ts | 2 +- packages/processing/src/index.ts | 3 + .../processing/src/strategy/getTimeframe.ts | 18 + .../processing/src/strategy/getWatchers.ts | 61 ++ packages/processing/src/strategy/runPolicy.ts | 39 + packages/types/src/strategy-runner/context.ts | 6 +- pnpm-lock.yaml | 952 ++++++++---------- 32 files changed, 1181 insertions(+), 860 deletions(-) delete mode 100644 packages/bot-templates/src/templates/test/okx.ts create mode 100644 packages/bot/src/consumers/orderbook.consumer.ts create mode 100644 packages/bot/src/consumers/ticker.consumer.ts delete mode 100644 packages/bot/src/consumers/timeframe.cron.ts create mode 100644 packages/db/src/types/bot/bot-with-exchange-account.ts create mode 100644 packages/processing/src/strategy/getTimeframe.ts create mode 100644 packages/processing/src/strategy/getWatchers.ts create mode 100644 packages/processing/src/strategy/runPolicy.ts diff --git a/packages/bot-processor/src/strategy-runner.ts b/packages/bot-processor/src/strategy-runner.ts index 4e8b1b13..a18d184d 100644 --- a/packages/bot-processor/src/strategy-runner.ts +++ b/packages/bot-processor/src/strategy-runner.ts @@ -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, StrategyTriggerEventType } from "@opentrader/types"; import { BotControl } from "./bot-control.js"; import { effectRunnerMap } from "./effect-runner.js"; import { isEffect } from "./effects/index.js"; @@ -43,8 +43,8 @@ export class StrategyRunner { 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?: StrategyTriggerEventType, market?: MarketData) { + const context = createContext(this.control, this.botConfig, this.exchange, "process", state, market, event); await this.runTemplate(context); } diff --git a/packages/bot-processor/src/types/bot/bot-configuration.interface.ts b/packages/bot-processor/src/types/bot/bot-configuration.interface.ts index 204b550f..1c0d929d 100644 --- a/packages/bot-processor/src/types/bot/bot-configuration.interface.ts +++ b/packages/bot-processor/src/types/bot/bot-configuration.interface.ts @@ -1,4 +1,4 @@ -import type { ExchangeCode } from "@opentrader/types"; +import type { BarSize, ExchangeCode } from '@opentrader/types'; export type IBotConfiguration = { id: number; @@ -6,4 +6,5 @@ export type IBotConfiguration = { quoteCurrency: string; exchangeCode: ExchangeCode; settings: T; + timeframe?: BarSize | null; }; diff --git a/packages/bot-processor/src/types/bot/bot-context.type.ts b/packages/bot-processor/src/types/bot/bot-context.type.ts index a4e7d3de..122077fc 100644 --- a/packages/bot-processor/src/types/bot/bot-context.type.ts +++ b/packages/bot-processor/src/types/bot/bot-context.type.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData } from "@opentrader/types"; +import type { MarketData, StrategyTriggerEventType } 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"; @@ -25,6 +25,7 @@ export type TBotContext = 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 { (ctx: TBotContext): Generator; /** @@ -13,6 +25,11 @@ export interface BotTemplate { * 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. */ @@ -23,12 +40,58 @@ export interface BotTemplate { */ 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; + [Watcher.watchOrderbook]?: WatchCondition; + [Watcher.watchTicker]?: WatchCondition; + [Watcher.watchCandles]?: WatchCondition; + }; 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[]); + [StrategyTriggerEventType.onCandleClosed]?: boolean | ((botConfig: T) => boolean); + [StrategyTriggerEventType.onPublicTrade]?: boolean | ((botConfig: T) => boolean); + [StrategyTriggerEventType.onOrderbookChange]?: boolean | ((botConfig: T) => boolean); + [StrategyTriggerEventType.onTickerChange]?: boolean | ((botConfig: T) => boolean); + [StrategyTriggerEventType.onOrderFilled]?: boolean | ((botConfig: T) => boolean); }; } diff --git a/packages/bot-processor/src/utils/createContext.ts b/packages/bot-processor/src/utils/createContext.ts index ecb758e2..2884bcc6 100644 --- a/packages/bot-processor/src/utils/createContext.ts +++ b/packages/bot-processor/src/utils/createContext.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData } from "@opentrader/types"; +import type { MarketData, StrategyTriggerEventType } from "@opentrader/types"; import type { BotState, IBotConfiguration, IBotControl, TBotContext } from "../types/index.js"; export function createContext( @@ -11,6 +11,7 @@ export function createContext( market: MarketData = { candles: [], }, + event?: StrategyTriggerEventType, ): TBotContext { return { control, @@ -22,5 +23,6 @@ export function createContext( onProcess: command === "process", state, market, + event, }; } diff --git a/packages/bot-templates/src/templates/grid-bot.ts b/packages/bot-templates/src/templates/grid-bot.ts index 130a8e0e..dbcb4062 100644 --- a/packages/bot-templates/src/templates/grid-bot.ts +++ b/packages/bot-templates/src/templates/grid-bot.ts @@ -69,5 +69,8 @@ gridBot.schema = z.object({ }), ), }); +gridBot.runPolicy = { + onOrderFilled: true, +}; export type GridBotConfig = IBotConfiguration>; diff --git a/packages/bot-templates/src/templates/grid.ts b/packages/bot-templates/src/templates/grid.ts index 936f94e8..eccc5912 100644 --- a/packages/bot-templates/src/templates/grid.ts +++ b/packages/bot-templates/src/templates/grid.ts @@ -38,10 +38,10 @@ grid.schema = z.object({ highPrice: z.number().positive().describe("Highest price of the grid"), lowPrice: z.number().positive().describe("Lowest price of the grid"), gridLevels: z.number().positive().describe("Number of grid lines"), - quantityPerGrid: z - .number() - .positive() - .describe("Quantity of base currency per each grid"), + quantityPerGrid: z.number().positive().describe("Quantity of base currency per each grid"), }); +grid.runPolicy = { + onOrderFilled: true, +}; export type GridBotLiteConfig = IBotConfiguration>; diff --git a/packages/bot-templates/src/templates/rsi.ts b/packages/bot-templates/src/templates/rsi.ts index 2736d16a..3cd947ba 100644 --- a/packages/bot-templates/src/templates/rsi.ts +++ b/packages/bot-templates/src/templates/rsi.ts @@ -1,13 +1,6 @@ -import { logger } from "@opentrader/logger"; import { z } from "zod"; -import { - buy, - cancelSmartTrade, - IBotConfiguration, - sell, - TBotContext, - useRSI, -} from "@opentrader/bot-processor"; +import { buy, cancelSmartTrade, IBotConfiguration, sell, TBotContext, useRSI } from "@opentrader/bot-processor"; +import { logger } from "@opentrader/logger"; /** * Inspired by https://github.com/askmike/gekko/blob/develop/strategies/RSI.js @@ -107,32 +100,21 @@ export function* rsi(ctx: TBotContext) { rsi.displayName = "RSI Strategy"; rsi.schema = z.object({ - high: z - .number() - .min(0) - .max(100) - .default(70) - .describe("Sell when RSI is above this value"), - low: z - .number() - .min(0) - .max(100) - .default(30) - .describe("Buy when RSI is below this value"), + high: z.number().min(0).max(100).default(70).describe("Sell when RSI is above this value"), + low: z.number().min(0).max(100).default(30).describe("Buy when RSI is below this value"), periods: z.number().positive().default(14).describe("RSI period"), - persistence: z - .number() - .positive() - .default(1) - .describe("Number of candles to persist in trend before buying/selling"), - quantity: z - .number() - .positive() - .default(0.0001) - .describe("Quantity to buy/sell"), + persistence: z.number().positive().default(1).describe("Number of candles to persist in trend before buying/selling"), + quantity: z.number().positive().default(0.0001).describe("Quantity to buy/sell"), }); rsi.requiredHistory = 15; +rsi.timeframe = ({ timeframe }: IBotConfiguration) => timeframe; +rsi.runPolicy = { + onCandleClosed: true, +}; +rsi.watchers = { + watchCandles: ({ baseCurrency, quoteCurrency }: IBotConfiguration) => `${baseCurrency}/${quoteCurrency}`, +}; type RsiState = { trend?: { diff --git a/packages/bot-templates/src/templates/test/index.ts b/packages/bot-templates/src/templates/test/index.ts index 91f02620..814a0af4 100644 --- a/packages/bot-templates/src/templates/test/index.ts +++ b/packages/bot-templates/src/templates/test/index.ts @@ -4,5 +4,4 @@ export * from "./debug.js"; export * from "./candle.js"; export * from "./rsi.js"; export * from "./state.js"; -export * from "./okx.js"; export * from "./trades.js"; diff --git a/packages/bot-templates/src/templates/test/okx.ts b/packages/bot-templates/src/templates/test/okx.ts deleted file mode 100644 index ab4dcbff..00000000 --- a/packages/bot-templates/src/templates/test/okx.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from "zod"; -import { logger } from "@opentrader/logger"; -import { buy, sell, TBotContext, useExchange } from "@opentrader/bot-processor"; -import { IExchange } from "@opentrader/exchanges"; -import { IGetMarketPriceResponse } from "@opentrader/types"; - -export function* okx(ctx: TBotContext) { - logger.info("[OKX STRATEGY]: Strategy exec"); - - const exchange: IExchange = yield useExchange(); - - const { price }: IGetMarketPriceResponse = yield exchange.getMarketPrice({ symbol: "BTC/USDT" }); - logger.info(`[OKX STRATEGY]: Market price: ${price}`); - - const result = yield sell({ - pair: "BTC/USDT", - quantity: 0.0001, - price: 10000, // much lower than current price - orderType: "Limit", - }); - - console.log("[OKX STRATEGY]: Sell result", result); -} - -okx.displayName = "OKX Strategy"; -okx.hidden = true; -okx.schema = z.object({}); -okx.runPolicy = { - watchTrades: "BTC/USDT", -}; diff --git a/packages/bot-templates/src/templates/test/trades.ts b/packages/bot-templates/src/templates/test/trades.ts index 7e518089..25cf9439 100644 --- a/packages/bot-templates/src/templates/test/trades.ts +++ b/packages/bot-templates/src/templates/test/trades.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { logger } from "@opentrader/logger"; -import { buy, sell, TBotContext, useExchange } from "@opentrader/bot-processor"; +import { TBotContext } from "@opentrader/bot-processor"; export function* testTrades(ctx: TBotContext) { logger.info("[TRADES]: Strategy exec"); @@ -10,6 +10,6 @@ export function* testTrades(ctx: TBotContext) { testTrades.displayName = "Trades Strategy"; testTrades.hidden = true; testTrades.schema = z.object({}); -testTrades.runPolicy = { +testTrades.watchers = { watchTrades: ["BTC/USDT", "ETH/USDT"], }; diff --git a/packages/bot/src/consumers/candles.consumer.ts b/packages/bot/src/consumers/candles.consumer.ts index 11073a47..37dd82f8 100644 --- a/packages/bot/src/consumers/candles.consumer.ts +++ b/packages/bot/src/consumers/candles.consumer.ts @@ -1,18 +1,24 @@ +import { EventEmitter } from "node:events"; import { exchangeProvider } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; import type { TBot } from "@opentrader/db"; -import { xprisma } from "@opentrader/db"; -import type { BarSize } from "@opentrader/types"; +import { getWatchers, getTimeframe } from "@opentrader/processing"; +import { decomposeSymbolId } from "@opentrader/tools"; +import { BarSize, ExchangeCode } from "@opentrader/types"; import { findStrategy } from "@opentrader/bot-templates/server"; -import type { CandleEvent } from "../channels/index.js"; +import { CandleEvent } from "../channels/index.js"; import { CandlesChannel } from "../channels/index.js"; -import { processingQueue } from "../queue/index.js"; -export class CandlesConsumer { +/** + * Emits: + * - candle: CandleEvent + */ +export class CandlesConsumer extends EventEmitter { private channels: CandlesChannel[] = []; private bots: TBot[] = []; constructor(bots: TBot[]) { + super(); this.bots = bots; } @@ -31,57 +37,68 @@ export class CandlesConsumer { * @returns */ async addBot(bot: TBot) { - const exchangeAccount = await xprisma.exchangeAccount.findUniqueOrThrow({ - where: { - id: bot.exchangeAccountId, - }, - }); - const exchange = exchangeProvider.fromAccount(exchangeAccount); - const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; - - if (bot.timeframe === null) { + const { strategyFn } = await findStrategy(bot.template); + const { watchCandles: symbols } = getWatchers(strategyFn, bot); + + const timeframe = getTimeframe(strategyFn, bot); + if (!timeframe) { logger.warn( - `[CandlesProcessor]: Skip adding bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel. Reason: The bot is not timeframe based.`, + `[CandlesProcessor]: Skip adding bot [${bot.id}:"${bot.name}"] to the candles channel. Reason: The bot has no timeframe defined.`, ); return; } - let channel = this.channels.find((channel) => channel.exchangeCode === exchange.exchangeCode); + for (const symbolId of symbols) { + const { exchangeCode, currencyPairSymbol: symbol } = decomposeSymbolId(symbolId); + + const channel = this.getChannel(exchangeCode); + await channel.add(symbol, timeframe, strategyFn.requiredHistory); + logger.info( + `[CandlesProcessor]: Subscribed bot [${bot.id}:"${bot.name}"] to the ${exchangeCode}:${symbol} channel`, + ); + } + } + + /** + * Return existing channel or create a new one. + */ + private getChannel(exchangeCode: ExchangeCode) { + let channel = this.channels.find((channel) => channel.exchangeCode === exchangeCode); if (!channel) { + const exchange = exchangeProvider.fromCode(exchangeCode); + channel = new CandlesChannel(exchange); this.channels.push(channel); - logger.info(`[CandlesProcessor] Created ${exchange.exchangeCode}:${symbol} channel`); + logger.info(`[CandlesConsumer] Created ${exchangeCode} channel`); // @todo type channel.on("candle", this.handleCandle); } - const { strategyFn } = await findStrategy(bot.template); - await channel.add(symbol, bot.timeframe as BarSize, strategyFn.requiredHistory); - logger.info( - `[CandlesProcessor]: Subscribed bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel`, - ); + return channel; } /** * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels() { - const bots = await xprisma.bot.findMany({ - where: { - timeframe: { not: null }, - enabled: true, - }, - include: { - exchangeAccount: true, - }, - }); + async cleanStaleChannels(bots: TBot[]) { + const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; + for (const bot of bots) { + const { strategyFn } = await findStrategy(bot.template); + const { watchCandles } = getWatchers(strategyFn, bot); + + botsInUse.push({ + timeframe: getTimeframe(strategyFn, bot), // override + symbols: watchCandles, + exchangeCodes: [...new Set(watchCandles.map((symbolId) => decomposeSymbolId(symbolId).exchangeCode))], + }); + } for (const channel of this.channels) { // Clean stale channels - const isChannelUsedByAnyBot = bots.some((bot) => bot.exchangeAccount.exchangeCode === channel.exchangeCode); + const isChannelUsedByAnyBot = botsInUse.some((bot) => bot.exchangeCodes.includes(channel.exchangeCode)); if (!isChannelUsedByAnyBot) { logger.info(`[CandlesProcessor] Removing stale channel ${channel.exchangeCode}`); this.removeChannel(channel); @@ -90,10 +107,8 @@ export class CandlesConsumer { // Clean up stale watchers for (const watcher of channel.getWatchers()) { - const isWatcherUsedByAnyBot = bots.some( - (bot) => - bot.exchangeAccount.exchangeCode === channel.exchangeCode && - `${bot.baseCurrency}/${bot.quoteCurrency}` === watcher.symbol, + const isWatcherUsedByAnyBot = botsInUse.some((bot) => + bot.symbols.includes(`${channel.exchangeCode}/${watcher.symbol}`), ); if (!isWatcherUsedByAnyBot) { @@ -104,10 +119,9 @@ export class CandlesConsumer { // Clean stale aggregators for (const aggregator of channel.getAggregators()) { - const isAggregatorUsedByAnyBot = bots.some( + const isAggregatorUsedByAnyBot = botsInUse.some( (bot) => - bot.exchangeAccount.exchangeCode === channel.exchangeCode && - `${bot.baseCurrency}/${bot.quoteCurrency}` === aggregator.symbol && + bot.symbols.includes(`${channel.exchangeCode}/${aggregator.symbol}`) && bot.timeframe === aggregator.timeframe, ); @@ -122,31 +136,7 @@ export class CandlesConsumer { } private handleCandle = async (data: CandleEvent) => { - const { candle, history, symbol, timeframe } = data; - - logger.info(`CandlesProcessor: Received candle ${timeframe} for ${symbol}. Start processing.`); - - const bots = await xprisma.bot.custom.findMany({ - where: { - timeframe, - enabled: true, - }, - }); - logger.info(`CandlesProcessor: ${timeframe}. Found ${bots.length} bots`); - - for (const bot of bots) { - if (!bot.enabled) { - logger.warn("❗ Cannot run bot process when the bot is disabled"); - continue; - } - - processingQueue.push({ - type: "onCandleClosed", - bot, - candle, - candles: history, - }); - } + this.emit("candle", data); }; /** diff --git a/packages/bot/src/consumers/orderbook.consumer.ts b/packages/bot/src/consumers/orderbook.consumer.ts new file mode 100644 index 00000000..fd4f2756 --- /dev/null +++ b/packages/bot/src/consumers/orderbook.consumer.ts @@ -0,0 +1,126 @@ +import { EventEmitter } from "node:events"; +import { exchangeProvider } from "@opentrader/exchanges"; +import { logger } from "@opentrader/logger"; +import type { TBot } from "@opentrader/db"; +import { xprisma } from "@opentrader/db"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { getWatchers, getTimeframe } from "@opentrader/processing"; +import { decomposeSymbolId } from "@opentrader/tools"; +import { BarSize, ExchangeCode } from "@opentrader/types"; +import type { OrderbookEvent } from "../channels/index.js"; +import { OrderbookChannel } from "../channels/index.js"; + +/** + * Emits: + * - orderbook: OrderbookEvent + */ +export class OrderbookConsumer extends EventEmitter { + private channels: OrderbookChannel[] = []; + private bots: TBot[] = []; + + constructor(bots: TBot[]) { + super(); + this.bots = bots; + } + + async create() { + logger.info(`[OrderbookConsumer] Creating orderbook channel for ${this.bots.length} bots`); + + for (const bot of this.bots) { + await this.addBot(bot); + } + } + + /** + * Subscribes the bot to the orderbook channel. + * It will create the channel if necessary or reusing it if it already exists. + * @param bot Bot to add + * @returns + */ + async addBot(bot: TBot) { + const exchangeAccount = await xprisma.exchangeAccount.findUniqueOrThrow({ + where: { + id: bot.exchangeAccountId, + }, + }); + const exchange = exchangeProvider.fromAccount(exchangeAccount); + const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; + + let channel = this.channels.find((channel) => channel.exchangeCode === exchange.exchangeCode); + if (!channel) { + channel = new OrderbookChannel(exchange); + this.channels.push(channel); + + logger.info(`[OrderbookConsumer] Created ${exchange.exchangeCode}:${symbol} channel`); + + // @todo type + channel.on("orderbook", this.handleOrderbook); + } + + await channel.add(symbol); + logger.info( + `[OrderbookConsumer]: Subscribed bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel`, + ); + } + + /** + * Remove unused channels that are no longer used by any bots. + * Triggered when any bot was stopped. + */ + async cleanStaleChannels(bots: TBot[]) { + const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; + for (const bot of bots) { + const { strategyFn } = await findStrategy(bot.template); + const { watchCandles } = getWatchers(strategyFn, bot); + + botsInUse.push({ + timeframe: getTimeframe(strategyFn, bot), // override + symbols: watchCandles, + exchangeCodes: [...new Set(watchCandles.map((symbolId) => decomposeSymbolId(symbolId).exchangeCode))], + }); + } + + for (const channel of this.channels) { + // Clean stale channels + const isChannelUsedByAnyBot = botsInUse.some((bot) => bot.exchangeCodes.includes(channel.exchangeCode)); + if (!isChannelUsedByAnyBot) { + logger.info(`[OrderbookConsumer] Removing stale channel ${channel.exchangeCode}`); + this.removeChannel(channel); + continue; // no need to check watchers + } + + // Clean up stale watchers + for (const watcher of channel.getWatchers()) { + const isWatcherUsedByAnyBot = botsInUse.some((bot) => + bot.symbols.includes(`${channel.exchangeCode}/${watcher.symbol}`), + ); + + if (!isWatcherUsedByAnyBot) { + logger.info(`[OrderbookConsumer] Removing stale watcher ${channel.exchangeCode}:${watcher.symbol}`); + channel.removeWatcher(watcher); + } + } + } + } + + private handleOrderbook = async (data: OrderbookEvent) => { + this.emit("orderbook", data); + }; + + /** + * Destroy and remove the channel from the list. + * @param exchangeCode + */ + private removeChannel(channel: OrderbookChannel) { + channel.off("orderbook", this.handleOrderbook); + channel.destroy(); + + this.channels = this.channels.filter((c) => c !== channel); + } + + destroy() { + for (const channel of this.channels) { + channel.destroy(); + } + } +} diff --git a/packages/bot/src/consumers/orders.consumer.ts b/packages/bot/src/consumers/orders.consumer.ts index 5b025112..edc1c92b 100644 --- a/packages/bot/src/consumers/orders.consumer.ts +++ b/packages/bot/src/consumers/orders.consumer.ts @@ -1,5 +1,6 @@ +import { findStrategy } from "@opentrader/bot-templates/server"; import type { IWatchOrder } from "@opentrader/types"; -import { BotProcessing } from "@opentrader/processing"; +import { BotProcessing, shouldRunStrategy } from "@opentrader/processing"; import type { OrderWithSmartTrade, ExchangeAccountWithCredentials } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { logger } from "@opentrader/logger"; @@ -86,18 +87,16 @@ export class OrdersConsumer { return; } - if (botProcessor.getTimeframe()) { - logger.error( - `❕ The bot #${botProcessor.getId()} is timeframe-based: ${botProcessor.getTimeframe()}. Skip processing`, - ); - return; - } + const bot = botProcessor.getBot(); + const { strategyFn } = await findStrategy(bot.template); - processingQueue.push({ - type: "onOrderFilled", - bot: botProcessor.getBot(), - orderId: order.id, - }); + if (shouldRunStrategy(strategyFn, bot, "onOrderFilled")) { + processingQueue.push({ + type: "onOrderFilled", + bot, + orderId: order.id, + }); + } } private async onOrderCanceled(exchangeOrder: IWatchOrder, order: OrderWithSmartTrade) { diff --git a/packages/bot/src/consumers/ticker.consumer.ts b/packages/bot/src/consumers/ticker.consumer.ts new file mode 100644 index 00000000..407115bf --- /dev/null +++ b/packages/bot/src/consumers/ticker.consumer.ts @@ -0,0 +1,131 @@ +import { EventEmitter } from "node:events"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { exchangeProvider } from "@opentrader/exchanges"; +import { getTimeframe, getWatchers } from "@opentrader/processing"; +import { logger } from "@opentrader/logger"; +import type { TBot } from "@opentrader/db"; +import { decomposeSymbolId } from "@opentrader/tools"; +import { BarSize, ExchangeCode } from "@opentrader/types"; +import type { TickerEvent } from "../channels/index.js"; +import { TickerChannel } from "../channels/index.js"; + +/** + * Emits: + * - ticker: TickerEvent + */ +export class TickerConsumer extends EventEmitter { + private channels: TickerChannel[] = []; + private bots: TBot[] = []; + + constructor(bots: TBot[]) { + super(); + this.bots = bots; + } + + async create() { + logger.info(`[TickerConsumer] Creating ticker channel for ${this.bots.length} bots`); + + for (const bot of this.bots) { + await this.addBot(bot); + } + } + + /** + * Subscribes the bot to the ticker channel. + * It will create the channel if necessary or reusing it if it already exists. + */ + async addBot(bot: TBot) { + const { strategyFn } = await findStrategy(bot.template); + const { watchTicker: symbols } = getWatchers(strategyFn, bot); + + for (const symbolId of symbols) { + const { exchangeCode, currencyPairSymbol: symbol } = decomposeSymbolId(symbolId); + + const channel = this.getChannel(exchangeCode); + await channel.add(symbol); + logger.info( + `[TickerConsumer]: Subscribed bot [${bot.id}:"${bot.name}"] to the ${exchangeCode}:${symbol} channel`, + ); + } + } + + /** + * Return existing channel or create a new one. + */ + private getChannel(exchangeCode: ExchangeCode) { + let channel = this.channels.find((channel) => channel.exchangeCode === exchangeCode); + if (!channel) { + const exchange = exchangeProvider.fromCode(exchangeCode); + + channel = new TickerChannel(exchange); + this.channels.push(channel); + + logger.info(`[TickerConsumer] Created ${exchangeCode} channel`); + + // @todo type + channel.on("ticker", this.handleTicker); + } + + return channel; + } + + /** + * Remove unused channels that are no longer used by any bots. + * Triggered when any bot was stopped. + */ + async cleanStaleChannels(bots: TBot[]) { + const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; + for (const bot of bots) { + const { strategyFn } = await findStrategy(bot.template); + const { watchCandles } = getWatchers(strategyFn, bot); + + botsInUse.push({ + timeframe: getTimeframe(strategyFn, bot), // override + symbols: watchCandles, + exchangeCodes: [...new Set(watchCandles.map((symbolId) => decomposeSymbolId(symbolId).exchangeCode))], + }); + } + + for (const channel of this.channels) { + // Clean stale channels + const isChannelUsedByAnyBot = botsInUse.some((bot) => bot.exchangeCodes.includes(channel.exchangeCode)); + if (!isChannelUsedByAnyBot) { + logger.info(`[TickerConsumer] Removing stale channel ${channel.exchangeCode}`); + this.removeChannel(channel); + continue; // no need to check watchers + } + + // Clean up stale watchers + for (const watcher of channel.getWatchers()) { + const isWatcherUsedByAnyBot = botsInUse.some((bot) => + bot.symbols.includes(`${channel.exchangeCode}/${watcher.symbol}`), + ); + + if (!isWatcherUsedByAnyBot) { + logger.info(`[TickerConsumer] Removing stale watcher ${channel.exchangeCode}:${watcher.symbol}`); + channel.removeWatcher(watcher); + } + } + } + } + + private handleTicker = async (data: TickerEvent) => { + this.emit("ticker", data); + }; + + /** + * Destroy and remove the channel from the list. + */ + private removeChannel(channel: TickerChannel) { + channel.off("ticker", this.handleTicker); + channel.destroy(); + + this.channels = this.channels.filter((c) => c !== channel); + } + + destroy() { + for (const channel of this.channels) { + channel.destroy(); + } + } +} diff --git a/packages/bot/src/consumers/timeframe.cron.ts b/packages/bot/src/consumers/timeframe.cron.ts deleted file mode 100644 index 08d4b2e7..00000000 --- a/packages/bot/src/consumers/timeframe.cron.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { BotProcessing } from "@opentrader/processing"; -import { xprisma } from "@opentrader/db"; -import { CronJob } from "cron"; -import { logger } from "@opentrader/logger"; - -type Timeframe = "1m" | "5m" | "10m" | "15m" | "30m" | "1h" | "4h" | "1d"; - -const CronExpression: Record = { - "1m": "20 * * * * *", - "5m": "20 */5 * * * *", - "10m": "20 */10 * * * *", - "15m": "20 */15 * * * *", - "30m": "20 */30 * * * *", - "1h": "20 0 * * * *", - "4h": "20 0 */4 * * *", - "1d": "20 0 0 * * *", -}; - -// Not used -export class TimeframeCron { - tasks: CronJob[] = []; - - create() { - for (const [timeframe, cronExpression] of Object.entries(CronExpression)) { - this.tasks.push( - new CronJob(cronExpression, async () => { - await this.execTemplate(timeframe as Timeframe); - }), - ); - } - } - - destroy() { - for (const task of this.tasks) { - task.stop(); - } - } - - async execTemplate(timeframe: Timeframe) { - const bots = await xprisma.bot.findMany({ - where: { - timeframe, - enabled: true, - }, - }); - logger.info(`TimeframeCron: ${timeframe}. Found ${bots.length} bots`); - - for (const bot of bots) { - logger.info(`Exec bot #${bot.id} template`); - const botProcessor = await BotProcessing.fromId(bot.id); - - if (botProcessor.isBotStopped()) { - logger.warn("❗ Cannot run bot process when the bot is disabled"); - continue; - } - - await botProcessor.process(); - await botProcessor.placePendingOrders(); - - logger.info(`Exec bot #${bot.id} template done`); - } - } -} diff --git a/packages/bot/src/consumers/trades.consumer.ts b/packages/bot/src/consumers/trades.consumer.ts index 2d1dfddb..86d92fe8 100644 --- a/packages/bot/src/consumers/trades.consumer.ts +++ b/packages/bot/src/consumers/trades.consumer.ts @@ -1,40 +1,25 @@ +import { EventEmitter } from "node:events"; import { exchangeProvider } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; import type { TBot } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { findStrategy } from "@opentrader/bot-templates/server"; +import { getWatchers, getTimeframe } from "@opentrader/processing"; +import { decomposeSymbolId } from "@opentrader/tools"; +import { BarSize, ExchangeCode } from "@opentrader/types"; import type { TradeEvent } from "../channels/index.js"; import { TradesChannel } from "../channels/index.js"; -import { processingQueue } from "../queue/index.js"; -import { BotTemplate } from "../../../bot-processor/src/index.js"; -function getSymbolsToWatch(runPolicy: BotTemplate["runPolicy"], bot: TBot): string[] { - if (!runPolicy?.watchTrades) { - return []; - } - - const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; - - let symbols: string[] | string = []; - - if (typeof runPolicy.watchTrades === "string" || Array.isArray(runPolicy.watchTrades)) { - symbols = runPolicy.watchTrades; - } else if (typeof runPolicy.watchTrades === "function") { - symbols = runPolicy.watchTrades(bot); - } - - if (Array.isArray(symbols)) { - return symbols; - } else { - return [symbols]; - } -} - -export class TradesConsumer { +/** + * Emits: + * - trade: TradeEvent + */ +export class TradesConsumer extends EventEmitter { private channels: TradesChannel[] = []; private bots: TBot[] = []; constructor(bots: TBot[]) { + super(); this.bots = bots; } @@ -61,14 +46,6 @@ export class TradesConsumer { const exchange = exchangeProvider.fromAccount(exchangeAccount); const symbol = `${bot.baseCurrency}/${bot.quoteCurrency}`; - const { strategyFn } = await findStrategy(bot.template); - if (!isWatchingSymbol(strategyFn.runPolicy, bot)) { - logger.warn( - `[TradesConsumer]: Skip adding bot [${bot.id}:"${bot.name}"] to the ${exchange.exchangeCode}:${symbol} channel. Reason: No runPolicty for watchTrades.`, - ); - return; - } - let channel = this.channels.find((channel) => channel.exchangeCode === exchange.exchangeCode); if (!channel) { channel = new TradesChannel(exchange); @@ -90,20 +67,22 @@ export class TradesConsumer { * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels() { - const bots = await xprisma.bot.findMany({ - where: { - timeframe: { not: null }, - enabled: true, - }, - include: { - exchangeAccount: true, - }, - }); + async cleanStaleChannels(bots: TBot[]) { + const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; + for (const bot of bots) { + const { strategyFn } = await findStrategy(bot.template); + const { watchCandles } = getWatchers(strategyFn, bot); + + botsInUse.push({ + timeframe: getTimeframe(strategyFn, bot), // override + symbols: watchCandles, + exchangeCodes: [...new Set(watchCandles.map((symbolId) => decomposeSymbolId(symbolId).exchangeCode))], + }); + } for (const channel of this.channels) { // Clean stale channels - const isChannelUsedByAnyBot = bots.some((bot) => bot.exchangeAccount.exchangeCode === channel.exchangeCode); + const isChannelUsedByAnyBot = botsInUse.some((bot) => bot.exchangeCodes.includes(channel.exchangeCode)); if (!isChannelUsedByAnyBot) { logger.info(`[TradesConsumer] Removing stale channel ${channel.exchangeCode}`); this.removeChannel(channel); @@ -112,10 +91,8 @@ export class TradesConsumer { // Clean up stale watchers for (const watcher of channel.getWatchers()) { - const isWatcherUsedByAnyBot = bots.some( - (bot) => - bot.exchangeAccount.exchangeCode === channel.exchangeCode && - `${bot.baseCurrency}/${bot.quoteCurrency}` === watcher.symbol, + const isWatcherUsedByAnyBot = botsInUse.some((bot) => + bot.symbols.includes(`${channel.exchangeCode}/${watcher.symbol}`), ); if (!isWatcherUsedByAnyBot) { @@ -127,44 +104,11 @@ export class TradesConsumer { } private handleTrade = async (data: TradeEvent) => { - const { trade, symbol } = data; - - logger.info(`[TradesConsumer] New trade: ${trade.side} ${trade.amount} of ${symbol}. Start processing.`); - - const enabledBots = await xprisma.bot.custom.findMany({ - where: { - enabled: true, - }, - }); - const targetBots: TBot[] = []; - - for (const bot of enabledBots) { - const { strategyFn } = await findStrategy(bot.template); - - if (getSymbolsToWatch(strategyFn.runPolicy, bot)) { - targetBots.push(bot); - } - } - - logger.info(`[TradesConsumer]: Targeted ${targetBots.length} bots`); - - for (const bot of targetBots) { - if (!bot.enabled) { - logger.warn("❗ Cannot run bot process when the bot is disabled"); - continue; - } - - processingQueue.push({ - type: "onPublicTrade", - bot, - trade, - }); - } + this.emit("trade", data); }; /** * Destroy and remove the channel from the list. - * @param exchangeCode */ private removeChannel(channel: TradesChannel) { channel.off("trade", this.handleTrade); diff --git a/packages/bot/src/platform.ts b/packages/bot/src/platform.ts index b1ef957a..6685436a 100644 --- a/packages/bot/src/platform.ts +++ b/packages/bot/src/platform.ts @@ -1,24 +1,48 @@ -import { xprisma, type ExchangeAccountWithCredentials, type TBot } from "@opentrader/db"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { xprisma, type ExchangeAccountWithCredentials, TBotWithExchangeAccount } from "@opentrader/db"; import { logger } from "@opentrader/logger"; import { exchangeProvider } from "@opentrader/exchanges"; -import { BotProcessing } from "@opentrader/processing"; +import { BotProcessing, getWatchers, shouldRunStrategy } from "@opentrader/processing"; import { eventBus } from "@opentrader/event-bus"; +import { CandleEvent, OrderbookEvent, TickerEvent, TradeEvent } from "./channels/index.js"; +import { processingQueue } from "./queue/index.js"; +import { ProcessingEvent } from "./queue/types.js"; import { CandlesConsumer } from "./consumers/candles.consumer.js"; import { TradesConsumer } from "./consumers/trades.consumer.js"; +import { OrderbookConsumer } from "./consumers/orderbook.consumer.js"; +import { TickerConsumer } from "./consumers/ticker.consumer.js"; import { OrdersConsumer } from "./consumers/orders.consumer.js"; export class Platform { private ordersConsumer: OrdersConsumer; private candlesConsumer: CandlesConsumer; private tradesConsumer: TradesConsumer; - + private orderbookConsumer: OrderbookConsumer; + private tickerConsumer: TickerConsumer; private unsubscribeFromEventBus = () => {}; + private enabledBots: TBotWithExchangeAccount[] = []; - constructor(exchangeAccounts: ExchangeAccountWithCredentials[], bots: TBot[]) { + constructor(exchangeAccounts: ExchangeAccountWithCredentials[], bots: TBotWithExchangeAccount[]) { this.ordersConsumer = new OrdersConsumer(exchangeAccounts); + this.candlesConsumer = new CandlesConsumer(bots); + this.candlesConsumer.on("candle", ({ candle, history }: CandleEvent) => + this.handleProcess({ type: "onCandleClosed", candle, candles: history }), + ); + this.tradesConsumer = new TradesConsumer(bots); + this.tradesConsumer.on("trade", ({ trade }: TradeEvent) => this.handleProcess({ type: "onPublicTrade", trade })); + + this.orderbookConsumer = new OrderbookConsumer(bots); + this.orderbookConsumer.on("orderbook", ({ orderbook }: OrderbookEvent) => + this.handleProcess({ type: "onOrderbookChange", orderbook }), + ); + + this.tickerConsumer = new TickerConsumer(bots); + this.tickerConsumer.on("ticker", ({ ticker }: TickerEvent) => + this.handleProcess({ type: "onTickerChange", ticker }), + ); } async bootstrap() { @@ -27,15 +51,18 @@ export class Platform { logger.info("[Processor] OrdersProcessor created"); await this.ordersConsumer.create(); - // logger.info("[Processor] TimeframeProcessor created"); - // this.timeframeCron.create(); - logger.info("[Processor] CandlesProcessor created"); await this.candlesConsumer.create(); logger.info("[Processor] TradesProcessor created"); await this.tradesConsumer.create(); + logger.info("[Processor] OrderbookProcessor created"); + await this.orderbookConsumer.create(); + + logger.info("[Processor] TickerProcessor created"); + await this.tickerConsumer.create(); + this.unsubscribeFromEventBus = this.subscribeToEventBus(); } @@ -45,15 +72,18 @@ export class Platform { logger.info("[Processor] OrdersProcessor destroyed"); await this.ordersConsumer.destroy(); - // logger.info("[Processor] TimeframeProcessor destroyed"); - // this.timeframeCron.destroy(); - logger.info("[Processor] CandlesProcessor destroyed"); this.candlesConsumer.destroy(); logger.info("[Processor] TradesProcessor destroyed"); this.tradesConsumer.destroy(); + logger.info("[Processor] OrderbookProcessor destroyed"); + this.orderbookConsumer.destroy(); + + logger.info("[Processor] TickerProcessor destroyed"); + this.tickerConsumer.destroy(); + this.unsubscribeFromEventBus(); } @@ -108,13 +138,31 @@ export class Platform { * - When an exchange account was updated → Resubcribe to orders channel with new credentials */ private subscribeToEventBus() { - const onBotStarted = async (bot: TBot) => { - await this.candlesConsumer.addBot(bot); - await this.tradesConsumer.addBot(bot); + const onBotStarted = async (bot: TBotWithExchangeAccount) => { + const { strategyFn } = await findStrategy(bot.template); + const { watchTrades, watchOrderbook, watchTicker, watchCandles } = getWatchers(strategyFn, bot); + + if (watchCandles.length > 0) await this.candlesConsumer.addBot(bot); + if (watchTrades.length > 0) await this.tradesConsumer.addBot(bot); + if (watchOrderbook.length > 0) await this.orderbookConsumer.addBot(bot); + if (watchTicker.length > 0) await this.tickerConsumer.addBot(bot); + + this.enabledBots = await xprisma.bot.custom.findMany({ + where: { enabled: true }, + include: { exchangeAccount: true }, + }); }; - const onBotStopped = async (bot: TBot) => { - await this.candlesConsumer.cleanStaleChannels(); - await this.tradesConsumer.cleanStaleChannels(); + + const onBotStopped = async (bot: TBotWithExchangeAccount) => { + this.enabledBots = await xprisma.bot.custom.findMany({ + where: { enabled: true }, + include: { exchangeAccount: true }, + }); + + await this.candlesConsumer.cleanStaleChannels(this.enabledBots); + await this.tradesConsumer.cleanStaleChannels(this.enabledBots); + await this.orderbookConsumer.cleanStaleChannels(this.enabledBots); + await this.tickerConsumer.cleanStaleChannels(this.enabledBots); }; const addExchangeAccount = async (exchangeAccount: ExchangeAccountWithCredentials) => @@ -145,4 +193,17 @@ export class Platform { eventBus.off("onExchangeAccountUpdated", updateExchangeAccount); }; } + + async handleProcess(event: ProcessingEvent) { + for (const bot of this.enabledBots) { + const { strategyFn } = await findStrategy(bot.template); + + if (shouldRunStrategy(strategyFn, bot, event.type)) { + processingQueue.push({ + ...event, + bot, + }); + } + } + } } diff --git a/packages/bot/src/queue/queue.ts b/packages/bot/src/queue/queue.ts index 17a21180..c71710bb 100644 --- a/packages/bot/src/queue/queue.ts +++ b/packages/bot/src/queue/queue.ts @@ -2,9 +2,9 @@ import { cargoQueue, QueueObject } from "async"; import type { TBot } from "@opentrader/db"; import { BotProcessing } from "@opentrader/processing"; import { logger } from "@opentrader/logger"; -import { ProcessingEvent } from "./types.js"; +import { QueueEvent } from "./types.js"; -async function queueHandler(tasks: ProcessingEvent[]) { +async function queueHandler(tasks: QueueEvent[]) { const event = tasks[tasks.length - 1]; // getting last task from the queue if (tasks.length > 1) { @@ -15,39 +15,61 @@ async function queueHandler(tasks: ProcessingEvent[]) { const botProcessor = new BotProcessing(event.bot); - if (event.type === "onOrderFilled") { - await botProcessor.process({ - triggerEventType: event.type, - }); - } else if (event.type === "onCandleClosed") { - await botProcessor.process({ - triggerEventType: event.type, - market: { - candle: event.candle, - candles: event.candles, - }, - }); - } else if (event.type === "onPublicTrade") { - await botProcessor.process({ - triggerEventType: event.type, - market: { - trade: event.trade, - candles: [], - }, - }); - } else { - throw new Error(`❗ Unknown event type: ${event}`); + switch (event.type) { + case "onOrderFilled": + await botProcessor.process({ + triggerEventType: event.type, + }); + break; + case "onCandleClosed": + await botProcessor.process({ + triggerEventType: event.type, + market: { + candle: event.candle, + candles: event.candles, + }, + }); + break; + case "onPublicTrade": + await botProcessor.process({ + triggerEventType: event.type, + market: { + trade: event.trade, + candles: [], + }, + }); + break; + case "onOrderbookChange": + await botProcessor.process({ + triggerEventType: event.type, + market: { + orderbook: event.orderbook, + candles: [], + }, + }); + break; + case "onTickerChange": + await botProcessor.process({ + triggerEventType: event.type, + market: { + ticker: event.ticker, + candles: [], + }, + }); + break; + default: + throw new Error(`❗ Unknown event type: ${event}`); } await botProcessor.placePendingOrders(); } -const createQueue = () => cargoQueue(queueHandler); +const createQueue = () => cargoQueue(queueHandler); class Queue { - queues: Record> = {}; + queues: Record> = {}; - push(event: ProcessingEvent) { + push(event: QueueEvent) { // Create a queue bot if it doesn't exist if (!this.queues[event.bot.id]) { this.queues[event.bot.id] = createQueue(); diff --git a/packages/bot/src/queue/types.ts b/packages/bot/src/queue/types.ts index 82f382ca..64b08bbb 100644 --- a/packages/bot/src/queue/types.ts +++ b/packages/bot/src/queue/types.ts @@ -1,23 +1,37 @@ import type { TBot } from "@opentrader/db"; -import { ICandlestick, ITrade, StrategyTriggerEventType } from "@opentrader/types"; +import { ICandlestick, IOrderbook, ITicker, ITrade, StrategyTriggerEventType } from "@opentrader/types"; export type OrderFilledEvent = { type: typeof StrategyTriggerEventType.onOrderFilled; - bot: TBot; orderId: number; }; export type CandleClosedEvent = { type: typeof StrategyTriggerEventType.onCandleClosed; - bot: TBot; candle: ICandlestick; // current closed candle candles: ICandlestick[]; // previous candles history }; export type PublicTradeEvent = { type: typeof StrategyTriggerEventType.onPublicTrade; - bot: TBot; trade: ITrade; }; -export type ProcessingEvent = OrderFilledEvent | CandleClosedEvent | PublicTradeEvent; +export type OrderbookChangeEvent = { + type: typeof StrategyTriggerEventType.onOrderbookChange; + orderbook: IOrderbook; +}; + +export type TickerChangeEvent = { + type: typeof StrategyTriggerEventType.onTickerChange; + ticker: ITicker; +}; + +export type ProcessingEvent = + | OrderFilledEvent + | CandleClosedEvent + | PublicTradeEvent + | OrderbookChangeEvent + | TickerChangeEvent; + +export type QueueEvent = ProcessingEvent & { bot: TBot }; diff --git a/packages/daemon/src/platform.ts b/packages/daemon/src/platform.ts index 49527594..63189a6d 100644 --- a/packages/daemon/src/platform.ts +++ b/packages/daemon/src/platform.ts @@ -10,6 +10,7 @@ export async function bootstrapPlatform() { where: { label: "default", }, + include: { exchangeAccount: true }, }); logger.info(`Found bot: ${bot ? bot.label : "none"}`); diff --git a/packages/db/src/types/bot/bot-with-exchange-account.ts b/packages/db/src/types/bot/bot-with-exchange-account.ts new file mode 100644 index 00000000..2735500c --- /dev/null +++ b/packages/db/src/types/bot/bot-with-exchange-account.ts @@ -0,0 +1,8 @@ +import { Prisma } from "@prisma/client"; +import { TBot } from "../bot/bot.schema.js"; + +const exchangeAccount = Prisma.validator()({}); + +export type TBotWithExchangeAccount = TBot & { + exchangeAccount: Prisma.ExchangeAccountGetPayload; +}; diff --git a/packages/db/src/types/bot/index.ts b/packages/db/src/types/bot/index.ts index c5c95ee4..5856487f 100644 --- a/packages/db/src/types/bot/index.ts +++ b/packages/db/src/types/bot/index.ts @@ -1,3 +1,4 @@ export * from "./bot-settings.schema.js"; export * from "./bot.schema.js"; export * from "./bot-state.schema.js"; +export * from "./bot-with-exchange-account.js"; diff --git a/packages/event-bus/src/index.ts b/packages/event-bus/src/index.ts index e02f9ba4..720b7831 100644 --- a/packages/event-bus/src/index.ts +++ b/packages/event-bus/src/index.ts @@ -55,9 +55,8 @@ class EventBus extends EventEmitter { botStarted(botId: number) { xprisma.bot .findUniqueOrThrow({ - where: { - id: botId, - }, + where: { id: botId }, + include: { exchangeAccount: true }, }) .then((bot) => { this.emit(EVENT.onBotStarted, bot); @@ -70,9 +69,8 @@ class EventBus extends EventEmitter { botStopped(botId: number) { xprisma.bot .findUniqueOrThrow({ - where: { - id: botId, - }, + where: { id: botId }, + include: { exchangeAccount: true }, }) .then((bot) => { this.emit(EVENT.onBotStopped, bot); diff --git a/packages/processing/package.json b/packages/processing/package.json index 077ae39b..ba4510d7 100644 --- a/packages/processing/package.json +++ b/packages/processing/package.json @@ -32,6 +32,7 @@ "@opentrader/db": "workspace:*", "@opentrader/exchanges": "workspace:*", "@opentrader/logger": "workspace:*", + "@opentrader/tools": "workspace:*", "@prisma/client": "5.17.0", "ccxt": "4.3.59" } diff --git a/packages/processing/src/bot/bot.processing.ts b/packages/processing/src/bot/bot.processing.ts index 1318268b..c8d37d77 100644 --- a/packages/processing/src/bot/bot.processing.ts +++ b/packages/processing/src/bot/bot.processing.ts @@ -79,7 +79,7 @@ export class BotProcessing { } else if (command === "stop") { await processor.stop(botState); } else if (command === "process") { - await processor.process(botState, market); + await processor.process(botState, triggerEventType, market); } } catch (err) { await xprisma.bot.setProcessing(false, this.bot.id); diff --git a/packages/processing/src/index.ts b/packages/processing/src/index.ts index 9ccf0f57..7b0997d3 100644 --- a/packages/processing/src/index.ts +++ b/packages/processing/src/index.ts @@ -1,4 +1,7 @@ export * from "./bot/index.js"; +export * from "./strategy/runPolicy.js"; +export * from "./strategy/getWatchers.js"; +export * from "./strategy/getTimeframe.js"; export * from "./smart-trade/index.js"; export * from "./exchange-account/index.js"; export * from "./executors/index.js"; diff --git a/packages/processing/src/strategy/getTimeframe.ts b/packages/processing/src/strategy/getTimeframe.ts new file mode 100644 index 00000000..aa88e6cc --- /dev/null +++ b/packages/processing/src/strategy/getTimeframe.ts @@ -0,0 +1,18 @@ +import { BarSize } from "@opentrader/types"; +import { BotTemplate, IBotConfiguration } from "@opentrader/bot-processor"; + +/** + * Either return the strategy timeframe or the bot timeframe. + * If both are not provided, return `undefined`. + */ +export function getTimeframe(strategyFn: BotTemplate, botConfig: T): BarSize | null { + let strategyTimeframe: BarSize | null | undefined; + + if (typeof strategyFn.timeframe === "function") { + strategyTimeframe = strategyFn.timeframe(botConfig); + } else { + strategyTimeframe = strategyFn.timeframe; + } + + return strategyTimeframe || botConfig.timeframe || null; +} diff --git a/packages/processing/src/strategy/getWatchers.ts b/packages/processing/src/strategy/getWatchers.ts new file mode 100644 index 00000000..f9f49645 --- /dev/null +++ b/packages/processing/src/strategy/getWatchers.ts @@ -0,0 +1,61 @@ +import { TBotWithExchangeAccount } from "@opentrader/db"; +import { isValidSymbol } from "@opentrader/tools"; +import { BotTemplate, IBotConfiguration, WatchCondition, Watcher } from "@opentrader/bot-processor"; +import { BarSize, ExchangeCode } from "@opentrader/types"; + +/** + * Extracts symbols from the watch condition. + */ +const extractSymbols = ( + watchCondition: WatchCondition | undefined, + bot: TBotWithExchangeAccount, +): string[] => { + let symbols: string | string[] | undefined; + + if (typeof watchCondition === "function") { + symbols = watchCondition({ + id: bot.id, + baseCurrency: bot.baseCurrency, + quoteCurrency: bot.quoteCurrency, + settings: bot.settings, + timeframe: bot.timeframe as BarSize | null, + exchangeCode: bot.exchangeAccount.exchangeCode as ExchangeCode, + }); + } else { + symbols = watchCondition; + } + + symbols = Array.isArray(symbols) ? symbols : typeof symbols === "string" ? [symbols] : []; + + return symbols.map((symbol) => { + const isSymbol = isValidSymbol(symbol); + + // If the symbol doesn't contain exchange code, add the default exchange code from the bot config + // Example: BTC/USDT -> OKX:BTC/USDT + return isSymbol ? `${bot.exchangeAccount.exchangeCode}:${symbol}` : symbol; + }); +}; + +/** + * Retrieve watchers configurations from the strategy. + * If the watcher is a function, it will be invoked and the result will be returned. + */ +export function getWatchers(strategyFn: BotTemplate, bot: TBotWithExchangeAccount): Record { + if (!strategyFn.watchers) { + console.warn(`Strategy ${strategyFn.name} does not contain any watcher`); + + return { + [Watcher.watchTrades]: [], + [Watcher.watchOrderbook]: [], + [Watcher.watchTicker]: [], + [Watcher.watchCandles]: [], + }; + } + + return { + [Watcher.watchTrades]: extractSymbols(strategyFn.watchers[Watcher.watchTrades], bot), + [Watcher.watchOrderbook]: extractSymbols(strategyFn.watchers[Watcher.watchOrderbook], bot), + [Watcher.watchTicker]: extractSymbols(strategyFn.watchers[Watcher.watchTicker], bot), + [Watcher.watchCandles]: extractSymbols(strategyFn.watchers[Watcher.watchCandles], bot), + }; +} diff --git a/packages/processing/src/strategy/runPolicy.ts b/packages/processing/src/strategy/runPolicy.ts new file mode 100644 index 00000000..395b628d --- /dev/null +++ b/packages/processing/src/strategy/runPolicy.ts @@ -0,0 +1,39 @@ +import { StrategyTriggerEventType } from "@opentrader/types"; +import { BotTemplate, IBotConfiguration } from "@opentrader/bot-processor"; + +/** + * Determines if the strategy should run based on the run policy and event type. + * + * This function checks the `runPolicy` of the strategy and evaluates whether the strategy + * should run for a given `StrategyTriggerEventType`. If the `runPolicy` for the event type + * is a function, it will invoke the function with the current bot configuration (`botConfig`). + * + * @param strategyFn - The strategy function. + * @param botConfig - The bot configuration. + * @param eventType - The market event type. If not provided, it indicates that either start or stop action is performed. + */ +export function shouldRunStrategy( + strategyFn: BotTemplate, + botConfig: T, + eventType?: StrategyTriggerEventType, +): boolean { + if (!strategyFn.runPolicy) { + console.warn(`Strategy ${strategyFn.name} does not have a run policy`); + + return false; + } + + if (!eventType) { + // Always execute the strategy template when start/stop bot actions are performed + return true; + } + + const { runPolicy } = strategyFn; + const eventPolicy = runPolicy[eventType]; + + if (typeof eventPolicy === "function") { + return eventPolicy(botConfig) === true; + } + + return eventPolicy === true; +} diff --git a/packages/types/src/strategy-runner/context.ts b/packages/types/src/strategy-runner/context.ts index aaa9666f..44c0cb80 100644 --- a/packages/types/src/strategy-runner/context.ts +++ b/packages/types/src/strategy-runner/context.ts @@ -1,4 +1,4 @@ -import type { ICandlestick, ITrade } from "../exchange/index.js"; +import type { ICandlestick, IOrderbook, ITicker, ITrade } from "../exchange/index.js"; /** * Action that strategy should perform: @@ -21,6 +21,8 @@ export const StrategyTriggerEventType = { onOrderFilled: "onOrderFilled", onCandleClosed: "onCandleClosed", onPublicTrade: "onPublicTrade", + onOrderbookChange: "onOrderbookChange", + onTickerChange: "onTickerChange", } as const; export type StrategyTriggerEventType = (typeof StrategyTriggerEventType)[keyof typeof StrategyTriggerEventType]; @@ -46,4 +48,6 @@ export interface MarketData { * Last public trade */ trade?: ITrade; + orderbook?: IOrderbook; + ticker?: ITicker; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de4dc833..23f39c26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.27.7 execa: specifier: ^9.3.0 - version: 9.3.0 + version: 9.3.1 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -76,7 +76,7 @@ importers: version: 12.1.0 execa: specifier: ^9.3.0 - version: 9.3.0 + version: 9.3.1 express: specifier: ^4.19.2 version: 4.19.2 @@ -125,13 +125,13 @@ importers: version: 6.0.1 ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.5.4)(webpack@5.93.0(esbuild@0.23.0)) + version: 9.5.1(typescript@5.5.4)(webpack@5.93.0) ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) tsup: specifier: ^8.2.4 - version: 8.2.4(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.4.5) + version: 8.2.4(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -238,7 +238,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.15)(terser@5.31.2) + version: 2.0.5(@types/node@20.14.15) packages/bot-processor: dependencies: @@ -321,10 +321,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -468,7 +468,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.15)(terser@5.31.2) + version: 2.0.5(@types/node@20.14.15) packages/exchanges: dependencies: @@ -530,10 +530,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) @@ -629,6 +629,9 @@ importers: '@opentrader/logger': specifier: workspace:* version: link:../logger + '@opentrader/tools': + specifier: workspace:* + version: link:../tools '@prisma/client': specifier: 5.17.0 version: 5.17.0(prisma@5.17.0) @@ -659,10 +662,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -696,13 +699,13 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) tsup: specifier: ^8.2.4 - version: 8.2.4(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.4.5) + version: 8.2.4(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -780,7 +783,7 @@ importers: version: 8.57.0 tsup: specifier: ^8.2.4 - version: 8.2.4(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.4.5) + version: 8.2.4(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -795,40 +798,28 @@ packages: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.24.9': - resolution: {integrity: sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.24.9': - resolution: {integrity: sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.9': - resolution: {integrity: sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A==} + '@babel/compat-data@7.25.2': + resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.24.8': - resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} + '@babel/core@7.25.2': + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} engines: {node: '>=6.9.0'} - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + '@babel/generator@7.25.0': + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} engines: {node: '>=6.9.0'} - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.24.7': - resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + '@babel/helper-compilation-targets@7.25.2': + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.24.7': resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.24.9': - resolution: {integrity: sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==} + '@babel/helper-module-transforms@7.25.2': + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -841,10 +832,6 @@ packages: resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} - '@babel/helper-split-export-declaration@7.24.7': - resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.24.8': resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} @@ -857,16 +844,16 @@ packages: resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.24.8': - resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==} + '@babel/helpers@7.25.0': + resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.24.8': - resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} + '@babel/parser@7.25.3': + resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} engines: {node: '>=6.0.0'} hasBin: true @@ -885,6 +872,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.24.7': + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -931,6 +930,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-top-level-await@7.14.5': resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -943,20 +948,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.24.8': - resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} + '@babel/runtime@7.25.0': + resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} engines: {node: '>=6.9.0'} - '@babel/template@7.24.7': - resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.8': - resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==} + '@babel/traverse@7.25.3': + resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.24.9': - resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -1471,12 +1476,12 @@ packages: prisma: optional: true - '@prisma/debug@5.16.2': - resolution: {integrity: sha512-ItzB4nR4O8eLzuJiuP3WwUJfoIvewMHqpGCad+64gvThcKEVOtaUza9AEJo2DPqAOa/AWkFyK54oM4WwHeew+A==} - '@prisma/debug@5.17.0': resolution: {integrity: sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==} + '@prisma/debug@5.18.0': + resolution: {integrity: sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==} + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': resolution: {integrity: sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==} @@ -1486,89 +1491,89 @@ packages: '@prisma/fetch-engine@5.17.0': resolution: {integrity: sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==} - '@prisma/generator-helper@5.16.2': - resolution: {integrity: sha512-ajdZ5OTKuLEYB7KQQPNYGPr4s56wD4+vH6KqIGiyQVw8ze8dPaxUB3MLzf0vCq2yYq6CZynSExf4InFXYBliTA==} + '@prisma/generator-helper@5.18.0': + resolution: {integrity: sha512-3ffmrd9KE8ssg/fwyvfwMxrDAunLF8DLFjfwYnDRE7VaNIhkUVZwB77jAwpMCtukvCsAp14WGWu4itvLMzH3GQ==} '@prisma/get-platform@5.17.0': resolution: {integrity: sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==} - '@rollup/rollup-android-arm-eabi@4.19.0': - resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==} + '@rollup/rollup-android-arm-eabi@4.20.0': + resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.19.0': - resolution: {integrity: sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==} + '@rollup/rollup-android-arm64@4.20.0': + resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.19.0': - resolution: {integrity: sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==} + '@rollup/rollup-darwin-arm64@4.20.0': + resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.19.0': - resolution: {integrity: sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==} + '@rollup/rollup-darwin-x64@4.20.0': + resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.19.0': - resolution: {integrity: sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==} + '@rollup/rollup-linux-arm-gnueabihf@4.20.0': + resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.19.0': - resolution: {integrity: sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==} + '@rollup/rollup-linux-arm-musleabihf@4.20.0': + resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.19.0': - resolution: {integrity: sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==} + '@rollup/rollup-linux-arm64-gnu@4.20.0': + resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.19.0': - resolution: {integrity: sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==} + '@rollup/rollup-linux-arm64-musl@4.20.0': + resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': - resolution: {integrity: sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': + resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.19.0': - resolution: {integrity: sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==} + '@rollup/rollup-linux-riscv64-gnu@4.20.0': + resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.19.0': - resolution: {integrity: sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==} + '@rollup/rollup-linux-s390x-gnu@4.20.0': + resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.19.0': - resolution: {integrity: sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==} + '@rollup/rollup-linux-x64-gnu@4.20.0': + resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.19.0': - resolution: {integrity: sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==} + '@rollup/rollup-linux-x64-musl@4.20.0': + resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.19.0': - resolution: {integrity: sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==} + '@rollup/rollup-win32-arm64-msvc@4.20.0': + resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.19.0': - resolution: {integrity: sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==} + '@rollup/rollup-win32-ia32-msvc@4.20.0': + resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.19.0': - resolution: {integrity: sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==} + '@rollup/rollup-win32-x64-msvc@4.20.0': + resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} cpu: [x64] os: [win32] @@ -1638,8 +1643,8 @@ packages: '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - '@types/eslint@8.56.10': - resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + '@types/eslint@9.6.0': + resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==} '@types/eslint__js@8.42.3': resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} @@ -1692,9 +1697,6 @@ packages: '@types/node@6.14.13': resolution: {integrity: sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==} - '@types/parse-json@4.0.2': - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} @@ -1719,8 +1721,8 @@ packages: '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - '@types/yargs@17.0.32': - resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} '@typescript-eslint/eslint-plugin@7.18.0': resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} @@ -1974,12 +1976,8 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-macros@3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} - - babel-preset-current-node-syntax@1.0.1: - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} peerDependencies: '@babel/core': ^7.0.0 @@ -2020,8 +2018,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.23.2: - resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2072,8 +2070,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001642: - resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==} + caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} ccxt@4.3.59: resolution: {integrity: sha512-10wSovFgZjTt9R4thW6oFwkuXCBCxH65U2FcINnGXJ2+xf54OZeRP8ECBp+1bcKHdJ0yA6CcEyKm6Tl0DP0ULA==} @@ -2200,10 +2198,6 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} - cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} - create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2233,8 +2227,8 @@ packages: supports-color: optional: true - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2308,8 +2302,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.4.828: - resolution: {integrity: sha512-QOIJiWpQJDHAVO4P58pwb133Cwee0nbvy/MV1CwzZVGpkH1RX33N3vsaWRCpR6bF63AAq366neZrRTu7Qlsbbw==} + electron-to-chromium@1.5.8: + resolution: {integrity: sha512-4Nx0gP2tPNBLTrFxBMHpkQbtn2hidPVr/+/FTtcCiBYTucqc70zRyVZiOLj17Ui3wTO7SQ1/N+hkHYzJjBzt6A==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -2328,8 +2322,8 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.17.0: - resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -2448,8 +2442,8 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.3.0: - resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + execa@9.3.1: + resolution: {integrity: sha512-gdhefCCNy/8tpH/2+ajP9IQc14vXchNdd0weyzSJEFURhRMGncQ+zKFxwjAufIewPEJm9BPOaJnvg2UtlH2gPQ==} engines: {node: ^18.19.0 || >=20.5.0} exit@0.1.2: @@ -2543,8 +2537,8 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} forwarded@0.2.0: @@ -2605,8 +2599,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-tsconfig@4.7.5: - resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2697,8 +2691,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - human-signals@7.0.0: - resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} iconv-lite@0.4.24: @@ -2708,16 +2702,16 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-local@3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} hasBin: true @@ -2743,8 +2737,8 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-core-module@2.14.0: - resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + is-core-module@2.15.0: + resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} engines: {node: '>= 0.4'} is-extglob@2.1.1: @@ -2837,8 +2831,8 @@ packages: resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==} engines: {node: 20 || >=22} - jake@10.9.1: - resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} hasBin: true @@ -3098,8 +3092,8 @@ packages: resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} engines: {node: '>=12'} - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -3218,8 +3212,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3416,8 +3410,8 @@ packages: yaml: optional: true - postcss@8.4.39: - resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} + postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} preferred-pm@3.1.4: @@ -3442,8 +3436,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-ms@9.0.0: - resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} engines: {node: '>=18'} prisma@5.17.0: @@ -3577,8 +3571,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.19.0: - resolution: {integrity: sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==} + rollup@4.20.0: + resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3609,8 +3603,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true @@ -3828,8 +3822,8 @@ packages: uglify-js: optional: true - terser@5.31.2: - resolution: {integrity: sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==} + terser@5.31.6: + resolution: {integrity: sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==} engines: {node: '>=10'} hasBin: true @@ -3850,8 +3844,8 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - tinybench@2.8.0: - resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinypool@1.0.0: resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} @@ -4081,8 +4075,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.3.5: - resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} + vite@5.4.1: + resolution: {integrity: sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -4090,6 +4084,7 @@ packages: less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 @@ -4102,6 +4097,8 @@ packages: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: @@ -4137,8 +4134,8 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.1: - resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} + watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} webidl-conversions@4.0.2: @@ -4220,15 +4217,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yaml@2.4.5: - resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} - engines: {node: '>= 14'} - hasBin: true - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4268,71 +4256,57 @@ snapshots: '@babel/highlight': 7.24.7 picocolors: 1.0.1 - '@babel/compat-data@7.24.9': {} + '@babel/compat-data@7.25.2': {} - '@babel/core@7.24.9': + '@babel/core@7.25.2': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helpers': 7.24.8 - '@babel/parser': 7.24.8 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 + '@babel/generator': 7.25.0 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.0 + '@babel/parser': 7.25.3 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.6 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.24.9': + '@babel/generator@7.25.0': dependencies: - '@babel/types': 7.24.9 + '@babel/types': 7.25.2 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - '@babel/helper-compilation-targets@7.24.8': + '@babel/helper-compilation-targets@7.25.2': dependencies: - '@babel/compat-data': 7.24.9 + '@babel/compat-data': 7.25.2 '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.2 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-function-name@7.24.7': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 - - '@babel/helper-hoist-variables@7.24.7': - dependencies: - '@babel/types': 7.24.9 - '@babel/helper-module-imports@7.24.7': dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.24.9(@babel/core@7.24.9)': + '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.24.7 '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.3 transitivePeerDependencies: - supports-color @@ -4340,25 +4314,21 @@ snapshots: '@babel/helper-simple-access@7.24.7': dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.24.9 - '@babel/helper-string-parser@7.24.8': {} '@babel/helper-validator-identifier@7.24.7': {} '@babel/helper-validator-option@7.24.8': {} - '@babel/helpers@7.24.8': + '@babel/helpers@7.25.0': dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 '@babel/highlight@7.24.7': dependencies: @@ -4367,106 +4337,118 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.1 - '@babel/parser@7.24.8': + '@babel/parser@7.25.3': + dependencies: + '@babel/types': 7.25.2 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2)': dependencies: - '@babel/types': 7.24.9 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.9)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.9)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.24.9)': + '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/runtime@7.24.8': + '@babel/runtime@7.25.0': dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.24.7': + '@babel/template@7.25.0': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 - '@babel/traverse@7.24.8': + '@babel/traverse@7.25.3': dependencies: '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 - debug: 4.3.5 + '@babel/generator': 7.25.0 + '@babel/parser': 7.25.3 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + debug: 4.3.6 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.24.9': + '@babel/types@7.25.2': dependencies: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 @@ -4476,7 +4458,7 @@ snapshots: '@changesets/apply-release-plan@7.0.4': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/config': 3.0.2 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.0 @@ -4489,17 +4471,17 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.2 + semver: 7.6.3 '@changesets/assemble-release-plan@6.0.3': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.1 '@changesets/should-skip-package': 0.1.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.2 + semver: 7.6.3 '@changesets/changelog-git@0.2.0': dependencies: @@ -4507,7 +4489,7 @@ snapshots: '@changesets/cli@2.27.7': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/apply-release-plan': 7.0.4 '@changesets/assemble-release-plan': 6.0.3 '@changesets/changelog-git': 0.2.0 @@ -4536,7 +4518,7 @@ snapshots: p-limit: 2.3.0 preferred-pm: 3.1.4 resolve-from: 5.0.0 - semver: 7.6.2 + semver: 7.6.3 spawndamnit: 2.0.0 term-size: 2.2.1 @@ -4560,11 +4542,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 7.6.2 + semver: 7.6.3 '@changesets/get-release-plan@4.0.3': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/assemble-release-plan': 6.0.3 '@changesets/config': 3.0.2 '@changesets/pre': 2.0.0 @@ -4576,7 +4558,7 @@ snapshots: '@changesets/git@3.0.0': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -4595,7 +4577,7 @@ snapshots: '@changesets/pre@2.0.0': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -4603,7 +4585,7 @@ snapshots: '@changesets/read@0.6.0': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/git': 3.0.0 '@changesets/logger': 0.1.0 '@changesets/parse': 0.4.0 @@ -4614,7 +4596,7 @@ snapshots: '@changesets/should-skip-package@0.1.0': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -4624,7 +4606,7 @@ snapshots: '@changesets/write@0.3.1': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/types': 6.0.0 fs-extra: 7.0.1 human-id: 1.0.2 @@ -4785,10 +4767,10 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.5 + debug: 4.3.6 espree: 9.6.1 globals: 13.24.0 - ignore: 5.3.1 + ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -4803,7 +4785,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.5 + debug: 4.3.6 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4840,7 +4822,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4))': + '@jest/core@29.7.0(ts-node@10.9.2)': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -4854,7 +4836,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4966,7 +4948,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -4990,7 +4972,7 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 '@types/node': 20.14.15 - '@types/yargs': 17.0.32 + '@types/yargs': 17.0.33 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.5': @@ -5022,14 +5004,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.0 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -5052,13 +5034,13 @@ snapshots: optional: true '@prisma/client@5.17.0(prisma@5.17.0)': - optionalDependencies: + dependencies: prisma: 5.17.0 - '@prisma/debug@5.16.2': {} - '@prisma/debug@5.17.0': {} + '@prisma/debug@5.18.0': {} + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': {} '@prisma/engines@5.17.0': @@ -5074,60 +5056,60 @@ snapshots: '@prisma/engines-version': 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 '@prisma/get-platform': 5.17.0 - '@prisma/generator-helper@5.16.2': + '@prisma/generator-helper@5.18.0': dependencies: - '@prisma/debug': 5.16.2 + '@prisma/debug': 5.18.0 '@prisma/get-platform@5.17.0': dependencies: '@prisma/debug': 5.17.0 - '@rollup/rollup-android-arm-eabi@4.19.0': + '@rollup/rollup-android-arm-eabi@4.20.0': optional: true - '@rollup/rollup-android-arm64@4.19.0': + '@rollup/rollup-android-arm64@4.20.0': optional: true - '@rollup/rollup-darwin-arm64@4.19.0': + '@rollup/rollup-darwin-arm64@4.20.0': optional: true - '@rollup/rollup-darwin-x64@4.19.0': + '@rollup/rollup-darwin-x64@4.20.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.19.0': + '@rollup/rollup-linux-arm-gnueabihf@4.20.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.19.0': + '@rollup/rollup-linux-arm-musleabihf@4.20.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.19.0': + '@rollup/rollup-linux-arm64-gnu@4.20.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.19.0': + '@rollup/rollup-linux-arm64-musl@4.20.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.19.0': + '@rollup/rollup-linux-riscv64-gnu@4.20.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.19.0': + '@rollup/rollup-linux-s390x-gnu@4.20.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.19.0': + '@rollup/rollup-linux-x64-gnu@4.20.0': optional: true - '@rollup/rollup-linux-x64-musl@4.19.0': + '@rollup/rollup-linux-x64-musl@4.20.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.19.0': + '@rollup/rollup-win32-arm64-msvc@4.20.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.19.0': + '@rollup/rollup-win32-ia32-msvc@4.20.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.19.0': + '@rollup/rollup-win32-x64-msvc@4.20.0': optional: true '@sec-ant/readable-stream@0.4.1': {} @@ -5162,24 +5144,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.24.9 + '@babel/types': 7.25.2 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.24.9 + '@babel/types': 7.25.2 '@types/big.js@6.2.2': {} @@ -5198,17 +5180,17 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: - '@types/eslint': 8.56.10 + '@types/eslint': 9.6.0 '@types/estree': 1.0.5 - '@types/eslint@8.56.10': + '@types/eslint@9.6.0': dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 '@types/eslint__js@8.42.3': dependencies: - '@types/eslint': 8.56.10 + '@types/eslint': 9.6.0 '@types/estree@1.0.5': {} @@ -5263,9 +5245,6 @@ snapshots: '@types/node@6.14.13': {} - '@types/parse-json@4.0.2': - optional: true - '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} @@ -5291,11 +5270,11 @@ snapshots: '@types/yargs-parser@21.0.3': {} - '@types/yargs@17.0.32': + '@types/yargs@17.0.33': dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) @@ -5305,10 +5284,9 @@ snapshots: '@typescript-eslint/visitor-keys': 7.18.0 eslint: 8.57.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5319,9 +5297,8 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.5 + debug: 4.3.6 eslint: 8.57.0 - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5335,10 +5312,9 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.5 + debug: 4.3.6 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5349,13 +5325,12 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.5 + debug: 4.3.6 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.2 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5397,7 +5372,7 @@ snapshots: '@vitest/snapshot@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 - magic-string: 0.30.10 + magic-string: 0.30.11 pathe: 1.1.2 '@vitest/spy@2.0.5': @@ -5581,13 +5556,13 @@ snapshots: atomic-sleep@1.0.0: {} - babel-jest@29.7.0(@babel/core@7.24.9): + babel-jest@29.7.0(@babel/core@7.25.2): dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.24.9) + babel-preset-jest: 29.6.3(@babel/core@7.25.2) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -5606,39 +5581,35 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 - babel-plugin-macros@3.1.0: - dependencies: - '@babel/runtime': 7.24.8 - cosmiconfig: 7.1.0 - resolve: 1.22.8 - optional: true - - babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.9): - dependencies: - '@babel/core': 7.24.9 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.9) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.9) - - babel-preset-jest@29.6.3(@babel/core@7.24.9): - dependencies: - '@babel/core': 7.24.9 + babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + + babel-preset-jest@29.6.3(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.9) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.2) balanced-match@1.0.2: {} @@ -5682,12 +5653,12 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.23.2: + browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001642 - electron-to-chromium: 1.4.828 - node-releases: 2.0.14 - update-browserslist-db: 1.1.0(browserslist@4.23.2) + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.5.8 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) bs-logger@0.2.6: dependencies: @@ -5729,7 +5700,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001642: {} + caniuse-lite@1.0.30001651: {} ccxt@4.3.59: dependencies: @@ -5842,22 +5813,13 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@7.1.0: - dependencies: - '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - optional: true - - create-jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): + create-jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -5891,13 +5853,11 @@ snapshots: dependencies: ms: 2.0.0 - debug@4.3.5: + debug@4.3.6: dependencies: ms: 2.1.2 - dedent@1.5.3(babel-plugin-macros@3.1.0): - optionalDependencies: - babel-plugin-macros: 3.1.0 + dedent@1.5.3: {} deep-eql@5.0.2: {} @@ -5937,9 +5897,9 @@ snapshots: ejs@3.1.10: dependencies: - jake: 10.9.1 + jake: 10.9.2 - electron-to-chromium@1.4.828: {} + electron-to-chromium@1.5.8: {} emittery@0.13.1: {} @@ -5953,7 +5913,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.17.0: + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 @@ -6063,7 +6023,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5 + debug: 4.3.6 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -6077,7 +6037,7 @@ snapshots: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -6149,17 +6109,17 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.3.0: + execa@9.3.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 cross-spawn: 7.0.3 figures: 6.1.0 get-stream: 9.0.1 - human-signals: 7.0.0 + human-signals: 8.0.0 is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 5.3.0 - pretty-ms: 9.0.0 + pretty-ms: 9.1.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.1 @@ -6303,7 +6263,7 @@ snapshots: flatted@3.3.1: {} - foreground-child@3.2.1: + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 @@ -6356,7 +6316,7 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-tsconfig@4.7.5: + get-tsconfig@4.7.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -6372,7 +6332,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -6381,7 +6341,7 @@ snapshots: glob@11.0.0: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 4.0.1 minimatch: 10.0.1 minipass: 7.1.2 @@ -6408,7 +6368,7 @@ snapshots: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.1 + ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 @@ -6454,7 +6414,7 @@ snapshots: human-signals@5.0.0: {} - human-signals@7.0.0: {} + human-signals@8.0.0: {} iconv-lite@0.4.24: dependencies: @@ -6462,14 +6422,14 @@ snapshots: ieee754@1.2.1: {} - ignore@5.3.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-local@3.1.0: + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 @@ -6491,7 +6451,7 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-core-module@2.14.0: + is-core-module@2.15.0: dependencies: hasown: 2.0.2 @@ -6533,8 +6493,8 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.24.9 - '@babel/parser': 7.24.8 + '@babel/core': 7.25.2 + '@babel/parser': 7.25.3 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -6543,11 +6503,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.24.9 - '@babel/parser': 7.24.8 + '@babel/core': 7.25.2 + '@babel/parser': 7.25.3 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -6559,7 +6519,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.5 + debug: 4.3.6 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -6582,7 +6542,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jake@10.9.1: + jake@10.9.2: dependencies: async: 3.2.5 chalk: 4.1.2 @@ -6595,7 +6555,7 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@29.7.0(babel-plugin-macros@3.1.0): + jest-circus@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -6604,7 +6564,7 @@ snapshots: '@types/node': 20.14.15 chalk: 4.1.2 co: 4.6.0 - dedent: 1.5.3(babel-plugin-macros@3.1.0) + dedent: 1.5.3 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -6621,16 +6581,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): + jest-cli@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + create-jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6640,18 +6600,19 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): + jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.9) + '@types/node': 20.14.15 + babel-jest: 29.7.0(@babel/core@7.25.2) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-circus: 29.7.0 jest-environment-node: 29.7.0 jest-get-type: 29.6.3 jest-regex-util: 29.6.3 @@ -6664,8 +6625,6 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.14.15 ts-node: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros @@ -6748,7 +6707,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: + dependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -6827,15 +6786,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.24.9 - '@babel/generator': 7.24.9 - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.9) - '@babel/types': 7.24.9 + '@babel/core': 7.25.2 + '@babel/generator': 7.25.0 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.2) + '@babel/types': 7.25.2 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.9) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.2) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -6846,7 +6805,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -6892,12 +6851,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): + jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7002,13 +6961,13 @@ snapshots: luxon@3.4.4: {} - magic-string@0.30.10: + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 make-dir@4.0.0: dependencies: - semver: 7.6.2 + semver: 7.6.3 make-error@1.3.6: {} @@ -7093,7 +7052,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.14: {} + node-releases@2.0.18: {} normalize-path@3.0.0: {} @@ -7263,15 +7222,11 @@ snapshots: dependencies: find-up: 4.1.0 - postcss-load-config@6.0.1(postcss@8.4.39)(tsx@4.17.0)(yaml@2.4.5): + postcss-load-config@6.0.1: dependencies: lilconfig: 3.1.2 - optionalDependencies: - postcss: 8.4.39 - tsx: 4.17.0 - yaml: 2.4.5 - postcss@8.4.39: + postcss@8.4.41: dependencies: nanoid: 3.3.7 picocolors: 1.0.1 @@ -7296,7 +7251,7 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-ms@9.0.0: + pretty-ms@9.1.0: dependencies: parse-ms: 4.0.0 @@ -7401,7 +7356,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.14.0 + is-core-module: 2.15.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -7416,26 +7371,26 @@ snapshots: glob: 11.0.0 package-json-from-dist: 1.0.0 - rollup@4.19.0: + rollup@4.20.0: dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.19.0 - '@rollup/rollup-android-arm64': 4.19.0 - '@rollup/rollup-darwin-arm64': 4.19.0 - '@rollup/rollup-darwin-x64': 4.19.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.19.0 - '@rollup/rollup-linux-arm-musleabihf': 4.19.0 - '@rollup/rollup-linux-arm64-gnu': 4.19.0 - '@rollup/rollup-linux-arm64-musl': 4.19.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.19.0 - '@rollup/rollup-linux-riscv64-gnu': 4.19.0 - '@rollup/rollup-linux-s390x-gnu': 4.19.0 - '@rollup/rollup-linux-x64-gnu': 4.19.0 - '@rollup/rollup-linux-x64-musl': 4.19.0 - '@rollup/rollup-win32-arm64-msvc': 4.19.0 - '@rollup/rollup-win32-ia32-msvc': 4.19.0 - '@rollup/rollup-win32-x64-msvc': 4.19.0 + '@rollup/rollup-android-arm-eabi': 4.20.0 + '@rollup/rollup-android-arm64': 4.20.0 + '@rollup/rollup-darwin-arm64': 4.20.0 + '@rollup/rollup-darwin-x64': 4.20.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 + '@rollup/rollup-linux-arm-musleabihf': 4.20.0 + '@rollup/rollup-linux-arm64-gnu': 4.20.0 + '@rollup/rollup-linux-arm64-musl': 4.20.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 + '@rollup/rollup-linux-riscv64-gnu': 4.20.0 + '@rollup/rollup-linux-s390x-gnu': 4.20.0 + '@rollup/rollup-linux-x64-gnu': 4.20.0 + '@rollup/rollup-linux-x64-musl': 4.20.0 + '@rollup/rollup-win32-arm64-msvc': 4.20.0 + '@rollup/rollup-win32-ia32-msvc': 4.20.0 + '@rollup/rollup-win32-x64-msvc': 4.20.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -7460,7 +7415,7 @@ snapshots: semver@6.3.1: {} - semver@7.6.2: {} + semver@7.6.3: {} send@0.18.0: dependencies: @@ -7678,18 +7633,17 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(esbuild@0.23.0)(webpack@5.93.0(esbuild@0.23.0)): + terser-webpack-plugin@5.3.10(esbuild@0.23.0)(webpack@5.93.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 + esbuild: 0.23.0 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 - terser: 5.31.2 + terser: 5.31.6 webpack: 5.93.0(esbuild@0.23.0) - optionalDependencies: - esbuild: 0.23.0 - terser@5.31.2: + terser@5.31.6: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.12.1 @@ -7716,7 +7670,7 @@ snapshots: dependencies: real-require: 0.2.0 - tinybench@2.8.0: {} + tinybench@2.9.0: {} tinypool@1.0.0: {} @@ -7750,31 +7704,28 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.4(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4): + ts-jest@29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4): dependencies: + '@babel/core': 7.25.2 bs-logger: 0.2.6 ejs: 3.1.10 + esbuild: 0.23.0 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.14.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.6.2 + semver: 7.6.3 typescript: 5.5.4 yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.24.9 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.9) - ts-loader@9.5.1(typescript@5.5.4)(webpack@5.93.0(esbuild@0.23.0)): + ts-loader@9.5.1(typescript@5.5.4)(webpack@5.93.0): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.17.0 + enhanced-resolve: 5.17.1 micromatch: 4.0.7 - semver: 7.6.2 + semver: 7.6.3 source-map: 0.7.4 typescript: 5.5.4 webpack: 5.93.0(esbuild@0.23.0) @@ -7799,26 +7750,24 @@ snapshots: tslib@2.6.3: {} - tsup@8.2.4(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.4.5): + tsup@8.2.4(typescript@5.5.4): dependencies: bundle-require: 5.0.0(esbuild@0.23.0) cac: 6.7.14 chokidar: 3.6.0 consola: 3.2.3 - debug: 4.3.5 + debug: 4.3.6 esbuild: 0.23.0 execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 picocolors: 1.0.1 - postcss-load-config: 6.0.1(postcss@8.4.39)(tsx@4.17.0)(yaml@2.4.5) + postcss-load-config: 6.0.1 resolve-from: 5.0.0 - rollup: 4.19.0 + rollup: 4.20.0 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.4.39 typescript: 5.5.4 transitivePeerDependencies: - jiti @@ -7829,7 +7778,7 @@ snapshots: tsx@4.17.0: dependencies: esbuild: 0.23.0 - get-tsconfig: 4.7.5 + get-tsconfig: 4.7.6 optionalDependencies: fsevents: 2.3.3 @@ -7877,11 +7826,10 @@ snapshots: typescript-eslint@7.18.0(eslint@8.57.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -7894,9 +7842,9 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.1.0(browserslist@4.23.2): + update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: - browserslist: 4.23.2 + browserslist: 4.23.3 escalade: 3.1.2 picocolors: 1.0.1 @@ -7916,36 +7864,37 @@ snapshots: vary@1.1.2: {} - vite-node@2.0.5(@types/node@20.14.15)(terser@5.31.2): + vite-node@2.0.5(@types/node@20.14.15): dependencies: cac: 6.7.14 - debug: 4.3.5 + debug: 4.3.6 pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.3.5(@types/node@20.14.15)(terser@5.31.2) + vite: 5.4.1(@types/node@20.14.15) transitivePeerDependencies: - '@types/node' - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color - terser - vite@5.3.5(@types/node@20.14.15)(terser@5.31.2): + vite@5.4.1(@types/node@20.14.15): dependencies: + '@types/node': 20.14.15 esbuild: 0.21.5 - postcss: 8.4.39 - rollup: 4.19.0 + postcss: 8.4.41 + rollup: 4.20.0 optionalDependencies: - '@types/node': 20.14.15 fsevents: 2.3.3 - terser: 5.31.2 - vitest@2.0.5(@types/node@20.14.15)(terser@5.31.2): + vitest@2.0.5(@types/node@20.14.15): dependencies: '@ampproject/remapping': 2.3.0 + '@types/node': 20.14.15 '@vitest/expect': 2.0.5 '@vitest/pretty-format': 2.0.5 '@vitest/runner': 2.0.5 @@ -7953,23 +7902,22 @@ snapshots: '@vitest/spy': 2.0.5 '@vitest/utils': 2.0.5 chai: 5.1.1 - debug: 4.3.5 + debug: 4.3.6 execa: 8.0.1 - magic-string: 0.30.10 + magic-string: 0.30.11 pathe: 1.1.2 std-env: 3.7.0 - tinybench: 2.8.0 + tinybench: 2.9.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.3.5(@types/node@20.14.15)(terser@5.31.2) - vite-node: 2.0.5(@types/node@20.14.15)(terser@5.31.2) + vite: 5.4.1(@types/node@20.14.15) + vite-node: 2.0.5(@types/node@20.14.15) why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.14.15 transitivePeerDependencies: - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color @@ -7979,7 +7927,7 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.4.1: + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -7997,9 +7945,9 @@ snapshots: '@webassemblyjs/wasm-parser': 1.12.1 acorn: 8.12.1 acorn-import-attributes: 1.9.5(acorn@8.12.1) - browserslist: 4.23.2 + browserslist: 4.23.3 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.17.0 + enhanced-resolve: 5.17.1 es-module-lexer: 1.5.4 eslint-scope: 5.1.1 events: 3.3.0 @@ -8011,8 +7959,8 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.23.0)(webpack@5.93.0(esbuild@0.23.0)) - watchpack: 2.4.1 + terser-webpack-plugin: 5.3.10(esbuild@0.23.0)(webpack@5.93.0) + watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: - '@swc/core' @@ -8072,12 +8020,6 @@ snapshots: yallist@3.1.1: {} - yaml@1.10.2: - optional: true - - yaml@2.4.5: - optional: true - yargs-parser@21.1.1: {} yargs@17.7.2: @@ -8098,7 +8040,7 @@ snapshots: zod-prisma-types@3.1.8: dependencies: - '@prisma/generator-helper': 5.16.2 + '@prisma/generator-helper': 5.18.0 code-block-writer: 12.0.0 lodash: 4.17.21 zod: 3.23.8 From 42fae57d3c02e77dee58fdc9408ba5e6a2e9fa61 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 16 Aug 2024 03:29:53 +0100 Subject: [PATCH 08/16] fix(backtesting): fix context --- packages/backtesting/src/backtesting.ts | 15 +---- .../src/exchange/memory-exchange.ts | 66 +++++++++---------- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/packages/backtesting/src/backtesting.ts b/packages/backtesting/src/backtesting.ts index 1deaf80a..04198d55 100644 --- a/packages/backtesting/src/backtesting.ts +++ b/packages/backtesting/src/backtesting.ts @@ -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"; @@ -59,11 +54,7 @@ export class Backtesting> { 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(); @@ -78,7 +69,7 @@ export class Backtesting> { // 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), }); diff --git a/packages/backtesting/src/exchange/memory-exchange.ts b/packages/backtesting/src/exchange/memory-exchange.ts index c7a85eec..5dd8cb5d 100644 --- a/packages/backtesting/src/exchange/memory-exchange.ts +++ b/packages/backtesting/src/exchange/memory-exchange.ts @@ -23,6 +23,9 @@ import type { IWatchCandlesResponse, IPlaceMarketOrderRequest, IPlaceMarketOrderResponse, + ITrade, + IOrderbook, + ITicker, } from "@opentrader/types"; import { ExchangeCode } from "@opentrader/types"; import type { MarketSimulator } from "../market-simulator.js"; @@ -30,12 +33,15 @@ 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 {}; } @@ -44,9 +50,7 @@ export class MemoryExchange implements IExchange { return []; } - async getLimitOrder( - _body: IGetLimitOrderRequest, - ): Promise { + async getLimitOrder(_body: IGetLimitOrderRequest): Promise { return { exchangeOrderId: "", clientOrderId: "", @@ -61,44 +65,34 @@ export class MemoryExchange implements IExchange { }; } - async placeLimitOrder( - _body: IPlaceLimitOrderRequest, - ): Promise { + async placeLimitOrder(_body: IPlaceLimitOrderRequest): Promise { return { orderId: "", clientOrderId: "", }; } - async placeMarketOrder( - _body: IPlaceMarketOrderRequest, - ): Promise { + async placeMarketOrder(_body: IPlaceMarketOrderRequest): Promise { return { orderId: "", clientOrderId: "", }; } - async placeStopOrder( - _body: IPlaceStopOrderRequest, - ): Promise { + async placeStopOrder(_body: IPlaceStopOrderRequest): Promise { return { orderId: "", clientOrderId: "", }; } - async cancelLimitOrder( - _body: ICancelLimitOrderRequest, - ): Promise { + async cancelLimitOrder(_body: ICancelLimitOrderRequest): Promise { return { orderId: "", }; } - async getMarketPrice( - params: IGetMarketPriceRequest, - ): Promise { + async getMarketPrice(params: IGetMarketPriceRequest): Promise { const candlestick = this.marketSimulator.currentCandle; const assetPrice = candlestick.close; const { symbol } = params; @@ -110,15 +104,11 @@ export class MemoryExchange implements IExchange { }; } - async getCandlesticks( - _params: IGetCandlesticksRequest, - ): Promise { + async getCandlesticks(_params: IGetCandlesticksRequest): Promise { return []; } - async getTradingFeeRates( - _params: IGetTradingFeeRatesRequest, - ): Promise { + async getTradingFeeRates(_params: IGetTradingFeeRatesRequest): Promise { return { makerFee: 0, takerFee: 0, @@ -176,19 +166,23 @@ export class MemoryExchange implements IExchange { return []; } - async watchOrders( - _params?: IWatchOrdersRequest, - ): Promise { - throw new Error( - "Not implemented. Backtesting doesn't require this method.", - ); + async watchOrders(_params?: IWatchOrdersRequest): Promise { + throw new Error("Not implemented. Backtesting doesn't require this method."); + } + + async watchCandles(_params?: IWatchCandlesRequest): Promise { + throw new Error("Not implemented. Backtesting doesn't require this method."); + } + + async watchTrades(): Promise { + throw new Error("Not implemented. Backtesting doesn't require this method."); + } + + async watchOrderbook(): Promise { + throw new Error("Not implemented. Backtesting doesn't require this method."); } - async watchCandles( - _params?: IWatchCandlesRequest, - ): Promise { - throw new Error( - "Not implemented. Backtesting doesn't require this method.", - ); + async watchTicker(): Promise { + throw new Error("Not implemented. Backtesting doesn't require this method."); } } From 26322d68f9c5c218cbff0f178d9585b91bad5f71 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 00:33:49 +0100 Subject: [PATCH 09/16] feat: add MarketStream and BotStore --- packages/bot-processor/src/strategy-runner.ts | 20 +- .../src/types/bot/bot-context.type.ts | 8 +- .../bot-processor/src/utils/createContext.ts | 4 +- packages/bot-store/.gitignore | 2 + packages/bot-store/LICENSE | 177 ++++++++++++++++++ packages/bot-store/eslint.config.js | 10 + packages/bot-store/package.json | 31 +++ packages/bot-store/src/index.ts | 125 +++++++++++++ packages/bot-store/tsconfig.json | 7 + packages/bot-store/vitest.config.ts | 12 ++ .../bot-templates/src/templates/test/index.ts | 1 + .../src/templates/test/testSimpleArb.ts | 22 +++ packages/bot/package.json | 1 + .../src/channels/candles/candles.channel.ts | 4 +- packages/bot/src/channels/candles/types.ts | 4 +- .../channels/orderbook/orderbook.channel.ts | 4 +- packages/bot/src/channels/orderbook/types.ts | 4 +- .../order-synchronizer-polling.watcher.ts | 17 +- .../orders/order-synchronizer-ws.watcher.ts | 7 +- packages/bot/src/channels/orders/types.ts | 4 +- .../bot/src/channels/ticker/ticker.channel.ts | 4 +- packages/bot/src/channels/ticker/types.ts | 4 +- .../bot/src/channels/trades/trades.channel.ts | 4 +- packages/bot/src/channels/trades/types.ts | 4 +- .../bot/src/consumers/candles.consumer.ts | 10 +- packages/bot/src/consumers/markets.stream.ts | 106 +++++++++++ .../bot/src/consumers/orderbook.consumer.ts | 10 +- packages/bot/src/consumers/orders.consumer.ts | 6 +- packages/bot/src/consumers/ticker.consumer.ts | 10 +- packages/bot/src/consumers/trades.consumer.ts | 10 +- packages/bot/src/platform.ts | 83 ++------ packages/bot/src/queue/queue.ts | 51 +---- packages/bot/src/queue/types.ts | 31 +-- packages/event-bus/src/index.ts | 32 +--- packages/processing/src/bot/bot.processing.ts | 9 +- .../routers/private/bot/start-bot/handler.ts | 2 +- .../routers/private/bot/stop-bot/handler.ts | 2 +- packages/trpc/src/services/bot.service.ts | 4 +- packages/types/src/index.ts | 2 + packages/types/src/market/common.ts | 4 + packages/types/src/market/events.ts | 34 ++++ 41 files changed, 663 insertions(+), 223 deletions(-) create mode 100644 packages/bot-store/.gitignore create mode 100644 packages/bot-store/LICENSE create mode 100644 packages/bot-store/eslint.config.js create mode 100644 packages/bot-store/package.json create mode 100644 packages/bot-store/src/index.ts create mode 100644 packages/bot-store/tsconfig.json create mode 100644 packages/bot-store/vitest.config.ts create mode 100644 packages/bot-templates/src/templates/test/testSimpleArb.ts create mode 100644 packages/bot/src/consumers/markets.stream.ts create mode 100644 packages/types/src/market/common.ts create mode 100644 packages/types/src/market/events.ts diff --git a/packages/bot-processor/src/strategy-runner.ts b/packages/bot-processor/src/strategy-runner.ts index a18d184d..30f9ce86 100644 --- a/packages/bot-processor/src/strategy-runner.ts +++ b/packages/bot-processor/src/strategy-runner.ts @@ -16,7 +16,7 @@ * Repository URL: https://github.com/bludnic/opentrader */ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, StrategyTriggerEventType } from "@opentrader/types"; +import type { MarketData, MarketId, StrategyTriggerEventType } from "@opentrader/types"; import { BotControl } from "./bot-control.js"; import { effectRunnerMap } from "./effect-runner.js"; import { isEffect } from "./effects/index.js"; @@ -43,8 +43,22 @@ export class StrategyRunner { await this.runTemplate(context); } - async process(state: BotState, event?: StrategyTriggerEventType, market?: MarketData) { - const context = createContext(this.control, this.botConfig, this.exchange, "process", state, market, event); + async process( + state: BotState, + event?: StrategyTriggerEventType, + market?: MarketData, + markets: Record = {}, + ) { + const context = createContext( + this.control, + this.botConfig, + this.exchange, + "process", + state, + market, + markets, + event, + ); await this.runTemplate(context); } diff --git a/packages/bot-processor/src/types/bot/bot-context.type.ts b/packages/bot-processor/src/types/bot/bot-context.type.ts index 122077fc..2f6fde17 100644 --- a/packages/bot-processor/src/types/bot/bot-context.type.ts +++ b/packages/bot-processor/src/types/bot/bot-context.type.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, StrategyTriggerEventType } from "@opentrader/types"; +import type { MarketData, MarketId, StrategyTriggerEventType } 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"; @@ -30,7 +30,11 @@ export type TBotContext; }; diff --git a/packages/bot-processor/src/utils/createContext.ts b/packages/bot-processor/src/utils/createContext.ts index 2884bcc6..c8b4d779 100644 --- a/packages/bot-processor/src/utils/createContext.ts +++ b/packages/bot-processor/src/utils/createContext.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, StrategyTriggerEventType } from "@opentrader/types"; +import type { MarketData, MarketId, StrategyTriggerEventType } from "@opentrader/types"; import type { BotState, IBotConfiguration, IBotControl, TBotContext } from "../types/index.js"; export function createContext( @@ -11,6 +11,7 @@ export function createContext( market: MarketData = { candles: [], }, + markets: Record = {}, event?: StrategyTriggerEventType, ): TBotContext { return { @@ -23,6 +24,7 @@ export function createContext( onProcess: command === "process", state, market, + markets, event, }; } diff --git a/packages/bot-store/.gitignore b/packages/bot-store/.gitignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/packages/bot-store/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/bot-store/LICENSE b/packages/bot-store/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/packages/bot-store/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/bot-store/eslint.config.js b/packages/bot-store/eslint.config.js new file mode 100644 index 00000000..bf58e5c2 --- /dev/null +++ b/packages/bot-store/eslint.config.js @@ -0,0 +1,10 @@ +import EslintConfig from "@opentrader/eslint/module.js"; + +export default [ + ...EslintConfig, + { + rules: { + // overriding rules here + }, + }, +]; diff --git a/packages/bot-store/package.json b/packages/bot-store/package.json new file mode 100644 index 00000000..c79cc9ba --- /dev/null +++ b/packages/bot-store/package.json @@ -0,0 +1,31 @@ +{ + "name": "@opentrader/bot-store", + "version": "0.0.0", + "private": true, + "description": "", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "?build": "Internal package", + "lint": "eslint . --quiet", + "lint:fix": "eslint . --fix", + "test": "vitest" + }, + "author": "bludnic", + "license": "Apache-2.0", + "devDependencies": { + "@opentrader/eslint": "workspace:*", + "@opentrader/tsconfig": "workspace:*", + "@opentrader/types": "workspace:*", + "@types/node": "^20.14.15", + "eslint": "8.57.0", + "typescript": "5.5.4", + "vitest": "^2.0.5" + }, + "dependencies": { + "@opentrader/bot-processor": "workspace:*", + "@opentrader/db": "workspace:*", + "@opentrader/event-bus": "workspace:*" + } +} diff --git a/packages/bot-store/src/index.ts b/packages/bot-store/src/index.ts new file mode 100644 index 00000000..7c10012a --- /dev/null +++ b/packages/bot-store/src/index.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2024 bludnic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Repository URL: https://github.com/bludnic/opentrader + */ + +import { TBotWithExchangeAccount, xprisma } from "@opentrader/db"; +import { BotState } from "@opentrader/bot-processor"; +import { MarketData, MarketEvent, MarketId, StrategyTriggerEventType } from "@opentrader/types"; +import { eventBus } from "@opentrader/event-bus"; + +type BotId = number; + +export class BotStore { + unsubscribeFromEventBus?: () => void; + + bots: TBotWithExchangeAccount[] = []; + state: Record = {}; + processing: Record = {}; + markets: Record = {}; + + constructor() {} + + async init() { + await this.pullBots(); + + this.unsubscribeFromEventBus = this.subcribeToEventBus(); + } + + destroy() { + this.unsubscribeFromEventBus?.(); + console.log("BotStore destroyed"); + } + + private subcribeToEventBus() { + const onBotStarted = (bot: TBotWithExchangeAccount) => { + console.log("Bot started"); + this.bots = [...this.bots, bot]; + }; + eventBus.on("onBotStarted", onBotStarted); + + const onBotStopped = (bot: TBotWithExchangeAccount) => { + console.log("Bot stopped"); + this.bots = this.bots.filter((b) => b.id !== bot.id); + }; + eventBus.on("onBotStopped", onBotStopped); + + return () => { + eventBus.off("onBotStarted", onBotStarted); + eventBus.off("onBotStopped", onBotStopped); + }; + } + + private async pullBots() { + this.bots = await xprisma.bot.custom.findMany({ + where: { enabled: true }, + include: { exchangeAccount: true }, + }); + } + + getState(botId: number) { + return this.state[botId] || {}; + } + + setState(botId: number, state: BotState) { + this.state[botId] = state; + } + + isProcessing(botId: number) { + return this.processing[botId] ?? false; + } + + setProcessing(botId: number, value: boolean) { + this.processing[botId] = value; + } + + getMarket(marketId: MarketId): MarketData | undefined { + return this.markets[marketId]; + } + + updateMarket(data: MarketEvent) { + const { marketId } = data; + + // Create market if does not exist + if (!this.markets[marketId]) { + this.markets[marketId] = { + candles: [], + }; + } + + switch (data.type) { + // @todo strategy event type -> market event type + case StrategyTriggerEventType.onCandleClosed: + this.markets[marketId].candle = data.candle; + this.markets[marketId].candles.push(data.candle); + break; + case StrategyTriggerEventType.onOrderbookChange: + this.markets[marketId].orderbook = data.orderbook; + break; + case StrategyTriggerEventType.onTickerChange: + this.markets[marketId].ticker = data.ticker; + break; + case StrategyTriggerEventType.onPublicTrade: + this.markets[marketId].trade = data.trade; + break; + default: + console.error("Unrecognized event type", data); + break; + } + } +} + +export const store = new BotStore(); diff --git a/packages/bot-store/tsconfig.json b/packages/bot-store/tsconfig.json new file mode 100644 index 00000000..7c095f42 --- /dev/null +++ b/packages/bot-store/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@opentrader/tsconfig/esm.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./dist" + } +} diff --git a/packages/bot-store/vitest.config.ts b/packages/bot-store/vitest.config.ts new file mode 100644 index 00000000..4b533b67 --- /dev/null +++ b/packages/bot-store/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + coverage: { + reporter: ["text", "json", "html"], + }, + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/bot-templates/src/templates/test/index.ts b/packages/bot-templates/src/templates/test/index.ts index 814a0af4..a0be2012 100644 --- a/packages/bot-templates/src/templates/test/index.ts +++ b/packages/bot-templates/src/templates/test/index.ts @@ -5,3 +5,4 @@ export * from "./candle.js"; export * from "./rsi.js"; export * from "./state.js"; export * from "./trades.js"; +export * from "./testSimpleArb.js"; diff --git a/packages/bot-templates/src/templates/test/testSimpleArb.ts b/packages/bot-templates/src/templates/test/testSimpleArb.ts new file mode 100644 index 00000000..833a9b73 --- /dev/null +++ b/packages/bot-templates/src/templates/test/testSimpleArb.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import { logger } from "@opentrader/logger"; +import { TBotContext } from "@opentrader/bot-processor"; + +export function* testSimpleArb(ctx: TBotContext) { + logger.info("[testSimpleArb]: Ticker"); + + const price1 = ctx.markets?.["BINANCE:UNI/USDT"]?.ticker?.ask; + const price2 = ctx.markets?.["BYBIT:UNI/USDT"]?.ticker?.last; + + logger.info(`[UNI Price] Binance: ${price1} BYBIT: ${price2}`); +} + +testSimpleArb.displayName = "Test Simple Arb"; +testSimpleArb.hidden = true; +testSimpleArb.schema = z.object({}); +testSimpleArb.watchers = { + watchTicker: ["BINANCE:UNI/USDT", "BYBIT:UNI/USDT"], +}; +testSimpleArb.runPolicy = { + onTickerChange: true, +}; diff --git a/packages/bot/package.json b/packages/bot/package.json index 4c5ed025..53eaedd5 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -26,6 +26,7 @@ "vitest": "^2.0.5" }, "dependencies": { + "@opentrader/bot-store": "workspace:*", "@opentrader/bot-templates": "workspace:*", "@opentrader/db": "workspace:*", "@opentrader/event-bus": "workspace:*", diff --git a/packages/bot/src/channels/candles/candles.channel.ts b/packages/bot/src/channels/candles/candles.channel.ts index 2ba7a34e..21864a44 100644 --- a/packages/bot/src/channels/candles/candles.channel.ts +++ b/packages/bot/src/channels/candles/candles.channel.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IExchange } from "@opentrader/exchanges"; -import type { BarSize, ICandlestick } from "@opentrader/types"; +import type { BarSize, ICandlestick, MarketId } from "@opentrader/types"; import { logger } from "@opentrader/logger"; import type { CandleEvent } from "./types.js"; import { CandlesWatcher } from "./candles.watcher.js"; @@ -68,7 +68,9 @@ export class CandlesChannel extends EventEmitter { aggregator = new CandlesAggregator(timeframe, watcher, this.exchange); aggregator.on("candle", (candle: ICandlestick, history: ICandlestick[]) => { const candleEvent: CandleEvent = { + exchangeCode: this.exchangeCode, symbol, + marketId: `${this.exchangeCode}:${symbol}` as MarketId, timeframe, candle, history, diff --git a/packages/bot/src/channels/candles/types.ts b/packages/bot/src/channels/candles/types.ts index df8e79c6..9b1f6151 100644 --- a/packages/bot/src/channels/candles/types.ts +++ b/packages/bot/src/channels/candles/types.ts @@ -1,7 +1,9 @@ -import type { BarSize, ICandlestick } from "@opentrader/types"; +import type { BarSize, ExchangeCode, ICandlestick, MarketId } from "@opentrader/types"; export type CandleEvent = { + exchangeCode: ExchangeCode; symbol: string; + marketId: MarketId; timeframe: BarSize; /** * Last closed candle diff --git a/packages/bot/src/channels/orderbook/orderbook.channel.ts b/packages/bot/src/channels/orderbook/orderbook.channel.ts index ca15ca62..ca047f0e 100644 --- a/packages/bot/src/channels/orderbook/orderbook.channel.ts +++ b/packages/bot/src/channels/orderbook/orderbook.channel.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import type { IExchange } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import { IOrderbook } from "@opentrader/types"; +import { IOrderbook, MarketId } from "@opentrader/types"; import type { OrderbookEvent } from "./types.js"; import { OrderbookWatcher } from "./orderbook.watcher.js"; @@ -51,6 +51,8 @@ export class OrderbookChannel extends EventEmitter { handleOrderbook = (orderbook: IOrderbook) => { const event: OrderbookEvent = { + exchangeCode: this.exchangeCode, + marketId: `${this.exchangeCode}:${orderbook.symbol}` as MarketId, symbol: orderbook.symbol, orderbook, }; diff --git a/packages/bot/src/channels/orderbook/types.ts b/packages/bot/src/channels/orderbook/types.ts index a7bfb59f..a0c79ad9 100644 --- a/packages/bot/src/channels/orderbook/types.ts +++ b/packages/bot/src/channels/orderbook/types.ts @@ -1,6 +1,8 @@ -import type { IOrderbook } from "@opentrader/types"; +import type { ExchangeCode, IOrderbook, MarketId } from "@opentrader/types"; export type OrderbookEvent = { + exchangeCode: ExchangeCode; + marketId: MarketId; symbol: string; orderbook: IOrderbook; }; diff --git a/packages/bot/src/channels/orders/order-synchronizer-polling.watcher.ts b/packages/bot/src/channels/orders/order-synchronizer-polling.watcher.ts index 639ebfd0..e62a4e5b 100644 --- a/packages/bot/src/channels/orders/order-synchronizer-polling.watcher.ts +++ b/packages/bot/src/channels/orders/order-synchronizer-polling.watcher.ts @@ -19,6 +19,7 @@ import { NetworkError, RequestTimeout } from "ccxt"; import { ExchangeAccountProcessor } from "@opentrader/processing"; import { logger } from "@opentrader/logger"; import { OrderSynchronizerWatcher } from "./order-synchronizer-watcher.abstract.js"; +import { ExchangeCode } from "@opentrader/types"; /** * This is a fallback for `OrderSynchronizerWsWatcher`. @@ -40,27 +41,21 @@ export class OrderSynchronizerPollingWatcher extends OrderSynchronizerWatcher { } private async syncOrders() { - logger.debug( - `PollingWatcher: Start syncing order statuses of "${this.exchange.name}"`, - ); + logger.debug(`PollingWatcher: Start syncing order statuses of "${this.exchange.name}"`); const processor = new ExchangeAccountProcessor(this.exchange); try { await processor.syncOrders({ onFilled: (exchangeOrder, order) => - this.emit("onFilled", [exchangeOrder, order]), + this.emit("onFilled", [exchangeOrder, order, this.exchange.exchangeCode as ExchangeCode]), onCanceled: (exchangeOrder, order) => - this.emit("onCanceled", [exchangeOrder, order]), + this.emit("onCanceled", [exchangeOrder, order, this.exchange.exchangeCode as ExchangeCode]), }); } catch (err) { if (err instanceof NetworkError) { - logger.info( - `❕ NetworkError during ExchangeAccountProcessor.syncOrders(): ${err.message}`, - ); + logger.info(`❕ NetworkError during ExchangeAccountProcessor.syncOrders(): ${err.message}`); } else if (err instanceof RequestTimeout) { - logger.info( - `❗ RequestTimeout during ExchangeAccountProcessor.syncOrders(): ${err.message}`, - ); + logger.info(`❗ RequestTimeout during ExchangeAccountProcessor.syncOrders(): ${err.message}`); logger.info(err); } else { throw err; diff --git a/packages/bot/src/channels/orders/order-synchronizer-ws.watcher.ts b/packages/bot/src/channels/orders/order-synchronizer-ws.watcher.ts index 6550cd82..197d63ed 100644 --- a/packages/bot/src/channels/orders/order-synchronizer-ws.watcher.ts +++ b/packages/bot/src/channels/orders/order-synchronizer-ws.watcher.ts @@ -19,6 +19,7 @@ import { xprisma } from "@opentrader/db"; import { ExchangeClosedByUser, NetworkError, RequestTimeout } from "ccxt"; import { logger } from "@opentrader/logger"; import { OrderSynchronizerWatcher } from "./order-synchronizer-watcher.abstract.js"; +import { ExchangeCode } from "@opentrader/types"; export class OrderSynchronizerWsWatcher extends OrderSynchronizerWatcher { protocol = "ws" as const; @@ -56,18 +57,18 @@ export class OrderSynchronizerWsWatcher extends OrderSynchronizerWatcher { symbol: smartTrade.exchangeSymbolId, }); - this.emit("onPlaced", [actualExchangeOrder, order]); + this.emit("onPlaced", [actualExchangeOrder, order, this.exchange.exchangeCode as ExchangeCode]); } else if (exchangeOrder.status === "filled") { const statusChanged = order.status !== "Filled"; if (statusChanged) { - this.emit("onFilled", [exchangeOrder, order]); + this.emit("onFilled", [exchangeOrder, order, this.exchange.exchangeCode as ExchangeCode]); } } else if (exchangeOrder.status === "canceled") { const statusChanged = order.status !== "Canceled"; if (statusChanged) { - this.emit("onCanceled", [exchangeOrder, order]); + this.emit("onCanceled", [exchangeOrder, order, this.exchange.exchangeCode as ExchangeCode]); } } } diff --git a/packages/bot/src/channels/orders/types.ts b/packages/bot/src/channels/orders/types.ts index f67a8b44..4caaecc6 100644 --- a/packages/bot/src/channels/orders/types.ts +++ b/packages/bot/src/channels/orders/types.ts @@ -1,9 +1,9 @@ import type { OrderWithSmartTrade } from "@opentrader/db"; -import type { IWatchOrder } from "@opentrader/types"; +import type { ExchangeCode, IWatchOrder } from "@opentrader/types"; export type OrderEvent = "onFilled" | "onCanceled" | "onPlaced"; export type Subscription = { event: OrderEvent; - callback: (exchangeOrder: IWatchOrder, order: OrderWithSmartTrade) => void; + callback: (exchangeOrder: IWatchOrder, order: OrderWithSmartTrade, exchangeCode: ExchangeCode) => void; }; diff --git a/packages/bot/src/channels/ticker/ticker.channel.ts b/packages/bot/src/channels/ticker/ticker.channel.ts index 828c00d4..0057a8b6 100644 --- a/packages/bot/src/channels/ticker/ticker.channel.ts +++ b/packages/bot/src/channels/ticker/ticker.channel.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import type { IExchange } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import { ITicker } from "@opentrader/types"; +import { ITicker, MarketId } from "@opentrader/types"; import type { TickerEvent } from "./types.js"; import { TickerWatcher } from "./ticker.watcher.js"; @@ -51,6 +51,8 @@ export class TickerChannel extends EventEmitter { handleTicker = (ticker: ITicker) => { const event: TickerEvent = { + exchangeCode: this.exchangeCode, + marketId: `${this.exchangeCode}:${ticker.symbol}` as MarketId, symbol: ticker.symbol, ticker, }; diff --git a/packages/bot/src/channels/ticker/types.ts b/packages/bot/src/channels/ticker/types.ts index 5e721dff..b5cbd9a0 100644 --- a/packages/bot/src/channels/ticker/types.ts +++ b/packages/bot/src/channels/ticker/types.ts @@ -1,6 +1,8 @@ -import type { ITicker } from "@opentrader/types"; +import type { ExchangeCode, ITicker, MarketId } from "@opentrader/types"; export type TickerEvent = { + exchangeCode: ExchangeCode; + marketId: MarketId; symbol: string; ticker: ITicker; }; diff --git a/packages/bot/src/channels/trades/trades.channel.ts b/packages/bot/src/channels/trades/trades.channel.ts index 3c7f47f2..54a1804b 100644 --- a/packages/bot/src/channels/trades/trades.channel.ts +++ b/packages/bot/src/channels/trades/trades.channel.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import type { IExchange } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import { ITrade } from "@opentrader/types"; +import { ITrade, MarketId } from "@opentrader/types"; import type { TradeEvent } from "./types.js"; import { TradesWatcher } from "./trades.watcher.js"; @@ -51,6 +51,8 @@ export class TradesChannel extends EventEmitter { handleTrade = (trade: ITrade) => { const tradeEvent: TradeEvent = { + exchangeCode: this.exchangeCode, + marketId: `${this.exchangeCode}:${trade.symbol}` as MarketId, symbol: trade.symbol, trade, }; diff --git a/packages/bot/src/channels/trades/types.ts b/packages/bot/src/channels/trades/types.ts index bc8fd9c0..6d4b2031 100644 --- a/packages/bot/src/channels/trades/types.ts +++ b/packages/bot/src/channels/trades/types.ts @@ -1,6 +1,8 @@ -import type { ITrade } from "@opentrader/types"; +import type { ExchangeCode, ITrade, MarketId } from "@opentrader/types"; export type TradeEvent = { + exchangeCode: ExchangeCode; + marketId: MarketId; symbol: string; trade: ITrade; }; diff --git a/packages/bot/src/consumers/candles.consumer.ts b/packages/bot/src/consumers/candles.consumer.ts index 37dd82f8..80b2bf76 100644 --- a/packages/bot/src/consumers/candles.consumer.ts +++ b/packages/bot/src/consumers/candles.consumer.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import { exchangeProvider } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import type { TBot } from "@opentrader/db"; +import type { TBotWithExchangeAccount } from "@opentrader/db"; import { getWatchers, getTimeframe } from "@opentrader/processing"; import { decomposeSymbolId } from "@opentrader/tools"; import { BarSize, ExchangeCode } from "@opentrader/types"; @@ -15,9 +15,9 @@ import { CandlesChannel } from "../channels/index.js"; */ export class CandlesConsumer extends EventEmitter { private channels: CandlesChannel[] = []; - private bots: TBot[] = []; + private bots: TBotWithExchangeAccount[] = []; - constructor(bots: TBot[]) { + constructor(bots: TBotWithExchangeAccount[]) { super(); this.bots = bots; } @@ -36,7 +36,7 @@ export class CandlesConsumer extends EventEmitter { * @param bot Bot to add * @returns */ - async addBot(bot: TBot) { + async addBot(bot: TBotWithExchangeAccount) { const { strategyFn } = await findStrategy(bot.template); const { watchCandles: symbols } = getWatchers(strategyFn, bot); @@ -83,7 +83,7 @@ export class CandlesConsumer extends EventEmitter { * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels(bots: TBot[]) { + async cleanStaleChannels(bots: TBotWithExchangeAccount[]) { const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; for (const bot of bots) { const { strategyFn } = await findStrategy(bot.template); diff --git a/packages/bot/src/consumers/markets.stream.ts b/packages/bot/src/consumers/markets.stream.ts new file mode 100644 index 00000000..f9c7b477 --- /dev/null +++ b/packages/bot/src/consumers/markets.stream.ts @@ -0,0 +1,106 @@ +import { EventEmitter } from "node:events"; +import { TBotWithExchangeAccount } from "@opentrader/db"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { getWatchers } from "@opentrader/processing"; +import { + CandleClosedMarketEvent, + OrderbookChangeMarketEvent, + PublicTradeMarketEvent, + TickerChangeMarketEvent, +} from "@opentrader/types"; +import { CandlesConsumer } from "./candles.consumer.js"; +import { OrderbookConsumer } from "./orderbook.consumer.js"; +import { TradesConsumer } from "./trades.consumer.js"; +import { TickerConsumer } from "./ticker.consumer.js"; +import { CandleEvent, OrderbookEvent, TradeEvent, TickerEvent } from "../channels/index.js"; + +/** + * Emits: + * - market: MarketEvent + */ +export class MarketsStream extends EventEmitter { + private unsubscribeAll = () => {}; + + candlesStream: CandlesConsumer; + orderbookStream: OrderbookConsumer; + tradesStream: TradesConsumer; + tickerStream: TickerConsumer; + + constructor(bots: TBotWithExchangeAccount[]) { + super(); + + this.candlesStream = new CandlesConsumer(bots); + this.orderbookStream = new OrderbookConsumer(bots); + this.tradesStream = new TradesConsumer(bots); + this.tickerStream = new TickerConsumer(bots); + + this.unsubscribeAll = this.subscribe(); + } + + async add(bot: TBotWithExchangeAccount) { + const { strategyFn } = await findStrategy(bot.template); + const { watchTrades, watchOrderbook, watchTicker, watchCandles } = getWatchers(strategyFn, bot); + + if (watchCandles.length > 0) await this.candlesStream.addBot(bot); + if (watchTrades.length > 0) await this.tradesStream.addBot(bot); + if (watchOrderbook.length > 0) await this.orderbookStream.addBot(bot); + if (watchTicker.length > 0) await this.tickerStream.addBot(bot); + } + + private subscribe() { + const handleCandle = ({ candle, history, marketId }: CandleEvent) => { + this.emit("market", { + type: "onCandleClosed", + candle, + candles: history, + marketId, + } satisfies CandleClosedMarketEvent); + }; + this.candlesStream.on("candle", handleCandle); + + const handleTrade = ({ trade, marketId }: TradeEvent) => { + this.emit("market", { type: "onPublicTrade", trade, marketId } satisfies PublicTradeMarketEvent); + }; + this.tradesStream.on("trade", handleTrade); + + const handleOrderbook = ({ orderbook, marketId }: OrderbookEvent) => { + this.emit("market", { type: "onOrderbookChange", orderbook, marketId } satisfies OrderbookChangeMarketEvent); + }; + this.orderbookStream.on("orderbook", handleOrderbook); + + const handleTicker = ({ ticker, marketId }: TickerEvent) => { + this.emit("market", { type: "onTickerChange", ticker, marketId } satisfies TickerChangeMarketEvent); + }; + this.tickerStream.on("ticker", handleTicker); + + return () => { + this.candlesStream.off("market", handleCandle); + this.tradesStream.off("market", handleTrade); + this.orderbookStream.off("market", handleOrderbook); + this.tickerStream.off("market", handleTicker); + }; + } + + async create() { + await this.candlesStream.create(); + await this.orderbookStream.create(); + await this.tradesStream.create(); + await this.tickerStream.create(); + } + + async clean(bots: TBotWithExchangeAccount[]) { + await this.candlesStream.cleanStaleChannels(bots); + await this.orderbookStream.cleanStaleChannels(bots); + await this.tradesStream.cleanStaleChannels(bots); + await this.tickerStream.cleanStaleChannels(bots); + } + + destroy() { + this.unsubscribeAll(); + + this.candlesStream.destroy(); + this.orderbookStream.destroy(); + this.tradesStream.destroy(); + this.tickerStream.destroy(); + } +} diff --git a/packages/bot/src/consumers/orderbook.consumer.ts b/packages/bot/src/consumers/orderbook.consumer.ts index fd4f2756..f9613379 100644 --- a/packages/bot/src/consumers/orderbook.consumer.ts +++ b/packages/bot/src/consumers/orderbook.consumer.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import { exchangeProvider } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import type { TBot } from "@opentrader/db"; +import type { TBotWithExchangeAccount } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { findStrategy } from "@opentrader/bot-templates/server"; import { getWatchers, getTimeframe } from "@opentrader/processing"; @@ -16,9 +16,9 @@ import { OrderbookChannel } from "../channels/index.js"; */ export class OrderbookConsumer extends EventEmitter { private channels: OrderbookChannel[] = []; - private bots: TBot[] = []; + private bots: TBotWithExchangeAccount[] = []; - constructor(bots: TBot[]) { + constructor(bots: TBotWithExchangeAccount[]) { super(); this.bots = bots; } @@ -37,7 +37,7 @@ export class OrderbookConsumer extends EventEmitter { * @param bot Bot to add * @returns */ - async addBot(bot: TBot) { + async addBot(bot: TBotWithExchangeAccount) { const exchangeAccount = await xprisma.exchangeAccount.findUniqueOrThrow({ where: { id: bot.exchangeAccountId, @@ -67,7 +67,7 @@ export class OrderbookConsumer extends EventEmitter { * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels(bots: TBot[]) { + async cleanStaleChannels(bots: TBotWithExchangeAccount[]) { const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; for (const bot of bots) { const { strategyFn } = await findStrategy(bot.template); diff --git a/packages/bot/src/consumers/orders.consumer.ts b/packages/bot/src/consumers/orders.consumer.ts index edc1c92b..a6dd4676 100644 --- a/packages/bot/src/consumers/orders.consumer.ts +++ b/packages/bot/src/consumers/orders.consumer.ts @@ -1,5 +1,5 @@ import { findStrategy } from "@opentrader/bot-templates/server"; -import type { IWatchOrder } from "@opentrader/types"; +import type { ExchangeCode, IWatchOrder, MarketId } from "@opentrader/types"; import { BotProcessing, shouldRunStrategy } from "@opentrader/processing"; import type { OrderWithSmartTrade, ExchangeAccountWithCredentials } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; @@ -69,7 +69,7 @@ export class OrdersConsumer { } } - private async onOrderFilled(exchangeOrder: IWatchOrder, order: OrderWithSmartTrade) { + private async onOrderFilled(exchangeOrder: IWatchOrder, order: OrderWithSmartTrade, exchangeCode: ExchangeCode) { logger.info( `🔋 onOrderFilled: Order #${order.id}: ${order.exchangeOrderId} was filled with price ${exchangeOrder.filledPrice} at ${exchangeOrder.lastTradeTimestamp} timestamp`, ); @@ -88,11 +88,13 @@ export class OrdersConsumer { } const bot = botProcessor.getBot(); + const marketId: MarketId = `${exchangeCode}:${order.smartTrade.baseCurrency}/${order.smartTrade.quoteCurrency}`; const { strategyFn } = await findStrategy(bot.template); if (shouldRunStrategy(strategyFn, bot, "onOrderFilled")) { processingQueue.push({ type: "onOrderFilled", + marketId, bot, orderId: order.id, }); diff --git a/packages/bot/src/consumers/ticker.consumer.ts b/packages/bot/src/consumers/ticker.consumer.ts index 407115bf..9a4249d0 100644 --- a/packages/bot/src/consumers/ticker.consumer.ts +++ b/packages/bot/src/consumers/ticker.consumer.ts @@ -3,7 +3,7 @@ import { findStrategy } from "@opentrader/bot-templates/server"; import { exchangeProvider } from "@opentrader/exchanges"; import { getTimeframe, getWatchers } from "@opentrader/processing"; import { logger } from "@opentrader/logger"; -import type { TBot } from "@opentrader/db"; +import type { TBotWithExchangeAccount } from "@opentrader/db"; import { decomposeSymbolId } from "@opentrader/tools"; import { BarSize, ExchangeCode } from "@opentrader/types"; import type { TickerEvent } from "../channels/index.js"; @@ -15,9 +15,9 @@ import { TickerChannel } from "../channels/index.js"; */ export class TickerConsumer extends EventEmitter { private channels: TickerChannel[] = []; - private bots: TBot[] = []; + private bots: TBotWithExchangeAccount[] = []; - constructor(bots: TBot[]) { + constructor(bots: TBotWithExchangeAccount[]) { super(); this.bots = bots; } @@ -34,7 +34,7 @@ export class TickerConsumer extends EventEmitter { * Subscribes the bot to the ticker channel. * It will create the channel if necessary or reusing it if it already exists. */ - async addBot(bot: TBot) { + async addBot(bot: TBotWithExchangeAccount) { const { strategyFn } = await findStrategy(bot.template); const { watchTicker: symbols } = getWatchers(strategyFn, bot); @@ -73,7 +73,7 @@ export class TickerConsumer extends EventEmitter { * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels(bots: TBot[]) { + async cleanStaleChannels(bots: TBotWithExchangeAccount[]) { const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; for (const bot of bots) { const { strategyFn } = await findStrategy(bot.template); diff --git a/packages/bot/src/consumers/trades.consumer.ts b/packages/bot/src/consumers/trades.consumer.ts index 86d92fe8..c279c12b 100644 --- a/packages/bot/src/consumers/trades.consumer.ts +++ b/packages/bot/src/consumers/trades.consumer.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import { exchangeProvider } from "@opentrader/exchanges"; import { logger } from "@opentrader/logger"; -import type { TBot } from "@opentrader/db"; +import type { TBotWithExchangeAccount } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { findStrategy } from "@opentrader/bot-templates/server"; import { getWatchers, getTimeframe } from "@opentrader/processing"; @@ -16,9 +16,9 @@ import { TradesChannel } from "../channels/index.js"; */ export class TradesConsumer extends EventEmitter { private channels: TradesChannel[] = []; - private bots: TBot[] = []; + private bots: TBotWithExchangeAccount[] = []; - constructor(bots: TBot[]) { + constructor(bots: TBotWithExchangeAccount[]) { super(); this.bots = bots; } @@ -37,7 +37,7 @@ export class TradesConsumer extends EventEmitter { * @param bot Bot to add * @returns */ - async addBot(bot: TBot) { + async addBot(bot: TBotWithExchangeAccount) { const exchangeAccount = await xprisma.exchangeAccount.findUniqueOrThrow({ where: { id: bot.exchangeAccountId, @@ -67,7 +67,7 @@ export class TradesConsumer extends EventEmitter { * Remove unused channels that are no longer used by any bots. * Triggered when any bot was stopped. */ - async cleanStaleChannels(bots: TBot[]) { + async cleanStaleChannels(bots: TBotWithExchangeAccount[]) { const botsInUse: Array<{ timeframe: BarSize | null; symbols: string[]; exchangeCodes: ExchangeCode[] }> = []; for (const bot of bots) { const { strategyFn } = await findStrategy(bot.template); diff --git a/packages/bot/src/platform.ts b/packages/bot/src/platform.ts index 6685436a..80e54399 100644 --- a/packages/bot/src/platform.ts +++ b/packages/bot/src/platform.ts @@ -2,47 +2,26 @@ import { findStrategy } from "@opentrader/bot-templates/server"; import { xprisma, type ExchangeAccountWithCredentials, TBotWithExchangeAccount } from "@opentrader/db"; import { logger } from "@opentrader/logger"; import { exchangeProvider } from "@opentrader/exchanges"; -import { BotProcessing, getWatchers, shouldRunStrategy } from "@opentrader/processing"; +import { BotProcessing, shouldRunStrategy } from "@opentrader/processing"; import { eventBus } from "@opentrader/event-bus"; -import { CandleEvent, OrderbookEvent, TickerEvent, TradeEvent } from "./channels/index.js"; +import { store } from "@opentrader/bot-store"; +import { MarketEvent } from "@opentrader/types"; import { processingQueue } from "./queue/index.js"; -import { ProcessingEvent } from "./queue/types.js"; -import { CandlesConsumer } from "./consumers/candles.consumer.js"; -import { TradesConsumer } from "./consumers/trades.consumer.js"; -import { OrderbookConsumer } from "./consumers/orderbook.consumer.js"; -import { TickerConsumer } from "./consumers/ticker.consumer.js"; +import { MarketsStream } from "./consumers/markets.stream.js"; import { OrdersConsumer } from "./consumers/orders.consumer.js"; export class Platform { - private ordersConsumer: OrdersConsumer; - private candlesConsumer: CandlesConsumer; - private tradesConsumer: TradesConsumer; - private orderbookConsumer: OrderbookConsumer; - private tickerConsumer: TickerConsumer; + private ordersConsumer; + private marketStream: MarketsStream; private unsubscribeFromEventBus = () => {}; private enabledBots: TBotWithExchangeAccount[] = []; constructor(exchangeAccounts: ExchangeAccountWithCredentials[], bots: TBotWithExchangeAccount[]) { this.ordersConsumer = new OrdersConsumer(exchangeAccounts); - this.candlesConsumer = new CandlesConsumer(bots); - this.candlesConsumer.on("candle", ({ candle, history }: CandleEvent) => - this.handleProcess({ type: "onCandleClosed", candle, candles: history }), - ); - - this.tradesConsumer = new TradesConsumer(bots); - this.tradesConsumer.on("trade", ({ trade }: TradeEvent) => this.handleProcess({ type: "onPublicTrade", trade })); - - this.orderbookConsumer = new OrderbookConsumer(bots); - this.orderbookConsumer.on("orderbook", ({ orderbook }: OrderbookEvent) => - this.handleProcess({ type: "onOrderbookChange", orderbook }), - ); - - this.tickerConsumer = new TickerConsumer(bots); - this.tickerConsumer.on("ticker", ({ ticker }: TickerEvent) => - this.handleProcess({ type: "onTickerChange", ticker }), - ); + this.marketStream = new MarketsStream(this.enabledBots); + this.marketStream.on("market", this.handleMarketEvent); } async bootstrap() { @@ -51,17 +30,8 @@ export class Platform { logger.info("[Processor] OrdersProcessor created"); await this.ordersConsumer.create(); - logger.info("[Processor] CandlesProcessor created"); - await this.candlesConsumer.create(); - - logger.info("[Processor] TradesProcessor created"); - await this.tradesConsumer.create(); - - logger.info("[Processor] OrderbookProcessor created"); - await this.orderbookConsumer.create(); - - logger.info("[Processor] TickerProcessor created"); - await this.tickerConsumer.create(); + logger.info("[Market] MarketsStream created"); + await this.marketStream.create(); this.unsubscribeFromEventBus = this.subscribeToEventBus(); } @@ -72,17 +42,9 @@ export class Platform { logger.info("[Processor] OrdersProcessor destroyed"); await this.ordersConsumer.destroy(); - logger.info("[Processor] CandlesProcessor destroyed"); - this.candlesConsumer.destroy(); - - logger.info("[Processor] TradesProcessor destroyed"); - this.tradesConsumer.destroy(); - - logger.info("[Processor] OrderbookProcessor destroyed"); - this.orderbookConsumer.destroy(); - - logger.info("[Processor] TickerProcessor destroyed"); - this.tickerConsumer.destroy(); + logger.info("[Market] MarketStream destroyed"); + this.marketStream.off("market", this.handleMarketEvent); + this.marketStream.destroy(); this.unsubscribeFromEventBus(); } @@ -139,13 +101,7 @@ export class Platform { */ private subscribeToEventBus() { const onBotStarted = async (bot: TBotWithExchangeAccount) => { - const { strategyFn } = await findStrategy(bot.template); - const { watchTrades, watchOrderbook, watchTicker, watchCandles } = getWatchers(strategyFn, bot); - - if (watchCandles.length > 0) await this.candlesConsumer.addBot(bot); - if (watchTrades.length > 0) await this.tradesConsumer.addBot(bot); - if (watchOrderbook.length > 0) await this.orderbookConsumer.addBot(bot); - if (watchTicker.length > 0) await this.tickerConsumer.addBot(bot); + this.marketStream.add(bot); this.enabledBots = await xprisma.bot.custom.findMany({ where: { enabled: true }, @@ -159,10 +115,7 @@ export class Platform { include: { exchangeAccount: true }, }); - await this.candlesConsumer.cleanStaleChannels(this.enabledBots); - await this.tradesConsumer.cleanStaleChannels(this.enabledBots); - await this.orderbookConsumer.cleanStaleChannels(this.enabledBots); - await this.tickerConsumer.cleanStaleChannels(this.enabledBots); + await this.marketStream.clean(this.enabledBots); }; const addExchangeAccount = async (exchangeAccount: ExchangeAccountWithCredentials) => @@ -194,7 +147,9 @@ export class Platform { }; } - async handleProcess(event: ProcessingEvent) { + handleMarketEvent = async (event: MarketEvent) => { + store.updateMarket(event); + for (const bot of this.enabledBots) { const { strategyFn } = await findStrategy(bot.template); @@ -205,5 +160,5 @@ export class Platform { }); } } - } + }; } diff --git a/packages/bot/src/queue/queue.ts b/packages/bot/src/queue/queue.ts index c71710bb..03f4efca 100644 --- a/packages/bot/src/queue/queue.ts +++ b/packages/bot/src/queue/queue.ts @@ -2,6 +2,7 @@ import { cargoQueue, QueueObject } from "async"; import type { TBot } from "@opentrader/db"; import { BotProcessing } from "@opentrader/processing"; import { logger } from "@opentrader/logger"; +import { store } from "@opentrader/bot-store"; import { QueueEvent } from "./types.js"; async function queueHandler(tasks: QueueEvent[]) { @@ -15,51 +16,11 @@ async function queueHandler(tasks: QueueEvent[]) { const botProcessor = new BotProcessing(event.bot); - switch (event.type) { - case "onOrderFilled": - await botProcessor.process({ - triggerEventType: event.type, - }); - break; - case "onCandleClosed": - await botProcessor.process({ - triggerEventType: event.type, - market: { - candle: event.candle, - candles: event.candles, - }, - }); - break; - case "onPublicTrade": - await botProcessor.process({ - triggerEventType: event.type, - market: { - trade: event.trade, - candles: [], - }, - }); - break; - case "onOrderbookChange": - await botProcessor.process({ - triggerEventType: event.type, - market: { - orderbook: event.orderbook, - candles: [], - }, - }); - break; - case "onTickerChange": - await botProcessor.process({ - triggerEventType: event.type, - market: { - ticker: event.ticker, - candles: [], - }, - }); - break; - default: - throw new Error(`❗ Unknown event type: ${event}`); - } + await botProcessor.process({ + triggerEventType: event.type, + market: store.getMarket(event.marketId), + markets: store.markets, + }); await botProcessor.placePendingOrders(); } diff --git a/packages/bot/src/queue/types.ts b/packages/bot/src/queue/types.ts index 64b08bbb..669981ee 100644 --- a/packages/bot/src/queue/types.ts +++ b/packages/bot/src/queue/types.ts @@ -1,37 +1,12 @@ import type { TBot } from "@opentrader/db"; -import { ICandlestick, IOrderbook, ITicker, ITrade, StrategyTriggerEventType } from "@opentrader/types"; +import { MarketEvent, MarketId, StrategyTriggerEventType } from "@opentrader/types"; export type OrderFilledEvent = { type: typeof StrategyTriggerEventType.onOrderFilled; + marketId: MarketId; orderId: number; }; -export type CandleClosedEvent = { - type: typeof StrategyTriggerEventType.onCandleClosed; - candle: ICandlestick; // current closed candle - candles: ICandlestick[]; // previous candles history -}; - -export type PublicTradeEvent = { - type: typeof StrategyTriggerEventType.onPublicTrade; - trade: ITrade; -}; - -export type OrderbookChangeEvent = { - type: typeof StrategyTriggerEventType.onOrderbookChange; - orderbook: IOrderbook; -}; - -export type TickerChangeEvent = { - type: typeof StrategyTriggerEventType.onTickerChange; - ticker: ITicker; -}; - -export type ProcessingEvent = - | OrderFilledEvent - | CandleClosedEvent - | PublicTradeEvent - | OrderbookChangeEvent - | TickerChangeEvent; +export type ProcessingEvent = MarketEvent | OrderFilledEvent; export type QueueEvent = ProcessingEvent & { bot: TBot }; diff --git a/packages/event-bus/src/index.ts b/packages/event-bus/src/index.ts index 720b7831..dbdfb896 100644 --- a/packages/event-bus/src/index.ts +++ b/packages/event-bus/src/index.ts @@ -16,7 +16,7 @@ * Repository URL: https://github.com/bludnic/opentrader */ -import { ExchangeAccountWithCredentials, TBot, xprisma } from "@opentrader/db"; +import { ExchangeAccountWithCredentials, TBotWithExchangeAccount } from "@opentrader/db"; import { EventEmitter } from "node:events"; export const EVENT = { @@ -48,36 +48,16 @@ class EventBus extends EventEmitter { this.emit(EVENT.onExchangeAccountUpdated, exchangeAccount); } - botCreated(bot: TBot) { + botCreated(bot: TBotWithExchangeAccount) { this.emit(EVENT.onBotCreated, bot); } - botStarted(botId: number) { - xprisma.bot - .findUniqueOrThrow({ - where: { id: botId }, - include: { exchangeAccount: true }, - }) - .then((bot) => { - this.emit(EVENT.onBotStarted, bot); - }) - .catch((error) => { - console.error("EventBus: Error in botStarted", error); - }); + botStarted(bot: TBotWithExchangeAccount) { + this.emit(EVENT.onBotStarted, bot); } - botStopped(botId: number) { - xprisma.bot - .findUniqueOrThrow({ - where: { id: botId }, - include: { exchangeAccount: true }, - }) - .then((bot) => { - this.emit(EVENT.onBotStopped, bot); - }) - .catch((error) => { - console.error("EventBus: Error in botStopped", error); - }); + botStopped(bot: TBotWithExchangeAccount) { + this.emit(EVENT.onBotStopped, bot); } } diff --git a/packages/processing/src/bot/bot.processing.ts b/packages/processing/src/bot/bot.processing.ts index c8d37d77..5a8b0696 100644 --- a/packages/processing/src/bot/bot.processing.ts +++ b/packages/processing/src/bot/bot.processing.ts @@ -5,13 +5,14 @@ import { exchangeProvider } from "@opentrader/exchanges"; import type { TBot } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { logger } from "@opentrader/logger"; -import type { ExchangeCode, MarketData, StrategyTriggerEventType } from "@opentrader/types"; +import type { ExchangeCode, MarketData, MarketId, StrategyTriggerEventType } from "@opentrader/types"; import { SmartTradeExecutor } from "../executors/index.js"; import { BotStoreAdapter } from "./bot-store-adapter.js"; type ProcessParams = { triggerEventType?: StrategyTriggerEventType; - market?: MarketData; + market?: MarketData; // default market + markets?: Record; // aditional markets }; export class BotProcessing { @@ -56,7 +57,7 @@ export class BotProcessing { } private async processCommand(command: "start" | "stop" | "process", params: ProcessParams) { - const { market, triggerEventType } = params; + const { triggerEventType, market, markets } = params; console.log(`🤖 Exec "${command}" command`, { context: `candle=${JSON.stringify(market?.candle)} candlesHistory=${market?.candles.length || 0} trade=${JSON.stringify(market?.trade)}`, @@ -79,7 +80,7 @@ export class BotProcessing { } else if (command === "stop") { await processor.stop(botState); } else if (command === "process") { - await processor.process(botState, triggerEventType, market); + await processor.process(botState, triggerEventType, market, markets); } } catch (err) { await xprisma.bot.setProcessing(false, this.bot.id); diff --git a/packages/trpc/src/routers/private/bot/start-bot/handler.ts b/packages/trpc/src/routers/private/bot/start-bot/handler.ts index 5a7e0e0b..756326cc 100644 --- a/packages/trpc/src/routers/private/bot/start-bot/handler.ts +++ b/packages/trpc/src/routers/private/bot/start-bot/handler.ts @@ -25,7 +25,7 @@ export async function startGridBot({ input }: Options) { await botProcessor.placePendingOrders(); - eventBus.botStarted(botId); + eventBus.botStarted(botService.bot); return { ok: true, diff --git a/packages/trpc/src/routers/private/bot/stop-bot/handler.ts b/packages/trpc/src/routers/private/bot/stop-bot/handler.ts index 0e153a39..0a6128e9 100644 --- a/packages/trpc/src/routers/private/bot/stop-bot/handler.ts +++ b/packages/trpc/src/routers/private/bot/stop-bot/handler.ts @@ -23,7 +23,7 @@ export async function stopGridBot({ input }: Options) { await botService.stop(); - eventBus.botStopped(botId); + eventBus.botStopped(botService.bot); return { ok: true, diff --git a/packages/trpc/src/services/bot.service.ts b/packages/trpc/src/services/bot.service.ts index dbd12aa6..1be28668 100644 --- a/packages/trpc/src/services/bot.service.ts +++ b/packages/trpc/src/services/bot.service.ts @@ -1,9 +1,9 @@ -import type { TBot } from "@opentrader/db"; +import type { TBotWithExchangeAccount } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { TRPCError } from "@trpc/server"; export class BotService { - constructor(public bot: TBot) {} + constructor(public bot: TBotWithExchangeAccount) {} static async fromId(id: number) { const bot = await xprisma.bot.custom.findUniqueOrThrow({ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e6296ddb..aa006a5d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -18,6 +18,8 @@ export * from "./common/index.js"; export * from "./exchange/index.js"; export * from "./grid-bot/index.js"; +export * from "./market/common.js"; +export * from "./market/events.js"; export * from "./smart-trade/enums.js"; export * from "./indicators/index.js"; export * from "./strategy-runner/context.js"; diff --git a/packages/types/src/market/common.ts b/packages/types/src/market/common.ts new file mode 100644 index 00000000..80f24a7d --- /dev/null +++ b/packages/types/src/market/common.ts @@ -0,0 +1,4 @@ +import { ExchangeCode } from "../common/enums.js"; + +// e.g. OKX:BTC/USDT +export type MarketId = `${ExchangeCode}:${Base}/${Quote}`; diff --git a/packages/types/src/market/events.ts b/packages/types/src/market/events.ts new file mode 100644 index 00000000..21bc1798 --- /dev/null +++ b/packages/types/src/market/events.ts @@ -0,0 +1,34 @@ +import { StrategyTriggerEventType } from "../strategy-runner/context.js"; +import { ICandlestick, IOrderbook, ITicker, ITrade } from "../exchange/index.js"; +import { MarketId } from "./common.js"; + +export type CandleClosedMarketEvent = { + type: typeof StrategyTriggerEventType.onCandleClosed; + marketId: MarketId; + candle: ICandlestick; // current closed candle + candles: ICandlestick[]; // previous candles history +}; + +export type PublicTradeMarketEvent = { + type: typeof StrategyTriggerEventType.onPublicTrade; + marketId: MarketId; + trade: ITrade; +}; + +export type OrderbookChangeMarketEvent = { + type: typeof StrategyTriggerEventType.onOrderbookChange; + marketId: MarketId; + orderbook: IOrderbook; +}; + +export type TickerChangeMarketEvent = { + type: typeof StrategyTriggerEventType.onTickerChange; + marketId: MarketId; + ticker: ITicker; +}; + +export type MarketEvent = + | CandleClosedMarketEvent + | PublicTradeMarketEvent + | OrderbookChangeMarketEvent + | TickerChangeMarketEvent; From 6ceabace583a4bec48d65955de33b13906c38fdc Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 00:46:31 +0100 Subject: [PATCH 10/16] fix(BotStore): missing candle history after warmup --- packages/bot-store/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bot-store/src/index.ts b/packages/bot-store/src/index.ts index 7c10012a..7bbfacb9 100644 --- a/packages/bot-store/src/index.ts +++ b/packages/bot-store/src/index.ts @@ -104,7 +104,7 @@ export class BotStore { // @todo strategy event type -> market event type case StrategyTriggerEventType.onCandleClosed: this.markets[marketId].candle = data.candle; - this.markets[marketId].candles.push(data.candle); + this.markets[marketId].candles = data.candles; break; case StrategyTriggerEventType.onOrderbookChange: this.markets[marketId].orderbook = data.orderbook; From b0778b8fb5ab18db6e44ced5c36a1594ad81a949 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 01:19:47 +0100 Subject: [PATCH 11/16] refactor(bot): rename consumers to streams --- packages/bot/src/platform.ts | 6 ++--- .../candles.stream.ts} | 2 +- .../{consumers => streams}/markets.stream.ts | 24 +++++++++---------- .../orderbook.stream.ts} | 2 +- .../orders.stream.ts} | 2 +- .../ticker.stream.ts} | 2 +- .../trades.stream.ts} | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) rename packages/bot/src/{consumers/candles.consumer.ts => streams/candles.stream.ts} (99%) rename packages/bot/src/{consumers => streams}/markets.stream.ts (84%) rename packages/bot/src/{consumers/orderbook.consumer.ts => streams/orderbook.stream.ts} (98%) rename packages/bot/src/{consumers/orders.consumer.ts => streams/orders.stream.ts} (99%) rename packages/bot/src/{consumers/ticker.consumer.ts => streams/ticker.stream.ts} (98%) rename packages/bot/src/{consumers/trades.consumer.ts => streams/trades.stream.ts} (98%) diff --git a/packages/bot/src/platform.ts b/packages/bot/src/platform.ts index 80e54399..dcfb022b 100644 --- a/packages/bot/src/platform.ts +++ b/packages/bot/src/platform.ts @@ -8,8 +8,8 @@ import { store } from "@opentrader/bot-store"; import { MarketEvent } from "@opentrader/types"; import { processingQueue } from "./queue/index.js"; -import { MarketsStream } from "./consumers/markets.stream.js"; -import { OrdersConsumer } from "./consumers/orders.consumer.js"; +import { MarketsStream } from "./streams/markets.stream.js"; +import { OrdersStream } from "./streams/orders.stream.js"; export class Platform { private ordersConsumer; @@ -18,7 +18,7 @@ export class Platform { private enabledBots: TBotWithExchangeAccount[] = []; constructor(exchangeAccounts: ExchangeAccountWithCredentials[], bots: TBotWithExchangeAccount[]) { - this.ordersConsumer = new OrdersConsumer(exchangeAccounts); + this.ordersConsumer = new OrdersStream(exchangeAccounts); this.marketStream = new MarketsStream(this.enabledBots); this.marketStream.on("market", this.handleMarketEvent); diff --git a/packages/bot/src/consumers/candles.consumer.ts b/packages/bot/src/streams/candles.stream.ts similarity index 99% rename from packages/bot/src/consumers/candles.consumer.ts rename to packages/bot/src/streams/candles.stream.ts index 80b2bf76..d86c51e2 100644 --- a/packages/bot/src/consumers/candles.consumer.ts +++ b/packages/bot/src/streams/candles.stream.ts @@ -13,7 +13,7 @@ import { CandlesChannel } from "../channels/index.js"; * Emits: * - candle: CandleEvent */ -export class CandlesConsumer extends EventEmitter { +export class CandlesStream extends EventEmitter { private channels: CandlesChannel[] = []; private bots: TBotWithExchangeAccount[] = []; diff --git a/packages/bot/src/consumers/markets.stream.ts b/packages/bot/src/streams/markets.stream.ts similarity index 84% rename from packages/bot/src/consumers/markets.stream.ts rename to packages/bot/src/streams/markets.stream.ts index f9c7b477..877413d9 100644 --- a/packages/bot/src/consumers/markets.stream.ts +++ b/packages/bot/src/streams/markets.stream.ts @@ -8,10 +8,10 @@ import { PublicTradeMarketEvent, TickerChangeMarketEvent, } from "@opentrader/types"; -import { CandlesConsumer } from "./candles.consumer.js"; -import { OrderbookConsumer } from "./orderbook.consumer.js"; -import { TradesConsumer } from "./trades.consumer.js"; -import { TickerConsumer } from "./ticker.consumer.js"; +import { CandlesStream } from "./candles.stream.js"; +import { OrderbookStream } from "./orderbook.stream.js"; +import { TradesStream } from "./trades.stream.js"; +import { TickerStream } from "./ticker.stream.js"; import { CandleEvent, OrderbookEvent, TradeEvent, TickerEvent } from "../channels/index.js"; /** @@ -21,18 +21,18 @@ import { CandleEvent, OrderbookEvent, TradeEvent, TickerEvent } from "../channel export class MarketsStream extends EventEmitter { private unsubscribeAll = () => {}; - candlesStream: CandlesConsumer; - orderbookStream: OrderbookConsumer; - tradesStream: TradesConsumer; - tickerStream: TickerConsumer; + candlesStream: CandlesStream; + orderbookStream: OrderbookStream; + tradesStream: TradesStream; + tickerStream: TickerStream; constructor(bots: TBotWithExchangeAccount[]) { super(); - this.candlesStream = new CandlesConsumer(bots); - this.orderbookStream = new OrderbookConsumer(bots); - this.tradesStream = new TradesConsumer(bots); - this.tickerStream = new TickerConsumer(bots); + this.candlesStream = new CandlesStream(bots); + this.orderbookStream = new OrderbookStream(bots); + this.tradesStream = new TradesStream(bots); + this.tickerStream = new TickerStream(bots); this.unsubscribeAll = this.subscribe(); } diff --git a/packages/bot/src/consumers/orderbook.consumer.ts b/packages/bot/src/streams/orderbook.stream.ts similarity index 98% rename from packages/bot/src/consumers/orderbook.consumer.ts rename to packages/bot/src/streams/orderbook.stream.ts index f9613379..3c3c7e2c 100644 --- a/packages/bot/src/consumers/orderbook.consumer.ts +++ b/packages/bot/src/streams/orderbook.stream.ts @@ -14,7 +14,7 @@ import { OrderbookChannel } from "../channels/index.js"; * Emits: * - orderbook: OrderbookEvent */ -export class OrderbookConsumer extends EventEmitter { +export class OrderbookStream extends EventEmitter { private channels: OrderbookChannel[] = []; private bots: TBotWithExchangeAccount[] = []; diff --git a/packages/bot/src/consumers/orders.consumer.ts b/packages/bot/src/streams/orders.stream.ts similarity index 99% rename from packages/bot/src/consumers/orders.consumer.ts rename to packages/bot/src/streams/orders.stream.ts index a6dd4676..965bf633 100644 --- a/packages/bot/src/consumers/orders.consumer.ts +++ b/packages/bot/src/streams/orders.stream.ts @@ -7,7 +7,7 @@ import { logger } from "@opentrader/logger"; import { OrdersChannel } from "../channels/index.js"; import { processingQueue } from "../queue/index.js"; -export class OrdersConsumer { +export class OrdersStream { private channels: OrdersChannel[] = []; private initialExchangeAccounts: ExchangeAccountWithCredentials[]; diff --git a/packages/bot/src/consumers/ticker.consumer.ts b/packages/bot/src/streams/ticker.stream.ts similarity index 98% rename from packages/bot/src/consumers/ticker.consumer.ts rename to packages/bot/src/streams/ticker.stream.ts index 9a4249d0..6284d921 100644 --- a/packages/bot/src/consumers/ticker.consumer.ts +++ b/packages/bot/src/streams/ticker.stream.ts @@ -13,7 +13,7 @@ import { TickerChannel } from "../channels/index.js"; * Emits: * - ticker: TickerEvent */ -export class TickerConsumer extends EventEmitter { +export class TickerStream extends EventEmitter { private channels: TickerChannel[] = []; private bots: TBotWithExchangeAccount[] = []; diff --git a/packages/bot/src/consumers/trades.consumer.ts b/packages/bot/src/streams/trades.stream.ts similarity index 98% rename from packages/bot/src/consumers/trades.consumer.ts rename to packages/bot/src/streams/trades.stream.ts index c279c12b..0cd260b3 100644 --- a/packages/bot/src/consumers/trades.consumer.ts +++ b/packages/bot/src/streams/trades.stream.ts @@ -14,7 +14,7 @@ import { TradesChannel } from "../channels/index.js"; * Emits: * - trade: TradeEvent */ -export class TradesConsumer extends EventEmitter { +export class TradesStream extends EventEmitter { private channels: TradesChannel[] = []; private bots: TBotWithExchangeAccount[] = []; From 6ba7a6c7ea5a534c9dfca2abe1bfb679f353a96c Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 01:27:39 +0100 Subject: [PATCH 12/16] refactor(bot): rename enum StrategyTriggerEventType -> MarketEventType --- packages/bot-processor/src/strategy-runner.ts | 4 +-- .../src/types/bot/bot-context.type.ts | 4 +-- .../src/types/bot/bot-template.type.ts | 12 +++---- .../bot-processor/src/utils/createContext.ts | 4 +-- packages/bot-store/src/index.ts | 11 +++--- packages/bot/src/queue/types.ts | 4 +-- .../db/src/types/bot-logs/bot-log.schema.ts | 4 +-- packages/db/src/xprisma.ts | 4 +-- packages/prisma/src/schema.prisma | 36 +++++++++---------- packages/processing/src/bot/bot.processing.ts | 4 +-- packages/processing/src/strategy/runPolicy.ts | 4 +-- .../private/bot/get-bot-logs/handler.ts | 4 +-- packages/types/src/market/events.ts | 10 +++--- packages/types/src/strategy-runner/context.ts | 4 +-- 14 files changed, 54 insertions(+), 55 deletions(-) diff --git a/packages/bot-processor/src/strategy-runner.ts b/packages/bot-processor/src/strategy-runner.ts index 30f9ce86..1ef8fec0 100644 --- a/packages/bot-processor/src/strategy-runner.ts +++ b/packages/bot-processor/src/strategy-runner.ts @@ -16,7 +16,7 @@ * Repository URL: https://github.com/bludnic/opentrader */ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, MarketId, StrategyTriggerEventType } 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"; @@ -45,7 +45,7 @@ export class StrategyRunner { async process( state: BotState, - event?: StrategyTriggerEventType, + event?: MarketEventType, market?: MarketData, markets: Record = {}, ) { diff --git a/packages/bot-processor/src/types/bot/bot-context.type.ts b/packages/bot-processor/src/types/bot/bot-context.type.ts index 2f6fde17..31039120 100644 --- a/packages/bot-processor/src/types/bot/bot-context.type.ts +++ b/packages/bot-processor/src/types/bot/bot-context.type.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, MarketId, StrategyTriggerEventType } 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"; @@ -25,7 +25,7 @@ export type TBotContext { * The size of the candle is determined by `timeframe` property above. * If not provided, the channel will listen to 1m candles. */ - [StrategyTriggerEventType.onCandleClosed]?: boolean | ((botConfig: T) => boolean); - [StrategyTriggerEventType.onPublicTrade]?: boolean | ((botConfig: T) => boolean); - [StrategyTriggerEventType.onOrderbookChange]?: boolean | ((botConfig: T) => boolean); - [StrategyTriggerEventType.onTickerChange]?: boolean | ((botConfig: T) => boolean); - [StrategyTriggerEventType.onOrderFilled]?: boolean | ((botConfig: T) => boolean); + [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); }; } diff --git a/packages/bot-processor/src/utils/createContext.ts b/packages/bot-processor/src/utils/createContext.ts index c8b4d779..2754b5f3 100644 --- a/packages/bot-processor/src/utils/createContext.ts +++ b/packages/bot-processor/src/utils/createContext.ts @@ -1,5 +1,5 @@ import type { IExchange } from "@opentrader/exchanges"; -import type { MarketData, MarketId, StrategyTriggerEventType } from "@opentrader/types"; +import type { MarketData, MarketId, MarketEventType } from "@opentrader/types"; import type { BotState, IBotConfiguration, IBotControl, TBotContext } from "../types/index.js"; export function createContext( @@ -12,7 +12,7 @@ export function createContext( candles: [], }, markets: Record = {}, - event?: StrategyTriggerEventType, + event?: MarketEventType, ): TBotContext { return { control, diff --git a/packages/bot-store/src/index.ts b/packages/bot-store/src/index.ts index 7bbfacb9..a40d3ef3 100644 --- a/packages/bot-store/src/index.ts +++ b/packages/bot-store/src/index.ts @@ -18,7 +18,7 @@ import { TBotWithExchangeAccount, xprisma } from "@opentrader/db"; import { BotState } from "@opentrader/bot-processor"; -import { MarketData, MarketEvent, MarketId, StrategyTriggerEventType } from "@opentrader/types"; +import { MarketData, MarketEvent, MarketId, MarketEventType } from "@opentrader/types"; import { eventBus } from "@opentrader/event-bus"; type BotId = number; @@ -101,18 +101,17 @@ export class BotStore { } switch (data.type) { - // @todo strategy event type -> market event type - case StrategyTriggerEventType.onCandleClosed: + case MarketEventType.onCandleClosed: this.markets[marketId].candle = data.candle; this.markets[marketId].candles = data.candles; break; - case StrategyTriggerEventType.onOrderbookChange: + case MarketEventType.onOrderbookChange: this.markets[marketId].orderbook = data.orderbook; break; - case StrategyTriggerEventType.onTickerChange: + case MarketEventType.onTickerChange: this.markets[marketId].ticker = data.ticker; break; - case StrategyTriggerEventType.onPublicTrade: + case MarketEventType.onPublicTrade: this.markets[marketId].trade = data.trade; break; default: diff --git a/packages/bot/src/queue/types.ts b/packages/bot/src/queue/types.ts index 669981ee..2ebb723d 100644 --- a/packages/bot/src/queue/types.ts +++ b/packages/bot/src/queue/types.ts @@ -1,8 +1,8 @@ import type { TBot } from "@opentrader/db"; -import { MarketEvent, MarketId, StrategyTriggerEventType } from "@opentrader/types"; +import { MarketEvent, MarketId, MarketEventType } from "@opentrader/types"; export type OrderFilledEvent = { - type: typeof StrategyTriggerEventType.onOrderFilled; + type: typeof MarketEventType.onOrderFilled; marketId: MarketId; orderId: number; }; diff --git a/packages/db/src/types/bot-logs/bot-log.schema.ts b/packages/db/src/types/bot-logs/bot-log.schema.ts index b1499d3e..775e30c1 100644 --- a/packages/db/src/types/bot-logs/bot-log.schema.ts +++ b/packages/db/src/types/bot-logs/bot-log.schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { zt } from "@opentrader/prisma"; -import { StrategyAction, StrategyTriggerEventType } from "@opentrader/types"; +import { StrategyAction, MarketEventType } from "@opentrader/types"; const ZCandlestick = z.object({ open: z.number(), @@ -22,7 +22,7 @@ const ZStrategyError = z.object({ export const ZBotLog = zt.BotLogSchema.extend({ action: z.nativeEnum(StrategyAction), - triggerEventType: z.nativeEnum(StrategyTriggerEventType), + triggerEventType: z.nativeEnum(MarketEventType), context: ZMarketData.optional(), error: ZStrategyError.optional(), }); diff --git a/packages/db/src/xprisma.ts b/packages/db/src/xprisma.ts index 47bf48b4..04f5aeaf 100644 --- a/packages/db/src/xprisma.ts +++ b/packages/db/src/xprisma.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "@prisma/client"; -import { MarketData, StrategyAction, StrategyError, StrategyTriggerEventType } from "@opentrader/types"; +import { MarketData, StrategyAction, StrategyError, MarketEventType } from "@opentrader/types"; import { gridBotModel } from "./extension/models/grid-bot.model.js"; import { orderModel } from "./extension/models/order.model.js"; import { smartTradeModel } from "./extension/models/smart-trade.model.js"; @@ -54,7 +54,7 @@ const xprismaClient = prismaClient.$extends({ endedAt: Date; botId: number; action: StrategyAction; - triggerEventType?: StrategyTriggerEventType; + triggerEventType?: MarketEventType; context?: MarketData; error?: StrategyError; }) { diff --git a/packages/prisma/src/schema.prisma b/packages/prisma/src/schema.prisma index b39c72a6..efe91284 100644 --- a/packages/prisma/src/schema.prisma +++ b/packages/prisma/src/schema.prisma @@ -107,10 +107,10 @@ model ExchangeAccount { exchangeCode String // ExchangeCode // Credentials - apiKey String - secretKey String - password String? - isDemoAccount Boolean @default(false) + apiKey String + secretKey String + password String? + isDemoAccount Boolean @default(false) isPaperAccount Boolean @default(false) owner User @relation(fields: [ownerId], references: [id]) @@ -218,7 +218,7 @@ model Bot { model BotLog { id Int @id @default(autoincrement()) action String // type StrategyAction - triggerEventType String? // type StrategyTriggerEventType + triggerEventType String? // type MarketEventType context String? // type MarketData, JSON stringified error String? // type StrategyError, An error occured during the strategy execution, JSON striginfied startedAt DateTime // start execution time @@ -235,20 +235,20 @@ model Markets { } model PaperAsset { - currency String @id - balance Float + currency String @id + balance Float } model PaperOrder { - id Int @id @default(autoincrement()) - type String // OrderType - symbol String - side String // OrderSide - quantity Float - price Float? - filledPrice Float? + id Int @id @default(autoincrement()) + type String // OrderType + symbol String + side String // OrderSide + quantity Float + price Float? + filledPrice Float? lastTradeTimestamp DateTime @default(now()) - status String @default("open") // OrderStatus - fee Float @default(0) - createdAt DateTime @default(now()) -} \ No newline at end of file + status String @default("open") // OrderStatus + fee Float @default(0) + createdAt DateTime @default(now()) +} diff --git a/packages/processing/src/bot/bot.processing.ts b/packages/processing/src/bot/bot.processing.ts index 5a8b0696..6885eb02 100644 --- a/packages/processing/src/bot/bot.processing.ts +++ b/packages/processing/src/bot/bot.processing.ts @@ -5,12 +5,12 @@ import { exchangeProvider } from "@opentrader/exchanges"; import type { TBot } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { logger } from "@opentrader/logger"; -import type { ExchangeCode, MarketData, MarketId, StrategyTriggerEventType } from "@opentrader/types"; +import type { ExchangeCode, MarketData, MarketId, MarketEventType } from "@opentrader/types"; import { SmartTradeExecutor } from "../executors/index.js"; import { BotStoreAdapter } from "./bot-store-adapter.js"; type ProcessParams = { - triggerEventType?: StrategyTriggerEventType; + triggerEventType?: MarketEventType; market?: MarketData; // default market markets?: Record; // aditional markets }; diff --git a/packages/processing/src/strategy/runPolicy.ts b/packages/processing/src/strategy/runPolicy.ts index 395b628d..d3ad40be 100644 --- a/packages/processing/src/strategy/runPolicy.ts +++ b/packages/processing/src/strategy/runPolicy.ts @@ -1,4 +1,4 @@ -import { StrategyTriggerEventType } from "@opentrader/types"; +import { MarketEventType } from "@opentrader/types"; import { BotTemplate, IBotConfiguration } from "@opentrader/bot-processor"; /** @@ -15,7 +15,7 @@ import { BotTemplate, IBotConfiguration } from "@opentrader/bot-processor"; export function shouldRunStrategy( strategyFn: BotTemplate, botConfig: T, - eventType?: StrategyTriggerEventType, + eventType?: MarketEventType, ): boolean { if (!strategyFn.runPolicy) { console.warn(`Strategy ${strategyFn.name} does not have a run policy`); diff --git a/packages/trpc/src/routers/private/bot/get-bot-logs/handler.ts b/packages/trpc/src/routers/private/bot/get-bot-logs/handler.ts index 4f41b656..d11615ec 100644 --- a/packages/trpc/src/routers/private/bot/get-bot-logs/handler.ts +++ b/packages/trpc/src/routers/private/bot/get-bot-logs/handler.ts @@ -1,5 +1,5 @@ import { xprisma, TBotLog } from "@opentrader/db"; -import { MarketData, StrategyAction, StrategyError, StrategyTriggerEventType } from "@opentrader/types"; +import { MarketData, StrategyAction, StrategyError, MarketEventType } from "@opentrader/types"; import type { Context } from "../../../../utils/context.js"; import type { TGetBotLogs } from "./schema.js"; @@ -33,7 +33,7 @@ export async function getBotLogs({ input }: Options) { const logs: TBotLog[] = botLogs.map((log) => ({ ...log, action: log.action as StrategyAction, - triggerEventType: log.triggerEventType as StrategyTriggerEventType, + triggerEventType: log.triggerEventType as MarketEventType, context: parseJson(log.context), error: parseJson(log.error), })); diff --git a/packages/types/src/market/events.ts b/packages/types/src/market/events.ts index 21bc1798..f481206b 100644 --- a/packages/types/src/market/events.ts +++ b/packages/types/src/market/events.ts @@ -1,28 +1,28 @@ -import { StrategyTriggerEventType } from "../strategy-runner/context.js"; +import { MarketEventType } from "../strategy-runner/context.js"; import { ICandlestick, IOrderbook, ITicker, ITrade } from "../exchange/index.js"; import { MarketId } from "./common.js"; export type CandleClosedMarketEvent = { - type: typeof StrategyTriggerEventType.onCandleClosed; + type: typeof MarketEventType.onCandleClosed; marketId: MarketId; candle: ICandlestick; // current closed candle candles: ICandlestick[]; // previous candles history }; export type PublicTradeMarketEvent = { - type: typeof StrategyTriggerEventType.onPublicTrade; + type: typeof MarketEventType.onPublicTrade; marketId: MarketId; trade: ITrade; }; export type OrderbookChangeMarketEvent = { - type: typeof StrategyTriggerEventType.onOrderbookChange; + type: typeof MarketEventType.onOrderbookChange; marketId: MarketId; orderbook: IOrderbook; }; export type TickerChangeMarketEvent = { - type: typeof StrategyTriggerEventType.onTickerChange; + type: typeof MarketEventType.onTickerChange; marketId: MarketId; ticker: ITicker; }; diff --git a/packages/types/src/strategy-runner/context.ts b/packages/types/src/strategy-runner/context.ts index 44c0cb80..16e66f4d 100644 --- a/packages/types/src/strategy-runner/context.ts +++ b/packages/types/src/strategy-runner/context.ts @@ -17,14 +17,14 @@ export type StrategyAction = (typeof StrategyAction)[keyof typeof StrategyAction /** * Event that triggers strategy execution */ -export const StrategyTriggerEventType = { +export const MarketEventType = { onOrderFilled: "onOrderFilled", onCandleClosed: "onCandleClosed", onPublicTrade: "onPublicTrade", onOrderbookChange: "onOrderbookChange", onTickerChange: "onTickerChange", } as const; -export type StrategyTriggerEventType = (typeof StrategyTriggerEventType)[keyof typeof StrategyTriggerEventType]; +export type MarketEventType = (typeof MarketEventType)[keyof typeof MarketEventType]; /** * An error occurred during strategy execution From a6e5845b3a90fe7422e235891bd6a85aba8383af Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 01:29:27 +0100 Subject: [PATCH 13/16] fix(tRPC, create-bot): missing `exchangeAccount` in bot type --- packages/trpc/src/routers/private/bot/create-bot/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trpc/src/routers/private/bot/create-bot/handler.ts b/packages/trpc/src/routers/private/bot/create-bot/handler.ts index d5a77a74..2f72617d 100644 --- a/packages/trpc/src/routers/private/bot/create-bot/handler.ts +++ b/packages/trpc/src/routers/private/bot/create-bot/handler.ts @@ -65,6 +65,7 @@ export async function createBot({ ctx, input }: Options) { }, }, }, + include: { exchangeAccount: true }, }); eventBus.botCreated(bot); From 97690c533e265526ece8ad4ad2fcfebcf75c42fa Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 02:21:58 +0100 Subject: [PATCH 14/16] fix(PaperExchange, WS): delay filling orders to ensure `exchangeOrderId` is saved in DB after order placement --- .../src/exchanges/ccxt/paper-exchange.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/exchanges/src/exchanges/ccxt/paper-exchange.ts b/packages/exchanges/src/exchanges/ccxt/paper-exchange.ts index d4d0a087..37813104 100644 --- a/packages/exchanges/src/exchanges/ccxt/paper-exchange.ts +++ b/packages/exchanges/src/exchanges/ccxt/paper-exchange.ts @@ -42,6 +42,9 @@ import { import { PaperOrder, xprisma } from "@opentrader/db"; import { CCXTExchange } from "./exchange.js"; +const ORDER_PLACEMENT_DELAY = 100; +const ORDER_FULFILLMENT_DELAY = 200; + export class PaperExchange extends CCXTExchange { /** * @override @@ -216,9 +219,13 @@ export class PaperExchange extends CCXTExchange { price: params.price, }, }); - this.emitOrder(order); await this.pullOpenOrders(); - void this.match(); + + // simulate order execution by delaying it + setTimeout(() => { + this.emitOrder(order); + void this.match(); + }, ORDER_PLACEMENT_DELAY); return { orderId: `${order.id}`, @@ -238,8 +245,6 @@ export class PaperExchange extends CCXTExchange { }, }); - this.emitOrder(order); // emit opened order - const ticker = await this.ccxt.fetchTicker(params.symbol); const filledOrder = await xprisma.paperOrder.update({ where: { id: order.id }, @@ -250,7 +255,13 @@ export class PaperExchange extends CCXTExchange { }, }); - this.emitOrder(filledOrder); // then fill it immediately + setTimeout(() => { + this.emitOrder(order); + }, ORDER_PLACEMENT_DELAY); + + setTimeout(() => { + this.emitOrder(filledOrder); + }, ORDER_FULFILLMENT_DELAY); return { orderId: `${filledOrder.id}`, From 1ef001927c9434caaae5a521499be40a421c040a Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 02:31:04 +0100 Subject: [PATCH 15/16] fix(strategies, buySell): add runPolicy for test strategy --- packages/bot-templates/src/templates/test/buySell.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/bot-templates/src/templates/test/buySell.ts b/packages/bot-templates/src/templates/test/buySell.ts index 6b7afd54..6093dc89 100644 --- a/packages/bot-templates/src/templates/test/buySell.ts +++ b/packages/bot-templates/src/templates/test/buySell.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { buy, cancelSmartTrade, sell, type TBotContext } from "@opentrader/bot-processor"; +import { buy, cancelSmartTrade, IBotConfiguration, sell, type TBotContext } from "@opentrader/bot-processor"; import { logger } from "@opentrader/logger"; export function* testBuySell(ctx: TBotContext) { @@ -39,3 +39,9 @@ export function* testBuySell(ctx: TBotContext) { testBuySell.schema = z.object({}); testBuySell.hidden = true; +testBuySell.watchers = { + watchCandles: ({ baseCurrency, quoteCurrency }: IBotConfiguration) => `${baseCurrency}/${quoteCurrency}`, +}; +testBuySell.runPolicy = { + onCandleClosed: true, +}; From 1e6502226eb67a2713a3a5e167cc3b4323a3fd6e Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 19 Aug 2024 02:57:20 +0100 Subject: [PATCH 16/16] chore(pnpm): upgrade lockfile --- pnpm-lock.yaml | 154 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 104 insertions(+), 50 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23f39c26..eb4df198 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,13 +125,13 @@ importers: version: 6.0.1 ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.5.4)(webpack@5.93.0) + version: 9.5.1(typescript@5.5.4)(webpack@5.93.0(esbuild@0.23.0)) ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) tsup: specifier: ^8.2.4 - version: 8.2.4(typescript@5.5.4) + version: 8.2.4(postcss@8.4.41)(tsx@4.17.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -181,6 +181,9 @@ importers: packages/bot: dependencies: + '@opentrader/bot-store': + specifier: workspace:* + version: link:../bot-store '@opentrader/bot-templates': specifier: workspace:* version: link:../bot-templates @@ -238,7 +241,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.15) + version: 2.0.5(@types/node@20.14.15)(terser@5.31.6) packages/bot-processor: dependencies: @@ -280,6 +283,40 @@ importers: specifier: 5.5.4 version: 5.5.4 + packages/bot-store: + dependencies: + '@opentrader/bot-processor': + specifier: workspace:* + version: link:../bot-processor + '@opentrader/db': + specifier: workspace:* + version: link:../db + '@opentrader/event-bus': + specifier: workspace:* + version: link:../event-bus + devDependencies: + '@opentrader/eslint': + specifier: workspace:* + version: link:../eslint + '@opentrader/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@opentrader/types': + specifier: workspace:* + version: link:../types + '@types/node': + specifier: ^20.14.15 + version: 20.14.15 + eslint: + specifier: 8.57.0 + version: 8.57.0 + typescript: + specifier: 5.5.4 + version: 5.5.4 + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@20.14.15)(terser@5.31.6) + packages/bot-templates: dependencies: '@opentrader/bot-processor': @@ -321,10 +358,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -468,7 +505,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.15) + version: 2.0.5(@types/node@20.14.15)(terser@5.31.6) packages/exchanges: dependencies: @@ -530,10 +567,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) @@ -662,10 +699,10 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -699,13 +736,13 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4) tsup: specifier: ^8.2.4 - version: 8.2.4(typescript@5.5.4) + version: 8.2.4(postcss@8.4.41)(tsx@4.17.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -783,7 +820,7 @@ importers: version: 8.57.0 tsup: specifier: ^8.2.4 - version: 8.2.4(typescript@5.5.4) + version: 8.2.4(postcss@8.4.41)(tsx@4.17.0)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -4822,7 +4859,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2)': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -4836,7 +4873,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -5034,7 +5071,7 @@ snapshots: optional: true '@prisma/client@5.17.0(prisma@5.17.0)': - dependencies: + optionalDependencies: prisma: 5.17.0 '@prisma/debug@5.17.0': {} @@ -5274,7 +5311,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) @@ -5287,6 +5324,7 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5299,6 +5337,7 @@ snapshots: '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.3.6 eslint: 8.57.0 + optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5315,6 +5354,7 @@ snapshots: debug: 4.3.6 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5331,6 +5371,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -5813,13 +5854,13 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - create-jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): + create-jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -6581,16 +6622,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): + jest-cli@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6600,12 +6641,11 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): + jest-config@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.15 babel-jest: 29.7.0(@babel/core@7.25.2) chalk: 4.1.2 ci-info: 3.9.0 @@ -6625,6 +6665,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.14.15 ts-node: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros @@ -6707,7 +6749,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -6851,12 +6893,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2): + jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7222,9 +7264,12 @@ snapshots: dependencies: find-up: 4.1.0 - postcss-load-config@6.0.1: + postcss-load-config@6.0.1(postcss@8.4.41)(tsx@4.17.0): dependencies: lilconfig: 3.1.2 + optionalDependencies: + postcss: 8.4.41 + tsx: 4.17.0 postcss@8.4.41: dependencies: @@ -7633,15 +7678,16 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(esbuild@0.23.0)(webpack@5.93.0): + terser-webpack-plugin@5.3.10(esbuild@0.23.0)(webpack@5.93.0(esbuild@0.23.0)): dependencies: '@jridgewell/trace-mapping': 0.3.25 - esbuild: 0.23.0 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.6 webpack: 5.93.0(esbuild@0.23.0) + optionalDependencies: + esbuild: 0.23.0 terser@5.31.6: dependencies: @@ -7704,14 +7750,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4): + ts-jest@29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)))(typescript@5.5.4): dependencies: - '@babel/core': 7.25.2 bs-logger: 0.2.6 ejs: 3.1.10 - esbuild: 0.23.0 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -7719,8 +7763,13 @@ snapshots: semver: 7.6.3 typescript: 5.5.4 yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.25.2 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.25.2) - ts-loader@9.5.1(typescript@5.5.4)(webpack@5.93.0): + ts-loader@9.5.1(typescript@5.5.4)(webpack@5.93.0(esbuild@0.23.0)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.17.1 @@ -7750,7 +7799,7 @@ snapshots: tslib@2.6.3: {} - tsup@8.2.4(typescript@5.5.4): + tsup@8.2.4(postcss@8.4.41)(tsx@4.17.0)(typescript@5.5.4): dependencies: bundle-require: 5.0.0(esbuild@0.23.0) cac: 6.7.14 @@ -7762,12 +7811,14 @@ snapshots: globby: 11.1.0 joycon: 3.1.1 picocolors: 1.0.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(postcss@8.4.41)(tsx@4.17.0) resolve-from: 5.0.0 rollup: 4.20.0 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.4.41 typescript: 5.5.4 transitivePeerDependencies: - jiti @@ -7826,10 +7877,11 @@ snapshots: typescript-eslint@7.18.0(eslint@8.57.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 + optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -7864,13 +7916,13 @@ snapshots: vary@1.1.2: {} - vite-node@2.0.5(@types/node@20.14.15): + vite-node@2.0.5(@types/node@20.14.15)(terser@5.31.6): dependencies: cac: 6.7.14 debug: 4.3.6 pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@20.14.15) + vite: 5.4.1(@types/node@20.14.15)(terser@5.31.6) transitivePeerDependencies: - '@types/node' - less @@ -7882,19 +7934,19 @@ snapshots: - supports-color - terser - vite@5.4.1(@types/node@20.14.15): + vite@5.4.1(@types/node@20.14.15)(terser@5.31.6): dependencies: - '@types/node': 20.14.15 esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.20.0 optionalDependencies: + '@types/node': 20.14.15 fsevents: 2.3.3 + terser: 5.31.6 - vitest@2.0.5(@types/node@20.14.15): + vitest@2.0.5(@types/node@20.14.15)(terser@5.31.6): dependencies: '@ampproject/remapping': 2.3.0 - '@types/node': 20.14.15 '@vitest/expect': 2.0.5 '@vitest/pretty-format': 2.0.5 '@vitest/runner': 2.0.5 @@ -7910,9 +7962,11 @@ snapshots: tinybench: 2.9.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@20.14.15) - vite-node: 2.0.5(@types/node@20.14.15) + vite: 5.4.1(@types/node@20.14.15)(terser@5.31.6) + vite-node: 2.0.5(@types/node@20.14.15)(terser@5.31.6) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.15 transitivePeerDependencies: - less - lightningcss @@ -7959,7 +8013,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.23.0)(webpack@5.93.0) + terser-webpack-plugin: 5.3.10(esbuild@0.23.0)(webpack@5.93.0(esbuild@0.23.0)) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: