Skip to content

Commit

Permalink
feat(cli): improve bot config
Browse files Browse the repository at this point in the history
  • Loading branch information
bludnic committed May 19, 2024
1 parent b7a44f4 commit 2c5af9b
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 61 deletions.
6 changes: 2 additions & 4 deletions config.default.json5
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
label: "default",
settings: {
highPrice: 0.8,
lowPrice: 0.35,
gridLevels: 6,
quantityPerGrid: 10,
},
baseCurrency: "ADA",
quoteCurrency: "USDT",
exchangeCode: "OKX",
pair: "ADA/USDT",
exchange: "OKX", // specify exchange credentials in the `exchanges.json5` config using this key
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ export type IBotConfiguration<T = any> = {
quoteCurrency: string;
exchangeCode: ExchangeCode;
settings: T;
label?: string;
};
2 changes: 1 addition & 1 deletion packages/bot/src/market-data/ccxt-candles.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class CCXTCandlesProvider

this.on("start", () => {
console.log(
"CandleProvider: Start fetching candles from",
`CandleProvider: Start fetching ${this.symbol} candles from`,
this.startDate,
"to",
this.endDate,
Expand Down
49 changes: 33 additions & 16 deletions packages/cli/src/api/run-backtest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ import { templates } from "@opentrader/bot-templates";
import { Backtesting } from "@opentrader/backtesting";
import { CCXTCandlesProvider } from "@opentrader/bot";
import { logger } from "@opentrader/logger";
import type { BarSize, ICandlestick } from "@opentrader/types";
import { exchangeCodeMapCCXT } from "@opentrader/exchanges";
import type { BarSize, ExchangeCode, ICandlestick } from "@opentrader/types";
import type { CommandResult, ConfigName } from "../types";
import { readBotConfig } from "../config";
import { exchangeCodeMapCCXT } from "@opentrader/exchanges";

type Options = {
config: ConfigName;
from: Date;
to: Date;
timeframe: BarSize;
pair: string;
exchange: ExchangeCode;
};

export async function runBacktest(
strategyName: keyof typeof templates,
options: {
config: ConfigName;
from: Date;
to: Date;
timeframe: BarSize;
symbol: string;
},
options: Options,
): Promise<CommandResult> {
const config = readBotConfig(options.config);
logger.debug(config, "Parsed bot config");
const botConfig = readBotConfig(options.config);
logger.debug(botConfig, "Parsed bot config");

const strategyExists = strategyName in templates;
if (!strategyExists) {
Expand All @@ -33,21 +36,35 @@ export async function runBacktest(
};
}

const ccxtExchange = exchangeCodeMapCCXT[config.exchangeCode];
const botTemplate = strategyName || botConfig.template;
const botTimeframe = options.timeframe || botConfig.timeframe || null;
const botPair = options.pair || botConfig.pair;
const [baseCurrency, quoteCurrency] = botPair.split("/");

const ccxtExchange = exchangeCodeMapCCXT[options.exchange];
const exchange = new ccxt[ccxtExchange]();

logger.info(
`Using ${botPair} on ${options.exchange} exchange with ${botTimeframe} timeframe`,
);
const backtesting = new Backtesting({
botConfig: config,
botTemplate: templates[strategyName],
botConfig: {
id: 0,
baseCurrency,
quoteCurrency,
exchangeCode: options.exchange,
settings: botConfig.settings,
},
botTemplate: templates[botTemplate],
});

return new Promise((resolve) => {
const candles: ICandlestick[] = [];

const candleProvider = new CCXTCandlesProvider({
exchange,
symbol: options.symbol,
timeframe: options.timeframe,
symbol: botPair,
timeframe: botTimeframe,
startDate: options.from,
endDate: options.to,
});
Expand Down
81 changes: 54 additions & 27 deletions packages/cli/src/api/run-trading.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import type { IBotConfiguration } from "@opentrader/bot-processor";
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 type { CommandResult, ConfigName, ExchangeConfig } from "../types";
import { BarSize } from "@opentrader/types";
import type {
BotConfig,
CommandResult,
ConfigName,
ExchangeConfig,
} from "../types";
import { readBotConfig, readExchangesConfig } from "../config";

type Options = {
config: ConfigName;
pair?: string;
exchange?: string;
timeframe?: BarSize;
};

export async function runTrading(
strategyName: keyof typeof templates,
options: {
config: ConfigName;
},
options: Options,
): Promise<CommandResult> {
const config = readBotConfig(options.config);
logger.debug(config, "Parsed bot config");
Expand All @@ -35,7 +45,12 @@ export async function runTrading(
// Saving exchange accounts to DB if not exists
const exchangeAccounts: ExchangeAccountWithCredentials[] =
await createOrUpdateExchangeAccounts(exchangesConfig);
const bot = await createOrUpdateBot(strategyName, config, exchangeAccounts);
const bot = await createOrUpdateBot(
strategyName,
options,
config,
exchangeAccounts,
);

const processor = new Processor(exchangeAccounts, [bot]);
await processor.onApplicationBootstrap();
Expand Down Expand Up @@ -127,40 +142,51 @@ async function createOrUpdateExchangeAccounts(
return exchangeAccounts;
}

async function createOrUpdateBot<T = object>(
async function createOrUpdateBot<T = any>(
strategyName: string,
botConfig: IBotConfiguration<T>,
options: Options,
botConfig: BotConfig<T>,
exchangeAccounts: ExchangeAccountWithCredentials[],
): Promise<TBot> {
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.exchangeCode === botConfig.exchangeCode,
(exchangeAccount) => exchangeAccount.label === exchangeLabel,
);
if (!exchangeAccount) {
throw new Error(
`Exchange account with code "${botConfig.exchangeCode}" not found to create the bot`,
`Exchange account with label "${exchangeLabel}" not found. Check the exchanges config file.`,
);
}

let bot = await xprisma.bot.custom.findFirst({
where: {
label: botConfig.label,
label: botLabel,
},
});

if (bot) {
logger.info(`Bot "${botConfig.label}" found in DB. Updating...`);
logger.info(`Bot "${botLabel}" found in DB. Updating...`);

bot = await xprisma.bot.custom.update({
where: {
id: bot.id,
},
data: {
type: "Bot",
name: "Default bot",
label: botConfig.label,
template: strategyName,
baseCurrency: botConfig.baseCurrency,
quoteCurrency: botConfig.quoteCurrency,
type: botType,
name: botName,
label: botLabel,
template: botTemplate,
timeframe: botTimeframe,
baseCurrency,
quoteCurrency,
settings: botConfig.settings as object,
exchangeAccount: {
connect: {
Expand All @@ -175,17 +201,18 @@ async function createOrUpdateBot<T = object>(
},
});

logger.info(`Bot "${botConfig.label}" updated`);
logger.info(`Bot "${botLabel}" updated`);
} else {
logger.info(`Bot "${botConfig.label}" not found. Adding to DB...`);
logger.info(`Bot "${botLabel}" not found. Adding to DB...`);
bot = await xprisma.bot.custom.create({
data: {
type: "Bot",
name: "Default bot",
label: botConfig.label,
type: botType,
name: botName,
label: botLabel,
template: strategyName,
baseCurrency: botConfig.baseCurrency,
quoteCurrency: botConfig.quoteCurrency,
timeframe: botTimeframe,
baseCurrency,
quoteCurrency,
settings: botConfig.settings as object,
exchangeAccount: {
connect: {
Expand All @@ -200,7 +227,7 @@ async function createOrUpdateBot<T = object>(
},
});

logger.info(`Bot "${botConfig.label}" created`);
logger.info(`Bot "${botLabel}" created`);
}

return bot;
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/api/stop-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ export async function stopCommand(options: {
const exchangesConfig = readExchangesConfig(options.config);
logger.debug(exchangesConfig, "Parsed exchanges config");

const botLabel = config.label || "default";

const bot = await xprisma.bot.custom.findUnique({
where: {
label: config.label,
label: botLabel,
},
});

if (!bot) {
logger.info(`Bot "${config.label}" does not exists. Nothing to stop`);
logger.info(`Bot "${botLabel}" does not exists. Nothing to stop`);

return {
result: undefined,
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/commands/backtest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ExchangeCode } from "@opentrader/types";
import type { Command } from "commander";
import { Argument, Option } from "commander";
import { DEFAULT_CONFIG_NAME } from "../config";
import { validateExchange, validatePair } from "../utils/validate";
import { handle } from "../utils/command";
import * as api from "../api";

Expand All @@ -20,9 +22,7 @@ export function addBacktestCommand(program: Command) {
.default(new Date("2024-01-07")),
)
.addOption(
new Option("-s, --symbol <symbol>", "Symbol")
.argParser((symbol) => symbol.toUpperCase())
.default("BTC/USDT"),
new Option("-p, --pair <pair>", "Trading pair").argParser(validatePair),
)
.addOption(
new Option("-b, --timeframe <timeframe>", "Timeframe").default("1h"),
Expand All @@ -32,5 +32,10 @@ export function addBacktestCommand(program: Command) {
DEFAULT_CONFIG_NAME,
),
)
.addOption(
new Option("-e, --exchange <exchange>", "Exchange")
.argParser(validateExchange)
.default(ExchangeCode.OKX),
)
.action(handle(api.runBacktest));
}
10 changes: 10 additions & 0 deletions packages/cli/src/commands/trade.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Command } from "commander";
import { Argument, Option } from "commander";
import { DEFAULT_CONFIG_NAME } from "../config";
import { validatePair, validateTimeframe } from "../utils/validate";
import { handle } from "../utils/command";
import * as api from "../api";

Expand All @@ -14,5 +15,14 @@ export function addTradeCommand(program: Command) {
DEFAULT_CONFIG_NAME,
),
)
.addOption(
new Option("-p, --pair <pair>", "Trading pair").argParser(validatePair),
)
.addOption(new Option("-e, --exchange <exchange>", "Exchange account"))
.addOption(
new Option("-t, --timeframe <timeframe>", "Timeframe")
.argParser(validateTimeframe)
.default(null),
)
.action(handle(api.runTrading));
}
9 changes: 3 additions & 6 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as fs from "node:fs";
import { join } from "node:path";
import JSON5 from "json5";
import { logger } from "@opentrader/logger";
import type { IBotConfiguration } from "@opentrader/bot-processor";
import type { ConfigName, ExchangeConfig } from "./types";
import { BotConfig, ConfigName, ExchangeConfig } from "./types";

const rootDir = join(__dirname, "..");

Expand All @@ -24,14 +23,12 @@ export const DEFAULT_CONFIG_NAME: ConfigName =
*/
export function readBotConfig<T = any>(
configName: ConfigName = DEFAULT_CONFIG_NAME,
): IBotConfiguration<T> {
): BotConfig<T> {
const configFileName = `config.${configName}.json5`;
const configPath = `${rootDir}/${configFileName}`;

logger.info(`Using bot config file: ${configFileName}`);
const config = JSON5.parse<IBotConfiguration<T>>(
fs.readFileSync(configPath, "utf8"),
);
const config = JSON5.parse<BotConfig<T>>(fs.readFileSync(configPath, "utf8"));

return config;
}
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ExchangeAccount } from "@prisma/client";
import type { ExchangeAccount, Bot } from "@prisma/client";

export type ConfigName = "default" | "dev" | "prod";

Expand All @@ -15,3 +15,22 @@ export type ExchangeConfig = Pick<
| "exchangeCode"
| "isDemoAccount"
>;

export type BotConfig<S = any> = Partial<
Pick<Bot, "name" | "type" | "timeframe" | "label">
> &
Pick<Bot, "template"> & {
/**
* Strategy params
*/
settings: S;
/**
* Exchange account label.
* This label should match the key in the exchanges config file (exchanges.json5).
*/
exchange: string;
/**
* Trading pair
*/
pair: string;
};
Loading

0 comments on commit 2c5af9b

Please sign in to comment.