feat: paper trading switch + config API + max positions limit
This commit is contained in:
parent
e054db112d
commit
282aed138a
@ -500,6 +500,39 @@ async def get_signal_trades(
|
|||||||
|
|
||||||
# ─── 模拟盘 API ──────────────────────────────────────────────────
|
# ─── 模拟盘 API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# 模拟盘配置状态(与signal_engine共享的运行时状态)
|
||||||
|
paper_config = {
|
||||||
|
"enabled": False,
|
||||||
|
"initial_balance": 10000,
|
||||||
|
"risk_per_trade": 0.02,
|
||||||
|
"max_positions": 4,
|
||||||
|
"tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/paper/config")
|
||||||
|
async def paper_get_config(user: dict = Depends(get_current_user)):
|
||||||
|
"""获取模拟盘配置"""
|
||||||
|
return paper_config
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/paper/config")
|
||||||
|
async def paper_set_config(request: Request, user: dict = Depends(get_current_user)):
|
||||||
|
"""修改模拟盘配置(仅admin)"""
|
||||||
|
if user.get("role") != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="仅管理员可修改")
|
||||||
|
body = await request.json()
|
||||||
|
for k in ["enabled", "initial_balance", "risk_per_trade", "max_positions"]:
|
||||||
|
if k in body:
|
||||||
|
paper_config[k] = body[k]
|
||||||
|
# 写入配置文件让signal_engine也能读到
|
||||||
|
import json
|
||||||
|
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
||||||
|
with open(config_path, "w") as f:
|
||||||
|
json.dump(paper_config, f, indent=2)
|
||||||
|
return {"ok": True, "config": paper_config}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/paper/summary")
|
@app.get("/api/paper/summary")
|
||||||
async def paper_summary(user: dict = Depends(get_current_user)):
|
async def paper_summary(user: dict = Depends(get_current_user)):
|
||||||
"""模拟盘总览"""
|
"""模拟盘总览"""
|
||||||
|
|||||||
@ -37,6 +37,33 @@ logger = logging.getLogger("signal-engine")
|
|||||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
||||||
LOOP_INTERVAL = 15 # 秒(从5改15,CPU降60%,信号质量无影响)
|
LOOP_INTERVAL = 15 # 秒(从5改15,CPU降60%,信号质量无影响)
|
||||||
|
|
||||||
|
# ─── 模拟盘配置 ───────────────────────────────────────────────────
|
||||||
|
PAPER_TRADING_ENABLED = False # 开关(范总确认后通过API开启)
|
||||||
|
PAPER_INITIAL_BALANCE = 10000 # 虚拟初始资金 USDT
|
||||||
|
PAPER_RISK_PER_TRADE = 0.02 # 单笔风险 2%(即200U)
|
||||||
|
PAPER_MAX_POSITIONS = 4 # 最大同时持仓数
|
||||||
|
PAPER_TIER_MULTIPLIER = { # 档位仓位倍数
|
||||||
|
"light": 0.5, # 轻仓: 1%
|
||||||
|
"standard": 1.0, # 标准: 2%
|
||||||
|
"heavy": 1.5, # 加仓: 3%
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_paper_config():
|
||||||
|
"""从配置文件加载模拟盘开关和参数"""
|
||||||
|
global PAPER_TRADING_ENABLED, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS
|
||||||
|
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
||||||
|
try:
|
||||||
|
with open(config_path, "r") as f:
|
||||||
|
import json as _json2
|
||||||
|
cfg = _json2.load(f)
|
||||||
|
PAPER_TRADING_ENABLED = cfg.get("enabled", False)
|
||||||
|
PAPER_INITIAL_BALANCE = cfg.get("initial_balance", 10000)
|
||||||
|
PAPER_RISK_PER_TRADE = cfg.get("risk_per_trade", 0.02)
|
||||||
|
PAPER_MAX_POSITIONS = cfg.get("max_positions", 4)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# 窗口大小(毫秒)
|
# 窗口大小(毫秒)
|
||||||
WINDOW_FAST = 30 * 60 * 1000 # 30分钟
|
WINDOW_FAST = 30 * 60 * 1000 # 30分钟
|
||||||
WINDOW_MID = 4 * 3600 * 1000 # 4小时
|
WINDOW_MID = 4 * 3600 * 1000 # 4小时
|
||||||
@ -574,6 +601,14 @@ def paper_has_active_position(symbol: str) -> bool:
|
|||||||
return cur.fetchone()[0] > 0
|
return cur.fetchone()[0] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def paper_active_count() -> int:
|
||||||
|
"""当前所有币种活跃持仓总数"""
|
||||||
|
with get_sync_conn() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')")
|
||||||
|
return cur.fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
# ─── 主循环 ──────────────────────────────────────────────────────
|
# ─── 主循环 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -591,6 +626,8 @@ def main():
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
now_ms = int(time.time() * 1000)
|
now_ms = int(time.time() * 1000)
|
||||||
|
# 每轮重新加载配置(支持API热更新开关)
|
||||||
|
load_paper_config()
|
||||||
for sym, state in states.items():
|
for sym, state in states.items():
|
||||||
new_trades = fetch_new_trades(sym, state.last_processed_id)
|
new_trades = fetch_new_trades(sym, state.last_processed_id)
|
||||||
for t in new_trades:
|
for t in new_trades:
|
||||||
@ -607,16 +644,19 @@ def main():
|
|||||||
|
|
||||||
if result.get("signal"):
|
if result.get("signal"):
|
||||||
logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}")
|
logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}")
|
||||||
# 模拟盘开仓
|
# 模拟盘开仓(需开关开启)
|
||||||
if not paper_has_active_position(sym):
|
if PAPER_TRADING_ENABLED and not paper_has_active_position(sym):
|
||||||
|
active_count = paper_active_count()
|
||||||
|
if active_count < PAPER_MAX_POSITIONS:
|
||||||
|
tier = result.get("tier", "standard")
|
||||||
paper_open_trade(
|
paper_open_trade(
|
||||||
sym, result["signal"], result["price"],
|
sym, result["signal"], result["price"],
|
||||||
result["score"], result.get("tier", "standard"),
|
result["score"], tier,
|
||||||
result["atr"], now_ms
|
result["atr"], now_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
# 模拟盘持仓检查(每次循环都检查,不管有没有新信号)
|
# 模拟盘持仓检查(开关开着才检查)
|
||||||
if result.get("price") and result["price"] > 0:
|
if PAPER_TRADING_ENABLED and result.get("price") and result["price"] > 0:
|
||||||
paper_check_positions(sym, result["price"], now_ms)
|
paper_check_positions(sym, result["price"], now_ms)
|
||||||
|
|
||||||
cycle += 1
|
cycle += 1
|
||||||
|
|||||||
@ -15,6 +15,55 @@ function fmtPrice(p: number) {
|
|||||||
return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 控制面板(开关+配置)──────────────────────────────────────
|
||||||
|
|
||||||
|
function ControlPanel() {
|
||||||
|
const [config, setConfig] = useState<any>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = async () => { try { const r = await authFetch("/api/paper/config"); if (r.ok) setConfig(await r.json()); } catch {} };
|
||||||
|
f();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggle = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const r = await authFetch("/api/paper/config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ enabled: !config.enabled }),
|
||||||
|
});
|
||||||
|
if (r.ok) setConfig(await r.json().then(j => j.config));
|
||||||
|
} catch {} finally { setSaving(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-xl border-2 ${config.enabled ? "border-emerald-400 bg-emerald-50" : "border-slate-200 bg-white"} px-3 py-2 flex items-center justify-between`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button onClick={toggle} disabled={saving}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||||
|
config.enabled
|
||||||
|
? "bg-red-500 text-white hover:bg-red-600"
|
||||||
|
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||||
|
}`}>
|
||||||
|
{saving ? "..." : config.enabled ? "⏹ 停止模拟盘" : "▶️ 启动模拟盘"}
|
||||||
|
</button>
|
||||||
|
<span className={`text-xs font-medium ${config.enabled ? "text-emerald-700" : "text-slate-500"}`}>
|
||||||
|
{config.enabled ? "🟢 运行中" : "⚪ 已停止"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 text-[10px] text-slate-500">
|
||||||
|
<span>初始资金: ${config.initial_balance?.toLocaleString()}</span>
|
||||||
|
<span>单笔风险: {(config.risk_per_trade * 100).toFixed(0)}%</span>
|
||||||
|
<span>最大持仓: {config.max_positions}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 总览面板 ────────────────────────────────────────────────────
|
// ─── 总览面板 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SummaryCards() {
|
function SummaryCards() {
|
||||||
@ -288,6 +337,7 @@ export default function PaperTradingPage() {
|
|||||||
<p className="text-[10px] text-slate-500">V5.1信号引擎自动交易 · 实时追踪 · 数据驱动优化</p>
|
<p className="text-[10px] text-slate-500">V5.1信号引擎自动交易 · 实时追踪 · 数据驱动优化</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ControlPanel />
|
||||||
<SummaryCards />
|
<SummaryCards />
|
||||||
<ActivePositions />
|
<ActivePositions />
|
||||||
<EquityCurve />
|
<EquityCurve />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user