From 27f33dc0f20113404a9c2c0b2c54467802f54129 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 10 Jun 2024 03:14:55 +0100 Subject: [PATCH 01/12] feat: cli draft --- bin/ot.sh | 3 + package.json | 5 +- packages/cli/package.json | 12 +- packages/cli/src/api/down.ts | 41 ++++ packages/cli/src/api/index.ts | 3 + packages/cli/src/api/logs.ts | 81 ++++++++ packages/cli/src/api/run-backtest.ts | 14 +- packages/cli/src/api/run-trading.ts | 274 ++------------------------- packages/cli/src/api/up/daemon.ts | 203 ++++++++++++++++++++ packages/cli/src/api/up/index.ts | 51 +++++ packages/cli/src/commands/down.ts | 14 ++ packages/cli/src/commands/logs.ts | 10 + packages/cli/src/commands/trade.ts | 2 +- packages/cli/src/commands/up.ts | 12 ++ packages/cli/src/index.ts | 13 +- packages/cli/src/utils/app-path.ts | 7 + packages/cli/src/utils/bot.ts | 216 +++++++++++++++++++++ packages/cli/src/utils/pid.ts | 28 +++ packages/cli/webpack.config.js | 46 +++++ packages/logger/src/logger.ts | 40 +++- test-strategy.mjs | 3 + 21 files changed, 798 insertions(+), 280 deletions(-) create mode 100755 bin/ot.sh create mode 100644 packages/cli/src/api/down.ts create mode 100644 packages/cli/src/api/logs.ts create mode 100644 packages/cli/src/api/up/daemon.ts create mode 100644 packages/cli/src/api/up/index.ts create mode 100644 packages/cli/src/commands/down.ts create mode 100644 packages/cli/src/commands/logs.ts create mode 100644 packages/cli/src/commands/up.ts create mode 100644 packages/cli/src/utils/app-path.ts create mode 100644 packages/cli/src/utils/bot.ts create mode 100644 packages/cli/src/utils/pid.ts create mode 100644 packages/cli/webpack.config.js create mode 100644 test-strategy.mjs diff --git a/bin/ot.sh b/bin/ot.sh new file mode 100755 index 00000000..7518d175 --- /dev/null +++ b/bin/ot.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Use "$@" to pass all additional command line arguments to your script +node packages/cli/dist/main.js "$@" diff --git a/package.json b/package.json index 185ae5f8..fd4d7691 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@9.1.3", + "packageManager": "pnpm@9.1.4", "devDependencies": { "prettier": "^3.2.5", "ts-node": "10.9.2", @@ -26,6 +26,7 @@ "debug": "ts-node --transpile-only packages/cli/src/index.ts trade debug" }, "bin": { - "opentrader": "./bin/opentrader.sh" + "opentrader": "./bin/opentrader.sh", + "ot": "./bin/ot.sh" } } diff --git a/packages/cli/package.json b/packages/cli/package.json index b11bc87b..e6327df9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,6 +6,7 @@ "types": "src/index.ts", "scripts": { "build": "tsc", + "build:webpack": "webpack", "lint": "eslint . --quiet", "lint:fix": "eslint . --fix" }, @@ -17,8 +18,12 @@ "@opentrader/types": "workspace:*", "@types/node": "^20.12.11", "eslint": "8.54.0", + "ts-loader": "^9.5.1", "ts-node": "10.9.2", - "typescript": "5.4.5" + "typescript": "5.4.5", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0" }, "dependencies": { "@opentrader/backtesting": "workspace:*", @@ -33,6 +38,9 @@ "@prisma/client": "5.15.0", "ccxt": "4.3.27", "commander": "^12.0.0", - "json5": "^2.2.3" + "jayson": "^4.1.0", + "json5": "^2.2.3", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0" } } diff --git a/packages/cli/src/api/down.ts b/packages/cli/src/api/down.ts new file mode 100644 index 00000000..8880fb3b --- /dev/null +++ b/packages/cli/src/api/down.ts @@ -0,0 +1,41 @@ +import { logger } from "@opentrader/logger"; +import { CommandResult } from "../types"; +import { getPid, clearPid } from "../utils/pid"; + +type Options = { + force: boolean; +}; + +export async function down(options: Options): Promise { + const pid = getPid(); + + if (!pid) { + logger.warn("There is no running daemon process. Nothing to stop."); + return { + result: undefined, + }; + } + + try { + if (options.force) { + process.kill(pid, "SIGKILL"); + logger.info( + `Daemon process with PID ${pid} has been forcefully stopped.`, + ); + } else { + process.kill(pid, "SIGTERM"); + logger.info( + `Daemon process with PID ${pid} has been gracefully stopped.`, + ); + } + } catch (err) { + logger.info(`Failed to stop daemon process with PID ${pid}`); + logger.error(err); + } + + clearPid(); + + return { + result: undefined, + }; +} diff --git a/packages/cli/src/api/index.ts b/packages/cli/src/api/index.ts index 096e8166..1fbddb52 100644 --- a/packages/cli/src/api/index.ts +++ b/packages/cli/src/api/index.ts @@ -2,3 +2,6 @@ export * from "./grid-lines"; export * from "./run-backtest"; export * from "./run-trading"; export * from "./stop-command"; +export * from "./up/index"; +export * from "./down"; +export * from "./logs"; diff --git a/packages/cli/src/api/logs.ts b/packages/cli/src/api/logs.ts new file mode 100644 index 00000000..0eaaeaeb --- /dev/null +++ b/packages/cli/src/api/logs.ts @@ -0,0 +1,81 @@ +import { logger } from "@opentrader/logger"; +import { + existsSync, + readFileSync, + createReadStream, + watchFile, + read, +} from "fs"; +import { createInterface } from "readline"; +import { CommandResult } from "../types"; +import { logPath } from "../utils/app-path"; + +logger; + +type Options = { + follow: boolean; +}; + +export async function logs(options: Options): Promise { + // const pid = getPid(); + + // if (!pid) { + // logger.info("Not running daemon process. Nothing to show logs for."); + + // return { + // result: undefined, + // }; + // } + + const logFileExists = existsSync(logPath); + + if (!logFileExists) { + logger.info("Log file does not exist. Nothing to show logs for."); + + return { + result: undefined, + }; + } + + if (options.follow) { + const logsData = readFileSync(logPath, "utf8"); + const logsLines = logsData.split("\n"); + + // Print the last 10 lines + for (const line of logsLines.slice(-10)) { + console.log(line); + } + + // Keep track of the last file size + let lastSize = 0; + + watchFile(logPath, (curr, prev) => { + if (curr.size > prev.size) { + const stream = createReadStream(logPath, { + start: lastSize, + end: curr.size, + }); + const rl = createInterface({ input: stream }); + + rl.on("line", (line) => { + console.log(line); + }); + + rl.on("close", () => { + lastSize = curr.size; + }); + } + }); + } else { + const logsData = readFileSync(logPath, "utf8"); + const logsLines = logsData.split("\n"); + + for (const line of logsLines) { + console.log(line); + } + } + + return { + result: undefined, + }; +} diff --git a/packages/cli/src/api/run-backtest.ts b/packages/cli/src/api/run-backtest.ts index 46d49f82..3c15757b 100644 --- a/packages/cli/src/api/run-backtest.ts +++ b/packages/cli/src/api/run-backtest.ts @@ -7,6 +7,8 @@ import { exchangeCodeMapCCXT } from "@opentrader/exchanges"; import type { BarSize, ExchangeCode, ICandlestick } from "@opentrader/types"; import type { CommandResult, ConfigName } from "../types"; import { readBotConfig } from "../config"; +import { existsSync } from "fs"; +import { join } from "path"; type Options = { config: ConfigName; @@ -24,8 +26,16 @@ export async function runBacktest( const botConfig = readBotConfig(options.config); logger.debug(botConfig, "Parsed bot config"); + let strategyFn; + const isCustomStrategyFile = existsSync(join(process.cwd(), strategyName)); const strategyExists = strategyName in templates; - if (!strategyExists) { + + if (isCustomStrategyFile) { + const { default: fn } = await import(join(process.cwd(), strategyName)); + strategyFn = fn; + } else if (strategyExists) { + strategyFn = templates[strategyName]; + } else { const availableStrategies = Object.keys(templates).join(", "); logger.info( `Strategy "${strategyName}" does not exists. Available strategies: ${availableStrategies}`, @@ -55,7 +65,7 @@ export async function runBacktest( exchangeCode: options.exchange, settings: botConfig.settings, }, - botTemplate: templates[botTemplate], + botTemplate: strategyFn, }); return new Promise((resolve) => { diff --git a/packages/cli/src/api/run-trading.ts b/packages/cli/src/api/run-trading.ts index 87cdf315..82fac8c1 100644 --- a/packages/cli/src/api/run-trading.ts +++ b/packages/cli/src/api/run-trading.ts @@ -1,17 +1,7 @@ import { templates } from "@opentrader/bot-templates"; -import { logger } from "@opentrader/logger"; -import { Processor } from "@opentrader/bot"; -import { xprisma } from "@opentrader/db"; -import type { TBot, ExchangeAccountWithCredentials } from "@opentrader/db"; -import { BotProcessing } from "@opentrader/processing"; import { BarSize } from "@opentrader/types"; -import type { - BotConfig, - CommandResult, - ConfigName, - ExchangeConfig, -} from "../types"; -import { readBotConfig, readExchangesConfig } from "../config"; +import { Client } from "jayson/promise"; +import type { CommandResult, ConfigName } from "../types"; type Options = { config: ConfigName; @@ -24,261 +14,19 @@ export async function runTrading( strategyName: keyof typeof templates, options: Options, ): Promise { - const config = readBotConfig(options.config); - logger.debug(config, "Parsed bot config"); - - const exchangesConfig = readExchangesConfig(options.config); - logger.debug(exchangesConfig, "Parsed exchanges config"); - - const strategyExists = strategyName in templates; - if (!strategyExists) { - const availableStrategies = Object.keys(templates).join(", "); - logger.info( - `Strategy "${strategyName}" does not exists. Available strategies: ${availableStrategies}`, - ); - - return { - result: undefined, - }; - } + const client = Client.http({ + port: 8000, + }); - // Saving exchange accounts to DB if not exists - const exchangeAccounts: ExchangeAccountWithCredentials[] = - await createOrUpdateExchangeAccounts(exchangesConfig); - const bot = await createOrUpdateBot( + const result = client.request("startBot", [ strategyName, - options, - config, - exchangeAccounts, - ); - - const processor = new Processor(exchangeAccounts, [bot]); - await processor.onApplicationBootstrap(); - - if (bot.enabled) { - logger.info( - `Bot "${bot.label}" is already enabled. Cancelling previous orders...`, - ); - await stopBot(bot.id); - logger.info(`The bot state was cleared`); - } - - if (bot.processing) { - logger.warn( - `Bot "${bot.label}" is already processing. It could happen because previous process was interrupted.`, - ); - await resetProcessing(bot.id); - logger.warn(`The bot processing state was cleared`); - } - - await startBot(bot.id); - logger.info(`Bot "${bot.label}" started`); + options.config, + options.pair, + options.exchange, + options.timeframe, + ]); return { result: undefined, }; } - -/** - * Save exchange accounts to DB if not exists - * @param exchangesConfig - Exchange accounts configuration - */ -async function createOrUpdateExchangeAccounts( - exchangesConfig: Record, -) { - const exchangeAccounts: ExchangeAccountWithCredentials[] = []; - - for (const [exchangeLabel, exchangeData] of Object.entries(exchangesConfig)) { - let exchangeAccount = await xprisma.exchangeAccount.findFirst({ - where: { - label: exchangeLabel, - }, - }); - - if (exchangeAccount) { - logger.info( - `Exchange account "${exchangeLabel}" found in DB. Updating credentials...`, - ); - - exchangeAccount = await xprisma.exchangeAccount.update({ - where: { - id: exchangeAccount.id, - }, - data: { - ...exchangeData, - label: exchangeLabel, - owner: { - connect: { - id: 1, - }, - }, - }, - }); - - logger.info(`Exchange account "${exchangeLabel}" updated`); - } else { - logger.info( - `Exchange account "${exchangeLabel}" not found. Adding to DB...`, - ); - - exchangeAccount = await xprisma.exchangeAccount.create({ - data: { - ...exchangeData, - label: exchangeLabel, - owner: { - connect: { - id: 1, - }, - }, - }, - }); - - logger.info(`Exchange account "${exchangeLabel}" created`); - } - - exchangeAccounts.push(exchangeAccount); - } - - return exchangeAccounts; -} - -async function createOrUpdateBot( - strategyName: string, - options: Options, - botConfig: BotConfig, - exchangeAccounts: ExchangeAccountWithCredentials[], -): Promise { - const exchangeLabel = options.exchange || botConfig.exchange; - const botType = botConfig.type || "Bot"; - const botName = botConfig.name || "Default bot"; - const botLabel = botConfig.label || "default"; - const botTemplate = strategyName || botConfig.template; - const botTimeframe = options.timeframe || botConfig.timeframe || null; - const botPair = options.pair || botConfig.pair; - const [baseCurrency, quoteCurrency] = botPair.split("/"); - - const exchangeAccount = exchangeAccounts.find( - (exchangeAccount) => exchangeAccount.label === exchangeLabel, - ); - if (!exchangeAccount) { - throw new Error( - `Exchange account with label "${exchangeLabel}" not found. Check the exchanges config file.`, - ); - } - - let bot = await xprisma.bot.custom.findFirst({ - where: { - label: botLabel, - }, - }); - - if (bot) { - logger.info(`Bot "${botLabel}" found in DB. Updating...`); - - bot = await xprisma.bot.custom.update({ - where: { - id: bot.id, - }, - data: { - type: botType, - name: botName, - label: botLabel, - template: botTemplate, - timeframe: botTimeframe, - baseCurrency, - quoteCurrency, - settings: botConfig.settings as object, - state: {}, // resets bot state - exchangeAccount: { - connect: { - id: exchangeAccount.id, - }, - }, - owner: { - connect: { - id: 1, - }, - }, - }, - }); - - logger.info(`Bot "${botLabel}" updated`); - } else { - logger.info(`Bot "${botLabel}" not found. Adding to DB...`); - bot = await xprisma.bot.custom.create({ - data: { - type: botType, - name: botName, - label: botLabel, - template: strategyName, - timeframe: botTimeframe, - baseCurrency, - quoteCurrency, - settings: botConfig.settings as object, - exchangeAccount: { - connect: { - id: exchangeAccount.id, - }, - }, - owner: { - connect: { - id: 1, - }, - }, - }, - }); - - logger.info(`Bot "${botLabel}" created`); - } - - return bot; -} - -async function startBot(botId: number) { - const botProcessor = await BotProcessing.fromId(botId); - await botProcessor.processStartCommand(); - - await enableBot(botId); - - await botProcessor.placePendingOrders(); -} - -async function enableBot(botId: number) { - await xprisma.bot.custom.update({ - where: { - id: botId, - }, - data: { - enabled: true, - }, - }); -} - -async function stopBot(botId: number) { - const botProcessor = await BotProcessing.fromId(botId); - await botProcessor.processStopCommand(); - - await disableBot(botId); -} - -async function disableBot(botId: number) { - await xprisma.bot.custom.update({ - where: { - id: botId, - }, - data: { - enabled: false, - }, - }); -} - -async function resetProcessing(botId: number) { - await xprisma.bot.custom.update({ - where: { - id: botId, - }, - data: { - processing: false, - }, - }); -} diff --git a/packages/cli/src/api/up/daemon.ts b/packages/cli/src/api/up/daemon.ts new file mode 100644 index 00000000..eac67919 --- /dev/null +++ b/packages/cli/src/api/up/daemon.ts @@ -0,0 +1,203 @@ +import { Server } from "jayson/promise"; + +import { Processor } from "@opentrader/bot"; +import { ExchangeAccountWithCredentials, xprisma } from "@opentrader/db"; +import { logger } from "@opentrader/logger"; +import { BarSize } from "@opentrader/types"; +import { templates } from "@opentrader/bot-templates"; +import { readBotConfig, readExchangesConfig } from "../../config"; +import { + createOrUpdateBot, + createOrUpdateExchangeAccounts, + resetProcessing, + startBot, + stopBot, +} from "../../utils/bot"; +import { ConfigName } from "../../types"; + +let app: App | null = null; + +class App { + constructor(private processor: Processor) {} + + static async create() { + const exchangeAccounts = await xprisma.exchangeAccount.findMany(); + logger.info(`Found ${exchangeAccounts.length} exchange accounts`); + + const bot = await xprisma.bot.custom.findFirst({ + where: { + label: "default", + }, + }); + logger.info(`Found bot: ${bot ? bot.label : "none"}`); + + const processor = new Processor(exchangeAccounts, bot ? [bot] : []); + await processor.onApplicationBootstrap(); + + return new App(processor); + } + + async destroy() { + if (this.processor) { + await this.processor.beforeApplicationShutdown(); + } + } + + async restart() { + await this.destroy(); + + const exchangeAccounts = await xprisma.exchangeAccount.findMany(); + logger.info(`Found ${exchangeAccounts.length} exchange accounts`); + + const bot = await xprisma.bot.custom.findFirst({ + where: { + label: "default", + }, + }); + logger.info(`Found bot: ${bot ? bot.label : "none"}`); + + const processor = new Processor(exchangeAccounts, bot ? [bot] : []); + await processor.onApplicationBootstrap(); + } +} + +async function createApp() { + app = await App.create(); + logger.info("App created"); + + server.http().listen(8000); + logger.info("RPC Server started on port 8000"); +} +void createApp(); + +type Options = { + config: ConfigName; + pair?: string; + exchange?: string; + timeframe?: BarSize; +}; + +const server = Server({ + async startBot( + args: [ + strategyName: string, + configName: ConfigName, + pair?: string, + exchange?: string, + timeframe?: BarSize, + ], + ) { + const [strategyName, configName, pair, exchange, timeframe] = args; + const options = { + strategyName, + config: configName, + pair, + exchange, + timeframe, + }; + + const config = readBotConfig(options.config); + logger.debug(config, "Parsed bot config"); + + const exchangesConfig = readExchangesConfig(options.config); + logger.debug(exchangesConfig, "Parsed exchanges config"); + + const strategyExists = strategyName in templates; + if (!strategyExists) { + const availableStrategies = Object.keys(templates).join(", "); + logger.info( + `Strategy "${strategyName}" does not exists. Available strategies: ${availableStrategies}`, + ); + + return false; + } + + // Saving exchange accounts to DB if not exists + const exchangeAccounts: ExchangeAccountWithCredentials[] = + await createOrUpdateExchangeAccounts(exchangesConfig); + const bot = await createOrUpdateBot( + strategyName, + options, + config, + exchangeAccounts, + ); + + await app?.restart(); + + if (bot.enabled) { + logger.info( + `Bot "${bot.label}" is already enabled. Cancelling previous orders...`, + ); + await stopBot(bot.id); + logger.info(`The bot state was cleared`); + } + + if (bot.processing) { + logger.warn( + `Bot "${bot.label}" is already processing. It could happen because previous process was interrupted.`, + ); + await resetProcessing(bot.id); + logger.warn(`The bot processing state was cleared`); + } + + await startBot(bot.id); + logger.info(`Bot "${bot.label}" started`); + + return true; + }, + async stopBot(args: [configName: ConfigName]) { + const [configName] = args; + + const config = readBotConfig(configName); + logger.debug(config, "Parsed bot config"); + + const exchangesConfig = readExchangesConfig(configName); + logger.debug(exchangesConfig, "Parsed exchanges config"); + + const botLabel = config.label || "default"; + + const bot = await xprisma.bot.custom.findUnique({ + where: { + label: botLabel, + }, + }); + + if (!bot) { + logger.info(`Bot "${botLabel}" does not exists. Nothing to stop`); + + return false; + } + + const strategyExists = bot.template in templates; + if (!strategyExists) { + const availableStrategies = Object.keys(templates).join(", "); + logger.info( + `Strategy "${bot.template}" does not exists. Available strategies: ${availableStrategies}`, + ); + + return false; + } + + logger.info(`Processing stop command for bot "${bot.label}"...`); + await stopBot(bot.id); + logger.info(`Command stop processed successfully for bot "${bot.label}"`); + + return true; + }, +}); + +async function shutdown() { + logger.info("SIGTERM received"); + + if (app) { + await app.destroy(); + logger.info("App shutted down gracefully."); + } + + server.http().close(); + logger.info("RPC Server shutted down gracefully."); + + process.exit(0); +} +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); diff --git a/packages/cli/src/api/up/index.ts b/packages/cli/src/api/up/index.ts new file mode 100644 index 00000000..3fd48118 --- /dev/null +++ b/packages/cli/src/api/up/index.ts @@ -0,0 +1,51 @@ +import { join } from "path"; +import { spawn } from "child_process"; +import { Processor } from "@opentrader/bot"; +import { xprisma } from "@opentrader/db"; +import { logger } from "@opentrader/logger"; +import { CommandResult } from "../../types"; +import { appPath } from "../../utils/app-path"; +import { getPid, savePid } from "../../utils/pid"; + +type Options = { + detach: boolean; +}; + +export async function up(options: Options): Promise { + const pid = getPid(); + + if (pid) { + logger.warn(`Daemon process is already running with PID: ${pid}`); + + return { + result: undefined, + }; + } + + const daemonProcess = spawn("ts-node", [join(__dirname, "daemon.ts")], { + detached: options.detach, + stdio: options.detach ? "ignore" : undefined, + }); + + if (daemonProcess.pid === undefined) { + throw new Error("Failed to start daemon process"); + } + + logger.info(`OpenTrader daemon started with PID: ${daemonProcess.pid}`); + + if (options.detach) { + daemonProcess.unref(); + savePid(daemonProcess.pid); + logger.info(`Daemon process detached and saved to ${appPath}`); + } else { + daemonProcess.stdout?.pipe(process.stdout); + daemonProcess.stderr?.pipe(process.stderr); + } + + console.log("Main process PID:", process.pid); + console.log("Daemon process PID:", daemonProcess.pid); + + return { + result: undefined, + }; +} diff --git a/packages/cli/src/commands/down.ts b/packages/cli/src/commands/down.ts new file mode 100644 index 00000000..6cc69f87 --- /dev/null +++ b/packages/cli/src/commands/down.ts @@ -0,0 +1,14 @@ +import { Command, Option } from "commander"; +import { handle } from "../utils/command"; +import { down } from "../api"; + +export function addDownCommand(program: Command) { + program + .command("down") + .addOption( + new Option("-f, --force", "Forcefully stop the daemon process").default( + false, + ), + ) + .action(handle(down)); +} diff --git a/packages/cli/src/commands/logs.ts b/packages/cli/src/commands/logs.ts new file mode 100644 index 00000000..84301184 --- /dev/null +++ b/packages/cli/src/commands/logs.ts @@ -0,0 +1,10 @@ +import { Command, Option } from "commander"; +import { handle } from "../utils/command"; +import { logs } from "../api"; + +export function addLogsCommand(program: Command) { + program + .command("logs") + .addOption(new Option("-f, --follow", "Follow logs").default(false)) + .action(handle(logs)); +} diff --git a/packages/cli/src/commands/trade.ts b/packages/cli/src/commands/trade.ts index c4114af3..1d77e6d0 100644 --- a/packages/cli/src/commands/trade.ts +++ b/packages/cli/src/commands/trade.ts @@ -9,7 +9,7 @@ export function addTradeCommand(program: Command) { program .command("trade") .description("Live trading") - .addArgument(new Argument("", "Strategy name")) + .addArgument(new Argument("", "Strategy name").argOptional()) .addOption( new Option("-c, --config ", "Config file").default( DEFAULT_CONFIG_NAME, diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts new file mode 100644 index 00000000..3c34a215 --- /dev/null +++ b/packages/cli/src/commands/up.ts @@ -0,0 +1,12 @@ +import { Command, Option } from "commander"; +import { handle } from "../utils/command"; +import { up } from "../api"; + +export function addUpCommand(program: Command) { + program + .command("up") + .addOption( + new Option("-d, --detach", "Run in detached mode").default(false), + ) + .action(handle(up)); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6991caa7..3e32c1bd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,11 +15,19 @@ * * Repository URL: https://github.com/bludnic/opentrader */ -import { Command } from "commander"; + +import { logPath } from "./utils/app-path"; +process.env.LOG_FILE = logPath; + +import { Command, Option } from "commander"; import { addStopCommand } from "./commands/stop"; import { addBacktestCommand } from "./commands/backtest"; import { addGridLinesCommand } from "./commands/grid-lines"; import { addTradeCommand } from "./commands/trade"; +import { addUpCommand } from "./commands/up"; +import { addDownCommand } from "./commands/down"; +import { addLogsCommand } from "./commands/logs"; +import { Client } from "jayson/promise"; const program = new Command(); @@ -32,5 +40,8 @@ addBacktestCommand(program); addGridLinesCommand(program); addTradeCommand(program); addStopCommand(program); +addUpCommand(program); +addDownCommand(program); +addLogsCommand(program); program.parse(); diff --git a/packages/cli/src/utils/app-path.ts b/packages/cli/src/utils/app-path.ts new file mode 100644 index 00000000..6490ded4 --- /dev/null +++ b/packages/cli/src/utils/app-path.ts @@ -0,0 +1,7 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +const APP_DIR = ".opentrader"; + +export const appPath = join(homedir(), APP_DIR); +export const logPath = join(appPath, "log.log"); diff --git a/packages/cli/src/utils/bot.ts b/packages/cli/src/utils/bot.ts new file mode 100644 index 00000000..0ec2587b --- /dev/null +++ b/packages/cli/src/utils/bot.ts @@ -0,0 +1,216 @@ +import { ExchangeAccountWithCredentials, TBot, xprisma } from "@opentrader/db"; +import { logger } from "@opentrader/logger"; +import { BotProcessing } from "@opentrader/processing"; +import { BarSize } from "@opentrader/types"; +import { BotConfig, ConfigName, ExchangeConfig } from "../types"; + +/** + * Save exchange accounts to DB if not exists + * @param exchangesConfig - Exchange accounts configuration + */ +export async function createOrUpdateExchangeAccounts( + exchangesConfig: Record, +) { + const exchangeAccounts: ExchangeAccountWithCredentials[] = []; + + for (const [exchangeLabel, exchangeData] of Object.entries(exchangesConfig)) { + let exchangeAccount = await xprisma.exchangeAccount.findFirst({ + where: { + label: exchangeLabel, + }, + }); + + if (exchangeAccount) { + logger.info( + `Exchange account "${exchangeLabel}" found in DB. Updating credentials...`, + ); + + exchangeAccount = await xprisma.exchangeAccount.update({ + where: { + id: exchangeAccount.id, + }, + data: { + ...exchangeData, + label: exchangeLabel, + owner: { + connect: { + id: 1, + }, + }, + }, + }); + + logger.info(`Exchange account "${exchangeLabel}" updated`); + } else { + logger.info( + `Exchange account "${exchangeLabel}" not found. Adding to DB...`, + ); + + exchangeAccount = await xprisma.exchangeAccount.create({ + data: { + ...exchangeData, + label: exchangeLabel, + owner: { + connect: { + id: 1, + }, + }, + }, + }); + + logger.info(`Exchange account "${exchangeLabel}" created`); + } + + exchangeAccounts.push(exchangeAccount); + } + + return exchangeAccounts; +} + +type CreateOrUpdateBotOptions = { + config: ConfigName; + pair?: string; + exchange?: string; + timeframe?: BarSize; +}; + +export async function createOrUpdateBot( + strategyName: string, + options: CreateOrUpdateBotOptions, + botConfig: BotConfig, + exchangeAccounts: ExchangeAccountWithCredentials[], +): Promise { + const exchangeLabel = options.exchange || botConfig.exchange; + const botType = botConfig.type || "Bot"; + const botName = botConfig.name || "Default bot"; + const botLabel = botConfig.label || "default"; + const botTemplate = strategyName || botConfig.template; + const botTimeframe = options.timeframe || botConfig.timeframe || null; + const botPair = options.pair || botConfig.pair; + const [baseCurrency, quoteCurrency] = botPair.split("/"); + + const exchangeAccount = exchangeAccounts.find( + (exchangeAccount) => exchangeAccount.label === exchangeLabel, + ); + if (!exchangeAccount) { + throw new Error( + `Exchange account with label "${exchangeLabel}" not found. Check the exchanges config file.`, + ); + } + + let bot = await xprisma.bot.custom.findFirst({ + where: { + label: botLabel, + }, + }); + + if (bot) { + logger.info(`Bot "${botLabel}" found in DB. Updating...`); + + bot = await xprisma.bot.custom.update({ + where: { + id: bot.id, + }, + data: { + type: botType, + name: botName, + label: botLabel, + template: botTemplate, + timeframe: botTimeframe, + baseCurrency, + quoteCurrency, + settings: botConfig.settings as object, + state: {}, // resets bot state + exchangeAccount: { + connect: { + id: exchangeAccount.id, + }, + }, + owner: { + connect: { + id: 1, + }, + }, + }, + }); + + logger.info(`Bot "${botLabel}" updated`); + } else { + logger.info(`Bot "${botLabel}" not found. Adding to DB...`); + bot = await xprisma.bot.custom.create({ + data: { + type: botType, + name: botName, + label: botLabel, + template: strategyName, + timeframe: botTimeframe, + baseCurrency, + quoteCurrency, + settings: botConfig.settings as object, + exchangeAccount: { + connect: { + id: exchangeAccount.id, + }, + }, + owner: { + connect: { + id: 1, + }, + }, + }, + }); + + logger.info(`Bot "${botLabel}" created`); + } + + return bot; +} + +export async function startBot(botId: number) { + const botProcessor = await BotProcessing.fromId(botId); + await botProcessor.processStartCommand(); + + await enableBot(botId); + + await botProcessor.placePendingOrders(); +} + +export async function enableBot(botId: number) { + await xprisma.bot.custom.update({ + where: { + id: botId, + }, + data: { + enabled: true, + }, + }); +} + +export async function stopBot(botId: number) { + const botProcessor = await BotProcessing.fromId(botId); + await botProcessor.processStopCommand(); + + await disableBot(botId); +} + +export async function disableBot(botId: number) { + await xprisma.bot.custom.update({ + where: { + id: botId, + }, + data: { + enabled: false, + }, + }); +} + +export async function resetProcessing(botId: number) { + await xprisma.bot.custom.update({ + where: { + id: botId, + }, + data: { + processing: false, + }, + }); +} diff --git a/packages/cli/src/utils/pid.ts b/packages/cli/src/utils/pid.ts new file mode 100644 index 00000000..fb21621e --- /dev/null +++ b/packages/cli/src/utils/pid.ts @@ -0,0 +1,28 @@ +import { join } from "node:path"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { appPath } from "./app-path"; + +/** + * Save daemon process PID to file + */ +export const savePid = (pid: number) => { + mkdirSync(appPath, { recursive: true }); + writeFileSync(join(appPath, "pid"), pid.toString()); +}; + +/** + * Return current daemon process PID + */ +export const getPid = () => { + try { + const pid = parseInt(readFileSync(join(appPath, "pid"), "utf8")); + return isNaN(pid) ? null : pid; + } catch (err) { + return null; + } +}; + +export const clearPid = () => { + mkdirSync(appPath, { recursive: true }); + writeFileSync(join(appPath, "pid"), ""); +}; diff --git a/packages/cli/webpack.config.js b/packages/cli/webpack.config.js new file mode 100644 index 00000000..050c8b9b --- /dev/null +++ b/packages/cli/webpack.config.js @@ -0,0 +1,46 @@ +const path = require("node:path"); +const nodeExternals = require("webpack-node-externals"); + +module.exports = { + target: "node", + entry: { + main: "./src/index.ts", + daemon: "./src/api/up/daemon.ts", + }, + mode: "production", + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + alias: { + // resolve TS paths + "#db": path.resolve(__dirname, "../../packages/db/src"), + "#processing": path.resolve(__dirname, "../../packages/processing/src"), + "#exchanges": path.resolve(__dirname, "../../packages/exchanges/src"), + "#trpc": path.resolve(__dirname, "../../packages/trpc/src"), + }, + }, + // in order to ignore all modules in node_modules folder + externals: [ + nodeExternals({ + allowlist: [ + /^@opentrader/, // bundle only `@opentrader/*` packages + ], + }), + ], + externalsPresets: { + node: true, // in order to ignore built-in modules like path, fs, etc. + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "dist"), + clean: true, + }, +}; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 0bebc43a..15b94e02 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -1,11 +1,33 @@ import pino from "pino"; -export const logger = pino({ - transport: { - target: "pino-pretty", - options: { - ignore: "pid,hostname", - sync: true, - }, - }, -}); +const logFile = process.env.LOG_FILE; + +export const logger = logFile + ? pino({ + transport: { + targets: [ + { + target: "pino-pretty", + options: { + ignore: "pid,hostname", + sync: true, + }, + }, + { + target: "pino/file", + options: { + destination: logFile, + }, + }, + ], + }, + }) + : pino({ + transport: { + target: "pino-pretty", + options: { + ignore: "pid,hostname", + sync: true, + }, + }, + }); diff --git a/test-strategy.mjs b/test-strategy.mjs new file mode 100644 index 00000000..3b1b179d --- /dev/null +++ b/test-strategy.mjs @@ -0,0 +1,3 @@ +export default function* testStrategy() { + console.log("Hello world"); +} From 9877c5160cf86c728153ee39c38123084ebf57f3 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 10 Jun 2024 05:49:20 +0100 Subject: [PATCH 02/12] feat(cli): prettify logs --- packages/cli/src/api/logs.ts | 18 +++++++++++------- packages/cli/src/utils/pretty-log.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/utils/pretty-log.ts diff --git a/packages/cli/src/api/logs.ts b/packages/cli/src/api/logs.ts index 0eaaeaeb..f5b46440 100644 --- a/packages/cli/src/api/logs.ts +++ b/packages/cli/src/api/logs.ts @@ -4,13 +4,11 @@ import { readFileSync, createReadStream, watchFile, - read, } from "fs"; import { createInterface } from "readline"; -import { CommandResult } from "../types"; +import { prettyLog } from "../utils/pretty-log"; import { logPath } from "../utils/app-path"; - -logger; +import { CommandResult } from "../types"; type Options = { follow: boolean; @@ -43,7 +41,10 @@ export async function logs(options: Options): Promise { // Print the last 10 lines for (const line of logsLines.slice(-10)) { - console.log(line); + const isBreak = line === ""; + if (!isBreak) { + prettyLog(line); + } } // Keep track of the last file size @@ -58,7 +59,7 @@ export async function logs(options: Options): Promise { const rl = createInterface({ input: stream }); rl.on("line", (line) => { - console.log(line); + prettyLog(line); }); rl.on("close", () => { @@ -71,7 +72,10 @@ export async function logs(options: Options): Promise { const logsLines = logsData.split("\n"); for (const line of logsLines) { - console.log(line); + const isBreak = line === ""; + if (!isBreak) { + prettyLog(line); + } } } diff --git a/packages/cli/src/utils/pretty-log.ts b/packages/cli/src/utils/pretty-log.ts new file mode 100644 index 00000000..9a4f9abd --- /dev/null +++ b/packages/cli/src/utils/pretty-log.ts @@ -0,0 +1,9 @@ +import { prettyFactory } from "pino-pretty"; + +const pretty = prettyFactory({}); + +export const prettyLog = (message: string) => { + const parsedMessage = JSON.parse(message); + const prettifiedMessage = pretty(parsedMessage); + console.log(prettifiedMessage.replace(/\n/g, "")); +}; From 88179ac187098aea78f86275cd7af99a07d3a85b Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 13 Jun 2024 23:13:36 +0100 Subject: [PATCH 03/12] feat(cli): add version command --- packages/cli/src/api/index.ts | 1 + packages/cli/src/api/version.ts | 7 +++++++ packages/cli/src/commands/version.ts | 7 +++++++ packages/cli/src/index.ts | 3 ++- 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/api/version.ts create mode 100644 packages/cli/src/commands/version.ts diff --git a/packages/cli/src/api/index.ts b/packages/cli/src/api/index.ts index 1fbddb52..2cdb607a 100644 --- a/packages/cli/src/api/index.ts +++ b/packages/cli/src/api/index.ts @@ -5,3 +5,4 @@ export * from "./stop-command"; export * from "./up/index"; export * from "./down"; export * from "./logs"; +export * from "./version"; diff --git a/packages/cli/src/api/version.ts b/packages/cli/src/api/version.ts new file mode 100644 index 00000000..ddf3ee04 --- /dev/null +++ b/packages/cli/src/api/version.ts @@ -0,0 +1,7 @@ +import packageJson from "../../package.json"; + +export async function version() { + return { + result: `OpenTrader version: ${packageJson.version}`, + }; +} diff --git a/packages/cli/src/commands/version.ts b/packages/cli/src/commands/version.ts new file mode 100644 index 00000000..7281d43f --- /dev/null +++ b/packages/cli/src/commands/version.ts @@ -0,0 +1,7 @@ +import { Command, Option } from "commander"; +import { handle } from "../utils/command"; +import { version } from "../api"; + +export function addVersionCommand(program: Command) { + program.command("version").action(handle(version)); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3e32c1bd..218b0755 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -27,7 +27,7 @@ import { addTradeCommand } from "./commands/trade"; import { addUpCommand } from "./commands/up"; import { addDownCommand } from "./commands/down"; import { addLogsCommand } from "./commands/logs"; -import { Client } from "jayson/promise"; +import { addVersionCommand } from "./commands/version"; const program = new Command(); @@ -43,5 +43,6 @@ addStopCommand(program); addUpCommand(program); addDownCommand(program); addLogsCommand(program); +addVersionCommand(program); program.parse(); From 153ba8fe07860add72f59401af8565f0d13bd2dd Mon Sep 17 00:00:00 2001 From: bludnic Date: Sun, 16 Jun 2024 22:47:18 +0100 Subject: [PATCH 04/12] chore(bot-templates): add Jest --- packages/bot-templates/jest.config.js | 6 ++++++ packages/bot-templates/package.json | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 packages/bot-templates/jest.config.js diff --git a/packages/bot-templates/jest.config.js b/packages/bot-templates/jest.config.js new file mode 100644 index 00000000..9fb637a8 --- /dev/null +++ b/packages/bot-templates/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleDirectories: ['node_modules', ''], +}; diff --git a/packages/bot-templates/package.json b/packages/bot-templates/package.json index 13ddbb5b..36fd79d7 100644 --- a/packages/bot-templates/package.json +++ b/packages/bot-templates/package.json @@ -7,16 +7,21 @@ "scripts": { "build": "tsc", "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "test": "jest" }, "author": "bludnic", "license": "Apache-2.0", "devDependencies": { + "@jest/globals": "^29.7.0", "@opentrader/eslint-config": "workspace:*", "@opentrader/tsconfig": "workspace:*", "@opentrader/types": "workspace:*", + "@types/jest": "^29.5.12", "@types/node": "^20.12.11", "eslint": "8.54.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", "typescript": "5.4.5" }, "dependencies": { From 1bab28fd901399fcdfe0727f265494db6c859073 Mon Sep 17 00:00:00 2001 From: bludnic Date: Sun, 16 Jun 2024 23:10:18 +0100 Subject: [PATCH 05/12] feat(strategies): add `findStrategy` func --- packages/bot-templates/index.d.ts | 1 + packages/bot-templates/package.json | 5 ++++ packages/bot-templates/server.d.ts | 1 + packages/bot-templates/src/server.ts | 34 ++++++++++++++++++++++++++++ packages/cli/src/api/run-backtest.ts | 23 ++++++------------- 5 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 packages/bot-templates/index.d.ts create mode 100644 packages/bot-templates/server.d.ts create mode 100644 packages/bot-templates/src/server.ts diff --git a/packages/bot-templates/index.d.ts b/packages/bot-templates/index.d.ts new file mode 100644 index 00000000..3bd16e17 --- /dev/null +++ b/packages/bot-templates/index.d.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/bot-templates/package.json b/packages/bot-templates/package.json index 36fd79d7..8f034389 100644 --- a/packages/bot-templates/package.json +++ b/packages/bot-templates/package.json @@ -4,6 +4,11 @@ "description": "", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./server": "./src/server.ts", + "./dist": "./src/index.ts" + }, "scripts": { "build": "tsc", "lint": "eslint . --quiet", diff --git a/packages/bot-templates/server.d.ts b/packages/bot-templates/server.d.ts new file mode 100644 index 00000000..abe846de --- /dev/null +++ b/packages/bot-templates/server.d.ts @@ -0,0 +1 @@ +export * from "./src/server"; diff --git a/packages/bot-templates/src/server.ts b/packages/bot-templates/src/server.ts new file mode 100644 index 00000000..d124dec7 --- /dev/null +++ b/packages/bot-templates/src/server.ts @@ -0,0 +1,34 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import type { BotTemplate } from "@opentrader/bot-processor"; + +import * as templates from "./templates"; + +export async function findStrategy( + strategyNameOrFile: string, +): Promise> { + let strategyFn; + + const isCustomStrategyFile = existsSync( + join(process.cwd(), strategyNameOrFile), + ); + + const strategyExists = strategyNameOrFile in templates; + + if (isCustomStrategyFile) { + const { default: fn } = await import( + join(process.cwd(), strategyNameOrFile) + ); + strategyFn = fn; + } else if (strategyExists) { + strategyFn = templates[strategyNameOrFile as keyof typeof templates]; + } else { + throw new Error( + `Strategy "${strategyNameOrFile}" does not exists. Use one of predefined strategies: ${Object.keys( + templates, + ).join(", ")}, or specify the path to the custom strategy file.`, + ); + } + + return strategyFn; +} diff --git a/packages/cli/src/api/run-backtest.ts b/packages/cli/src/api/run-backtest.ts index 3c15757b..9d2a178b 100644 --- a/packages/cli/src/api/run-backtest.ts +++ b/packages/cli/src/api/run-backtest.ts @@ -1,5 +1,7 @@ import { pro as ccxt } from "ccxt"; import { templates } from "@opentrader/bot-templates"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { BotTemplate } from "@opentrader/bot-processor"; import { Backtesting } from "@opentrader/backtesting"; import { CCXTCandlesProvider } from "@opentrader/bot"; import { logger } from "@opentrader/logger"; @@ -7,8 +9,6 @@ import { exchangeCodeMapCCXT } from "@opentrader/exchanges"; import type { BarSize, ExchangeCode, ICandlestick } from "@opentrader/types"; import type { CommandResult, ConfigName } from "../types"; import { readBotConfig } from "../config"; -import { existsSync } from "fs"; -import { join } from "path"; type Options = { config: ConfigName; @@ -26,27 +26,18 @@ export async function runBacktest( const botConfig = readBotConfig(options.config); logger.debug(botConfig, "Parsed bot config"); - let strategyFn; - const isCustomStrategyFile = existsSync(join(process.cwd(), strategyName)); - const strategyExists = strategyName in templates; + let strategyFn: BotTemplate; - if (isCustomStrategyFile) { - const { default: fn } = await import(join(process.cwd(), strategyName)); - strategyFn = fn; - } else if (strategyExists) { - strategyFn = templates[strategyName]; - } else { - const availableStrategies = Object.keys(templates).join(", "); - logger.info( - `Strategy "${strategyName}" does not exists. Available strategies: ${availableStrategies}`, - ); + try { + strategyFn = await findStrategy(strategyName); + } catch (err) { + logger.info((err as Error).message); return { result: undefined, }; } - const botTemplate = strategyName || botConfig.template; const botTimeframe = options.timeframe || botConfig.timeframe || null; const botPair = options.pair || botConfig.pair; const [baseCurrency, quoteCurrency] = botPair.split("/"); From f3bf463abce4cd98da5f7fcedae2fcef0e8cc165 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 00:28:01 +0100 Subject: [PATCH 06/12] feat: support custom strategy file --- packages/bot-templates/src/server.ts | 29 +++++++++------- .../bot/src/processing/candles.processor.ts | 6 ++-- packages/cli/src/api/run-backtest.ts | 8 ++--- packages/cli/src/api/stop-command.ts | 15 ++++----- packages/cli/src/api/up/daemon.ts | 33 +++++++++---------- packages/eslint-config/rules/rules.js | 1 + packages/processing/src/bot/bot.processing.ts | 6 ++-- .../routers/private/bot/create-bot/handler.ts | 9 +++-- test-strategy.mjs | 3 -- 9 files changed, 54 insertions(+), 56 deletions(-) delete mode 100644 test-strategy.mjs diff --git a/packages/bot-templates/src/server.ts b/packages/bot-templates/src/server.ts index d124dec7..96153e81 100644 --- a/packages/bot-templates/src/server.ts +++ b/packages/bot-templates/src/server.ts @@ -1,24 +1,27 @@ -import { existsSync } from "node:fs"; import { join } from "node:path"; import type { BotTemplate } from "@opentrader/bot-processor"; - import * as templates from "./templates"; +type FindStrategyResult = { + strategyFn: BotTemplate; + isCustom: boolean; + strategyFilePath: string; // empty string if not a custom strategy +}; + export async function findStrategy( strategyNameOrFile: string, -): Promise> { +): Promise { let strategyFn; - const isCustomStrategyFile = existsSync( - join(process.cwd(), strategyNameOrFile), - ); + const isCustomStrategyFile = strategyNameOrFile.endsWith(".js"); + const customStrategyFilePath = strategyNameOrFile.startsWith("/") + ? strategyNameOrFile + : join(process.cwd(), strategyNameOrFile); const strategyExists = strategyNameOrFile in templates; if (isCustomStrategyFile) { - const { default: fn } = await import( - join(process.cwd(), strategyNameOrFile) - ); + const { default: fn } = await import(customStrategyFilePath); strategyFn = fn; } else if (strategyExists) { strategyFn = templates[strategyNameOrFile as keyof typeof templates]; @@ -26,9 +29,13 @@ export async function findStrategy( throw new Error( `Strategy "${strategyNameOrFile}" does not exists. Use one of predefined strategies: ${Object.keys( templates, - ).join(", ")}, or specify the path to the custom strategy file.`, + ).join(", ")}, or specify the path to custom strategy file.`, ); } - return strategyFn; + return { + strategyFn, + isCustom: isCustomStrategyFile, + strategyFilePath: isCustomStrategyFile ? customStrategyFilePath : "", + }; } diff --git a/packages/bot/src/processing/candles.processor.ts b/packages/bot/src/processing/candles.processor.ts index b3f452d5..74767da4 100644 --- a/packages/bot/src/processing/candles.processor.ts +++ b/packages/bot/src/processing/candles.processor.ts @@ -4,7 +4,7 @@ import type { TBot } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; import { BotProcessing } from "@opentrader/processing"; import type { BarSize } from "@opentrader/types"; -import { findTemplate } from "@opentrader/bot-templates"; +import { findStrategy } from "@opentrader/bot-templates/server"; import type { CandleEvent } from "../channels"; import { CandlesChannel } from "../channels"; @@ -60,11 +60,11 @@ export class CandlesProcessor { return; } - const template = findTemplate(bot.template); + const { strategyFn } = await findStrategy(bot.template); await channel.add( symbol, bot.timeframe as BarSize, - template.requiredHistory, + strategyFn.requiredHistory, ); } diff --git a/packages/cli/src/api/run-backtest.ts b/packages/cli/src/api/run-backtest.ts index 9d2a178b..8091fcac 100644 --- a/packages/cli/src/api/run-backtest.ts +++ b/packages/cli/src/api/run-backtest.ts @@ -1,7 +1,6 @@ import { pro as ccxt } from "ccxt"; import { templates } from "@opentrader/bot-templates"; import { findStrategy } from "@opentrader/bot-templates/server"; -import { BotTemplate } from "@opentrader/bot-processor"; import { Backtesting } from "@opentrader/backtesting"; import { CCXTCandlesProvider } from "@opentrader/bot"; import { logger } from "@opentrader/logger"; @@ -26,10 +25,9 @@ export async function runBacktest( const botConfig = readBotConfig(options.config); logger.debug(botConfig, "Parsed bot config"); - let strategyFn: BotTemplate; - + let strategy: Awaited>; try { - strategyFn = await findStrategy(strategyName); + strategy = await findStrategy(strategyName); } catch (err) { logger.info((err as Error).message); @@ -56,7 +54,7 @@ export async function runBacktest( exchangeCode: options.exchange, settings: botConfig.settings, }, - botTemplate: strategyFn, + botTemplate: strategy.strategyFn, }); return new Promise((resolve) => { diff --git a/packages/cli/src/api/stop-command.ts b/packages/cli/src/api/stop-command.ts index 6b56da87..305b4b2a 100644 --- a/packages/cli/src/api/stop-command.ts +++ b/packages/cli/src/api/stop-command.ts @@ -1,5 +1,5 @@ -import { templates } from "@opentrader/bot-templates"; -import { xprisma } from "@opentrader/db/dist"; +import { findStrategy } from "@opentrader/bot-templates/server"; +import { xprisma } from "@opentrader/db"; import { logger } from "@opentrader/logger"; import { BotProcessing } from "@opentrader/processing"; import type { CommandResult, ConfigName } from "../types"; @@ -30,12 +30,11 @@ export async function stopCommand(options: { }; } - const strategyExists = bot.template in templates; - if (!strategyExists) { - const availableStrategies = Object.keys(templates).join(", "); - logger.info( - `Strategy "${bot.template}" does not exists. Available strategies: ${availableStrategies}`, - ); + // check if bot strategy file exists + try { + await findStrategy(bot.template); + } catch (err) { + logger.info((err as Error).message); return { result: undefined, diff --git a/packages/cli/src/api/up/daemon.ts b/packages/cli/src/api/up/daemon.ts index eac67919..0e6b2683 100644 --- a/packages/cli/src/api/up/daemon.ts +++ b/packages/cli/src/api/up/daemon.ts @@ -1,10 +1,10 @@ import { Server } from "jayson/promise"; - import { Processor } from "@opentrader/bot"; -import { ExchangeAccountWithCredentials, xprisma } from "@opentrader/db"; +import type { ExchangeAccountWithCredentials } from "@opentrader/db"; +import { xprisma } from "@opentrader/db"; import { logger } from "@opentrader/logger"; -import { BarSize } from "@opentrader/types"; -import { templates } from "@opentrader/bot-templates"; +import type { BarSize } from "@opentrader/types"; +import { findStrategy } from "@opentrader/bot-templates/server"; import { readBotConfig, readExchangesConfig } from "../../config"; import { createOrUpdateBot, @@ -13,7 +13,7 @@ import { startBot, stopBot, } from "../../utils/bot"; -import { ConfigName } from "../../types"; +import type { ConfigName } from "../../types"; let app: App | null = null; @@ -102,12 +102,11 @@ const server = Server({ const exchangesConfig = readExchangesConfig(options.config); logger.debug(exchangesConfig, "Parsed exchanges config"); - const strategyExists = strategyName in templates; - if (!strategyExists) { - const availableStrategies = Object.keys(templates).join(", "); - logger.info( - `Strategy "${strategyName}" does not exists. Available strategies: ${availableStrategies}`, - ); + let strategy: Awaited>; + try { + strategy = await findStrategy(strategyName); + } catch (err) { + logger.info((err as Error).message); return false; } @@ -116,7 +115,7 @@ const server = Server({ const exchangeAccounts: ExchangeAccountWithCredentials[] = await createOrUpdateExchangeAccounts(exchangesConfig); const bot = await createOrUpdateBot( - strategyName, + strategy.isCustom ? strategy.strategyFilePath : strategyName, options, config, exchangeAccounts, @@ -168,12 +167,10 @@ const server = Server({ return false; } - const strategyExists = bot.template in templates; - if (!strategyExists) { - const availableStrategies = Object.keys(templates).join(", "); - logger.info( - `Strategy "${bot.template}" does not exists. Available strategies: ${availableStrategies}`, - ); + try { + await findStrategy(bot.template); + } catch (err) { + logger.info((err as Error).message); return false; } diff --git a/packages/eslint-config/rules/rules.js b/packages/eslint-config/rules/rules.js index 27467166..2a7f1102 100644 --- a/packages/eslint-config/rules/rules.js +++ b/packages/eslint-config/rules/rules.js @@ -45,4 +45,5 @@ module.exports = { "@typescript-eslint/no-unused-vars": "warn", "no-unused-vars": "warn", "require-yield": "off", + "import/namespace": "off", }; diff --git a/packages/processing/src/bot/bot.processing.ts b/packages/processing/src/bot/bot.processing.ts index f9bd7f71..adc78d60 100644 --- a/packages/processing/src/bot/bot.processing.ts +++ b/packages/processing/src/bot/bot.processing.ts @@ -4,7 +4,7 @@ import type { MarketData, } from "@opentrader/bot-processor"; import { createStrategyRunner } from "@opentrader/bot-processor"; -import { findTemplate } from "@opentrader/bot-templates"; +import { findStrategy } from "@opentrader/bot-templates/server"; import { exchangeProvider } from "@opentrader/exchanges"; import type { TBot } from "@opentrader/db"; import { xprisma } from "@opentrader/db"; @@ -170,13 +170,13 @@ export class BotProcessing { }; const storeAdapter = new BotStoreAdapter(() => this.stop()); - const botTemplate = findTemplate(this.bot.template); + const { strategyFn } = await findStrategy(this.bot.template); const processor = createStrategyRunner({ store: storeAdapter, exchange, botConfig: configuration, - botTemplate, + botTemplate: strategyFn, }); return processor; 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 a21b1e27..2f74f4f9 100644 --- a/packages/trpc/src/routers/private/bot/create-bot/handler.ts +++ b/packages/trpc/src/routers/private/bot/create-bot/handler.ts @@ -1,5 +1,4 @@ -import { BotTemplate } from "@opentrader/bot-processor"; -import { findTemplate } from "@opentrader/bot-templates"; +import { findStrategy } from "@opentrader/bot-templates/server"; import { TRPCError } from "@trpc/server"; import { xprisma } from "@opentrader/db"; import { eventBus } from "../../../../event-bus"; @@ -32,9 +31,9 @@ export async function createBot({ ctx, input }: Options) { }); } - let strategy: BotTemplate; + let strategy: Awaited>; try { - strategy = findTemplate(data.template); + strategy = await findStrategy(data.template); } catch (err) { throw new TRPCError({ message: `Strategy ${data.template} not found`, @@ -42,7 +41,7 @@ export async function createBot({ ctx, input }: Options) { }); } - const parsed = strategy.schema.safeParse(data.settings); + const parsed = strategy.strategyFn.schema.safeParse(data.settings); if (!parsed.success) { throw new TRPCError({ message: `Invalid strategy params: ${parsed.error.message}`, diff --git a/test-strategy.mjs b/test-strategy.mjs deleted file mode 100644 index 3b1b179d..00000000 --- a/test-strategy.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export default function* testStrategy() { - console.log("Hello world"); -} From cb77238631623bd3915143de2fef99fdb3f705bf Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 01:38:45 +0100 Subject: [PATCH 07/12] chore: move CLI package to /apps --- {packages => apps}/cli/.eslintrc.js | 0 {packages => apps}/cli/.gitignore | 0 {packages => apps}/cli/LICENSE | 0 {packages => apps}/cli/config.default.json5 | 0 {packages => apps}/cli/config.dev.json5 | 0 {packages => apps}/cli/config.prod.json5 | 0 {packages => apps}/cli/exchanges.default.json5 | 0 {packages => apps}/cli/exchanges.dev.json5 | 0 {packages => apps}/cli/exchanges.prod.json5 | 0 {packages => apps}/cli/package.json | 0 {packages => apps}/cli/src/api/down.ts | 0 {packages => apps}/cli/src/api/grid-lines.ts | 0 {packages => apps}/cli/src/api/index.ts | 0 {packages => apps}/cli/src/api/logs.ts | 0 {packages => apps}/cli/src/api/run-backtest.ts | 0 {packages => apps}/cli/src/api/run-trading.ts | 0 {packages => apps}/cli/src/api/stop-command.ts | 0 {packages => apps}/cli/src/api/up/daemon.ts | 0 {packages => apps}/cli/src/api/up/index.ts | 0 {packages => apps}/cli/src/api/version.ts | 0 {packages => apps}/cli/src/commands/backtest.ts | 0 {packages => apps}/cli/src/commands/down.ts | 0 {packages => apps}/cli/src/commands/grid-lines.ts | 0 {packages => apps}/cli/src/commands/logs.ts | 0 {packages => apps}/cli/src/commands/stop.ts | 0 {packages => apps}/cli/src/commands/trade.ts | 0 {packages => apps}/cli/src/commands/up.ts | 0 {packages => apps}/cli/src/commands/version.ts | 0 {packages => apps}/cli/src/config.ts | 0 {packages => apps}/cli/src/index.ts | 0 {packages => apps}/cli/src/types.ts | 0 {packages => apps}/cli/src/utils/app-path.ts | 0 {packages => apps}/cli/src/utils/bot.ts | 0 {packages => apps}/cli/src/utils/command.ts | 0 {packages => apps}/cli/src/utils/pid.ts | 0 {packages => apps}/cli/src/utils/pretty-log.ts | 0 {packages => apps}/cli/src/utils/validate.ts | 0 {packages => apps}/cli/tsconfig.json | 0 {packages => apps}/cli/webpack.config.js | 0 bin/opentrader.sh | 2 +- pnpm-workspace.yaml | 1 + strategies/test.js | 5 +++++ 42 files changed, 7 insertions(+), 1 deletion(-) rename {packages => apps}/cli/.eslintrc.js (100%) rename {packages => apps}/cli/.gitignore (100%) rename {packages => apps}/cli/LICENSE (100%) rename {packages => apps}/cli/config.default.json5 (100%) rename {packages => apps}/cli/config.dev.json5 (100%) rename {packages => apps}/cli/config.prod.json5 (100%) rename {packages => apps}/cli/exchanges.default.json5 (100%) rename {packages => apps}/cli/exchanges.dev.json5 (100%) rename {packages => apps}/cli/exchanges.prod.json5 (100%) rename {packages => apps}/cli/package.json (100%) rename {packages => apps}/cli/src/api/down.ts (100%) rename {packages => apps}/cli/src/api/grid-lines.ts (100%) rename {packages => apps}/cli/src/api/index.ts (100%) rename {packages => apps}/cli/src/api/logs.ts (100%) rename {packages => apps}/cli/src/api/run-backtest.ts (100%) rename {packages => apps}/cli/src/api/run-trading.ts (100%) rename {packages => apps}/cli/src/api/stop-command.ts (100%) rename {packages => apps}/cli/src/api/up/daemon.ts (100%) rename {packages => apps}/cli/src/api/up/index.ts (100%) rename {packages => apps}/cli/src/api/version.ts (100%) rename {packages => apps}/cli/src/commands/backtest.ts (100%) rename {packages => apps}/cli/src/commands/down.ts (100%) rename {packages => apps}/cli/src/commands/grid-lines.ts (100%) rename {packages => apps}/cli/src/commands/logs.ts (100%) rename {packages => apps}/cli/src/commands/stop.ts (100%) rename {packages => apps}/cli/src/commands/trade.ts (100%) rename {packages => apps}/cli/src/commands/up.ts (100%) rename {packages => apps}/cli/src/commands/version.ts (100%) rename {packages => apps}/cli/src/config.ts (100%) rename {packages => apps}/cli/src/index.ts (100%) rename {packages => apps}/cli/src/types.ts (100%) rename {packages => apps}/cli/src/utils/app-path.ts (100%) rename {packages => apps}/cli/src/utils/bot.ts (100%) rename {packages => apps}/cli/src/utils/command.ts (100%) rename {packages => apps}/cli/src/utils/pid.ts (100%) rename {packages => apps}/cli/src/utils/pretty-log.ts (100%) rename {packages => apps}/cli/src/utils/validate.ts (100%) rename {packages => apps}/cli/tsconfig.json (100%) rename {packages => apps}/cli/webpack.config.js (100%) create mode 100644 strategies/test.js diff --git a/packages/cli/.eslintrc.js b/apps/cli/.eslintrc.js similarity index 100% rename from packages/cli/.eslintrc.js rename to apps/cli/.eslintrc.js diff --git a/packages/cli/.gitignore b/apps/cli/.gitignore similarity index 100% rename from packages/cli/.gitignore rename to apps/cli/.gitignore diff --git a/packages/cli/LICENSE b/apps/cli/LICENSE similarity index 100% rename from packages/cli/LICENSE rename to apps/cli/LICENSE diff --git a/packages/cli/config.default.json5 b/apps/cli/config.default.json5 similarity index 100% rename from packages/cli/config.default.json5 rename to apps/cli/config.default.json5 diff --git a/packages/cli/config.dev.json5 b/apps/cli/config.dev.json5 similarity index 100% rename from packages/cli/config.dev.json5 rename to apps/cli/config.dev.json5 diff --git a/packages/cli/config.prod.json5 b/apps/cli/config.prod.json5 similarity index 100% rename from packages/cli/config.prod.json5 rename to apps/cli/config.prod.json5 diff --git a/packages/cli/exchanges.default.json5 b/apps/cli/exchanges.default.json5 similarity index 100% rename from packages/cli/exchanges.default.json5 rename to apps/cli/exchanges.default.json5 diff --git a/packages/cli/exchanges.dev.json5 b/apps/cli/exchanges.dev.json5 similarity index 100% rename from packages/cli/exchanges.dev.json5 rename to apps/cli/exchanges.dev.json5 diff --git a/packages/cli/exchanges.prod.json5 b/apps/cli/exchanges.prod.json5 similarity index 100% rename from packages/cli/exchanges.prod.json5 rename to apps/cli/exchanges.prod.json5 diff --git a/packages/cli/package.json b/apps/cli/package.json similarity index 100% rename from packages/cli/package.json rename to apps/cli/package.json diff --git a/packages/cli/src/api/down.ts b/apps/cli/src/api/down.ts similarity index 100% rename from packages/cli/src/api/down.ts rename to apps/cli/src/api/down.ts diff --git a/packages/cli/src/api/grid-lines.ts b/apps/cli/src/api/grid-lines.ts similarity index 100% rename from packages/cli/src/api/grid-lines.ts rename to apps/cli/src/api/grid-lines.ts diff --git a/packages/cli/src/api/index.ts b/apps/cli/src/api/index.ts similarity index 100% rename from packages/cli/src/api/index.ts rename to apps/cli/src/api/index.ts diff --git a/packages/cli/src/api/logs.ts b/apps/cli/src/api/logs.ts similarity index 100% rename from packages/cli/src/api/logs.ts rename to apps/cli/src/api/logs.ts diff --git a/packages/cli/src/api/run-backtest.ts b/apps/cli/src/api/run-backtest.ts similarity index 100% rename from packages/cli/src/api/run-backtest.ts rename to apps/cli/src/api/run-backtest.ts diff --git a/packages/cli/src/api/run-trading.ts b/apps/cli/src/api/run-trading.ts similarity index 100% rename from packages/cli/src/api/run-trading.ts rename to apps/cli/src/api/run-trading.ts diff --git a/packages/cli/src/api/stop-command.ts b/apps/cli/src/api/stop-command.ts similarity index 100% rename from packages/cli/src/api/stop-command.ts rename to apps/cli/src/api/stop-command.ts diff --git a/packages/cli/src/api/up/daemon.ts b/apps/cli/src/api/up/daemon.ts similarity index 100% rename from packages/cli/src/api/up/daemon.ts rename to apps/cli/src/api/up/daemon.ts diff --git a/packages/cli/src/api/up/index.ts b/apps/cli/src/api/up/index.ts similarity index 100% rename from packages/cli/src/api/up/index.ts rename to apps/cli/src/api/up/index.ts diff --git a/packages/cli/src/api/version.ts b/apps/cli/src/api/version.ts similarity index 100% rename from packages/cli/src/api/version.ts rename to apps/cli/src/api/version.ts diff --git a/packages/cli/src/commands/backtest.ts b/apps/cli/src/commands/backtest.ts similarity index 100% rename from packages/cli/src/commands/backtest.ts rename to apps/cli/src/commands/backtest.ts diff --git a/packages/cli/src/commands/down.ts b/apps/cli/src/commands/down.ts similarity index 100% rename from packages/cli/src/commands/down.ts rename to apps/cli/src/commands/down.ts diff --git a/packages/cli/src/commands/grid-lines.ts b/apps/cli/src/commands/grid-lines.ts similarity index 100% rename from packages/cli/src/commands/grid-lines.ts rename to apps/cli/src/commands/grid-lines.ts diff --git a/packages/cli/src/commands/logs.ts b/apps/cli/src/commands/logs.ts similarity index 100% rename from packages/cli/src/commands/logs.ts rename to apps/cli/src/commands/logs.ts diff --git a/packages/cli/src/commands/stop.ts b/apps/cli/src/commands/stop.ts similarity index 100% rename from packages/cli/src/commands/stop.ts rename to apps/cli/src/commands/stop.ts diff --git a/packages/cli/src/commands/trade.ts b/apps/cli/src/commands/trade.ts similarity index 100% rename from packages/cli/src/commands/trade.ts rename to apps/cli/src/commands/trade.ts diff --git a/packages/cli/src/commands/up.ts b/apps/cli/src/commands/up.ts similarity index 100% rename from packages/cli/src/commands/up.ts rename to apps/cli/src/commands/up.ts diff --git a/packages/cli/src/commands/version.ts b/apps/cli/src/commands/version.ts similarity index 100% rename from packages/cli/src/commands/version.ts rename to apps/cli/src/commands/version.ts diff --git a/packages/cli/src/config.ts b/apps/cli/src/config.ts similarity index 100% rename from packages/cli/src/config.ts rename to apps/cli/src/config.ts diff --git a/packages/cli/src/index.ts b/apps/cli/src/index.ts similarity index 100% rename from packages/cli/src/index.ts rename to apps/cli/src/index.ts diff --git a/packages/cli/src/types.ts b/apps/cli/src/types.ts similarity index 100% rename from packages/cli/src/types.ts rename to apps/cli/src/types.ts diff --git a/packages/cli/src/utils/app-path.ts b/apps/cli/src/utils/app-path.ts similarity index 100% rename from packages/cli/src/utils/app-path.ts rename to apps/cli/src/utils/app-path.ts diff --git a/packages/cli/src/utils/bot.ts b/apps/cli/src/utils/bot.ts similarity index 100% rename from packages/cli/src/utils/bot.ts rename to apps/cli/src/utils/bot.ts diff --git a/packages/cli/src/utils/command.ts b/apps/cli/src/utils/command.ts similarity index 100% rename from packages/cli/src/utils/command.ts rename to apps/cli/src/utils/command.ts diff --git a/packages/cli/src/utils/pid.ts b/apps/cli/src/utils/pid.ts similarity index 100% rename from packages/cli/src/utils/pid.ts rename to apps/cli/src/utils/pid.ts diff --git a/packages/cli/src/utils/pretty-log.ts b/apps/cli/src/utils/pretty-log.ts similarity index 100% rename from packages/cli/src/utils/pretty-log.ts rename to apps/cli/src/utils/pretty-log.ts diff --git a/packages/cli/src/utils/validate.ts b/apps/cli/src/utils/validate.ts similarity index 100% rename from packages/cli/src/utils/validate.ts rename to apps/cli/src/utils/validate.ts diff --git a/packages/cli/tsconfig.json b/apps/cli/tsconfig.json similarity index 100% rename from packages/cli/tsconfig.json rename to apps/cli/tsconfig.json diff --git a/packages/cli/webpack.config.js b/apps/cli/webpack.config.js similarity index 100% rename from packages/cli/webpack.config.js rename to apps/cli/webpack.config.js diff --git a/bin/opentrader.sh b/bin/opentrader.sh index 7db65707..968a1f67 100755 --- a/bin/opentrader.sh +++ b/bin/opentrader.sh @@ -1,3 +1,3 @@ #!/bin/bash # Use "$@" to pass all additional command line arguments to your script -pnpm exec ts-node --transpile-only packages/cli/src/index.ts "$@" +pnpm exec ts-node --transpile-only apps/cli/src/index.ts "$@" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2f7fc52c..c85c1310 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: + - "apps/*" - "packages/*" - "pro/*" diff --git a/strategies/test.js b/strategies/test.js new file mode 100644 index 00000000..3b40e27a --- /dev/null +++ b/strategies/test.js @@ -0,0 +1,5 @@ +function* strategy(ctx) { + console.log('Test strategy executed!', ctx.market.candle) +} + +module.exports = strategy From 5efe25d0800571e323c0f560de58d5c562e3988b Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 01:41:08 +0100 Subject: [PATCH 08/12] chore: rename binaries --- apps/cli/package.json | 3 +-- bin/{ot.sh => dev.sh} | 2 +- bin/opentrader.sh | 2 +- package.json | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) rename bin/{ot.sh => dev.sh} (57%) diff --git a/apps/cli/package.json b/apps/cli/package.json index e6327df9..67f7444e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -5,8 +5,7 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "tsc", - "build:webpack": "webpack", + "build": "webpack", "lint": "eslint . --quiet", "lint:fix": "eslint . --fix" }, diff --git a/bin/ot.sh b/bin/dev.sh similarity index 57% rename from bin/ot.sh rename to bin/dev.sh index 7518d175..968a1f67 100755 --- a/bin/ot.sh +++ b/bin/dev.sh @@ -1,3 +1,3 @@ #!/bin/bash # Use "$@" to pass all additional command line arguments to your script -node packages/cli/dist/main.js "$@" +pnpm exec ts-node --transpile-only apps/cli/src/index.ts "$@" diff --git a/bin/opentrader.sh b/bin/opentrader.sh index 968a1f67..8152d270 100755 --- a/bin/opentrader.sh +++ b/bin/opentrader.sh @@ -1,3 +1,3 @@ #!/bin/bash # Use "$@" to pass all additional command line arguments to your script -pnpm exec ts-node --transpile-only apps/cli/src/index.ts "$@" +node apps/cli/dist/main.js "$@" diff --git a/package.json b/package.json index 710459ca..95ff58a4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "debug": "ts-node --transpile-only packages/cli/src/index.ts trade debug" }, "bin": { - "opentrader": "./bin/opentrader.sh", - "ot": "./bin/ot.sh" + "dev": "./bin/dev.sh", + "opentrader": "./bin/opentrader.sh" } } From 2ece71e3e3fd5534ddc477767609bb6c75260823 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 02:32:47 +0100 Subject: [PATCH 09/12] fix(webpack): add `dynamicImport` function --- apps/cli/.eslintrc.js | 1 + apps/cli/package.json | 2 +- apps/cli/src/api/up/index.ts | 23 +++++++++++-------- apps/cli/webpack.config.js | 8 +------ packages/bot-templates/package.json | 2 +- .../src/server/dynamic-import.d.ts | 1 + .../src/server/dynamic-import.js | 4 ++++ .../src/{server.ts => server/index.ts} | 6 ++--- 8 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 packages/bot-templates/src/server/dynamic-import.d.ts create mode 100644 packages/bot-templates/src/server/dynamic-import.js rename packages/bot-templates/src/{server.ts => server/index.ts} (88%) diff --git a/apps/cli/.eslintrc.js b/apps/cli/.eslintrc.js index 3402c71b..835f8e81 100644 --- a/apps/cli/.eslintrc.js +++ b/apps/cli/.eslintrc.js @@ -4,5 +4,6 @@ module.exports = { extends: ["@opentrader/eslint-config/module.js"], rules: { "import/namespace": "off", + "@typescript-eslint/no-var-requires": "off", }, }; diff --git a/apps/cli/package.json b/apps/cli/package.json index 67f7444e..41714946 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@opentrader/cli", + "name": "opentrader", "version": "0.0.1", "description": "", "main": "src/index.ts", diff --git a/apps/cli/src/api/up/index.ts b/apps/cli/src/api/up/index.ts index 3fd48118..f008b057 100644 --- a/apps/cli/src/api/up/index.ts +++ b/apps/cli/src/api/up/index.ts @@ -1,12 +1,12 @@ -import { join } from "path"; -import { spawn } from "child_process"; -import { Processor } from "@opentrader/bot"; -import { xprisma } from "@opentrader/db"; +import { join } from "node:path"; +import { spawn } from "node:child_process"; import { logger } from "@opentrader/logger"; -import { CommandResult } from "../../types"; +import type { CommandResult } from "../../types"; import { appPath } from "../../utils/app-path"; import { getPid, savePid } from "../../utils/pid"; +const isDevelopment = process.env.NODE_ENV !== "production"; + type Options = { detach: boolean; }; @@ -22,10 +22,15 @@ export async function up(options: Options): Promise { }; } - const daemonProcess = spawn("ts-node", [join(__dirname, "daemon.ts")], { - detached: options.detach, - stdio: options.detach ? "ignore" : undefined, - }); + const daemonProcess = isDevelopment + ? spawn("ts-node", [join(__dirname, "daemon.ts")], { + detached: options.detach, + stdio: options.detach ? "ignore" : undefined, + }) + : spawn("node", [join(__dirname, "daemon.js")], { + detached: options.detach, + stdio: options.detach ? "ignore" : undefined, + }); if (daemonProcess.pid === undefined) { throw new Error("Failed to start daemon process"); diff --git a/apps/cli/webpack.config.js b/apps/cli/webpack.config.js index 050c8b9b..3f5ccb87 100644 --- a/apps/cli/webpack.config.js +++ b/apps/cli/webpack.config.js @@ -16,16 +16,10 @@ module.exports = { exclude: /node_modules/, }, ], + noParse: /\/dynamic-import.js$/, }, resolve: { extensions: [".tsx", ".ts", ".js"], - alias: { - // resolve TS paths - "#db": path.resolve(__dirname, "../../packages/db/src"), - "#processing": path.resolve(__dirname, "../../packages/processing/src"), - "#exchanges": path.resolve(__dirname, "../../packages/exchanges/src"), - "#trpc": path.resolve(__dirname, "../../packages/trpc/src"), - }, }, // in order to ignore all modules in node_modules folder externals: [ diff --git a/packages/bot-templates/package.json b/packages/bot-templates/package.json index 8f034389..79256fee 100644 --- a/packages/bot-templates/package.json +++ b/packages/bot-templates/package.json @@ -6,7 +6,7 @@ "types": "src/index.ts", "exports": { ".": "./src/index.ts", - "./server": "./src/server.ts", + "./server": "./src/server/index.ts", "./dist": "./src/index.ts" }, "scripts": { diff --git a/packages/bot-templates/src/server/dynamic-import.d.ts b/packages/bot-templates/src/server/dynamic-import.d.ts new file mode 100644 index 00000000..f3b83c06 --- /dev/null +++ b/packages/bot-templates/src/server/dynamic-import.d.ts @@ -0,0 +1 @@ +export default function dynamicImport(path: string): any; diff --git a/packages/bot-templates/src/server/dynamic-import.js b/packages/bot-templates/src/server/dynamic-import.js new file mode 100644 index 00000000..c6ff02bb --- /dev/null +++ b/packages/bot-templates/src/server/dynamic-import.js @@ -0,0 +1,4 @@ +// Webpack replaces calls to `require()` from within a bundle. This module +// is not parsed by webpack and exports the real `require` +// NOTE: since the module is unparsed, do not use es6 exports +module.exports = require; diff --git a/packages/bot-templates/src/server.ts b/packages/bot-templates/src/server/index.ts similarity index 88% rename from packages/bot-templates/src/server.ts rename to packages/bot-templates/src/server/index.ts index 96153e81..abbc666b 100644 --- a/packages/bot-templates/src/server.ts +++ b/packages/bot-templates/src/server/index.ts @@ -1,6 +1,7 @@ import { join } from "node:path"; import type { BotTemplate } from "@opentrader/bot-processor"; -import * as templates from "./templates"; +import * as templates from "../templates"; +import dynamicImport from "./dynamic-import"; type FindStrategyResult = { strategyFn: BotTemplate; @@ -21,8 +22,7 @@ export async function findStrategy( const strategyExists = strategyNameOrFile in templates; if (isCustomStrategyFile) { - const { default: fn } = await import(customStrategyFilePath); - strategyFn = fn; + strategyFn = dynamicImport(customStrategyFilePath); } else if (strategyExists) { strategyFn = templates[strategyNameOrFile as keyof typeof templates]; } else { From bb57a276a9365f62e857bf5e5b5b05705d8b8922 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 02:36:58 +0100 Subject: [PATCH 10/12] fix(ESLint): solve setting `process.env.LOG_FILE` before imports --- apps/cli/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cli/.eslintrc.js b/apps/cli/.eslintrc.js index 835f8e81..1a0bc443 100644 --- a/apps/cli/.eslintrc.js +++ b/apps/cli/.eslintrc.js @@ -5,5 +5,6 @@ module.exports = { rules: { "import/namespace": "off", "@typescript-eslint/no-var-requires": "off", + "import/first": "off", }, }; From 0578d3fc0ad3a99f5b78f34d8f70dcf08d6fa278 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 02:37:39 +0100 Subject: [PATCH 11/12] chore: remove unused import --- apps/cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 218b0755..6602f5f3 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -19,7 +19,7 @@ import { logPath } from "./utils/app-path"; process.env.LOG_FILE = logPath; -import { Command, Option } from "commander"; +import { Command } from "commander"; import { addStopCommand } from "./commands/stop"; import { addBacktestCommand } from "./commands/backtest"; import { addGridLinesCommand } from "./commands/grid-lines"; From 51e66e9db0e9033d0982f0b6d9aff9a1118b9c0c Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 17 Jun 2024 03:09:21 +0100 Subject: [PATCH 12/12] chore: remove custom strategy file --- strategies/test.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 strategies/test.js diff --git a/strategies/test.js b/strategies/test.js deleted file mode 100644 index 3b40e27a..00000000 --- a/strategies/test.js +++ /dev/null @@ -1,5 +0,0 @@ -function* strategy(ctx) { - console.log('Test strategy executed!', ctx.market.candle) -} - -module.exports = strategy