feat: 添加命令行参数解析和策略启动功能,支持静默模式和帮助信息

This commit is contained in:
discountry
2025-09-28 01:26:00 +08:00
parent 0e4ae67573
commit 5c1283fac2
4 changed files with 273 additions and 2 deletions
+7 -1
View File
@@ -7,7 +7,13 @@
"dev": "bun run index.ts", "dev": "bun run index.ts",
"start": "bun run index.ts", "start": "bun run index.ts",
"test": "bun x vitest run", "test": "bun x vitest run",
"test:watch": "bun x vitest" "test:watch": "bun x vitest",
"start:trend:silent": "bun run index.ts --strategy trend --silent",
"start:maker:silent": "bun run index.ts --strategy maker --silent",
"start:offset:silent": "bun run index.ts --strategy offset-maker --silent",
"pm2:start:trend": "pm2 start bun --name ritmex-trend --cwd . --restart-delay 5000 -- run index.ts --strategy trend --silent",
"pm2:start:maker": "pm2 start bun --name ritmex-maker --cwd . --restart-delay 5000 -- run index.ts --strategy maker --silent",
"pm2:start:offset": "pm2 start bun --name ritmex-offset --cwd . --restart-delay 5000 -- run index.ts --strategy offset-maker --silent"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
+62
View File
@@ -0,0 +1,62 @@
export type StrategyId = "trend" | "maker" | "offset-maker";
export interface CliOptions {
strategy?: StrategyId;
silent: boolean;
help: boolean;
}
const STRATEGY_VALUES = new Set<StrategyId>(["trend", "maker", "offset-maker"]);
export function parseCliArgs(argv: string[] = process.argv.slice(2)): CliOptions {
const options: CliOptions = { silent: false, help: false };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg) continue;
if (arg === "--silent" || arg === "-q" || arg === "--quiet") {
options.silent = true;
continue;
}
if (arg === "--help" || arg === "-h") {
options.help = true;
continue;
}
if (arg.startsWith("--strategy=")) {
const value = arg.split("=", 2)[1] ?? "";
assignStrategy(options, value);
continue;
}
if (arg === "--strategy" || arg === "-s") {
const value = argv[i + 1];
if (value) {
assignStrategy(options, value);
i += 1;
}
continue;
}
}
return options;
}
function assignStrategy(options: CliOptions, raw: string): void {
const normalized = raw.trim().toLowerCase();
if (!normalized) return;
if (STRATEGY_VALUES.has(normalized as StrategyId)) {
options.strategy = normalized as StrategyId;
} else if (normalized === "offset" || normalized === "offsetmaker" || normalized === "offset-maker") {
options.strategy = "offset-maker";
}
}
export function printCliHelp(): void {
// eslint-disable-next-line no-console
console.log(`Usage: bun run index.ts [--strategy <trend|maker|offset-maker>] [--silent]\n\n` +
`Options:\n` +
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
` Aliases: offset, offset-maker for the offset maker engine.\n` +
` --silent, -q Reduce console output. When used with --strategy, runs in silent daemon mode.\n` +
` --help, -h Show this help message.\n`);
}
+186
View File
@@ -0,0 +1,186 @@
import { makerConfig, tradingConfig } from "../config";
import {
createExchangeAdapter,
getExchangeDisplayName,
resolveExchangeId,
} from "../exchanges/create-adapter";
import type { ExchangeAdapter } from "../exchanges/adapter";
import {
MakerEngine,
type MakerEngineSnapshot,
} from "../strategy/maker-engine";
import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../strategy/offset-maker-engine";
import { TrendEngine, type TrendEngineSnapshot } from "../strategy/trend-engine";
import { extractMessage } from "../utils/errors";
import type { StrategyId } from "./args";
interface RunnerOptions {
silent?: boolean;
}
type StrategyRunner = (options: RunnerOptions) => Promise<void>;
export const STRATEGY_LABELS: Record<StrategyId, string> = {
trend: "Trend Following",
maker: "Maker",
"offset-maker": "Offset Maker",
};
export async function startStrategy(strategyId: StrategyId, options: RunnerOptions = {}): Promise<void> {
const runner = STRATEGY_FACTORIES[strategyId];
if (!runner) {
throw new Error(`Unsupported strategy: ${strategyId}`);
}
await runner(options);
}
const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
trend: async (opts) => {
const config = tradingConfig;
const adapter = createAdapterOrThrow(config.symbol);
const engine = new TrendEngine(config, adapter);
await runEngine({
engine,
strategy: "trend",
silent: opts.silent,
getSnapshot: () => engine.getSnapshot(),
onUpdate: (emitter) => engine.on("update", emitter),
offUpdate: (emitter) => engine.off("update", emitter),
});
},
maker: async (opts) => {
const config = makerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const engine = new MakerEngine(config, adapter);
await runEngine({
engine,
strategy: "maker",
silent: opts.silent,
getSnapshot: () => engine.getSnapshot(),
onUpdate: (emitter) => engine.on("update", emitter),
offUpdate: (emitter) => engine.off("update", emitter),
});
},
"offset-maker": async (opts) => {
const config = makerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const engine = new OffsetMakerEngine(config, adapter);
await runEngine({
engine,
strategy: "offset-maker",
silent: opts.silent,
getSnapshot: () => engine.getSnapshot(),
onUpdate: (emitter) => engine.on("update", emitter),
offUpdate: (emitter) => engine.off("update", emitter),
});
},
};
interface EngineHarness<TSnapshot> {
engine: { start(): void; stop(): void };
strategy: StrategyId;
silent?: boolean;
getSnapshot: () => TSnapshot;
onUpdate: (handler: (snapshot: TSnapshot) => void) => void;
offUpdate: (handler: (snapshot: TSnapshot) => void) => void;
}
async function runEngine<TSnapshot extends TrendEngineSnapshot | MakerEngineSnapshot | OffsetMakerEngineSnapshot>(
harness: EngineHarness<TSnapshot>
): Promise<void> {
const { engine, strategy, silent, getSnapshot, onUpdate, offUpdate } = harness;
const exchangeId = resolveExchangeId();
const exchangeName = getExchangeDisplayName(exchangeId);
const label = STRATEGY_LABELS[strategy];
const initial = getSnapshot();
let lastLogKey: string | undefined;
if (Array.isArray(initial.tradeLog) && initial.tradeLog.length > 0) {
const lastEntry = initial.tradeLog[initial.tradeLog.length - 1]!;
lastLogKey = createLogKey(lastEntry);
}
let readyLogged = initial.ready === true;
const emitter = (snapshot: TSnapshot) => {
if (!Array.isArray(snapshot.tradeLog)) return;
if (!readyLogged && snapshot.ready) {
readyLogged = true;
console.info(`[${label}] Strategy ready. Listening for market data…`);
}
const pending = diffTradeLog(snapshot.tradeLog, lastLogKey);
if (!pending.length) return;
for (const entry of pending) {
console.info(`[${label}] [${entry.time}] [${entry.type}] ${entry.detail}`);
}
const lastEntry = pending[pending.length - 1]!;
if (lastEntry) {
lastLogKey = createLogKey(lastEntry);
}
};
onUpdate(emitter);
engine.start();
console.info(`[${label}] Starting on ${exchangeName}. Mode: ${silent ? "silent" : "interactive"}. Press Ctrl+C to exit.`);
const shutdown = (signal: NodeJS.Signals) => {
try {
console.info(`[${label}] Received ${signal}. Shutting down…`);
engine.stop();
offUpdate(emitter);
} catch (error) {
console.error(`[${label}] Error during shutdown: ${extractMessage(error)}`);
}
};
await new Promise<void>((resolve) => {
const wrapper = (signal: NodeJS.Signals) => {
shutdown(signal);
process.off("SIGINT", wrapper);
process.off("SIGTERM", wrapper);
resolve();
};
process.on("SIGINT", wrapper);
process.on("SIGTERM", wrapper);
});
}
function createAdapterOrThrow(symbol: string): ExchangeAdapter {
const exchangeId = resolveExchangeId();
if (exchangeId === "aster") {
const apiKey = process.env.ASTER_API_KEY;
const apiSecret = process.env.ASTER_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing ASTER_API_KEY or ASTER_API_SECRET environment variables");
}
return createExchangeAdapter({
exchange: exchangeId,
symbol,
aster: { apiKey, apiSecret },
});
}
return createExchangeAdapter({
exchange: exchangeId,
symbol,
grvt: { symbol },
});
}
type TradeLogEntry = { time: string; type: string; detail: string };
function diffTradeLog(tradeLog: TradeLogEntry[], lastKey: string | undefined): TradeLogEntry[] {
if (!tradeLog.length) return [];
if (!lastKey) return tradeLog;
const lastIndex = tradeLog.findIndex((entry) => createLogKey(entry) === lastKey);
if (lastIndex === -1) {
return tradeLog;
}
if (lastIndex === tradeLog.length - 1) return [];
return tradeLog.slice(lastIndex + 1);
}
function createLogKey(entry: TradeLogEntry): string {
return `${entry.time}|${entry.type}|${entry.detail}`;
}
+17
View File
@@ -2,7 +2,24 @@ import React from "react";
import { render } from "ink"; import { render } from "ink";
import { App } from "./ui/App"; import { App } from "./ui/App";
import { setupGlobalErrorHandlers } from "./runtime-errors"; import { setupGlobalErrorHandlers } from "./runtime-errors";
import { parseCliArgs, printCliHelp } from "./cli/args";
import { startStrategy } from "./cli/strategy-runner";
setupGlobalErrorHandlers(); setupGlobalErrorHandlers();
const options = parseCliArgs();
if (options.help) {
printCliHelp();
process.exit(0);
}
if (options.strategy) {
startStrategy(options.strategy, { silent: options.silent })
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[Strategy] Failed to start: ${message}`);
process.exit(1);
});
} else {
render(<App />); render(<App />);
}