add ETH/XRP/SOL factory strategies and plaza filters
This commit is contained in:
parent
e0e203bbcc
commit
cd1d41cff5
@ -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
|
||||
|
||||
244
scripts/bootstrap_eth_xrp_sol_strategies.py
Normal file
244
scripts/bootstrap_eth_xrp_sol_strategies.py
Normal 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=2ATR,TP=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()
|
||||
Loading…
Reference in New Issue
Block a user