feat: add Guardian strategy to manage existing positions with stop loss and trailing stop functionality

This commit is contained in:
discountry
2025-11-09 14:09:54 +08:00
parent e94cf1bda2
commit 1baee3a207
8 changed files with 965 additions and 23 deletions
+8 -1
View File
@@ -1,6 +1,7 @@
import React, { useMemo, useState } from "react";
import { Box, Text, useInput } from "ink";
import { TrendApp } from "./TrendApp";
import { GuardianApp } from "./GuardianApp";
import { MakerApp } from "./MakerApp";
import { OffsetMakerApp } from "./OffsetMakerApp";
import { GridApp } from "./GridApp";
@@ -10,7 +11,7 @@ import { loadCopyrightFragments, verifyCopyrightIntegrity } from "../utils/copyr
import { resolveExchangeId } from "../exchanges/create-adapter";
interface StrategyOption {
id: "trend" | "maker" | "offset-maker" | "basis" | "grid";
id: "trend" | "guardian" | "maker" | "offset-maker" | "basis" | "grid";
label: string;
description: string;
component: React.ComponentType<{ onExit: () => void }>;
@@ -23,6 +24,12 @@ const BASE_STRATEGIES: StrategyOption[] = [
description: "监控均线信号,自动进出场并维护止损/止盈",
component: TrendApp,
},
{
id: "guardian",
label: "Guardian 防守策略",
description: "不主动开仓,只为现有仓位补挂/移动止损,防止裸奔",
component: GuardianApp,
},
{
id: "maker",
label: "做市刷单策略",
+149
View File
@@ -0,0 +1,149 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Box, Text, useInput } from "ink";
import { tradingConfig } from "../config";
import { resolveExchangeId, getExchangeDisplayName } from "../exchanges/create-adapter";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { GuardianEngine, type GuardianEngineSnapshot } from "../strategy/guardian-engine";
import { formatNumber } from "../utils/format";
import { DataTable, type TableColumn } from "./components/DataTable";
interface GuardianAppProps {
onExit: () => void;
}
const READY_MESSAGE = "正在等待行情/账户推送…";
const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY);
export function GuardianApp({ onExit }: GuardianAppProps) {
const [snapshot, setSnapshot] = useState<GuardianEngineSnapshot | null>(null);
const [error, setError] = useState<Error | null>(null);
const engineRef = useRef<GuardianEngine | null>(null);
const exchangeId = useMemo(() => resolveExchangeId(), []);
const exchangeName = useMemo(() => getExchangeDisplayName(exchangeId), [exchangeId]);
useInput(
(input, key) => {
if (key.escape) {
engineRef.current?.stop();
onExit();
}
},
{ isActive: inputSupported }
);
useEffect(() => {
try {
const adapter = buildAdapterFromEnv({ exchangeId, symbol: tradingConfig.symbol });
const engine = new GuardianEngine(tradingConfig, adapter);
engineRef.current = engine;
setSnapshot(engine.getSnapshot());
const handler = (next: GuardianEngineSnapshot) => {
setSnapshot({ ...next, tradeLog: [...next.tradeLog] });
};
engine.on("update", handler);
engine.start();
return () => {
engine.off("update", handler);
engine.stop();
};
} catch (err) {
console.error(err);
setError(err instanceof Error ? err : new Error(String(err)));
}
}, [exchangeId]);
if (error) {
return (
<Box flexDirection="column" padding={1}>
<Text color="red">Guardian : {error.message}</Text>
<Text color="gray"></Text>
</Box>
);
}
if (!snapshot) {
return (
<Box padding={1}>
<Text> Guardian </Text>
</Box>
);
}
const { position, stopOrder, trailingOrder, tradeLog, ready, guardStatus } = snapshot;
const hasPosition = Math.abs(position.positionAmt) > 1e-8;
const stopOrderPrice = stopOrder ? Number(stopOrder.stopPrice ?? stopOrder.price) : null;
const trailingActivate = trailingOrder ? Number(trailingOrder.activatePrice ?? (trailingOrder as any).activationPrice) : null;
const lastLogs = tradeLog.slice(-6);
const orderColumns: TableColumn[] = [
{ key: "id", header: "ID", align: "right", minWidth: 6 },
{ key: "side", header: "Side", minWidth: 4 },
{ key: "type", header: "Type", minWidth: 12 },
{ key: "price", header: "Price", align: "right", minWidth: 10 },
{ key: "qty", header: "Qty", align: "right", minWidth: 8 },
{ key: "status", header: "Status", minWidth: 10 },
];
const orderRows = [...snapshot.openOrders]
.sort((a, b) => (Number(b.updateTime ?? 0) - Number(a.updateTime ?? 0)) || Number(b.orderId) - Number(a.orderId))
.slice(0, 8)
.map((order) => ({
id: order.orderId,
side: order.side,
type: order.type,
price: order.price ?? order.stopPrice,
qty: order.origQty,
status: order.status,
}));
return (
<Box flexDirection="column" paddingX={1} paddingY={0}>
<Box flexDirection="column" marginBottom={1}>
<Text color="cyanBright">Guardian Strategy Dashboard</Text>
<Text>
: {exchangeName} : {snapshot.symbol} : {formatNumber(snapshot.lastPrice, 2)} : {ready ? "实时运行" : READY_MESSAGE}
</Text>
<Text color="gray">/ Esc </Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text color="greenBright"></Text>
{hasPosition ? (
<>
<Text>
: {position.positionAmt > 0 ? "多" : "空"} : {formatNumber(Math.abs(position.positionAmt), 4)} : {formatNumber(position.entryPrice, 2)} : {formatNumber(snapshot.pnl, 4)} USDT
</Text>
<Text>
: {formatNumber(snapshot.targetStopPrice, 2)} : {formatNumber(stopOrderPrice, 2)} : {formatNumber(snapshot.trailingActivationPrice, 2)} : {formatNumber(trailingActivate, 2)}
</Text>
<Text color={snapshot.requiresStop ? "yellow" : "gray"}>
Guardian : {guardStatus === "protecting" ? "已挂止损" : guardStatus === "pending" ? "缺少止损,正在同步" : "监听中"}
</Text>
</>
) : (
<Text color="gray">Guardian </Text>
)}
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text color="yellow"></Text>
{orderRows.length > 0 ? (
<DataTable columns={orderColumns} rows={orderRows} />
) : (
<Text color="gray"></Text>
)}
</Box>
<Box flexDirection="column">
<Text color="yellow"></Text>
{lastLogs.length > 0 ? (
lastLogs.map((item, index) => (
<Text key={`${item.time}-${index}`}>
[{item.time}] [{item.type}] {item.detail}
</Text>
))
) : (
<Text color="gray"></Text>
)}
</Box>
</Box>
);
}