add ETH/XRP/SOL factory strategies and plaza filters

This commit is contained in:
fanziqi 2026-03-13 17:53:28 +08:00
parent e0e203bbcc
commit cd1d41cff5
2 changed files with 375 additions and 2 deletions

View File

@ -317,6 +317,16 @@ export default function StrategyPlazaPage() {
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [addBalanceTarget, setAddBalanceTarget] = useState<StrategyCard | null>(null);
type SymbolFilter = "ALL" | "BTCUSDT" | "ETHUSDT" | "XRPUSDT" | "SOLUSDT";
type StatusFilter = "all" | "running" | "paused" | "error";
type PnlFilter = "all" | "positive" | "negative";
type SortKey = "recent" | "net_usdt_desc" | "net_usdt_asc" | "pnl24h_desc" | "pnl24h_asc";
const [symbolFilter, setSymbolFilter] = useState<SymbolFilter>("ALL");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
const [pnlFilter, setPnlFilter] = useState<PnlFilter>("all");
const [sortKey, setSortKey] = useState<SortKey>("net_usdt_desc");
const fetchData = useCallback(async () => {
try {
const res = await authFetch("/api/strategies");
@ -350,6 +360,33 @@ export default function StrategyPlazaPage() {
}
};
const filteredStrategies = strategies
.filter((s) => {
if (symbolFilter !== "ALL" && s.symbol !== symbolFilter) return false;
if (statusFilter !== "all" && s.status !== statusFilter) return false;
if (pnlFilter === "positive" && s.net_usdt <= 0) return false;
if (pnlFilter === "negative" && s.net_usdt >= 0) return false;
return true;
})
.sort((a, b) => {
switch (sortKey) {
case "net_usdt_desc":
return b.net_usdt - a.net_usdt;
case "net_usdt_asc":
return a.net_usdt - b.net_usdt;
case "pnl24h_desc":
return b.pnl_usdt_24h - a.pnl_usdt_24h;
case "pnl24h_asc":
return a.pnl_usdt_24h - b.pnl_usdt_24h;
case "recent":
default: {
const aTs = a.last_trade_at ?? a.started_at ?? 0;
const bTs = b.last_trade_at ?? b.started_at ?? 0;
return bTs - aTs;
}
}
});
if (loading) {
return (
<div className="flex items-center justify-center min-h-64">
@ -383,9 +420,101 @@ export default function StrategyPlazaPage() {
</div>
</div>
{/* Filters & Sorting */}
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
{/* 左侧:币种 + 盈亏过滤 */}
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-[11px] text-slate-400">
<span>:</span>
{(["ALL", "BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] as SymbolFilter[]).map((sym) => {
const label = sym === "ALL" ? "全部" : sym.replace("USDT", "");
const active = symbolFilter === sym;
return (
<button
key={sym}
onClick={() => setSymbolFilter(sym)}
className={`px-2 py-0.5 rounded-full border text-[11px] ${
active
? "border-blue-500 bg-blue-50 text-blue-600"
: "border-slate-200 text-slate-500 hover:bg-slate-50"
}`}
>
{label}
</button>
);
})}
</div>
<div className="flex items-center gap-1 text-[11px] text-slate-400">
<span>:</span>
{[
{ key: "all" as PnlFilter, label: "全部" },
{ key: "positive" as PnlFilter, label: "仅盈利" },
{ key: "negative" as PnlFilter, label: "仅亏损" },
].map((opt) => {
const active = pnlFilter === opt.key;
return (
<button
key={opt.key}
onClick={() => setPnlFilter(opt.key)}
className={`px-2 py-0.5 rounded-full border text-[11px] ${
active
? "border-emerald-500 bg-emerald-50 text-emerald-600"
: "border-slate-200 text-slate-500 hover:bg-slate-50"
}`}
>
{opt.label}
</button>
);
})}
</div>
</div>
{/* 右侧:状态 + 排序 */}
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-[11px] text-slate-400">
<span>:</span>
{[
{ key: "all" as StatusFilter, label: "全部" },
{ key: "running" as StatusFilter, label: "运行中" },
{ key: "paused" as StatusFilter, label: "已暂停" },
{ key: "error" as StatusFilter, label: "异常" },
].map((opt) => {
const active = statusFilter === opt.key;
return (
<button
key={opt.key}
onClick={() => setStatusFilter(opt.key)}
className={`px-2 py-0.5 rounded-full border text-[11px] ${
active
? "border-slate-700 bg-slate-800 text-white"
: "border-slate-200 text-slate-500 hover:bg-slate-50"
}`}
>
{opt.label}
</button>
);
})}
</div>
<div className="flex items-center gap-1 text-[11px] text-slate-400">
<span>:</span>
<select
value={sortKey}
onChange={(e) => setSortKey(e.target.value as SortKey)}
className="border border-slate-200 rounded-lg px-2 py-1 text-[11px] text-slate-700 bg-white"
>
<option value="net_usdt_desc"></option>
<option value="net_usdt_asc"></option>
<option value="pnl24h_desc">24h </option>
<option value="pnl24h_asc">24h </option>
<option value="recent"></option>
</select>
</div>
</div>
</div>
{/* Strategy Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{strategies.map((s) => (
{filteredStrategies.map((s) => (
<StrategyCardComponent
key={s.strategy_id}
s={s}
@ -395,7 +524,7 @@ export default function StrategyPlazaPage() {
))}
</div>
{strategies.length === 0 && (
{filteredStrategies.length === 0 && (
<div className="text-center text-slate-400 text-sm py-16">
<p className="mb-3"></p>
<button

View File

@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
bootstrap_eth_xrp_sol_strategies.py
基于 v5.3 Optuna 结果 + v53.json symbol_gates为策略工厂批量创建
ETH/XRP/SOL 54 条单币种策略配置
约定
- 仍沿用 BTC 工厂当前的 CVD 组合与 TP 档位
* CVD 窗口: (5m,30m), (5m,1h), (15m,1h), (15m,4h), (30m,1h), (30m,4h)
* TP 档位: 保守 / 平衡 / 激进sl=2ATRTP=0.75/1.0/1.5R 1.5/2.0/2.5R
- 每个 symbol 的门控阈值复刻 v53.json symbol_gates
* ETHUSDT: min_vol=0.003, whale_usd=50000, obi=0.35, spd=0.005
* XRPUSDT: min_vol=0.0025, whale_usd=30000, obi=0.40, spd=0.006
* SOLUSDT: min_vol=0.004, whale_usd=20000, obi=0.45, spd=0.008
- 五门开关与 BTC 工厂当前配置保持一致
* 波动率门 / CVD / 巨鲸门 / OBI 启用
* 期现门关闭仅写入阈值保留以后启用的空间
- 权重与开仓阈值取自 optuna_results_v3_cn.xlsx 中各 symbol v53/v53_fast Top1
* ETHUSDT (v53): dir=51, env=18, aux=28, mom=3, threshold=75
* XRPUSDT (v53): dir=58, env=8, aux=32, mom=2, threshold=80
* SOLUSDT (v53_fast): dir=38, env=42, aux=8, mom=12, threshold=65
注意
- 如果某个 display_name 已存在于 strategies 将跳过不会重复插入
- 连接参数走 backend.db.get_sync_conn()运行脚本时请设置
PG_HOST=127.0.0.1 PG_PORT=9470 PG_DB=arb_engine PG_USER=arb PG_PASS=...
或在服务器上直接使用 Cloud SQL 内网地址
"""
from __future__ import annotations
import os
import sys
from dataclasses import dataclass
# 确保可以从 backend 导入 db.get_sync_conn
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
from db import get_sync_conn # type: ignore
@dataclass
class SymbolProfile:
symbol: str
weight_direction: int
weight_env: int
weight_aux: int
weight_momentum: int
entry_score: int
min_vol: float
whale_usd_threshold: float
whale_flow_pct: float
obi_threshold: float
spot_perp_threshold: float
# 基于 optuna_results_v3_cn.xlsx Top1 Summary + v53.json symbol_gates
SYMBOL_PROFILES: list[SymbolProfile] = [
SymbolProfile(
symbol="ETHUSDT",
weight_direction=51,
weight_env=18,
weight_aux=28,
weight_momentum=3,
entry_score=75,
min_vol=0.003,
whale_usd_threshold=50_000,
whale_flow_pct=0.5, # ALT 分支主要用 whale_usd_threshold此处保持默认
obi_threshold=0.35,
spot_perp_threshold=0.005,
),
SymbolProfile(
symbol="XRPUSDT",
weight_direction=58,
weight_env=8,
weight_aux=32,
weight_momentum=2,
entry_score=80,
min_vol=0.0025,
whale_usd_threshold=30_000,
whale_flow_pct=0.5,
obi_threshold=0.40,
spot_perp_threshold=0.006,
),
SymbolProfile(
symbol="SOLUSDT",
weight_direction=38,
weight_env=42,
weight_aux=8,
weight_momentum=12,
entry_score=65,
min_vol=0.004,
whale_usd_threshold=20_000,
whale_flow_pct=0.5,
obi_threshold=0.45,
spot_perp_threshold=0.008,
),
]
# 与 BTC 工厂一致的 CVD 组合
CVD_COMBOS: list[tuple[str, str]] = [
("5m", "30m"),
("5m", "1h"),
("15m", "1h"),
("15m", "4h"),
("30m", "1h"),
("30m", "4h"),
]
# TP 档位:保守 / 平衡 / 激进(统一 sl_multiplier=2.0
TP_PROFILES: dict[str, dict[str, float]] = {
"保守": {"sl_atr_multiplier": 2.0, "tp1_ratio": 0.75, "tp2_ratio": 1.5},
"平衡": {"sl_atr_multiplier": 2.0, "tp1_ratio": 1.0, "tp2_ratio": 2.0},
"激进": {"sl_atr_multiplier": 2.0, "tp1_ratio": 1.5, "tp2_ratio": 2.5},
}
def build_display_name(symbol: str, fast_win: str, slow_win: str, tp_label: str) -> str:
"""
生成与 BTC 工厂一致的 display_name例如
BTC_CVD5x30m_TP保守 ETH_CVD5x30m_TP保守
"""
base = symbol.replace("USDT", "")
fast_label = fast_win.replace("m", "") # "5m" → "5"
return f"{base}_CVD{fast_label}x{slow_win}_TP{tp_label}"
def main() -> None:
created = 0
skipped = 0
with get_sync_conn() as conn:
with conn.cursor() as cur:
for profile in SYMBOL_PROFILES:
sym = profile.symbol
for fast_win, slow_win in CVD_COMBOS:
for tp_label, tp_cfg in TP_PROFILES.items():
display_name = build_display_name(sym, fast_win, slow_win, tp_label)
# 避免重复插入:按 display_name 检查
cur.execute(
"SELECT 1 FROM strategies WHERE display_name=%s",
(display_name,),
)
if cur.fetchone():
skipped += 1
continue
cur.execute(
"""
INSERT INTO strategies (
display_name,
symbol,
direction,
cvd_fast_window,
cvd_slow_window,
weight_direction,
weight_env,
weight_aux,
weight_momentum,
entry_score,
gate_vol_enabled,
vol_atr_pct_min,
gate_cvd_enabled,
gate_whale_enabled,
whale_usd_threshold,
whale_flow_pct,
gate_obi_enabled,
obi_threshold,
gate_spot_perp_enabled,
spot_perp_threshold,
sl_atr_multiplier,
tp1_ratio,
tp2_ratio,
timeout_minutes,
flip_threshold
)
VALUES (
%s, -- display_name
%s, -- symbol
%s, -- direction
%s, -- cvd_fast_window
%s, -- cvd_slow_window
%s, -- weight_direction
%s, -- weight_env
%s, -- weight_aux
%s, -- weight_momentum
%s, -- entry_score
%s, -- gate_vol_enabled
%s, -- vol_atr_pct_min
%s, -- gate_cvd_enabled
%s, -- gate_whale_enabled
%s, -- whale_usd_threshold
%s, -- whale_flow_pct
%s, -- gate_obi_enabled
%s, -- obi_threshold
%s, -- gate_spot_perp_enabled
%s, -- spot_perp_threshold
%s, -- sl_atr_multiplier
%s, -- tp1_ratio
%s, -- tp2_ratio
%s, -- timeout_minutes
%s -- flip_threshold
)
""",
(
display_name,
sym,
"both", # 方向:多空双向
fast_win,
slow_win,
profile.weight_direction,
profile.weight_env,
profile.weight_aux,
profile.weight_momentum,
profile.entry_score,
True, # gate_vol_enabled
profile.min_vol,
True, # gate_cvd_enabled
True, # gate_whale_enabled
profile.whale_usd_threshold,
profile.whale_flow_pct,
True, # gate_obi_enabled
profile.obi_threshold,
False, # gate_spot_perp_enabled与当前 BTC 工厂一致,先关闭)
profile.spot_perp_threshold,
tp_cfg["sl_atr_multiplier"],
tp_cfg["tp1_ratio"],
tp_cfg["tp2_ratio"],
240, # timeout_minutes沿用 BTC 工厂
80, # flip_threshold沿用 v5.4 设计
),
)
created += 1
conn.commit()
print(f"[bootstrap] created={created}, skipped={skipped}")
if __name__ == "__main__":
main()