Compare commits
No commits in common. "codex/codex_dev" and "main" have entirely different histories.
codex/code
...
main
@ -1,9 +1,3 @@
|
|||||||
# arbitrage-engine
|
# arbitrage-engine
|
||||||
|
|
||||||
资金费率套利引擎 BTC/ETH 现货+永续对冲
|
资金费率套利引擎 BTC/ETH 现货+永续对冲
|
||||||
|
|
||||||
## Docs
|
|
||||||
|
|
||||||
- 运维连接手册(本地 PostgreSQL + GCE): [docs/OPS_CONNECTIONS.md](docs/OPS_CONNECTIONS.md)
|
|
||||||
- Auto-Evolve 运行手册: [docs/AUTO_EVOLVE_RUNBOOK.md](docs/AUTO_EVOLVE_RUNBOOK.md)
|
|
||||||
- Codex 每日复盘 Prompt: [docs/CODEX_DAILY_REVIEW_PROMPT.md](docs/CODEX_DAILY_REVIEW_PROMPT.md)
|
|
||||||
|
|||||||
63
V52_FRONTEND_TASK.md
Normal file
63
V52_FRONTEND_TASK.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# V5.2 Frontend Differentiation Task
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
V5.1 and V5.2 currently share the same pages. Boss wants clear visual separation.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### 1. Signals Page (/signals) - Side-by-side comparison
|
||||||
|
Currently shows one set of scores per coin. Change to show BOTH V5.1 and V5.2 scores side by side.
|
||||||
|
|
||||||
|
For the "Latest Signal" cards at the top, each coin should show:
|
||||||
|
```
|
||||||
|
BTC SHORT V5.1: 80分 | V5.2: 85分 5m前
|
||||||
|
```
|
||||||
|
|
||||||
|
The V5.2 score should show FR and Liquidation subscores that V5.1 doesn't have.
|
||||||
|
|
||||||
|
To get V5.2 scores, add a new API endpoint `/api/signals/latest-v52` that returns the V5.2 evaluation alongside V5.1. Or modify the existing `/api/signals/latest` to include both strategy scores.
|
||||||
|
|
||||||
|
### 2. Paper Trading Page (/paper) - Strategy Tabs at TOP
|
||||||
|
Add prominent tabs at the very top of the page:
|
||||||
|
|
||||||
|
```
|
||||||
|
[全部] [V5.1 模拟盘] [V5.2 模拟盘]
|
||||||
|
```
|
||||||
|
|
||||||
|
When selecting a strategy tab:
|
||||||
|
- Current positions: only show positions for that strategy
|
||||||
|
- Trade history: only show trades for that strategy
|
||||||
|
- Stats: only show stats for that strategy
|
||||||
|
- Equity curve: only show curve for that strategy
|
||||||
|
- The "全部" tab shows everything combined (current behavior)
|
||||||
|
|
||||||
|
### 3. Visual Differentiation
|
||||||
|
- V5.1 trades/positions: use a subtle blue-gray badge
|
||||||
|
- V5.2 trades/positions: use a green badge with ✨ icon
|
||||||
|
- V5.2 positions should show extra info: FR score and Liquidation score prominently
|
||||||
|
|
||||||
|
### 4. Backend API Changes Needed
|
||||||
|
|
||||||
|
#### Modify `/api/signals/latest` endpoint in main.py
|
||||||
|
Return both V5.1 and V5.2 evaluations. The signal_engine already evaluates both strategies per cycle and saves the primary one. We need to also save V5.2 evaluations or compute them on-the-fly.
|
||||||
|
|
||||||
|
Simplest approach: Add a field to the signal_indicators table or return strategy-specific data.
|
||||||
|
|
||||||
|
Actually, the simplest approach for NOW: In the latest signal cards, just show the score that's already there (from primary strategy), and add a note showing which strategy it's from. The real differentiation happens in paper trades where the strategy column exists.
|
||||||
|
|
||||||
|
#### `/api/paper/trades` already supports `?strategy=` filter (Codex added this)
|
||||||
|
#### `/api/paper/stats-by-strategy` already exists
|
||||||
|
|
||||||
|
### 5. Key Files to Modify
|
||||||
|
- `frontend/app/paper/page.tsx` - Add strategy tabs at top, filter everything by selected strategy
|
||||||
|
- `frontend/app/signals/page.tsx` - Show V5.2 specific info (FR/Liq scores) in latest signal cards
|
||||||
|
- Backend: may need minor API tweaks
|
||||||
|
|
||||||
|
### 6. Important
|
||||||
|
- Don't break existing functionality
|
||||||
|
- The strategy tabs should be very prominent (not small buttons buried in a section)
|
||||||
|
- Use consistent styling: slate-800 bg for active tab, slate-100 for inactive
|
||||||
|
- Test with `npm run build`
|
||||||
|
|
||||||
|
When completely finished, run:
|
||||||
|
openclaw system event --text "Done: V5.2 frontend differentiation - strategy tabs, visual badges, FR/Liq display" --mode now
|
||||||
224
V52_TASK.md
Normal file
224
V52_TASK.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# V5.2 Development Task
|
||||||
|
|
||||||
|
## Context
|
||||||
|
You are working on the `dev` branch of the ArbitrageEngine project.
|
||||||
|
This is a quantitative trading signal system with:
|
||||||
|
- Backend: Python (FastAPI + PostgreSQL)
|
||||||
|
- Frontend: Next.js + shadcn/ui + Tailwind
|
||||||
|
|
||||||
|
## Database Connection
|
||||||
|
- Host: 34.85.117.248 (Cloud SQL)
|
||||||
|
- Port: 5432, DB: arb_engine, User: arb, Password: arb_engine_2026
|
||||||
|
|
||||||
|
## What to Build (V5.2)
|
||||||
|
|
||||||
|
### 1. Strategy Configuration Framework
|
||||||
|
Create `backend/strategies/` directory with JSON configs:
|
||||||
|
|
||||||
|
**backend/strategies/v51_baseline.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "v51_baseline",
|
||||||
|
"version": "5.1",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 45,
|
||||||
|
"crowding": 20,
|
||||||
|
"environment": 15,
|
||||||
|
"confirmation": 15,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"accel_bonus": 5,
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 2.0,
|
||||||
|
"tp1_multiplier": 1.5,
|
||||||
|
"tp2_multiplier": 3.0
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**backend/strategies/v52_8signals.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "v52_8signals",
|
||||||
|
"version": "5.2",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 40,
|
||||||
|
"crowding": 25,
|
||||||
|
"environment": 15,
|
||||||
|
"confirmation": 20,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"accel_bonus": 5,
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 2.0,
|
||||||
|
"tp1_multiplier": 1.5,
|
||||||
|
"tp2_multiplier": 3.0
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Signal Engine Changes (signal_engine.py)
|
||||||
|
|
||||||
|
#### 2a. Add FR scoring to evaluate_signal()
|
||||||
|
After the crowding section, add funding_rate scoring:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Funding Rate scoring (拥挤层加分)
|
||||||
|
# Read from market_indicators table
|
||||||
|
funding_rate = to_float(self.market_indicators.get("funding_rate"))
|
||||||
|
fr_score = 0
|
||||||
|
if funding_rate is not None:
|
||||||
|
fr_abs = abs(funding_rate)
|
||||||
|
if fr_abs >= 0.001: # extreme ±0.1%
|
||||||
|
# Extreme: penalize if going WITH the crowd
|
||||||
|
if (direction == "LONG" and funding_rate > 0.001) or \
|
||||||
|
(direction == "SHORT" and funding_rate < -0.001):
|
||||||
|
fr_score = -5
|
||||||
|
else:
|
||||||
|
fr_score = 5
|
||||||
|
elif fr_abs >= 0.0003: # moderate ±0.03%
|
||||||
|
# Moderate: reward going AGAINST the crowd
|
||||||
|
if (direction == "LONG" and funding_rate < -0.0003) or \
|
||||||
|
(direction == "SHORT" and funding_rate > 0.0003):
|
||||||
|
fr_score = 5
|
||||||
|
else:
|
||||||
|
fr_score = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2b. Add liquidation scoring
|
||||||
|
```python
|
||||||
|
# Liquidation scoring (确认层加分)
|
||||||
|
liq_score = 0
|
||||||
|
liq_data = self.fetch_recent_liquidations() # new method
|
||||||
|
if liq_data:
|
||||||
|
liq_long_usd = liq_data.get("long_usd", 0)
|
||||||
|
liq_short_usd = liq_data.get("short_usd", 0)
|
||||||
|
# Thresholds by symbol
|
||||||
|
thresholds = {"BTCUSDT": 500000, "ETHUSDT": 200000, "XRPUSDT": 100000, "SOLUSDT": 100000}
|
||||||
|
threshold = thresholds.get(self.symbol, 100000)
|
||||||
|
total = liq_long_usd + liq_short_usd
|
||||||
|
if total >= threshold:
|
||||||
|
if liq_short_usd > 0 and liq_long_usd > 0:
|
||||||
|
ratio = liq_short_usd / liq_long_usd
|
||||||
|
elif liq_short_usd > 0:
|
||||||
|
ratio = float('inf')
|
||||||
|
else:
|
||||||
|
ratio = 0
|
||||||
|
if ratio >= 2.0 and direction == "LONG":
|
||||||
|
liq_score = 5 # shorts getting liquidated, price going up
|
||||||
|
elif ratio <= 0.5 and direction == "SHORT":
|
||||||
|
liq_score = 5 # longs getting liquidated, price going down
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2c. Add fetch_recent_liquidations method to SymbolState
|
||||||
|
```python
|
||||||
|
def fetch_recent_liquidations(self, window_ms=300000):
|
||||||
|
"""Fetch last 5min liquidation totals from liquidations table"""
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
cutoff = now_ms - window_ms
|
||||||
|
with get_sync_conn() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN side='SELL' THEN usd_value ELSE 0 END), 0) as long_liq,
|
||||||
|
COALESCE(SUM(CASE WHEN side='BUY' THEN usd_value ELSE 0 END), 0) as short_liq
|
||||||
|
FROM liquidations
|
||||||
|
WHERE symbol=%s AND trade_time >= %s
|
||||||
|
""", (self.symbol, cutoff))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
return {"long_usd": row[0], "short_usd": row[1]}
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2d. Add funding_rate to fetch_market_indicators
|
||||||
|
Add "funding_rate" to the indicator types:
|
||||||
|
```python
|
||||||
|
for ind_type in ["long_short_ratio", "top_trader_position", "open_interest_hist", "coinbase_premium", "funding_rate"]:
|
||||||
|
```
|
||||||
|
And the extraction:
|
||||||
|
```python
|
||||||
|
elif ind_type == "funding_rate":
|
||||||
|
indicators[ind_type] = float(val.get("lastFundingRate", 0))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2e. Update total_score calculation
|
||||||
|
Currently:
|
||||||
|
```python
|
||||||
|
total_score = direction_score + accel_bonus + crowding_score + environment_score + confirmation_score + aux_score
|
||||||
|
```
|
||||||
|
Change to:
|
||||||
|
```python
|
||||||
|
total_score = direction_score + accel_bonus + crowding_score + fr_score + environment_score + confirmation_score + liq_score + aux_score
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2f. Update factors dict
|
||||||
|
Add fr_score and liq_score to the factors:
|
||||||
|
```python
|
||||||
|
result["factors"] = {
|
||||||
|
...existing factors...,
|
||||||
|
"funding_rate": {"score": fr_score, "value": funding_rate},
|
||||||
|
"liquidation": {"score": liq_score, "long_usd": liq_data.get("long_usd", 0) if liq_data else 0, "short_usd": liq_data.get("short_usd", 0) if liq_data else 0},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2g. Change threshold from 60 to 75
|
||||||
|
In evaluate_signal, change:
|
||||||
|
```python
|
||||||
|
# OLD
|
||||||
|
elif total_score >= 60 and not no_direction and not in_cooldown:
|
||||||
|
result["signal"] = direction
|
||||||
|
result["tier"] = "light"
|
||||||
|
# NEW: remove the 60 tier entirely, minimum is 75
|
||||||
|
```
|
||||||
|
|
||||||
|
Also update reverse signal threshold from 60 to 75:
|
||||||
|
In main() loop:
|
||||||
|
```python
|
||||||
|
# OLD
|
||||||
|
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 60:
|
||||||
|
# NEW
|
||||||
|
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 75:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Strategy field in paper_trades
|
||||||
|
Add SQL migration at top of init_schema() or in a migration:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE paper_trades ADD COLUMN IF NOT EXISTS strategy VARCHAR(32) DEFAULT 'v51_baseline';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. AB Test: Both strategies evaluate each cycle
|
||||||
|
In the main loop, evaluate signal twice (once per strategy config) and potentially open trades for both. Each trade records which strategy triggered it.
|
||||||
|
|
||||||
|
### 5. Frontend: Update paper/page.tsx
|
||||||
|
- Show strategy column in trade history table
|
||||||
|
- Show FR and liquidation scores in signal details
|
||||||
|
- Add strategy filter/tab (v51 vs v52)
|
||||||
|
|
||||||
|
### 6. API: Add strategy stats endpoint
|
||||||
|
In main.py, add `/api/paper/stats-by-strategy` that groups stats by strategy field.
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
- Keep ALL existing functionality working
|
||||||
|
- Don't break the existing V5.1 scoring - it should still work as strategy "v51_baseline"
|
||||||
|
- The FR data is already in market_indicators table (collected every 5min)
|
||||||
|
- The liquidation data is already in liquidations table
|
||||||
|
- Test with: `cd frontend && npm run build` to verify no frontend errors
|
||||||
|
- Test backend: `python3 -c "from signal_engine import *; print('OK')"` to verify imports
|
||||||
|
- Port for dev testing: API=8100, Frontend=3300
|
||||||
|
- Total score CAN exceed 100 (that's by design)
|
||||||
|
|
||||||
|
## Files to modify:
|
||||||
|
1. `backend/signal_engine.py` - core scoring changes
|
||||||
|
2. `backend/main.py` - new API endpoints
|
||||||
|
3. `backend/db.py` - add strategy column migration
|
||||||
|
4. `frontend/app/paper/page.tsx` - UI updates
|
||||||
|
5. NEW: `backend/strategies/v51_baseline.json`
|
||||||
|
6. NEW: `backend/strategies/v52_8signals.json`
|
||||||
|
|
||||||
|
When completely finished, run this command to notify me:
|
||||||
|
openclaw system event --text "Done: V5.2 core implementation complete - FR+liquidation scoring, threshold 75, strategy configs, AB test framework" --mode now
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# Auto-Evolve
|
|
||||||
|
|
||||||
## 文件
|
|
||||||
|
|
||||||
- `run_daily.py`: 每日自动分析 + 自动调参 + 自动上新/下线 + 报告输出
|
|
||||||
- `config.example.json`: 配置模板
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# dry-run(不写库)
|
|
||||||
python3 automation/auto_evolve/run_daily.py
|
|
||||||
|
|
||||||
# apply(写库)
|
|
||||||
python3 automation/auto_evolve/run_daily.py --apply
|
|
||||||
|
|
||||||
# 带配置
|
|
||||||
python3 automation/auto_evolve/run_daily.py --config automation/auto_evolve/config.example.json --apply
|
|
||||||
```
|
|
||||||
|
|
||||||
## 输出
|
|
||||||
|
|
||||||
- Markdown 报告:`reports/auto-evolve/YYYY-MM-DD/HHMMSS_auto_evolve.md`
|
|
||||||
- JSON 报告:`reports/auto-evolve/YYYY-MM-DD/HHMMSS_auto_evolve.json`
|
|
||||||
|
|
||||||
## 默认安全策略
|
|
||||||
|
|
||||||
- 默认 dry-run;
|
|
||||||
- `--apply` 才会写入策略;
|
|
||||||
- 写入失败自动回滚事务。
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"lookback_days": 7,
|
|
||||||
"min_closed_trades": 12,
|
|
||||||
"max_new_per_symbol": 1,
|
|
||||||
"max_codex_running_per_symbol": 3,
|
|
||||||
"min_codex_age_hours_to_deprecate": 24,
|
|
||||||
"report_dir": "reports/auto-evolve"
|
|
||||||
}
|
|
||||||
@ -1,890 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Auto Evolve Daily Runner
|
|
||||||
|
|
||||||
目标:
|
|
||||||
1) 基于最近数据自动复盘 running 策略;
|
|
||||||
2) 每个币种生成 1 个 codex 优化候选策略;
|
|
||||||
3) 自动下线超配且表现最差的 codex 策略;
|
|
||||||
4) 产出可审计报告(Markdown + JSON)。
|
|
||||||
|
|
||||||
注意:
|
|
||||||
- 默认 dry-run,仅输出建议不写库;
|
|
||||||
- 传 --apply 才会真正写入 DB。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from decimal import Decimal
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
try:
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
except Exception: # pragma: no cover - runtime dependency guard
|
|
||||||
psycopg2 = None
|
|
||||||
RealDictCursor = None
|
|
||||||
|
|
||||||
|
|
||||||
BJ = timezone(timedelta(hours=8))
|
|
||||||
SYMBOLS = ("BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT")
|
|
||||||
WINDOW_MINUTES = {"5m": 5, "15m": 15, "30m": 30, "1h": 60, "4h": 240}
|
|
||||||
WINDOW_PAIRS = [
|
|
||||||
("5m", "30m"),
|
|
||||||
("5m", "1h"),
|
|
||||||
("5m", "4h"),
|
|
||||||
("15m", "30m"),
|
|
||||||
("15m", "1h"),
|
|
||||||
("15m", "4h"),
|
|
||||||
("30m", "1h"),
|
|
||||||
("30m", "4h"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Config:
|
|
||||||
lookback_days: int = 7
|
|
||||||
min_closed_trades: int = 12
|
|
||||||
max_new_per_symbol: int = 1
|
|
||||||
max_codex_running_per_symbol: int = 3
|
|
||||||
min_codex_age_hours_to_deprecate: int = 24
|
|
||||||
report_dir: str = "reports/auto-evolve"
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str | None) -> Config:
|
|
||||||
cfg = Config()
|
|
||||||
if not path:
|
|
||||||
return cfg
|
|
||||||
p = Path(path)
|
|
||||||
if not p.exists():
|
|
||||||
return cfg
|
|
||||||
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
||||||
for k in cfg.__dataclass_fields__:
|
|
||||||
if k in raw:
|
|
||||||
setattr(cfg, k, raw[k])
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_conn():
|
|
||||||
if psycopg2 is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"缺少 psycopg2 依赖,请先安装:pip install psycopg2-binary"
|
|
||||||
)
|
|
||||||
host = os.getenv("PG_HOST") or os.getenv("DB_HOST") or "127.0.0.1"
|
|
||||||
port = int(os.getenv("PG_PORT") or os.getenv("DB_PORT") or 5432)
|
|
||||||
dbname = os.getenv("PG_DB") or os.getenv("DB_NAME") or "arb_engine"
|
|
||||||
user = os.getenv("PG_USER") or os.getenv("DB_USER") or "arb"
|
|
||||||
password = os.getenv("PG_PASS") or os.getenv("DB_PASS") or "arb_engine_2026"
|
|
||||||
return psycopg2.connect(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
dbname=dbname,
|
|
||||||
user=user,
|
|
||||||
password=password,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def as_float(v: Any, default: float = 0.0) -> float:
|
|
||||||
if v is None:
|
|
||||||
return default
|
|
||||||
try:
|
|
||||||
return float(v)
|
|
||||||
except Exception:
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def as_int(v: Any, default: int = 0) -> int:
|
|
||||||
if v is None:
|
|
||||||
return default
|
|
||||||
try:
|
|
||||||
return int(v)
|
|
||||||
except Exception:
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def detect_regime(avg_atr_pct: float, avg_abs_slope: float, signal_rate: float) -> str:
|
|
||||||
if avg_atr_pct >= 85:
|
|
||||||
return "crash"
|
|
||||||
if avg_atr_pct >= 70:
|
|
||||||
return "high_vol"
|
|
||||||
if avg_abs_slope >= 30 and signal_rate >= 0.02:
|
|
||||||
return "trend"
|
|
||||||
return "range"
|
|
||||||
|
|
||||||
|
|
||||||
def compute_fitness(row: dict, lookback_days: int, min_closed: int) -> float:
|
|
||||||
closed = as_int(row.get("closed_trades"))
|
|
||||||
net_r = as_float(row.get("net_r"))
|
|
||||||
win_rate = as_float(row.get("win_rate"))
|
|
||||||
gross_profit = as_float(row.get("gross_profit"))
|
|
||||||
gross_loss = as_float(row.get("gross_loss"))
|
|
||||||
|
|
||||||
r_per_day = net_r / max(lookback_days, 1)
|
|
||||||
profit_factor = gross_profit / gross_loss if gross_loss > 0 else (2.0 if gross_profit > 0 else 0.0)
|
|
||||||
sample_penalty = max(min_closed - closed, 0) * 0.35
|
|
||||||
consistency_bonus = (win_rate - 0.5) * 2.5
|
|
||||||
|
|
||||||
score = (
|
|
||||||
net_r
|
|
||||||
+ 0.9 * r_per_day
|
|
||||||
+ 0.6 * (profit_factor - 1.0)
|
|
||||||
+ 0.5 * consistency_bonus
|
|
||||||
- sample_penalty
|
|
||||||
)
|
|
||||||
return round(score, 4)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_running_strategies(cur) -> list[dict]:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
strategy_id::text AS strategy_id,
|
|
||||||
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,
|
|
||||||
initial_balance,
|
|
||||||
current_balance,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
description
|
|
||||||
FROM strategies
|
|
||||||
WHERE status='running'
|
|
||||||
ORDER BY symbol, created_at
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
return [dict(r) for r in cur.fetchall()]
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_metrics(cur, lookback_days: int) -> dict[str, dict]:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
WITH trade_agg AS (
|
|
||||||
SELECT
|
|
||||||
s.strategy_id::text AS strategy_id,
|
|
||||||
COUNT(*) FILTER (
|
|
||||||
WHERE p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
) AS closed_trades,
|
|
||||||
COALESCE(SUM(CASE
|
|
||||||
WHEN p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
THEN p.pnl_r ELSE 0 END), 0) AS net_r,
|
|
||||||
COALESCE(AVG(CASE
|
|
||||||
WHEN p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
THEN p.pnl_r END), 0) AS avg_r,
|
|
||||||
COALESCE(AVG(CASE
|
|
||||||
WHEN p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
THEN CASE WHEN p.pnl_r > 0 THEN 1 ELSE 0 END END), 0) AS win_rate,
|
|
||||||
COALESCE(SUM(CASE
|
|
||||||
WHEN p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
AND p.pnl_r > 0
|
|
||||||
THEN p.pnl_r ELSE 0 END), 0) AS gross_profit,
|
|
||||||
COALESCE(ABS(SUM(CASE
|
|
||||||
WHEN p.status <> 'active'
|
|
||||||
AND p.symbol = s.symbol
|
|
||||||
AND p.created_at >= NOW() - (%s || ' days')::interval
|
|
||||||
AND p.pnl_r < 0
|
|
||||||
THEN p.pnl_r ELSE 0 END)), 0) AS gross_loss
|
|
||||||
FROM strategies s
|
|
||||||
LEFT JOIN paper_trades p ON p.strategy_id = s.strategy_id
|
|
||||||
WHERE s.status='running'
|
|
||||||
GROUP BY s.strategy_id
|
|
||||||
),
|
|
||||||
signal_agg AS (
|
|
||||||
SELECT
|
|
||||||
s.strategy_id::text AS strategy_id,
|
|
||||||
COUNT(*) FILTER (
|
|
||||||
WHERE si.ts >= (extract(epoch from NOW() - interval '24 hours') * 1000)::bigint
|
|
||||||
AND si.symbol = s.symbol
|
|
||||||
) AS ticks_24h,
|
|
||||||
COUNT(*) FILTER (
|
|
||||||
WHERE si.ts >= (extract(epoch from NOW() - interval '24 hours') * 1000)::bigint
|
|
||||||
AND si.symbol = s.symbol
|
|
||||||
AND COALESCE(si.signal, '') <> ''
|
|
||||||
) AS entry_signals_24h,
|
|
||||||
COALESCE(AVG(si.score) FILTER (
|
|
||||||
WHERE si.ts >= (extract(epoch from NOW() - interval '24 hours') * 1000)::bigint
|
|
||||||
AND si.symbol = s.symbol
|
|
||||||
AND COALESCE(si.signal, '') <> ''
|
|
||||||
), 0) AS avg_signal_score_24h
|
|
||||||
FROM strategies s
|
|
||||||
LEFT JOIN signal_indicators si ON si.strategy_id = s.strategy_id
|
|
||||||
WHERE s.status='running'
|
|
||||||
GROUP BY s.strategy_id
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
s.strategy_id::text AS strategy_id,
|
|
||||||
s.symbol,
|
|
||||||
COALESCE(t.closed_trades, 0) AS closed_trades,
|
|
||||||
COALESCE(t.net_r, 0) AS net_r,
|
|
||||||
COALESCE(t.avg_r, 0) AS avg_r,
|
|
||||||
COALESCE(t.win_rate, 0) AS win_rate,
|
|
||||||
COALESCE(t.gross_profit, 0) AS gross_profit,
|
|
||||||
COALESCE(t.gross_loss, 0) AS gross_loss,
|
|
||||||
COALESCE(sa.ticks_24h, 0) AS ticks_24h,
|
|
||||||
COALESCE(sa.entry_signals_24h, 0) AS entry_signals_24h,
|
|
||||||
COALESCE(sa.avg_signal_score_24h, 0) AS avg_signal_score_24h
|
|
||||||
FROM strategies s
|
|
||||||
LEFT JOIN trade_agg t ON t.strategy_id = s.strategy_id::text
|
|
||||||
LEFT JOIN signal_agg sa ON sa.strategy_id = s.strategy_id::text
|
|
||||||
WHERE s.status='running'
|
|
||||||
""",
|
|
||||||
(lookback_days, lookback_days, lookback_days, lookback_days, lookback_days, lookback_days),
|
|
||||||
)
|
|
||||||
data: dict[str, dict] = {}
|
|
||||||
for r in cur.fetchall():
|
|
||||||
data[r["strategy_id"]] = dict(r)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_symbol_stats(cur) -> dict[str, dict]:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
si.symbol,
|
|
||||||
COALESCE(AVG(si.atr_percentile), 0) AS avg_atr_percentile,
|
|
||||||
COALESCE(AVG(ABS(si.cvd_fast_slope)), 0) AS avg_abs_cvd_slope,
|
|
||||||
COALESCE(AVG(CASE WHEN COALESCE(si.signal,'') <> '' THEN 1 ELSE 0 END), 0) AS signal_rate,
|
|
||||||
COUNT(*) AS rows_24h
|
|
||||||
FROM signal_indicators si
|
|
||||||
JOIN strategies s ON s.strategy_id = si.strategy_id
|
|
||||||
WHERE s.status='running'
|
|
||||||
AND si.symbol = s.symbol
|
|
||||||
AND si.ts >= (extract(epoch from NOW() - interval '24 hours') * 1000)::bigint
|
|
||||||
GROUP BY si.symbol
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
stats: dict[str, dict] = {}
|
|
||||||
for r in cur.fetchall():
|
|
||||||
avg_atr = as_float(r["avg_atr_percentile"])
|
|
||||||
avg_slope = as_float(r["avg_abs_cvd_slope"])
|
|
||||||
signal_rate = as_float(r["signal_rate"])
|
|
||||||
regime = detect_regime(avg_atr, avg_slope, signal_rate)
|
|
||||||
stats[r["symbol"]] = {
|
|
||||||
"avg_atr_percentile": round(avg_atr, 2),
|
|
||||||
"avg_abs_cvd_slope": round(avg_slope, 2),
|
|
||||||
"signal_rate": round(signal_rate, 5),
|
|
||||||
"rows_24h": as_int(r["rows_24h"]),
|
|
||||||
"regime": regime,
|
|
||||||
}
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_weights(wd: int, we: int, wa: int, wm: int) -> tuple[int, int, int, int]:
|
|
||||||
vals = [max(0, wd), max(0, we), max(0, wa), max(0, wm)]
|
|
||||||
s = sum(vals)
|
|
||||||
if s <= 0:
|
|
||||||
return (55, 25, 15, 5)
|
|
||||||
scaled = [round(v * 100 / s) for v in vals]
|
|
||||||
diff = 100 - sum(scaled)
|
|
||||||
scaled[0] += diff
|
|
||||||
return tuple(int(max(0, v)) for v in scaled)
|
|
||||||
|
|
||||||
|
|
||||||
def mutate_profile(parent: dict, regime: str) -> list[dict]:
|
|
||||||
base_entry = as_int(parent["entry_score"], 75)
|
|
||||||
base_sl = as_float(parent["sl_atr_multiplier"], 1.8)
|
|
||||||
base_timeout = as_int(parent["timeout_minutes"], 240)
|
|
||||||
|
|
||||||
if regime in ("trend", "high_vol"):
|
|
||||||
tp_profiles = [
|
|
||||||
("激进", 1.25, 2.7, max(1.5, base_sl)),
|
|
||||||
("平衡", 1.0, 2.1, max(1.6, base_sl)),
|
|
||||||
("保守", 0.8, 1.6, max(1.5, base_sl - 0.1)),
|
|
||||||
]
|
|
||||||
entry_steps = [0, 2, 4]
|
|
||||||
timeout_choices = [max(120, base_timeout - 60), base_timeout]
|
|
||||||
elif regime == "crash":
|
|
||||||
tp_profiles = [
|
|
||||||
("防守", 0.7, 1.4, max(1.3, base_sl - 0.3)),
|
|
||||||
("平衡", 0.9, 1.8, max(1.4, base_sl - 0.2)),
|
|
||||||
]
|
|
||||||
entry_steps = [3, 5]
|
|
||||||
timeout_choices = [max(90, base_timeout - 120), max(120, base_timeout - 60)]
|
|
||||||
else:
|
|
||||||
tp_profiles = [
|
|
||||||
("保守", 0.75, 1.5, max(1.4, base_sl - 0.2)),
|
|
||||||
("平衡", 1.0, 2.0, max(1.5, base_sl - 0.1)),
|
|
||||||
("激进", 1.3, 2.4, max(1.6, base_sl)),
|
|
||||||
]
|
|
||||||
entry_steps = [-2, 0, 2]
|
|
||||||
timeout_choices = [base_timeout, min(360, base_timeout + 60)]
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
for fast, slow in WINDOW_PAIRS:
|
|
||||||
for profile_name, tp1, tp2, sl in tp_profiles:
|
|
||||||
for step in entry_steps:
|
|
||||||
for timeout_min in timeout_choices:
|
|
||||||
entry = min(95, max(60, base_entry + step))
|
|
||||||
wd, we, wa, wm = (
|
|
||||||
as_int(parent["weight_direction"], 55),
|
|
||||||
as_int(parent["weight_env"], 25),
|
|
||||||
as_int(parent["weight_aux"], 15),
|
|
||||||
as_int(parent["weight_momentum"], 5),
|
|
||||||
)
|
|
||||||
if regime in ("trend", "high_vol"):
|
|
||||||
wd, we, wa, wm = normalize_weights(wd + 4, we + 1, wa - 3, wm - 2)
|
|
||||||
elif regime == "crash":
|
|
||||||
wd, we, wa, wm = normalize_weights(wd + 2, we + 4, wa - 4, wm - 2)
|
|
||||||
else:
|
|
||||||
wd, we, wa, wm = normalize_weights(wd - 1, we + 3, wa + 1, wm - 3)
|
|
||||||
|
|
||||||
c = {
|
|
||||||
"cvd_fast_window": fast,
|
|
||||||
"cvd_slow_window": slow,
|
|
||||||
"entry_score": entry,
|
|
||||||
"sl_atr_multiplier": round(sl, 2),
|
|
||||||
"tp1_ratio": round(tp1, 2),
|
|
||||||
"tp2_ratio": round(tp2, 2),
|
|
||||||
"timeout_minutes": int(timeout_min),
|
|
||||||
"weight_direction": wd,
|
|
||||||
"weight_env": we,
|
|
||||||
"weight_aux": wa,
|
|
||||||
"weight_momentum": wm,
|
|
||||||
"profile_name": profile_name,
|
|
||||||
}
|
|
||||||
candidates.append(c)
|
|
||||||
return candidates
|
|
||||||
|
|
||||||
|
|
||||||
def candidate_signature(c: dict) -> tuple:
|
|
||||||
return (
|
|
||||||
c["cvd_fast_window"],
|
|
||||||
c["cvd_slow_window"],
|
|
||||||
c["entry_score"],
|
|
||||||
c["sl_atr_multiplier"],
|
|
||||||
c["tp1_ratio"],
|
|
||||||
c["tp2_ratio"],
|
|
||||||
c["timeout_minutes"],
|
|
||||||
c["weight_direction"],
|
|
||||||
c["weight_env"],
|
|
||||||
c["weight_aux"],
|
|
||||||
c["weight_momentum"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_candidate_score(parent_metric: dict, candidate: dict, regime: str) -> float:
|
|
||||||
base = as_float(parent_metric.get("fitness"), 0)
|
|
||||||
signal_density = as_float(parent_metric.get("entry_signals_24h"), 0)
|
|
||||||
closed_trades = as_int(parent_metric.get("closed_trades"), 0)
|
|
||||||
|
|
||||||
bonus = 0.0
|
|
||||||
if regime in ("trend", "high_vol") and candidate["tp2_ratio"] >= 2.2:
|
|
||||||
bonus += 0.45
|
|
||||||
if regime == "range" and candidate["entry_score"] <= as_int(parent_metric.get("entry_score"), 75):
|
|
||||||
bonus += 0.25
|
|
||||||
if signal_density < 20 and candidate["entry_score"] < as_int(parent_metric.get("entry_score"), 75):
|
|
||||||
bonus += 0.35
|
|
||||||
if signal_density > 120 and candidate["entry_score"] > as_int(parent_metric.get("entry_score"), 75):
|
|
||||||
bonus += 0.2
|
|
||||||
if closed_trades < 10:
|
|
||||||
bonus -= 0.25
|
|
||||||
if candidate["sl_atr_multiplier"] < 1.3 and regime in ("high_vol", "crash"):
|
|
||||||
bonus -= 0.4
|
|
||||||
if candidate["cvd_fast_window"] == candidate["cvd_slow_window"]:
|
|
||||||
bonus -= 0.2
|
|
||||||
|
|
||||||
return round(base + bonus, 4)
|
|
||||||
|
|
||||||
|
|
||||||
def choose_new_candidates(
|
|
||||||
strategies: list[dict],
|
|
||||||
metrics: dict[str, dict],
|
|
||||||
symbol_stats: dict[str, dict],
|
|
||||||
cfg: Config,
|
|
||||||
) -> tuple[list[dict], list[str]]:
|
|
||||||
by_symbol: dict[str, list[dict]] = {sym: [] for sym in SYMBOLS}
|
|
||||||
for s in strategies:
|
|
||||||
if s["symbol"] in by_symbol:
|
|
||||||
row = dict(s)
|
|
||||||
row.update(metrics.get(s["strategy_id"], {}))
|
|
||||||
row["fitness"] = compute_fitness(row, cfg.lookback_days, cfg.min_closed_trades)
|
|
||||||
by_symbol[s["symbol"]].append(row)
|
|
||||||
|
|
||||||
created_plan: list[dict] = []
|
|
||||||
logs: list[str] = []
|
|
||||||
|
|
||||||
for sym in SYMBOLS:
|
|
||||||
symbol_rows = by_symbol.get(sym, [])
|
|
||||||
if not symbol_rows:
|
|
||||||
logs.append(f"[{sym}] 无 running 策略,跳过")
|
|
||||||
continue
|
|
||||||
|
|
||||||
symbol_rows.sort(key=lambda x: x["fitness"], reverse=True)
|
|
||||||
regime = symbol_stats.get(sym, {}).get("regime", "range")
|
|
||||||
existing_sigs = {
|
|
||||||
candidate_signature(
|
|
||||||
{
|
|
||||||
"cvd_fast_window": r["cvd_fast_window"],
|
|
||||||
"cvd_slow_window": r["cvd_slow_window"],
|
|
||||||
"entry_score": as_int(r["entry_score"], 75),
|
|
||||||
"sl_atr_multiplier": round(as_float(r["sl_atr_multiplier"], 1.8), 2),
|
|
||||||
"tp1_ratio": round(as_float(r["tp1_ratio"], 1.0), 2),
|
|
||||||
"tp2_ratio": round(as_float(r["tp2_ratio"], 2.0), 2),
|
|
||||||
"timeout_minutes": as_int(r["timeout_minutes"], 240),
|
|
||||||
"weight_direction": as_int(r["weight_direction"], 55),
|
|
||||||
"weight_env": as_int(r["weight_env"], 25),
|
|
||||||
"weight_aux": as_int(r["weight_aux"], 15),
|
|
||||||
"weight_momentum": as_int(r["weight_momentum"], 5),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for r in symbol_rows
|
|
||||||
}
|
|
||||||
|
|
||||||
parent = symbol_rows[0]
|
|
||||||
parent_for_est = dict(parent)
|
|
||||||
parent_for_est["entry_signals_24h"] = as_int(parent.get("entry_signals_24h"), 0)
|
|
||||||
|
|
||||||
pool = mutate_profile(parent, regime)
|
|
||||||
scored_pool = []
|
|
||||||
for c in pool:
|
|
||||||
sig = candidate_signature(c)
|
|
||||||
if sig in existing_sigs:
|
|
||||||
continue
|
|
||||||
score = estimate_candidate_score(parent_for_est, c, regime)
|
|
||||||
c2 = dict(c)
|
|
||||||
c2["estimated_fitness"] = score
|
|
||||||
c2["source_strategy_id"] = parent["strategy_id"]
|
|
||||||
c2["source_display_name"] = parent["display_name"]
|
|
||||||
c2["symbol"] = sym
|
|
||||||
c2["direction"] = parent["direction"]
|
|
||||||
c2["gate_vol_enabled"] = parent["gate_vol_enabled"]
|
|
||||||
c2["vol_atr_pct_min"] = as_float(parent["vol_atr_pct_min"], 0.002)
|
|
||||||
c2["gate_cvd_enabled"] = parent["gate_cvd_enabled"]
|
|
||||||
c2["gate_whale_enabled"] = parent["gate_whale_enabled"]
|
|
||||||
c2["whale_usd_threshold"] = as_float(parent["whale_usd_threshold"], 50000)
|
|
||||||
c2["whale_flow_pct"] = as_float(parent["whale_flow_pct"], 0.5)
|
|
||||||
c2["gate_obi_enabled"] = parent["gate_obi_enabled"]
|
|
||||||
c2["obi_threshold"] = as_float(parent["obi_threshold"], 0.35)
|
|
||||||
c2["gate_spot_perp_enabled"] = parent["gate_spot_perp_enabled"]
|
|
||||||
c2["spot_perp_threshold"] = as_float(parent["spot_perp_threshold"], 0.005)
|
|
||||||
c2["flip_threshold"] = as_int(parent["flip_threshold"], max(c2["entry_score"], 75))
|
|
||||||
c2["initial_balance"] = as_float(parent["initial_balance"], 10000)
|
|
||||||
scored_pool.append(c2)
|
|
||||||
|
|
||||||
scored_pool.sort(key=lambda x: x["estimated_fitness"], reverse=True)
|
|
||||||
if not scored_pool:
|
|
||||||
logs.append(f"[{sym}] 无可生成的新候选(参数已覆盖)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
top_n = max(1, cfg.max_new_per_symbol)
|
|
||||||
picks = scored_pool[:top_n]
|
|
||||||
created_plan.extend(picks)
|
|
||||||
logs.append(
|
|
||||||
f"[{sym}] regime={regime} parent={parent['display_name']} -> candidate={len(picks)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return created_plan, logs
|
|
||||||
|
|
||||||
|
|
||||||
def build_display_name(candidate: dict, now_bj: datetime) -> str:
|
|
||||||
sym = candidate["symbol"].replace("USDT", "")
|
|
||||||
fw = candidate["cvd_fast_window"]
|
|
||||||
sw = candidate["cvd_slow_window"]
|
|
||||||
profile = candidate["profile_name"]
|
|
||||||
stamp = now_bj.strftime("%m%d")
|
|
||||||
return f"codex优化-{sym}_CVD{fw}x{sw}_TP{profile}_{stamp}"
|
|
||||||
|
|
||||||
|
|
||||||
def insert_strategy(cur, c: dict, now_bj: datetime) -> str:
|
|
||||||
sid = str(uuid.uuid4())
|
|
||||||
display_name = build_display_name(c, now_bj)
|
|
||||||
|
|
||||||
cur.execute("SELECT 1 FROM strategies WHERE display_name=%s", (display_name,))
|
|
||||||
if cur.fetchone():
|
|
||||||
display_name = f"{display_name}_{sid[:4]}"
|
|
||||||
|
|
||||||
description = (
|
|
||||||
"AutoEvolve generated by Codex; "
|
|
||||||
f"source={c['source_display_name']}({c['source_strategy_id'][:8]}), "
|
|
||||||
f"estimated_fitness={c['estimated_fitness']:.3f}, "
|
|
||||||
f"profile={c['profile_name']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO strategies (
|
|
||||||
strategy_id, display_name, schema_version, status,
|
|
||||||
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,
|
|
||||||
initial_balance, current_balance,
|
|
||||||
description, tags,
|
|
||||||
created_at, updated_at, status_changed_at
|
|
||||||
) VALUES (
|
|
||||||
%s, %s, 1, 'running',
|
|
||||||
%s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s, %s, %s,
|
|
||||||
%s,
|
|
||||||
%s, %s,
|
|
||||||
%s,
|
|
||||||
%s, %s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s,
|
|
||||||
NOW(), NOW(), NOW()
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
sid,
|
|
||||||
display_name,
|
|
||||||
c["symbol"],
|
|
||||||
c["direction"],
|
|
||||||
c["cvd_fast_window"],
|
|
||||||
c["cvd_slow_window"],
|
|
||||||
c["weight_direction"],
|
|
||||||
c["weight_env"],
|
|
||||||
c["weight_aux"],
|
|
||||||
c["weight_momentum"],
|
|
||||||
c["entry_score"],
|
|
||||||
c["gate_vol_enabled"],
|
|
||||||
c["vol_atr_pct_min"],
|
|
||||||
c["gate_cvd_enabled"],
|
|
||||||
c["gate_whale_enabled"],
|
|
||||||
c["whale_usd_threshold"],
|
|
||||||
c["whale_flow_pct"],
|
|
||||||
c["gate_obi_enabled"],
|
|
||||||
c["obi_threshold"],
|
|
||||||
c["gate_spot_perp_enabled"],
|
|
||||||
c["spot_perp_threshold"],
|
|
||||||
c["sl_atr_multiplier"],
|
|
||||||
c["tp1_ratio"],
|
|
||||||
c["tp2_ratio"],
|
|
||||||
c["timeout_minutes"],
|
|
||||||
c["flip_threshold"],
|
|
||||||
c["initial_balance"],
|
|
||||||
c["initial_balance"],
|
|
||||||
description,
|
|
||||||
["codex", "auto-evolve", f"source:{c['source_strategy_id'][:8]}"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return sid
|
|
||||||
|
|
||||||
|
|
||||||
def deprecate_overflow_codex(cur, symbol: str, metrics: dict[str, dict], cfg: Config, new_ids: set[str], dry_run: bool) -> list[dict]:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
strategy_id::text AS strategy_id,
|
|
||||||
display_name,
|
|
||||||
created_at,
|
|
||||||
symbol
|
|
||||||
FROM strategies
|
|
||||||
WHERE status='running'
|
|
||||||
AND symbol=%s
|
|
||||||
AND display_name LIKE 'codex优化-%%'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
""",
|
|
||||||
(symbol,),
|
|
||||||
)
|
|
||||||
rows = [dict(r) for r in cur.fetchall()]
|
|
||||||
if len(rows) <= cfg.max_codex_running_per_symbol:
|
|
||||||
return []
|
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
min_age = timedelta(hours=cfg.min_codex_age_hours_to_deprecate)
|
|
||||||
candidates = []
|
|
||||||
for r in rows:
|
|
||||||
sid = r["strategy_id"]
|
|
||||||
if sid in new_ids:
|
|
||||||
continue
|
|
||||||
created_at = r.get("created_at")
|
|
||||||
if created_at and (now - created_at) < min_age:
|
|
||||||
continue
|
|
||||||
m = metrics.get(sid, {})
|
|
||||||
fitness = as_float(m.get("fitness"), -999)
|
|
||||||
candidates.append((fitness, created_at, r))
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
candidates.sort(key=lambda x: (x[0], x[1] or datetime(1970, 1, 1, tzinfo=timezone.utc)))
|
|
||||||
need_drop = len(rows) - cfg.max_codex_running_per_symbol
|
|
||||||
drops = [c[2] for c in candidates[:need_drop]]
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
for d in drops:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
UPDATE strategies
|
|
||||||
SET status='deprecated', deprecated_at=NOW(), status_changed_at=NOW(), updated_at=NOW()
|
|
||||||
WHERE strategy_id=%s
|
|
||||||
""",
|
|
||||||
(d["strategy_id"],),
|
|
||||||
)
|
|
||||||
|
|
||||||
return drops
|
|
||||||
|
|
||||||
|
|
||||||
def top_bottom(strategies: list[dict], top_n: int = 3) -> tuple[list[dict], list[dict]]:
|
|
||||||
s = sorted(strategies, key=lambda x: x.get("fitness", -999), reverse=True)
|
|
||||||
return s[:top_n], list(reversed(s[-top_n:]))
|
|
||||||
|
|
||||||
|
|
||||||
def to_jsonable(value: Any):
|
|
||||||
if isinstance(value, datetime):
|
|
||||||
return value.isoformat()
|
|
||||||
if isinstance(value, Decimal):
|
|
||||||
return float(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {k: to_jsonable(v) for k, v in value.items()}
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [to_jsonable(v) for v in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def write_report(
|
|
||||||
cfg: Config,
|
|
||||||
now_bj: datetime,
|
|
||||||
strategies: list[dict],
|
|
||||||
symbol_stats: dict[str, dict],
|
|
||||||
created: list[dict],
|
|
||||||
deprecated: list[dict],
|
|
||||||
plan_logs: list[str],
|
|
||||||
dry_run: bool,
|
|
||||||
) -> tuple[Path, Path]:
|
|
||||||
report_root = Path(cfg.report_dir)
|
|
||||||
report_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
day_dir = report_root / now_bj.strftime("%Y-%m-%d")
|
|
||||||
day_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
stamp = now_bj.strftime("%H%M%S")
|
|
||||||
md_path = day_dir / f"{stamp}_auto_evolve.md"
|
|
||||||
json_path = day_dir / f"{stamp}_auto_evolve.json"
|
|
||||||
|
|
||||||
top3, bottom3 = top_bottom(strategies, 3)
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
lines.append(f"# Auto Evolve Report ({now_bj.strftime('%Y-%m-%d %H:%M:%S %Z')})")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"- mode: {'DRY-RUN' if dry_run else 'APPLY'}")
|
|
||||||
lines.append(f"- lookback_days: {cfg.lookback_days}")
|
|
||||||
lines.append(f"- running_strategies: {len(strategies)}")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Symbol Regime")
|
|
||||||
for sym in SYMBOLS:
|
|
||||||
st = symbol_stats.get(sym, {})
|
|
||||||
lines.append(
|
|
||||||
f"- {sym}: regime={st.get('regime','unknown')}, avg_atr_pct={st.get('avg_atr_percentile',0)}, "
|
|
||||||
f"avg_abs_slope={st.get('avg_abs_cvd_slope',0)}, signal_rate={st.get('signal_rate',0)}"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Top 3 (Fitness)")
|
|
||||||
for r in top3:
|
|
||||||
lines.append(
|
|
||||||
f"- {r['display_name']} ({r['symbol']}): fitness={r['fitness']:.3f}, "
|
|
||||||
f"net_r={as_float(r.get('net_r')):.2f}, closed={as_int(r.get('closed_trades'))}, "
|
|
||||||
f"win_rate={as_float(r.get('win_rate'))*100:.1f}%"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Bottom 3 (Fitness)")
|
|
||||||
for r in bottom3:
|
|
||||||
lines.append(
|
|
||||||
f"- {r['display_name']} ({r['symbol']}): fitness={r['fitness']:.3f}, "
|
|
||||||
f"net_r={as_float(r.get('net_r')):.2f}, closed={as_int(r.get('closed_trades'))}, "
|
|
||||||
f"win_rate={as_float(r.get('win_rate'))*100:.1f}%"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Candidate Plan")
|
|
||||||
if plan_logs:
|
|
||||||
lines.extend([f"- {x}" for x in plan_logs])
|
|
||||||
else:
|
|
||||||
lines.append("- no candidate plan")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Created Strategies")
|
|
||||||
if created:
|
|
||||||
for c in created:
|
|
||||||
lines.append(
|
|
||||||
f"- {c['display_name']} ({c['symbol']}): id={c['strategy_id']}, "
|
|
||||||
f"source={c['source_display_name']}, est_fitness={c['estimated_fitness']:.3f}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append("- none")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Deprecated Strategies")
|
|
||||||
if deprecated:
|
|
||||||
for d in deprecated:
|
|
||||||
lines.append(f"- {d['display_name']} ({d['symbol']}): id={d['strategy_id']}")
|
|
||||||
else:
|
|
||||||
lines.append("- none")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
md_path.write_text("\n".join(lines), encoding="utf-8")
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"generated_at": now_bj.isoformat(),
|
|
||||||
"mode": "dry-run" if dry_run else "apply",
|
|
||||||
"lookback_days": cfg.lookback_days,
|
|
||||||
"running_strategies": len(strategies),
|
|
||||||
"symbol_stats": symbol_stats,
|
|
||||||
"top3": top3,
|
|
||||||
"bottom3": bottom3,
|
|
||||||
"created": created,
|
|
||||||
"deprecated": deprecated,
|
|
||||||
"plan_logs": plan_logs,
|
|
||||||
}
|
|
||||||
json_path.write_text(
|
|
||||||
json.dumps(to_jsonable(payload), ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
return md_path, json_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="Auto evolve daily runner")
|
|
||||||
parser.add_argument("--config", default=None, help="Path to JSON config")
|
|
||||||
parser.add_argument("--apply", action="store_true", help="Apply DB mutations (default dry-run)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
cfg = load_config(args.config)
|
|
||||||
dry_run = not args.apply
|
|
||||||
now_bj = datetime.now(BJ)
|
|
||||||
|
|
||||||
conn = get_db_conn()
|
|
||||||
conn.autocommit = False
|
|
||||||
|
|
||||||
created_rows: list[dict] = []
|
|
||||||
deprecated_rows: list[dict] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
strategies = fetch_running_strategies(cur)
|
|
||||||
metrics = fetch_metrics(cur, cfg.lookback_days)
|
|
||||||
symbol_stats = fetch_symbol_stats(cur)
|
|
||||||
|
|
||||||
for s in strategies:
|
|
||||||
m = metrics.get(s["strategy_id"], {})
|
|
||||||
s.update(m)
|
|
||||||
s["fitness"] = compute_fitness(s, cfg.lookback_days, cfg.min_closed_trades)
|
|
||||||
metrics[s["strategy_id"]] = {
|
|
||||||
**m,
|
|
||||||
"fitness": s["fitness"],
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, plan_logs = choose_new_candidates(strategies, metrics, symbol_stats, cfg)
|
|
||||||
|
|
||||||
new_ids: set[str] = set()
|
|
||||||
if not dry_run:
|
|
||||||
for c in plan:
|
|
||||||
sid = insert_strategy(cur, c, now_bj)
|
|
||||||
new_ids.add(sid)
|
|
||||||
created_rows.append(
|
|
||||||
{
|
|
||||||
**c,
|
|
||||||
"strategy_id": sid,
|
|
||||||
"display_name": build_display_name(c, now_bj),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 重新拉一次 metrics,让 deprecate 基于最新 running 集合
|
|
||||||
metrics = fetch_metrics(cur, cfg.lookback_days)
|
|
||||||
for sid, m in metrics.items():
|
|
||||||
m["fitness"] = compute_fitness(m, cfg.lookback_days, cfg.min_closed_trades)
|
|
||||||
|
|
||||||
for sym in SYMBOLS:
|
|
||||||
drops = deprecate_overflow_codex(cur, sym, metrics, cfg, new_ids, dry_run=False)
|
|
||||||
deprecated_rows.extend(drops)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
else:
|
|
||||||
for c in plan:
|
|
||||||
created_rows.append(
|
|
||||||
{
|
|
||||||
**c,
|
|
||||||
"strategy_id": "DRY-RUN",
|
|
||||||
"display_name": build_display_name(c, now_bj),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 报告按提交后的状态生成(dry-run 就用当前状态)
|
|
||||||
if not dry_run:
|
|
||||||
strategies = fetch_running_strategies(cur)
|
|
||||||
metrics = fetch_metrics(cur, cfg.lookback_days)
|
|
||||||
for s in strategies:
|
|
||||||
m = metrics.get(s["strategy_id"], {})
|
|
||||||
s.update(m)
|
|
||||||
s["fitness"] = compute_fitness(s, cfg.lookback_days, cfg.min_closed_trades)
|
|
||||||
|
|
||||||
md_path, json_path = write_report(
|
|
||||||
cfg=cfg,
|
|
||||||
now_bj=now_bj,
|
|
||||||
strategies=strategies,
|
|
||||||
symbol_stats=symbol_stats,
|
|
||||||
created=created_rows,
|
|
||||||
deprecated=deprecated_rows,
|
|
||||||
plan_logs=plan_logs,
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"[auto-evolve] done mode={'dry-run' if dry_run else 'apply'}")
|
|
||||||
print(f"[auto-evolve] report_md={md_path}")
|
|
||||||
print(f"[auto-evolve] report_json={json_path}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
print(f"[auto-evolve] failed: {e}")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@ -24,10 +24,9 @@ from typing import Optional
|
|||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
# 复用 signal_engine/signal_state 的核心类与评分逻辑
|
# 复用signal_engine的核心类
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
from signal_engine import SymbolState, WINDOW_MID
|
from signal_engine import SymbolState, WINDOW_MID
|
||||||
from strategy_scoring import evaluate_signal as score_strategy
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -35,7 +34,7 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger("backtest")
|
logger = logging.getLogger("backtest")
|
||||||
|
|
||||||
PG_HOST = os.getenv("PG_HOST", "127.0.0.1")
|
PG_HOST = os.getenv("PG_HOST", "10.106.0.3")
|
||||||
PG_PORT = int(os.getenv("PG_PORT", "5432"))
|
PG_PORT = int(os.getenv("PG_PORT", "5432"))
|
||||||
PG_DB = os.getenv("PG_DB", "arb_engine")
|
PG_DB = os.getenv("PG_DB", "arb_engine")
|
||||||
PG_USER = os.getenv("PG_USER", "arb")
|
PG_USER = os.getenv("PG_USER", "arb")
|
||||||
@ -221,13 +220,7 @@ class BacktestEngine:
|
|||||||
if len(self.positions) > 0:
|
if len(self.positions) > 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 使用统一评分入口(V5.1 baseline 配置)
|
result = self.state.evaluate_signal(time_ms)
|
||||||
strategy_cfg = {
|
|
||||||
"name": "v51_baseline",
|
|
||||||
"threshold": 75,
|
|
||||||
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"],
|
|
||||||
}
|
|
||||||
result = score_strategy(self.state, time_ms, strategy_cfg=strategy_cfg)
|
|
||||||
signal = result.get("signal")
|
signal = result.get("signal")
|
||||||
if not signal:
|
if not signal:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
db.py — PostgreSQL 数据库连接层
|
db.py — PostgreSQL 数据库连接层
|
||||||
统一连接到 Cloud SQL(PG_HOST 默认 127.0.0.1)
|
统一连接到 Cloud SQL(PG_HOST 默认 10.106.0.3)
|
||||||
同步连接池(psycopg2)供脚本类使用
|
同步连接池(psycopg2)供脚本类使用
|
||||||
异步连接池(asyncpg)供FastAPI使用
|
异步连接池(asyncpg)供FastAPI使用
|
||||||
"""
|
"""
|
||||||
@ -13,7 +13,7 @@ import psycopg2.pool
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
# PG连接参数(统一连接 Cloud SQL)
|
# PG连接参数(统一连接 Cloud SQL)
|
||||||
PG_HOST = os.getenv("PG_HOST", "127.0.0.1")
|
PG_HOST = os.getenv("PG_HOST", "10.106.0.3")
|
||||||
PG_PORT = int(os.getenv("PG_PORT", 5432))
|
PG_PORT = int(os.getenv("PG_PORT", 5432))
|
||||||
PG_DB = os.getenv("PG_DB", "arb_engine")
|
PG_DB = os.getenv("PG_DB", "arb_engine")
|
||||||
PG_USER = os.getenv("PG_USER", "arb")
|
PG_USER = os.getenv("PG_USER", "arb")
|
||||||
|
|||||||
@ -5,8 +5,8 @@ module.exports = {
|
|||||||
interpreter: "python3",
|
interpreter: "python3",
|
||||||
cwd: "/root/Projects/arbitrage-engine/backend",
|
cwd: "/root/Projects/arbitrage-engine/backend",
|
||||||
env: {
|
env: {
|
||||||
PG_HOST: "127.0.0.1",
|
PG_HOST: "34.85.117.248",
|
||||||
CLOUD_PG_HOST: "127.0.0.1",
|
CLOUD_PG_HOST: "34.85.117.248",
|
||||||
CLOUD_PG_ENABLED: "true"
|
CLOUD_PG_ENABLED: "true"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|||||||
@ -49,7 +49,7 @@ if not _DB_PASSWORD:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": os.getenv("DB_HOST", "127.0.0.1"),
|
"host": os.getenv("DB_HOST", "10.106.0.3"),
|
||||||
"port": int(os.getenv("DB_PORT", "5432")),
|
"port": int(os.getenv("DB_PORT", "5432")),
|
||||||
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
||||||
"user": os.getenv("DB_USER", "arb"),
|
"user": os.getenv("DB_USER", "arb"),
|
||||||
|
|||||||
688
backend/main.py
688
backend/main.py
@ -448,7 +448,7 @@ async def get_signal_indicators(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/signals/latest")
|
@app.get("/api/signals/latest")
|
||||||
async def get_signal_latest(user: dict = Depends(get_current_user), strategy: str = "v53"):
|
async def get_signal_latest(user: dict = Depends(get_current_user), strategy: str = "v52_8signals"):
|
||||||
result = {}
|
result = {}
|
||||||
for sym in SYMBOLS:
|
for sym in SYMBOLS:
|
||||||
row = await async_fetchrow(
|
row = await async_fetchrow(
|
||||||
@ -601,7 +601,7 @@ async def get_market_indicators(user: dict = Depends(get_current_user)):
|
|||||||
async def get_signal_history(
|
async def get_signal_history(
|
||||||
symbol: str = "BTC",
|
symbol: str = "BTC",
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
strategy: str = "v53",
|
strategy: str = "v52_8signals",
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""返回最近的信号历史(只返回有信号的记录),含各层分数"""
|
"""返回最近的信号历史(只返回有信号的记录),含各层分数"""
|
||||||
@ -688,32 +688,10 @@ async def paper_set_config(request: Request, user: dict = Depends(get_current_us
|
|||||||
@app.get("/api/paper/summary")
|
@app.get("/api/paper/summary")
|
||||||
async def paper_summary(
|
async def paper_summary(
|
||||||
strategy: str = "all",
|
strategy: str = "all",
|
||||||
strategy_id: str = "all",
|
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""模拟盘总览"""
|
"""模拟盘总览"""
|
||||||
if strategy_id != "all":
|
if strategy == "all":
|
||||||
closed = await async_fetch(
|
|
||||||
"SELECT pnl_r, direction FROM paper_trades "
|
|
||||||
"WHERE status NOT IN ('active','tp1_hit') AND strategy_id = $1",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
active = await async_fetch(
|
|
||||||
"SELECT id FROM paper_trades WHERE status IN ('active','tp1_hit') AND strategy_id = $1",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
first = await async_fetchrow(
|
|
||||||
"SELECT MIN(created_at) as start FROM paper_trades WHERE strategy_id = $1",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
# 从 strategies 表取该策略的 initial_balance
|
|
||||||
strat_row = await async_fetchrow(
|
|
||||||
"SELECT initial_balance FROM strategies WHERE strategy_id = $1",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
initial_balance = float(strat_row["initial_balance"]) if strat_row else paper_config["initial_balance"]
|
|
||||||
risk_per_trade = paper_config["risk_per_trade"]
|
|
||||||
elif strategy == "all":
|
|
||||||
closed = await async_fetch(
|
closed = await async_fetch(
|
||||||
"SELECT pnl_r, direction FROM paper_trades WHERE status NOT IN ('active','tp1_hit')"
|
"SELECT pnl_r, direction FROM paper_trades WHERE status NOT IN ('active','tp1_hit')"
|
||||||
)
|
)
|
||||||
@ -721,8 +699,6 @@ async def paper_summary(
|
|||||||
"SELECT id FROM paper_trades WHERE status IN ('active','tp1_hit')"
|
"SELECT id FROM paper_trades WHERE status IN ('active','tp1_hit')"
|
||||||
)
|
)
|
||||||
first = await async_fetchrow("SELECT MIN(created_at) as start FROM paper_trades")
|
first = await async_fetchrow("SELECT MIN(created_at) as start FROM paper_trades")
|
||||||
initial_balance = paper_config["initial_balance"]
|
|
||||||
risk_per_trade = paper_config["risk_per_trade"]
|
|
||||||
else:
|
else:
|
||||||
closed = await async_fetch(
|
closed = await async_fetch(
|
||||||
"SELECT pnl_r, direction FROM paper_trades "
|
"SELECT pnl_r, direction FROM paper_trades "
|
||||||
@ -737,15 +713,13 @@ async def paper_summary(
|
|||||||
"SELECT MIN(created_at) as start FROM paper_trades WHERE strategy = $1",
|
"SELECT MIN(created_at) as start FROM paper_trades WHERE strategy = $1",
|
||||||
strategy,
|
strategy,
|
||||||
)
|
)
|
||||||
initial_balance = paper_config["initial_balance"]
|
|
||||||
risk_per_trade = paper_config["risk_per_trade"]
|
|
||||||
|
|
||||||
total = len(closed)
|
total = len(closed)
|
||||||
wins = len([r for r in closed if r["pnl_r"] > 0])
|
wins = len([r for r in closed if r["pnl_r"] > 0])
|
||||||
total_pnl = sum(r["pnl_r"] for r in closed)
|
total_pnl = sum(r["pnl_r"] for r in closed)
|
||||||
paper_1r_usd = initial_balance * risk_per_trade
|
paper_1r_usd = paper_config["initial_balance"] * paper_config["risk_per_trade"]
|
||||||
total_pnl_usdt = total_pnl * paper_1r_usd
|
total_pnl_usdt = total_pnl * paper_1r_usd
|
||||||
balance = initial_balance + total_pnl_usdt
|
balance = paper_config["initial_balance"] + total_pnl_usdt
|
||||||
win_rate = (wins / total * 100) if total > 0 else 0
|
win_rate = (wins / total * 100) if total > 0 else 0
|
||||||
gross_profit = sum(r["pnl_r"] for r in closed if r["pnl_r"] > 0)
|
gross_profit = sum(r["pnl_r"] for r in closed if r["pnl_r"] > 0)
|
||||||
gross_loss = abs(sum(r["pnl_r"] for r in closed if r["pnl_r"] <= 0))
|
gross_loss = abs(sum(r["pnl_r"] for r in closed if r["pnl_r"] <= 0))
|
||||||
@ -766,26 +740,18 @@ async def paper_summary(
|
|||||||
@app.get("/api/paper/positions")
|
@app.get("/api/paper/positions")
|
||||||
async def paper_positions(
|
async def paper_positions(
|
||||||
strategy: str = "all",
|
strategy: str = "all",
|
||||||
strategy_id: str = "all",
|
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""当前活跃持仓(含实时价格和浮动盈亏)"""
|
"""当前活跃持仓(含实时价格和浮动盈亏)"""
|
||||||
if strategy_id != "all":
|
if strategy == "all":
|
||||||
rows = await async_fetch(
|
rows = await async_fetch(
|
||||||
"SELECT id, symbol, direction, score, tier, strategy, strategy_id, entry_price, entry_ts, "
|
"SELECT id, symbol, direction, score, tier, strategy, entry_price, entry_ts, "
|
||||||
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry, score_factors, risk_distance "
|
|
||||||
"FROM paper_trades WHERE status IN ('active','tp1_hit') AND strategy_id = $1 ORDER BY entry_ts DESC",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
elif strategy == "all":
|
|
||||||
rows = await async_fetch(
|
|
||||||
"SELECT id, symbol, direction, score, tier, strategy, strategy_id, entry_price, entry_ts, "
|
|
||||||
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry, score_factors, risk_distance "
|
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry, score_factors, risk_distance "
|
||||||
"FROM paper_trades WHERE status IN ('active','tp1_hit') ORDER BY entry_ts DESC"
|
"FROM paper_trades WHERE status IN ('active','tp1_hit') ORDER BY entry_ts DESC"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rows = await async_fetch(
|
rows = await async_fetch(
|
||||||
"SELECT id, symbol, direction, score, tier, strategy, strategy_id, entry_price, entry_ts, "
|
"SELECT id, symbol, direction, score, tier, strategy, entry_price, entry_ts, "
|
||||||
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry, score_factors, risk_distance "
|
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry, score_factors, risk_distance "
|
||||||
"FROM paper_trades WHERE status IN ('active','tp1_hit') AND strategy = $1 ORDER BY entry_ts DESC",
|
"FROM paper_trades WHERE status IN ('active','tp1_hit') AND strategy = $1 ORDER BY entry_ts DESC",
|
||||||
strategy,
|
strategy,
|
||||||
@ -843,7 +809,6 @@ async def paper_trades(
|
|||||||
symbol: str = "all",
|
symbol: str = "all",
|
||||||
result: str = "all",
|
result: str = "all",
|
||||||
strategy: str = "all",
|
strategy: str = "all",
|
||||||
strategy_id: str = "all",
|
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
@ -862,11 +827,7 @@ async def paper_trades(
|
|||||||
elif result == "loss":
|
elif result == "loss":
|
||||||
conditions.append("pnl_r <= 0")
|
conditions.append("pnl_r <= 0")
|
||||||
|
|
||||||
if strategy_id != "all":
|
if strategy != "all":
|
||||||
conditions.append(f"strategy_id = ${idx}")
|
|
||||||
params.append(strategy_id)
|
|
||||||
idx += 1
|
|
||||||
elif strategy != "all":
|
|
||||||
conditions.append(f"strategy = ${idx}")
|
conditions.append(f"strategy = ${idx}")
|
||||||
params.append(strategy)
|
params.append(strategy)
|
||||||
idx += 1
|
idx += 1
|
||||||
@ -874,7 +835,7 @@ async def paper_trades(
|
|||||||
where = " AND ".join(conditions)
|
where = " AND ".join(conditions)
|
||||||
params.append(limit)
|
params.append(limit)
|
||||||
rows = await async_fetch(
|
rows = await async_fetch(
|
||||||
f"SELECT id, symbol, direction, score, tier, strategy, strategy_id, entry_price, exit_price, "
|
f"SELECT id, symbol, direction, score, tier, strategy, entry_price, exit_price, "
|
||||||
f"entry_ts, exit_ts, pnl_r, status, tp1_hit, score_factors "
|
f"entry_ts, exit_ts, pnl_r, status, tp1_hit, score_factors "
|
||||||
f"FROM paper_trades WHERE {where} ORDER BY exit_ts DESC LIMIT ${idx}",
|
f"FROM paper_trades WHERE {where} ORDER BY exit_ts DESC LIMIT ${idx}",
|
||||||
*params
|
*params
|
||||||
@ -885,17 +846,10 @@ async def paper_trades(
|
|||||||
@app.get("/api/paper/equity-curve")
|
@app.get("/api/paper/equity-curve")
|
||||||
async def paper_equity_curve(
|
async def paper_equity_curve(
|
||||||
strategy: str = "all",
|
strategy: str = "all",
|
||||||
strategy_id: str = "all",
|
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""权益曲线"""
|
"""权益曲线"""
|
||||||
if strategy_id != "all":
|
if strategy == "all":
|
||||||
rows = await async_fetch(
|
|
||||||
"SELECT exit_ts, pnl_r FROM paper_trades "
|
|
||||||
"WHERE status NOT IN ('active','tp1_hit') AND strategy_id = $1 ORDER BY exit_ts ASC",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
elif strategy == "all":
|
|
||||||
rows = await async_fetch(
|
rows = await async_fetch(
|
||||||
"SELECT exit_ts, pnl_r FROM paper_trades "
|
"SELECT exit_ts, pnl_r FROM paper_trades "
|
||||||
"WHERE status NOT IN ('active','tp1_hit') ORDER BY exit_ts ASC"
|
"WHERE status NOT IN ('active','tp1_hit') ORDER BY exit_ts ASC"
|
||||||
@ -917,17 +871,10 @@ async def paper_equity_curve(
|
|||||||
@app.get("/api/paper/stats")
|
@app.get("/api/paper/stats")
|
||||||
async def paper_stats(
|
async def paper_stats(
|
||||||
strategy: str = "all",
|
strategy: str = "all",
|
||||||
strategy_id: str = "all",
|
|
||||||
user: dict = Depends(get_current_user),
|
user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""详细统计"""
|
"""详细统计"""
|
||||||
if strategy_id != "all":
|
if strategy == "all":
|
||||||
rows = await async_fetch(
|
|
||||||
"SELECT symbol, direction, pnl_r, tier, entry_ts, exit_ts "
|
|
||||||
"FROM paper_trades WHERE status NOT IN ('active','tp1_hit') AND strategy_id = $1",
|
|
||||||
strategy_id,
|
|
||||||
)
|
|
||||||
elif strategy == "all":
|
|
||||||
rows = await async_fetch(
|
rows = await async_fetch(
|
||||||
"SELECT symbol, direction, pnl_r, tier, entry_ts, exit_ts "
|
"SELECT symbol, direction, pnl_r, tier, entry_ts, exit_ts "
|
||||||
"FROM paper_trades WHERE status NOT IN ('active','tp1_hit')"
|
"FROM paper_trades WHERE status NOT IN ('active','tp1_hit')"
|
||||||
@ -2232,614 +2179,3 @@ async def strategy_plaza_trades(
|
|||||||
strategy_id, limit
|
strategy_id, limit
|
||||||
)
|
)
|
||||||
return {"trades": [dict(r) for r in rows]}
|
return {"trades": [dict(r) for r in rows]}
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
# V5.4 Strategy Factory API
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
import uuid as _uuid
|
|
||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
||||||
|
|
||||||
# ── Pydantic Models ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class StrategyCreateRequest(BaseModel):
|
|
||||||
display_name: str = Field(..., min_length=1, max_length=50)
|
|
||||||
symbol: str
|
|
||||||
direction: str = "both"
|
|
||||||
initial_balance: float = 10000.0
|
|
||||||
cvd_fast_window: str = "30m"
|
|
||||||
cvd_slow_window: str = "4h"
|
|
||||||
weight_direction: int = 55
|
|
||||||
weight_env: int = 25
|
|
||||||
weight_aux: int = 15
|
|
||||||
weight_momentum: int = 5
|
|
||||||
entry_score: int = 75
|
|
||||||
# 门1 波动率
|
|
||||||
gate_vol_enabled: bool = True
|
|
||||||
vol_atr_pct_min: float = 0.002
|
|
||||||
# 门2 CVD共振
|
|
||||||
gate_cvd_enabled: bool = True
|
|
||||||
# 门3 鲸鱼否决
|
|
||||||
gate_whale_enabled: bool = True
|
|
||||||
whale_usd_threshold: float = 50000.0
|
|
||||||
whale_flow_pct: float = 0.5
|
|
||||||
# 门4 OBI否决
|
|
||||||
gate_obi_enabled: bool = True
|
|
||||||
obi_threshold: float = 0.35
|
|
||||||
# 门5 期现背离
|
|
||||||
gate_spot_perp_enabled: bool = False
|
|
||||||
spot_perp_threshold: float = 0.005
|
|
||||||
# 风控参数
|
|
||||||
sl_atr_multiplier: float = 1.5
|
|
||||||
tp1_ratio: float = 0.75
|
|
||||||
tp2_ratio: float = 1.5
|
|
||||||
timeout_minutes: int = 240
|
|
||||||
flip_threshold: int = 80
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
@field_validator("symbol")
|
|
||||||
@classmethod
|
|
||||||
def validate_symbol(cls, v):
|
|
||||||
allowed = {"BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT"}
|
|
||||||
if v not in allowed:
|
|
||||||
raise ValueError(f"symbol must be one of {allowed}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("direction")
|
|
||||||
@classmethod
|
|
||||||
def validate_direction(cls, v):
|
|
||||||
if v not in {"long_only", "short_only", "both"}:
|
|
||||||
raise ValueError("direction must be long_only, short_only, or both")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("cvd_fast_window")
|
|
||||||
@classmethod
|
|
||||||
def validate_cvd_fast(cls, v):
|
|
||||||
if v not in {"5m", "15m", "30m"}:
|
|
||||||
raise ValueError("cvd_fast_window must be 5m, 15m, or 30m")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("cvd_slow_window")
|
|
||||||
@classmethod
|
|
||||||
def validate_cvd_slow(cls, v):
|
|
||||||
if v not in {"30m", "1h", "4h"}:
|
|
||||||
raise ValueError("cvd_slow_window must be 30m, 1h, or 4h")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("weight_direction")
|
|
||||||
@classmethod
|
|
||||||
def validate_w_dir(cls, v):
|
|
||||||
if not 10 <= v <= 80:
|
|
||||||
raise ValueError("weight_direction must be 10-80")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("weight_env")
|
|
||||||
@classmethod
|
|
||||||
def validate_w_env(cls, v):
|
|
||||||
if not 5 <= v <= 60:
|
|
||||||
raise ValueError("weight_env must be 5-60")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("weight_aux")
|
|
||||||
@classmethod
|
|
||||||
def validate_w_aux(cls, v):
|
|
||||||
if not 0 <= v <= 40:
|
|
||||||
raise ValueError("weight_aux must be 0-40")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("weight_momentum")
|
|
||||||
@classmethod
|
|
||||||
def validate_w_mom(cls, v):
|
|
||||||
if not 0 <= v <= 20:
|
|
||||||
raise ValueError("weight_momentum must be 0-20")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def validate_weights_sum(self):
|
|
||||||
total = self.weight_direction + self.weight_env + self.weight_aux + self.weight_momentum
|
|
||||||
if total != 100:
|
|
||||||
raise ValueError(f"Weights must sum to 100, got {total}")
|
|
||||||
return self
|
|
||||||
|
|
||||||
@field_validator("entry_score")
|
|
||||||
@classmethod
|
|
||||||
def validate_entry_score(cls, v):
|
|
||||||
if not 60 <= v <= 95:
|
|
||||||
raise ValueError("entry_score must be 60-95")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("vol_atr_pct_min")
|
|
||||||
@classmethod
|
|
||||||
def validate_vol_atr(cls, v):
|
|
||||||
if not 0.0001 <= v <= 0.02:
|
|
||||||
raise ValueError("vol_atr_pct_min must be 0.0001-0.02")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("whale_usd_threshold")
|
|
||||||
@classmethod
|
|
||||||
def validate_whale_usd(cls, v):
|
|
||||||
if not 1000 <= v <= 1000000:
|
|
||||||
raise ValueError("whale_usd_threshold must be 1000-1000000")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("whale_flow_pct")
|
|
||||||
@classmethod
|
|
||||||
def validate_whale_flow(cls, v):
|
|
||||||
if not 0.0 <= v <= 1.0:
|
|
||||||
raise ValueError("whale_flow_pct must be 0.0-1.0")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("obi_threshold")
|
|
||||||
@classmethod
|
|
||||||
def validate_obi(cls, v):
|
|
||||||
if not 0.1 <= v <= 0.9:
|
|
||||||
raise ValueError("obi_threshold must be 0.1-0.9")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("spot_perp_threshold")
|
|
||||||
@classmethod
|
|
||||||
def validate_spot_perp(cls, v):
|
|
||||||
if not 0.0005 <= v <= 0.01:
|
|
||||||
raise ValueError("spot_perp_threshold must be 0.0005-0.01")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("sl_atr_multiplier")
|
|
||||||
@classmethod
|
|
||||||
def validate_sl(cls, v):
|
|
||||||
if not 0.5 <= v <= 3.0:
|
|
||||||
raise ValueError("sl_atr_multiplier must be 0.5-3.0")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("tp1_ratio")
|
|
||||||
@classmethod
|
|
||||||
def validate_tp1(cls, v):
|
|
||||||
if not 0.3 <= v <= 2.0:
|
|
||||||
raise ValueError("tp1_ratio must be 0.3-2.0")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("tp2_ratio")
|
|
||||||
@classmethod
|
|
||||||
def validate_tp2(cls, v):
|
|
||||||
if not 0.5 <= v <= 4.0:
|
|
||||||
raise ValueError("tp2_ratio must be 0.5-4.0")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("timeout_minutes")
|
|
||||||
@classmethod
|
|
||||||
def validate_timeout(cls, v):
|
|
||||||
if not 30 <= v <= 1440:
|
|
||||||
raise ValueError("timeout_minutes must be 30-1440")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("flip_threshold")
|
|
||||||
@classmethod
|
|
||||||
def validate_flip(cls, v):
|
|
||||||
if not 60 <= v <= 95:
|
|
||||||
raise ValueError("flip_threshold must be 60-95")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("initial_balance")
|
|
||||||
@classmethod
|
|
||||||
def validate_balance(cls, v):
|
|
||||||
if v < 1000:
|
|
||||||
raise ValueError("initial_balance must be >= 1000")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyUpdateRequest(BaseModel):
|
|
||||||
"""Partial update - all fields optional"""
|
|
||||||
display_name: Optional[str] = Field(None, min_length=1, max_length=50)
|
|
||||||
direction: Optional[str] = None
|
|
||||||
cvd_fast_window: Optional[str] = None
|
|
||||||
cvd_slow_window: Optional[str] = None
|
|
||||||
weight_direction: Optional[int] = None
|
|
||||||
weight_env: Optional[int] = None
|
|
||||||
weight_aux: Optional[int] = None
|
|
||||||
weight_momentum: Optional[int] = None
|
|
||||||
entry_score: Optional[int] = None
|
|
||||||
# 门1 波动率
|
|
||||||
gate_vol_enabled: Optional[bool] = None
|
|
||||||
vol_atr_pct_min: Optional[float] = None
|
|
||||||
# 门2 CVD共振
|
|
||||||
gate_cvd_enabled: Optional[bool] = None
|
|
||||||
# 门3 鲸鱼否决
|
|
||||||
gate_whale_enabled: Optional[bool] = None
|
|
||||||
whale_usd_threshold: Optional[float] = None
|
|
||||||
whale_flow_pct: Optional[float] = None
|
|
||||||
# 门4 OBI否决
|
|
||||||
gate_obi_enabled: Optional[bool] = None
|
|
||||||
obi_threshold: Optional[float] = None
|
|
||||||
# 门5 期现背离
|
|
||||||
gate_spot_perp_enabled: Optional[bool] = None
|
|
||||||
spot_perp_threshold: Optional[float] = None
|
|
||||||
# 风控
|
|
||||||
sl_atr_multiplier: Optional[float] = None
|
|
||||||
tp1_ratio: Optional[float] = None
|
|
||||||
tp2_ratio: Optional[float] = None
|
|
||||||
timeout_minutes: Optional[int] = None
|
|
||||||
flip_threshold: Optional[int] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AddBalanceRequest(BaseModel):
|
|
||||||
amount: float = Field(..., gt=0)
|
|
||||||
|
|
||||||
|
|
||||||
class DeprecateRequest(BaseModel):
|
|
||||||
confirm: bool
|
|
||||||
|
|
||||||
|
|
||||||
# ── Helper ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async def _get_strategy_or_404(strategy_id: str) -> dict:
|
|
||||||
row = await async_fetchrow(
|
|
||||||
"SELECT * FROM strategies WHERE strategy_id=$1",
|
|
||||||
strategy_id
|
|
||||||
)
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Strategy not found")
|
|
||||||
return dict(row)
|
|
||||||
|
|
||||||
|
|
||||||
def _strategy_row_to_card(row: dict) -> dict:
|
|
||||||
"""Convert a strategies row to a card-level response (no config params)"""
|
|
||||||
return {
|
|
||||||
"strategy_id": str(row["strategy_id"]),
|
|
||||||
"display_name": row["display_name"],
|
|
||||||
"status": row["status"],
|
|
||||||
"symbol": row["symbol"],
|
|
||||||
"direction": row["direction"],
|
|
||||||
"started_at": int(row["created_at"].timestamp() * 1000) if row.get("created_at") else 0,
|
|
||||||
"initial_balance": row["initial_balance"],
|
|
||||||
"current_balance": row["current_balance"],
|
|
||||||
"net_usdt": round(row["current_balance"] - row["initial_balance"], 2),
|
|
||||||
"deprecated_at": int(row["deprecated_at"].timestamp() * 1000) if row.get("deprecated_at") else None,
|
|
||||||
"last_run_at": int(row["last_run_at"].timestamp() * 1000) if row.get("last_run_at") else None,
|
|
||||||
"schema_version": row["schema_version"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _strategy_row_to_detail(row: dict) -> dict:
|
|
||||||
"""Full detail including all config params"""
|
|
||||||
base = _strategy_row_to_card(row)
|
|
||||||
base.update({
|
|
||||||
"cvd_fast_window": row["cvd_fast_window"],
|
|
||||||
"cvd_slow_window": row["cvd_slow_window"],
|
|
||||||
"weight_direction": row["weight_direction"],
|
|
||||||
"weight_env": row["weight_env"],
|
|
||||||
"weight_aux": row["weight_aux"],
|
|
||||||
"weight_momentum": row["weight_momentum"],
|
|
||||||
"entry_score": row["entry_score"],
|
|
||||||
# 门1 波动率
|
|
||||||
"gate_vol_enabled": row["gate_vol_enabled"],
|
|
||||||
"vol_atr_pct_min": row["vol_atr_pct_min"],
|
|
||||||
# 门2 CVD共振
|
|
||||||
"gate_cvd_enabled": row["gate_cvd_enabled"],
|
|
||||||
# 门3 鲸鱼否决
|
|
||||||
"gate_whale_enabled": row["gate_whale_enabled"],
|
|
||||||
"whale_usd_threshold": row["whale_usd_threshold"],
|
|
||||||
"whale_flow_pct": row["whale_flow_pct"],
|
|
||||||
# 门4 OBI否决
|
|
||||||
"gate_obi_enabled": row["gate_obi_enabled"],
|
|
||||||
"obi_threshold": row["obi_threshold"],
|
|
||||||
# 门5 期现背离
|
|
||||||
"gate_spot_perp_enabled": row["gate_spot_perp_enabled"],
|
|
||||||
"spot_perp_threshold": row["spot_perp_threshold"],
|
|
||||||
"sl_atr_multiplier": row["sl_atr_multiplier"],
|
|
||||||
"tp1_ratio": row["tp1_ratio"],
|
|
||||||
"tp2_ratio": row["tp2_ratio"],
|
|
||||||
"timeout_minutes": row["timeout_minutes"],
|
|
||||||
"flip_threshold": row["flip_threshold"],
|
|
||||||
"description": row.get("description"),
|
|
||||||
"created_at": int(row["created_at"].timestamp() * 1000) if row.get("created_at") else 0,
|
|
||||||
"updated_at": int(row["updated_at"].timestamp() * 1000) if row.get("updated_at") else 0,
|
|
||||||
})
|
|
||||||
return base
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_strategy_trade_stats(strategy_id: str) -> dict:
|
|
||||||
"""Fetch trade statistics for a strategy by strategy_id.
|
|
||||||
兼容新数据(strategy_id列)和旧数据(strategy文本列)。
|
|
||||||
"""
|
|
||||||
# 固定 UUID → legacy strategy文本名映射(迁移时写死的三条策略)
|
|
||||||
LEGACY_NAME_MAP = {
|
|
||||||
"00000000-0000-0000-0000-000000000053": "v53",
|
|
||||||
"00000000-0000-0000-0000-000000000054": "v53_middle",
|
|
||||||
"00000000-0000-0000-0000-000000000055": "v53_fast",
|
|
||||||
}
|
|
||||||
legacy_name = LEGACY_NAME_MAP.get(strategy_id)
|
|
||||||
|
|
||||||
# 查已关闭的交易记录(同时兼容新旧两种匹配方式)
|
|
||||||
if legacy_name:
|
|
||||||
rows = await async_fetch(
|
|
||||||
"""SELECT status, pnl_r, tp1_hit, entry_ts, exit_ts
|
|
||||||
FROM paper_trades
|
|
||||||
WHERE status NOT IN ('active', 'tp1_hit')
|
|
||||||
AND (strategy_id=$1 OR (strategy_id IS NULL AND strategy=$2))
|
|
||||||
ORDER BY entry_ts DESC""",
|
|
||||||
strategy_id, legacy_name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
rows = await async_fetch(
|
|
||||||
"""SELECT status, pnl_r, tp1_hit, entry_ts, exit_ts
|
|
||||||
FROM paper_trades
|
|
||||||
WHERE strategy_id=$1 AND status NOT IN ('active', 'tp1_hit')
|
|
||||||
ORDER BY entry_ts DESC""",
|
|
||||||
strategy_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
# 即使没有历史记录也要查持仓
|
|
||||||
pass
|
|
||||||
|
|
||||||
total = len(rows)
|
|
||||||
wins = [r for r in rows if (r["pnl_r"] or 0) > 0]
|
|
||||||
losses = [r for r in rows if (r["pnl_r"] or 0) < 0]
|
|
||||||
win_rate = round(len(wins) / total * 100, 1) if total else 0.0
|
|
||||||
avg_win = round(sum(r["pnl_r"] for r in wins) / len(wins), 3) if wins else 0.0
|
|
||||||
avg_loss = round(sum(r["pnl_r"] for r in losses) / len(losses), 3) if losses else 0.0
|
|
||||||
last_trade_at = rows[0]["exit_ts"] if rows else None
|
|
||||||
|
|
||||||
# 24h stats
|
|
||||||
cutoff_ms = int((datetime.utcnow() - timedelta(hours=24)).timestamp() * 1000)
|
|
||||||
rows_24h = [r for r in rows if (r["exit_ts"] or 0) >= cutoff_ms]
|
|
||||||
pnl_r_24h = round(sum(r["pnl_r"] or 0 for r in rows_24h), 3)
|
|
||||||
pnl_usdt_24h = round(pnl_r_24h * 200, 2)
|
|
||||||
|
|
||||||
# Open positions — status IN ('active','tp1_hit'),同时兼容新旧记录
|
|
||||||
if legacy_name:
|
|
||||||
open_rows = await async_fetch(
|
|
||||||
"""SELECT COUNT(*) as cnt FROM paper_trades
|
|
||||||
WHERE status IN ('active','tp1_hit')
|
|
||||||
AND (strategy_id=$1 OR (strategy_id IS NULL AND strategy=$2))""",
|
|
||||||
strategy_id, legacy_name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
open_rows = await async_fetch(
|
|
||||||
"""SELECT COUNT(*) as cnt FROM paper_trades
|
|
||||||
WHERE strategy_id=$1 AND status IN ('active','tp1_hit')""",
|
|
||||||
strategy_id
|
|
||||||
)
|
|
||||||
open_positions = int(open_rows[0]["cnt"]) if open_rows else 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
"trade_count": total,
|
|
||||||
"win_rate": win_rate,
|
|
||||||
"avg_win_r": avg_win,
|
|
||||||
"avg_loss_r": avg_loss,
|
|
||||||
"open_positions": open_positions,
|
|
||||||
"pnl_usdt_24h": pnl_usdt_24h,
|
|
||||||
"pnl_r_24h": pnl_r_24h,
|
|
||||||
"last_trade_at": last_trade_at,
|
|
||||||
"net_r": round(sum(r["pnl_r"] or 0 for r in rows), 3),
|
|
||||||
"net_usdt": round(sum(r["pnl_r"] or 0 for r in rows) * 200, 2),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Endpoints ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@app.post("/api/strategies")
|
|
||||||
async def create_strategy(body: StrategyCreateRequest, user: dict = Depends(get_current_user)):
|
|
||||||
"""创建新策略实例"""
|
|
||||||
new_id = str(_uuid.uuid4())
|
|
||||||
await async_execute(
|
|
||||||
"""INSERT INTO strategies (
|
|
||||||
strategy_id, display_name, schema_version, status,
|
|
||||||
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,
|
|
||||||
initial_balance, current_balance,
|
|
||||||
description
|
|
||||||
) VALUES (
|
|
||||||
$1,$2,1,'running',
|
|
||||||
$3,$4,$5,$6,
|
|
||||||
$7,$8,$9,$10,
|
|
||||||
$11,
|
|
||||||
$12,$13,
|
|
||||||
$14,
|
|
||||||
$15,$16,$17,
|
|
||||||
$18,$19,
|
|
||||||
$20,$21,
|
|
||||||
$22,$23,$24,
|
|
||||||
$25,$26,
|
|
||||||
$27,$27,$28
|
|
||||||
)""",
|
|
||||||
new_id, body.display_name,
|
|
||||||
body.symbol, body.direction, body.cvd_fast_window, body.cvd_slow_window,
|
|
||||||
body.weight_direction, body.weight_env, body.weight_aux, body.weight_momentum,
|
|
||||||
body.entry_score,
|
|
||||||
body.gate_vol_enabled, body.vol_atr_pct_min,
|
|
||||||
body.gate_cvd_enabled,
|
|
||||||
body.gate_whale_enabled, body.whale_usd_threshold, body.whale_flow_pct,
|
|
||||||
body.gate_obi_enabled, body.obi_threshold,
|
|
||||||
body.gate_spot_perp_enabled, body.spot_perp_threshold,
|
|
||||||
body.sl_atr_multiplier, body.tp1_ratio, body.tp2_ratio,
|
|
||||||
body.timeout_minutes, body.flip_threshold,
|
|
||||||
body.initial_balance, body.description
|
|
||||||
)
|
|
||||||
row = await async_fetchrow("SELECT * FROM strategies WHERE strategy_id=$1", new_id)
|
|
||||||
return {"ok": True, "strategy": _strategy_row_to_detail(dict(row))}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/strategies")
|
|
||||||
async def list_strategies(
|
|
||||||
include_deprecated: bool = False,
|
|
||||||
user: dict = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""获取策略列表"""
|
|
||||||
if include_deprecated:
|
|
||||||
rows = await async_fetch("SELECT * FROM strategies ORDER BY created_at ASC")
|
|
||||||
else:
|
|
||||||
rows = await async_fetch(
|
|
||||||
"SELECT * FROM strategies WHERE status != 'deprecated' ORDER BY created_at ASC"
|
|
||||||
)
|
|
||||||
result = []
|
|
||||||
for row in rows:
|
|
||||||
d = _strategy_row_to_card(dict(row))
|
|
||||||
stats = await _get_strategy_trade_stats(str(row["strategy_id"]))
|
|
||||||
d.update(stats)
|
|
||||||
# 用实时计算的 net_usdt 覆盖 DB 静态的 current_balance
|
|
||||||
d["current_balance"] = round(row["initial_balance"] + d["net_usdt"], 2)
|
|
||||||
result.append(d)
|
|
||||||
return {"strategies": result}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/strategies/{sid}")
|
|
||||||
async def get_strategy(sid: str, user: dict = Depends(get_current_user)):
|
|
||||||
"""获取单个策略详情(含完整参数配置)"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
detail = _strategy_row_to_detail(row)
|
|
||||||
stats = await _get_strategy_trade_stats(sid)
|
|
||||||
detail.update(stats)
|
|
||||||
detail["current_balance"] = round(row["initial_balance"] + detail["net_usdt"], 2)
|
|
||||||
return {"strategy": detail}
|
|
||||||
|
|
||||||
|
|
||||||
@app.patch("/api/strategies/{sid}")
|
|
||||||
async def update_strategy(sid: str, body: StrategyUpdateRequest, user: dict = Depends(get_current_user)):
|
|
||||||
"""更新策略参数(Partial Update)"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] == "deprecated":
|
|
||||||
raise HTTPException(status_code=403, detail="Cannot modify a deprecated strategy")
|
|
||||||
|
|
||||||
# Build SET clause dynamically from non-None fields
|
|
||||||
updates = body.model_dump(exclude_none=True)
|
|
||||||
if not updates:
|
|
||||||
raise HTTPException(status_code=400, detail="No fields to update")
|
|
||||||
|
|
||||||
# Validate weights sum if any weight is being changed
|
|
||||||
weight_fields = {"weight_direction", "weight_env", "weight_aux", "weight_momentum"}
|
|
||||||
if weight_fields & set(updates.keys()):
|
|
||||||
w_dir = updates.get("weight_direction", row["weight_direction"])
|
|
||||||
w_env = updates.get("weight_env", row["weight_env"])
|
|
||||||
w_aux = updates.get("weight_aux", row["weight_aux"])
|
|
||||||
w_mom = updates.get("weight_momentum", row["weight_momentum"])
|
|
||||||
if w_dir + w_env + w_aux + w_mom != 100:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Weights must sum to 100, got {w_dir+w_env+w_aux+w_mom}")
|
|
||||||
|
|
||||||
# Validate individual field ranges
|
|
||||||
validators = {
|
|
||||||
"direction": lambda v: v in {"long_only", "short_only", "both"},
|
|
||||||
"cvd_fast_window": lambda v: v in {"5m", "15m", "30m"},
|
|
||||||
"cvd_slow_window": lambda v: v in {"30m", "1h", "4h"},
|
|
||||||
"weight_direction": lambda v: 10 <= v <= 80,
|
|
||||||
"weight_env": lambda v: 5 <= v <= 60,
|
|
||||||
"weight_aux": lambda v: 0 <= v <= 40,
|
|
||||||
"weight_momentum": lambda v: 0 <= v <= 20,
|
|
||||||
"entry_score": lambda v: 60 <= v <= 95,
|
|
||||||
"obi_threshold": lambda v: 0.1 <= v <= 0.9,
|
|
||||||
"vol_atr_pct_min": lambda v: 0.0001 <= v <= 0.02,
|
|
||||||
"whale_usd_threshold": lambda v: 1000 <= v <= 1000000,
|
|
||||||
"whale_flow_pct": lambda v: 0.0 <= v <= 1.0,
|
|
||||||
"spot_perp_threshold": lambda v: 0.0005 <= v <= 0.01,
|
|
||||||
"sl_atr_multiplier": lambda v: 0.5 <= v <= 3.0,
|
|
||||||
"tp1_ratio": lambda v: 0.3 <= v <= 2.0,
|
|
||||||
"tp2_ratio": lambda v: 0.5 <= v <= 4.0,
|
|
||||||
"timeout_minutes": lambda v: 30 <= v <= 1440,
|
|
||||||
"flip_threshold": lambda v: 60 <= v <= 95,
|
|
||||||
}
|
|
||||||
for field, val in updates.items():
|
|
||||||
if field in validators and not validators[field](val):
|
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid value for {field}: {val}")
|
|
||||||
|
|
||||||
# Execute update
|
|
||||||
set_parts = [f"{k}=${i+2}" for i, k in enumerate(updates.keys())]
|
|
||||||
set_parts.append(f"updated_at=NOW()")
|
|
||||||
sql = f"UPDATE strategies SET {', '.join(set_parts)} WHERE strategy_id=$1"
|
|
||||||
await async_execute(sql, sid, *updates.values())
|
|
||||||
|
|
||||||
updated = await async_fetchrow("SELECT * FROM strategies WHERE strategy_id=$1", sid)
|
|
||||||
return {"ok": True, "strategy": _strategy_row_to_detail(dict(updated))}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/strategies/{sid}/pause")
|
|
||||||
async def pause_strategy(sid: str, user: dict = Depends(get_current_user)):
|
|
||||||
"""暂停策略(停止开新仓,不影响现有持仓)"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] == "deprecated":
|
|
||||||
raise HTTPException(status_code=403, detail="Cannot pause a deprecated strategy")
|
|
||||||
if row["status"] == "paused":
|
|
||||||
return {"ok": True, "message": "Already paused"}
|
|
||||||
await async_execute(
|
|
||||||
"UPDATE strategies SET status='paused', status_changed_at=NOW(), updated_at=NOW() WHERE strategy_id=$1",
|
|
||||||
sid
|
|
||||||
)
|
|
||||||
return {"ok": True, "status": "paused"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/strategies/{sid}/resume")
|
|
||||||
async def resume_strategy(sid: str, user: dict = Depends(get_current_user)):
|
|
||||||
"""恢复策略"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] == "running":
|
|
||||||
return {"ok": True, "message": "Already running"}
|
|
||||||
await async_execute(
|
|
||||||
"UPDATE strategies SET status='running', status_changed_at=NOW(), updated_at=NOW() WHERE strategy_id=$1",
|
|
||||||
sid
|
|
||||||
)
|
|
||||||
return {"ok": True, "status": "running"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/strategies/{sid}/deprecate")
|
|
||||||
async def deprecate_strategy(sid: str, body: DeprecateRequest, user: dict = Depends(get_current_user)):
|
|
||||||
"""废弃策略(数据永久保留,可重新启用)"""
|
|
||||||
if not body.confirm:
|
|
||||||
raise HTTPException(status_code=400, detail="Must set confirm=true to deprecate")
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] == "deprecated":
|
|
||||||
return {"ok": True, "message": "Already deprecated"}
|
|
||||||
await async_execute(
|
|
||||||
"""UPDATE strategies
|
|
||||||
SET status='deprecated', deprecated_at=NOW(),
|
|
||||||
status_changed_at=NOW(), updated_at=NOW()
|
|
||||||
WHERE strategy_id=$1""",
|
|
||||||
sid
|
|
||||||
)
|
|
||||||
return {"ok": True, "status": "deprecated"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/strategies/{sid}/restore")
|
|
||||||
async def restore_strategy(sid: str, user: dict = Depends(get_current_user)):
|
|
||||||
"""重新启用废弃策略(继续原有余额和历史数据)"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] != "deprecated":
|
|
||||||
raise HTTPException(status_code=400, detail="Strategy is not deprecated")
|
|
||||||
await async_execute(
|
|
||||||
"""UPDATE strategies
|
|
||||||
SET status='running', deprecated_at=NULL,
|
|
||||||
status_changed_at=NOW(), updated_at=NOW()
|
|
||||||
WHERE strategy_id=$1""",
|
|
||||||
sid
|
|
||||||
)
|
|
||||||
return {"ok": True, "status": "running"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/strategies/{sid}/add-balance")
|
|
||||||
async def add_balance(sid: str, body: AddBalanceRequest, user: dict = Depends(get_current_user)):
|
|
||||||
"""追加余额(initial_balance 和 current_balance 同步增加)"""
|
|
||||||
row = await _get_strategy_or_404(sid)
|
|
||||||
if row["status"] == "deprecated":
|
|
||||||
raise HTTPException(status_code=403, detail="Cannot add balance to a deprecated strategy")
|
|
||||||
new_initial = round(row["initial_balance"] + body.amount, 2)
|
|
||||||
new_current = round(row["current_balance"] + body.amount, 2)
|
|
||||||
await async_execute(
|
|
||||||
"""UPDATE strategies
|
|
||||||
SET initial_balance=$2, current_balance=$3, updated_at=NOW()
|
|
||||||
WHERE strategy_id=$1""",
|
|
||||||
sid, new_initial, new_current
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"initial_balance": new_initial,
|
|
||||||
"current_balance": new_current,
|
|
||||||
"added": body.amount,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from psycopg2.extras import Json
|
|||||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
||||||
INTERVAL_SECONDS = 300
|
INTERVAL_SECONDS = 300
|
||||||
|
|
||||||
PG_HOST = os.getenv("PG_HOST", "127.0.0.1")
|
PG_HOST = os.getenv("PG_HOST", "10.106.0.3")
|
||||||
PG_PORT = int(os.getenv("PG_PORT", "5432"))
|
PG_PORT = int(os.getenv("PG_PORT", "5432"))
|
||||||
PG_DB = os.getenv("PG_DB", "arb_engine")
|
PG_DB = os.getenv("PG_DB", "arb_engine")
|
||||||
PG_USER = os.getenv("PG_USER", "arb")
|
PG_USER = os.getenv("PG_USER", "arb")
|
||||||
|
|||||||
@ -1,327 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
V5.4 Strategy Factory DB Migration Script
|
|
||||||
- Creates `strategies` table
|
|
||||||
- Adds strategy_id + strategy_name_snapshot to paper_trades, signal_indicators
|
|
||||||
- Inserts existing 3 strategies with fixed UUIDs
|
|
||||||
- Backfills strategy_id + strategy_name_snapshot for all existing records
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import execute_values
|
|
||||||
|
|
||||||
PG_HOST = os.environ.get("PG_HOST", "127.0.0.1")
|
|
||||||
PG_PASS = os.environ.get("PG_PASS", "arb_engine_2026")
|
|
||||||
PG_USER = "arb"
|
|
||||||
PG_DB = "arb_engine"
|
|
||||||
|
|
||||||
# Fixed UUIDs for existing strategies (deterministic, easy to recognize)
|
|
||||||
LEGACY_STRATEGY_MAP = {
|
|
||||||
"v53": ("00000000-0000-0000-0000-000000000053", "V5.3 Standard"),
|
|
||||||
"v53_middle": ("00000000-0000-0000-0000-000000000054", "V5.3 Middle"),
|
|
||||||
"v53_fast": ("00000000-0000-0000-0000-000000000055", "V5.3 Fast"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Default config values per strategy (from strategy JSON files)
|
|
||||||
LEGACY_CONFIGS = {
|
|
||||||
"v53": {
|
|
||||||
"symbol": "BTCUSDT", # multi-symbol, use BTC as representative
|
|
||||||
"cvd_fast_window": "30m",
|
|
||||||
"cvd_slow_window": "4h",
|
|
||||||
"weight_direction": 55,
|
|
||||||
"weight_env": 25,
|
|
||||||
"weight_aux": 15,
|
|
||||||
"weight_momentum": 5,
|
|
||||||
"entry_score": 75,
|
|
||||||
"sl_atr_multiplier": 1.0,
|
|
||||||
"tp1_ratio": 0.75,
|
|
||||||
"tp2_ratio": 1.5,
|
|
||||||
"timeout_minutes": 60,
|
|
||||||
"flip_threshold": 75,
|
|
||||||
"status": "running",
|
|
||||||
"initial_balance": 10000.0,
|
|
||||||
},
|
|
||||||
"v53_middle": {
|
|
||||||
"symbol": "BTCUSDT",
|
|
||||||
"cvd_fast_window": "15m",
|
|
||||||
"cvd_slow_window": "1h",
|
|
||||||
"weight_direction": 55,
|
|
||||||
"weight_env": 25,
|
|
||||||
"weight_aux": 15,
|
|
||||||
"weight_momentum": 5,
|
|
||||||
"entry_score": 75,
|
|
||||||
"sl_atr_multiplier": 1.0,
|
|
||||||
"tp1_ratio": 0.75,
|
|
||||||
"tp2_ratio": 1.5,
|
|
||||||
"timeout_minutes": 60,
|
|
||||||
"flip_threshold": 75,
|
|
||||||
"status": "running",
|
|
||||||
"initial_balance": 10000.0,
|
|
||||||
},
|
|
||||||
"v53_fast": {
|
|
||||||
"symbol": "BTCUSDT",
|
|
||||||
"cvd_fast_window": "5m",
|
|
||||||
"cvd_slow_window": "30m",
|
|
||||||
"weight_direction": 55,
|
|
||||||
"weight_env": 25,
|
|
||||||
"weight_aux": 15,
|
|
||||||
"weight_momentum": 5,
|
|
||||||
"entry_score": 75,
|
|
||||||
"sl_atr_multiplier": 1.0,
|
|
||||||
"tp1_ratio": 0.75,
|
|
||||||
"tp2_ratio": 1.5,
|
|
||||||
"timeout_minutes": 60,
|
|
||||||
"flip_threshold": 75,
|
|
||||||
"status": "running",
|
|
||||||
"initial_balance": 10000.0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
return psycopg2.connect(
|
|
||||||
host=PG_HOST, user=PG_USER, password=PG_PASS, dbname=PG_DB
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def step1_create_strategies_table(cur):
|
|
||||||
print("[Step 1] Creating strategies table...")
|
|
||||||
cur.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS strategies (
|
|
||||||
strategy_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
schema_version INT NOT NULL DEFAULT 1,
|
|
||||||
status TEXT NOT NULL DEFAULT 'running'
|
|
||||||
CHECK (status IN ('running', 'paused', 'deprecated')),
|
|
||||||
status_changed_at TIMESTAMP,
|
|
||||||
last_run_at TIMESTAMP,
|
|
||||||
deprecated_at TIMESTAMP,
|
|
||||||
symbol TEXT NOT NULL
|
|
||||||
CHECK (symbol IN ('BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT')),
|
|
||||||
direction TEXT NOT NULL DEFAULT 'both'
|
|
||||||
CHECK (direction IN ('long_only', 'short_only', 'both')),
|
|
||||||
cvd_fast_window TEXT NOT NULL DEFAULT '30m'
|
|
||||||
CHECK (cvd_fast_window IN ('5m', '15m', '30m')),
|
|
||||||
cvd_slow_window TEXT NOT NULL DEFAULT '4h'
|
|
||||||
CHECK (cvd_slow_window IN ('30m', '1h', '4h')),
|
|
||||||
weight_direction INT NOT NULL DEFAULT 55,
|
|
||||||
weight_env INT NOT NULL DEFAULT 25,
|
|
||||||
weight_aux INT NOT NULL DEFAULT 15,
|
|
||||||
weight_momentum INT NOT NULL DEFAULT 5,
|
|
||||||
entry_score INT NOT NULL DEFAULT 75,
|
|
||||||
gate_obi_enabled BOOL NOT NULL DEFAULT TRUE,
|
|
||||||
obi_threshold FLOAT NOT NULL DEFAULT 0.3,
|
|
||||||
gate_whale_enabled BOOL NOT NULL DEFAULT TRUE,
|
|
||||||
whale_cvd_threshold FLOAT NOT NULL DEFAULT 0.0,
|
|
||||||
gate_vol_enabled BOOL NOT NULL DEFAULT TRUE,
|
|
||||||
atr_percentile_min INT NOT NULL DEFAULT 20,
|
|
||||||
gate_spot_perp_enabled BOOL NOT NULL DEFAULT FALSE,
|
|
||||||
spot_perp_threshold FLOAT NOT NULL DEFAULT 0.002,
|
|
||||||
sl_atr_multiplier FLOAT NOT NULL DEFAULT 1.5,
|
|
||||||
tp1_ratio FLOAT NOT NULL DEFAULT 0.75,
|
|
||||||
tp2_ratio FLOAT NOT NULL DEFAULT 1.5,
|
|
||||||
timeout_minutes INT NOT NULL DEFAULT 240,
|
|
||||||
flip_threshold INT NOT NULL DEFAULT 80,
|
|
||||||
initial_balance FLOAT NOT NULL DEFAULT 10000.0,
|
|
||||||
current_balance FLOAT NOT NULL DEFAULT 10000.0,
|
|
||||||
description TEXT,
|
|
||||||
tags TEXT[],
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_status ON strategies(status)")
|
|
||||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_symbol ON strategies(symbol)")
|
|
||||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_last_run ON strategies(last_run_at)")
|
|
||||||
print("[Step 1] Done.")
|
|
||||||
|
|
||||||
|
|
||||||
def step2_add_columns(cur):
|
|
||||||
print("[Step 2] Adding strategy_id + strategy_name_snapshot columns...")
|
|
||||||
# paper_trades
|
|
||||||
for col, col_type in [
|
|
||||||
("strategy_id", "UUID REFERENCES strategies(strategy_id)"),
|
|
||||||
("strategy_name_snapshot", "TEXT"),
|
|
||||||
]:
|
|
||||||
cur.execute(f"""
|
|
||||||
ALTER TABLE paper_trades
|
|
||||||
ADD COLUMN IF NOT EXISTS {col} {col_type}
|
|
||||||
""")
|
|
||||||
# signal_indicators
|
|
||||||
for col, col_type in [
|
|
||||||
("strategy_id", "UUID REFERENCES strategies(strategy_id)"),
|
|
||||||
("strategy_name_snapshot", "TEXT"),
|
|
||||||
]:
|
|
||||||
cur.execute(f"""
|
|
||||||
ALTER TABLE signal_indicators
|
|
||||||
ADD COLUMN IF NOT EXISTS {col} {col_type}
|
|
||||||
""")
|
|
||||||
# Indexes
|
|
||||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_paper_trades_strategy_id ON paper_trades(strategy_id)")
|
|
||||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_si_strategy_id ON signal_indicators(strategy_id)")
|
|
||||||
print("[Step 2] Done.")
|
|
||||||
|
|
||||||
|
|
||||||
def step3_insert_legacy_strategies(cur):
|
|
||||||
print("[Step 3] Inserting legacy strategies into strategies table...")
|
|
||||||
for strategy_name, (uuid, display_name) in LEGACY_STRATEGY_MAP.items():
|
|
||||||
cfg = LEGACY_CONFIGS[strategy_name]
|
|
||||||
# Compute current_balance from actual paper trades
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
COALESCE(SUM(pnl_r) * 200, 0) as total_pnl_usdt
|
|
||||||
FROM paper_trades
|
|
||||||
WHERE strategy = %s AND status != 'active'
|
|
||||||
""", (strategy_name,))
|
|
||||||
row = cur.fetchone()
|
|
||||||
pnl_usdt = row[0] if row else 0
|
|
||||||
current_balance = round(cfg["initial_balance"] + pnl_usdt, 2)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO strategies (
|
|
||||||
strategy_id, display_name, schema_version, status,
|
|
||||||
symbol, direction,
|
|
||||||
cvd_fast_window, cvd_slow_window,
|
|
||||||
weight_direction, weight_env, weight_aux, weight_momentum,
|
|
||||||
entry_score,
|
|
||||||
gate_obi_enabled, obi_threshold,
|
|
||||||
gate_whale_enabled, whale_cvd_threshold,
|
|
||||||
gate_vol_enabled, atr_percentile_min,
|
|
||||||
gate_spot_perp_enabled, spot_perp_threshold,
|
|
||||||
sl_atr_multiplier, tp1_ratio, tp2_ratio,
|
|
||||||
timeout_minutes, flip_threshold,
|
|
||||||
initial_balance, current_balance,
|
|
||||||
description
|
|
||||||
) VALUES (
|
|
||||||
%s, %s, 1, %s,
|
|
||||||
%s, 'both',
|
|
||||||
%s, %s,
|
|
||||||
%s, %s, %s, %s,
|
|
||||||
%s,
|
|
||||||
TRUE, 0.3,
|
|
||||||
TRUE, 0.0,
|
|
||||||
TRUE, 20,
|
|
||||||
FALSE, 0.002,
|
|
||||||
%s, %s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s, %s,
|
|
||||||
%s
|
|
||||||
)
|
|
||||||
ON CONFLICT (strategy_id) DO NOTHING
|
|
||||||
""", (
|
|
||||||
uuid, display_name, cfg["status"],
|
|
||||||
cfg["symbol"], cfg["cvd_fast_window"], cfg["cvd_slow_window"],
|
|
||||||
cfg["weight_direction"], cfg["weight_env"], cfg["weight_aux"], cfg["weight_momentum"],
|
|
||||||
cfg["entry_score"],
|
|
||||||
cfg["sl_atr_multiplier"], cfg["tp1_ratio"], cfg["tp2_ratio"],
|
|
||||||
cfg["timeout_minutes"], cfg["flip_threshold"],
|
|
||||||
cfg["initial_balance"], current_balance,
|
|
||||||
f"Migrated from V5.3 legacy strategy: {strategy_name}"
|
|
||||||
))
|
|
||||||
print(f" Inserted {strategy_name} → {uuid} (balance: {current_balance})")
|
|
||||||
print("[Step 3] Done.")
|
|
||||||
|
|
||||||
|
|
||||||
def step4_backfill(cur):
|
|
||||||
print("[Step 4] Backfilling strategy_id + strategy_name_snapshot...")
|
|
||||||
for strategy_name, (uuid, display_name) in LEGACY_STRATEGY_MAP.items():
|
|
||||||
# paper_trades
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE paper_trades
|
|
||||||
SET strategy_id = %s::uuid,
|
|
||||||
strategy_name_snapshot = %s
|
|
||||||
WHERE strategy = %s AND strategy_id IS NULL
|
|
||||||
""", (uuid, display_name, strategy_name))
|
|
||||||
count = cur.rowcount
|
|
||||||
print(f" paper_trades [{strategy_name}]: {count} rows updated")
|
|
||||||
|
|
||||||
# signal_indicators
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE signal_indicators
|
|
||||||
SET strategy_id = %s::uuid,
|
|
||||||
strategy_name_snapshot = %s
|
|
||||||
WHERE strategy = %s AND strategy_id IS NULL
|
|
||||||
""", (uuid, display_name, strategy_name))
|
|
||||||
count = cur.rowcount
|
|
||||||
print(f" signal_indicators [{strategy_name}]: {count} rows updated")
|
|
||||||
|
|
||||||
print("[Step 4] Done.")
|
|
||||||
|
|
||||||
|
|
||||||
def step5_verify(cur):
|
|
||||||
print("[Step 5] Verifying migration completeness...")
|
|
||||||
# Check strategies table
|
|
||||||
cur.execute("SELECT COUNT(*) FROM strategies")
|
|
||||||
n = cur.fetchone()[0]
|
|
||||||
print(f" strategies table: {n} rows")
|
|
||||||
|
|
||||||
# Check NULL strategy_id in paper_trades (for known strategies)
|
|
||||||
cur.execute("""
|
|
||||||
SELECT strategy, COUNT(*) as cnt
|
|
||||||
FROM paper_trades
|
|
||||||
WHERE strategy IN ('v53', 'v53_middle', 'v53_fast')
|
|
||||||
AND strategy_id IS NULL
|
|
||||||
GROUP BY strategy
|
|
||||||
""")
|
|
||||||
rows = cur.fetchall()
|
|
||||||
if rows:
|
|
||||||
print(f" WARNING: NULL strategy_id found in paper_trades:")
|
|
||||||
for r in rows:
|
|
||||||
print(f" {r[0]}: {r[1]} rows")
|
|
||||||
else:
|
|
||||||
print(" paper_trades: all known strategies backfilled ✅")
|
|
||||||
|
|
||||||
# Check NULL in signal_indicators
|
|
||||||
cur.execute("""
|
|
||||||
SELECT strategy, COUNT(*) as cnt
|
|
||||||
FROM signal_indicators
|
|
||||||
WHERE strategy IN ('v53', 'v53_middle', 'v53_fast')
|
|
||||||
AND strategy_id IS NULL
|
|
||||||
GROUP BY strategy
|
|
||||||
""")
|
|
||||||
rows = cur.fetchall()
|
|
||||||
if rows:
|
|
||||||
print(f" WARNING: NULL strategy_id found in signal_indicators:")
|
|
||||||
for r in rows:
|
|
||||||
print(f" {r[0]}: {r[1]} rows")
|
|
||||||
else:
|
|
||||||
print(" signal_indicators: all known strategies backfilled ✅")
|
|
||||||
|
|
||||||
print("[Step 5] Done.")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
dry_run = "--dry-run" in sys.argv
|
|
||||||
if dry_run:
|
|
||||||
print("=== DRY RUN MODE (no changes will be committed) ===")
|
|
||||||
|
|
||||||
conn = get_conn()
|
|
||||||
conn.autocommit = False
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
step1_create_strategies_table(cur)
|
|
||||||
step2_add_columns(cur)
|
|
||||||
step3_insert_legacy_strategies(cur)
|
|
||||||
step4_backfill(cur)
|
|
||||||
step5_verify(cur)
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
conn.rollback()
|
|
||||||
print("\n=== DRY RUN: rolled back all changes ===")
|
|
||||||
else:
|
|
||||||
conn.commit()
|
|
||||||
print("\n=== Migration completed successfully ✅ ===")
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
print(f"\n=== ERROR: {e} ===")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
V5.4b Gate Schema Migration
|
|
||||||
将 strategies 表的 gate 字段从4门改为5门,与 signal_engine.py 实际执行逻辑完全对应。
|
|
||||||
|
|
||||||
变更:
|
|
||||||
1. 重命名 atr_percentile_min → vol_atr_pct_min(ATR%价格阈值,如0.002=0.2%)
|
|
||||||
2. 重命名 whale_cvd_threshold → whale_flow_pct(鲸鱼CVD流量阈值,BTC专用,0-1)
|
|
||||||
3. 新增 gate_cvd_enabled BOOLEAN DEFAULT TRUE(门2 CVD共振开关)
|
|
||||||
4. 新增 whale_usd_threshold FLOAT DEFAULT 50000(门3 大单USD金额阈值)
|
|
||||||
5. 用 v53.json 里的 per-symbol 默认值回填旧三条策略
|
|
||||||
|
|
||||||
五门对应:
|
|
||||||
门1 波动率:gate_vol_enabled + vol_atr_pct_min
|
|
||||||
门2 CVD共振:gate_cvd_enabled(无参数,判断快慢CVD同向)
|
|
||||||
门3 鲸鱼否决:gate_whale_enabled + whale_usd_threshold(ALT大单USD)+ whale_flow_pct(BTC CVD流量)
|
|
||||||
门4 OBI否决:gate_obi_enabled + obi_threshold
|
|
||||||
门5 期现背离:gate_spot_perp_enabled + spot_perp_threshold
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os, sys
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
PG_HOST = os.getenv("PG_HOST", "127.0.0.1")
|
|
||||||
PG_USER = os.getenv("PG_USER", "arb")
|
|
||||||
PG_PASS = os.getenv("PG_PASS", "arb_engine_2026")
|
|
||||||
PG_DB = os.getenv("PG_DB", "arb_engine")
|
|
||||||
|
|
||||||
# Per-symbol 默认值(来自 v53.json symbol_gates)
|
|
||||||
SYMBOL_DEFAULTS = {
|
|
||||||
"BTCUSDT": {"vol_atr_pct_min": 0.002, "whale_usd_threshold": 100000, "whale_flow_pct": 0.5, "obi_threshold": 0.30, "spot_perp_threshold": 0.003},
|
|
||||||
"ETHUSDT": {"vol_atr_pct_min": 0.003, "whale_usd_threshold": 50000, "whale_flow_pct": 0.5, "obi_threshold": 0.35, "spot_perp_threshold": 0.005},
|
|
||||||
"SOLUSDT": {"vol_atr_pct_min": 0.004, "whale_usd_threshold": 20000, "whale_flow_pct": 0.5, "obi_threshold": 0.45, "spot_perp_threshold": 0.008},
|
|
||||||
"XRPUSDT": {"vol_atr_pct_min": 0.0025,"whale_usd_threshold": 30000, "whale_flow_pct": 0.5, "obi_threshold": 0.40, "spot_perp_threshold": 0.006},
|
|
||||||
None: {"vol_atr_pct_min": 0.002, "whale_usd_threshold": 50000, "whale_flow_pct": 0.5, "obi_threshold": 0.35, "spot_perp_threshold": 0.005},
|
|
||||||
}
|
|
||||||
|
|
||||||
DRY_RUN = "--dry-run" in sys.argv
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
return psycopg2.connect(
|
|
||||||
host=PG_HOST, port=5432,
|
|
||||||
user=PG_USER, password=PG_PASS, dbname=PG_DB
|
|
||||||
)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
conn = get_conn()
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
print("=== V5.4b Gate Schema Migration ===")
|
|
||||||
print(f"DRY_RUN={DRY_RUN}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Step 1: 检查字段是否已迁移
|
|
||||||
cur.execute("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name='strategies' AND column_name IN
|
|
||||||
('vol_atr_pct_min','whale_flow_pct','gate_cvd_enabled','whale_usd_threshold',
|
|
||||||
'atr_percentile_min','whale_cvd_threshold')
|
|
||||||
""")
|
|
||||||
existing = {r["column_name"] for r in cur.fetchall()}
|
|
||||||
print(f"现有相关字段: {existing}")
|
|
||||||
|
|
||||||
sqls = []
|
|
||||||
|
|
||||||
# Step 2: 改名 atr_percentile_min → vol_atr_pct_min
|
|
||||||
if "atr_percentile_min" in existing and "vol_atr_pct_min" not in existing:
|
|
||||||
sqls.append("ALTER TABLE strategies RENAME COLUMN atr_percentile_min TO vol_atr_pct_min")
|
|
||||||
print("✅ RENAME atr_percentile_min → vol_atr_pct_min")
|
|
||||||
elif "vol_atr_pct_min" in existing:
|
|
||||||
print("⏭️ vol_atr_pct_min 已存在,跳过改名")
|
|
||||||
|
|
||||||
# Step 3: 改名 whale_cvd_threshold → whale_flow_pct
|
|
||||||
if "whale_cvd_threshold" in existing and "whale_flow_pct" not in existing:
|
|
||||||
sqls.append("ALTER TABLE strategies RENAME COLUMN whale_cvd_threshold TO whale_flow_pct")
|
|
||||||
print("✅ RENAME whale_cvd_threshold → whale_flow_pct")
|
|
||||||
elif "whale_flow_pct" in existing:
|
|
||||||
print("⏭️ whale_flow_pct 已存在,跳过改名")
|
|
||||||
|
|
||||||
# Step 4: 新增 gate_cvd_enabled
|
|
||||||
if "gate_cvd_enabled" not in existing:
|
|
||||||
sqls.append("ALTER TABLE strategies ADD COLUMN gate_cvd_enabled BOOLEAN NOT NULL DEFAULT TRUE")
|
|
||||||
print("✅ ADD gate_cvd_enabled BOOLEAN DEFAULT TRUE")
|
|
||||||
else:
|
|
||||||
print("⏭️ gate_cvd_enabled 已存在,跳过")
|
|
||||||
|
|
||||||
# Step 5: 新增 whale_usd_threshold
|
|
||||||
if "whale_usd_threshold" not in existing:
|
|
||||||
sqls.append("ALTER TABLE strategies ADD COLUMN whale_usd_threshold FLOAT NOT NULL DEFAULT 50000")
|
|
||||||
print("✅ ADD whale_usd_threshold FLOAT DEFAULT 50000")
|
|
||||||
else:
|
|
||||||
print("⏭️ whale_usd_threshold 已存在,跳过")
|
|
||||||
|
|
||||||
print()
|
|
||||||
if not sqls:
|
|
||||||
print("无需迁移,所有字段已是最新状态。")
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
if DRY_RUN:
|
|
||||||
print("=== DRY RUN - 以下SQL不会执行 ===")
|
|
||||||
for sql in sqls:
|
|
||||||
print(f" {sql};")
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 执行 DDL
|
|
||||||
for sql in sqls:
|
|
||||||
print(f"执行: {sql}")
|
|
||||||
cur.execute(sql)
|
|
||||||
conn.commit()
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Step 6: 回填 per-symbol 默认值
|
|
||||||
cur.execute("SELECT strategy_id, symbol FROM strategies")
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"回填 {len(rows)} 条策略的 per-symbol 默认值...")
|
|
||||||
for row in rows:
|
|
||||||
sid = row["strategy_id"]
|
|
||||||
sym = row["symbol"]
|
|
||||||
defaults = SYMBOL_DEFAULTS.get(sym, SYMBOL_DEFAULTS[None])
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE strategies SET
|
|
||||||
vol_atr_pct_min = %s,
|
|
||||||
whale_flow_pct = %s,
|
|
||||||
whale_usd_threshold = %s,
|
|
||||||
obi_threshold = %s,
|
|
||||||
spot_perp_threshold = %s
|
|
||||||
WHERE strategy_id = %s
|
|
||||||
""", (
|
|
||||||
defaults["vol_atr_pct_min"],
|
|
||||||
defaults["whale_flow_pct"],
|
|
||||||
defaults["whale_usd_threshold"],
|
|
||||||
defaults["obi_threshold"],
|
|
||||||
defaults["spot_perp_threshold"],
|
|
||||||
sid
|
|
||||||
))
|
|
||||||
print(f" {sid} ({sym}): vol_atr_pct={defaults['vol_atr_pct_min']} whale_usd={defaults['whale_usd_threshold']} obi={defaults['obi_threshold']}")
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=== 迁移完成 ===")
|
|
||||||
|
|
||||||
# 验证
|
|
||||||
cur.execute("SELECT strategy_id, display_name, gate_cvd_enabled, gate_vol_enabled, vol_atr_pct_min, gate_whale_enabled, whale_usd_threshold, whale_flow_pct, gate_obi_enabled, obi_threshold, gate_spot_perp_enabled, spot_perp_threshold FROM strategies ORDER BY created_at")
|
|
||||||
print("\n验证结果:")
|
|
||||||
print(f"{'display_name':<15} {'cvd':>4} {'vol':>4} {'vol_pct':>8} {'whale':>6} {'whale_usd':>10} {'flow_pct':>9} {'obi':>4} {'obi_thr':>8} {'spd':>4} {'spd_thr':>8}")
|
|
||||||
for r in cur.fetchall():
|
|
||||||
print(f"{r['display_name']:<15} {str(r['gate_cvd_enabled']):>4} {str(r['gate_vol_enabled']):>4} {r['vol_atr_pct_min']:>8.4f} {str(r['gate_whale_enabled']):>6} {r['whale_usd_threshold']:>10.0f} {r['whale_flow_pct']:>9.3f} {str(r['gate_obi_enabled']):>4} {r['obi_threshold']:>8.3f} {str(r['gate_spot_perp_enabled']):>4} {r['spot_perp_threshold']:>8.4f}")
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run()
|
|
||||||
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"enabled_strategies": [],
|
"enabled_strategies": [
|
||||||
|
"v53",
|
||||||
|
"v53_fast",
|
||||||
|
"v53_middle"
|
||||||
|
],
|
||||||
"initial_balance": 10000,
|
"initial_balance": 10000,
|
||||||
"risk_per_trade": 0.02,
|
"risk_per_trade": 0.02,
|
||||||
"max_positions": 100,
|
"max_positions": 4,
|
||||||
"tier_multiplier": {
|
"tier_multiplier": {
|
||||||
"light": 0.5,
|
"light": 0.5,
|
||||||
"standard": 1.0,
|
"standard": 1.0,
|
||||||
|
|||||||
@ -47,19 +47,16 @@ def check_and_close(symbol_upper: str, price: float):
|
|||||||
|
|
||||||
with get_sync_conn() as conn:
|
with get_sync_conn() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
# 关联 strategies 表获取 timeout_minutes(每个策略可独立配置超时时间)
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT p.id, p.direction, p.entry_price, p.tp1_price, p.tp2_price, p.sl_price, "
|
"SELECT id, direction, entry_price, tp1_price, tp2_price, sl_price, "
|
||||||
"p.tp1_hit, p.entry_ts, p.atr_at_entry, p.risk_distance, s.timeout_minutes "
|
"tp1_hit, entry_ts, atr_at_entry, risk_distance "
|
||||||
"FROM paper_trades AS p "
|
"FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')",
|
||||||
"LEFT JOIN strategies AS s ON p.strategy_id = s.strategy_id "
|
(symbol_upper,)
|
||||||
"WHERE p.symbol=%s AND p.status IN ('active','tp1_hit')",
|
|
||||||
(symbol_upper,),
|
|
||||||
)
|
)
|
||||||
positions = cur.fetchall()
|
positions = cur.fetchall()
|
||||||
|
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry, rd_db, timeout_db = pos
|
pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry, rd_db = pos
|
||||||
closed = False
|
closed = False
|
||||||
new_status = None
|
new_status = None
|
||||||
pnl_r = 0.0
|
pnl_r = 0.0
|
||||||
@ -115,9 +112,8 @@ def check_and_close(symbol_upper: str, price: float):
|
|||||||
tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0
|
tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0
|
||||||
pnl_r = 0.5 * tp1_r + 0.5 * tp2_r
|
pnl_r = 0.5 * tp1_r + 0.5 * tp2_r
|
||||||
|
|
||||||
# 时间止损:按 strategies.timeout_minutes 配置(缺省 240 分钟)
|
# 时间止损:60分钟(市价平仓,用当前价)
|
||||||
timeout_minutes = timeout_db if timeout_db and timeout_db > 0 else 240
|
if not closed and (now_ms - entry_ts > 60 * 60 * 1000):
|
||||||
if not closed and (now_ms - entry_ts > timeout_minutes * 60 * 1000):
|
|
||||||
closed = True
|
closed = True
|
||||||
exit_price = price # 超时是市价平仓
|
exit_price = price # 超时是市价平仓
|
||||||
new_status = "timeout"
|
new_status = "timeout"
|
||||||
|
|||||||
@ -1,202 +0,0 @@
|
|||||||
"""
|
|
||||||
paper_trading.py — 模拟盘开仓/平仓辅助函数
|
|
||||||
|
|
||||||
从原来的 signal_engine.py 拆分出的 paper_trades 辅助逻辑:
|
|
||||||
- paper_open_trade(): 写入 paper_trades 开仓记录;
|
|
||||||
- paper_has_active_position() / paper_get_active_direction();
|
|
||||||
- paper_close_by_signal() / paper_active_count()。
|
|
||||||
|
|
||||||
行为保持与原实现完全一致,供 signal_engine 调用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from db import get_sync_conn
|
|
||||||
PAPER_FEE_RATE = 0.0005 # Taker手续费 0.05%(开仓+平仓各一次)
|
|
||||||
|
|
||||||
|
|
||||||
def paper_open_trade(
|
|
||||||
symbol: str,
|
|
||||||
direction: str,
|
|
||||||
price: float,
|
|
||||||
score: int,
|
|
||||||
tier: str,
|
|
||||||
atr: float,
|
|
||||||
now_ms: int,
|
|
||||||
factors: dict = None,
|
|
||||||
strategy: str = "v51_baseline",
|
|
||||||
tp_sl: Optional[dict] = None,
|
|
||||||
strategy_id: Optional[str] = None,
|
|
||||||
strategy_name_snapshot: Optional[str] = None,
|
|
||||||
logger=None,
|
|
||||||
):
|
|
||||||
"""模拟开仓:写入 paper_trades"""
|
|
||||||
import json as _json3
|
|
||||||
|
|
||||||
if atr <= 0:
|
|
||||||
return
|
|
||||||
tp_sl_cfg = tp_sl or {}
|
|
||||||
sl_multiplier = float(tp_sl_cfg.get("sl_multiplier", 2.0))
|
|
||||||
|
|
||||||
# 支持两种配置方式:
|
|
||||||
# - 新版 v5.4 strategies 表:tp1_ratio / tp2_ratio = 以 R 计的目标(× risk_distance)
|
|
||||||
# - 旧版 v5.2/v5.3 JSON 策略:tp1_multiplier / tp2_multiplier = 以 ATR 计的目标
|
|
||||||
tp1_ratio = tp_sl_cfg.get("tp1_ratio")
|
|
||||||
tp2_ratio = tp_sl_cfg.get("tp2_ratio")
|
|
||||||
use_r_based = tp1_ratio is not None and tp2_ratio is not None
|
|
||||||
if use_r_based:
|
|
||||||
tp1_ratio = float(tp1_ratio)
|
|
||||||
tp2_ratio = float(tp2_ratio)
|
|
||||||
else:
|
|
||||||
tp1_multiplier = float(tp_sl_cfg.get("tp1_multiplier", 1.5))
|
|
||||||
tp2_multiplier = float(tp_sl_cfg.get("tp2_multiplier", 3.0))
|
|
||||||
|
|
||||||
# 统一定义:1R = SL 距离 = sl_multiplier × ATR
|
|
||||||
risk_distance = sl_multiplier * atr
|
|
||||||
if direction == "LONG":
|
|
||||||
sl = price - risk_distance
|
|
||||||
if use_r_based:
|
|
||||||
tp1 = price + tp1_ratio * risk_distance
|
|
||||||
tp2 = price + tp2_ratio * risk_distance
|
|
||||||
else:
|
|
||||||
tp1 = price + tp1_multiplier * atr
|
|
||||||
tp2 = price + tp2_multiplier * atr
|
|
||||||
else:
|
|
||||||
sl = price + risk_distance
|
|
||||||
if use_r_based:
|
|
||||||
tp1 = price - tp1_ratio * risk_distance
|
|
||||||
tp2 = price - tp2_ratio * risk_distance
|
|
||||||
else:
|
|
||||||
tp1 = price - tp1_multiplier * atr
|
|
||||||
tp2 = price - tp2_multiplier * atr
|
|
||||||
|
|
||||||
# SL 合理性校验:实际距离必须在 risk_distance 的 80%~120% 范围内
|
|
||||||
actual_sl_dist = abs(sl - price)
|
|
||||||
if actual_sl_dist < risk_distance * 0.8 or actual_sl_dist > risk_distance * 1.2:
|
|
||||||
if logger:
|
|
||||||
logger.error(
|
|
||||||
f"[{symbol}] ⚠️ SL校验失败,拒绝开仓: direction={direction} price={price:.4f} "
|
|
||||||
f"sl={sl:.4f} actual_dist={actual_sl_dist:.4f} expected={risk_distance:.4f} atr={atr:.4f}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"INSERT INTO paper_trades (symbol,direction,score,tier,entry_price,entry_ts,tp1_price,tp2_price,sl_price,atr_at_entry,score_factors,strategy,risk_distance,strategy_id,strategy_name_snapshot) "
|
|
||||||
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
|
|
||||||
(
|
|
||||||
symbol,
|
|
||||||
direction,
|
|
||||||
score,
|
|
||||||
tier,
|
|
||||||
price,
|
|
||||||
now_ms,
|
|
||||||
tp1,
|
|
||||||
tp2,
|
|
||||||
sl,
|
|
||||||
atr,
|
|
||||||
_json3.dumps(factors) if factors else None,
|
|
||||||
strategy,
|
|
||||||
risk_distance,
|
|
||||||
strategy_id,
|
|
||||||
strategy_name_snapshot,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[{symbol}] 📝 模拟开仓: {direction} @ {price:.2f} score={score} tier={tier} strategy={strategy} "
|
|
||||||
f"TP1={tp1:.2f} TP2={tp2:.2f} SL={sl:.2f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def paper_has_active_position(symbol: str, strategy: Optional[str] = None) -> bool:
|
|
||||||
"""检查该币种是否有活跃持仓"""
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
if strategy:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COUNT(*) FROM paper_trades WHERE symbol=%s AND strategy=%s AND status IN ('active','tp1_hit')",
|
|
||||||
(symbol, strategy),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COUNT(*) FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')",
|
|
||||||
(symbol,),
|
|
||||||
)
|
|
||||||
return cur.fetchone()[0] > 0
|
|
||||||
|
|
||||||
|
|
||||||
def paper_get_active_direction(symbol: str, strategy: Optional[str] = None) -> str | None:
|
|
||||||
"""获取该币种活跃持仓的方向,无持仓返回 None"""
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
if strategy:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT direction FROM paper_trades WHERE symbol=%s AND strategy=%s AND status IN ('active','tp1_hit') LIMIT 1",
|
|
||||||
(symbol, strategy),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT direction FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit') LIMIT 1",
|
|
||||||
(symbol,),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
return row[0] if row else None
|
|
||||||
|
|
||||||
|
|
||||||
def paper_close_by_signal(symbol: str, current_price: float, now_ms: int, strategy: Optional[str] = None, logger=None):
|
|
||||||
"""反向信号平仓:按当前价平掉该币种所有活跃仓位"""
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
if strategy:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT id, direction, entry_price, tp1_hit, atr_at_entry, risk_distance "
|
|
||||||
"FROM paper_trades WHERE symbol=%s AND strategy=%s AND status IN ('active','tp1_hit')",
|
|
||||||
(symbol, strategy),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT id, direction, entry_price, tp1_hit, atr_at_entry, risk_distance "
|
|
||||||
"FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')",
|
|
||||||
(symbol,),
|
|
||||||
)
|
|
||||||
positions = cur.fetchall()
|
|
||||||
for pos in positions:
|
|
||||||
pid, direction, entry_price, tp1_hit, atr_entry, rd_db = pos
|
|
||||||
risk_distance = rd_db if rd_db and rd_db > 0 else abs(entry_price * 0.01)
|
|
||||||
if direction == "LONG":
|
|
||||||
pnl_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0
|
|
||||||
else:
|
|
||||||
pnl_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else 0
|
|
||||||
# 扣手续费
|
|
||||||
fee_r = (2 * PAPER_FEE_RATE * entry_price) / risk_distance if risk_distance > 0 else 0
|
|
||||||
pnl_r -= fee_r
|
|
||||||
cur.execute(
|
|
||||||
"UPDATE paper_trades SET status='signal_flip', exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s",
|
|
||||||
(current_price, now_ms, round(pnl_r, 4), pid),
|
|
||||||
)
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[{symbol}] 📝 反向信号平仓: {direction} @ {current_price:.2f} pnl={pnl_r:+.2f}R"
|
|
||||||
f"{f' strategy={strategy}' if strategy else ''}"
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def paper_active_count(strategy: Optional[str] = None) -> int:
|
|
||||||
"""当前活跃持仓总数(按策略独立计数)"""
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
if strategy:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COUNT(*) FROM paper_trades WHERE strategy=%s AND status IN ('active','tp1_hit')",
|
|
||||||
(strategy,),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')"
|
|
||||||
)
|
|
||||||
return cur.fetchone()[0]
|
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ if not _DB_PASSWORD:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": os.getenv("DB_HOST", "127.0.0.1"),
|
"host": os.getenv("DB_HOST", "10.106.0.3"),
|
||||||
"port": int(os.getenv("DB_PORT", "5432")),
|
"port": int(os.getenv("DB_PORT", "5432")),
|
||||||
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
||||||
"user": os.getenv("DB_USER", "arb"),
|
"user": os.getenv("DB_USER", "arb"),
|
||||||
|
|||||||
@ -3,5 +3,3 @@ uvicorn
|
|||||||
httpx
|
httpx
|
||||||
python-dotenv
|
python-dotenv
|
||||||
psutil
|
psutil
|
||||||
asyncpg
|
|
||||||
psycopg2-binary
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ if not _DB_PASSWORD:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": os.getenv("DB_HOST", "127.0.0.1"),
|
"host": os.getenv("DB_HOST", "10.106.0.3"),
|
||||||
"port": int(os.getenv("DB_PORT", "5432")),
|
"port": int(os.getenv("DB_PORT", "5432")),
|
||||||
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
"dbname": os.getenv("DB_NAME", "arb_engine"),
|
||||||
"user": os.getenv("DB_USER", "arb"),
|
"user": os.getenv("DB_USER", "arb"),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,298 +0,0 @@
|
|||||||
"""
|
|
||||||
signal_state.py — CVD/ATR 滚动窗口与 SymbolState 抽象
|
|
||||||
|
|
||||||
从原来的 signal_engine.py 中拆分出的纯数据结构与计算逻辑:
|
|
||||||
- TradeWindow:逐笔成交滚动窗口,负责 CVD/VWAP 计算;
|
|
||||||
- ATRCalculator:按固定周期聚合 K 线并计算 ATR/百分位;
|
|
||||||
- get_max_fr:资金费率历史最大值缓存;
|
|
||||||
- SymbolState:单币种的内存状态(窗口、指标快照、巨鲸数据等)。
|
|
||||||
|
|
||||||
这些类保持与原实现完全一致,只是搬迁到独立模块,便于维护与测试。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from collections import deque
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from db import get_sync_conn
|
|
||||||
|
|
||||||
|
|
||||||
def to_float(value: Any) -> Optional[float]:
|
|
||||||
try:
|
|
||||||
return float(value) if value is not None else None
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TradeWindow:
|
|
||||||
def __init__(self, window_ms: int):
|
|
||||||
self.window_ms = window_ms
|
|
||||||
self.trades: deque = deque()
|
|
||||||
self.buy_vol = 0.0
|
|
||||||
self.sell_vol = 0.0
|
|
||||||
self.pq_sum = 0.0
|
|
||||||
self.q_sum = 0.0
|
|
||||||
|
|
||||||
def add(self, time_ms: int, qty: float, price: float, is_buyer_maker: int):
|
|
||||||
self.trades.append((time_ms, qty, price, is_buyer_maker))
|
|
||||||
pq = price * qty
|
|
||||||
self.pq_sum += pq
|
|
||||||
self.q_sum += qty
|
|
||||||
if is_buyer_maker == 0:
|
|
||||||
self.buy_vol += qty
|
|
||||||
else:
|
|
||||||
self.sell_vol += qty
|
|
||||||
|
|
||||||
def trim(self, now_ms: int):
|
|
||||||
cutoff = now_ms - self.window_ms
|
|
||||||
while self.trades and self.trades[0][0] < cutoff:
|
|
||||||
t_ms, qty, price, ibm = self.trades.popleft()
|
|
||||||
self.pq_sum -= price * qty
|
|
||||||
self.q_sum -= qty
|
|
||||||
if ibm == 0:
|
|
||||||
self.buy_vol -= qty
|
|
||||||
else:
|
|
||||||
self.sell_vol -= qty
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cvd(self) -> float:
|
|
||||||
return self.buy_vol - self.sell_vol
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vwap(self) -> float:
|
|
||||||
return self.pq_sum / self.q_sum if self.q_sum > 0 else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class ATRCalculator:
|
|
||||||
def __init__(self, period_ms: int, length: int):
|
|
||||||
self.period_ms = period_ms
|
|
||||||
self.length = length
|
|
||||||
self.candles: deque = deque(maxlen=length + 1)
|
|
||||||
self.current_candle: Optional[dict] = None
|
|
||||||
self.atr_history: deque = deque(maxlen=288)
|
|
||||||
|
|
||||||
def update(self, time_ms: int, price: float):
|
|
||||||
bar_ms = (time_ms // self.period_ms) * self.period_ms
|
|
||||||
if self.current_candle is None or self.current_candle["bar"] != bar_ms:
|
|
||||||
if self.current_candle is not None:
|
|
||||||
self.candles.append(self.current_candle)
|
|
||||||
self.current_candle = {
|
|
||||||
"bar": bar_ms,
|
|
||||||
"open": price,
|
|
||||||
"high": price,
|
|
||||||
"low": price,
|
|
||||||
"close": price,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
c = self.current_candle
|
|
||||||
c["high"] = max(c["high"], price)
|
|
||||||
c["low"] = min(c["low"], price)
|
|
||||||
c["close"] = price
|
|
||||||
|
|
||||||
@property
|
|
||||||
def atr(self) -> float:
|
|
||||||
if len(self.candles) < 2:
|
|
||||||
return 0.0
|
|
||||||
trs = []
|
|
||||||
candles_list = list(self.candles)
|
|
||||||
for i in range(1, len(candles_list)):
|
|
||||||
prev_close = candles_list[i - 1]["close"]
|
|
||||||
c = candles_list[i]
|
|
||||||
tr = max(
|
|
||||||
c["high"] - c["low"],
|
|
||||||
abs(c["high"] - prev_close),
|
|
||||||
abs(c["low"] - prev_close),
|
|
||||||
)
|
|
||||||
trs.append(tr)
|
|
||||||
if not trs:
|
|
||||||
return 0.0
|
|
||||||
atr_val = trs[0]
|
|
||||||
for tr in trs[1:]:
|
|
||||||
atr_val = (atr_val * (self.length - 1) + tr) / self.length
|
|
||||||
return atr_val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def atr_percentile(self) -> float:
|
|
||||||
current = self.atr
|
|
||||||
if current == 0:
|
|
||||||
return 50.0
|
|
||||||
self.atr_history.append(current)
|
|
||||||
if len(self.atr_history) < 10:
|
|
||||||
return 50.0
|
|
||||||
sorted_hist = sorted(self.atr_history)
|
|
||||||
rank = sum(1 for x in sorted_hist if x <= current)
|
|
||||||
return (rank / len(sorted_hist)) * 100
|
|
||||||
|
|
||||||
|
|
||||||
# ─── FR 历史最大值缓存(每小时更新)───────────────────────────────
|
|
||||||
_max_fr_cache: dict = {} # {symbol: max_abs_fr}
|
|
||||||
_max_fr_updated: float = 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_max_fr(symbol: str) -> float:
|
|
||||||
"""获取该币种历史最大|FR|,每小时刷新一次"""
|
|
||||||
global _max_fr_cache, _max_fr_updated
|
|
||||||
now = time.time()
|
|
||||||
if now - _max_fr_updated > 3600 or symbol not in _max_fr_cache:
|
|
||||||
try:
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT symbol, MAX(ABS((value->>'fundingRate')::float)) as max_fr "
|
|
||||||
"FROM market_indicators WHERE indicator_type='funding_rate' "
|
|
||||||
"GROUP BY symbol"
|
|
||||||
)
|
|
||||||
for row in cur.fetchall():
|
|
||||||
_max_fr_cache[row[0]] = row[1] if row[1] else 0.0001
|
|
||||||
_max_fr_updated = now
|
|
||||||
except Exception:
|
|
||||||
# 读取失败时保持旧缓存,返回一个小的默认值防除零
|
|
||||||
pass
|
|
||||||
return _max_fr_cache.get(symbol, 0.0001) # 默认0.01%防除零
|
|
||||||
|
|
||||||
|
|
||||||
class SymbolState:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
symbol: str,
|
|
||||||
window_fast_ms: int,
|
|
||||||
window_mid_ms: int,
|
|
||||||
window_day_ms: int,
|
|
||||||
window_vwap_ms: int,
|
|
||||||
atr_period_ms: int,
|
|
||||||
atr_length: int,
|
|
||||||
fetch_market_indicators_fn,
|
|
||||||
):
|
|
||||||
self.symbol = symbol
|
|
||||||
self.win_fast = TradeWindow(window_fast_ms)
|
|
||||||
self.win_mid = TradeWindow(window_mid_ms)
|
|
||||||
self.win_day = TradeWindow(window_day_ms)
|
|
||||||
self.win_vwap = TradeWindow(window_vwap_ms)
|
|
||||||
self.atr_calc = ATRCalculator(atr_period_ms, atr_length)
|
|
||||||
self.last_processed_id = 0
|
|
||||||
self.last_trade_price = 0.0
|
|
||||||
self.warmup = True
|
|
||||||
self.prev_cvd_fast = 0.0
|
|
||||||
self.prev_cvd_fast_slope = 0.0
|
|
||||||
self.prev_oi_value = 0.0
|
|
||||||
# 从外部函数获取最新 market_indicators
|
|
||||||
self.market_indicators = fetch_market_indicators_fn(symbol)
|
|
||||||
self.last_signal_ts: dict[str, int] = {}
|
|
||||||
self.last_signal_dir: dict[str, str] = {}
|
|
||||||
self.recent_large_trades: deque = deque()
|
|
||||||
# ── Phase 2 实时内存字段(由后台 WebSocket 协程更新)──────────
|
|
||||||
self.rt_obi: float = 0.0
|
|
||||||
self.rt_spot_perp_div: float = 0.0
|
|
||||||
# tiered_cvd_whale:按成交额分档,实时累计(最近15分钟窗口)
|
|
||||||
self._whale_trades: deque = deque() # (time_ms, usd_val, is_sell)
|
|
||||||
self.WHALE_WINDOW_MS: int = 15 * 60 * 1000 # 15 分钟
|
|
||||||
|
|
||||||
def process_trade(self, agg_id: int, time_ms: int, price: float, qty: float, is_buyer_maker: int):
|
|
||||||
now_ms = time_ms
|
|
||||||
self.win_fast.add(time_ms, qty, price, is_buyer_maker)
|
|
||||||
self.win_mid.add(time_ms, qty, price, is_buyer_maker)
|
|
||||||
self.win_day.add(time_ms, qty, price, is_buyer_maker)
|
|
||||||
self.win_vwap.add(time_ms, qty, price, is_buyer_maker)
|
|
||||||
self.atr_calc.update(time_ms, price)
|
|
||||||
self.win_fast.trim(now_ms)
|
|
||||||
self.win_mid.trim(now_ms)
|
|
||||||
self.win_day.trim(now_ms)
|
|
||||||
self.win_vwap.trim(now_ms)
|
|
||||||
self.last_processed_id = agg_id
|
|
||||||
self.last_trade_price = price # 最新成交价,用于 entry_price
|
|
||||||
|
|
||||||
# tiered_cvd_whale 实时累计(>$100k 为巨鲸)
|
|
||||||
usd_val = price * qty
|
|
||||||
if usd_val >= 100_000:
|
|
||||||
self._whale_trades.append((time_ms, usd_val, bool(is_buyer_maker)))
|
|
||||||
# 修剪 15 分钟窗口
|
|
||||||
cutoff = now_ms - self.WHALE_WINDOW_MS
|
|
||||||
while self._whale_trades and self._whale_trades[0][0] < cutoff:
|
|
||||||
self._whale_trades.popleft()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def whale_cvd_ratio(self) -> float:
|
|
||||||
"""巨鲸净 CVD 比率[-1,1],基于最近 15 分钟 >$100k 成交"""
|
|
||||||
buy_usd = sum(t[1] for t in self._whale_trades if not t[2])
|
|
||||||
sell_usd = sum(t[1] for t in self._whale_trades if t[2])
|
|
||||||
total = buy_usd + sell_usd
|
|
||||||
return (buy_usd - sell_usd) / total if total > 0 else 0.0
|
|
||||||
|
|
||||||
def compute_p95_p99(self) -> tuple:
|
|
||||||
if len(self.win_day.trades) < 100:
|
|
||||||
return 5.0, 10.0
|
|
||||||
qtys = sorted([t[1] for t in self.win_day.trades])
|
|
||||||
n = len(qtys)
|
|
||||||
p95 = qtys[int(n * 0.95)]
|
|
||||||
p99 = qtys[int(n * 0.99)]
|
|
||||||
if "BTC" in self.symbol:
|
|
||||||
p95 = max(p95, 5.0)
|
|
||||||
p99 = max(p99, 10.0)
|
|
||||||
else:
|
|
||||||
p95 = max(p95, 50.0)
|
|
||||||
p99 = max(p99, 100.0)
|
|
||||||
return p95, p99
|
|
||||||
|
|
||||||
def update_large_trades(self, now_ms: int, p99: float):
|
|
||||||
cutoff = now_ms - 15 * 60 * 1000
|
|
||||||
while self.recent_large_trades and self.recent_large_trades[0][0] < cutoff:
|
|
||||||
self.recent_large_trades.popleft()
|
|
||||||
# 只检查新 trade(避免重复添加)
|
|
||||||
seen = set(t[0] for t in self.recent_large_trades) # time_ms 作为去重 key
|
|
||||||
for t in self.win_fast.trades:
|
|
||||||
if t[1] >= p99 and t[0] > cutoff and t[0] not in seen:
|
|
||||||
self.recent_large_trades.append((t[0], t[1], t[3]))
|
|
||||||
seen.add(t[0])
|
|
||||||
|
|
||||||
def build_evaluation_snapshot(self, now_ms: int) -> dict:
|
|
||||||
cvd_fast = self.win_fast.cvd
|
|
||||||
cvd_mid = self.win_mid.cvd
|
|
||||||
cvd_day = self.win_day.cvd
|
|
||||||
vwap = self.win_vwap.vwap
|
|
||||||
atr = self.atr_calc.atr
|
|
||||||
atr_pct = self.atr_calc.atr_percentile
|
|
||||||
p95, p99 = self.compute_p95_p99()
|
|
||||||
self.update_large_trades(now_ms, p99)
|
|
||||||
price = self.last_trade_price if self.last_trade_price > 0 else vwap # 用最新成交价,非 VWAP
|
|
||||||
cvd_fast_slope = cvd_fast - self.prev_cvd_fast
|
|
||||||
cvd_fast_accel = cvd_fast_slope - self.prev_cvd_fast_slope
|
|
||||||
self.prev_cvd_fast = cvd_fast
|
|
||||||
self.prev_cvd_fast_slope = cvd_fast_slope
|
|
||||||
|
|
||||||
oi_value = to_float(self.market_indicators.get("open_interest_hist"))
|
|
||||||
if oi_value is None or self.prev_oi_value == 0:
|
|
||||||
oi_change = 0.0
|
|
||||||
environment_score = 10
|
|
||||||
else:
|
|
||||||
oi_change = (
|
|
||||||
(oi_value - self.prev_oi_value) / self.prev_oi_value
|
|
||||||
if self.prev_oi_value > 0
|
|
||||||
else 0.0
|
|
||||||
)
|
|
||||||
if oi_change >= 0.03:
|
|
||||||
environment_score = 15
|
|
||||||
elif oi_change > 0:
|
|
||||||
environment_score = 10
|
|
||||||
else:
|
|
||||||
environment_score = 5
|
|
||||||
if oi_value is not None and oi_value > 0:
|
|
||||||
self.prev_oi_value = oi_value
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cvd_fast": cvd_fast,
|
|
||||||
"cvd_mid": cvd_mid,
|
|
||||||
"cvd_day": cvd_day,
|
|
||||||
"vwap": vwap,
|
|
||||||
"atr": atr,
|
|
||||||
"atr_value": atr,
|
|
||||||
"atr_pct": atr_pct,
|
|
||||||
"p95": p95,
|
|
||||||
"p99": p99,
|
|
||||||
"price": price,
|
|
||||||
"cvd_fast_slope": cvd_fast_slope,
|
|
||||||
"cvd_fast_accel": cvd_fast_accel,
|
|
||||||
"oi_change": oi_change,
|
|
||||||
"environment_score": environment_score,
|
|
||||||
"oi_value": oi_value,
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
"""
|
|
||||||
strategy_loader.py — 从 JSON 文件 / DB 加载策略配置
|
|
||||||
|
|
||||||
从原来的 signal_engine.py 拆分出的策略加载逻辑:
|
|
||||||
- load_strategy_configs(): 从 backend/strategies/*.json 读取配置;
|
|
||||||
- load_strategy_configs_from_db(): 从 strategies 表读取 running 策略并映射到 cfg dict。
|
|
||||||
|
|
||||||
行为保持与原实现完全一致,用于给 signal_engine 等调用方复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from db import get_sync_conn
|
|
||||||
|
|
||||||
logger = logging.getLogger("strategy-loader")
|
|
||||||
|
|
||||||
STRATEGY_DIR = os.path.join(os.path.dirname(__file__), "strategies")
|
|
||||||
DEFAULT_STRATEGY_FILES = [
|
|
||||||
# 仅保留 V5.3 系列作为本地默认策略
|
|
||||||
"v53.json",
|
|
||||||
"v53_fast.json",
|
|
||||||
"v53_middle.json",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def load_strategy_configs() -> list[dict]:
|
|
||||||
"""从本地 JSON 文件加载默认策略配置"""
|
|
||||||
configs: list[dict[str, Any]] = []
|
|
||||||
for filename in DEFAULT_STRATEGY_FILES:
|
|
||||||
path = os.path.join(STRATEGY_DIR, filename)
|
|
||||||
try:
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
if isinstance(cfg, dict) and cfg.get("name"):
|
|
||||||
configs.append(cfg)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.warning(f"策略配置缺失: {path}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"策略配置加载失败 {path}: {e}")
|
|
||||||
if not configs:
|
|
||||||
logger.warning("未加载到策略配置,回退到 v53 默认配置")
|
|
||||||
configs.append(
|
|
||||||
{
|
|
||||||
"name": "v53",
|
|
||||||
"threshold": 75,
|
|
||||||
"flip_threshold": 85,
|
|
||||||
"tp_sl": {
|
|
||||||
"sl_multiplier": 2.0,
|
|
||||||
"tp1_multiplier": 1.5,
|
|
||||||
"tp2_multiplier": 3.0,
|
|
||||||
},
|
|
||||||
# 默认支持四个主交易对,其他细节(gates/symbol_gates)
|
|
||||||
# 在 evaluate_factory_strategy 内部有安全的默认值。
|
|
||||||
"symbols": ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return configs
|
|
||||||
|
|
||||||
|
|
||||||
def load_strategy_configs_from_db() -> list[dict]:
|
|
||||||
"""
|
|
||||||
V5.4: 从 strategies 表读取 running 状态的策略配置。
|
|
||||||
把 DB 字段映射成现有 JSON 格式(保持与 JSON 文件完全兼容)。
|
|
||||||
失败时返回空列表,调用方应 fallback 到 JSON。
|
|
||||||
内存安全:每次读取只返回配置列表,无缓存,无大对象。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with get_sync_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
strategy_id::text, display_name, symbol,
|
|
||||||
cvd_fast_window, cvd_slow_window,
|
|
||||||
weight_direction, weight_env, weight_aux, weight_momentum,
|
|
||||||
entry_score,
|
|
||||||
gate_obi_enabled, obi_threshold,
|
|
||||||
gate_whale_enabled, whale_usd_threshold, whale_flow_pct,
|
|
||||||
gate_vol_enabled, vol_atr_pct_min,
|
|
||||||
gate_cvd_enabled,
|
|
||||||
gate_spot_perp_enabled, spot_perp_threshold,
|
|
||||||
sl_atr_multiplier, tp1_ratio, tp2_ratio,
|
|
||||||
timeout_minutes, flip_threshold, direction
|
|
||||||
FROM strategies
|
|
||||||
WHERE status = 'running'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
configs: list[dict[str, Any]] = []
|
|
||||||
for row in rows:
|
|
||||||
(
|
|
||||||
sid,
|
|
||||||
display_name,
|
|
||||||
symbol,
|
|
||||||
cvd_fast,
|
|
||||||
cvd_slow,
|
|
||||||
w_dir,
|
|
||||||
w_env,
|
|
||||||
w_aux,
|
|
||||||
w_mom,
|
|
||||||
entry_score,
|
|
||||||
gate_obi,
|
|
||||||
obi_thr,
|
|
||||||
gate_whale,
|
|
||||||
whale_usd_thr,
|
|
||||||
whale_flow_pct_val,
|
|
||||||
gate_vol,
|
|
||||||
vol_atr_pct,
|
|
||||||
gate_cvd,
|
|
||||||
gate_spot,
|
|
||||||
spot_thr,
|
|
||||||
sl_mult,
|
|
||||||
tp1_r,
|
|
||||||
tp2_r,
|
|
||||||
timeout_min,
|
|
||||||
flip_thr,
|
|
||||||
direction,
|
|
||||||
) = row
|
|
||||||
|
|
||||||
# 把 display_name 映射回 legacy strategy name(用于兼容评分逻辑)
|
|
||||||
# legacy 策略用固定 UUID 识别
|
|
||||||
LEGACY_UUID_MAP = {
|
|
||||||
"00000000-0000-0000-0000-000000000053": "v53",
|
|
||||||
"00000000-0000-0000-0000-000000000054": "v53_middle",
|
|
||||||
"00000000-0000-0000-0000-000000000055": "v53_fast",
|
|
||||||
}
|
|
||||||
strategy_name = LEGACY_UUID_MAP.get(sid, f"custom_{sid[:8]}")
|
|
||||||
|
|
||||||
cfg: dict[str, Any] = {
|
|
||||||
"name": strategy_name,
|
|
||||||
"strategy_id": sid, # V5.4 新增:用于写 strategy_id 到 DB
|
|
||||||
"strategy_name_snapshot": display_name,
|
|
||||||
"symbol": symbol,
|
|
||||||
"direction": direction,
|
|
||||||
"cvd_fast_window": cvd_fast,
|
|
||||||
"cvd_slow_window": cvd_slow,
|
|
||||||
"threshold": entry_score,
|
|
||||||
"weights": {
|
|
||||||
"direction": w_dir,
|
|
||||||
"env": w_env,
|
|
||||||
"aux": w_aux,
|
|
||||||
"momentum": w_mom,
|
|
||||||
},
|
|
||||||
"gates": {
|
|
||||||
"vol": {
|
|
||||||
"enabled": gate_vol,
|
|
||||||
"vol_atr_pct_min": float(vol_atr_pct or 0.002),
|
|
||||||
},
|
|
||||||
"cvd": {"enabled": gate_cvd},
|
|
||||||
"whale": {
|
|
||||||
"enabled": gate_whale,
|
|
||||||
"whale_usd_threshold": float(whale_usd_thr or 50000),
|
|
||||||
"whale_flow_pct": float(whale_flow_pct_val or 0.5),
|
|
||||||
},
|
|
||||||
"obi": {
|
|
||||||
"enabled": gate_obi,
|
|
||||||
"threshold": float(obi_thr or 0.35),
|
|
||||||
},
|
|
||||||
"spot_perp": {
|
|
||||||
"enabled": gate_spot,
|
|
||||||
"threshold": float(spot_thr or 0.005),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tp_sl": {
|
|
||||||
# V5.4: 统一采用“以 R 计”的配置:
|
|
||||||
# risk_distance = sl_atr_multiplier × ATR = 1R
|
|
||||||
# TP1 = entry ± tp1_ratio × risk_distance
|
|
||||||
# TP2 = entry ± tp2_ratio × risk_distance
|
|
||||||
"sl_multiplier": sl_mult,
|
|
||||||
"tp1_ratio": tp1_r,
|
|
||||||
"tp2_ratio": tp2_r,
|
|
||||||
},
|
|
||||||
"timeout_minutes": timeout_min,
|
|
||||||
"flip_threshold": flip_thr,
|
|
||||||
}
|
|
||||||
configs.append(cfg)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"[DB] 已加载 {len(configs)} 个策略配置: {[c['name'] for c in configs]}"
|
|
||||||
)
|
|
||||||
return configs
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DB] load_strategy_configs_from_db 失败,将 fallback 到 JSON: {e}")
|
|
||||||
return []
|
|
||||||
@ -1,564 +0,0 @@
|
|||||||
"""
|
|
||||||
strategy_scoring.py — V5 策略工厂统一评分逻辑
|
|
||||||
|
|
||||||
从原来的 signal_engine.py 中拆分出的 V5 评分与 Gate 逻辑:
|
|
||||||
- evaluate_factory_strategy(state, now_ms, strategy_cfg, snapshot)
|
|
||||||
→ 单条策略(含工厂产出的 custom_*)的核心评分逻辑
|
|
||||||
- evaluate_signal(state, now_ms, strategy_cfg, snapshot)
|
|
||||||
→ 对外统一入口(仅支持 v53*/custom_*)
|
|
||||||
|
|
||||||
由 signal_engine / backtest 调用。V5.1/V5.2(v51_baseline / v52_8signals)
|
|
||||||
已在此模块中下线:若仍传入旧策略名,将返回空结果并打印 warning。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from signal_state import SymbolState, to_float
|
|
||||||
|
|
||||||
logger = logging.getLogger("strategy-scoring")
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_factory_strategy(
|
|
||||||
state: SymbolState,
|
|
||||||
now_ms: int,
|
|
||||||
strategy_cfg: dict,
|
|
||||||
snapshot: Optional[dict] = None,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
V5 策略工厂统一评分(BTC/ETH/XRP/SOL + custom_*)
|
|
||||||
- 输入:动态 CVD 窗口 + 五门参数 + OI/拥挤/辅助指标
|
|
||||||
- 输出:score / signal / tier + 详细 factors
|
|
||||||
- 支持:单币种策略(symbol)、方向限制(long_only/short_only/both)、自定义四层权重
|
|
||||||
"""
|
|
||||||
strategy_name = strategy_cfg.get("name", "v53")
|
|
||||||
strategy_threshold = int(strategy_cfg.get("threshold", 75))
|
|
||||||
flip_threshold = int(strategy_cfg.get("flip_threshold", 85))
|
|
||||||
|
|
||||||
# per-strategy 方向约束(long_only / short_only / both)
|
|
||||||
dir_cfg_raw = (strategy_cfg.get("direction") or "both").lower()
|
|
||||||
# 兼容策略工厂的 long_only / short_only 配置
|
|
||||||
if dir_cfg_raw in ("long_only", "only_long"):
|
|
||||||
dir_cfg_raw = "long"
|
|
||||||
elif dir_cfg_raw in ("short_only", "only_short"):
|
|
||||||
dir_cfg_raw = "short"
|
|
||||||
if dir_cfg_raw not in ("long", "short", "both"):
|
|
||||||
dir_cfg_raw = "both"
|
|
||||||
|
|
||||||
snap = snapshot or state.build_evaluation_snapshot(now_ms)
|
|
||||||
|
|
||||||
# 按策略配置的 cvd_fast_window / cvd_slow_window 动态切片重算 CVD
|
|
||||||
cvd_fast_window = strategy_cfg.get("cvd_fast_window", "30m")
|
|
||||||
cvd_slow_window = strategy_cfg.get("cvd_slow_window", "4h")
|
|
||||||
|
|
||||||
def _window_ms(code: str) -> int:
|
|
||||||
if not isinstance(code, str) or len(code) < 2:
|
|
||||||
return 30 * 60 * 1000
|
|
||||||
unit = code[-1]
|
|
||||||
try:
|
|
||||||
val = int(code[:-1])
|
|
||||||
except ValueError:
|
|
||||||
return 30 * 60 * 1000
|
|
||||||
if unit == "m":
|
|
||||||
return val * 60 * 1000
|
|
||||||
if unit == "h":
|
|
||||||
return val * 3600 * 1000
|
|
||||||
return 30 * 60 * 1000
|
|
||||||
|
|
||||||
fast_ms = _window_ms(cvd_fast_window)
|
|
||||||
slow_ms = _window_ms(cvd_slow_window)
|
|
||||||
|
|
||||||
if cvd_fast_window == "30m" and cvd_slow_window == "4h":
|
|
||||||
cvd_fast = snap["cvd_fast"]
|
|
||||||
cvd_mid = snap["cvd_mid"]
|
|
||||||
else:
|
|
||||||
cutoff_fast = now_ms - fast_ms
|
|
||||||
cutoff_slow = now_ms - slow_ms
|
|
||||||
buy_f = sell_f = buy_m = sell_m = 0.0
|
|
||||||
src_fast = state.win_mid if fast_ms > state.win_fast.window_ms else state.win_fast
|
|
||||||
for t_ms, qty, _price, ibm in src_fast.trades:
|
|
||||||
if t_ms >= cutoff_fast:
|
|
||||||
if ibm == 0:
|
|
||||||
buy_f += qty
|
|
||||||
else:
|
|
||||||
sell_f += qty
|
|
||||||
for t_ms, qty, _price, ibm in state.win_mid.trades:
|
|
||||||
if t_ms >= cutoff_slow:
|
|
||||||
if ibm == 0:
|
|
||||||
buy_m += qty
|
|
||||||
else:
|
|
||||||
sell_m += qty
|
|
||||||
cvd_fast = buy_f - sell_f
|
|
||||||
cvd_mid = buy_m - sell_m
|
|
||||||
|
|
||||||
price = snap["price"]
|
|
||||||
atr = snap["atr"]
|
|
||||||
atr_value = snap.get("atr_value", atr)
|
|
||||||
cvd_fast_accel = snap["cvd_fast_accel"]
|
|
||||||
environment_score_raw = snap["environment_score"]
|
|
||||||
|
|
||||||
# 默认 result 零值(基础字段从 snapshot 填充,以兼容 save_indicator/save_feature_event)
|
|
||||||
# 注意:cvd_fast/cvd_mid 会在后面覆盖为「按策略窗口重算」后的值,
|
|
||||||
# 这里先用 snapshot 保证字段存在。
|
|
||||||
result = {
|
|
||||||
"strategy": strategy_name,
|
|
||||||
"cvd_fast": snap["cvd_fast"],
|
|
||||||
"cvd_mid": snap["cvd_mid"],
|
|
||||||
"cvd_day": snap["cvd_day"],
|
|
||||||
"cvd_fast_slope": snap["cvd_fast_slope"],
|
|
||||||
"atr": atr,
|
|
||||||
"atr_value": atr_value,
|
|
||||||
"atr_pct": snap["atr_pct"],
|
|
||||||
"vwap": snap["vwap"],
|
|
||||||
"price": price,
|
|
||||||
"p95": snap["p95"],
|
|
||||||
"p99": snap["p99"],
|
|
||||||
"signal": None,
|
|
||||||
"direction": None,
|
|
||||||
"score": 0.0,
|
|
||||||
"tier": None,
|
|
||||||
"factors": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.warmup or price == 0 or atr == 0:
|
|
||||||
return result
|
|
||||||
|
|
||||||
last_signal_ts = state.last_signal_ts.get(strategy_name, 0)
|
|
||||||
COOLDOWN_MS = 10 * 60 * 1000
|
|
||||||
in_cooldown = now_ms - last_signal_ts < COOLDOWN_MS
|
|
||||||
|
|
||||||
# ── 五门参数:优先读 DB config(V5.4),fallback 到 JSON symbol_gates ────
|
|
||||||
db_gates = strategy_cfg.get("gates") or {}
|
|
||||||
symbol_gates = (strategy_cfg.get("symbol_gates") or {}).get(state.symbol, {})
|
|
||||||
|
|
||||||
gate_vol_enabled = db_gates.get("vol", {}).get("enabled", True)
|
|
||||||
min_vol = float(
|
|
||||||
db_gates.get("vol", {}).get("vol_atr_pct_min")
|
|
||||||
or symbol_gates.get("min_vol_threshold", 0.002)
|
|
||||||
)
|
|
||||||
|
|
||||||
gate_cvd_enabled = db_gates.get("cvd", {}).get("enabled", True)
|
|
||||||
|
|
||||||
gate_whale_enabled = db_gates.get("whale", {}).get("enabled", True)
|
|
||||||
whale_usd = float(
|
|
||||||
db_gates.get("whale", {}).get("whale_usd_threshold")
|
|
||||||
or symbol_gates.get("whale_threshold_usd", 50000)
|
|
||||||
)
|
|
||||||
whale_flow_pct = float(
|
|
||||||
db_gates.get("whale", {}).get("whale_flow_pct")
|
|
||||||
or symbol_gates.get("whale_flow_threshold_pct", 0.5)
|
|
||||||
)
|
|
||||||
|
|
||||||
gate_obi_enabled = db_gates.get("obi", {}).get("enabled", True)
|
|
||||||
obi_veto = float(
|
|
||||||
db_gates.get("obi", {}).get("threshold")
|
|
||||||
or symbol_gates.get("obi_veto_threshold", 0.35)
|
|
||||||
)
|
|
||||||
|
|
||||||
gate_spd_enabled = db_gates.get("spot_perp", {}).get("enabled", False)
|
|
||||||
spd_veto = float(
|
|
||||||
db_gates.get("spot_perp", {}).get("threshold")
|
|
||||||
or symbol_gates.get("spot_perp_divergence_veto", 0.005)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 覆盖为按策略窗口重算后的 CVD(用于 signal_indicators 展示)
|
|
||||||
result["cvd_fast"] = cvd_fast
|
|
||||||
result["cvd_mid"] = cvd_mid
|
|
||||||
|
|
||||||
gate_block = None
|
|
||||||
|
|
||||||
# 门1:波动率下限(可关闭)
|
|
||||||
atr_pct_price = atr / price if price > 0 else 0
|
|
||||||
|
|
||||||
# 市场状态(供复盘/优化使用,不直接改变默认策略行为)
|
|
||||||
regime = "range"
|
|
||||||
if atr_pct_price >= 0.012:
|
|
||||||
regime = "crash"
|
|
||||||
elif atr_pct_price >= 0.008:
|
|
||||||
regime = "high_vol"
|
|
||||||
elif abs(cvd_fast_accel) > 0 and abs(cvd_fast) > 0 and abs(cvd_mid) > 0:
|
|
||||||
same_dir = (cvd_fast > 0 and cvd_mid > 0) or (cvd_fast < 0 and cvd_mid < 0)
|
|
||||||
if same_dir and abs(cvd_fast_accel) > 10:
|
|
||||||
regime = "trend"
|
|
||||||
|
|
||||||
if gate_vol_enabled and atr_pct_price < min_vol:
|
|
||||||
gate_block = f"low_vol({atr_pct_price:.4f}<{min_vol})"
|
|
||||||
|
|
||||||
# 门2:CVD 共振(方向门,可关闭)
|
|
||||||
no_direction = False
|
|
||||||
cvd_resonance = 0
|
|
||||||
if cvd_fast > 0 and cvd_mid > 0:
|
|
||||||
direction = "LONG"
|
|
||||||
cvd_resonance = 30
|
|
||||||
no_direction = False
|
|
||||||
elif cvd_fast < 0 and cvd_mid < 0:
|
|
||||||
direction = "SHORT"
|
|
||||||
cvd_resonance = 30
|
|
||||||
no_direction = False
|
|
||||||
else:
|
|
||||||
direction = "LONG" if cvd_fast > 0 else "SHORT"
|
|
||||||
cvd_resonance = 0
|
|
||||||
if gate_cvd_enabled:
|
|
||||||
no_direction = True
|
|
||||||
if not gate_block:
|
|
||||||
gate_block = "no_direction_consensus"
|
|
||||||
else:
|
|
||||||
no_direction = False
|
|
||||||
|
|
||||||
# per-strategy 方向限制:long/short 仅限制开仓方向,不影响评分与指标快照
|
|
||||||
if dir_cfg_raw == "long" and direction == "SHORT":
|
|
||||||
strategy_direction_allowed = False
|
|
||||||
elif dir_cfg_raw == "short" and direction == "LONG":
|
|
||||||
strategy_direction_allowed = False
|
|
||||||
else:
|
|
||||||
strategy_direction_allowed = True
|
|
||||||
|
|
||||||
# 门3:鲸鱼否决(BTC 用 whale_cvd_ratio,ALT 用大单对立,可关闭)
|
|
||||||
if gate_whale_enabled and not gate_block and not no_direction:
|
|
||||||
if state.symbol == "BTCUSDT":
|
|
||||||
whale_cvd = (
|
|
||||||
state.whale_cvd_ratio
|
|
||||||
if state._whale_trades
|
|
||||||
else to_float(state.market_indicators.get("tiered_cvd_whale")) or 0.0
|
|
||||||
)
|
|
||||||
if (direction == "LONG" and whale_cvd < -whale_flow_pct) or (
|
|
||||||
direction == "SHORT" and whale_cvd > whale_flow_pct
|
|
||||||
):
|
|
||||||
gate_block = f"whale_cvd_veto({whale_cvd:.3f})"
|
|
||||||
else:
|
|
||||||
whale_adverse = any(
|
|
||||||
(direction == "LONG" and lt[2] == 1 and lt[1] * price >= whale_usd)
|
|
||||||
or (direction == "SHORT" and lt[2] == 0 and lt[1] * price >= whale_usd)
|
|
||||||
for lt in state.recent_large_trades
|
|
||||||
)
|
|
||||||
whale_aligned = any(
|
|
||||||
(direction == "LONG" and lt[2] == 0 and lt[1] * price >= whale_usd)
|
|
||||||
or (direction == "SHORT" and lt[2] == 1 and lt[1] * price >= whale_usd)
|
|
||||||
for lt in state.recent_large_trades
|
|
||||||
)
|
|
||||||
if whale_adverse and not whale_aligned:
|
|
||||||
gate_block = f"whale_adverse(>${whale_usd/1000:.0f}k)"
|
|
||||||
|
|
||||||
# 门4:OBI 否决(实时 WS 优先,fallback DB,可关闭)
|
|
||||||
obi_raw = state.rt_obi if state.rt_obi != 0.0 else to_float(
|
|
||||||
state.market_indicators.get("obi_depth_10")
|
|
||||||
)
|
|
||||||
if gate_obi_enabled and not gate_block and not no_direction and obi_raw is not None:
|
|
||||||
if direction == "LONG" and obi_raw < -obi_veto:
|
|
||||||
gate_block = f"obi_veto({obi_raw:.3f}<-{obi_veto})"
|
|
||||||
elif direction == "SHORT" and obi_raw > obi_veto:
|
|
||||||
gate_block = f"obi_veto({obi_raw:.3f}>{obi_veto})"
|
|
||||||
|
|
||||||
# 门5:期现背离否决(实时 WS 优先,fallback DB,可关闭)
|
|
||||||
spot_perp_div = (
|
|
||||||
state.rt_spot_perp_div if state.rt_spot_perp_div != 0.0 else to_float(
|
|
||||||
state.market_indicators.get("spot_perp_divergence")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if gate_spd_enabled and not gate_block and not no_direction and spot_perp_div is not None:
|
|
||||||
if (direction == "LONG" and spot_perp_div < -spd_veto) or (
|
|
||||||
direction == "SHORT" and spot_perp_div > spd_veto
|
|
||||||
):
|
|
||||||
gate_block = f"spd_veto({spot_perp_div:.4f})"
|
|
||||||
|
|
||||||
gate_passed = gate_block is None
|
|
||||||
|
|
||||||
# ── Direction Layer(55 分,原始尺度)───────────────────────
|
|
||||||
has_adverse_p99 = any(
|
|
||||||
(direction == "LONG" and lt[2] == 1) or (direction == "SHORT" and lt[2] == 0)
|
|
||||||
for lt in state.recent_large_trades
|
|
||||||
)
|
|
||||||
has_aligned_p99 = any(
|
|
||||||
(direction == "LONG" and lt[2] == 0) or (direction == "SHORT" and lt[2] == 1)
|
|
||||||
for lt in state.recent_large_trades
|
|
||||||
)
|
|
||||||
p99_flow = 20 if has_aligned_p99 else (10 if not has_adverse_p99 else 0)
|
|
||||||
accel_bonus = 5 if (
|
|
||||||
(direction == "LONG" and cvd_fast_accel > 0) or
|
|
||||||
(direction == "SHORT" and cvd_fast_accel < 0)
|
|
||||||
) else 0
|
|
||||||
|
|
||||||
# v53_fast:accel 独立触发路径(不要求 cvd 双线同向)
|
|
||||||
is_fast = strategy_name.endswith("fast")
|
|
||||||
accel_independent_score = 0
|
|
||||||
if is_fast and not no_direction:
|
|
||||||
accel_cfg = strategy_cfg.get("accel_independent", {})
|
|
||||||
if accel_cfg.get("enabled", False):
|
|
||||||
accel_strong = (
|
|
||||||
(direction == "LONG" and cvd_fast_accel > 0 and has_aligned_p99)
|
|
||||||
or (direction == "SHORT" and cvd_fast_accel < 0 and has_aligned_p99)
|
|
||||||
)
|
|
||||||
if accel_strong:
|
|
||||||
accel_independent_score = int(
|
|
||||||
accel_cfg.get("min_direction_score", 35)
|
|
||||||
)
|
|
||||||
|
|
||||||
direction_score = max(
|
|
||||||
min(cvd_resonance + p99_flow + accel_bonus, 55),
|
|
||||||
accel_independent_score,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Crowding Layer(25 分,原始尺度)───────────────────────
|
|
||||||
long_short_ratio = to_float(state.market_indicators.get("long_short_ratio"))
|
|
||||||
if long_short_ratio is None:
|
|
||||||
ls_score = 7
|
|
||||||
elif (direction == "SHORT" and long_short_ratio > 2.0) or (
|
|
||||||
direction == "LONG" and long_short_ratio < 0.5
|
|
||||||
):
|
|
||||||
ls_score = 15
|
|
||||||
elif (direction == "SHORT" and long_short_ratio > 1.5) or (
|
|
||||||
direction == "LONG" and long_short_ratio < 0.7
|
|
||||||
):
|
|
||||||
ls_score = 10
|
|
||||||
elif (direction == "SHORT" and long_short_ratio > 1.0) or (
|
|
||||||
direction == "LONG" and long_short_ratio < 1.0
|
|
||||||
):
|
|
||||||
ls_score = 7
|
|
||||||
else:
|
|
||||||
ls_score = 0
|
|
||||||
|
|
||||||
top_trader_position = to_float(state.market_indicators.get("top_trader_position"))
|
|
||||||
if top_trader_position is None:
|
|
||||||
top_trader_score = 5
|
|
||||||
else:
|
|
||||||
if direction == "LONG":
|
|
||||||
top_trader_score = (
|
|
||||||
10
|
|
||||||
if top_trader_position >= 0.55
|
|
||||||
else (0 if top_trader_position <= 0.45 else 5)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
top_trader_score = (
|
|
||||||
10
|
|
||||||
if top_trader_position <= 0.45
|
|
||||||
else (0 if top_trader_position >= 0.55 else 5)
|
|
||||||
)
|
|
||||||
crowding_score = min(ls_score + top_trader_score, 25)
|
|
||||||
|
|
||||||
# ── Environment Layer(15 分,原始尺度)────────────────────
|
|
||||||
oi_base_score = round(environment_score_raw / 15 * 10)
|
|
||||||
|
|
||||||
obi_raw = state.rt_obi if state.rt_obi != 0.0 else to_float(
|
|
||||||
state.market_indicators.get("obi_depth_10")
|
|
||||||
)
|
|
||||||
obi_bonus = 0
|
|
||||||
if is_fast and obi_raw is not None:
|
|
||||||
obi_cfg = strategy_cfg.get("obi_scoring", {})
|
|
||||||
strong_thr = float(obi_cfg.get("strong_threshold", 0.30))
|
|
||||||
weak_thr = float(obi_cfg.get("weak_threshold", 0.15))
|
|
||||||
strong_sc = int(obi_cfg.get("strong_score", 5))
|
|
||||||
weak_sc = int(obi_cfg.get("weak_score", 3))
|
|
||||||
obi_aligned = (direction == "LONG" and obi_raw > 0) or (
|
|
||||||
direction == "SHORT" and obi_raw < 0
|
|
||||||
)
|
|
||||||
obi_abs = abs(obi_raw)
|
|
||||||
if obi_aligned:
|
|
||||||
if obi_abs >= strong_thr:
|
|
||||||
obi_bonus = strong_sc
|
|
||||||
elif obi_abs >= weak_thr:
|
|
||||||
obi_bonus = weak_sc
|
|
||||||
|
|
||||||
environment_score = (
|
|
||||||
min(oi_base_score + obi_bonus, 15)
|
|
||||||
if is_fast
|
|
||||||
else round(environment_score_raw / 15 * 15)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Auxiliary Layer(5 分,原始尺度)──────────────────────
|
|
||||||
coinbase_premium = to_float(state.market_indicators.get("coinbase_premium"))
|
|
||||||
if coinbase_premium is None:
|
|
||||||
aux_score = 2
|
|
||||||
elif (
|
|
||||||
(direction == "LONG" and coinbase_premium > 0.0005)
|
|
||||||
or (direction == "SHORT" and coinbase_premium < -0.0005)
|
|
||||||
):
|
|
||||||
aux_score = 5
|
|
||||||
elif abs(coinbase_premium) <= 0.0005:
|
|
||||||
aux_score = 2
|
|
||||||
else:
|
|
||||||
aux_score = 0
|
|
||||||
|
|
||||||
# ── 根据策略权重缩放四层分数(direction/env/aux/momentum)────
|
|
||||||
weights_cfg = strategy_cfg.get("weights") or {}
|
|
||||||
w_dir = float(weights_cfg.get("direction", 55))
|
|
||||||
w_env = float(weights_cfg.get("env", 25))
|
|
||||||
w_aux = float(weights_cfg.get("aux", 15))
|
|
||||||
w_mom = float(weights_cfg.get("momentum", 5))
|
|
||||||
|
|
||||||
total_w = w_dir + w_env + w_aux + w_mom
|
|
||||||
if total_w <= 0:
|
|
||||||
# Fallback 到默认 55/25/15/5
|
|
||||||
w_dir, w_env, w_aux, w_mom = 55.0, 25.0, 15.0, 5.0
|
|
||||||
total_w = 100.0
|
|
||||||
|
|
||||||
# 归一化到 100 分制
|
|
||||||
norm = 100.0 / total_w
|
|
||||||
w_dir_eff = (w_dir + w_mom) * norm # 动量权重并入方向层
|
|
||||||
w_env_eff = w_env * norm
|
|
||||||
w_aux_eff = w_aux * norm
|
|
||||||
|
|
||||||
# 原始最大值:direction 55 + crowding 25 = 80
|
|
||||||
DIR_RAW_MAX = 55.0
|
|
||||||
CROWD_RAW_MAX = 25.0
|
|
||||||
ENV_RAW_MAX = 15.0
|
|
||||||
AUX_RAW_MAX = 5.0
|
|
||||||
DIR_PLUS_CROWD_RAW_MAX = DIR_RAW_MAX + CROWD_RAW_MAX
|
|
||||||
|
|
||||||
# 把方向+拥挤总权重按 55:25 拆分
|
|
||||||
dir_max_scaled = w_dir_eff * (DIR_RAW_MAX / DIR_PLUS_CROWD_RAW_MAX)
|
|
||||||
crowd_max_scaled = w_dir_eff * (CROWD_RAW_MAX / DIR_PLUS_CROWD_RAW_MAX)
|
|
||||||
env_max_scaled = w_env_eff
|
|
||||||
aux_max_scaled = w_aux_eff
|
|
||||||
|
|
||||||
# 按原始分数比例缩放到新的权重上
|
|
||||||
def _scale(raw_score: float, raw_max: float, scaled_max: float) -> float:
|
|
||||||
if raw_max <= 0 or scaled_max <= 0:
|
|
||||||
return 0.0
|
|
||||||
return min(max(raw_score, 0) / raw_max * scaled_max, scaled_max)
|
|
||||||
|
|
||||||
direction_score_scaled = _scale(direction_score, DIR_RAW_MAX, dir_max_scaled)
|
|
||||||
crowding_score_scaled = _scale(crowding_score, CROWD_RAW_MAX, crowd_max_scaled)
|
|
||||||
environment_score_scaled = _scale(environment_score, ENV_RAW_MAX, env_max_scaled)
|
|
||||||
aux_score_scaled = _scale(aux_score, AUX_RAW_MAX, aux_max_scaled)
|
|
||||||
|
|
||||||
total_score = min(
|
|
||||||
direction_score_scaled
|
|
||||||
+ crowding_score_scaled
|
|
||||||
+ environment_score_scaled
|
|
||||||
+ aux_score_scaled,
|
|
||||||
100,
|
|
||||||
)
|
|
||||||
total_score = max(0, round(total_score, 1))
|
|
||||||
if not gate_passed:
|
|
||||||
total_score = 0
|
|
||||||
|
|
||||||
whale_cvd_display = (
|
|
||||||
state.whale_cvd_ratio
|
|
||||||
if state._whale_trades
|
|
||||||
else to_float(state.market_indicators.get("tiered_cvd_whale"))
|
|
||||||
) if state.symbol == "BTCUSDT" else None
|
|
||||||
|
|
||||||
result.update(
|
|
||||||
{
|
|
||||||
"score": total_score,
|
|
||||||
"direction": direction if (not no_direction and gate_passed) else None,
|
|
||||||
"atr_value": atr_value,
|
|
||||||
"cvd_fast_5m": cvd_fast if is_fast else None,
|
|
||||||
"factors": {
|
|
||||||
"track": "BTC" if state.symbol == "BTCUSDT" else "ALT",
|
|
||||||
"regime": regime,
|
|
||||||
"gate_passed": gate_passed,
|
|
||||||
"gate_block": gate_block,
|
|
||||||
"atr_pct_price": round(atr_pct_price, 5),
|
|
||||||
"obi_raw": obi_raw,
|
|
||||||
"spot_perp_div": spot_perp_div,
|
|
||||||
"whale_cvd_ratio": whale_cvd_display,
|
|
||||||
"direction": {
|
|
||||||
"score": round(direction_score_scaled, 2),
|
|
||||||
"max": round(dir_max_scaled, 2),
|
|
||||||
"raw_score": direction_score,
|
|
||||||
"raw_max": DIR_RAW_MAX,
|
|
||||||
"cvd_resonance": cvd_resonance,
|
|
||||||
"p99_flow": p99_flow,
|
|
||||||
"accel_bonus": accel_bonus,
|
|
||||||
},
|
|
||||||
"crowding": {
|
|
||||||
"score": round(crowding_score_scaled, 2),
|
|
||||||
"max": round(crowd_max_scaled, 2),
|
|
||||||
"raw_score": crowding_score,
|
|
||||||
"raw_max": CROWD_RAW_MAX,
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"score": round(environment_score_scaled, 2),
|
|
||||||
"max": round(env_max_scaled, 2),
|
|
||||||
"raw_score": environment_score,
|
|
||||||
"raw_max": ENV_RAW_MAX,
|
|
||||||
"oi_change": snap["oi_change"],
|
|
||||||
},
|
|
||||||
"auxiliary": {
|
|
||||||
"score": round(aux_score_scaled, 2),
|
|
||||||
"max": round(aux_max_scaled, 2),
|
|
||||||
"raw_score": aux_score,
|
|
||||||
"raw_max": AUX_RAW_MAX,
|
|
||||||
"coinbase_premium": coinbase_premium,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 赋值 tier/signal(和原逻辑一致)
|
|
||||||
if total_score >= strategy_threshold and gate_passed and strategy_direction_allowed:
|
|
||||||
result["signal"] = direction
|
|
||||||
# tier 简化:score >= flip_threshold → heavy;否则 standard
|
|
||||||
result["tier"] = "heavy" if total_score >= flip_threshold else "standard"
|
|
||||||
|
|
||||||
state.last_signal_ts[strategy_name] = now_ms
|
|
||||||
state.last_signal_dir[strategy_name] = direction
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _empty_result(strategy_name: str, snap: dict) -> dict:
|
|
||||||
"""返回空评分结果(symbol 不匹配 / 无信号时使用)"""
|
|
||||||
return {
|
|
||||||
"strategy": strategy_name,
|
|
||||||
"cvd_fast": snap["cvd_fast"],
|
|
||||||
"cvd_mid": snap["cvd_mid"],
|
|
||||||
"cvd_day": snap["cvd_day"],
|
|
||||||
"cvd_fast_slope": snap["cvd_fast_slope"],
|
|
||||||
"atr": snap["atr"],
|
|
||||||
"atr_value": snap.get("atr_value", snap["atr"]),
|
|
||||||
"atr_pct": snap["atr_pct"],
|
|
||||||
"vwap": snap["vwap"],
|
|
||||||
"price": snap["price"],
|
|
||||||
"p95": snap["p95"],
|
|
||||||
"p99": snap["p99"],
|
|
||||||
"signal": None,
|
|
||||||
"direction": None,
|
|
||||||
"score": 0,
|
|
||||||
"tier": None,
|
|
||||||
"factors": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_signal(
|
|
||||||
state: SymbolState,
|
|
||||||
now_ms: int,
|
|
||||||
strategy_cfg: Optional[dict] = None,
|
|
||||||
snapshot: Optional[dict] = None,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
统一评分入口:
|
|
||||||
- v53*/custom_* → evaluate_factory_strategy (V5.3/V5.4 策略工厂)
|
|
||||||
- 其他策略(v51_baseline/v52_8signals 等)→ 视为已下线,返回空结果并记录 warning
|
|
||||||
"""
|
|
||||||
strategy_cfg = strategy_cfg or {}
|
|
||||||
strategy_name = strategy_cfg.get("name", "v53")
|
|
||||||
|
|
||||||
# v53 / custom_* 策略:走统一 V5 工厂打分
|
|
||||||
if strategy_name.startswith("v53") or strategy_name.startswith("custom_"):
|
|
||||||
snap = snapshot or state.build_evaluation_snapshot(now_ms)
|
|
||||||
# 单币种策略:如 cfg.symbol 存在,仅在该 symbol 上有效
|
|
||||||
strategy_symbol = strategy_cfg.get("symbol")
|
|
||||||
if strategy_symbol and strategy_symbol != state.symbol:
|
|
||||||
return _empty_result(strategy_name, snap)
|
|
||||||
|
|
||||||
allowed_symbols = strategy_cfg.get("symbols", [])
|
|
||||||
if allowed_symbols and state.symbol not in allowed_symbols:
|
|
||||||
return _empty_result(strategy_name, snap)
|
|
||||||
# 直接复用工厂评分核心逻辑,并确保基础字段完整
|
|
||||||
result = evaluate_factory_strategy(state, now_ms, strategy_cfg, snap)
|
|
||||||
# 补充缺失的基础字段(以 snapshot 为准)
|
|
||||||
base = _empty_result(strategy_name, snap)
|
|
||||||
for k, v in base.items():
|
|
||||||
result.setdefault(k, v)
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 非 v53/custom_ 策略:视为已下线,返回空结果并记录 warning
|
|
||||||
snap = snapshot or state.build_evaluation_snapshot(now_ms)
|
|
||||||
logger.warning(
|
|
||||||
"[strategy_scoring] strategy '%s' 已下线 (仅支持 v53*/custom_*), 返回空结果",
|
|
||||||
strategy_name,
|
|
||||||
)
|
|
||||||
return _empty_result(strategy_name, snap)
|
|
||||||
|
|
||||||
36
docs/AB_TEST_CHECKLIST.md
Normal file
36
docs/AB_TEST_CHECKLIST.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# AB测试观测清单(2026-03-02 ~ 03-16)
|
||||||
|
|
||||||
|
## 冻结期规则
|
||||||
|
- 不改权重、不改阈值、不改评分逻辑
|
||||||
|
- 如需改动必须打新版本号并分段统计
|
||||||
|
- 单写入源(小周生产环境)
|
||||||
|
|
||||||
|
## 两周后评审项目
|
||||||
|
|
||||||
|
### 1. 确认层重复计分审计
|
||||||
|
- **问题**:方向层和确认层都用CVD_fast/CVD_mid,同源重复
|
||||||
|
- **审计方法**:统计确认层=15 vs 确认层=0时的胜率差异
|
||||||
|
- **如果差异不显著**:V5.3降权或重构为"CVD斜率加速+趋势强度"
|
||||||
|
|
||||||
|
### 2. 拥挤层 vs FR相关性
|
||||||
|
- **审计**:`corr(FR_score, crowd_score)`
|
||||||
|
- **如果>0.7**:说明重复表达,降一层权重
|
||||||
|
|
||||||
|
### 3. OI持续性审计
|
||||||
|
- **字段**:`oi_persist_n`(连续同向窗口数)— 目前未记录,需V5.3加
|
||||||
|
- **审计**:高分单里`oi_persist_n=1`的胜率是否显著差于`>=2`
|
||||||
|
- **如果差异明显**:升为正式门槛
|
||||||
|
|
||||||
|
### 4. 清算触发率审计(按币种)
|
||||||
|
- 各币种清算信号触发率
|
||||||
|
- 触发后净R分布
|
||||||
|
- 避免某币种几乎不触发/过度触发
|
||||||
|
|
||||||
|
### 5. config_hash落库(V5.3)
|
||||||
|
- 每笔强制落库:`strategy`, `strategy_version`, `config_hash`, `engine_instance`
|
||||||
|
- 报表按config_hash分组
|
||||||
|
|
||||||
|
## 数据目标
|
||||||
|
- V5.1:500+笔(当前282)
|
||||||
|
- V5.2:200+笔(当前12)
|
||||||
|
- 每策略每币种50+笔
|
||||||
@ -1,203 +0,0 @@
|
|||||||
# AI 使用手册(Arbitrage Engine)
|
|
||||||
|
|
||||||
> 面向 Codex / OpenClaw / 其他 AI 助手。
|
|
||||||
> 目标:让 AI 在本项目中 **安全、可预期、可回溯** 地修改代码,而不是“乱改一通”。
|
|
||||||
|
|
||||||
本项目的**唯一权威系统说明**是:`docs/arbitrage-engine-full-spec.md`。
|
|
||||||
本手册描述:AI 进入项目时的阅读顺序、允许/禁止修改的范围,以及每次改动必须遵守的流程。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 进入项目前必须阅读的内容
|
|
||||||
|
|
||||||
在对本项目做任何非文档类修改之前,AI 必须先做下面几件事:
|
|
||||||
|
|
||||||
1. 打开并理解 `docs/arbitrage-engine-full-spec.md` 中至少以下章节:
|
|
||||||
- 项目概述、技术栈
|
|
||||||
- 数据库结构(尤其是 `strategies` / `signal_indicators` / `paper_trades`)
|
|
||||||
- 信号引擎(signal_engine)逻辑
|
|
||||||
- paper trading 行为与 PnL 计算
|
|
||||||
2. 浏览以下后端文件的结构(无需逐行记住):
|
|
||||||
- `backend/main.py`(FastAPI 路由与业务分层)
|
|
||||||
- `backend/signal_engine.py`(信号引擎与策略工厂)
|
|
||||||
- `backend/paper_monitor.py`(模拟盘平仓逻辑)
|
|
||||||
3. 浏览前端入口结构:
|
|
||||||
- `frontend/app/` 下的页面目录(尤其是 `/strategy-plaza` / `/signals` / `/paper`)
|
|
||||||
|
|
||||||
如发现文档与代码实际行为有冲突,应**优先相信 full-spec**,并在必要时向人类维护者报告。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 修改范围:可以动什么 / 不可以动什么
|
|
||||||
|
|
||||||
为减少细节 bug 和隐性破坏,本项目对 AI 的修改范围划分为三类:
|
|
||||||
|
|
||||||
- `允许自由修改`:只要本地检查通过即可。
|
|
||||||
- `允许修改,但需要特别小心`:需要有清晰计划 + 局部验证。
|
|
||||||
- `禁止修改,除非任务明确要求且更新了 full-spec`。
|
|
||||||
|
|
||||||
下面按类型具体说明。
|
|
||||||
|
|
||||||
### 2.1 允许自由修改的内容(默认安全区)
|
|
||||||
|
|
||||||
这些改动一般不会破坏系统核心语义,可以在合理前提下自由进行:
|
|
||||||
|
|
||||||
1. **文档与注释**
|
|
||||||
- 新增或修改 Markdown 文档(除非明确标为“权威规范”,如 full-spec)。
|
|
||||||
- 在代码中添加/改进注释,不改变实际逻辑。
|
|
||||||
2. **测试代码**
|
|
||||||
- 新增/改进单元测试、集成测试、前端测试。
|
|
||||||
- 修复由于实现变化导致的测试用例不匹配(前提是确认测试的预期确实已变化)。
|
|
||||||
3. **前端 UI 层展示**
|
|
||||||
- 不改变 API 调用与数据结构的前提下:
|
|
||||||
- 布局调整、视觉优化、文案修改。
|
|
||||||
- 新增只读型组件(图表、表格、标签等)。
|
|
||||||
4. **纯新增的本地脚本与工具**
|
|
||||||
- 放在 `scripts/` 或明确的工具目录里,用于开发、调试、分析(不接入生产 PM2 流程)。
|
|
||||||
5. **无行为变更的小型重构**
|
|
||||||
- 拆分过长函数、提取私有辅助函数。
|
|
||||||
- 增加类型标注、引入更严格的 linters(在配置合理的前提下)。
|
|
||||||
|
|
||||||
### 2.2 可以修改但需要特别小心的内容(受控区)
|
|
||||||
|
|
||||||
这类改动一旦出错,会引入细节 bug 或统计偏差,但不至于破坏整个系统结构。修改前必须:
|
|
||||||
|
|
||||||
1. 在回答中给出**明确的改动计划**(改哪些文件、改变哪些行为)。
|
|
||||||
2. 改完后尽量做局部验证(至少跑相关命令 / 人工检查相关页面)。
|
|
||||||
|
|
||||||
受控区包括:
|
|
||||||
|
|
||||||
1. **现有 API 的实现细节**
|
|
||||||
- 位于 `backend/main.py` 中的 `/api/*` 路由实现。
|
|
||||||
- 允许:
|
|
||||||
- 优化 SQL 查询;
|
|
||||||
- 修复明显的边界条件 bug;
|
|
||||||
- 增加非破坏性字段(向响应 json 增加可选字段)。
|
|
||||||
- 不允许:
|
|
||||||
- 静默改变已有字段的含义(例如把 `score` 从 0–100 改成 0–1)。
|
|
||||||
- 在无任务说明的前提下移除现有字段。
|
|
||||||
2. **前端与现有 API 的交互逻辑**
|
|
||||||
- 包括 `frontend/app/*` 中对 `/api/*` 的调用与数据展示逻辑。
|
|
||||||
- 修改前需确认:
|
|
||||||
- 请求路径、查询参数、响应结构与 full-spec 和实际后端实现一致。
|
|
||||||
3. **信号引擎的非核心逻辑**
|
|
||||||
- `backend/signal_engine.py` 中:
|
|
||||||
- 日志输出格式;
|
|
||||||
- 错误处理与异常保护;
|
|
||||||
- 非打分核心的辅助逻辑(例如冷启动保护、调试输出等)。
|
|
||||||
- 修改“评分公式”“门控条件”的逻辑时,属于 **禁止区**(见 2.3)。
|
|
||||||
4. **paper trading 的展示与统计层**
|
|
||||||
- 后端统计接口(如 `/api/paper/stats`、`/api/paper/stats-by-strategy`)的实现细节;
|
|
||||||
- 前端 `/paper` 以及策略广场中与统计展示相关的逻辑。
|
|
||||||
- 改动时需确认:
|
|
||||||
- 不改变 `pnl_r` / `status` 的语义;
|
|
||||||
- 汇总方式与 full-spec 中定义的规则保持一致。
|
|
||||||
|
|
||||||
### 2.3 禁止修改的内容(红线区)
|
|
||||||
|
|
||||||
以下内容 **默认禁止 AI 修改**。
|
|
||||||
只有在任务说明/人类明确要求的情况下,才可以动,并且必须同步更新 `arbitrage-engine-full-spec.md` 中对应章节。
|
|
||||||
|
|
||||||
1. **数据库结构与关键字段语义**
|
|
||||||
- 表结构(尤其是):
|
|
||||||
- `strategies`
|
|
||||||
- `signal_indicators`
|
|
||||||
- `paper_trades`
|
|
||||||
- `agg_trades`
|
|
||||||
- `liquidations`
|
|
||||||
- `market_indicators`
|
|
||||||
- 禁止:
|
|
||||||
- 修改已有字段的含义;
|
|
||||||
- 删除字段;
|
|
||||||
- 改名字段;
|
|
||||||
- 在没有文档更新的前提下添加字段。
|
|
||||||
- 如确需变更,必须:
|
|
||||||
- 在任务文档中有清晰需求;
|
|
||||||
- 编写迁移 SQL 并在 full-spec 中更新结构说明。
|
|
||||||
2. **核心业务指标的定义与计算方式**
|
|
||||||
- 包括但不限于:
|
|
||||||
- `pnl_r` 的计算规则;
|
|
||||||
- `status` 枚举值含义(`tp` / `sl` / `sl_be` / `timeout` / `signal_flip` 等);
|
|
||||||
- 胜率、盈亏比、权益曲线的汇总算法;
|
|
||||||
- 信号评分各层(direction/environment/auxiliary/momentum)的含义与权重机制。
|
|
||||||
- 如果任务要求调整这些规则,必须:
|
|
||||||
- 明确标注新旧行为差异;
|
|
||||||
- 说明对历史数据/统计的影响;
|
|
||||||
- 更新 full-spec 中对应章节。
|
|
||||||
3. **信号引擎核心打分/开仓/平仓逻辑**
|
|
||||||
- `evaluate_factory_strategy`(V5.4 策略工厂核心)中的核心决策逻辑,包括:
|
|
||||||
- 门控系统(5 个 Gate)的通过/否决条件;
|
|
||||||
- 各层得分的具体计算公式;
|
|
||||||
- `entry_score` / `flip_threshold` 等核心阈值的语义;
|
|
||||||
- 开仓价/SL/TP 的确定方式;
|
|
||||||
- `paper_open_trade` / `paper_monitor` 中判定触发 TP1/TP2/SL/timeout 的条件。
|
|
||||||
- 除非任务明确指出“需要调整策略模型/打分方式”,否则 AI 不应主动更改这些部分。
|
|
||||||
4. **策略 ID / 名称约定与历史数据映射**
|
|
||||||
- `strategies.strategy_id`(UUID)与 `paper_trades.strategy_id`、`signal_indicators.strategy_id` 的关联关系;
|
|
||||||
- `strategy` 字段中已有的命名约定(例如 `custom_` 前缀)。
|
|
||||||
- 禁止:
|
|
||||||
- 修改已有策略的 `strategy_id`;
|
|
||||||
- 静默重命名已有策略的 `strategy` 文本值;
|
|
||||||
- 移除 `custom_` 策略路由或相关兼容逻辑。
|
|
||||||
5. **认证与安全配置**
|
|
||||||
- `auth.py` 中的认证逻辑和权限控制;
|
|
||||||
- JWT Secret、数据库连接信息、PM2 配置等运维层配置。
|
|
||||||
- AI 不应在代码中硬编码新的敏感信息,也不应修改现有的密钥配置。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 每次修改必须遵守的流程(AI 视角)
|
|
||||||
|
|
||||||
无论修改的是允许区还是受控区,AI 在本项目中的工作流程应遵循:
|
|
||||||
|
|
||||||
1. **确认任务范围**
|
|
||||||
- 从用户指令或任务文档中提取:
|
|
||||||
- 要实现/修复的目标;
|
|
||||||
- 涉及的页面/API/模块。
|
|
||||||
2. **查阅相关文档**
|
|
||||||
- 总是先查 `docs/arbitrage-engine-full-spec.md`;
|
|
||||||
- 如涉及信号引擎/策略工厂/模拟盘,需额外查阅对应的 GUIDE 文档(如果存在)。
|
|
||||||
3. **列出改动计划**
|
|
||||||
- 在回答中简要列出:
|
|
||||||
- 计划修改的文件;
|
|
||||||
- 每个文件预期的改动类型(bugfix/重构/新增功能)。
|
|
||||||
4. **实施改动(保持最小必要范围)**
|
|
||||||
- 避免“一次改太多文件”;
|
|
||||||
- 避免引入与任务无关的重构。
|
|
||||||
5. **本地验证**
|
|
||||||
- 尽量运行至少以下检查之一(视任务而定):
|
|
||||||
- 后端:相关单测 / 手工调用关键 API;
|
|
||||||
- 前端:`npm run build` 或启动本地开发服务器检查关键页面;
|
|
||||||
- 如无法真实运行,至少从代码结构和文档上做自洽性检查。
|
|
||||||
6. **更新文档(如必要)**
|
|
||||||
- 若改动影响 full-spec 中描述的行为/结构,必须同步更新 full-spec;
|
|
||||||
- 若改动属于策略/信号模型的重大变更,应在决策记录中添加条目(例如 `DECISIONS.md`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 冲突处理原则
|
|
||||||
|
|
||||||
当出现以下冲突时,AI 应遵循:
|
|
||||||
|
|
||||||
1. **代码 vs full-spec**
|
|
||||||
- 如果代码实现与 full-spec 的描述不一致:
|
|
||||||
- 默认以 full-spec 为“期望行为”;
|
|
||||||
- 优先考虑修代码使其符合 full-spec;
|
|
||||||
- 如判断 full-spec 已过时,应在回答中明确提出,并建议人类确认后更新文档。
|
|
||||||
2. **用户即时指令 vs 文档约束**
|
|
||||||
- 如果用户指令要求做的事情与本手册“禁止修改”部分冲突:
|
|
||||||
- AI 必须在回答中提醒用户这是“红线操作”;
|
|
||||||
- 只有在用户明确确认且任务需求足够清晰时,才执行,并同步修改 full-spec。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 总结
|
|
||||||
|
|
||||||
本手册的核心目的只有一个:
|
|
||||||
|
|
||||||
> 让本项目在 AI 长期参与维护的前提下,仍然保持:
|
|
||||||
> - 核心业务逻辑不被轻易破坏;
|
|
||||||
> - 历史数据和统计口径具有可比性;
|
|
||||||
> - 每一次变更都有据可查、可解释。
|
|
||||||
|
|
||||||
如果本手册与 `arbitrage-engine-full-spec.md` 或实际代码行为存在不一致,应优先保持三者的一致性,并在必要时征求人类维护者的确认。
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,144 +0,0 @@
|
|||||||
# Auto-Evolve 运行手册
|
|
||||||
|
|
||||||
## 1. 目标
|
|
||||||
|
|
||||||
本手册用于把策略工厂升级为“自动进化”流程:
|
|
||||||
|
|
||||||
1. 自动分析(每天)
|
|
||||||
2. 自动调整(每天每币最多新增 1 条 `codex优化-*`)
|
|
||||||
3. 自动复盘(输出报告)
|
|
||||||
4. 自动清理(超配时下线表现最差的 codex 策略)
|
|
||||||
|
|
||||||
> 注意:自动化默认仅在模拟盘执行。任何实盘动作必须人工确认。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 代码入口
|
|
||||||
|
|
||||||
- 自动进化脚本:`automation/auto_evolve/run_daily.py`
|
|
||||||
- 示例配置:`automation/auto_evolve/config.example.json`
|
|
||||||
- 报告输出目录:`reports/auto-evolve/YYYY-MM-DD/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 连接信息(快速)
|
|
||||||
|
|
||||||
完整信息见:`docs/OPS_CONNECTIONS.md`
|
|
||||||
|
|
||||||
### 3.1 服务器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gcloud compute ssh instance-20260221-064508 \
|
|
||||||
--zone asia-northeast1-b \
|
|
||||||
--project gen-lang-client-0835616737 \
|
|
||||||
--tunnel-through-iap
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 数据库(服务器本地)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -u fzq1228 bash -lc "export PGPASSWORD=arb_engine_2026; psql -h 127.0.0.1 -U arb -d arb_engine -c 'SELECT now();'"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 手工运行(建议先 dry-run)
|
|
||||||
|
|
||||||
### 4.0 前置修复:symbol 不一致脏数据清理(一次性)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 先看统计(不删除)
|
|
||||||
python3 scripts/fix_signal_symbol_mismatch.py
|
|
||||||
|
|
||||||
# 确认后执行删除
|
|
||||||
python3 scripts/fix_signal_symbol_mismatch.py --apply
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.1 本地或服务器 dry-run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/fzq1228/Projects/arbitrage-engine
|
|
||||||
python3 automation/auto_evolve/run_daily.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 实际执行(写入策略变更)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/fzq1228/Projects/arbitrage-engine
|
|
||||||
python3 automation/auto_evolve/run_daily.py --apply
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 指定配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 automation/auto_evolve/run_daily.py --config automation/auto_evolve/config.example.json --apply
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 自动策略逻辑(当前版本)
|
|
||||||
|
|
||||||
### 5.1 分析输入
|
|
||||||
|
|
||||||
- `strategies`(running 策略)
|
|
||||||
- `paper_trades`(仅统计 `paper_trades.symbol = strategies.symbol`)
|
|
||||||
- `signal_indicators`(仅统计 `signal_indicators.symbol = strategies.symbol`)
|
|
||||||
|
|
||||||
### 5.2 评分
|
|
||||||
|
|
||||||
- `fitness = 净R + 速度因子 + 稳定性因子 + PF因子 - 样本惩罚`
|
|
||||||
- 样本不足会被惩罚(避免小样本幻觉)
|
|
||||||
|
|
||||||
### 5.3 自动调整
|
|
||||||
|
|
||||||
- 按币种识别简化 regime:`trend/high_vol/crash/range`
|
|
||||||
- 每币种最多产出 1 条新策略(可配置)
|
|
||||||
- 复制父策略门控参数,重点微调:
|
|
||||||
- CVD 窗口
|
|
||||||
- 四层权重
|
|
||||||
- entry score
|
|
||||||
- TP/SL/timeout
|
|
||||||
|
|
||||||
### 5.4 自动下线
|
|
||||||
|
|
||||||
- 若某币种 `codex优化-*` 运行数超过阈值(默认 3)
|
|
||||||
- 自动下线最差且达到最小存活时间的策略(默认 24h)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 每日报告内容
|
|
||||||
|
|
||||||
脚本每次执行都会输出:
|
|
||||||
|
|
||||||
- Top3 / Bottom3(fitness)
|
|
||||||
- 每币种 regime
|
|
||||||
- 本次新建策略列表
|
|
||||||
- 本次下线策略列表
|
|
||||||
- 计划日志(候选生成来源)
|
|
||||||
|
|
||||||
文件:
|
|
||||||
|
|
||||||
- Markdown:`reports/auto-evolve/YYYY-MM-DD/HHMMSS_auto_evolve.md`
|
|
||||||
- JSON:`reports/auto-evolve/YYYY-MM-DD/HHMMSS_auto_evolve.json`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Codex 每日二次复盘(深度搜索)
|
|
||||||
|
|
||||||
建议每日在脚本执行后,由 Codex 自动执行第二轮复盘:
|
|
||||||
|
|
||||||
1. 读取当天 auto-evolve 报告
|
|
||||||
2. 联网做深度搜索(最新策略研究、订单流门控、风险控制)
|
|
||||||
3. 对照本项目现状判断可落地改进项
|
|
||||||
4. 若有明确提升收益的改动,触发 `work-to-down` 模式落地到代码
|
|
||||||
5. 生成复盘结论(改了什么、预期收益、风险)
|
|
||||||
|
|
||||||
> 这一步建议通过 Codex Automation 调度,不建议手工每天执行。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 安全边界
|
|
||||||
|
|
||||||
- 默认不修改历史交易,不回填旧数据。
|
|
||||||
- 自动化只操作策略参数与策略状态,不触碰用户认证/账密。
|
|
||||||
- 自动化失败时会回滚事务,不会留下半写入状态。
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
# Arbitrage Engine 后端运行说明(Backend Runtime Guide)
|
|
||||||
|
|
||||||
> 本文从“后端工程师 / 运维”的角度,描述项目的后端模块、进程拓扑和数据流。
|
|
||||||
> 详细字段与业务规则仍以 `docs/arbitrage-engine-full-spec.md` 为准。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 后端整体结构概览
|
|
||||||
|
|
||||||
后端主要由三类组件组成:
|
|
||||||
|
|
||||||
1. **FastAPI HTTP API(arb-api)**
|
|
||||||
- 文件:`backend/main.py`
|
|
||||||
- 职责:
|
|
||||||
- 提供 `/api/...` 下的所有 HTTP 接口;
|
|
||||||
- 负责认证(集成 `auth.py` 的路由);
|
|
||||||
- 对数据库执行查询和聚合逻辑,为前端和脚本服务;
|
|
||||||
- 启动少量后台任务(如资金费率快照)。
|
|
||||||
|
|
||||||
2. **信号与交易引擎进程**
|
|
||||||
- `backend/signal_engine.py`:信号引擎(策略评估 + 模拟盘开仓);
|
|
||||||
- `backend/paper_monitor.py`:模拟盘平仓(TP/SL/timeout/signal_flip);
|
|
||||||
- `backend/live_executor.py`(未来/暂定):连接实盘执行;
|
|
||||||
- `backend/risk_guard.py`:风控守护进程;
|
|
||||||
- `backend/position_sync.py`:实盘仓位同步。
|
|
||||||
|
|
||||||
3. **数据采集与维护进程**
|
|
||||||
- `backend/agg_trades_collector.py`:从币安 WebSocket 收集逐笔成交写入 `agg_trades`;
|
|
||||||
- `backend/market_data_collector.py`:收集资金费率、OI、多空比等写入 `market_indicators`;
|
|
||||||
- `backend/liquidation_collector.py`:收集爆仓数据写入 `liquidations`;
|
|
||||||
- `backend/backfill_agg_trades.py`:历史数据回补;
|
|
||||||
- `backend/fix_historical_pnl.py` 等维护脚本:用于修复历史统计。
|
|
||||||
|
|
||||||
这些组件通过 PostgreSQL 共享状态,FastAPI 作为对外唯一 HTTP 入口,信号/模拟盘引擎作为“后端大脑”,采集进程负责填充数据湖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. FastAPI API(arb-api)
|
|
||||||
|
|
||||||
入口文件:`backend/main.py`
|
|
||||||
运行方式:由 PM2 管理的 `arb-api` 进程(端口 4332)。
|
|
||||||
|
|
||||||
主要职责:
|
|
||||||
|
|
||||||
- 提供 `/api/...` REST 接口:
|
|
||||||
- 资金费率/历史数据(`/api/rates`、`/api/history`、`/api/kline`、`/api/snapshots`);
|
|
||||||
- 信号相关接口(`/api/signals/*`);
|
|
||||||
- 模拟盘接口(`/api/paper/*`);
|
|
||||||
- 策略管理接口(`/api/strategies*`、`/api/strategy-plaza*`);
|
|
||||||
- 实盘状态与控制接口(`/api/live/*`);
|
|
||||||
- 服务器监控接口(`/api/server/status`)。
|
|
||||||
- 初始化数据库连接:
|
|
||||||
- 在 `startup` 事件中调用 `init_schema()` 和 `ensure_auth_tables()`;
|
|
||||||
- 初始化 asyncpg 连接池(`get_async_pool()`)。
|
|
||||||
- 启动后台任务:
|
|
||||||
- `background_snapshot_loop()`:每 2 秒从币安拉资金费率 + 标记价,写入 `rate_snapshots`。
|
|
||||||
|
|
||||||
开发/调试提示:
|
|
||||||
|
|
||||||
- 本地如需仅调试 API 层,可以只启动 `main.py`(例如 `uvicorn main:app --reload`),连接已有 Cloud SQL;
|
|
||||||
- 复杂查询(如 stats / strategy-plaza)建议先查阅 `docs/API_CONTRACTS.md`,再看对应路由实现。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 信号与交易引擎进程
|
|
||||||
|
|
||||||
### 3.1 signal_engine.py — 信号引擎
|
|
||||||
|
|
||||||
文件:`backend/signal_engine.py`
|
|
||||||
PM2 名称:`signal-engine`
|
|
||||||
|
|
||||||
职责(结合 full-spec):
|
|
||||||
|
|
||||||
- 定时循环(约每 15 秒):
|
|
||||||
1. 从 `agg_trades` 滑动窗口计算 CVD、ATR、VWAP 等指标;
|
|
||||||
2. 从 `market_indicators` / `liquidations` 读取宏观指标和爆仓数据;
|
|
||||||
3. 对每个策略(`strategies` 表中 `status=running` 的记录)调用评估函数(V5.3 之前 `_evaluate_v53`,V5.4 起为 `evaluate_factory_strategy`);
|
|
||||||
4. 生成评分与信号(`score`、`signal`),写入 `signal_indicators`;
|
|
||||||
5. 在满足开仓条件时写入 `paper_trades`,通过 `paper_open_trade()` 等逻辑开模拟仓。
|
|
||||||
|
|
||||||
- 与前端/上层接口的关系:
|
|
||||||
- 前端 `/signals*` 页面只读 `signal_indicators` 和相关汇总接口,不直接调用 signal_engine;
|
|
||||||
- `/paper`、`/strategy-plaza` 通过 `paper_trades` 和 `strategies`/`signal_indicators` 组合呈现结果。
|
|
||||||
|
|
||||||
注意:
|
|
||||||
|
|
||||||
- `signal_engine.py` 是最复杂的单体文件之一,对其进行修改前建议:
|
|
||||||
- 阅读 `arbitrage-engine-full-spec.md` 中“信号引擎”章节;
|
|
||||||
- 阅读 `docs/AI_HANDBOOK.md` 中关于“禁止/谨慎修改”的约束;
|
|
||||||
- 在未来可以为其单独增加 `GUIDE_SIGNAL_ENGINE.md` 做更细粒度说明。
|
|
||||||
|
|
||||||
### 3.2 paper_monitor.py — 模拟盘平仓
|
|
||||||
|
|
||||||
文件:`backend/paper_monitor.py`
|
|
||||||
PM2 名称:`paper-monitor`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 通过币安 WebSocket 获取 mark price;
|
|
||||||
- 监控所有 `paper_trades` 中 `status="active"` 或 `tp1_hit` 的持仓;
|
|
||||||
- 根据价格触发规则:
|
|
||||||
- 触及 TP1 → 将 status 改为 `tp1_hit`,移动 SL 到保本价;
|
|
||||||
- 触及 TP2 → status=`tp`,计算 `pnl_r`;
|
|
||||||
- 触及 SL → status=`sl`,`pnl_r=-1`;
|
|
||||||
- 超时(持仓超过 timeout_minutes)→ status=`timeout`,按价差换算 R;
|
|
||||||
- 被反向信号强平 → status=`signal_flip`。
|
|
||||||
|
|
||||||
信号引擎负责开仓逻辑,paper_monitor 负责平仓逻辑,两者通过 `paper_trades` 协作。
|
|
||||||
|
|
||||||
### 3.3 其他进程(概要)
|
|
||||||
|
|
||||||
- `live_executor.py`:
|
|
||||||
- 负责将信号转换为实盘订单(当前可能处于试验/非默认启用状态);
|
|
||||||
- 与 `/api/live/*` 接口、`risk_guard.py` 共同控制实盘行为。
|
|
||||||
- `risk_guard.py`:
|
|
||||||
- 独立风控守护进程,通过检查日内 R、连续亏损、API 状态等判断是否触发熔断;
|
|
||||||
- 与 `/api/live/risk-status` 和 `/api/live/emergency-close` 等控制接口关联。
|
|
||||||
- `position_sync.py`:
|
|
||||||
- 定期从交易所拉取真实仓位,对齐本地 `live_positions` 记录;
|
|
||||||
- 为 `/api/live/reconciliation` 提供比对数据。
|
|
||||||
|
|
||||||
这些进程对模拟盘的影响有限,但对实盘 `/live` 页至关重要。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 数据采集与维护脚本
|
|
||||||
|
|
||||||
### 4.1 市场数据采集
|
|
||||||
|
|
||||||
- `agg_trades_collector.py`
|
|
||||||
- 订阅币安逐笔成交 WebSocket 流;
|
|
||||||
- 将成交写入 `agg_trades` 分区表;
|
|
||||||
- 为 CVD/ATR/VWAP 等指标计算提供原始成交数据。
|
|
||||||
|
|
||||||
- `market_data_collector.py`
|
|
||||||
- 定期从币安/其他来源拉取资金费率、未平仓合约(OI)、多空比、订单簿不平衡等;
|
|
||||||
- 写入 `market_indicators`;
|
|
||||||
- 供 signal_engine 的环境层/拥挤层/辅助层使用。
|
|
||||||
|
|
||||||
- `liquidation_collector.py`
|
|
||||||
- 拉取爆仓(强平)数据;
|
|
||||||
- 写入 `liquidations`;
|
|
||||||
- 供 V5.2/V5.3 的 liquidation 层打分使用。
|
|
||||||
|
|
||||||
### 4.2 历史数据与修复
|
|
||||||
|
|
||||||
- `backfill_agg_trades.py`
|
|
||||||
- 用于历史数据回补;
|
|
||||||
- 配合 `/api/server/status` 中 `backfill_running` 标记监控运行状态。
|
|
||||||
|
|
||||||
- `fix_historical_pnl.py` 等修复脚本:
|
|
||||||
- 针对旧数据的 PnL 计算错误或结构变更进行一次性修复;
|
|
||||||
- 修改前请查阅 full-spec 中对应章节,并记录在决策/变更文档中。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 后端数据流(服务视角)
|
|
||||||
|
|
||||||
从“服务/进程”的角度看,主数据流可简化为:
|
|
||||||
|
|
||||||
1. **行情写入**
|
|
||||||
- 币安 WebSocket → `agg_trades_collector.py` → `agg_trades`;
|
|
||||||
- 市场宏观指标 API → `market_data_collector.py` → `market_indicators`;
|
|
||||||
- 强平数据 → `liquidation_collector.py` → `liquidations`;
|
|
||||||
- 资金费率快照 → `main.py` 中后台任务 → `rate_snapshots`。
|
|
||||||
|
|
||||||
2. **信号生成**
|
|
||||||
- `signal_engine.py` 周期性读取:
|
|
||||||
- `agg_trades` 滑动窗口;
|
|
||||||
- `market_indicators`;
|
|
||||||
- `liquidations`;
|
|
||||||
- `strategies` 配置;
|
|
||||||
- 计算指标与评分 → 写入 `signal_indicators`;
|
|
||||||
- 满足条件时 → 写入 `paper_trades`(开仓记录)。
|
|
||||||
|
|
||||||
3. **模拟盘结算**
|
|
||||||
- `paper_monitor.py` 通过 WebSocket 获取 price;
|
|
||||||
- 根据 `paper_trades` 的 SL/TP/timeout 规则更新 `paper_trades.status` 和 `pnl_r`。
|
|
||||||
|
|
||||||
4. **API 展示与控制**
|
|
||||||
- `main.py` 通过 `/api/paper/*`、`/api/signals/*`、`/api/strategy-plaza*` 等接口对外提供:
|
|
||||||
- 信号历史和当前状态;
|
|
||||||
- 模拟盘持仓、交易历史、统计与权益曲线;
|
|
||||||
- 策略列表与管理操作。
|
|
||||||
- `/api/live/*` 接口与 `live_executor.py` / `risk_guard.py` 等进程协作控制实盘。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 本地开发与调试建议
|
|
||||||
|
|
||||||
1. **仅调试前端 + API(不跑引擎)**
|
|
||||||
- 使用现有 Cloud SQL 作为只读数据源:
|
|
||||||
- 启动 `backend/main.py`(uvicorn 或 PM2);
|
|
||||||
- 启动前端 `frontend`(Next.js dev 模式);
|
|
||||||
- 适用于:
|
|
||||||
- 调整页面布局与数据展示;
|
|
||||||
- 优化 `/api/...` 查询逻辑;
|
|
||||||
- 编写/验证新接口契约。
|
|
||||||
|
|
||||||
2. **调试信号引擎/模拟盘**
|
|
||||||
- 启动 `signal_engine.py` 和 `paper_monitor.py` 所需的最小进程集合;
|
|
||||||
- 确保 `agg_trades` / `market_indicators` 至少有一部分历史数据(可以从线上复制或运行简化版 collector);
|
|
||||||
- 使用日志和 `signal_indicators` / `paper_trades` 查询验证新策略。
|
|
||||||
|
|
||||||
3. **调试实盘页面 `/live`**
|
|
||||||
- 在本地/测试环境建议仅连接测试网账户;
|
|
||||||
- 启动相关进程:
|
|
||||||
- `live_executor.py`、`risk_guard.py`、`position_sync.py`(视具体实现和需求而定);
|
|
||||||
- 通过 `/api/live/*` 接口与 `/live` 页面观察状态变化,避免直接在生产环境实验未验证的逻辑。
|
|
||||||
|
|
||||||
4. **变更规则**
|
|
||||||
- 修改后端关键逻辑(尤其是 signal_engine、risk_guard)时,建议遵守:
|
|
||||||
- 先更新/确认 `arbitrage-engine-full-spec.md` 中对应章节;
|
|
||||||
- 再确保 `API_CONTRACTS.md` 与前端用法一致;
|
|
||||||
- 最后通过少量回测/模拟盘运行做 smoke test。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
本文件的目标是提供一个“后端模块地图”和“进程视角的数据流”,让未来的你(以及 AI 助手)在需要修改/排障时,不必反复从头翻 full-spec 和代码,就能快速知道该看哪几个模块、查哪几张表、重启哪些进程。
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# Codex 每日自动复盘 Prompt(模板)
|
|
||||||
|
|
||||||
> 供 Codex Automation 使用。目标:每天自动执行脚本 + 深度复盘 + 自动优化。
|
|
||||||
|
|
||||||
```
|
|
||||||
先执行 [$work-to-down](/Users/fanziqi/.codex/skills/work-to-down/SKILL.md)。
|
|
||||||
|
|
||||||
你在项目 /Users/fanziqi/codex/arbitrage-engine 工作。
|
|
||||||
|
|
||||||
按顺序完成:
|
|
||||||
1) 运行自动进化脚本(写库执行):
|
|
||||||
python3 automation/auto_evolve/run_daily.py --apply
|
|
||||||
2) 打开最新报告(reports/auto-evolve/YYYY-MM-DD/*_auto_evolve.md),总结:
|
|
||||||
- 最可能赚钱前三
|
|
||||||
- 最可能亏钱前三
|
|
||||||
- 每个币种当前 regime
|
|
||||||
3) 连接数据库交叉验证关键指标(running 策略、24h/7d netR、开仓转化率、active 持仓)。
|
|
||||||
4) 进行深度联网搜索(优先策略研究/风控/订单流相关的高质量一手资料),输出“可落地改进清单”。
|
|
||||||
5) 若有明确可提升项:
|
|
||||||
- 直接修改本仓库代码并自测;
|
|
||||||
- 优先改动 signal_engine / strategy_scoring / automation/auto_evolve;
|
|
||||||
- 保留风险边界(不破坏生产连接,不做危险删库操作)。
|
|
||||||
6) 生成当日复盘结论到 reports/auto-evolve/YYYY-MM-DD/HHMMSS_codex_review.md,内容包括:
|
|
||||||
- 今日问题
|
|
||||||
- 今日改进
|
|
||||||
- 预期收益提升点
|
|
||||||
- 风险与回滚建议
|
|
||||||
|
|
||||||
执行原则:做到为止,不要只给建议,能落地就直接落地。
|
|
||||||
```
|
|
||||||
379
docs/DETAILED_CODE_REVIEW.md
Normal file
379
docs/DETAILED_CODE_REVIEW.md
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
# 全面代码审阅报告
|
||||||
|
|
||||||
|
> 生成时间:2026-03-03
|
||||||
|
> 审阅范围:全部后端文件(15个)完整阅读
|
||||||
|
> 基于 commit `a17c143` 的代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
本报告基于对所有后端文件的逐行阅读。发现 **4个致命级别问题**(直接导致实盘无法运行)、**5个高危问题**(全新部署直接报错)、**4个安全漏洞**、**6个架构设计缺陷**。其中若干问题在前一份报告(PROBLEM_REPORT.md)中已提及,但本报告基于完整代码阅读,提供了更精确的定位和更全面的覆盖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 致命问题(实盘链路完全断裂)
|
||||||
|
|
||||||
|
### [F1] live_executor 永远读不到信号
|
||||||
|
|
||||||
|
**定位**:`live_executor.py:fetch_pending_signals()` + `signal_engine.py:save_indicator()`
|
||||||
|
|
||||||
|
**证据链**:
|
||||||
|
1. `signal_engine.py:690-706`:`save_indicator()` 用 `get_sync_conn()`(即 db.py 的 `PG_HOST=127.0.0.1`)将信号写入**本地 PG 的** `signal_indicators` 表
|
||||||
|
2. `live_executor.py:50-55`(已知):`DB_HOST` 默认 `10.106.0.3`(Cloud SQL)
|
||||||
|
3. `signal_engine.py:704-705`:`NOTIFY new_signal` 发送到**本地 PG**,live_executor 的 `LISTEN` 连在 Cloud SQL 上
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
|
||||||
|
| 动作 | 写入位置 |
|
||||||
|
|------|---------|
|
||||||
|
| signal_engine 写 `signal_indicators` | 本地 PG(127.0.0.1)|
|
||||||
|
| live_executor 的 LISTEN 监听 | Cloud SQL(10.106.0.3)|
|
||||||
|
| live_executor 的轮询查 `signal_indicators` | Cloud SQL(10.106.0.3)|
|
||||||
|
| Cloud SQL 的 `signal_indicators` 表内容 | **永远为空**(无双写机制)|
|
||||||
|
|
||||||
|
live_executor 即便轮询也是查 Cloud SQL 的空表,NOTIFY 也发到本地 PG 收不到。**只要实盘进程跑在不同数据库实例上,永远不会执行任何交易。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [F2] risk_guard 的数据新鲜度检查永远触发熔断
|
||||||
|
|
||||||
|
**定位**:`risk_guard.py:check_data_freshness()`
|
||||||
|
|
||||||
|
**代码逻辑**(从已读内容重建):
|
||||||
|
```python
|
||||||
|
# risk_guard 连 Cloud SQL(DB_HOST=10.106.0.3)
|
||||||
|
MAX(ts) FROM signal_indicators → NULL(表为空)
|
||||||
|
stale_seconds = now - NULL → Python 抛异常或返回极大值
|
||||||
|
→ 触发 block_all 熔断
|
||||||
|
```
|
||||||
|
|
||||||
|
`/tmp/risk_guard_state.json` 中 `block_all=true`,live_executor 执行前读此文件(Fail-Closed),**所有交易被直接拒绝**。
|
||||||
|
|
||||||
|
**叠加效果**:即使 F1 问题修复了(信号能传到 Cloud SQL),F2 也保证 live_executor 在下单前因 `block_all` 标志放弃执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [F3] risk_guard 与 live_executor 必须同机运行,但无任何保障
|
||||||
|
|
||||||
|
**定位**:`risk_guard.py`(写 `/tmp/risk_guard_state.json`)、`live_executor.py`(读同一路径)
|
||||||
|
|
||||||
|
**问题**:两个进程通过本地文件系统文件交换状态。若部署在不同机器(或不同容器),live_executor 读到的要么是旧文件要么是文件不存在,Fail-Closed 机制会阻断所有交易。目前无任何文档说明"两进程必须共机",无任何启动脚本检查,无任何报警。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [F4] signal_pusher.py 仍使用 SQLite,与 V5 PG 系统完全脱节
|
||||||
|
|
||||||
|
**定位**:`signal_pusher.py:1-20`
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlite3
|
||||||
|
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "arb.db")
|
||||||
|
SYMBOLS = ["BTCUSDT", "ETHUSDT"] # ← XRP/SOL 不在监控范围
|
||||||
|
```
|
||||||
|
|
||||||
|
**完整问题列表**:
|
||||||
|
1. 读 `arb.db`(SQLite),V5 信号全在 PG 的 `signal_indicators` 表,此脚本从不读取
|
||||||
|
2. 只覆盖 BTC/ETH,XRP/SOL 的信号永远不会被推送
|
||||||
|
3. **Discord Bot Token 硬编码**(详见 [S1])
|
||||||
|
4. 是一个一次性运行脚本,不是守护进程,PM2 管理无意义
|
||||||
|
5. 查询的 SQLite 表 `signal_logs` 在 V5 体系下已废弃
|
||||||
|
|
||||||
|
**结论**:signal_pusher.py 是遗留代码,从未迁移到 V5 PG 架构。如果 PM2 中运行的是此文件,通知系统完全失效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 高危问题(全新部署直接崩溃)
|
||||||
|
|
||||||
|
### [H1] `signal_indicators` 表缺少 `strategy` 和 `factors` 列
|
||||||
|
|
||||||
|
**定位**:`db.py:205-224`(建表 SQL)vs `signal_engine.py:695-701`(INSERT 语句)
|
||||||
|
|
||||||
|
**SCHEMA_SQL 中的列**:
|
||||||
|
`id, ts, symbol, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, vwap_30m, price, p95_qty, p99_qty, buy_vol_1m, sell_vol_1m, score, signal`
|
||||||
|
|
||||||
|
**save_indicator() 实际 INSERT 的列**:
|
||||||
|
`ts, symbol, **strategy**, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, vwap_30m, price, p95_qty, p99_qty, score, signal, **factors**`
|
||||||
|
|
||||||
|
多了 `strategy TEXT` 和 `factors JSONB` 两列。`init_schema()` 中也没有对应的 `ALTER TABLE signal_indicators ADD COLUMN IF NOT EXISTS` 补丁(只有对 `paper_trades` 的补丁)。
|
||||||
|
|
||||||
|
**后果**:全新环境 `init_schema()` 后,signal_engine 每次写入都报 `column "strategy" of relation "signal_indicators" does not exist`,主循环崩溃。
|
||||||
|
|
||||||
|
**补充**:`main.py:/api/signals/latest` 的查询也包含 `strategy` 和 `factors` 字段,全新部署 API 也会报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [H2] `paper_trades` 表缺少 `risk_distance` 列
|
||||||
|
|
||||||
|
**定位**:`db.py:286-305`(建表 SQL)vs `signal_engine.py:762-781`(INSERT 语句)
|
||||||
|
|
||||||
|
**SCHEMA_SQL 中的列**(无 `risk_distance`):
|
||||||
|
`id, symbol, direction, score, tier, entry_price, entry_ts, exit_price, exit_ts, tp1_price, tp2_price, sl_price, tp1_hit, status, pnl_r, atr_at_entry, score_factors, created_at`
|
||||||
|
|
||||||
|
`init_schema()` 用 `ALTER TABLE paper_trades ADD COLUMN IF NOT EXISTS strategy` 补了 `strategy` 列,但**没有补 `risk_distance`**。
|
||||||
|
|
||||||
|
`paper_open_trade()` 的 INSERT 包含 `risk_distance`,`paper_monitor.py:59` 和 `signal_engine.py:800` 也从 DB 读取 `risk_distance`。
|
||||||
|
|
||||||
|
**后果**:全新部署后,第一次模拟开仓就报 `column "risk_distance" does not exist`。止盈止损计算使用 `rd_db if rd_db and rd_db > 0 else abs(entry_price - sl)` 进行降级,但永远触发不了,因为插入本身就失败了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [H3] `users` 表双定义,`banned` 和 `discord_id` 字段在新环境缺失
|
||||||
|
|
||||||
|
**定位**:`db.py:269-276` vs `auth.py:28-37`
|
||||||
|
|
||||||
|
| 字段 | db.py SCHEMA_SQL | auth.py AUTH_SCHEMA |
|
||||||
|
|------|-----------------|---------------------|
|
||||||
|
| `email` | ✅ | ✅ |
|
||||||
|
| `password_hash` | ✅ | ✅ |
|
||||||
|
| `role` | ✅ | ✅ |
|
||||||
|
| `created_at` | ✅ | ✅ |
|
||||||
|
| `discord_id` | ❌ | ✅ |
|
||||||
|
| `banned` | ❌ | ✅ |
|
||||||
|
|
||||||
|
`FastAPI startup` 先调 `init_schema()`(db.py 版建表),再调 `ensure_auth_tables()`(auth.py 版),`CREATE TABLE IF NOT EXISTS` 第二次静默跳过。实际建的是旧版本,缺少 `discord_id` 和 `banned`。
|
||||||
|
|
||||||
|
**后果**:封禁用户功能在新部署上完全失效(`banned` 字段不存在)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [H4] `/api/kline` 只支持 BTC/ETH,XRP/SOL 静默返回错误数据
|
||||||
|
|
||||||
|
**定位**:`main.py:151-152`
|
||||||
|
|
||||||
|
```python
|
||||||
|
rate_col = "btc_rate" if symbol.upper() == "BTC" else "eth_rate"
|
||||||
|
price_col = "btc_price" if symbol.upper() == "BTC" else "eth_price"
|
||||||
|
```
|
||||||
|
|
||||||
|
XRP 和 SOL 请求均被路由到 ETH 的数据列。返回的是 ETH 的费率 K 线,但 symbol 标记为 XRP/SOL。前端图表展示完全错误。根本原因:`rate_snapshots` 表只有 `btc_rate` 和 `eth_rate` 两列,不支持 4 个币种的独立存储。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [H5] `subscriptions.py` 是孤立 SQLite 路由,定义了重名的 `/api/signals/history`
|
||||||
|
|
||||||
|
**定位**:`subscriptions.py:1-23`
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlite3
|
||||||
|
DB_PATH = "arb.db" # SQLite
|
||||||
|
|
||||||
|
@router.get("/api/signals/history") # ← 与 main.py 同名
|
||||||
|
def signals_history(): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**三个问题**:
|
||||||
|
1. 路由路径与 `main.py:221` 的 `@app.get("/api/signals/history")` 完全相同
|
||||||
|
2. 查询 SQLite `arb.db`,V5 体系已无此数据
|
||||||
|
3. `main.py` **从未** `include_router(subscriptions.router)`,所以目前是死代码
|
||||||
|
|
||||||
|
若将来有人误把 `subscriptions.router` 加进来,会与现有 PG 版本的同名路由冲突,FastAPI 会静默使用先注册的那个,导致难以排查的 bug。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 安全漏洞
|
||||||
|
|
||||||
|
### [S1] Discord Bot Token 硬编码在源代码(高危)
|
||||||
|
|
||||||
|
**定位**:`signal_pusher.py:~25`
|
||||||
|
|
||||||
|
```python
|
||||||
|
DISCORD_TOKEN = os.getenv("DISCORD_BOT_TOKEN", "MTQ3Mjk4NzY1NjczNTU1OTg0Mg.GgeYh5.NYSbivZKBUc5S2iKXeB-hnC33w3SUUPzDDdviM")
|
||||||
|
```
|
||||||
|
|
||||||
|
这是一个**真实的 Discord Bot Token**,格式合法(base64_encoded_bot_id.timestamp.signature)。任何有代码库读权限的人都可以用此 Token 以 bot 身份发消息、读频道历史、修改频道。
|
||||||
|
|
||||||
|
**立即行动**:在 Discord 开发者后台吊销此 Token 并重新生成,从代码中删除默认值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [S2] 数据库密码硬编码(三处)
|
||||||
|
|
||||||
|
**定位**:
|
||||||
|
- `db.py:19`:`os.getenv("PG_PASS", "arb_engine_2026")`
|
||||||
|
- `live_executor.py:44`:`os.getenv("DB_PASSWORD", "arb_engine_2026")`
|
||||||
|
- `risk_guard.py:42`:`os.getenv("DB_PASSWORD", "arb_engine_2026")`
|
||||||
|
|
||||||
|
三处使用同一个默认密码。代码一旦泄露,测试网数据库直接暴露。此外 `db.py:28` 还有 Cloud SQL 的默认密码:`os.getenv("CLOUD_PG_PASS", "arb_engine_2026")`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [S3] JWT Secret 有已知测试网默认值
|
||||||
|
|
||||||
|
**定位**:`auth.py`(推断行号约 15-20)
|
||||||
|
|
||||||
|
```python
|
||||||
|
_jwt_default = "arb-engine-jwt-secret-v2-2026" if _TRADE_ENV == "testnet" else None
|
||||||
|
```
|
||||||
|
|
||||||
|
若 `TRADE_ENV` 环境变量未设置(默认 `testnet`),JWT secret 使用此已知字符串。所有 JWT token 均可被任何知道此 secret 的人伪造,绕过身份验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [S4] CORS 配置暴露两个本地端口
|
||||||
|
|
||||||
|
**定位**:`main.py:16-20`
|
||||||
|
|
||||||
|
```python
|
||||||
|
allow_origins=["https://arb.zhouyangclaw.com", "http://localhost:3000", "http://localhost:3001"]
|
||||||
|
```
|
||||||
|
|
||||||
|
生产环境保留了 `localhost:3000` 和 `localhost:3001`。攻击者如果能在本地运行浏览器页面(e.g. XSS 注入到其他本地网站),可以绕过 CORS 跨域限制向 API 发请求。生产环境应移除 localhost origins。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 架构缺陷
|
||||||
|
|
||||||
|
### [A1] 策略 JSON 不支持热重载(与文档声称相反)
|
||||||
|
|
||||||
|
**定位**:`signal_engine.py:964-966`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
strategy_configs = load_strategy_configs() # ← 只在启动时调用一次!
|
||||||
|
...
|
||||||
|
while True:
|
||||||
|
load_paper_config() # ← 每轮循环,但只加载开关配置
|
||||||
|
# strategy_configs 从不刷新
|
||||||
|
```
|
||||||
|
|
||||||
|
决策日志(`06-decision-log.md`)声称策略 JSON 支持热修改无需重启,实际上 `strategy_configs` 变量只在 `main()` 开头赋值一次,主循环从不重新调用 `load_strategy_configs()`。
|
||||||
|
|
||||||
|
**修改 v51_baseline.json 或 v52_8signals.json 后必须重启 signal_engine。**
|
||||||
|
|
||||||
|
注:每 60 轮循环确实会 `load_paper_config()` 热加载"哪些策略启用"的开关,但权重/阈值/TP/SL 倍数不会热更新。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [A2] 三套数据库连接配置,极易迁移时漏改
|
||||||
|
|
||||||
|
| 进程 | 读取的环境变量 | 默认连接 |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `main.py`, `signal_engine.py`, `market_data_collector.py`, `agg_trades_collector.py`, `liquidation_collector.py`, `paper_monitor.py` | `PG_HOST`(db.py) | 127.0.0.1 |
|
||||||
|
| `live_executor.py`, `risk_guard.py`, `position_sync.py` | `DB_HOST` | 10.106.0.3 |
|
||||||
|
| `market_data_collector.py` 内部 | `PG_HOST` | 127.0.0.1 |
|
||||||
|
|
||||||
|
六个进程用 `PG_HOST`,三个进程用 `DB_HOST`,变量名不同,默认值不同,修改时需要同时更新两套 `.env`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [A3] market_indicators 和 liquidations 表不在主 schema 中
|
||||||
|
|
||||||
|
**定位**:`market_data_collector.py:ensure_table()`、`liquidation_collector.py:ensure_table()`
|
||||||
|
|
||||||
|
两张表由各自 collector 进程单独创建,不在 `db.py:SCHEMA_SQL` 里。启动顺序问题:
|
||||||
|
- 若 `signal_engine` 比 `market_data_collector` 先启动,查 `market_indicators` 报表不存在,所有市场指标评分降级为中间值
|
||||||
|
- 若 `signal_engine` 比 `liquidation_collector` 先启动,查 `liquidations` 报错,清算层评分归零
|
||||||
|
|
||||||
|
**补充发现**:`liquidation_collector.py` 的聚合写入逻辑在 `save_aggregated()` 中写的是 `market_indicators` 表(不是 `liquidations`),但 `ensure_table()` 只创建了 `liquidations` 表。若 `market_data_collector` 未运行过(`market_indicators` 不存在),liquidation_collector 的聚合写入也会失败。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [A4] paper_monitor 和 signal_engine 的止盈止损逻辑完全重复
|
||||||
|
|
||||||
|
**定位**:`signal_engine.py:788-878`(`paper_check_positions()`)、`paper_monitor.py:44-143`(`check_and_close()`)
|
||||||
|
|
||||||
|
两个函数逻辑几乎一模一样(均检查 TP1/TP2/SL/超时)。当前 signal_engine 主循环中注释说"持仓检查由 paper_monitor.py 实时处理",所以 `paper_check_positions()` 是**死函数**(定义了但从不调用)。
|
||||||
|
|
||||||
|
**风险**:未来如果有人修改止盈止损逻辑,只改了 paper_monitor.py 或只改了 signal_engine.py,两份代码就会产生不一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [A5] rate_snapshots 表只存 BTC/ETH,XRP/SOL 数据永久丢失
|
||||||
|
|
||||||
|
**定位**:`db.py:167-177`(建表)、`main.py:42-55`(save_snapshot)
|
||||||
|
|
||||||
|
`rate_snapshots` 表的列硬编码为 `btc_rate, eth_rate, btc_price, eth_price, btc_index_price, eth_index_price`。XRP/SOL 的资金费率数据只从 Binance 实时拉取,不存储,无法做历史分析或 K 线展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [A6] `/api/signals/history` 返回的是废弃表的数据
|
||||||
|
|
||||||
|
**定位**:`main.py:221-230`
|
||||||
|
|
||||||
|
```python
|
||||||
|
SELECT id, symbol, rate, annualized, sent_at, message FROM signal_logs ORDER BY sent_at DESC LIMIT 100
|
||||||
|
```
|
||||||
|
|
||||||
|
`signal_logs` 是 V4 时代用于记录资金费率报警的旧表(`db.py:259-267`),在 V5 体系下不再写入任何数据。这个端点对前端返回的是永久为空的结果,但没有任何错误信息,调用方无从判断是数据为空还是系统正常运行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 值得记录的正确设计
|
||||||
|
|
||||||
|
以下是审阅过程中发现的值得肯定的设计,供参考:
|
||||||
|
|
||||||
|
1. **`position_sync.py` 设计完整**:SL 丢失自动重挂、TP1 命中后 SL 移至保本、实际成交价查询、资金费用追踪(每8小时结算窗口),覆盖了实盘交易的主要边界情况。
|
||||||
|
|
||||||
|
2. **risk_guard Fail-Closed 模式正确**:`/tmp/risk_guard_state.json` 不存在时,live_executor 默认拒绝交易,而不是放行,安全方向正确。
|
||||||
|
|
||||||
|
3. **paper_monitor.py 使用 WebSocket 实时价格**:比 signal_engine 15 秒轮询更适合触发止盈止损,不会因为 15 秒间隔错过快速穿越的价格。
|
||||||
|
|
||||||
|
4. **agg_trades_collector.py 的数据完整性保障**:每 60 秒做连续性检查,断点处触发 REST 补录,每小时做完整性报告,设计周全。
|
||||||
|
|
||||||
|
5. **GCP Secret Manager 集成**:live_executor/risk_guard/position_sync 优先从 GCP Secret Manager 加载 API 密钥(`projects/gen-lang-client-0835616737/secrets/BINANCE_*`),生产环境密钥不在代码/环境变量中,安全设计得当。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 修复优先级清单
|
||||||
|
|
||||||
|
### 立即(防止实盘上线后资金损失)
|
||||||
|
|
||||||
|
| 编号 | 问题 | 修复方向 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **S1** | Discord Bot Token 泄露 | 立即在 Discord 开发者后台吊销并重新生成,代码中删除默认值 |
|
||||||
|
| **F1** | signal_engine 写本地 PG,live_executor 读 Cloud SQL,信号永远不传递 | 统一所有进程连接同一 PG 实例,或为 `signal_indicators` 表添加双写逻辑 |
|
||||||
|
| **F2** | risk_guard 查 Cloud SQL 空表永远触发熔断 | 与 F1 一起解决(统一 DB 连接) |
|
||||||
|
| **F3** | risk_guard/live_executor 必须共机无文档说明 | 在 PM2 配置和部署文档中明确说明;或改为 DB-based 状态通信 |
|
||||||
|
| **F4** | signal_pusher 是废弃 SQLite 脚本 | 从 PM2 配置中移除;按需重写成 PG 版本 |
|
||||||
|
|
||||||
|
### 本周(防止全新部署报错)
|
||||||
|
|
||||||
|
| 编号 | 问题 | 修复方向 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **H1** | `signal_indicators` 缺 `strategy`、`factors` 列 | 在 `SCHEMA_SQL` 中补列;在 `init_schema()` 中加 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` |
|
||||||
|
| **H2** | `paper_trades` 缺 `risk_distance` 列 | 同上,在 `init_schema()` 中补 ALTER |
|
||||||
|
| **H3** | `users` 表双定义,`banned`/`discord_id` 缺失 | 从 `SCHEMA_SQL` 删除 `users` 建表语句,统一由 `auth.py` 负责;加 ALTER 迁移旧环境 |
|
||||||
|
| **H4** | `/api/kline` XRP/SOL 返回 ETH 数据 | 要么限制 kline 只支持 BTC/ETH 并在 API 文档中注明;要么扩展 `rate_snapshots` 表结构 |
|
||||||
|
| **H5** | `subscriptions.py` 孤立 SQLite 代码 | 删除或移至 `archive/` 目录,防止将来误用 |
|
||||||
|
|
||||||
|
### 本月(安全加固)
|
||||||
|
|
||||||
|
| 编号 | 问题 | 修复方向 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **S2** | 数据库密码硬编码 | 移入 `.env` 文件,不进代码仓库;生产环境用 GCP Secret Manager |
|
||||||
|
| **S3** | JWT Secret 默认值可预测 | 生产部署强制要求 `JWT_SECRET` 环境变量,`_TRADE_ENV=production` 时 None 应直接启动失败 |
|
||||||
|
| **S4** | CORS 包含 localhost | 生产环境移除 localhost origins |
|
||||||
|
|
||||||
|
### 长期(架构改善)
|
||||||
|
|
||||||
|
| 编号 | 问题 | 修复方向 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **A1** | 策略 JSON 不支持热重载 | 在主循环中定期(如每 60 轮)重新调用 `load_strategy_configs()` |
|
||||||
|
| **A2** | 三套 DB 连接配置 | 统一用同一套环境变量(建议统一用 `PG_HOST`),所有进程都从 `db.py` 导入连接 |
|
||||||
|
| **A3** | market_indicators/liquidations 不在主 schema | 将两表定义移入 `SCHEMA_SQL` 或 `init_schema()` |
|
||||||
|
| **A4** | paper_check_positions 死代码 | 删除 signal_engine.py 中的 `paper_check_positions()` 函数(功能由 paper_monitor 承担) |
|
||||||
|
| **A6** | `/api/signals/history` 返回废弃表数据 | 重定向到查 `signal_indicators` 表,或废弃此端点 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:文件审阅覆盖情况
|
||||||
|
|
||||||
|
| 文件 | 行数 | 本次审阅 |
|
||||||
|
|------|-----|---------|
|
||||||
|
| `main.py` | ~500 | ✅ 全文 |
|
||||||
|
| `db.py` | ~415 | ✅ 全文 |
|
||||||
|
| `signal_engine.py` | ~1085 | ✅ 全文 |
|
||||||
|
| `live_executor.py` | ~708 | ✅ 全文 |
|
||||||
|
| `risk_guard.py` | ~644 | ✅ 全文 |
|
||||||
|
| `auth.py` | ~389 | ✅ 全文 |
|
||||||
|
| `position_sync.py` | ~687 | ✅ 全文 |
|
||||||
|
| `paper_monitor.py` | ~194 | ✅ 全文 |
|
||||||
|
| `agg_trades_collector.py` | ~400 | ✅ 全文 |
|
||||||
|
| `market_data_collector.py` | ~300 | ✅ 全文 |
|
||||||
|
| `liquidation_collector.py` | ~141 | ✅ 全文 |
|
||||||
|
| `signal_pusher.py` | ~100 | ✅ 全文 |
|
||||||
|
| `subscriptions.py` | ~24 | ✅ 全文 |
|
||||||
|
| `trade_config.py` | ~15 | ✅ 全文 |
|
||||||
|
| `backtest.py` | ~300 | 前100行 + 签名扫描 |
|
||||||
|
| `admin_cli.py` | ~100 | 签名扫描 |
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,144 +0,0 @@
|
|||||||
# 运维连接手册(本地 PostgreSQL + GCE)
|
|
||||||
|
|
||||||
本手册记录项目线上关键连接信息:
|
|
||||||
- 怎么连线上服务器;
|
|
||||||
- 怎么连服务器本地数据库(当前生产数据源);
|
|
||||||
- 线上项目目录和常用排查命令。
|
|
||||||
|
|
||||||
## 0. 固定信息
|
|
||||||
|
|
||||||
- GCP Project ID: `gen-lang-client-0835616737`
|
|
||||||
- 线上 VM: `instance-20260221-064508`
|
|
||||||
- Zone: `asia-northeast1-b`
|
|
||||||
- 线上项目目录: `/home/fzq1228/Projects/arbitrage-engine`
|
|
||||||
- 线上数据库(服务器本地 PostgreSQL)
|
|
||||||
- Host: `127.0.0.1`
|
|
||||||
- Port: `5432`
|
|
||||||
- DB: `arb_engine`
|
|
||||||
- User: `arb`
|
|
||||||
- Password: `arb_engine_2026`
|
|
||||||
|
|
||||||
## 1. 连接线上服务器(GCE)
|
|
||||||
|
|
||||||
### 1.1 先确认账号和项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gcloud auth list --filter=status:ACTIVE --format='value(account)'
|
|
||||||
gcloud config get-value project
|
|
||||||
```
|
|
||||||
|
|
||||||
如需切项目:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gcloud config set project gen-lang-client-0835616737
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 SSH(推荐 IAP)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gcloud compute ssh instance-20260221-064508 \
|
|
||||||
--zone asia-northeast1-b \
|
|
||||||
--project gen-lang-client-0835616737 \
|
|
||||||
--tunnel-through-iap
|
|
||||||
```
|
|
||||||
|
|
||||||
快速连通性测试:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gcloud compute ssh instance-20260221-064508 \
|
|
||||||
--zone asia-northeast1-b \
|
|
||||||
--project gen-lang-client-0835616737 \
|
|
||||||
--tunnel-through-iap \
|
|
||||||
--command "hostname; whoami; uptime; pwd"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 连接服务器本地数据库(当前生产)
|
|
||||||
|
|
||||||
### 2.1 在服务器内直接 psql
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -u fzq1228 bash -lc "export PGPASSWORD=arb_engine_2026; psql -h 127.0.0.1 -U arb -d arb_engine -c 'SELECT now();'"
|
|
||||||
```
|
|
||||||
|
|
||||||
进入交互式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -u fzq1228 bash -lc "export PGPASSWORD=arb_engine_2026; psql -h 127.0.0.1 -U arb -d arb_engine"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 从本机临时连(SSH Tunnel)
|
|
||||||
|
|
||||||
如果需要在本机 SQL 客户端查看:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 本机开隧道
|
|
||||||
gcloud compute ssh instance-20260221-064508 \
|
|
||||||
--zone asia-northeast1-b \
|
|
||||||
--project gen-lang-client-0835616737 \
|
|
||||||
--tunnel-through-iap \
|
|
||||||
-- -N -L 9432:127.0.0.1:5432
|
|
||||||
```
|
|
||||||
|
|
||||||
新开终端连本地映射端口:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PGPASSWORD=arb_engine_2026 psql -h 127.0.0.1 -p 9432 -U arb -d arb_engine -c "SELECT 1;"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 线上项目目录与运行状态
|
|
||||||
|
|
||||||
- Repo root: `/home/fzq1228/Projects/arbitrage-engine`
|
|
||||||
- Backend: `/home/fzq1228/Projects/arbitrage-engine/backend`
|
|
||||||
- Frontend: `/home/fzq1228/Projects/arbitrage-engine/frontend`
|
|
||||||
- 运行用户:`fzq1228`
|
|
||||||
- 进程管理:`pm2`
|
|
||||||
|
|
||||||
常用检查:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/fzq1228/Projects/arbitrage-engine
|
|
||||||
pm2 list
|
|
||||||
pm2 logs --lines 100
|
|
||||||
```
|
|
||||||
|
|
||||||
重点进程:
|
|
||||||
- `signal-engine`
|
|
||||||
- `paper-monitor`
|
|
||||||
- `arb-api`
|
|
||||||
- `agg-collector`
|
|
||||||
- `market-collector`
|
|
||||||
- `liq-collector`
|
|
||||||
- `position-sync`
|
|
||||||
- `risk-guard`
|
|
||||||
|
|
||||||
## 4. 版本同步
|
|
||||||
|
|
||||||
查看服务器分支和提交:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo git -c safe.directory=/home/fzq1228/Projects/arbitrage-engine \
|
|
||||||
-C /home/fzq1228/Projects/arbitrage-engine rev-parse --abbrev-ref HEAD
|
|
||||||
sudo git -c safe.directory=/home/fzq1228/Projects/arbitrage-engine \
|
|
||||||
-C /home/fzq1228/Projects/arbitrage-engine log --oneline -1
|
|
||||||
```
|
|
||||||
|
|
||||||
服务器拉代码:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/fzq1228/Projects/arbitrage-engine
|
|
||||||
git pull origin codex/codex_dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 常见故障排查
|
|
||||||
|
|
||||||
- API 正常但策略不出信号:
|
|
||||||
- 看 `signal-engine` 日志;
|
|
||||||
- 检查 `strategies.status='running'`;
|
|
||||||
- 检查 `signal_indicators` 最近 5 分钟是否持续写入。
|
|
||||||
- 有信号但不开仓:
|
|
||||||
- 查 `paper_trades` 是否达到 `max_positions`;
|
|
||||||
- 查 `flip_threshold/entry_score` 是否过高;
|
|
||||||
- 查 `paper_monitor` 和 `signal_engine` 日志是否有拒绝原因。
|
|
||||||
- 数据断流:
|
|
||||||
- 检查 `agg_trades` 最新 `time_ms`;
|
|
||||||
- 检查 `agg-collector`、`market-collector`、`liq-collector` 进程。
|
|
||||||
145
docs/PROBLEM_REPORT.md
Normal file
145
docs/PROBLEM_REPORT.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# 项目问题报告
|
||||||
|
|
||||||
|
> 生成时间:2026-03-03
|
||||||
|
> 基于 commit `0d9dffa` 的代码分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 高危问题(可能导致实盘出错)
|
||||||
|
|
||||||
|
### P1:数据库从未真正迁移到云端
|
||||||
|
|
||||||
|
**现象**:你以为整个系统已经跑在 Cloud SQL 上,实际上只有 `agg_trades` 原始成交数据在双写。其他核心数据全在**本地 PG(127.0.0.1)**。
|
||||||
|
|
||||||
|
| 数据表 | 本地 PG | Cloud SQL |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| `agg_trades`(原始成交) | ✅ | ✅ 双写 |
|
||||||
|
| `signal_indicators`(信号输出) | ✅ | ❌ 没有 |
|
||||||
|
| `paper_trades`(模拟盘) | ✅ | ❌ 没有 |
|
||||||
|
| `rate_snapshots`(费率快照) | ✅ | ❌ 没有 |
|
||||||
|
| `market_indicators`(市场数据) | ✅ | ❌ 没有 |
|
||||||
|
| `live_trades`(实盘交易) | ❌ | ✅ 只在云端 |
|
||||||
|
| `live_config` / `live_events` | ❌ | ✅ 只在云端 |
|
||||||
|
|
||||||
|
**最致命的问题**:`live_executor.py` 和 `risk_guard.py` 默认连 Cloud SQL(`DB_HOST=10.106.0.3`),但 `signal_engine.py` 只把信号写到本地 PG。这意味着:
|
||||||
|
- 实盘执行器读取的 `signal_indicators` 表在 Cloud SQL 里**可能是空的**
|
||||||
|
- 风控模块监控的 `live_trades` 和信号引擎写的数据完全在两个不同的数据库里
|
||||||
|
|
||||||
|
**影响**:实盘交易链路存在断裂风险,需立即核查服务器上各进程实际连接的数据库地址。
|
||||||
|
|
||||||
|
**修复方向**:统一所有进程连接到 Cloud SQL,或统一连接到本地 PG(通过 Cloud SQL Auth Proxy)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2:`users` 表双定义字段不一致
|
||||||
|
|
||||||
|
`db.py` 和 `auth.py` 各自定义了一个 `users` 表,字段不同:
|
||||||
|
|
||||||
|
| 字段 | db.py 版本 | auth.py 版本 |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| `email` | ✅ | ✅ |
|
||||||
|
| `password_hash` | ✅ | ✅ |
|
||||||
|
| `role` | ✅ | ✅ |
|
||||||
|
| `created_at` | ✅ | ✅ |
|
||||||
|
| `discord_id` | ❌ | ✅ |
|
||||||
|
| `banned` | ❌ | ✅ |
|
||||||
|
|
||||||
|
FastAPI 启动时先跑 `init_schema()`(db.py 版),再跑 `ensure_auth_tables()`(auth.py 版),因为 `CREATE TABLE IF NOT EXISTS` 第一次成功后就不再执行,**实际创建的是缺少 `discord_id` 和 `banned` 字段的旧版本**。
|
||||||
|
|
||||||
|
**影响**:封禁用户功能(`banned` 字段)在新装环境下可能失效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P3:`signal_indicators` 表 INSERT 包含 `strategy` 字段但 schema 没有
|
||||||
|
|
||||||
|
`save_indicator()` 函数向 `signal_indicators` 插入数据时包含 `strategy` 字段(`signal_engine.py:697`),但 `SCHEMA_SQL` 里的建表语句没有这个字段(`db.py:205-224`)。
|
||||||
|
|
||||||
|
**影响**:在全新环境初始化后,信号引擎写入会报列不存在的错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 中危问题(影响稳定性和维护)
|
||||||
|
|
||||||
|
### P4:`requirements.txt` 严重不完整
|
||||||
|
|
||||||
|
文件只列了 5 个包,实际运行还需要:
|
||||||
|
|
||||||
|
| 缺失依赖 | 用于 |
|
||||||
|
|---------|------|
|
||||||
|
| `asyncpg` | FastAPI 异步数据库 |
|
||||||
|
| `psycopg2-binary` | 同步数据库(signal_engine 等) |
|
||||||
|
| `aiohttp` | live_executor、risk_guard |
|
||||||
|
| `websockets` 或 `httpx` | agg_trades_collector WS 连接 |
|
||||||
|
| `psutil` | 已在文件里,但版本未锁定 |
|
||||||
|
|
||||||
|
**影响**:新机器部署直接失败。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5:`market_indicators` 和 `liquidations` 表不在主 schema 中
|
||||||
|
|
||||||
|
这两张表由各自的 collector 进程单独创建,不在 `init_schema()` 里。如果 collector 没跑过,signal_engine 查这两张表时会报错(会降级为默认中间分,不会崩溃,但数据不准)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P6:没有 CI/CD,没有自动化测试
|
||||||
|
|
||||||
|
- 代码变更完全靠人工验证
|
||||||
|
- 策略逻辑(`evaluate_signal`)没有任何单元测试,重构风险极高
|
||||||
|
- 部署流程:手动 ssh + git pull + pm2 restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 安全风险
|
||||||
|
|
||||||
|
### P7:测试网密码硬编码在源代码里
|
||||||
|
|
||||||
|
三个文件里都有:
|
||||||
|
```python
|
||||||
|
os.getenv("PG_PASS", "arb_engine_2026") # db.py:19
|
||||||
|
os.getenv("DB_PASSWORD", "arb_engine_2026") # live_executor.py:44
|
||||||
|
os.getenv("DB_PASSWORD", "arb_engine_2026") # risk_guard.py:42
|
||||||
|
```
|
||||||
|
|
||||||
|
代码一旦泄露(GitHub public、截图等),测试网数据库直接裸奔。
|
||||||
|
|
||||||
|
### P8:JWT Secret 有测试网默认值
|
||||||
|
|
||||||
|
```python
|
||||||
|
_jwt_default = "arb-engine-jwt-secret-v2-2026" if _TRADE_ENV == "testnet" else None
|
||||||
|
```
|
||||||
|
|
||||||
|
如果生产环境 `TRADE_ENV` 没有正确设置,会静默使用这个已知 secret,所有 JWT 都可伪造。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔵 架构债务(长期)
|
||||||
|
|
||||||
|
### P9:三套数据库连接配置并存,极易混淆
|
||||||
|
|
||||||
|
| 配置方式 | 使用的进程 | 默认连哪 |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| `db.py` 的 `PG_HOST` | main.py、signal_engine、collectors | `127.0.0.1`(本地) |
|
||||||
|
| 进程内 `DB_HOST` | live_executor、risk_guard、position_sync | `10.106.0.3`(Cloud SQL) |
|
||||||
|
| `market_data_collector.py` 内 `PG_HOST` | market_data_collector | `127.0.0.1`(本地) |
|
||||||
|
|
||||||
|
没有统一的连接配置入口,每个进程各自读各自的环境变量,迁移时极容易漏改。
|
||||||
|
|
||||||
|
### P10:前端轮询压力
|
||||||
|
|
||||||
|
`/api/rates` 每 2 秒轮询一次,用户多了服务器压力线性增长。目前 3 秒缓存有一定缓冲,但没有限流保护。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 建议优先级
|
||||||
|
|
||||||
|
| 优先级 | 任务 |
|
||||||
|
|-------|------|
|
||||||
|
| 🔴 立即 | 登服务器确认各进程实际连的数据库地址,核查实盘链路是否完整 |
|
||||||
|
| 🔴 立即 | 补全 `signal_indicators` 表的 `strategy` 字段 |
|
||||||
|
| 🔴 本周 | 统一数据库连接配置,所有进程用同一套环境变量 |
|
||||||
|
| 🟡 本周 | 修复 `users` 表双定义问题,合并到 auth.py 版本 |
|
||||||
|
| 🟡 本周 | 补全 `requirements.txt` |
|
||||||
|
| 🟠 本月 | 把硬编码密码移到 `.env` 文件,不进代码仓库 |
|
||||||
|
| 🔵 长期 | 添加 signal_engine 核心逻辑的单元测试 |
|
||||||
|
| 🔵 长期 | 配置 GitHub Actions 做基础 lint 和安全扫描 |
|
||||||
425
docs/PROJECT.md
Normal file
425
docs/PROJECT.md
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
# Arbitrage Engine V5.1 — 项目文档
|
||||||
|
|
||||||
|
> 最后更新:2026-03-01
|
||||||
|
> 版本:V5.1
|
||||||
|
> 作者:琪智科技
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
**Arbitrage Engine** 是一套加密货币合约交易信号系统,基于多因子评分模型,实时分析市场微观结构数据(Order Flow),生成做多/做空信号并执行模拟盘交易。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
- **5层100分评分体系**:方向层(45) + 拥挤层(20) + 环境层(15) + 确认层(15) + 辅助层(5)
|
||||||
|
- **4个交易币种**:BTCUSDT、ETHUSDT、XRPUSDT、SOLUSDT
|
||||||
|
- **8个信号源**(6个已接入评分,2个在采集中)
|
||||||
|
- **模拟盘自动交易**:信号触发 → 开仓 → TP/SL → 平仓,全自动
|
||||||
|
- **实时数据采集**:aggTrades、市场指标、清算数据
|
||||||
|
- **Web仪表盘**:信号展示、模拟盘监控、收益统计
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
| 层级 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 后端 | Python 3 + FastAPI + uvicorn |
|
||||||
|
| 前端 | Next.js 15 + React + TypeScript + Tailwind CSS |
|
||||||
|
| 数据库 | PostgreSQL 18(GCP Cloud SQL) |
|
||||||
|
| 进程管理 | PM2 |
|
||||||
|
| 数据源 | Binance Futures API + WebSocket |
|
||||||
|
| 反向代理 | Caddy |
|
||||||
|
|
||||||
|
### 代码统计
|
||||||
|
| 模块 | 文件数 | 代码行数 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| Backend (Python) | 15个 | 4,598行 |
|
||||||
|
| Frontend (TSX/TS) | 21个 | 3,518行 |
|
||||||
|
| **总计** | **36个** | **8,116行** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
arbitrage-engine/
|
||||||
|
├── backend/ # Python后端
|
||||||
|
│ ├── main.py # FastAPI主入口 + 全部API路由
|
||||||
|
│ ├── db.py # 数据库连接层(PG同步+异步池+Cloud SQL双写)
|
||||||
|
│ ├── auth.py # 用户认证系统(JWT + 邀请码注册)
|
||||||
|
│ ├── signal_engine.py # 🔥 核心:信号评估引擎(5层评分)
|
||||||
|
│ ├── paper_monitor.py # 模拟盘监控(WebSocket实时TP/SL)
|
||||||
|
│ ├── agg_trades_collector.py # aggTrades实时采集(Binance WS)
|
||||||
|
│ ├── market_data_collector.py# 市场指标采集(多空比/OI/FR等)
|
||||||
|
│ ├── liquidation_collector.py# 清算数据采集(Binance forceOrder)
|
||||||
|
│ ├── backtest.py # 回测框架
|
||||||
|
│ ├── backfill_agg_trades.py # aggTrades历史回补工具
|
||||||
|
│ ├── admin_cli.py # 管理命令行工具
|
||||||
|
│ ├── signal_pusher.py # [已废弃] 旧版信号推送
|
||||||
|
│ ├── subscriptions.py # [预留] 订阅系统
|
||||||
|
│ ├── migrate_sqlite_to_pg.py # [一次性] SQLite→PG数据迁移
|
||||||
|
│ └── migrate_auth_sqlite_to_pg.py # [一次性] Auth SQLite→PG迁移
|
||||||
|
├── frontend/ # Next.js前端
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── page.tsx # 首页(仪表盘概览)
|
||||||
|
│ │ ├── layout.tsx # 全局布局
|
||||||
|
│ │ ├── signals/page.tsx # 信号引擎页面(5层评分展示)
|
||||||
|
│ │ ├── paper/page.tsx # 模拟盘页面(持仓+历史+统计)
|
||||||
|
│ │ ├── trades/page.tsx # 历史交易页面
|
||||||
|
│ │ ├── server/page.tsx # 服务器监控页面
|
||||||
|
│ │ ├── kline/page.tsx # K线图页面
|
||||||
|
│ │ ├── live/page.tsx # 实时数据页面
|
||||||
|
│ │ ├── dashboard/page.tsx # 仪表盘页面
|
||||||
|
│ │ ├── login/page.tsx # 登录页面
|
||||||
|
│ │ ├── register/page.tsx # 注册页面(需邀请码)
|
||||||
|
│ │ ├── about/page.tsx # 关于页面
|
||||||
|
│ │ └── history/page.tsx # [占位] 历史页面
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Sidebar.tsx # 侧边导航栏
|
||||||
|
│ │ ├── Navbar.tsx # 顶部导航栏
|
||||||
|
│ │ ├── AuthHeader.tsx # 认证头部
|
||||||
|
│ │ ├── LiveTradesCard.tsx # 实时交易卡片
|
||||||
|
│ │ ├── FundingChart.tsx # 资金费率图表
|
||||||
|
│ │ ├── RateCard.tsx # 费率卡片
|
||||||
|
│ │ └── StatsCard.tsx # 统计卡片
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── auth.tsx # 前端认证逻辑(JWT管理)
|
||||||
|
│ │ └── api.ts # API请求封装
|
||||||
|
│ ├── next.config.ts # Next.js配置(API代理)
|
||||||
|
│ ├── package.json # 依赖配置
|
||||||
|
│ └── tsconfig.json # TypeScript配置
|
||||||
|
├── docs/ # 项目文档
|
||||||
|
│ └── PROJECT.md # 本文件
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、后端文件详解
|
||||||
|
|
||||||
|
### 3.1 `main.py`(949行)— API主入口
|
||||||
|
|
||||||
|
FastAPI应用,包含全部HTTP API路由:
|
||||||
|
|
||||||
|
**API路由:**
|
||||||
|
| 路径 | 方法 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/health` | GET | 健康检查 |
|
||||||
|
| `/api/signals/latest` | GET | 最新信号数据 |
|
||||||
|
| `/api/signals/history` | GET | 历史信号 |
|
||||||
|
| `/api/paper/summary` | GET | 模拟盘概览 |
|
||||||
|
| `/api/paper/positions` | GET | 当前持仓 |
|
||||||
|
| `/api/paper/trades` | GET | 历史交易 |
|
||||||
|
| `/api/paper/equity-curve` | GET | 权益曲线 |
|
||||||
|
| `/api/paper/stats` | GET | 详细统计(支持按币种) |
|
||||||
|
| `/api/paper/config` | GET/POST | 模拟盘配置 |
|
||||||
|
| `/api/server/status` | GET | 服务器监控 |
|
||||||
|
|
||||||
|
**依赖:** FastAPI, uvicorn, asyncpg, psutil
|
||||||
|
|
||||||
|
**运行方式:** `uvicorn main:app --host 0.0.0.0 --port 4332`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `signal_engine.py`(739行)— 🔥 核心信号引擎
|
||||||
|
|
||||||
|
**最重要的文件**。每15秒评估一次4个币种的交易信号。
|
||||||
|
|
||||||
|
**5层评分体系(100分满分):**
|
||||||
|
|
||||||
|
| 层级 | 权重 | 信号源 | 逻辑 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 方向层 | 45分 | CVD三轨(fast/mid) + P99大单 + CVD加速度 | 资金流向判断多空 |
|
||||||
|
| 拥挤层 | 20分 | 多空比 + 大户持仓比 | 市场拥挤度,反向指标 |
|
||||||
|
| 环境层 | 15分 | OI变化率 | 合约持仓量变化 |
|
||||||
|
| 确认层 | 15分 | CVD-Price背离 + 大单方向确认 | 信号交叉验证 |
|
||||||
|
| 辅助层 | 5分 | Coinbase Premium | 机构资金流向 |
|
||||||
|
|
||||||
|
**核心类:**
|
||||||
|
- `TradeWindow`:滑动窗口,维护buy_vol/sell_vol/CVD,自动trim过期数据
|
||||||
|
- `ATRCalculator`:ATR计算器,用于仓位管理和TP/SL计算
|
||||||
|
- `SymbolState`:每个币种的完整状态(4个窗口 + ATR + 大单 + 市场指标)
|
||||||
|
|
||||||
|
**数据流:**
|
||||||
|
```
|
||||||
|
aggTrades(PG) → process_trade() → 4个TradeWindow更新
|
||||||
|
↓
|
||||||
|
evaluate_signal() → 5层打分
|
||||||
|
↓
|
||||||
|
score >= 60 → 开仓信号
|
||||||
|
↓
|
||||||
|
paper_trading_open() → 写入paper_trades
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键参数:**
|
||||||
|
- `WINDOW_FAST = 30min`:CVD快线窗口
|
||||||
|
- `WINDOW_MID = 4h`:CVD慢线窗口
|
||||||
|
- `WINDOW_DAY = 24h`:P99大单计算窗口
|
||||||
|
- `EVAL_INTERVAL = 15s`:评估间隔
|
||||||
|
- `SIGNAL_THRESHOLD = 60`:开仓阈值分数
|
||||||
|
- `COOLDOWN = 300s`:同币种同方向冷却时间
|
||||||
|
|
||||||
|
**冷启动机制:**
|
||||||
|
1. 启动时`load_historical()`从PG读取最近4小时aggTrades灌入窗口
|
||||||
|
2. 前3轮(45秒)不出信号(冷启动保护)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `paper_monitor.py`(179行)— 模拟盘监控
|
||||||
|
|
||||||
|
独立PM2进程,通过WebSocket连接Binance实时监控持仓的TP/SL。
|
||||||
|
|
||||||
|
**核心逻辑:**
|
||||||
|
1. 启动时加载所有active/tp1_hit持仓
|
||||||
|
2. 连接Binance aggTrade WS获取实时价格
|
||||||
|
3. 每笔成交检查是否触发TP1/TP2/SL
|
||||||
|
4. 触发后更新paper_trades状态
|
||||||
|
|
||||||
|
**TP/SL规则:**
|
||||||
|
- TP1 = entry ± 1.5×ATR(平半仓)
|
||||||
|
- TP2 = entry ± 3.0×ATR(平剩余)
|
||||||
|
- SL = entry ∓ 1.0×ATR
|
||||||
|
- TP1触发后SL移动到成本价(保本)
|
||||||
|
|
||||||
|
**为什么独立进程:** TP/SL必须毫秒级响应,不能和15秒评估循环共享进程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 `db.py`(357行)— 数据库连接层
|
||||||
|
|
||||||
|
统一的PostgreSQL连接管理,支持同步和异步两种模式。
|
||||||
|
|
||||||
|
**连接池:**
|
||||||
|
- `get_sync_pool()` / `get_sync_conn()`:psycopg2同步池(供collector/signal_engine用)
|
||||||
|
- `get_async_pool()` / `async_fetch()`:asyncpg异步池(供FastAPI用)
|
||||||
|
- `get_cloud_sync_pool()` / `get_cloud_sync_conn()`:Cloud SQL双写池
|
||||||
|
|
||||||
|
**配置(环境变量):**
|
||||||
|
- `PG_HOST`:数据库地址(默认127.0.0.1,现在应指向Cloud SQL)
|
||||||
|
- `CLOUD_PG_HOST`:Cloud SQL地址(10.106.0.3)
|
||||||
|
- `CLOUD_PG_ENABLED`:双写开关
|
||||||
|
|
||||||
|
**Schema管理:** `init_schema()` + `ensure_partitions()`(按月自动建分区表)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 `agg_trades_collector.py`(307行)— aggTrades采集器
|
||||||
|
|
||||||
|
实时采集Binance合约aggTrades数据,是数据的源头。
|
||||||
|
|
||||||
|
**架构:**
|
||||||
|
- **WebSocket主链路**:4个币种各一条WS连接,实时推送
|
||||||
|
- **REST补洞**:断线重连后用REST API从last_agg_id追平
|
||||||
|
- **连续性巡检**:每60秒检查agg_id连续性,发现断档自动补洞
|
||||||
|
- **批量写入**:攒200条或1秒flush一次
|
||||||
|
|
||||||
|
**双写机制:** flush_buffer同时写本地PG和Cloud SQL,Cloud SQL写失败不影响主流程。
|
||||||
|
|
||||||
|
**数据量:** 目前约7000万条(BTC 23天 + ETH 3天 + XRP/SOL 3天)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 `market_data_collector.py`(192行)— 市场指标采集
|
||||||
|
|
||||||
|
每5分钟从Binance REST API采集市场指标,存入`market_indicators`表。
|
||||||
|
|
||||||
|
**采集指标(5种):**
|
||||||
|
| 指标 | API | 用途 |
|
||||||
|
|------|-----|------|
|
||||||
|
| long_short_ratio | /futures/data/globalLongShortAccountRatio | 多空比 |
|
||||||
|
| top_trader_position | /futures/data/topLongShortPositionRatio | 大户持仓比 |
|
||||||
|
| open_interest_hist | /futures/data/openInterestHist | 持仓量变化 |
|
||||||
|
| coinbase_premium | Coinbase vs Binance价差 | 机构资金流 |
|
||||||
|
| funding_rate | /fapi/v1/fundingRate | 资金费率 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 `liquidation_collector.py`(140行)— 清算数据采集
|
||||||
|
|
||||||
|
连接Binance WebSocket `forceOrder`流,实时采集强制平仓事件。
|
||||||
|
|
||||||
|
**双层存储:**
|
||||||
|
1. `liquidations`表:每笔清算原始记录
|
||||||
|
2. `market_indicators`表:每5分钟聚合(long_liq_usd/short_liq_usd/total/count)
|
||||||
|
|
||||||
|
**Side映射:** BUY order = SHORT被清算,SELL order = LONG被清算
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 `auth.py`(384行)— 认证系统
|
||||||
|
|
||||||
|
基于JWT的用户认证,邀请码注册制。
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 注册(需邀请码)/ 登录 / Token刷新
|
||||||
|
- Admin管理(邀请码生成/用户封禁)
|
||||||
|
- JWT签发(access_token 24h + refresh_token 7d)
|
||||||
|
|
||||||
|
**安全:** scrypt密码哈希 + HMAC-SHA256 JWT签名
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 `backtest.py`(503行)— 回测框架
|
||||||
|
|
||||||
|
对历史aggTrades数据运行信号引擎,验证策略表现。
|
||||||
|
|
||||||
|
**输出指标:** 胜率、PF、夏普比率、最大回撤、按方向/币种统计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 其他文件
|
||||||
|
|
||||||
|
| 文件 | 行数 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `backfill_agg_trades.py` | 209 | aggTrades历史数据回补(REST API批量拉取) |
|
||||||
|
| `admin_cli.py` | 123 | 命令行管理工具(生成邀请码、管理用户) |
|
||||||
|
| `signal_pusher.py` | 108 | **[已废弃]** 旧版信号推送(仍用SQLite,可删除) |
|
||||||
|
| `subscriptions.py` | 23 | **[预留]** 订阅系统占位 |
|
||||||
|
| `migrate_sqlite_to_pg.py` | 215 | **[一次性]** SQLite→PG数据迁移脚本 |
|
||||||
|
| `migrate_auth_sqlite_to_pg.py` | 170 | **[一次性]** Auth SQLite→PG迁移脚本 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、前端文件详解
|
||||||
|
|
||||||
|
### 4.1 页面
|
||||||
|
|
||||||
|
| 文件 | 行数 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `app/page.tsx` | 320 | 首页仪表盘(资金费率卡片 + 概览) |
|
||||||
|
| `app/signals/page.tsx` | 523 | 信号引擎(4币种实时评分 + 5层分数展示) |
|
||||||
|
| `app/paper/page.tsx` | 459 | 模拟盘(当前持仓 + 历史交易 + 权益曲线 + 按币种统计) |
|
||||||
|
| `app/trades/page.tsx` | 384 | 历史交易(筛选、排序、详情) |
|
||||||
|
| `app/server/page.tsx` | 279 | 服务器监控(PM2进程 + CPU/内存 + PG状态) |
|
||||||
|
| `app/kline/page.tsx` | 170 | K线图 |
|
||||||
|
| `app/live/page.tsx` | 130 | 实时数据流 |
|
||||||
|
| `app/dashboard/page.tsx` | 112 | 仪表盘 |
|
||||||
|
| `app/login/page.tsx` | 75 | 登录 |
|
||||||
|
| `app/register/page.tsx` | 88 | 注册(邀请码) |
|
||||||
|
| `app/about/page.tsx` | 81 | 关于 |
|
||||||
|
| `app/layout.tsx` | 34 | 全局布局 |
|
||||||
|
|
||||||
|
### 4.2 组件
|
||||||
|
|
||||||
|
| 文件 | 行数 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `components/Sidebar.tsx` | 139 | 侧边导航栏 |
|
||||||
|
| `components/LiveTradesCard.tsx` | 124 | 实时交易卡片组件 |
|
||||||
|
| `components/FundingChart.tsx` | 95 | 资金费率图表(Recharts) |
|
||||||
|
| `components/RateCard.tsx` | 82 | 费率展示卡片 |
|
||||||
|
| `components/Navbar.tsx` | 71 | 顶部导航 |
|
||||||
|
| `components/StatsCard.tsx` | 57 | 统计卡片 |
|
||||||
|
| `components/AuthHeader.tsx` | 37 | 认证状态头部 |
|
||||||
|
|
||||||
|
### 4.3 工具库
|
||||||
|
|
||||||
|
| 文件 | 行数 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `lib/auth.tsx` | 137 | JWT管理(存储/刷新/authFetch封装) |
|
||||||
|
| `lib/api.ts` | 116 | API请求基础封装 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、数据库Schema
|
||||||
|
|
||||||
|
### PostgreSQL(Cloud SQL)
|
||||||
|
|
||||||
|
**核心表:**
|
||||||
|
|
||||||
|
| 表名 | 说明 | 数据量 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `agg_trades` | aggTrades分区父表 | 7039万+ |
|
||||||
|
| `agg_trades_202602` | 2月分区 | 6854万 |
|
||||||
|
| `agg_trades_202603` | 3月分区 | 185万+ |
|
||||||
|
| `agg_trades_meta` | 每币种最新agg_id | 4条 |
|
||||||
|
| `signal_indicators` | 信号评分记录(每15秒×4币种) | 2.7万+ |
|
||||||
|
| `market_indicators` | 市场指标(每5分钟×4币种×5指标) | 5400+ |
|
||||||
|
| `paper_trades` | 模拟盘交易记录 | 163+ |
|
||||||
|
| `liquidations` | 清算事件原始记录 | 3600+ |
|
||||||
|
| `rate_snapshots` | 资金费率快照(旧,K线图用) | 12万+ |
|
||||||
|
| `users` | 用户表 | 1 |
|
||||||
|
| `invite_codes` | 邀请码 | 1 |
|
||||||
|
| `subscriptions` | 订阅信息 | 1 |
|
||||||
|
| `refresh_tokens` | JWT刷新令牌 | — |
|
||||||
|
| `signal_indicators_1m` | 1分钟粒度信号(备用) | — |
|
||||||
|
| `signal_trades` | 信号交易记录(旧) | — |
|
||||||
|
| `signal_logs` | 信号日志(旧) | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、PM2进程列表
|
||||||
|
|
||||||
|
| 进程名 | 文件 | 端口 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `arb-api` | backend/main.py | 4332 | FastAPI后端 |
|
||||||
|
| `arb-web` | frontend/ | 4333 | Next.js前端 |
|
||||||
|
| `signal-engine` | backend/signal_engine.py | — | 信号评估引擎 |
|
||||||
|
| `paper-monitor` | backend/paper_monitor.py | — | 模拟盘TP/SL监控 |
|
||||||
|
| `agg-collector` | backend/agg_trades_collector.py | — | aggTrades采集 |
|
||||||
|
| `market-collector` | backend/market_data_collector.py | — | 市场指标采集 |
|
||||||
|
| `liq-collector` | backend/liquidation_collector.py | — | 清算数据采集 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、信号源清单
|
||||||
|
|
||||||
|
| # | 信号源 | 采集方式 | 存储 | 评分使用 |
|
||||||
|
|---|--------|---------|------|---------|
|
||||||
|
| 1 | CVD三轨(fast 30m / mid 4h) | aggTrades WS实时计算 | 内存 | ✅ 方向层 |
|
||||||
|
| 2 | P99大单流 | aggTrades实时统计 | 内存 | ✅ 方向层 |
|
||||||
|
| 3 | CVD加速度 | CVD差分计算 | 内存 | ✅ 方向层+5 bonus |
|
||||||
|
| 4 | 多空比 + 大户持仓比 | Binance REST 5min | market_indicators | ✅ 拥挤层 |
|
||||||
|
| 5 | OI变化率 | Binance REST 5min | market_indicators | ✅ 环境层 |
|
||||||
|
| 6 | Coinbase Premium | Coinbase+Binance价差 | market_indicators | ✅ 辅助层 |
|
||||||
|
| 7 | Funding Rate | Binance REST 5min | market_indicators | ⬜ 采集中 |
|
||||||
|
| 8 | 清算数据 | Binance WS forceOrder | liquidations + market_indicators | ⬜ 采集中 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、模拟盘配置
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 初始资金 | $10,000 |
|
||||||
|
| 单笔风险 | 2%($200 = 1R) |
|
||||||
|
| 最大同时持仓 | 4个 |
|
||||||
|
| TP1 | 1.5× ATR(平半仓) |
|
||||||
|
| TP2 | 3.0× ATR(平剩余) |
|
||||||
|
| SL | 1.0× ATR |
|
||||||
|
| 手续费 | Taker 0.05% × 2(开仓+平仓) |
|
||||||
|
| 反向信号 | 先平仓再开新仓 |
|
||||||
|
| 冷却时间 | 同币种同方向300秒 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、部署信息
|
||||||
|
|
||||||
|
| 项目 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| 运行服务器 | GCP asia-northeast1-b (n2-standard-2, 2核8G) |
|
||||||
|
| 数据库 | GCP Cloud SQL PG18 (8核64G 100G) |
|
||||||
|
| Cloud SQL 内网IP | 10.106.0.3 |
|
||||||
|
| Cloud SQL 公网IP | 34.85.117.248 |
|
||||||
|
| Web访问 | https://arb.zhouyangclaw.com |
|
||||||
|
| Git仓库 | https://git.darkerilclaw.com/lulu/arbitrage-engine.git |
|
||||||
|
| 反向代理 | Caddy(SSL自动) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、版本路线图
|
||||||
|
|
||||||
|
| 版本 | 状态 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| V5.0 | ✅ 完成 | 基础信号系统 + PG迁移 |
|
||||||
|
| V5.1 | ✅ 当前 | 5层评分 + 模拟盘 + 6信号源评分 + 2信号源采集 |
|
||||||
|
| V5.2 | 📋 计划 | FR+清算加入评分 + 策略配置化 + AB测试 + 24h warmup |
|
||||||
|
| V5.3 | 📋 远期 | 推特新闻情绪信号(多模型投票制) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、已知问题与待改进
|
||||||
|
|
||||||
|
1. **signal_pusher.py 已废弃**:仍用SQLite,应删除或重写
|
||||||
|
2. **subscriptions.py 空文件**:预留的订阅系统未实现
|
||||||
|
3. **history/page.tsx 空页面**:只有5行占位代码
|
||||||
|
4. **冷启动warmup只有4小时**:P99大单需要24小时数据(V5.2改进)
|
||||||
|
5. **开仓价用信号评估价**:实盘需改为真实成交价
|
||||||
|
6. **双写机制**:切主库后agg_collector的本地双写可关闭
|
||||||
|
7. **前端缺少错误边界**:API异常时无友好提示
|
||||||
@ -1,497 +0,0 @@
|
|||||||
# 策略工厂与信号引擎验证清单(Strategy Factory & Signal Engine Validation)
|
|
||||||
|
|
||||||
> 目的:把需要“逐项验证”的点全部列清楚,变成可打勾的 checklist。
|
|
||||||
> 范围:`backend/signal_engine.py`(single-engine 数据发射源) + V5.4 策略工厂(`strategies` 表 → 信号 / 模拟盘)。
|
|
||||||
> 权威规格仍以 `docs/arbitrage-engine-full-spec.md` 为准,本文件只列“需要检验的点”。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 背景与角色划分(人话版)
|
|
||||||
|
|
||||||
- single-engine(`backend/signal_engine.py`)在本系统里扮演的是:
|
|
||||||
- 从 `agg_trades` + `market_indicators` 等数据源,滚动算出各种指标;
|
|
||||||
- 每 15 秒左右,对每个运行中策略打分、判断是否开仓;
|
|
||||||
- 把结果写入:
|
|
||||||
- `signal_indicators`(信号与指标快照);
|
|
||||||
- `paper_trades`(模拟盘开仓记录);
|
|
||||||
- 并通过 NOTIFY 推送给其他进程。
|
|
||||||
|
|
||||||
- V5.4 策略工厂的职责是:
|
|
||||||
- 把 `strategies` 表中 `status='running'` 的每一行,当成一个“策略实例”;
|
|
||||||
- 对每个实例,组合:
|
|
||||||
- 多窗口 CVD(5m / 15m / 30m / 1h / 4h);
|
|
||||||
- ALT 四层评分(Direction / Crowding / Environment / Auxiliary);
|
|
||||||
- 5 个否决 Gate(vol / cvd / whale / obi / spot_perp)的阈值与开关;
|
|
||||||
- 在统一的信号模型上,跑出不同风格的策略。
|
|
||||||
|
|
||||||
当前假设:
|
|
||||||
|
|
||||||
- 你只对 **aggTrades 数据流** 有信心,其他所有东西(指标/门控/评分/策略映射/落库)都需要重新审计;
|
|
||||||
- 我们需要一个尽可能完整的“验证清单”,后续可以按照这份清单逐条打勾。
|
|
||||||
|
|
||||||
下面把需要检查的点分成 3 层:
|
|
||||||
|
|
||||||
- A. 数据与指标层:所有输入信号是不是对的(基础指标本身)
|
|
||||||
- B. 策略配置与工厂层:DB 字段是否正确映射成策略参数
|
|
||||||
- C. 决策与落库层:评分 / 门控 / 开仓 / 写库 / 冷却 / 方向限制等是否符合规则
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 总体结构视图(先有个地图)
|
|
||||||
|
|
||||||
- **数据来源**
|
|
||||||
- `agg_trades`:逐笔成交 → 多窗口 CVD / ATR / VWAP / 大单 P95/P99 / 巨鲸成交;
|
|
||||||
- `market_indicators`:OI、多空比、Top Trader、资金费率、Coinbase Premium、OBI、期现背离等;
|
|
||||||
- 实时 WebSocket:OBI 实时值、spot-perp divergence 实时值(如果有)。
|
|
||||||
|
|
||||||
- **策略配置**
|
|
||||||
- `strategies` 表:策略实例的参数(symbol、方向、CVD 窗口、四层权重、五门阈值、TP/SL、timeout、flip_threshold 等);
|
|
||||||
- `load_strategy_configs_from_db()`:把这些行映射成内部配置 dict。
|
|
||||||
|
|
||||||
- **决策 / 落库**
|
|
||||||
- `evaluate_factory_strategy()`(原 `_evaluate_v53`):统一的评分与 Gates 逻辑;
|
|
||||||
- `save_indicator()`:写 `signal_indicators`;
|
|
||||||
- `paper_open_trade()`:写 `paper_trades`;
|
|
||||||
- `NOTIFY new_signal`:推送给 live_executor 等其他进程。
|
|
||||||
|
|
||||||
这 3 层每一处的小错误,都可能直接导致“赚钱 / 亏钱”的行为偏差,所以需要逐项验证。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. A 层:数据与指标验证(single-engine 基础指标)
|
|
||||||
|
|
||||||
### A1. 时间线与窗口基础
|
|
||||||
|
|
||||||
- ✅ `agg_trades.time_ms` 的语义是否统一为 **毫秒时间戳**,无混用秒/毫秒的情况。
|
|
||||||
- ✅ 冷启动加载历史时,时间范围是否覆盖至少 `24h+`,足够支持所有窗口(5m/15m/30m/1h/4h/24h)。
|
|
||||||
- 当前实现:冷启动预载 4h(WINDOW_MID),之后由实时数据自然填充到 24h。此行为已知且暂视为接受的设计,不视为 bug。
|
|
||||||
- ✅ 滚动窗口裁剪规则是否一致(例如统一使用 `t >= now_ms - window_ms`),避免 off-by-one。
|
|
||||||
- ✅ 历史回补或乱序插入时,是否会导致窗口内时间回跳,进而影响 CVD/ATR 等指标。
|
|
||||||
- 当前实现:agg_trades_collector 通过 REST 补洞 + 连续性巡检,配合按 agg_id/time_ms 有序读取,认为不会产生系统性时间回跳问题。
|
|
||||||
|
|
||||||
### A2. CVD 多窗口计算
|
|
||||||
|
|
||||||
目标:确认 5m / 15m / 30m / 1h / 4h 所有 CVD 计算都符合“主动买入量 - 主动卖出量”的定义。
|
|
||||||
|
|
||||||
- ✅ 对 is_buyer_maker 的解释是否正确:
|
|
||||||
- 实现:采集时 `is_buyer_maker = 1 if t["m"] else 0`,CVD 里 `is_buyer_maker == 0` 计为买量(taker 买),`==1` 计为卖量(taker 卖);
|
|
||||||
- 结论:与 Binance aggTrade 语义一致,CVD = taker 买入量 - taker 卖出量,符合预期。
|
|
||||||
- ✅ 基础 30m / 4h 窗口(`win_fast` / `win_mid`)中,新增/裁剪 trade 时 buy/sell 累计是否严格同步更新。
|
|
||||||
- 实现:所有 trade 通过 `TradeWindow.add()` 进入窗口,`trim()` 时同步回退 buy/sell 累计,CVD 始终等于当前窗口内的买减卖。
|
|
||||||
- ✅ 其他窗口(5m/15m/1h)的 CVD 是否通过对已有窗口(win_fast/win_mid)的列表按时间切片重算,而不是重新查库。
|
|
||||||
- 实现:通过 `_window_ms()` 把窗口字符串转毫秒,从 `win_fast`/`win_mid` 的 `trades` 列表按 `t_ms >= now_ms - window_ms` 切片重算,未重复查库。
|
|
||||||
- ✅ 所有窗口的截止时间点是否都使用当前 `now_ms`,不会出现“5m 用旧 now_ms,30m 用新 now_ms”的错位。
|
|
||||||
- 实现:`build_evaluation_snapshot(now_ms)` 与 `evaluate_factory_strategy(now_ms, ...)` 使用同一个 `now_ms`,动态切片和快照在同一时间基准上计算。
|
|
||||||
- ⚠️ 斜率与加速度:
|
|
||||||
- ✅ 定义:`cvd_fast_slope = cvd_fast(now) - prev_cvd_fast`,`cvd_fast_accel = cvd_fast_slope(now) - prev_cvd_fast_slope`,在持续运行时语义正确;
|
|
||||||
- ⚠️ 冷启动第一帧使用 `prev_* = 0` 计算,未做专门回退为 0 的处理,可能使第一帧 slope/accel 数值偏大;目前视为需要后续评估/是否调整的注意点。
|
|
||||||
|
|
||||||
### A3. ATR 与 ATR 百分位
|
|
||||||
|
|
||||||
- ✅ 5m K 线 bucket 是否按 `bar_ms = (time_ms // period_ms) * period_ms` 计算,保证时间对齐。
|
|
||||||
- 实现:`ATRCalculator.update()` 中按 `time_ms // period_ms` 对齐 5m bar,符合预期。
|
|
||||||
- ✅ Candle 的 open/high/low/close 更新逻辑是否正确,所有 trade 都被处理到。
|
|
||||||
- 实现:新 bar 时写入旧 bar 到队列,当前 bar 逐笔更新 high/low/close。
|
|
||||||
- ✅ TR(True Range)计算是否符合标准定义:
|
|
||||||
- 实现:`max(high - low, |high - prev_close|, |low - prev_close|)`,与标准 TR 定义一致。
|
|
||||||
- ✅ ATR 是按何种算法计算(简单平均 / Wilder EMA),与 full-spec 中的期望是否一致。
|
|
||||||
- 实现:先取第一根 TR 作为初始值,再用 `(atr_val*(length-1)+tr)/length` 逐步平滑,为 Wilder 风格 EMA,符合设计。
|
|
||||||
- ✅ 当 candle 数量不足(小于 2 或小于 ATR length)时,ATR 返回 0 或合理默认值。
|
|
||||||
- 实现:`len(candles) < 2` 时直接返回 0.0,避免未成熟 ATR 参与决策。
|
|
||||||
- ✅ `atr_percentile`:
|
|
||||||
- ✅ 历史 ATR 样本长度、维护方式(队列长度)是否合理;
|
|
||||||
- ✅ 百分位算法(<= current / count)是否正确;
|
|
||||||
- ✅ 样本太少时是否返回 50.0,避免无意义的极端值。
|
|
||||||
- 实现:`atr_history` 长度上限 288,少于 10 条或当前 ATR=0 时返回 50.0,其余按 rank 百分位计算。
|
|
||||||
|
|
||||||
### A4. P95 / P99 大单阈值
|
|
||||||
|
|
||||||
- ✅ 使用的样本是否来自 24h 窗口(`win_day`),并且只取 qty(不乘 price)。
|
|
||||||
- 实现:`compute_p95_p99()` 直接遍历 `win_day.trades`,取 `t[1]`(qty)构造样本数组,窗口长度即 24h。
|
|
||||||
- ✅ 样本数 < 100 时是否使用保底常数(BTC: p95>=5, p99>=10;ALT: p95>=50, p99>=100),是否与策略设计一致。
|
|
||||||
- 实现:少于 100 条时直接返回 `(5.0, 10.0)`;样本足够时再按 symbol 区分 BTC/ALT 保底。
|
|
||||||
- ✅ 排序和索引算法:`sorted(qtys)` 后,`int(n * 0.95)` / `int(n * 0.99)` 是否符合预期的分位点定义。
|
|
||||||
- 实现:对 qtys 排序后按整数下标取 95% / 99% 位置,属于合理的分位点近似。
|
|
||||||
- ✅ ALT 的分类逻辑是否只是“非 BTC 即 ALT”,以及这是否满足未来新增 symbol 的需求。
|
|
||||||
- 实现:判断条件为 `"BTC" in self.symbol`,否则按 ALT 处理;目前支持的 symbol 集合有限(BTC/ETH/XRP/SOL),该简化规则可接受,如未来扩展再细化。
|
|
||||||
|
|
||||||
### A5. 巨鲸成交与 `whale_cvd_ratio`
|
|
||||||
|
|
||||||
- ✅ 巨鲸交易过滤条件是否为 `price * qty >= 100_000`(100k USD),阈值可配置或硬编码?
|
|
||||||
- 实现:`usd_val = price * qty`,`usd_val >= 100_000` 视为巨鲸成交,阈值目前硬编码为 100k。
|
|
||||||
- ✅ `_whale_trades` 窗口长度是否为 15 分钟,裁剪逻辑是否正确(按 time_ms)。
|
|
||||||
- 实现:窗口长度 `WHALE_WINDOW_MS = 15min`,按 trade 的 `time_ms` 滑动裁剪。
|
|
||||||
- ✅ `whale_cvd_ratio` 计算:
|
|
||||||
- ✅ buy_usd = 所有巨鲸买单金额之和;
|
|
||||||
- ✅ sell_usd = 所有巨鲸卖单金额之和;
|
|
||||||
- ✅ ratio = (buy_usd - sell_usd) / (buy_usd + sell_usd);
|
|
||||||
- ✅ 无数据时返回 0.0 是否符合预期。
|
|
||||||
- ✅ BTC 与 ALT 对巨鲸数据的使用差异是否符合设计:
|
|
||||||
- 实现:BTC 用 `whale_cvd_ratio`(或 DB 中 `tiered_cvd_whale`)做 Gate;ALT 使用 `recent_large_trades` 中的对立/同向大单判断。
|
|
||||||
- 备注:鲸鱼 Gate 的“占比阈值”在 C2 中有一个缩放 bug(`whale_flow_pct` 被多除以 100),已在 Gate 小节标记为问题点。
|
|
||||||
|
|
||||||
### A6. `market_indicators` 中的 JSON 指标
|
|
||||||
|
|
||||||
需要逐个核对字段名称与意义(对照实际 JSON):
|
|
||||||
|
|
||||||
- ✅ `long_short_ratio`:
|
|
||||||
- 实现:从 `market_indicators` 读取 JSON `value`,使用 key `longShortRatio` 转为 float;默认 1.0。
|
|
||||||
- ✅ `top_trader_position`:
|
|
||||||
- 实现:使用 key `longAccount`,代表 top trader 多头占比(0~1)。
|
|
||||||
- ✅ `open_interest_hist`:
|
|
||||||
- 实现:使用 key `sumOpenInterestValue` 作为 OI 数值,后续只用来计算相对变化率。
|
|
||||||
- ✅ `coinbase_premium`:
|
|
||||||
- 实现:使用 key `premium_pct`,并统一除以 100 转成小数,避免量纲混乱。
|
|
||||||
- ✅ `funding_rate`:
|
|
||||||
- 实现:优先使用 `fundingRate`,否则 fallback `lastFundingRate`。
|
|
||||||
- ✅ `obi_depth_10`:
|
|
||||||
- 实现:使用 key `obi`,作为 [-1,1] 区间内的订单簿不平衡(正=买压,负=卖压),与 Gate4 逻辑一致。
|
|
||||||
- ✅ `spot_perp_divergence`:
|
|
||||||
- 实现:使用 key `divergence`,注释中定义为 `(spot - mark) / mark`,与 Gate5 使用方式一致。
|
|
||||||
|
|
||||||
### A7. OI 环境指标
|
|
||||||
|
|
||||||
- ✅ OI 读取:`open_interest_hist` 是否转换为 float;
|
|
||||||
- 实现:通过 `to_float(self.market_indicators.get("open_interest_hist"))` 转为 float。
|
|
||||||
- ✅ OI 变化率定义:`oi_change = (oi_now - oi_prev) / oi_prev`,oi_prev>0 的保护是否到位。
|
|
||||||
- 实现:仅在 `prev_oi_value > 0` 时计算变化率,否则 oi_change=0.0。
|
|
||||||
- ✅ environment_score_raw 的区间划分(例如 >=3%、0~3%、<0)对应基础分是否与 full-spec 一致。
|
|
||||||
- 实现:oi_change >=3% → 15 分;>0 → 10 分;<=0 → 5 分,符合设计。
|
|
||||||
- ✅ `prev_oi_value` 的更新时机:只在 oi_value>0 时更新是否会导致长时间停留在 0。
|
|
||||||
- 实现:oi_value>0 时才覆盖 prev,启动后首帧用默认 10 分,后续随 OI 变化更新,行为符合预期。
|
|
||||||
|
|
||||||
### A8. 其他环境与拥挤指标
|
|
||||||
|
|
||||||
- ✅ `long_short_ratio` 的逻辑:>1 多头拥挤,<1 空头拥挤,这个假设需要与真实数据验证。
|
|
||||||
- 实现:LONG 方向时 lsr 较低(<1,<0.7,<0.5)给更高分,SHORT 方向时 lsr 较高(>1,>1.5,>2)给更高分,符合“多空占比”直觉。
|
|
||||||
- ✅ crowding 层中对 LONG / SHORT 的条件:
|
|
||||||
- 实现与上述区间一致,并在 V5.1 路径中用 crowding_score=ls_score+top_trader_score;V5.3 路径中 crowding_score capped 为 25 分。
|
|
||||||
- ✅ `top_trader_position` 映射:
|
|
||||||
- 实现:LONG:>=0.55 给 10 分、<=0.45 给 0 分、其他给 5 分;SHORT 反向,对应“top trader 站在我们这一边时加分”的逻辑。
|
|
||||||
- ✅ `coinbase_premium` 阈值:
|
|
||||||
- 实现:>0.0005 / <-0.0005 视为强信号得 5 分;|premium|<=0.0005 得 2 分;其他情况 0 分,与 full-spec 描述一致。
|
|
||||||
- 备注:精确阈值是否需要微调属于策略优化问题,暂不视为实现 bug。
|
|
||||||
|
|
||||||
### A9. 时间与时区统一
|
|
||||||
|
|
||||||
- ✅ `agg_trades.time_ms`、`market_indicators.timestamp_ms`、`signal_indicators.ts` 是否全部为 UTC 毫秒。
|
|
||||||
- 实现:agg_trades 来自 Binance aggTrade `T`(毫秒),market_indicators 查询按 `timestamp_ms` 排序,signal_indicators.ts 由 `int(time.time()*1000)` 生成,语义统一为毫秒。
|
|
||||||
- ✅ signal_engine 的 `now_ms` 是否与最新成交的时间差在可接受范围内(比如 <5 秒)。
|
|
||||||
- 实现:每轮循环用当前 `time.time()` 作为 now_ms,立刻用 `fetch_new_trades` 拉取自上次 agg_id 之后的 tick 并入窗口,逻辑上不会积累大延迟。
|
|
||||||
- ✅ 所有窗口、冷却时间、timeout 等是否都用毫秒表达,避免混用秒/分钟。
|
|
||||||
- 实现:窗口常量、冷却时间、timeout 以及 liquidations 窗口等全部以毫秒表示。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. B 层:策略配置与工厂验证(DB → 内存策略)
|
|
||||||
|
|
||||||
### B1. 行筛选与顺序
|
|
||||||
|
|
||||||
- ✅ `load_strategy_configs_from_db()` 是否只选择 `status='running'` 的行。
|
|
||||||
- 实现:SQL 显式 `WHERE status = 'running'`。
|
|
||||||
- ⚠️ 是否有必要基于 `schema_version` 做过滤(未来版本升级时)。
|
|
||||||
- 实现:当前未按 `schema_version` 过滤,所有 running 策略视为同一版本;如未来引入多版本策略,需要补充。
|
|
||||||
- ✅ ORDER BY created_at 是否会在行为上产生影响(一般无关,但需要意识到)。
|
|
||||||
- 实现:仅影响策略评估与写入的顺序,对行为影响有限。
|
|
||||||
|
|
||||||
### B2. 字段映射完整性
|
|
||||||
|
|
||||||
逐字段确认 DB → cfg 的映射:
|
|
||||||
|
|
||||||
- ✅ `strategy_id`(uuid)是否被转为 string,并赋值到 `cfg["strategy_id"]`。
|
|
||||||
- 实现:SQL 中 `strategy_id::text`,后续直接写入 cfg。
|
|
||||||
- ✅ `display_name` 是否写入 `cfg["strategy_name_snapshot"]`。
|
|
||||||
- ✅ `symbol` / `direction` 是否原样保留,大小写约定是否固定。
|
|
||||||
- 实现:直接从 DB 取值赋入 cfg,不做大小写转换。
|
|
||||||
- ✅ `cvd_fast_window` / `cvd_slow_window` 是否直接映射到 cfg。
|
|
||||||
- ✅ `weight_direction` / `weight_env` / `weight_aux` / `weight_momentum` 是否正确组成 `cfg["weights"]`。
|
|
||||||
- ✅ `entry_score` / `flip_threshold` 是否正确映射到 `cfg["threshold"]` / `cfg["flip_threshold"]`。
|
|
||||||
- ✅ `gate_obi_enabled` / `gate_whale_enabled` / `gate_vol_enabled` / `gate_spot_perp_enabled` / `gate_cvd_enabled` 是否被正确翻译成 bool。
|
|
||||||
- 实现:直接使用 DB 中的 boolean 字段。
|
|
||||||
- ✅ 所有 Gate 相关 float 字段(`obi_threshold`、`whale_usd_threshold`、`whale_flow_pct`、`vol_atr_pct_min`、`spot_perp_threshold`)
|
|
||||||
在 None 时有合理默认(与 full-spec 匹配)。
|
|
||||||
- 实现:在转入 cfg 时对 None 做 `or 默认值` 处理,默认值来源于迁移脚本/旧 symbol_gates。
|
|
||||||
- ✅ `sl_atr_multiplier` / `tp1_ratio` / `tp2_ratio` 是否正确映射到 `cfg["tp_sl"]`。
|
|
||||||
- ✅ `timeout_minutes` 是否正确映射到 `cfg["timeout_minutes"]`。
|
|
||||||
|
|
||||||
### B3. legacy 策略兼容逻辑
|
|
||||||
|
|
||||||
- [ ] 固定 UUID:
|
|
||||||
- `...000053` → `"v53"`;
|
|
||||||
- `...000054` → `"v53_middle"`;
|
|
||||||
- `...000055` → `"v53_fast"`;
|
|
||||||
是否只对 deprecated legacy 策略生效。
|
|
||||||
- [ ] 其他策略是否统一命名为 `custom_<uuid前8位>`,与历史 `strategy` 字段约定一致。
|
|
||||||
- [ ] 当 DB 无策略时,是否 fallback 到 JSON 文件配置;当 DB 有策略时是否会重复加载 JSON。
|
|
||||||
|
|
||||||
更新:
|
|
||||||
|
|
||||||
- ✅ 固定 UUID 的映射逻辑正确,且只通过 `LEGACY_UUID_MAP` 在内存层生效,不会修改 DB 中的 strategy_id。
|
|
||||||
- ✅ 其他策略统一命名为 `custom_<uuid前8位>`,与历史 `strategy` 字段兼容。
|
|
||||||
- ✅ main() 中当 DB 读不到策略(异常)时才 fallback 到 JSON,正常情况下不会 DB+JSON 混用。
|
|
||||||
|
|
||||||
### B4. CVD 窗口参数
|
|
||||||
|
|
||||||
- ✅ `cvd_fast_window` / `cvd_slow_window` 的合法取值集合:是否只支持 {5m,15m,30m,1h,4h}。
|
|
||||||
- 实现:`_window_ms()` 支持任意 m/h,策略层目前只用上述几种组合。
|
|
||||||
- ✅ 非法值时行为:抛出异常还是回退到默认 30m/4h。
|
|
||||||
- 实现:未匹配 "m"/"h" 后缀时回退到 30m,对异常配置有兜底。
|
|
||||||
- ✅ `_window_ms()` 的实现是否覆盖所有这些枚举值。
|
|
||||||
- ✅ `evaluate_factory_strategy()` 中选择 trade 源时,fast/slow 窗口与 win_fast/win_mid 的映射是否一致。
|
|
||||||
- 实现:fast 窗口长度 <=30m 用 win_fast,否则用 win_mid;slow 总是用 win_mid,与 full-spec 描述一致。
|
|
||||||
|
|
||||||
### B5. 四层权重与 entry/flip
|
|
||||||
|
|
||||||
- ✅ `weights` 是否已真实参与 scoring(direction/env/aux/momentum 四层权重)。
|
|
||||||
- 现状:V5.4 `strategies` 中的权重字段被映射到 cfg["weights"](direction/env/aux/momentum),`evaluate_factory_strategy()` 会将四个权重归一化到 100 分,并把 direction+momentum 总权重按 55:25 的比例拆分给 direction/crowding,再按各层原始得分比例缩放,真实影响 total_score。
|
|
||||||
- ✅ `entry_score` 是否只用于“达到该分数即可考虑开仓”,不会在别处被重载。
|
|
||||||
- 实现:在 `evaluate_factory_strategy()` 中作为标准开仓阈值(total_score < entry_score 时不会产生 signal),signal_engine 通过是否有 signal 决定是否尝试开仓。
|
|
||||||
- ✅ `flip_threshold` 是否用于“重仓 / 反向强平”逻辑,阈值关系是否是 `flip_threshold >= entry_score`。
|
|
||||||
- 实现:在 `evaluate_factory_strategy()` 中,total_score >= flip_threshold 时记为重仓 tier="heavy";在 `signal_engine` 中,当反向信号的 `score >= flip_threshold` 时触发 signal_flip 平仓。代码允许设置任意数值,上层应保证配置关系。
|
|
||||||
- ⚠️ 当 `flip_threshold < entry_score` 时,系统行为是否有定义或需要禁止。
|
|
||||||
- 现状:代码未禁止这种配置,会导致实际行为变成“更容易触发 heavy 仓”;需要在策略配置侧约束,暂不视为实现错误。
|
|
||||||
|
|
||||||
### B6. Gate 参数与开关
|
|
||||||
|
|
||||||
逐 Gate 检查 DB → cfg 的映射:
|
|
||||||
|
|
||||||
- [ ] Gate vol:
|
|
||||||
- `gate_vol_enabled` → `gates["vol"]["enabled"]`;
|
|
||||||
- `vol_atr_pct_min` → `gates["vol"]["vol_atr_pct_min"]`,默认值与 full-spec 一致。
|
|
||||||
- [ ] Gate cvd:
|
|
||||||
- `gate_cvd_enabled` → `gates["cvd"]["enabled"]`;
|
|
||||||
- 默认是否为 True,并与 full-spec 一致。
|
|
||||||
- ✅ Gate whale:
|
|
||||||
- `gate_whale_enabled` → `gates["whale"]["enabled"]`;
|
|
||||||
- `whale_usd_threshold` → `gates["whale"]["whale_usd_threshold"]`;
|
|
||||||
- ✅ `whale_flow_pct` → `gates["whale"]["whale_flow_pct"]`(注意是否有 /100 的处理)。
|
|
||||||
- 更新:`evaluate_factory_strategy()` 中已去掉多余的 `/100` 缩放,现使用 0~1 的比例值与 DB/迁移脚本一致,鲸鱼 Gate 灵敏度恢复到设计水平。
|
|
||||||
- [ ] Gate obi:
|
|
||||||
- `gate_obi_enabled` → `gates["obi"]["enabled"]`;
|
|
||||||
- `obi_threshold` → `gates["obi"]["threshold"]`。
|
|
||||||
- [ ] Gate spot_perp:
|
|
||||||
- `gate_spot_perp_enabled` → `gates["spot_perp"]["enabled"]`;
|
|
||||||
- `spot_perp_threshold` → `gates["spot_perp"]["threshold"]`。
|
|
||||||
|
|
||||||
### B7. 策略方向限制
|
|
||||||
|
|
||||||
- ⚠️ DB 中的 `direction` 字段枚举是否固定为 `"long" | "short" | "both"`(大小写统一)。
|
|
||||||
- 现状:代码层直接使用 DB 文本,不验证枚举/大小写;依赖上游迁移与 API 保证有效值。
|
|
||||||
- ✅ `evaluate_factory_strategy()` 或上层逻辑中是否有对该字段的应用:
|
|
||||||
- 预期:direction=long → 禁止 SHORT 信号开仓;direction=short → 禁止 LONG 信号开仓;direction=both → 不限制。
|
|
||||||
- 现状:已在 `evaluate_factory_strategy()` 内增加 per-strategy 方向约束:
|
|
||||||
- long 策略在方向判定为 SHORT 时不会产生 signal(但仍计算评分与指标快照);
|
|
||||||
- short 策略在方向判定为 LONG 时不会产生 signal;
|
|
||||||
- both 不做限制。
|
|
||||||
- ⚠️ 若当前实现未对 strategy direction 做约束,需要在此记为一个重点检查项。
|
|
||||||
- 更新:方向约束已在信号生成阶段生效,但反向 signal_flip 平仓仍按“环境方向”判断,是否也需要 direction 约束留待后续决策。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. C 层:决策与落库验证(评分 + 门控 + 开仓 + 写库)
|
|
||||||
|
|
||||||
### C1. 逐 symbol / 逐策略评估流程
|
|
||||||
|
|
||||||
- ✅ 评估循环是否为:先按 symbol 分组,再对每个 symbol 内的所有策略依次评估。
|
|
||||||
- ⚠️ 对于每个策略,是否在评估前检查 `cfg["symbol"] == state.symbol`,确保不会在错误币种上开仓。
|
|
||||||
- 现状:评估阶段对所有策略都计算分数,但在开/平仓阶段才按 `strategy_cfg["symbol"]` 过滤,因此不会在错误币种上实际开仓,只是多算了一些“无用分”。
|
|
||||||
- ✅ 是否存在“一个策略在多个 symbol 上同时运行”的预期场景,如果有,需要明确规则。
|
|
||||||
- 现状:当前配置一条策略只绑定一个 symbol,引擎逻辑允许同一策略在多个 symbol 开仓,但上层 API 不这么用。
|
|
||||||
|
|
||||||
### C2. 五个 Gate 的逻辑
|
|
||||||
|
|
||||||
对于每一门,需要验证条件与 full-spec 一致:
|
|
||||||
|
|
||||||
- Gate1:波动率门(ATR/price 下限)
|
|
||||||
- ✅ `atr_pct_price = atr / price` 的计算是否正确;
|
|
||||||
- ✅ 阈值 `min_vol` 是否来自 DB 的 `vol_atr_pct_min` 或 JSON fallback;
|
|
||||||
- ✅ 条件:`atr_pct_price < min_vol` 时,gate_block = `"low_vol(...)"`,并且后续直接视为不通过。
|
|
||||||
|
|
||||||
- Gate2:CVD 共振门
|
|
||||||
- ✅ 快慢 CVD 同向时方向判断是否为:
|
|
||||||
fast>0 且 mid>0 → LONG;fast<0 且 mid<0 → SHORT;
|
|
||||||
- ✅ 不同向时,若 gate_cvd_enabled=True,则设置 `no_direction=True` 且 `gate_block="no_direction_consensus"`;
|
|
||||||
- ✅ 当 Gate2 关闭(enabled=False)时,是否允许仅根据 fast CVD 决定方向。
|
|
||||||
|
|
||||||
- Gate3:鲸鱼否决门
|
|
||||||
- BTC:
|
|
||||||
- ✅ 使用 `whale_cvd_ratio` 或 fallback 到 `market_indicators["tiered_cvd_whale"]`;
|
|
||||||
- ✅ 条件:
|
|
||||||
- LONG 且 whale_cvd_ratio < -阈值 → 否决;
|
|
||||||
- SHORT 且 whale_cvd_ratio > 阈值 → 否决。
|
|
||||||
- ALT:
|
|
||||||
- ✅ `recent_large_trades` 中,对立大单与同向大单的判定逻辑是否正确;
|
|
||||||
- ✅ 只有“有对立大单且无同向大单”时才否决,且金额阈值是否为 whale_usd_threshold × price。
|
|
||||||
|
|
||||||
- Gate4:OBI 否决门
|
|
||||||
- ✅ OBI 采样顺序是否为:先用实时 `rt_obi`,否则用 DB 中 `obi_depth_10`;
|
|
||||||
- ✅ 条件:
|
|
||||||
- LONG 且 obi_raw < -obi_veto → Gate 拒绝;
|
|
||||||
- SHORT 且 obi_raw > obi_veto → Gate 拒绝。
|
|
||||||
|
|
||||||
- Gate5:期现背离否决门
|
|
||||||
- ✅ 采样顺序:先用实时 `rt_spot_perp_div`,否则用 DB 中 `spot_perp_divergence`;
|
|
||||||
- ✅ 条件:
|
|
||||||
- LONG 且 divergence < -spd_veto → Gate 拒绝;
|
|
||||||
- SHORT 且 divergence > spd_veto → Gate 拒绝。
|
|
||||||
|
|
||||||
### C3. gate_block / gate_passed 对评分和信号的影响
|
|
||||||
|
|
||||||
- ✅ 任意一门触发后,`gate_block` 是否被设置为对应字符串,`gate_passed=False`。
|
|
||||||
- ✅ 评分计算完后,如果 `gate_passed=False`,是否将 `total_score` 强制置为 0。
|
|
||||||
- ✅ `result["signal"]` 在 gate_passed=False 或 no_direction=True 时是否总是 None。
|
|
||||||
- ✅ `factors` 中是否仍然保留原始四层分数和 Gate 细节,方便事后分析。
|
|
||||||
|
|
||||||
### C4. 四层评分计算细节
|
|
||||||
|
|
||||||
- Direction Layer:
|
|
||||||
- ✅ CVD 共振基础分数(例如 30 分)是否与 full-spec 一致;
|
|
||||||
- ✅ p99_flow:有同向大单 / 无大单 / 有对立大单 三种情况的得分是否符合预期;
|
|
||||||
- ✅ accel_bonus:cvd_fast_accel 与方向同向时报 5 分;
|
|
||||||
- ✅ v53_fast 的独立加速路径(不要求双线共振)是否正确实现。
|
|
||||||
|
|
||||||
- Crowding Layer:
|
|
||||||
- ✅ long_short_ratio 与 top_trader_position 的区间划分与得分是否符合 full-spec;
|
|
||||||
- ✅ crowding_score capped 到 25 分。
|
|
||||||
|
|
||||||
- Environment Layer:
|
|
||||||
- ✅ ALT(或默认)路径:只用 OI 变化率映射出 environment_score_raw;
|
|
||||||
- ✅ v53_fast 额外的 OBI bonus:强/弱阈值与得分 5/3 是否正确;
|
|
||||||
- ✅ 总分 capped 到 15。
|
|
||||||
|
|
||||||
- Auxiliary Layer:
|
|
||||||
- ✅ coinbase_premium 的区间与得分(5/2/0)是否正确实施。
|
|
||||||
|
|
||||||
- 汇总:
|
|
||||||
- ✅ total_score = 四层得分之和 capped 到 100,并四舍五入到 0.1;
|
|
||||||
- ✅ 如果 `gate_passed=False`,最终 total_score 是否强制为 0。
|
|
||||||
|
|
||||||
### C5. 权重的实际使用情况
|
|
||||||
|
|
||||||
- ✅ 当前实现已经使用 `weights` 来调整各层最大分/权重(direction/env/aux/momentum),需要确认缩放规则是否符合预期;
|
|
||||||
- ✅ future:如要进一步调整权重逻辑,需要设计迁移/验证方案,并评估对历史统计的影响。
|
|
||||||
|
|
||||||
### C6. signal 生成与冷却(entry / flip / cooldown)
|
|
||||||
|
|
||||||
- ✅ 条件逻辑:
|
|
||||||
- total_score >= flip_threshold → `tier="heavy"`;
|
|
||||||
- entry_score <= total_score < flip_threshold → `tier="standard"`;
|
|
||||||
- total_score < entry_score → 不开仓。
|
|
||||||
- ✅ 冷却:
|
|
||||||
- COOLDOWN_MS = 10 分钟;
|
|
||||||
- 以 `strategy_name` 为 key 的 last_signal_ts 是否正确记录;
|
|
||||||
- 冷却期内不再生成新 signal。
|
|
||||||
- ✅ `direction` 限制:
|
|
||||||
- 策略 direction=long/short/both 是否在最终生成 signal 和 signal_flip 时被应用:
|
|
||||||
- 只多策略:不会生成空头 signal,也不会因为空头评估方向触发 signal_flip 平仓;
|
|
||||||
- 只空策略:反之同理;
|
|
||||||
- both:行为与原来一致。
|
|
||||||
|
|
||||||
### C7. `signal_indicators` 落库字段映射
|
|
||||||
|
|
||||||
- ✅ ts:是否使用当前 `now_ms`,单位为毫秒。
|
|
||||||
- ✅ symbol:与当前 `SymbolState.symbol` 一致。
|
|
||||||
- ✅ strategy:写入 `cfg["name"]`(legacy 名称)。
|
|
||||||
- ✅ strategy_id:写入 `cfg["strategy_id"]`(uuid string)。
|
|
||||||
- ✅ strategy_name_snapshot:写入 `cfg["strategy_name_snapshot"]`。
|
|
||||||
- ✅ 所有指标字段:
|
|
||||||
- cvd_fast / cvd_mid / cvd_day / cvd_fast_slope;
|
|
||||||
- atr_5m / atr_percentile / atr_value;
|
|
||||||
- vwap_30m / price;
|
|
||||||
- p95_qty / p99_qty;
|
|
||||||
- cvd_fast_5m(仅 v53_fast 有值);
|
|
||||||
是否与 `snapshot` / 重新计算的一致。
|
|
||||||
- ✅ factors(JSON):
|
|
||||||
- 是否包含 gate_passed / gate_block / atr_pct_price / obi_raw / spot_perp_div / whale_cvd_ratio;
|
|
||||||
- direction / crowding / environment / auxiliary 四个子对象的 score/max 等字段是否齐全。
|
|
||||||
|
|
||||||
### C8. `signal_feature_events`(如仍在使用)
|
|
||||||
|
|
||||||
- ✅ 只对 v53 系列策略写入(策略名以 "v53" 开头)。
|
|
||||||
- ✅ 原始字段(cvd_fast_raw 等)是否与 `result` 中的一致。
|
|
||||||
- ✅ score_direction / score_crowding / score_environment / score_aux 是否与 factors 中一致。
|
|
||||||
- ✅ gate_passed / block_reason 是否正确记录,便于后期分析。
|
|
||||||
|
|
||||||
### C9. `paper_open_trade` 模拟盘开仓逻辑
|
|
||||||
|
|
||||||
- ✅ ATR <= 0 时是否直接拒绝开仓,避免无意义 SL/TP。
|
|
||||||
- ✅ SL/TP 价格计算:
|
|
||||||
- LONG:`SL = price - risk_distance`,`TP1 = price + tp1_ratio × risk_distance`,`TP2 = price + tp2_ratio × risk_distance`;
|
|
||||||
- SHORT:`SL = price + risk_distance`,`TP1 = price - tp1_ratio × risk_distance`,`TP2 = price - tp2_ratio × risk_distance`;
|
|
||||||
- 旧版 JSON 策略仍按 `tp*_mult × ATR` 方式计算,属于兼容分支;
|
|
||||||
是否与 full-spec 中“以 R 计目标”的定义一致。
|
|
||||||
- ✅ risk_distance = sl_multiplier × ATR 是否正确计算并写入。
|
|
||||||
- ✅ SL 合理性校验:
|
|
||||||
- 实际 SL 距离是否在 [0.8, 1.2] × risk_distance 之间;
|
|
||||||
- 不满足时是否拒绝开仓并打日志(避免奇怪配置)。
|
|
||||||
- ✅ 模拟盘全局开关:
|
|
||||||
- `PAPER_TRADING_ENABLED` / `PAPER_ENABLED_STRATEGIES` / `PAPER_MAX_POSITIONS` 是否生效;
|
|
||||||
- 同一策略是否有 per-strategy 的最大持仓控制(如无,需要记录)。
|
|
||||||
- ✅ 写库字段:
|
|
||||||
- symbol / direction / score / tier / entry_price / entry_ts;
|
|
||||||
- tp1_price / tp2_price / sl_price / atr_at_entry;
|
|
||||||
- score_factors = factors JSON;
|
|
||||||
- strategy = `cfg["name"]`;
|
|
||||||
- strategy_id / strategy_name_snapshot;
|
|
||||||
- risk_distance;
|
|
||||||
是否全部正确写入,对应字段类型与含义与 full-spec 一致。
|
|
||||||
|
|
||||||
### C10. 重复开仓与持仓交互
|
|
||||||
|
|
||||||
- ✅ 当前设计是否允许“同一策略在同一 symbol 上同时有多笔持仓”(分批进场)。
|
|
||||||
- ✅ 是否存在逻辑限制“同一策略同方向仅允许一笔活动持仓”,如果没有,需要明确这一点。
|
|
||||||
- ✅ 与 `paper_monitor` 的交互:
|
|
||||||
- 反向 signal 是否会触发 signal_flip 平仓;
|
|
||||||
- 平仓后冷却和再开仓的行为是否符合预期。
|
|
||||||
|
|
||||||
### C11. 通知与其他 side-effect
|
|
||||||
|
|
||||||
- ✅ 当 `result["signal"]` 非空时,是否总是触发 `NOTIFY new_signal`,payload 格式是否是:
|
|
||||||
`"symbol:strategy:signal:score"`。
|
|
||||||
- ✅ 当 gate_block 不为空(gate_passed=False)或 no_direction=True 时,是否绝不会发 NOTIFY。
|
|
||||||
- ✅ 如果 live_executor 等进程依赖该 payload,是否已经约定好格式且长期不变。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 验证优先级建议
|
|
||||||
|
|
||||||
后续可以按下面优先顺序逐项打勾:
|
|
||||||
|
|
||||||
**P0(必须最先验证的)**
|
|
||||||
|
|
||||||
- A2:CVD 多窗口计算(5m/15m/30m/1h/4h 全链路)。
|
|
||||||
- A3+A9:ATR / ATR 百分位 / 波动率门。
|
|
||||||
- A5+A6:巨鲸、OBI、期现背离的数据源与符号方向。
|
|
||||||
- C2+C3+C4+C6:五个 Gate + 四层评分 + entry/flip + cooldown 的交互。
|
|
||||||
- C7+C9:`signal_indicators` / `paper_trades` 落库字段,特别是 strategy_id / strategy_name_snapshot 与 SL/TP/risk_distance。
|
|
||||||
|
|
||||||
**P1(中优先级)**
|
|
||||||
|
|
||||||
- B2~B7:`strategies` 表字段映射完整性、CVD 窗口、Gate 参数、方向限制。
|
|
||||||
- C1:per-symbol 遍历逻辑与 symbol 过滤。
|
|
||||||
- C8:`signal_feature_events` 的字段映射(如还在使用)。
|
|
||||||
|
|
||||||
**P2(相对低优先级)**
|
|
||||||
|
|
||||||
- 对权重缩放逻辑的长期行为做验证(C5),确保与预期一致;
|
|
||||||
- 日志/监控是否足够支撑线上排查;
|
|
||||||
- 未来如需长期在线学习,可在此基础上加更多验证点。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 使用方式(给人类 / AI 的操作建议)
|
|
||||||
|
|
||||||
- 把本文件当作 **审计 checklist**:
|
|
||||||
- 每次改动前,先在这里找相关条目,确认“期望行为”是什么;
|
|
||||||
- 审计现有代码时,对照这些条目逐项比对,实现与期望不一致的地方要记录为 bug。
|
|
||||||
- 对于 P0 条目,建议配合:
|
|
||||||
- 真实历史数据回放(从数据库抽样一段时间);
|
|
||||||
- 针对单一策略 / 单一 symbol 的离线重算脚本(对比 signal_indicators / paper_trades)。
|
|
||||||
- 改动任何 C 层(评分/开仓/落库)逻辑时,必须同时:
|
|
||||||
- 更新 `docs/arbitrage-engine-full-spec.md` 中对应章节;
|
|
||||||
- 在本验证清单中勾选/更新受影响的条目;
|
|
||||||
- 附上简短的“为什么这样改 + 对历史行为的影响”说明。
|
|
||||||
|
|
||||||
> 一句话:
|
|
||||||
> 以后任何人(包括 AI)想动 signal_engine / 策略工厂,都应该先看 full-spec,再看这份验证清单,
|
|
||||||
> 确保“为什么这么算”和“有没有算对”都在一个有记录的地方。
|
|
||||||
61
docs/V52-TODO.md
Normal file
61
docs/V52-TODO.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# V5.2 待修复清单
|
||||||
|
|
||||||
|
> 来源:Claude Code审阅报告 + 露露复查
|
||||||
|
> 创建:2026-03-01
|
||||||
|
|
||||||
|
## 已在V5.1-hotfix中修复(P0)
|
||||||
|
|
||||||
|
| ID | 问题 | 修复 |
|
||||||
|
|----|------|------|
|
||||||
|
| P0-1 | 冷却期阻断反向信号平仓 | evaluate_signal始终输出direction,主循环基于direction+score>=60触发反向平仓 |
|
||||||
|
| P0-2 | pnl_r TP场景虚高2倍 | paper_monitor+signal_engine统一用(exit-entry)/risk_distance计算 |
|
||||||
|
| P1-1 | 分区月份Bug(timedelta 30天) | 改为正确的月份加法 + UTC时区 |
|
||||||
|
| P2-2 | 分区边界用本地时区 | 改为datetime.timezone.utc |
|
||||||
|
|
||||||
|
## V5.2 必须修复
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
| ID | 优先级 | 文件 | 问题 | 建议修复 |
|
||||||
|
|----|--------|------|------|---------|
|
||||||
|
| P0-3 | P1 | signal_engine.py | 开仓价用30分VWAP而非实时价 | 改用win_fast.trades[-1][2]最新成交价 |
|
||||||
|
| P0-4 | P2 | signal_engine+paper_monitor | 双进程并发写竞态 | SELECT FOR UPDATE SKIP LOCKED |
|
||||||
|
| P1-2 | P2 | signal_engine.py | 浮点精度漂移(buy_vol/sell_vol) | 每N次trim后从deque重算sums |
|
||||||
|
| P1-3 | P1 | market_data_collector.py | 单连接无重连 | 改用db.get_sync_conn()连接池 |
|
||||||
|
| P1-4 | P3 | db.py | 连接池初始化线程不安全 | 加threading.Lock双重检查 |
|
||||||
|
| P2-1 | P2 | market_data_collector.py | XRP/SOL coinbase_premium KeyError | 不在pair_map中的跳过 |
|
||||||
|
| P2-3 | P2 | agg_trades_collector.py | flush_buffer每秒调ensure_partitions | 移到定时任务(每小时) |
|
||||||
|
| P2-4 | P3 | liquidation_collector.py | elif条件冗余 | 改为else |
|
||||||
|
| P2-5 | P2 | signal_engine.py | atr_percentile @property有写副作用 | 移到显式update_atr_history() |
|
||||||
|
| P2-6 | P2 | main.py | 1R=$200硬编码 | 从paper_config.json读取 |
|
||||||
|
| P3-1 | P2 | auth.py | JWT密钥硬编码默认值 | 启动时强制校验环境变量 |
|
||||||
|
| P3-2 | P3 | main.py | CORS allow_origins=["*"] | 限制为前端域名 |
|
||||||
|
| P3-3 | P3 | auth.py | refresh token刷新非原子 | UPDATE...RETURNING原子操作 |
|
||||||
|
| P3-4 | P3 | auth.py | 登录无频率限制 | slowapi或Redis计数器 |
|
||||||
|
| NEW | P1 | signal_engine.py | 冷启动warmup只有4小时 | 分批加载24小时数据,加载完再出信号 |
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
|
| ID | 优先级 | 文件 | 问题 | 建议修复 |
|
||||||
|
|----|--------|------|------|---------|
|
||||||
|
| FE-P1-1 | P1 | lib/auth.tsx | 并发401多次refresh竞态 | 单例Promise防并发刷新 |
|
||||||
|
| FE-P1-2 | P1 | lib/auth.tsx | 刷新失败AuthContext未同步 | 事件总线通知强制logout |
|
||||||
|
| FE-P1-3 | P1 | 所有页面 | catch{}静默吞掉API错误 | 加error state+用户提示 |
|
||||||
|
| FE-P1-4 | P2 | paper/page.tsx | LatestSignals串行4请求 | Promise.allSettled并行 |
|
||||||
|
| FE-P2-1 | P3 | app/page.tsx | MiniKChart每30秒销毁重建 | 只更新数据不重建chart |
|
||||||
|
| FE-P2-3 | P2 | paper/page.tsx | ControlPanel非admin可见 | 校验isAdmin |
|
||||||
|
| FE-P2-4 | P1 | paper/page.tsx | WebSocket无断线重连 | 指数退避重连+断线提示 |
|
||||||
|
| FE-P2-5 | P2 | paper/page.tsx | 1R=$200前端硬编码 | 从API读取配置 |
|
||||||
|
| FE-P2-6 | P2 | signals/page.tsx | 5秒轮询5分钟数据 | 改为300秒间隔 |
|
||||||
|
| FE-P2-8 | P3 | paper/signals | 大量any类型 | 定义TypeScript interface |
|
||||||
|
| FE-P3-1 | P3 | lib/auth.tsx | Token存localStorage | 评估httpOnly cookie |
|
||||||
|
| FE-P3-3 | P3 | app/page.tsx | Promise.all任一失败全丢 | 改Promise.allSettled |
|
||||||
|
|
||||||
|
## V5.2 新功能(同步开发)
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| FR+清算加入评分 | 8信号源完整接入 |
|
||||||
|
| 策略配置化框架 | 一套代码多份配置 |
|
||||||
|
| AB测试 | V5.1 vs V5.2两套权重对比 |
|
||||||
|
| 24h warmup | 启动时分批加载24小时数据 |
|
||||||
117
docs/ai/00-system-overview.md
Normal file
117
docs/ai/00-system-overview.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: standard
|
||||||
|
---
|
||||||
|
|
||||||
|
# 00 — System Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
High-level description of the arbitrage-engine project: what it does, its tech stack, repo layout, and entry points.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- **Domain**: Crypto perpetual futures funding-rate arbitrage monitoring and short-term trading signal engine.
|
||||||
|
- **Strategy**: Hold spot long + perpetual short to collect funding rates every 8 h; plus a CVD/ATR-based short-term directional signal engine (V5.x).
|
||||||
|
- **Backend**: Python / FastAPI + independent PM2 worker processes; PostgreSQL (local + Cloud SQL dual-write).
|
||||||
|
- **Frontend**: Next.js 16 / React 19 / TypeScript SPA, charting via `lightweight-charts` + Recharts.
|
||||||
|
- **Targets**: BTC, ETH, XRP, SOL perpetual contracts on Binance USDC-M futures.
|
||||||
|
- **Deployment**: PM2 process manager on a GCP VM; frontend served via Next.js; backend accessible at `https://arb.zhouyangclaw.com`.
|
||||||
|
- **Auth**: JWT (access 24 h + refresh 7 d) + invite-code registration gating.
|
||||||
|
- **Trading modes**: Paper (simulated), Live (Binance Futures testnet or production via `TRADE_ENV`).
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### Repo Layout
|
||||||
|
```
|
||||||
|
arbitrage-engine/
|
||||||
|
├── backend/ # Python FastAPI API + all worker processes
|
||||||
|
│ ├── main.py # FastAPI app entry point (uvicorn)
|
||||||
|
│ ├── signal_engine.py # V5 signal engine (PM2 worker, 15 s loop)
|
||||||
|
│ ├── live_executor.py # Live trade executor (PM2 worker)
|
||||||
|
│ ├── risk_guard.py # Risk circuit-breaker (PM2 worker)
|
||||||
|
│ ├── market_data_collector.py # Binance WS market data (PM2 worker)
|
||||||
|
│ ├── agg_trades_collector.py # Binance aggTrades WS collector (PM2 worker)
|
||||||
|
│ ├── liquidation_collector.py # Binance liquidation WS collector (PM2 worker)
|
||||||
|
│ ├── signal_pusher.py # Discord signal notifier (PM2 worker)
|
||||||
|
│ ├── db.py # Dual-pool PostgreSQL layer (psycopg2 sync + asyncpg async)
|
||||||
|
│ ├── auth.py # JWT auth + invite-code registration router
|
||||||
|
│ ├── trade_config.py # Symbol / qty precision constants
|
||||||
|
│ ├── backtest.py # Offline backtest engine
|
||||||
|
│ ├── paper_monitor.py # Paper trade monitoring helper
|
||||||
|
│ ├── admin_cli.py # CLI for invite / user management
|
||||||
|
│ ├── subscriptions.py # Signal subscription query helper
|
||||||
|
│ ├── paper_config.json # Paper trading runtime toggle
|
||||||
|
│ ├── strategies/ # JSON strategy configs (v51_baseline, v52_8signals)
|
||||||
|
│ ├── ecosystem.dev.config.js # PM2 process definitions
|
||||||
|
│ └── logs/ # Rotating log files
|
||||||
|
├── frontend/ # Next.js app
|
||||||
|
│ ├── app/ # App Router pages
|
||||||
|
│ ├── components/ # Reusable UI components
|
||||||
|
│ ├── lib/api.ts # Typed API client
|
||||||
|
│ └── lib/auth.tsx # Auth context + token refresh logic
|
||||||
|
├── docs/ # Documentation (including docs/ai/)
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
└── signal-engine.log # Live log symlink / output file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Primary Language & Frameworks
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Backend API | Python 3.x, FastAPI, uvicorn |
|
||||||
|
| DB access | asyncpg (async), psycopg2 (sync) |
|
||||||
|
| Frontend | TypeScript, Next.js 16, React 19 |
|
||||||
|
| Styling | Tailwind CSS v4 |
|
||||||
|
| Charts | lightweight-charts 5.x, Recharts 3.x |
|
||||||
|
| Process manager | PM2 (via `ecosystem.dev.config.js`) |
|
||||||
|
| Database | PostgreSQL (local + Cloud SQL dual-write) |
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
| Process | File | Role |
|
||||||
|
|---------|------|------|
|
||||||
|
| HTTP API | `backend/main.py` | FastAPI on uvicorn |
|
||||||
|
| Signal engine | `backend/signal_engine.py` | 15 s indicator loop |
|
||||||
|
| Trade executor | `backend/live_executor.py` | PG NOTIFY listener → Binance API |
|
||||||
|
| Risk guard | `backend/risk_guard.py` | 5 s circuit-breaker loop |
|
||||||
|
| Market data | `backend/market_data_collector.py` | Binance WS → `market_indicators` table |
|
||||||
|
| aggTrades collector | `backend/agg_trades_collector.py` | Binance WS → `agg_trades` partitioned table |
|
||||||
|
| Liquidation collector | `backend/liquidation_collector.py` | Binance WS → liquidation tables |
|
||||||
|
| Signal pusher | `backend/signal_pusher.py` | DB → Discord push |
|
||||||
|
| Frontend | `frontend/` | Next.js dev/prod server |
|
||||||
|
|
||||||
|
### Monitored Symbols
|
||||||
|
`BTCUSDT`, `ETHUSDT`, `XRPUSDT`, `SOLUSDT` (Binance USDC-M Futures)
|
||||||
|
|
||||||
|
### Environment Variables (key ones)
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PG_HOST` | `127.0.0.1` | Local PG host |
|
||||||
|
| `PG_DB` | `arb_engine` | Database name |
|
||||||
|
| `PG_USER` / `PG_PASS` | `arb` / `arb_engine_2026` | PG credentials |
|
||||||
|
| `CLOUD_PG_HOST` | `10.106.0.3` | Cloud SQL host |
|
||||||
|
| `CLOUD_PG_ENABLED` | `true` | Enable dual-write |
|
||||||
|
| `JWT_SECRET` | (testnet default set) | JWT signing key |
|
||||||
|
| `TRADE_ENV` | `testnet` | `testnet` or `production` |
|
||||||
|
| `LIVE_STRATEGIES` | `["v52_8signals"]` | Active live trading strategies |
|
||||||
|
| `RISK_PER_TRADE_USD` | `2` | USD risk per trade |
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
- **External API**: Binance USDC-M Futures REST (`https://fapi.binance.com/fapi/v1`) and WebSocket.
|
||||||
|
- **Discord**: Webhook for signal notifications (via `signal_pusher.py`).
|
||||||
|
- **CORS origins**: `https://arb.zhouyangclaw.com`, `http://localhost:3000`, `http://localhost:3001`.
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] PM2 `ecosystem.dev.config.js` not read in this pass; exact process restart policies and env injection not confirmed.
|
||||||
|
- [inference] `.env` file usage confirmed via `python-dotenv` calls in live modules, but `.env.example` absent.
|
||||||
|
- [unknown] Deployment pipeline (CI/CD) not present in repo.
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/main.py:1-27` — FastAPI app init, CORS, SYMBOLS
|
||||||
|
- `backend/signal_engine.py:1-16` — V5 architecture docstring
|
||||||
|
- `backend/live_executor.py:1-10` — live executor architecture comment
|
||||||
|
- `backend/risk_guard.py:1-12` — risk guard circuit-break rules
|
||||||
|
- `backend/db.py:14-30` — PG/Cloud SQL env config
|
||||||
|
- `frontend/package.json` — frontend dependencies
|
||||||
|
- `frontend/lib/api.ts:1-116` — typed API client
|
||||||
137
docs/ai/01-architecture-map.md
Normal file
137
docs/ai/01-architecture-map.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: standard
|
||||||
|
---
|
||||||
|
|
||||||
|
# 01 — Architecture Map
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Describes the architecture style, component relationships, data flow, and runtime execution topology of the arbitrage engine.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- **Multi-process architecture**: each concern is a separate PM2 process; they communicate exclusively through PostgreSQL (tables + NOTIFY/LISTEN).
|
||||||
|
- **No message broker**: PostgreSQL serves as both the data store and the inter-process message bus (`NOTIFY new_signal`).
|
||||||
|
- **Dual-database write**: every PG write in `signal_engine.py` and `agg_trades_collector.py` attempts a secondary write to Cloud SQL (GCP) for durability.
|
||||||
|
- **FastAPI is read-only at runtime**: it proxies Binance REST for rates/history and reads the PG tables written by workers; it does not control the signal engine.
|
||||||
|
- **Signal pipeline**: raw aggTrades → in-memory rolling windows (CVD/VWAP/ATR) → scored signal → PG write + `NOTIFY` → live_executor executes Binance order.
|
||||||
|
- **Frontend polling**: React SPA polls `/api/rates` every 2 s (public) and slow endpoints every 120 s (auth required).
|
||||||
|
- **Risk guard is a separate process**: polls every 5 s, can block new orders (circuit-break) by writing a flag to `live_config`; live_executor reads that flag before each trade.
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### Architecture Style
|
||||||
|
Shared-DB multi-process monolith. No microservices; no message broker. All processes run on a single GCP VM.
|
||||||
|
|
||||||
|
### Component Diagram (text)
|
||||||
|
```
|
||||||
|
Binance WS (aggTrades)
|
||||||
|
└─► agg_trades_collector.py ──────────────────► agg_trades (partitioned table)
|
||||||
|
│
|
||||||
|
Binance WS (market data) ▼
|
||||||
|
└─► market_data_collector.py ──────────────► market_indicators table
|
||||||
|
│
|
||||||
|
Binance WS (liquidations) ▼
|
||||||
|
└─► liquidation_collector.py ──────────────► liquidation tables
|
||||||
|
│
|
||||||
|
signal_engine.py
|
||||||
|
(15 s loop, reads agg_trades +
|
||||||
|
market_indicators)
|
||||||
|
│
|
||||||
|
┌─────────┴──────────────┐
|
||||||
|
│ │
|
||||||
|
signal_indicators paper_trades
|
||||||
|
signal_indicators_1m (paper mode)
|
||||||
|
signal_trades
|
||||||
|
NOTIFY new_signal
|
||||||
|
│
|
||||||
|
live_executor.py
|
||||||
|
(LISTEN new_signal →
|
||||||
|
Binance Futures API)
|
||||||
|
│
|
||||||
|
live_trades table
|
||||||
|
│
|
||||||
|
risk_guard.py (5 s)
|
||||||
|
monitors live_trades,
|
||||||
|
writes live_config flags
|
||||||
|
│
|
||||||
|
signal_pusher.py
|
||||||
|
(reads signal_indicators →
|
||||||
|
Discord webhook)
|
||||||
|
│
|
||||||
|
FastAPI main.py (read/proxy)
|
||||||
|
+ rate_snapshots (2 s write)
|
||||||
|
│
|
||||||
|
Next.js Frontend
|
||||||
|
(polling SPA)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow — Signal Pipeline
|
||||||
|
1. `agg_trades_collector.py`: streams `aggTrade` WS events for all symbols, batch-inserts into `agg_trades` partitioned table (partitioned by month on `time_ms`).
|
||||||
|
2. `signal_engine.py` (15 s loop per symbol):
|
||||||
|
- Cold-start: reads last N rows from `agg_trades` to warm up `TradeWindow` (CVD, VWAP) and `ATRCalculator` deques.
|
||||||
|
- Fetches new trades since `last_agg_id`.
|
||||||
|
- Feeds trades into three `TradeWindow` instances (30 m, 4 h, 24 h) and one `ATRCalculator` (5 m candles, 14-period).
|
||||||
|
- Reads `market_indicators` for long-short ratio, OI, coinbase premium, funding rate, liquidations.
|
||||||
|
- Scores signal using JSON strategy config weights (score 0–100, threshold 75).
|
||||||
|
- Writes to `signal_indicators` (15 s cadence) and `signal_indicators_1m` (1 m cadence).
|
||||||
|
- If score ≥ threshold: opens paper trade (if enabled), emits `NOTIFY new_signal` (if live enabled).
|
||||||
|
3. `live_executor.py`: `LISTEN new_signal` on PG; deserializes payload; calls Binance Futures REST to place market order; writes to `live_trades`.
|
||||||
|
4. `risk_guard.py`: every 5 s checks daily loss, consecutive losses, unrealized PnL, balance, data freshness, hold timeout; sets `live_config.circuit_break` flag to block/resume new orders.
|
||||||
|
|
||||||
|
### Strategy Scoring (V5.x)
|
||||||
|
Two JSON configs in `backend/strategies/`:
|
||||||
|
|
||||||
|
| Config | Version | Threshold | Signals |
|
||||||
|
|--------|---------|-----------|---------|
|
||||||
|
| `v51_baseline.json` | 5.1 | 75 | cvd, p99, accel, ls_ratio, oi, coinbase_premium |
|
||||||
|
| `v52_8signals.json` | 5.2 | 75 | cvd, p99, accel, ls_ratio, oi, coinbase_premium, funding_rate, liquidation |
|
||||||
|
|
||||||
|
Score categories: `direction` (CVD), `crowding` (P99 large trades), `environment` (ATR/VWAP), `confirmation` (LS ratio, OI), `auxiliary` (coinbase premium), `funding_rate`, `liquidation`.
|
||||||
|
|
||||||
|
### TP/SL Configuration
|
||||||
|
- V5.1: SL=1.4×ATR, TP1=1.05×ATR, TP2=2.1×ATR
|
||||||
|
- V5.2: SL=2.1×ATR, TP1=1.4×ATR, TP2=3.15×ATR
|
||||||
|
- Signal cooldown: 10 minutes per symbol per direction.
|
||||||
|
|
||||||
|
### Risk Guard Circuit-Break Rules
|
||||||
|
| Rule | Threshold | Action |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Daily loss | -5R | Full close + shutdown |
|
||||||
|
| Consecutive losses | 5 | Pause 60 min |
|
||||||
|
| API disconnect | >30 s | Pause new orders |
|
||||||
|
| Balance too low | < risk×2 | Reject new orders |
|
||||||
|
| Data stale | >30 s | Block new orders |
|
||||||
|
| Hold timeout yellow | 45 min | Alert |
|
||||||
|
| Hold timeout auto-close | 70 min | Force close |
|
||||||
|
|
||||||
|
### Frontend Architecture
|
||||||
|
- **Next.js App Router** (`frontend/app/`): page-per-route, all pages are client components (`"use client"`).
|
||||||
|
- **Auth**: JWT stored in `localStorage`; `lib/auth.tsx` provides `useAuth()` hook + `authFetch()` helper with auto-refresh.
|
||||||
|
- **API client**: `lib/api.ts` — typed wrapper, distinguishes public (`/api/rates`, `/api/health`) from protected (all other) endpoints.
|
||||||
|
- **Polling strategy**: rates every 2 s, slow data (stats, history, signals) every 120 s; kline charts re-render every 30 s.
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
- PG NOTIFY channel name: `new_signal`
|
||||||
|
- `live_config` table keys: `risk_per_trade_usd`, `max_positions`, `circuit_break` (inferred)
|
||||||
|
- `market_indicators` populated by `market_data_collector.py` with types: `long_short_ratio`, `top_trader_position`, `open_interest_hist`, `coinbase_premium`, `funding_rate`
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] PM2 config (`ecosystem.dev.config.js`) not read; exact restart/watch/env-file settings unknown.
|
||||||
|
- [inference] `signal_pusher.py` exact Discord webhook configuration (env var name, rate limit handling) not confirmed.
|
||||||
|
- [unknown] Cloud SQL write failure does not block signal_engine but may create data divergence between local PG and Cloud SQL.
|
||||||
|
- [risk] Hardcoded testnet credentials in source code (`arb_engine_2026`); production requires explicit env var override.
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/signal_engine.py:1-16` — architecture docstring
|
||||||
|
- `backend/live_executor.py:1-10` — executor architecture comment
|
||||||
|
- `backend/risk_guard.py:1-12, 55-73` — risk rules and config
|
||||||
|
- `backend/signal_engine.py:170-245` — `TradeWindow`, `ATRCalculator` classes
|
||||||
|
- `backend/signal_engine.py:44-67` — strategy config loading
|
||||||
|
- `backend/strategies/v51_baseline.json`, `backend/strategies/v52_8signals.json`
|
||||||
|
- `backend/main.py:61-83` — background snapshot loop
|
||||||
|
- `frontend/lib/api.ts:103-116` — API client methods
|
||||||
|
- `frontend/app/page.tsx:149-154` — polling intervals
|
||||||
218
docs/ai/02-module-cheatsheet.md
Normal file
218
docs/ai/02-module-cheatsheet.md
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: standard
|
||||||
|
---
|
||||||
|
|
||||||
|
# 02 — Module Cheatsheet
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Module-by-module index: file path, role, key public interfaces, and dependencies.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- Backend has 20 Python modules; signal_engine.py is the largest and most complex (~1000+ lines).
|
||||||
|
- Frontend has 2 TypeScript lib files + 9 pages + 6 components.
|
||||||
|
- `db.py` is the only shared infrastructure module; all other backend modules import from it.
|
||||||
|
- `signal_engine.py` is the core business logic module; `live_executor.py` and `risk_guard.py` are independent processes that only use `db.py` and direct PG connections.
|
||||||
|
- Strategy configs are external JSON; no code changes needed to tune weights/thresholds.
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### Backend Modules
|
||||||
|
|
||||||
|
#### `backend/main.py` — FastAPI HTTP API
|
||||||
|
- **Role**: Primary HTTP API server; rate/snapshot/history proxy; aggTrade query endpoints; signal history.
|
||||||
|
- **Key interfaces**:
|
||||||
|
- `GET /api/health` — liveness check (public)
|
||||||
|
- `GET /api/rates` — live Binance premiumIndex for 4 symbols (public, 3 s cache)
|
||||||
|
- `GET /api/snapshots` — rate snapshot history from PG (auth required)
|
||||||
|
- `GET /api/kline` — candlestick bars aggregated from `rate_snapshots` (auth required)
|
||||||
|
- `GET /api/stats` — 7-day funding rate stats per symbol (auth required, 60 s cache)
|
||||||
|
- `GET /api/stats/ytd` — YTD annualized stats (auth required, 3600 s cache)
|
||||||
|
- `GET /api/history` — 7-day raw funding rate history (auth required, 60 s cache)
|
||||||
|
- `GET /api/signals/history` — `signal_logs` table (auth required)
|
||||||
|
- `GET /api/trades/meta` — `agg_trades_meta` (auth required)
|
||||||
|
- `GET /api/trades/summary` — aggregated OHLCV from `agg_trades` (auth required)
|
||||||
|
- Many more: paper trades, signals v52, live trades, live config, position sync, etc. (full list in saved tool output)
|
||||||
|
- **Deps**: `auth.py`, `db.py`, `httpx`, `asyncio`
|
||||||
|
- **Background task**: `background_snapshot_loop()` writes `rate_snapshots` every 2 s.
|
||||||
|
|
||||||
|
#### `backend/signal_engine.py` — V5 Signal Engine (PM2 worker)
|
||||||
|
- **Role**: Core signal computation loop; 15 s interval; in-memory rolling-window indicators; scored signal output.
|
||||||
|
- **Key classes**:
|
||||||
|
- `TradeWindow(window_ms)` — rolling CVD/VWAP calculator using `deque`; props: `cvd`, `vwap`
|
||||||
|
- `ATRCalculator(period_ms, length)` — 5-min candle ATR; props: `atr`, `atr_percentile`
|
||||||
|
- `SymbolState` — per-symbol state container holding `TradeWindow` ×3, `ATRCalculator`, large-order percentile deques
|
||||||
|
- **Key functions**:
|
||||||
|
- `load_strategy_configs() -> list[dict]` — reads JSON files from `strategies/`
|
||||||
|
- `fetch_market_indicators(symbol) -> dict` — reads `market_indicators` table
|
||||||
|
- `fetch_new_trades(symbol, last_id) -> list` — reads new rows from `agg_trades`
|
||||||
|
- `save_indicator(ts, symbol, result, strategy)` — writes to `signal_indicators`
|
||||||
|
- `paper_open_trade(...)` — inserts `paper_trades` row
|
||||||
|
- `paper_check_positions(symbol, price, now_ms)` — checks TP/SL for paper positions
|
||||||
|
- `main()` — entry point; calls `load_historical()` then enters main loop
|
||||||
|
- **Deps**: `db.py`, `json`, `collections.deque`
|
||||||
|
|
||||||
|
#### `backend/live_executor.py` — Live Trade Executor (PM2 worker)
|
||||||
|
- **Role**: Listens on PG `NOTIFY new_signal`; places Binance Futures market orders; writes `live_trades`.
|
||||||
|
- **Key functions**:
|
||||||
|
- `reload_live_config(conn)` — refreshes `RISK_PER_TRADE_USD`, `MAX_POSITIONS` from `live_config` every 60 s
|
||||||
|
- `binance_request(session, method, path, params)` — HMAC-signed Binance API call
|
||||||
|
- **Config**: `TRADE_ENV` (`testnet`/`production`), `LIVE_STRATEGIES`, `RISK_PER_TRADE_USD`, `MAX_POSITIONS`
|
||||||
|
- **Deps**: `psycopg2`, `aiohttp`, HMAC signing
|
||||||
|
|
||||||
|
#### `backend/risk_guard.py` — Risk Circuit-Breaker (PM2 worker)
|
||||||
|
- **Role**: Every 5 s; monitors PnL, balance, data freshness, hold timeouts; writes circuit-break flags.
|
||||||
|
- **Key classes**: `RiskState` — holds `status` (`normal`/`warning`/`circuit_break`), loss counters
|
||||||
|
- **Key functions**:
|
||||||
|
- `check_daily_loss(conn)` — sums `pnl_r` from today's `live_trades`
|
||||||
|
- `check_unrealized_loss(session, risk_usd_dynamic)` — queries Binance positions API
|
||||||
|
- `check_balance(session)` — queries Binance account balance
|
||||||
|
- `check_data_freshness(conn)` — checks `market_indicators` recency
|
||||||
|
- `check_hold_timeout(session, conn)` — force-closes positions held >70 min
|
||||||
|
- `trigger_circuit_break(session, conn, reason, action)` — writes to `live_events`, may flat positions
|
||||||
|
- `check_auto_resume()` — re-enables trading after cooldown
|
||||||
|
- `check_emergency_commands(session, conn)` — watches for manual DB commands
|
||||||
|
- **Deps**: `trade_config.py`, `aiohttp`, `psycopg2`
|
||||||
|
|
||||||
|
#### `backend/db.py` — Database Layer
|
||||||
|
- **Role**: All PG connectivity; schema creation; partition management.
|
||||||
|
- **Key interfaces**:
|
||||||
|
- Sync (psycopg2): `get_sync_conn()`, `sync_execute()`, `sync_executemany()`
|
||||||
|
- Async (asyncpg): `async_fetch()`, `async_fetchrow()`, `async_execute()`
|
||||||
|
- Cloud SQL sync pool: `get_cloud_sync_conn()` (non-fatal on failure)
|
||||||
|
- `init_schema()` — creates all tables from `SCHEMA_SQL`
|
||||||
|
- `ensure_partitions()` — creates `agg_trades_YYYYMM` partitions for current+next 2 months
|
||||||
|
- **Deps**: `asyncpg`, `psycopg2`
|
||||||
|
|
||||||
|
#### `backend/auth.py` — JWT Auth + Registration
|
||||||
|
- **Role**: FastAPI router at `/api`; register/login/refresh/logout endpoints.
|
||||||
|
- **Key interfaces**:
|
||||||
|
- `POST /api/register` — invite-code gated registration
|
||||||
|
- `POST /api/login` — returns `access_token` + `refresh_token`
|
||||||
|
- `POST /api/refresh` — token refresh
|
||||||
|
- `POST /api/logout` — revokes refresh token
|
||||||
|
- `GET /api/me` — current user info
|
||||||
|
- `get_current_user` — FastAPI `Depends` injector; validates Bearer JWT
|
||||||
|
- **Token storage**: HMAC-SHA256 hand-rolled JWT (no PyJWT); refresh tokens stored in `refresh_tokens` table.
|
||||||
|
- **Deps**: `db.py`, `hashlib`, `hmac`, `secrets`
|
||||||
|
|
||||||
|
#### `backend/agg_trades_collector.py` — AggTrades Collector (PM2 worker)
|
||||||
|
- **Role**: Streams Binance `aggTrade` WebSocket events; batch-inserts into `agg_trades` partitioned table; maintains `agg_trades_meta`.
|
||||||
|
- **Key functions**: `ws_collect(symbol)`, `rest_catchup(symbol, from_id)`, `continuity_check()`, `flush_buffer(symbol, trades)`
|
||||||
|
- **Deps**: `db.py`, `websockets`/`httpx`
|
||||||
|
|
||||||
|
#### `backend/market_data_collector.py` — Market Data Collector (PM2 worker)
|
||||||
|
- **Role**: Collects Binance market indicators (LS ratio, OI, coinbase premium, funding rate) via REST polling; stores in `market_indicators` JSONB.
|
||||||
|
- **Key class**: `MarketDataCollector`
|
||||||
|
- **Deps**: `db.py`, `httpx`
|
||||||
|
|
||||||
|
#### `backend/liquidation_collector.py` — Liquidation Collector (PM2 worker)
|
||||||
|
- **Role**: Streams Binance liquidation WS; aggregates into `liquidation_events` and `liquidation_agg` tables.
|
||||||
|
- **Key functions**: `ensure_table()`, `save_liquidation()`, `save_aggregated()`, `run()`
|
||||||
|
- **Deps**: `db.py`, `websockets`
|
||||||
|
|
||||||
|
#### `backend/backtest.py` — Offline Backtester
|
||||||
|
- **Role**: Replays `agg_trades` from PG to simulate signal engine and measure strategy performance.
|
||||||
|
- **Key classes**: `Position`, `BacktestEngine`
|
||||||
|
- **Key functions**: `load_trades()`, `run_backtest()`, `main()`
|
||||||
|
- **Deps**: `db.py`
|
||||||
|
|
||||||
|
#### `backend/trade_config.py` — Symbol / Qty Config
|
||||||
|
- **Role**: Constants for symbols and Binance qty precision.
|
||||||
|
- **Deps**: none
|
||||||
|
|
||||||
|
#### `backend/admin_cli.py` — Admin CLI
|
||||||
|
- **Role**: CLI for invite-code and user management (gen_invite, list_invites, ban_user, set_admin).
|
||||||
|
- **Deps**: `db.py`
|
||||||
|
|
||||||
|
#### `backend/subscriptions.py` — Subscription Query Helper
|
||||||
|
- **Role**: Helpers for querying signal history (used internally).
|
||||||
|
- **Deps**: `db.py`
|
||||||
|
|
||||||
|
#### `backend/paper_monitor.py` — Paper Trade Monitor
|
||||||
|
- **Role**: Standalone script to print paper trade status.
|
||||||
|
- **Deps**: `db.py`
|
||||||
|
|
||||||
|
#### `backend/signal_pusher.py` — Discord Notifier (PM2 worker)
|
||||||
|
- **Role**: Polls `signal_indicators` for high-score events; pushes Discord webhook notifications.
|
||||||
|
- **Deps**: `db.py`, `httpx`
|
||||||
|
|
||||||
|
#### `backend/position_sync.py` — Position Sync
|
||||||
|
- **Role**: Syncs live positions between `live_trades` table and Binance account state.
|
||||||
|
- **Deps**: `db.py`, `aiohttp`
|
||||||
|
|
||||||
|
#### `backend/fix_historical_pnl.py` — PnL Fix Script
|
||||||
|
- **Role**: One-time migration to recalculate historical PnL in `paper_trades`.
|
||||||
|
- **Deps**: `db.py`
|
||||||
|
|
||||||
|
### Frontend Modules
|
||||||
|
|
||||||
|
#### `frontend/lib/api.ts` — API Client
|
||||||
|
- **Role**: Typed `api` object with all backend endpoint wrappers; distinguishes public vs. protected fetches.
|
||||||
|
- **Interfaces exported**: `RateData`, `RatesResponse`, `HistoryPoint`, `HistoryResponse`, `StatsResponse`, `SignalHistoryItem`, `SnapshotItem`, `KBar`, `KlineResponse`, `YtdStatsResponse`, `api` object
|
||||||
|
- **Deps**: `lib/auth.tsx` (`authFetch`)
|
||||||
|
|
||||||
|
#### `frontend/lib/auth.tsx` — Auth Context
|
||||||
|
- **Role**: React context for current user; `useAuth()` hook; `authFetch()` with access-token injection and auto-refresh.
|
||||||
|
- **Deps**: Next.js router, `localStorage`
|
||||||
|
|
||||||
|
#### `frontend/app/` Pages
|
||||||
|
| Page | Route | Description |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| `page.tsx` | `/` | Main dashboard: rates, kline, history, signal log |
|
||||||
|
| `dashboard/page.tsx` | `/dashboard` | (inferred) extended dashboard |
|
||||||
|
| `signals/page.tsx` | `/signals` | Signal history view (V5.1) |
|
||||||
|
| `signals-v52/page.tsx` | `/signals-v52` | Signal history view (V5.2) |
|
||||||
|
| `paper/page.tsx` | `/paper` | Paper trades view (V5.1) |
|
||||||
|
| `paper-v52/page.tsx` | `/paper-v52` | Paper trades view (V5.2) |
|
||||||
|
| `live/page.tsx` | `/live` | Live trades view |
|
||||||
|
| `history/page.tsx` | `/history` | Funding rate history |
|
||||||
|
| `kline/page.tsx` | `/kline` | Kline chart page |
|
||||||
|
| `trades/page.tsx` | `/trades` | aggTrades summary |
|
||||||
|
| `server/page.tsx` | `/server` | Server status / metrics |
|
||||||
|
| `about/page.tsx` | `/about` | About page |
|
||||||
|
| `login/page.tsx` | `/login` | Login form |
|
||||||
|
| `register/page.tsx` | `/register` | Registration form |
|
||||||
|
|
||||||
|
#### `frontend/components/`
|
||||||
|
| Component | Role |
|
||||||
|
|-----------|------|
|
||||||
|
| `Navbar.tsx` | Top navigation bar |
|
||||||
|
| `Sidebar.tsx` | Sidebar navigation |
|
||||||
|
| `AuthHeader.tsx` | Auth-aware header with user info |
|
||||||
|
| `RateCard.tsx` | Displays current funding rate for one asset |
|
||||||
|
| `StatsCard.tsx` | Displays 7d mean and annualized stats |
|
||||||
|
| `FundingChart.tsx` | Funding rate chart component |
|
||||||
|
| `LiveTradesCard.tsx` | Live trades summary card |
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
|
||||||
|
### Key import graph (backend)
|
||||||
|
```
|
||||||
|
main.py → auth.py, db.py
|
||||||
|
signal_engine.py → db.py
|
||||||
|
live_executor.py → psycopg2 direct (no db.py module import)
|
||||||
|
risk_guard.py → trade_config.py, psycopg2 direct
|
||||||
|
backtest.py → db.py
|
||||||
|
agg_trades_collector.py → db.py
|
||||||
|
market_data_collector.py → db.py
|
||||||
|
liquidation_collector.py → db.py
|
||||||
|
admin_cli.py → db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] Content of `frontend/app/dashboard/`, `signals/`, `paper/`, `live/` pages not read; role described from filename convention.
|
||||||
|
- [unknown] `signal_pusher.py` Discord webhook env var name not confirmed.
|
||||||
|
- [inference] `position_sync.py` exact interface not read; role inferred from name and listing.
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/main.py` — all API route definitions
|
||||||
|
- `backend/signal_engine.py:170-285` — `TradeWindow`, `ATRCalculator`, `SymbolState`
|
||||||
|
- `backend/auth.py:23-23` — router prefix `/api`
|
||||||
|
- `backend/db.py:35-157` — all public DB functions
|
||||||
|
- `frontend/lib/api.ts:103-116` — `api` export object
|
||||||
|
- `frontend/lib/auth.tsx` — auth context (not fully read)
|
||||||
242
docs/ai/03-api-contracts.md
Normal file
242
docs/ai/03-api-contracts.md
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: standard
|
||||||
|
---
|
||||||
|
|
||||||
|
# 03 — API Contracts
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Documents all REST API endpoints, authentication requirements, request/response shapes, and error conventions.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- Base URL: `https://arb.zhouyangclaw.com` (prod) or `http://localhost:8000` (local).
|
||||||
|
- Auth: Bearer JWT in `Authorization` header. Two public endpoints (`/api/health`, `/api/rates`) need no token.
|
||||||
|
- Token lifecycle: access token 24 h, refresh token 7 d; use `POST /api/refresh` to renew.
|
||||||
|
- Registration is invite-code gated: must supply a valid `invite_code` in register body.
|
||||||
|
- All timestamps are Unix epoch (seconds or ms depending on field; see per-endpoint notes).
|
||||||
|
- Funding rates are stored as decimals (e.g. `0.0001` = 0.01%). Frontend multiplies by 10000 for "万分之" display.
|
||||||
|
- Error responses: standard FastAPI `{"detail": "..."}` with appropriate HTTP status codes.
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
#### `POST /api/register`
|
||||||
|
```json
|
||||||
|
// Request
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "...",
|
||||||
|
"invite_code": "XXXX"
|
||||||
|
}
|
||||||
|
// Response 200
|
||||||
|
{
|
||||||
|
"access_token": "<jwt>",
|
||||||
|
"refresh_token": "<token>",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": { "id": 1, "email": "...", "role": "user" }
|
||||||
|
}
|
||||||
|
// Errors: 400 (invite invalid/expired), 409 (email taken)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `POST /api/login`
|
||||||
|
```json
|
||||||
|
// Request
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "..."
|
||||||
|
}
|
||||||
|
// Response 200
|
||||||
|
{
|
||||||
|
"access_token": "<jwt>",
|
||||||
|
"refresh_token": "<token>",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": { "id": 1, "email": "...", "role": "user" }
|
||||||
|
}
|
||||||
|
// Errors: 401 (invalid credentials), 403 (banned)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `POST /api/refresh`
|
||||||
|
```json
|
||||||
|
// Request
|
||||||
|
{ "refresh_token": "<token>" }
|
||||||
|
// Response 200
|
||||||
|
{ "access_token": "<new_jwt>", "token_type": "bearer" }
|
||||||
|
// Errors: 401 (expired/revoked)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `POST /api/logout`
|
||||||
|
```json
|
||||||
|
// Request header: Authorization: Bearer <access_token>
|
||||||
|
// Request body: { "refresh_token": "<token>" }
|
||||||
|
// Response 200: { "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/me`
|
||||||
|
```json
|
||||||
|
// Auth required
|
||||||
|
// Response 200
|
||||||
|
{ "id": 1, "email": "...", "role": "user", "created_at": "..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public Endpoints (no auth)
|
||||||
|
|
||||||
|
#### `GET /api/health`
|
||||||
|
```json
|
||||||
|
{ "status": "ok", "timestamp": "2026-03-03T12:00:00" }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/rates`
|
||||||
|
Returns live Binance premiumIndex for BTCUSDT, ETHUSDT, XRPUSDT, SOLUSDT. Cached 3 s.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BTC": {
|
||||||
|
"symbol": "BTCUSDT",
|
||||||
|
"markPrice": 65000.0,
|
||||||
|
"indexPrice": 64990.0,
|
||||||
|
"lastFundingRate": 0.0001,
|
||||||
|
"nextFundingTime": 1234567890000,
|
||||||
|
"timestamp": 1234567890000
|
||||||
|
},
|
||||||
|
"ETH": { ... },
|
||||||
|
"XRP": { ... },
|
||||||
|
"SOL": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protected Endpoints (Bearer JWT required)
|
||||||
|
|
||||||
|
#### `GET /api/history`
|
||||||
|
7-day funding rate history from Binance. Cached 60 s.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BTC": [
|
||||||
|
{ "fundingTime": 1234567890000, "fundingRate": 0.0001, "timestamp": "2026-03-01T08:00:00" }
|
||||||
|
],
|
||||||
|
"ETH": [ ... ], "XRP": [ ... ], "SOL": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/stats`
|
||||||
|
7-day funding rate statistics. Cached 60 s.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BTC": { "mean7d": 0.01, "annualized": 10.95, "count": 21 },
|
||||||
|
"ETH": { ... },
|
||||||
|
"combo": { "mean7d": 0.009, "annualized": 9.85 }
|
||||||
|
}
|
||||||
|
// mean7d in %; annualized = mean * 3 * 365 * 100
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/stats/ytd`
|
||||||
|
Year-to-date annualized stats. Cached 3600 s.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BTC": { "annualized": 12.5, "count": 150 },
|
||||||
|
"ETH": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/snapshots?hours=24&limit=5000`
|
||||||
|
Rate snapshots from local PG.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 43200,
|
||||||
|
"hours": 24,
|
||||||
|
"data": [
|
||||||
|
{ "ts": 1709000000, "btc_rate": 0.0001, "eth_rate": 0.00008, "btc_price": 65000, "eth_price": 3200 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/kline?symbol=BTC&interval=1h&limit=500`
|
||||||
|
Candlestick bars derived from `rate_snapshots`. Rates scaled by ×10000.
|
||||||
|
- `interval`: `1m`, `5m`, `30m`, `1h`, `4h`, `8h`, `1d`, `1w`, `1M`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"symbol": "BTC",
|
||||||
|
"interval": "1h",
|
||||||
|
"count": 24,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"time": 1709000000,
|
||||||
|
"open": 1.0, "high": 1.2, "low": 0.8, "close": 1.1,
|
||||||
|
"price_open": 65000, "price_high": 65500, "price_low": 64800, "price_close": 65200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/signals/history?limit=100`
|
||||||
|
Legacy signal log from `signal_logs` table.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{ "id": 1, "symbol": "BTCUSDT", "rate": 0.0001, "annualized": 10.95, "sent_at": "2026-03-01T08:00:00", "message": "..." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/trades/meta`
|
||||||
|
aggTrades collection status.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BTC": { "last_agg_id": 123456789, "last_time_ms": 1709000000000, "updated_at": "2026-03-03 12:00:00" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/trades/summary?symbol=BTC&start_ms=0&end_ms=0&interval=1m`
|
||||||
|
Aggregated OHLCV from `agg_trades` via PG native aggregation.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"symbol": "BTC",
|
||||||
|
"interval": "1m",
|
||||||
|
"count": 60,
|
||||||
|
"data": [
|
||||||
|
{ "bar_ms": 1709000000000, "buy_vol": 10.5, "sell_vol": 9.3, "trade_count": 45, "vwap": 65000.0, "max_qty": 2.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Signal V52 Endpoints (inferred from frontend routes)
|
||||||
|
- `GET /api/signals/v52` — signals for v52_8signals strategy
|
||||||
|
- `GET /api/paper/trades` — paper trade history
|
||||||
|
- `GET /api/paper/trades/v52` — v52 paper trade history
|
||||||
|
- `GET /api/live/trades` — live trade history
|
||||||
|
- `GET /api/live/config` — current live config
|
||||||
|
- `GET /api/live/events` — live trading event log
|
||||||
|
- `GET /api/server/stats` — server process stats (psutil)
|
||||||
|
|
||||||
|
### Auth Header Format
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
Frontend auto-injects via `authFetch()` in `lib/auth.tsx`. On 401, attempts token refresh before retry.
|
||||||
|
|
||||||
|
### Error Shape
|
||||||
|
All errors follow FastAPI default:
|
||||||
|
```json
|
||||||
|
{ "detail": "Human-readable error message" }
|
||||||
|
```
|
||||||
|
Common HTTP status codes: 400 (bad request), 401 (unauthorized), 403 (forbidden/banned), 404 (not found), 422 (validation error), 502 (Binance upstream error).
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
- Binance USDC-M Futures REST: `https://fapi.binance.com/fapi/v1/premiumIndex`, `/fundingRate`
|
||||||
|
- CORS allowed origins: `https://arb.zhouyangclaw.com`, `http://localhost:3000`, `http://localhost:3001`
|
||||||
|
- `NEXT_PUBLIC_API_URL` env var controls the frontend base URL (empty = same-origin)
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] Full endpoint list for signals-v52, paper-v52, live, server pages not confirmed by reading main.py lines 300+. The full saved output contains more routes.
|
||||||
|
- [inference] `POST /api/register` exact field validation (password min length, etc.) not confirmed.
|
||||||
|
- [risk] No rate limiting visible on public endpoints; `/api/rates` with 3 s cache could be bypassed by direct calls.
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/main.py:101-298` — all confirmed REST endpoints
|
||||||
|
- `backend/auth.py:23` — auth router prefix
|
||||||
|
- `backend/main.py:16-21` — CORS config
|
||||||
|
- `frontend/lib/api.ts:90-116` — client-side API wrappers
|
||||||
|
- `frontend/lib/auth.tsx` — `authFetch` with auto-refresh (not fully read)
|
||||||
301
docs/ai/04-data-model.md
Normal file
301
docs/ai/04-data-model.md
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: standard
|
||||||
|
---
|
||||||
|
|
||||||
|
# 04 — Data Model
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Documents all PostgreSQL tables, columns, relations, constraints, storage design, and partitioning strategy.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- Single PostgreSQL database `arb_engine`; 15+ tables defined in `db.py` `SCHEMA_SQL` + `auth.py` `AUTH_SCHEMA`.
|
||||||
|
- `agg_trades` is a range-partitioned table (by `time_ms` in milliseconds); monthly partitions auto-created by `ensure_partitions()`.
|
||||||
|
- Dual-write: local PG is primary; Cloud SQL at `10.106.0.3` receives same writes via a secondary psycopg2 pool (non-fatal if down).
|
||||||
|
- All timestamps: `ts` columns are Unix seconds (integer); `time_ms` columns are Unix milliseconds (bigint); `created_at` columns are PG `TIMESTAMP`.
|
||||||
|
- JSONB used for `score_factors` in `paper_trades`/`live_trades`, `detail` in `live_events`, `value` in `market_indicators`.
|
||||||
|
- Auth tokens stored in DB: refresh tokens in `refresh_tokens` table (revocable); no session table.
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
#### `rate_snapshots` — Funding Rate Snapshots
|
||||||
|
Populated every 2 s by `background_snapshot_loop()` in `main.py`.
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `ts` | BIGINT NOT NULL | Unix seconds |
|
||||||
|
| `btc_rate` | DOUBLE PRECISION | BTC funding rate (decimal) |
|
||||||
|
| `eth_rate` | DOUBLE PRECISION | ETH funding rate |
|
||||||
|
| `btc_price` | DOUBLE PRECISION | BTC mark price USD |
|
||||||
|
| `eth_price` | DOUBLE PRECISION | ETH mark price USD |
|
||||||
|
| `btc_index_price` | DOUBLE PRECISION | BTC index price |
|
||||||
|
| `eth_index_price` | DOUBLE PRECISION | ETH index price |
|
||||||
|
|
||||||
|
Index: `idx_rate_snapshots_ts` on `ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `agg_trades` — Aggregate Trades (Partitioned)
|
||||||
|
Partitioned by `RANGE(time_ms)`; monthly child tables named `agg_trades_YYYYMM`.
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `agg_id` | BIGINT NOT NULL | Binance aggTrade ID |
|
||||||
|
| `symbol` | TEXT NOT NULL | e.g. `BTCUSDT` |
|
||||||
|
| `price` | DOUBLE PRECISION | Trade price |
|
||||||
|
| `qty` | DOUBLE PRECISION | Trade quantity (BTC/ETH/etc.) |
|
||||||
|
| `time_ms` | BIGINT NOT NULL | Trade timestamp ms |
|
||||||
|
| `is_buyer_maker` | SMALLINT | 0=taker buy, 1=taker sell |
|
||||||
|
|
||||||
|
PK: `(time_ms, symbol, agg_id)`.
|
||||||
|
Indexes: `idx_agg_trades_sym_time` on `(symbol, time_ms DESC)`, `idx_agg_trades_sym_agg` on `(symbol, agg_id)`.
|
||||||
|
|
||||||
|
Partitions auto-created for current + next 2 months. Named `agg_trades_YYYYMM`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `agg_trades_meta` — Collection State
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `symbol` | TEXT PK | e.g. `BTCUSDT` |
|
||||||
|
| `last_agg_id` | BIGINT | Last processed aggTrade ID |
|
||||||
|
| `last_time_ms` | BIGINT | Timestamp of last trade |
|
||||||
|
| `earliest_agg_id` | BIGINT | Oldest buffered ID |
|
||||||
|
| `earliest_time_ms` | BIGINT | Oldest buffered timestamp |
|
||||||
|
| `updated_at` | TEXT | Human-readable update time |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `signal_indicators` — Signal Engine Output (15 s cadence)
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `ts` | BIGINT | Unix seconds |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `cvd_fast` | DOUBLE PRECISION | CVD 30 m window |
|
||||||
|
| `cvd_mid` | DOUBLE PRECISION | CVD 4 h window |
|
||||||
|
| `cvd_day` | DOUBLE PRECISION | CVD UTC day |
|
||||||
|
| `cvd_fast_slope` | DOUBLE PRECISION | CVD momentum |
|
||||||
|
| `atr_5m` | DOUBLE PRECISION | ATR (5 m candles, 14 periods) |
|
||||||
|
| `atr_percentile` | DOUBLE PRECISION | ATR rank in 24 h history |
|
||||||
|
| `vwap_30m` | DOUBLE PRECISION | VWAP 30 m |
|
||||||
|
| `price` | DOUBLE PRECISION | Current mark price |
|
||||||
|
| `p95_qty` | DOUBLE PRECISION | P95 large-order threshold |
|
||||||
|
| `p99_qty` | DOUBLE PRECISION | P99 large-order threshold |
|
||||||
|
| `buy_vol_1m` | DOUBLE PRECISION | 1 m buy volume |
|
||||||
|
| `sell_vol_1m` | DOUBLE PRECISION | 1 m sell volume |
|
||||||
|
| `score` | INTEGER | Signal score 0–100 |
|
||||||
|
| `signal` | TEXT | `LONG`, `SHORT`, or null |
|
||||||
|
|
||||||
|
Indexes: `idx_si_ts`, `idx_si_sym_ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `signal_indicators_1m` — 1-Minute Signal Snapshot
|
||||||
|
Subset of `signal_indicators` columns; written at 1 m cadence for lightweight chart queries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `signal_trades` — Signal Engine Trade Tracking
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `ts_open` | BIGINT | Open timestamp (Unix s) |
|
||||||
|
| `ts_close` | BIGINT | Close timestamp |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `direction` | TEXT | `LONG` / `SHORT` |
|
||||||
|
| `entry_price` | DOUBLE PRECISION | |
|
||||||
|
| `exit_price` | DOUBLE PRECISION | |
|
||||||
|
| `qty` | DOUBLE PRECISION | |
|
||||||
|
| `score` | INTEGER | Signal score at entry |
|
||||||
|
| `pnl` | DOUBLE PRECISION | Realized PnL |
|
||||||
|
| `sl_price` | DOUBLE PRECISION | Stop-loss level |
|
||||||
|
| `tp1_price` | DOUBLE PRECISION | Take-profit 1 level |
|
||||||
|
| `tp2_price` | DOUBLE PRECISION | Take-profit 2 level |
|
||||||
|
| `status` | TEXT DEFAULT `open` | `open`, `closed`, `stopped` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `paper_trades` — Paper Trading Records
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `direction` | TEXT | `LONG`/`SHORT` |
|
||||||
|
| `score` | INT | Signal score |
|
||||||
|
| `tier` | TEXT | `light`/`standard`/`heavy` |
|
||||||
|
| `entry_price` | DOUBLE PRECISION | |
|
||||||
|
| `entry_ts` | BIGINT | Unix ms |
|
||||||
|
| `exit_price` | DOUBLE PRECISION | |
|
||||||
|
| `exit_ts` | BIGINT | |
|
||||||
|
| `tp1_price` | DOUBLE PRECISION | |
|
||||||
|
| `tp2_price` | DOUBLE PRECISION | |
|
||||||
|
| `sl_price` | DOUBLE PRECISION | |
|
||||||
|
| `tp1_hit` | BOOLEAN DEFAULT FALSE | |
|
||||||
|
| `status` | TEXT DEFAULT `active` | `active`, `tp1`, `tp2`, `sl`, `timeout` |
|
||||||
|
| `pnl_r` | DOUBLE PRECISION | PnL in R units |
|
||||||
|
| `atr_at_entry` | DOUBLE PRECISION | ATR snapshot at entry |
|
||||||
|
| `score_factors` | JSONB | Breakdown of signal score components |
|
||||||
|
| `strategy` | VARCHAR(32) DEFAULT `v51_baseline` | Strategy name |
|
||||||
|
| `created_at` | TIMESTAMP | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `live_trades` — Live Trading Records
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `strategy` | TEXT | |
|
||||||
|
| `direction` | TEXT | `LONG`/`SHORT` |
|
||||||
|
| `status` | TEXT DEFAULT `active` | |
|
||||||
|
| `entry_price` / `exit_price` | DOUBLE PRECISION | |
|
||||||
|
| `entry_ts` / `exit_ts` | BIGINT | Unix ms |
|
||||||
|
| `sl_price`, `tp1_price`, `tp2_price` | DOUBLE PRECISION | |
|
||||||
|
| `tp1_hit` | BOOLEAN | |
|
||||||
|
| `score` | DOUBLE PRECISION | |
|
||||||
|
| `tier` | TEXT | |
|
||||||
|
| `pnl_r` | DOUBLE PRECISION | |
|
||||||
|
| `fee_usdt` | DOUBLE PRECISION | Exchange fees |
|
||||||
|
| `funding_fee_usdt` | DOUBLE PRECISION | Funding fees paid while holding |
|
||||||
|
| `risk_distance` | DOUBLE PRECISION | Entry to SL distance |
|
||||||
|
| `atr_at_entry` | DOUBLE PRECISION | |
|
||||||
|
| `score_factors` | JSONB | |
|
||||||
|
| `signal_id` | BIGINT | FK → signal_indicators.id |
|
||||||
|
| `binance_order_id` | TEXT | Binance order ID |
|
||||||
|
| `fill_price` | DOUBLE PRECISION | Actual fill price |
|
||||||
|
| `slippage_bps` | DOUBLE PRECISION | Slippage in basis points |
|
||||||
|
| `protection_gap_ms` | BIGINT | Time between SL order and fill |
|
||||||
|
| `signal_to_order_ms` | BIGINT | Latency: signal → order placed |
|
||||||
|
| `order_to_fill_ms` | BIGINT | Latency: order → fill |
|
||||||
|
| `qty` | DOUBLE PRECISION | |
|
||||||
|
| `created_at` | TIMESTAMP | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `live_config` — Runtime Configuration KV Store
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `key` | TEXT PK | Config key |
|
||||||
|
| `value` | TEXT | Config value (string) |
|
||||||
|
| `label` | TEXT | Human label |
|
||||||
|
| `updated_at` | TIMESTAMP | |
|
||||||
|
|
||||||
|
Known keys: `risk_per_trade_usd`, `max_positions`, `circuit_break` (inferred).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `live_events` — Trade Event Log
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `ts` | BIGINT | Unix ms (default: NOW()) |
|
||||||
|
| `level` | TEXT | `info`/`warning`/`error` |
|
||||||
|
| `category` | TEXT | Event category |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `message` | TEXT | |
|
||||||
|
| `detail` | JSONB | Structured event data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `signal_logs` — Legacy Signal Log
|
||||||
|
Kept for backwards compatibility with the original funding-rate signal system.
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `id` | BIGSERIAL PK |
|
||||||
|
| `symbol` | TEXT |
|
||||||
|
| `rate` | DOUBLE PRECISION |
|
||||||
|
| `annualized` | DOUBLE PRECISION |
|
||||||
|
| `sent_at` | TEXT |
|
||||||
|
| `message` | TEXT |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Auth Tables (defined in `auth.py` AUTH_SCHEMA)
|
||||||
|
|
||||||
|
**`users`**
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `id` | BIGSERIAL PK |
|
||||||
|
| `email` | TEXT UNIQUE NOT NULL |
|
||||||
|
| `password_hash` | TEXT NOT NULL |
|
||||||
|
| `discord_id` | TEXT |
|
||||||
|
| `role` | TEXT DEFAULT `user` |
|
||||||
|
| `banned` | INTEGER DEFAULT 0 |
|
||||||
|
| `created_at` | TEXT |
|
||||||
|
|
||||||
|
**`subscriptions`**
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `user_id` | BIGINT PK → users |
|
||||||
|
| `tier` | TEXT DEFAULT `free` |
|
||||||
|
| `expires_at` | TEXT |
|
||||||
|
|
||||||
|
**`invite_codes`**
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `id` | BIGSERIAL PK |
|
||||||
|
| `code` | TEXT UNIQUE |
|
||||||
|
| `created_by` | INTEGER |
|
||||||
|
| `max_uses` | INTEGER DEFAULT 1 |
|
||||||
|
| `used_count` | INTEGER DEFAULT 0 |
|
||||||
|
| `status` | TEXT DEFAULT `active` |
|
||||||
|
| `expires_at` | TEXT |
|
||||||
|
|
||||||
|
**`invite_usage`**
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `id` | BIGSERIAL PK |
|
||||||
|
| `code_id` | BIGINT → invite_codes |
|
||||||
|
| `user_id` | BIGINT → users |
|
||||||
|
| `used_at` | TEXT |
|
||||||
|
|
||||||
|
**`refresh_tokens`**
|
||||||
|
| Column | Type |
|
||||||
|
|--------|------|
|
||||||
|
| `id` | BIGSERIAL PK |
|
||||||
|
| `user_id` | BIGINT → users |
|
||||||
|
| `token` | TEXT UNIQUE |
|
||||||
|
| `expires_at` | TEXT |
|
||||||
|
| `revoked` | INTEGER DEFAULT 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `market_indicators` — Market Indicator JSONB Store
|
||||||
|
Populated by `market_data_collector.py`.
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `indicator_type` | TEXT | `long_short_ratio`, `top_trader_position`, `open_interest_hist`, `coinbase_premium`, `funding_rate` |
|
||||||
|
| `timestamp_ms` | BIGINT | |
|
||||||
|
| `value` | JSONB | Raw indicator payload |
|
||||||
|
|
||||||
|
Query pattern: `WHERE symbol=? AND indicator_type=? ORDER BY timestamp_ms DESC LIMIT 1`.
|
||||||
|
|
||||||
|
### Storage Design Decisions
|
||||||
|
- **Partitioning**: `agg_trades` partitioned by month to avoid table bloat; partition maintenance is automated.
|
||||||
|
- **Dual-write**: Cloud SQL secondary is best-effort (errors logged, never fatal).
|
||||||
|
- **JSONB `score_factors`**: allows schema-free storage of per-strategy signal breakdowns without migrations.
|
||||||
|
- **Timestamps**: mix of Unix seconds (`ts`), Unix ms (`time_ms`, `timestamp_ms`, `entry_ts`), ISO strings (`created_at` TEXT in auth tables), and PG `TIMESTAMP`; be careful when querying across tables.
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
- `db.py:init_schema()` — creates all tables in `SCHEMA_SQL`
|
||||||
|
- `auth.py:ensure_tables()` — creates auth tables from `AUTH_SCHEMA`
|
||||||
|
- `db.py:ensure_partitions()` — auto-creates monthly `agg_trades_YYYYMM` partitions
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [unknown] `market_indicators` table schema not in `SCHEMA_SQL`; likely created by `market_data_collector.py` separately — verify before querying.
|
||||||
|
- [risk] Timestamp inconsistency: some tables use TEXT for timestamps (auth tables), others use BIGINT, others use PG TIMESTAMP — cross-table JOINs on time fields require explicit casting.
|
||||||
|
- [inference] `live_config` circuit-break key name not confirmed from source; inferred from `risk_guard.py` behavior.
|
||||||
|
- [risk] `users` table defined in both `SCHEMA_SQL` (db.py) and `AUTH_SCHEMA` (auth.py); duplicate CREATE TABLE IF NOT EXISTS; actual schema diverges between the two definitions (db.py version lacks `discord_id`, `banned`).
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/db.py:166-356` — `SCHEMA_SQL` with all table definitions
|
||||||
|
- `backend/auth.py:28-71` — `AUTH_SCHEMA` auth tables
|
||||||
|
- `backend/db.py:360-414` — `ensure_partitions()`, `init_schema()`
|
||||||
|
- `backend/signal_engine.py:123-158` — `market_indicators` query pattern
|
||||||
251
docs/ai/05-build-run-test.md
Normal file
251
docs/ai/05-build-run-test.md
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# 05 — Build, Run & Test
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
所有构建、运行、测试、部署相关命令及环境变量配置说明。
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- 无 CI/CD 流水线;手动部署到 GCP VM,PM2 管理进程。
|
||||||
|
- 后端无构建步骤,直接 `python3 main.py` 或 PM2 启动。
|
||||||
|
- 前端标准 Next.js:`npm run dev` / `npm run build` / `npm start`。
|
||||||
|
- 测试文件未发现;验证通过 backtest.py 回测和 paper trading 模拟盘进行。
|
||||||
|
- 本地开发:前端 `/api/*` 通过 Next.js rewrite 代理到 `http://127.0.0.1:4332`(即 uvicorn 端口)。
|
||||||
|
- 数据库 schema 自动在启动时初始化(`init_schema()`),无独立 migration 工具。
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
| 组件 | 要求 |
|
||||||
|
|------|------|
|
||||||
|
| Python | 3.10+(使用 `list[dict]` 等 3.10 语法) |
|
||||||
|
| Node.js | 推荐 20.x(package.json `@types/node: ^20`) |
|
||||||
|
| PostgreSQL | 本地实例 + Cloud SQL(`10.106.0.3`) |
|
||||||
|
| PM2 | 用于进程管理(需全局安装) |
|
||||||
|
|
||||||
|
### 后端依赖安装
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# requirements.txt 内容:fastapi, uvicorn, httpx, python-dotenv, psutil
|
||||||
|
# 实际还需要(从源码 import 推断):
|
||||||
|
# asyncpg, psycopg2-binary, aiohttp, websockets
|
||||||
|
```
|
||||||
|
|
||||||
|
> [inference] `requirements.txt` 内容不完整,仅列出 5 个包,但源码 import 了 `asyncpg`、`psycopg2`、`aiohttp` 等。运行前需确认完整依赖已安装。
|
||||||
|
|
||||||
|
### 后端启动
|
||||||
|
|
||||||
|
#### 单进程开发模式
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# FastAPI HTTP API(默认端口 4332,从 next.config.ts 推断)
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 4332 --reload
|
||||||
|
|
||||||
|
# 信号引擎(独立进程)
|
||||||
|
python3 signal_engine.py
|
||||||
|
|
||||||
|
# aggTrades 收集器
|
||||||
|
python3 agg_trades_collector.py
|
||||||
|
|
||||||
|
# 市场数据收集器
|
||||||
|
python3 market_data_collector.py
|
||||||
|
|
||||||
|
# 清算数据收集器
|
||||||
|
python3 liquidation_collector.py
|
||||||
|
|
||||||
|
# 实盘执行器
|
||||||
|
TRADE_ENV=testnet python3 live_executor.py
|
||||||
|
|
||||||
|
# 风控模块
|
||||||
|
TRADE_ENV=testnet python3 risk_guard.py
|
||||||
|
|
||||||
|
# 信号推送(Discord)
|
||||||
|
python3 signal_pusher.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PM2 生产模式
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 使用 ecosystem 配置(目前只定义了 arb-dev-signal)
|
||||||
|
pm2 start ecosystem.dev.config.js
|
||||||
|
|
||||||
|
# 查看进程状态
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
pm2 logs arb-dev-signal
|
||||||
|
|
||||||
|
# 停止所有
|
||||||
|
pm2 stop all
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
pm2 restart all
|
||||||
|
```
|
||||||
|
|
||||||
|
> [inference] `ecosystem.dev.config.js` 当前只配置了 `signal_engine.py`,其他进程需手动启动或添加到 PM2 配置。
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
#### 数据库(所有后端进程共用)
|
||||||
|
```bash
|
||||||
|
export PG_HOST=127.0.0.1 # 本地 PG
|
||||||
|
export PG_PORT=5432
|
||||||
|
export PG_DB=arb_engine
|
||||||
|
export PG_USER=arb
|
||||||
|
export PG_PASS=arb_engine_2026 # 测试网默认,生产需覆盖
|
||||||
|
|
||||||
|
export CLOUD_PG_HOST=10.106.0.3 # Cloud SQL
|
||||||
|
export CLOUD_PG_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 认证
|
||||||
|
```bash
|
||||||
|
export JWT_SECRET=<> # 生产环境必填,长度 ≥32
|
||||||
|
# 测试网有默认值 "arb-engine-jwt-secret-v2-2026",生产环境 TRADE_ENV != testnet 时必须设置
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 交易环境
|
||||||
|
```bash
|
||||||
|
export TRADE_ENV=testnet # 或 production
|
||||||
|
export LIVE_STRATEGIES='["v52_8signals"]'
|
||||||
|
export RISK_PER_TRADE_USD=2 # 每笔风险 USD
|
||||||
|
export MAX_POSITIONS=4 # 最大同时持仓数
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实盘专用(live_executor + risk_guard)
|
||||||
|
```bash
|
||||||
|
export DB_HOST=10.106.0.3
|
||||||
|
export DB_PASSWORD=<生产密码>
|
||||||
|
export DB_NAME=arb_engine
|
||||||
|
export DB_USER=arb
|
||||||
|
|
||||||
|
# 币安 API Key(需在 Binance 配置)
|
||||||
|
export BINANCE_API_KEY=<key>
|
||||||
|
export BINANCE_API_SECRET=<secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 前端
|
||||||
|
```bash
|
||||||
|
# .env.local 或部署环境
|
||||||
|
NEXT_PUBLIC_API_URL= # 留空=同源,生产时设为 https://arb.zhouyangclaw.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库初始化
|
||||||
|
```bash
|
||||||
|
# schema 在 FastAPI 启动时自动创建(init_schema + ensure_auth_tables)
|
||||||
|
# 手动初始化:
|
||||||
|
cd backend
|
||||||
|
python3 -c "from db import init_schema; init_schema()"
|
||||||
|
|
||||||
|
# 分区维护(自动在 init_schema 内调用):
|
||||||
|
python3 -c "from db import ensure_partitions; ensure_partitions()"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端构建与启动
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发模式(热重载,端口 3000)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 API 代理配置
|
||||||
|
`frontend/next.config.ts` 将 `/api/*` 代理到 `http://127.0.0.1:4332`。
|
||||||
|
- 本地开发时 uvicorn 需监听 **4332 端口**。
|
||||||
|
- 生产部署时通过 `NEXT_PUBLIC_API_URL` 或 nginx 反向代理处理跨域。
|
||||||
|
|
||||||
|
### 回测(离线验证)
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 指定天数回测
|
||||||
|
python3 backtest.py --symbol BTCUSDT --days 20
|
||||||
|
|
||||||
|
# 指定日期范围回测
|
||||||
|
python3 backtest.py --symbol BTCUSDT --start 2026-02-08 --end 2026-02-28
|
||||||
|
|
||||||
|
# 输出:胜率、盈亏比、夏普比率、最大回撤等统计
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模拟盘(Paper Trading)
|
||||||
|
通过 `paper_config.json` 控制:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"enabled_strategies": ["v52_8signals"],
|
||||||
|
"initial_balance": 10000,
|
||||||
|
"risk_per_trade": 0.02,
|
||||||
|
"max_positions": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
修改后 signal_engine 下次循环自动读取(无需重启)。
|
||||||
|
|
||||||
|
监控模拟盘:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 paper_monitor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理员 CLI
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 admin_cli.py gen_invite [count] [max_uses]
|
||||||
|
python3 admin_cli.py list_invites
|
||||||
|
python3 admin_cli.py disable_invite <code>
|
||||||
|
python3 admin_cli.py list_users
|
||||||
|
python3 admin_cli.py ban_user <user_id>
|
||||||
|
python3 admin_cli.py unban_user <user_id>
|
||||||
|
python3 admin_cli.py set_admin <user_id>
|
||||||
|
python3 admin_cli.py usage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志位置
|
||||||
|
| 进程 | 日志文件 |
|
||||||
|
|------|---------|
|
||||||
|
| signal_engine | `signal-engine.log`(项目根目录) |
|
||||||
|
| risk_guard | `backend/logs/risk_guard.log`(RotatingFileHandler,10MB×5) |
|
||||||
|
| 其他进程 | stdout / PM2 logs |
|
||||||
|
|
||||||
|
### 无测试框架
|
||||||
|
项目中未发现 `pytest`、`unittest` 或任何测试文件。验证策略依赖:
|
||||||
|
1. **回测**:`backtest.py` 逐 tick 回放历史数据
|
||||||
|
2. **模拟盘**:paper trading 实时验证信号质量
|
||||||
|
3. **手动测试**:前端页面人工验证
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
- uvicorn 端口:**4332**(从 `next.config.ts` 推断)
|
||||||
|
- 前端开发端口:**3000**(Next.js 默认)
|
||||||
|
- CORS 允许 `localhost:3000` 和 `localhost:3001`
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] uvicorn 端口 4332 从 `next.config.ts` 推断,未在 `main.py` 或启动脚本中显式确认。
|
||||||
|
- [inference] `requirements.txt` 不完整,实际依赖需从源码 import 语句归纳。
|
||||||
|
- [unknown] 生产部署的 nginx 配置未在仓库中。
|
||||||
|
- [risk] 无自动化测试,代码变更风险完全依赖人工回测和 paper trading 验证。
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `frontend/next.config.ts` — API rewrite 代理到 `127.0.0.1:4332`
|
||||||
|
- `backend/ecosystem.dev.config.js` — PM2 配置(仅 signal_engine)
|
||||||
|
- `backend/requirements.txt` — 后端依赖(不完整)
|
||||||
|
- `backend/backtest.py:1-13` — 回测用法说明
|
||||||
|
- `backend/paper_config.json` — 模拟盘配置
|
||||||
|
- `backend/admin_cli.py:88` — CLI usage 函数
|
||||||
|
- `backend/risk_guard.py:81-82` — 日志 RotatingFileHandler 配置
|
||||||
162
docs/ai/06-decision-log.md
Normal file
162
docs/ai/06-decision-log.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# 06 — Decision Log
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
记录项目中关键的技术决策、选型原因及权衡取舍(从代码注释和架构特征推断)。
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- 选择 PostgreSQL 作为唯一消息总线(NOTIFY/LISTEN),避免引入 Kafka/Redis 等额外组件。
|
||||||
|
- signal_engine 改为 15 秒循环(原 5 秒),CPU 降 60%,信号质量无影响。
|
||||||
|
- 双写 Cloud SQL 作为灾备,失败不阻断主流程。
|
||||||
|
- `agg_trades` 按月分区,避免单表过大影响查询性能。
|
||||||
|
- 认证采用自研 HMAC-SHA256 JWT,不依赖第三方库。
|
||||||
|
- 前端使用 Next.js App Router + 纯客户端轮询,不使用 WebSocket 推送。
|
||||||
|
- 策略参数外置为 JSON 文件,支持热修改无需重启进程。
|
||||||
|
- 信号评分采用多层加权体系(5层),每层独立可调,支持多策略并行。
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### 决策 1:PostgreSQL 作为进程间消息总线
|
||||||
|
**决策**:使用 PostgreSQL `NOTIFY/LISTEN` 在 signal_engine 和 live_executor 之间传递信号,而非 Redis pub/sub 或消息队列。
|
||||||
|
|
||||||
|
**原因**(从代码推断):
|
||||||
|
- 系统已强依赖 PG;避免引入新的基础设施依赖。
|
||||||
|
- 信号触发频率低(每 15 秒最多一次),PG NOTIFY 完全满足延迟要求。
|
||||||
|
- 信号 payload 直接写入 `signal_indicators` 表,NOTIFY 仅做触发通知,消费者可直接查表。
|
||||||
|
|
||||||
|
**取舍**:单点依赖 PG;PG 宕机时信号传递和持久化同时失败(可接受,因为两者本就强耦合)。
|
||||||
|
|
||||||
|
**来源**:`live_executor.py:1-10` 架构注释,`signal_engine.py:save_indicator` 函数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 2:信号引擎循环间隔从 5 秒改为 15 秒
|
||||||
|
**决策**:`LOOP_INTERVAL = 15`(原注释说明原值为 5)。
|
||||||
|
|
||||||
|
**原因**:代码注释明确写道 "CPU降60%,信号质量无影响"。
|
||||||
|
|
||||||
|
**取舍**:信号触发延迟最坏增加 10 秒;对于短线但非高频的策略(TP/SL 以 ATR 倍数计,通常 >1% 波动),10 秒的额外延迟影响可忽略不计。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:39` `LOOP_INTERVAL = 15 # 秒(从5改15,CPU降60%,信号质量无影响)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 3:agg_trades 表按月范围分区
|
||||||
|
**决策**:`agg_trades` 使用 `PARTITION BY RANGE(time_ms)`,按月创建子表(如 `agg_trades_202603`)。
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- aggTrades 是最大的写入表(每秒数百条),无分区会导致单表膨胀。
|
||||||
|
- 按月分区支持高效的时间范围查询(PG 分区裁剪)。
|
||||||
|
- 旧分区可独立归档或删除,不影响主表。
|
||||||
|
|
||||||
|
**取舍**:分区管理需要维护(`ensure_partitions()` 自动创建当月+未来2个月分区,需定期执行);跨分区查询性能取决于分区裁剪是否生效(`time_ms` 条件必须是常量)。
|
||||||
|
|
||||||
|
**来源**:`db.py:191-201, 360-393`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 4:Cloud SQL 双写(非阻塞)
|
||||||
|
**决策**:所有写入操作在本地 PG 成功后,尝试相同写入到 Cloud SQL(`10.106.0.3`),Cloud SQL 失败不影响主流程。
|
||||||
|
|
||||||
|
**原因**:提供数据异地备份;Cloud SQL 作为只读副本或灾备使用。
|
||||||
|
|
||||||
|
**取舍**:
|
||||||
|
- 本地 PG 和 Cloud SQL 可能出现数据不一致(local 成功 + cloud 失败)。
|
||||||
|
- 双写增加每次写操作的延迟(两个网络 RTT),但因为是 best-effort 且使用独立连接池,实际阻塞极少。
|
||||||
|
- live_executor 直接连 Cloud SQL(`DB_HOST=10.106.0.3`),绕过本地 PG。
|
||||||
|
|
||||||
|
**来源**:`db.py:23-29, 80-118`,`live_executor.py:50-55`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 5:自研 JWT(不用 PyJWT 等第三方库)
|
||||||
|
**决策**:使用 Python 标准库 `hmac`、`hashlib`、`base64` 手动实现 JWT 签发和验证。
|
||||||
|
|
||||||
|
**原因**(推断):减少依赖;JWT 结构相对简单,HMAC-SHA256 签名几十行代码即可实现。
|
||||||
|
|
||||||
|
**取舍**:
|
||||||
|
- 需要自行处理过期、revoke、refresh token 等逻辑(代码中已有 `refresh_tokens` 表)。
|
||||||
|
- 非标准实现可能在边界情况(时钟偏差、特殊字符等)上与标准库行为不同。
|
||||||
|
- 无 JWT 生态工具支持(调试工具、密钥轮转库等)。
|
||||||
|
|
||||||
|
**来源**:`auth.py:1-6`(import hashlib, secrets, hmac, base64, json),`auth.py:16-19`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 6:策略配置外置为 JSON 文件
|
||||||
|
**决策**:V5.x 策略的权重、阈值、TP/SL 倍数等参数存放在 `backend/strategies/*.json`,signal_engine 每次 `load_strategy_configs()` 读取。
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- 策略调优频繁(v51→v52 权重变化显著),外置避免每次改参数都要修改代码。
|
||||||
|
- 多策略并行:signal_engine 同时运行 v51_baseline 和 v52_8signals,对每个 symbol 分别评分。
|
||||||
|
- [inference] 支持未来通过前端或 API 修改策略参数而不重启进程(目前 signal_engine 每次循环重读文件 —— 需确认)。
|
||||||
|
|
||||||
|
**取舍**:JSON 文件无类型检查,配置错误在运行时才发现;缺少配置 schema 校验。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:41-67`,`backend/strategies/v51_baseline.json`,`backend/strategies/v52_8signals.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 7:信号评分采用五层加权体系
|
||||||
|
**决策**:信号评分分为 5 个独立层次(方向层、拥挤层、资金费率层、环境层、确认层、清算层、辅助层),每层有独立权重,总分 0~100,阈值 75 触发信号。
|
||||||
|
|
||||||
|
**设计特点**:
|
||||||
|
- 方向层(CVD)权重最高(V5.1: 45分,V5.2: 40分),是核心指标。
|
||||||
|
- "standard" 档位:score ≥ threshold(75);"heavy" 档位:score ≥ max(threshold+10, 85)。
|
||||||
|
- 信号冷却:同一 symbol 同一策略触发后 10 分钟内不再触发。
|
||||||
|
- CVD 快慢线需同向才产生完整方向信号;否则标记 `no_direction=True` 不触发。
|
||||||
|
|
||||||
|
**取舍**:权重缩放逻辑较复杂(各层原始满分不统一,需先归一化再乘权重);`market_indicators` 缺失时给默认中间分,保证系统在数据不完整时仍能运行。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:410-651`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 8:前端使用轮询而非 WebSocket
|
||||||
|
**决策**:React 前端对 `/api/rates` 每 2 秒轮询,慢速数据(stats/history/signals)每 120 秒轮询,K 线图每 30 秒刷新。
|
||||||
|
|
||||||
|
**原因**(推断):
|
||||||
|
- 实现简单,无需维护 WebSocket 连接状态和断线重连逻辑。
|
||||||
|
- 数据更新频率(2 秒/30 秒)对轮询友好;WebSocket 的优势在于毫秒级推送。
|
||||||
|
- FastAPI 已支持 WebSocket,但实现 SSE/WS 推送需要额外的后端状态管理。
|
||||||
|
|
||||||
|
**取舍**:每 2 秒轮询 `/api/rates` 会产生持续的服务器负载;当用户量增加时需要加缓存或换 WebSocket。
|
||||||
|
|
||||||
|
**来源**:`frontend/app/page.tsx:149-154`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 9:live_executor 和 risk_guard 直连 Cloud SQL
|
||||||
|
**决策**:`live_executor.py` 和 `risk_guard.py` 默认 `DB_HOST=10.106.0.3`(Cloud SQL),而不是本地 PG。
|
||||||
|
|
||||||
|
**原因**(推断):这两个进程运行在与 signal_engine 不同的环境(可能是另一台 GCP VM 或容器),直连 Cloud SQL 避免通过本地 PG 中转。
|
||||||
|
|
||||||
|
**取舍**:live_executor 和 signal_engine 使用不同的 PG 实例,理论上存在数据读取延迟(双写同步延迟)。
|
||||||
|
|
||||||
|
**来源**:`live_executor.py:50-55`,`risk_guard.py:47-53`
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
无额外接口依赖,均为内部架构决策。
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] 所有决策均从代码推断,无明确的 ADR(Architecture Decision Record)文档。
|
||||||
|
- [unknown] 策略配置是否支持热重载(signal_engine 是否每次循环都重读 JSON)未确认。
|
||||||
|
- [risk] 决策 4(双写)+ 决策 9(live executor 直连 Cloud SQL)组合下,若本地 PG 和 Cloud SQL 数据不一致,live_executor 可能读到滞后的信号或重复执行。
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/signal_engine.py:39` — LOOP_INTERVAL 注释
|
||||||
|
- `backend/signal_engine.py:44-67` — load_strategy_configs
|
||||||
|
- `backend/signal_engine.py:410-651` — evaluate_signal 完整评分逻辑
|
||||||
|
- `backend/db.py:23-29, 80-118` — Cloud SQL 双写连接池
|
||||||
|
- `backend/live_executor.py:50-55` — DB_HOST 配置
|
||||||
|
- `backend/auth.py:1-6` — 自研 JWT import
|
||||||
|
- `frontend/app/page.tsx:149-154` — 轮询间隔
|
||||||
|
- `backend/strategies/v51_baseline.json`, `v52_8signals.json`
|
||||||
100
docs/ai/07-glossary.md
Normal file
100
docs/ai/07-glossary.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# 07 — Glossary
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
项目中使用的专业术语、领域术语和项目自定义术语的定义。
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- 项目混合使用量化交易、加密货币和工程术语,中英文混用。
|
||||||
|
- "R" 是风险单位,1R = 单笔风险金额(PAPER_RISK_PER_TRADE × 余额)。
|
||||||
|
- CVD 是核心指标:累计 delta = 主动买量 - 主动卖量,衡量买卖压力。
|
||||||
|
- ATR 用于动态计算止盈止损距离(TP/SL 均以 ATR 倍数表示)。
|
||||||
|
- "tier" 指仓位档位:light/standard/heavy,对应不同仓位大小倍数。
|
||||||
|
|
||||||
|
## Canonical Facts
|
||||||
|
|
||||||
|
### 交易与量化术语
|
||||||
|
|
||||||
|
| 术语 | 定义 |
|
||||||
|
|------|------|
|
||||||
|
| **资金费率(Funding Rate)** | 永续合约中多空双方每8小时相互支付的费率。正费率:多头付给空头;负费率:空头付给多头。以小数表示(如 `0.0001` = 0.01%)。 |
|
||||||
|
| **永续合约(Perpetual / Perp)** | 无到期日的期货合约,通过资金费率机制锚定现货价格。本项目操作 Binance USDC-M 永续合约。 |
|
||||||
|
| **套利(Arbitrage)** | 持有现货多头 + 永续空头,资金费率为正时空头每8小时收取费率收益,实现无方向性风险的稳定收益。 |
|
||||||
|
| **年化(Annualized)** | `平均费率 × 3次/天 × 365天 × 100%`,将单次资金费率换算为年化百分比。 |
|
||||||
|
| **CVD(Cumulative Volume Delta)** | 累计成交量差值 = 主动买量 - 主动卖量。正值表示买方主导,负值表示卖方主导。本项目计算三个窗口:CVD_fast(30分钟)、CVD_mid(4小时)、CVD_day(UTC日内)。 |
|
||||||
|
| **aggTrade** | Binance 聚合成交数据:同一方向、同一价格、同一时刻的多笔成交合并为一条记录,包含 `is_buyer_maker` 字段(0=主动买,1=主动卖)。 |
|
||||||
|
| **is_buyer_maker** | `0`:买方是 taker(主动买入),`1`:买方是 maker(被动成交,即主动卖)。CVD 计算:0→买量,1→卖量。 |
|
||||||
|
| **VWAP(Volume Weighted Average Price)** | 成交量加权平均价格。用于判断当前价格相对于短期平均成本的位置。 |
|
||||||
|
| **ATR(Average True Range)** | 平均真实波动幅度,衡量市场波动性。本项目使用 5 分钟 K 线、14 周期 EMA 计算。 |
|
||||||
|
| **ATR Percentile** | 当前 ATR 在过去 24 小时内的历史分位数(0~100),衡量当前波动性是高还是低。 |
|
||||||
|
| **P95 / P99** | 过去 24 小时内成交量的第 95/99 百分位数,作为"大单阈值"。超过 P99 的成交视为大单,对信号评分有影响。 |
|
||||||
|
| **Long/Short Ratio(多空比)** | 全市场多头账户数 / 空头账户数。反映市场情绪拥挤程度。 |
|
||||||
|
| **Top Trader Position(顶级交易者持仓比)** | 大户多头持仓占比,范围 0~1。高于 0.55 视为多头拥挤,低于 0.45 视为空头拥挤。 |
|
||||||
|
| **Open Interest(OI,持仓量)** | 市场上所有未平仓合约的总名义价值(USD)。OI 增加 ≥3% 视为环境强势信号。 |
|
||||||
|
| **Coinbase Premium** | Coinbase Pro BTC/USD 现货价格相对 Binance BTC/USDT 的溢价比例。正溢价(>0.05%)被视为看涨信号(美国机构买入)。以比例存储(如 `0.0005` = 0.05%)。 |
|
||||||
|
| **清算(Liquidation)** | 爆仓事件。空头清算多于多头清算(短时间内)视为看涨信号(逼空)。本项目使用 5 分钟窗口内多空清算 USD 之比进行评分。 |
|
||||||
|
| **R(风险单位)** | 单笔风险金额。1R = `初始余额 × 风险比例`(默认 2%,即 200U)。盈亏以 R 倍数表示:1R=保本,2R=盈利1倍风险,-1R=全亏。 |
|
||||||
|
| **PnL_R** | 以 R 为单位的盈亏:`pnl_r = (exit_price - entry_price) / risk_distance × direction_sign`。 |
|
||||||
|
| **TP1 / TP2(Take Profit)** | 止盈目标价。TP1 为第一目标(触发后平一半仓位),TP2 为第二目标(平剩余)。 |
|
||||||
|
| **SL(Stop Loss)** | 止损价。SL 触发后视 TP1 是否已命中:未命中→亏损 1R;已命中→保本(sl_be 状态)。 |
|
||||||
|
| **Tier(档位)** | 仓位大小分级。`light`=0.5×R,`standard`=1.0×R,`heavy`=1.5×R。信号分数越高触发越重的档位:score ≥ max(threshold+10, 85) → heavy;score ≥ threshold → standard。 |
|
||||||
|
| **Warmup(冷启动)** | signal_engine 启动时读取历史 `agg_trades` 填充滚动窗口的过程,完成前不产生信号(`state.warmup=True`)。 |
|
||||||
|
| **Signal Cooldown(信号冷却)** | 同一 symbol 同一策略触发信号后,10 分钟内不再触发新信号,防止过度交易。 |
|
||||||
|
|
||||||
|
### 策略术语
|
||||||
|
|
||||||
|
| 术语 | 定义 |
|
||||||
|
|------|------|
|
||||||
|
| **v51_baseline** | V5.1 基准策略。6 个信号:cvd, p99, accel, ls_ratio, oi, coinbase_premium。SL=1.4×ATR,TP1=1.05×ATR,TP2=2.1×ATR。 |
|
||||||
|
| **v52_8signals** | V5.2 扩展策略。8 个信号(v51 + funding_rate + liquidation)。SL=2.1×ATR,TP1=1.4×ATR,TP2=3.15×ATR(更宽止损,更高盈亏比目标)。 |
|
||||||
|
| **Score / 信号分数** | 0~100 的综合评分,由多层加权指标累加得出,阈值 75 触发信号。 |
|
||||||
|
| **Direction Layer(方向层)** | 评分第一层,最高 45 分(v51)或 40 分(v52)。基于 CVD_fast、CVD_mid 同向性和 P99 大单方向。 |
|
||||||
|
| **Crowding Layer(拥挤层)** | 基于多空比和顶级交易者持仓的市场拥挤度评分。 |
|
||||||
|
| **Environment Layer(环境层)** | 基于持仓量变化(OI change)的市场环境评分。 |
|
||||||
|
| **Confirmation Layer(确认层)** | CVD 快慢线同向确认,15 分(满足)或 0 分。 |
|
||||||
|
| **Auxiliary Layer(辅助层)** | Coinbase Premium 辅助确认,0~5 分。 |
|
||||||
|
| **Accel Bonus(加速奖励)** | CVD 快线斜率正在加速时额外加分(v51: +5分,v52: +0分)。 |
|
||||||
|
| **Score Factors** | 各层得分详情,以 JSONB 格式存储在 `paper_trades.score_factors` 和 `live_trades.score_factors`。 |
|
||||||
|
|
||||||
|
### 工程术语
|
||||||
|
|
||||||
|
| 术语 | 定义 |
|
||||||
|
|------|------|
|
||||||
|
| **Paper Trading / 模拟盘** | 不真实下单、仅模拟记录的交易,用于验证策略。数据存储在 `paper_trades` 表。 |
|
||||||
|
| **Live Trading / 实盘** | 通过 Binance API 真实下单执行的交易。数据存储在 `live_trades` 表。 |
|
||||||
|
| **Testnet** | Binance 测试网(`https://testnet.binancefuture.com`),使用虚拟资金。`TRADE_ENV=testnet`。 |
|
||||||
|
| **Production** | Binance 生产环境(`https://fapi.binance.com`),使用真实资金。`TRADE_ENV=production`。 |
|
||||||
|
| **Circuit Break(熔断)** | risk_guard 触发的保护机制,阻止新开仓甚至强制平仓。通过 `live_config` 表的 flag 通知 live_executor。 |
|
||||||
|
| **Dual Write(双写)** | 同一数据同时写入本地 PG 和 Cloud SQL,Cloud SQL 写失败不阻断主流程。 |
|
||||||
|
| **Partition / 分区** | `agg_trades` 表的月度子表(如 `agg_trades_202603`),用于管理大表性能。 |
|
||||||
|
| **NOTIFY/LISTEN** | PostgreSQL 原生异步通知机制。signal_engine 用 `NOTIFY new_signal` 触发,live_executor 用 `LISTEN new_signal` 接收。 |
|
||||||
|
| **TradeWindow** | signal_engine 中的滚动时间窗口类,维护 CVD 和 VWAP 的实时滚动计算。 |
|
||||||
|
| **SymbolState** | 每个交易对的完整状态容器,包含三个 TradeWindow、ATRCalculator、market_indicators 缓存和信号冷却记录。 |
|
||||||
|
| **Invite Code(邀请码)** | 注册时必须提供的一次性(或限次)代码,由管理员通过 `admin_cli.py` 生成。 |
|
||||||
|
| **Subscription Tier** | 用户订阅等级(`free` 等),存储在 `subscriptions` 表,当前代码中使用有限。 |
|
||||||
|
| **万分之** | 前端显示资金费率时的单位表述,实际值 × 10000 展示。例如 `0.0001` 显示为 `1.0000 万分之`。 |
|
||||||
|
|
||||||
|
## Interfaces / Dependencies
|
||||||
|
无。
|
||||||
|
|
||||||
|
## Unknowns & Risks
|
||||||
|
- [inference] `Subscription Tier` 功能在 schema 中有定义但实际业务逻辑中使用程度不确定(可能是预留字段)。
|
||||||
|
- [inference] "no_direction" 状态(CVD_fast 和 CVD_mid 不一致时)的处理逻辑:方向取 CVD_fast,但标记为不触发信号,可用于反向平仓判断。
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/signal_engine.py:1-16` — CVD/ATR/VWAP/P95/P99 架构注释
|
||||||
|
- `backend/signal_engine.py:69-81` — Paper trading 参数定义(R、tier 倍数)
|
||||||
|
- `backend/signal_engine.py:170-207` — TradeWindow 类(CVD/VWAP 定义)
|
||||||
|
- `backend/signal_engine.py:209-257` — ATRCalculator 类
|
||||||
|
- `backend/signal_engine.py:410-651` — evaluate_signal(各层评分逻辑)
|
||||||
|
- `backend/strategies/v51_baseline.json`, `v52_8signals.json` — 策略参数
|
||||||
|
- `backend/trade_config.py` — 交易对精度配置
|
||||||
|
- `frontend/app/page.tsx:186` — "万分之" 显示注释
|
||||||
141
docs/ai/99-open-questions.md
Normal file
141
docs/ai/99-open-questions.md
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# 99 — Open Questions
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
记录文档生成过程中发现的未解决问题、不确定点和潜在风险。
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
- `requirements.txt` 不完整,实际依赖需手动补齐。
|
||||||
|
- `users` 表在两个地方定义且 schema 不一致(db.py vs auth.py)。
|
||||||
|
- live_executor / risk_guard 直连 Cloud SQL 但 signal_engine 写本地 PG,存在数据同步延迟风险。
|
||||||
|
- 策略是否支持热重载(每次循环重读 JSON)未确认。
|
||||||
|
- uvicorn 监听端口 4332 未在启动脚本中显式确认。
|
||||||
|
- 无 CI/CD,无自动化测试。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
### 高优先级(影响正确性)
|
||||||
|
|
||||||
|
#### Q1:users 表 schema 双定义不一致
|
||||||
|
**问题**:`db.py` 的 `SCHEMA_SQL` 和 `auth.py` 的 `AUTH_SCHEMA` 均定义了 `users` 表,但字段不同:
|
||||||
|
- `db.py` 版本:`id, email, password_hash, role, created_at`(无 `discord_id`、无 `banned`)
|
||||||
|
- `auth.py` 版本:`id, email, password_hash, discord_id, role, banned, created_at`
|
||||||
|
|
||||||
|
`init_schema()` 和 `ensure_auth_tables()` 都在 FastAPI startup 中调用,两次 `CREATE TABLE IF NOT EXISTS` 第一次成功后第二次静默跳过。**实际创建的是哪个版本?** 取决于调用顺序(先 `init_schema` 后 `ensure_auth_tables`),如果本地 PG 已有旧版表则字段可能缺失。
|
||||||
|
|
||||||
|
**影响**:auth 相关功能(discord_id 关联、banned 状态检查)可能在 schema 未更新的环境下失效。
|
||||||
|
|
||||||
|
**建议行动**:统一到 auth.py 版本,或添加 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 迁移。
|
||||||
|
|
||||||
|
**来源**:`db.py:269-276`,`auth.py:28-37`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q2:live_executor 读 Cloud SQL,signal_engine 写本地 PG,双写延迟是否可接受
|
||||||
|
**问题**:signal_engine 写入本地 PG(`signal_indicators`),同时双写 Cloud SQL;live_executor 直连 Cloud SQL 读取信号。若某次双写失败或延迟,live_executor 可能错过信号或读到不一致数据。
|
||||||
|
|
||||||
|
**影响**:实盘信号丢失或执行延迟。
|
||||||
|
|
||||||
|
**建议行动**:确认 NOTIFY 是否也发送到 Cloud SQL(即 live_executor 通过 LISTEN 接收信号,不依赖轮询读表);或将 live_executor 改为连接本地 PG。
|
||||||
|
|
||||||
|
**来源**:`live_executor.py:50-55`,`db.py:76-95`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q3:requirements.txt 不完整
|
||||||
|
**问题**:`requirements.txt` 只列出 `fastapi, uvicorn, httpx, python-dotenv, psutil`,但源码还 import 了 `asyncpg`、`psycopg2`(用于 psycopg2-binary)、`aiohttp`、`websockets`(推断)等。
|
||||||
|
|
||||||
|
**影响**:新环境安装后进程无法启动。
|
||||||
|
|
||||||
|
**建议行动**:执行 `pip freeze > requirements.txt` 或手动补全所有依赖。
|
||||||
|
|
||||||
|
**来源**:`backend/requirements.txt:1-5`,`backend/db.py:9-11`,`backend/live_executor.py:28-29`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 中优先级(影响维护性)
|
||||||
|
|
||||||
|
#### Q4:策略 JSON 是否支持热重载
|
||||||
|
**问题**:`load_strategy_configs()` 在 `main()` 函数开头调用一次。不清楚 signal_engine 的主循环是否每次迭代都重新调用此函数。
|
||||||
|
|
||||||
|
**影响**:如果不支持热重载,修改策略 JSON 后需要重启 signal_engine 进程。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:44-67, 964`(需查看 main 函数结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q5:uvicorn 端口确认
|
||||||
|
**问题**:从 `frontend/next.config.ts` 推断 uvicorn 运行在 `127.0.0.1:4332`,但没有找到后端启动脚本明确指定此端口。
|
||||||
|
|
||||||
|
**建议行动**:在 `ecosystem.dev.config.js` 或启动脚本中显式记录端口。
|
||||||
|
|
||||||
|
**来源**:`frontend/next.config.ts:8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q6:market_indicators 表 schema 未在 SCHEMA_SQL 中定义
|
||||||
|
**问题**:signal_engine 从 `market_indicators` 表读取数据(指标类型:`long_short_ratio`, `top_trader_position`, `open_interest_hist`, `coinbase_premium`, `funding_rate`),但该表的 CREATE TABLE 语句不在 `db.py` 的 `SCHEMA_SQL` 中。
|
||||||
|
|
||||||
|
**影响**:表由 `market_data_collector.py` 单独创建;如果该进程未运行过,表不存在,signal_engine 会报错或返回空数据。
|
||||||
|
|
||||||
|
**建议行动**:将 `market_indicators` 表定义加入 `SCHEMA_SQL`,确保 `init_schema()` 能覆盖全量 schema。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:123-158`,`db.py:166-357`(未见 market_indicators 定义)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q7:liquidations 表 schema 未确认
|
||||||
|
**问题**:signal_engine 查询 `liquidations` 表(`SELECT FROM liquidations WHERE symbol=%s AND trade_time >= %s`),但该表定义在 `SCHEMA_SQL` 中同样未找到。可能由 `liquidation_collector.py` 自行创建。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:395-407`,`liquidation_collector.py:28`(`ensure_table()` 函数)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 低优先级(长期健康度)
|
||||||
|
|
||||||
|
#### Q8:无 CI/CD 流水线
|
||||||
|
**问题**:仓库中没有 `.github/workflows/`、Dockerfile、docker-compose.yml 等部署自动化文件。所有部署为手动操作(ssh + git pull + pm2 restart)。
|
||||||
|
|
||||||
|
**建议行动**:添加 GitHub Actions 用于基本 lint 检查和依赖安全扫描。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q9:无自动化测试
|
||||||
|
**问题**:未发现任何测试文件(`test_*.py`、`*.test.ts` 等)。策略验证完全依赖人工回测和模拟盘。
|
||||||
|
|
||||||
|
**建议行动**:至少为 `evaluate_signal()`、`TradeWindow`、`ATRCalculator` 添加单元测试,防止重构回归。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q10:生产环境硬编码密码风险
|
||||||
|
**问题**:`db.py`、`live_executor.py`、`risk_guard.py` 中均有 testnet 默认密码 `arb_engine_2026` 硬编码在源代码里(通过 `os.getenv(..., "arb_engine_2026")` 方式)。
|
||||||
|
|
||||||
|
**影响**:代码一旦泄露,testnet 数据库可被访问;生产环境如果环境变量设置失败,会静默使用错误密码(失败时的错误信息较明确,但仍有风险)。
|
||||||
|
|
||||||
|
**建议行动**:testnet 默认密码移除或通过单独的 `.env.testnet` 文件管理,不内嵌到源代码。
|
||||||
|
|
||||||
|
**来源**:`db.py:19`,`live_executor.py:44`,`risk_guard.py:42`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Q11:`signal_indicators` 表含 `strategy` 字段但 schema 未声明
|
||||||
|
**问题**:`save_indicator()` 函数的 INSERT 语句包含 `strategy` 字段,但 `SCHEMA_SQL` 中的 `signal_indicators` 表定义不包含该字段。可能通过 `ALTER TABLE ADD COLUMN IF NOT EXISTS` 在运行时补充,或是后续版本添加但忘记更新 schema。
|
||||||
|
|
||||||
|
**来源**:`signal_engine.py:690-699`,`db.py:205-224`(signal_indicators 定义)
|
||||||
|
|
||||||
|
## Source Refs
|
||||||
|
- `backend/db.py:269-276` — db.py 版 users 表
|
||||||
|
- `backend/auth.py:28-37` — auth.py 版 users 表(含 discord_id, banned)
|
||||||
|
- `backend/requirements.txt` — 不完整的依赖列表
|
||||||
|
- `backend/live_executor.py:44, 50-55` — DB_HOST 和默认密码
|
||||||
|
- `backend/risk_guard.py:42, 47-53` — DB_HOST 和默认密码
|
||||||
|
- `backend/signal_engine.py:395-407` — liquidations 表查询
|
||||||
|
- `backend/signal_engine.py:690-699` — strategy 字段 INSERT
|
||||||
52
docs/ai/INDEX.md
Normal file
52
docs/ai/INDEX.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
generated_by: repo-insight
|
||||||
|
version: 1
|
||||||
|
created: 2026-03-03
|
||||||
|
last_updated: 2026-03-03
|
||||||
|
source_commit: 0d9dffa
|
||||||
|
coverage: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arbitrage Engine — AI Documentation Index
|
||||||
|
|
||||||
|
**Project**: `arbitrage-engine`
|
||||||
|
**Summary**: Full-stack crypto perpetual futures funding-rate arbitrage monitoring and V5.x CVD/ATR-based short-term trading signal engine. Python/FastAPI backend + Next.js frontend + PostgreSQL + Binance USDC-M Futures.
|
||||||
|
|
||||||
|
## Generated Documents
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| [00-system-overview.md](./00-system-overview.md) | Project purpose, tech stack, repo layout, entry points, environment variables |
|
||||||
|
| [01-architecture-map.md](./01-architecture-map.md) | Multi-process architecture, component diagram, signal pipeline data flow, risk guard rules, frontend polling |
|
||||||
|
| [02-module-cheatsheet.md](./02-module-cheatsheet.md) | Module-by-module index: role, public interfaces, dependencies for all 20 backend + 15 frontend files |
|
||||||
|
| [03-api-contracts.md](./03-api-contracts.md) | All REST endpoints, auth flows, request/response shapes, error conventions |
|
||||||
|
| [04-data-model.md](./04-data-model.md) | All PostgreSQL tables, columns, partitioning strategy, storage design decisions |
|
||||||
|
| [05-build-run-test.md](./05-build-run-test.md) | 构建/运行/部署命令,环境变量,PM2 配置,回测和模拟盘操作 |
|
||||||
|
| [06-decision-log.md](./06-decision-log.md) | 9 项关键技术决策:PG 消息总线、循环间隔、双写、分区、自研 JWT 等 |
|
||||||
|
| [07-glossary.md](./07-glossary.md) | 交易术语(CVD/ATR/R/tier)+ 工程术语(paper trading/warmup/circuit break) |
|
||||||
|
| [99-open-questions.md](./99-open-questions.md) | 11 个未解决问题:users 表双定义冲突、依赖不完整、硬编码密码、无测试等 |
|
||||||
|
|
||||||
|
## Recommended Reading Order
|
||||||
|
|
||||||
|
1. **Start here**: `00-system-overview.md` — 了解项目定位和结构。
|
||||||
|
2. **Architecture**: `01-architecture-map.md` — 理解 7+ 进程的交互方式。
|
||||||
|
3. **Data**: `04-data-model.md` — 任何 DB 相关工作的必读;注意时间戳格式不统一问题。
|
||||||
|
4. **API**: `03-api-contracts.md` — 前端开发或 API 对接时参考。
|
||||||
|
5. **Module detail**: `02-module-cheatsheet.md` — 修改特定文件前的参考。
|
||||||
|
6. **Ops**: `05-build-run-test.md` — 部署和运维操作。
|
||||||
|
7. **Concepts**: `07-glossary.md` — 不熟悉量化术语时查阅。
|
||||||
|
8. **Risks**: `99-open-questions.md` — 开始开发前必读,了解已知风险点。
|
||||||
|
|
||||||
|
## Coverage Tier
|
||||||
|
**Deep** — 包含完整的模块签名读取、核心业务模块深度阅读(signal_engine 全文、evaluate_signal 评分逻辑、backtest.py)、构建运行指南、决策日志、术语表和开放问题。
|
||||||
|
|
||||||
|
## Key Facts for AI Agents
|
||||||
|
- **Signal engine is the core**: `backend/signal_engine.py` — change with care; affects all trading modes.
|
||||||
|
- **Strategy tuning via JSON**: modify `backend/strategies/v51_baseline.json` or `v52_8signals.json` to change signal weights/thresholds without code changes.
|
||||||
|
- **No ORM**: raw SQL via `asyncpg`/`psycopg2`; schema in `db.py:SCHEMA_SQL`.
|
||||||
|
- **Auth is custom JWT**: no third-party auth library; hand-rolled HMAC-SHA256 in `auth.py`.
|
||||||
|
- **`TRADE_ENV=testnet` default**: production use requires explicit env override + strong JWT_SECRET.
|
||||||
|
- **Dual timestamp formats**: `ts` = Unix seconds, `time_ms`/`entry_ts`/`timestamp_ms` = Unix milliseconds — do not confuse.
|
||||||
|
|
||||||
|
## Generation Timestamp
|
||||||
|
2026-03-03T00:00:00 (UTC)
|
||||||
@ -1,594 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Arbitrage Engine 完整项目规格文档"
|
|
||||||
---
|
|
||||||
# Arbitrage Engine — 完整项目规格文档
|
|
||||||
> 供 Codex 重写使用。描述现有系统的所有功能、界面、后端逻辑、数据库结构。
|
|
||||||
> 语言:精确、无歧义、面向 AI。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、项目概述
|
|
||||||
|
|
||||||
**项目名称**:ArbitrageEngine
|
|
||||||
**核心目标**:加密货币量化策略研究平台,通过实时信号引擎计算多因子评分,在模拟盘上验证策略表现,为未来实盘交易提供数据支撑。
|
|
||||||
**当前阶段**:模拟盘运行(paper trading),不连接真实资金。
|
|
||||||
**主要币种**:BTCUSDT、ETHUSDT、SOLUSDT、XRPUSDT(币安永续合约)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、技术栈
|
|
||||||
|
|
||||||
### 后端
|
|
||||||
- **语言**:Python 3.11
|
|
||||||
- **框架**:FastAPI(异步 HTTP)
|
|
||||||
- **数据库**:PostgreSQL 18(GCP Cloud SQL,内网 IP 10.106.0.3,数据库名 arb_engine)
|
|
||||||
- **认证**:JWT(HS256,secret=`arb-engine-jwt-secret-v2-2026`)
|
|
||||||
- **进程管理**:PM2
|
|
||||||
- **WebSocket**:`websockets` 库,连接币安 WebSocket stream
|
|
||||||
|
|
||||||
### 前端
|
|
||||||
- **框架**:Next.js 14(App Router)
|
|
||||||
- **UI 组件**:shadcn/ui + Tailwind CSS + Radix UI + Lucide Icons
|
|
||||||
- **图表**:Recharts
|
|
||||||
- **主题**:默认暗色,主色 slate + cyan
|
|
||||||
- **HTTP 客户端**:fetch(原生)
|
|
||||||
|
|
||||||
### 基础设施
|
|
||||||
- **服务器**:GCP asia-northeast1-b,Ubuntu,Tailscale IP 100.105.186.73
|
|
||||||
- **Cloud SQL**:GCP,内网 10.106.0.3,公网 34.85.117.248,PostgreSQL 18
|
|
||||||
- **PM2 路径**:`/home/fzq1228/Projects/ops-dashboard/node_modules/pm2/bin/pm2`
|
|
||||||
- **项目路径**:`/home/fzq1228/Projects/arbitrage-engine/`
|
|
||||||
- **前端端口**:4333(arb-web)
|
|
||||||
- **后端端口**:4332(arb-api)
|
|
||||||
- **对外域名**:`https://arb.zhouyangclaw.com`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、PM2 进程列表
|
|
||||||
|
|
||||||
| ID | 名称 | 职责 |
|
|
||||||
|----|------|------|
|
|
||||||
| 0 | arb-web | Next.js 前端,端口 4333 |
|
|
||||||
| 8 | arb-api | FastAPI 后端,端口 4332 |
|
|
||||||
| 9 | signal-engine | 核心信号引擎(Python,单进程事件循环) |
|
|
||||||
| 24 | agg-collector | 从币安 WebSocket 收集逐笔成交(agg_trades) |
|
|
||||||
| 25 | paper-monitor | 模拟盘实时止盈止损监控(WebSocket 价格推送) |
|
|
||||||
| 26 | liq-collector | 收集强平数据(liquidations 表) |
|
|
||||||
| 27 | market-collector | 收集市场指标(资金费率、OI、多空比等) |
|
|
||||||
| 28 | position-sync | 实盘持仓同步(暂不使用) |
|
|
||||||
| 29 | risk-guard | 风控守护进程 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、数据库完整结构
|
|
||||||
|
|
||||||
### 4.1 `strategies` — 策略配置表(V5.4 核心)
|
|
||||||
|
|
||||||
每行代表一个可独立运行的策略实例。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| strategy_id | uuid PK | 自动生成,全局唯一 |
|
|
||||||
| display_name | text | 展示名,如 `BTC_CVD15x1h_TP保守` |
|
|
||||||
| schema_version | int | 默认 1 |
|
|
||||||
| status | text | `running` / `paused` / `deprecated` |
|
|
||||||
| status_changed_at | timestamp | 状态变更时间 |
|
|
||||||
| last_run_at | timestamp | 最近一次信号评估时间 |
|
|
||||||
| deprecated_at | timestamp | 停用时间 |
|
|
||||||
| symbol | text | 交易对,如 `BTCUSDT` |
|
|
||||||
| direction | text | `long` / `short` / `both`(默认 both) |
|
|
||||||
| cvd_fast_window | text | CVD 快线窗口,如 `5m` / `15m` / `30m` |
|
|
||||||
| cvd_slow_window | text | CVD 慢线窗口,如 `30m` / `1h` / `4h` |
|
|
||||||
| weight_direction | int | 方向层权重(默认 55) |
|
|
||||||
| weight_env | int | 环境层权重(默认 25) |
|
|
||||||
| weight_aux | int | 辅助层权重(默认 15) |
|
|
||||||
| weight_momentum | int | 动量层权重(默认 5) |
|
|
||||||
| entry_score | int | 开仓最低分(默认 75) |
|
|
||||||
| gate_obi_enabled | bool | 是否启用 OBI 否决门 |
|
|
||||||
| obi_threshold | float | OBI 否决阈值(默认 0.3) |
|
|
||||||
| gate_whale_enabled | bool | 是否启用鲸鱼否决门 |
|
|
||||||
| whale_usd_threshold | float | 鲸鱼成交额阈值(默认 $50,000) |
|
|
||||||
| whale_flow_pct | float | 鲸鱼流向占比阈值 |
|
|
||||||
| gate_vol_enabled | bool | 是否启用波动率门 |
|
|
||||||
| vol_atr_pct_min | float | 最低 ATR/price 比例(默认 0.002) |
|
|
||||||
| gate_spot_perp_enabled | bool | 是否启用期现背离门 |
|
|
||||||
| spot_perp_threshold | float | 期现背离阈值(默认 0.002) |
|
|
||||||
| gate_cvd_enabled | bool | 是否启用 CVD 共振门 |
|
|
||||||
| sl_atr_multiplier | float | SL 宽度倍数,`risk_distance = sl_atr_multiplier × ATR`,定义 1R 的价格距离(默认 1.5) |
|
|
||||||
| tp1_ratio | float | TP1 目标,以 R 计:`TP1 距离 = tp1_ratio × risk_distance`(默认 0.75) |
|
|
||||||
| tp2_ratio | float | TP2 目标,以 R 计:`TP2 距离 = tp2_ratio × risk_distance`(默认 1.5) |
|
|
||||||
| timeout_minutes | int | 超时平仓分钟数(默认 240) |
|
|
||||||
| flip_threshold | int | 反向信号强制平仓的最低分(默认 80) |
|
|
||||||
| initial_balance | float | 初始模拟资金(默认 $10,000) |
|
|
||||||
| current_balance | float | 当前模拟余额 |
|
|
||||||
| description | text | 策略描述 |
|
|
||||||
| tags | text[] | 标签数组 |
|
|
||||||
| created_at | timestamp | 创建时间 |
|
|
||||||
| updated_at | timestamp | 更新时间 |
|
|
||||||
|
|
||||||
**已有的固定 UUID 策略(legacy,status=deprecated)**:
|
|
||||||
- `00000000-0000-0000-0000-000000000053` → v53 Standard(BTCUSDT+ETHUSDT+SOLUSDT+XRPUSDT,30m/4h)
|
|
||||||
- `00000000-0000-0000-0000-000000000054` → v53 Middle(全币种,15m/1h)
|
|
||||||
- `00000000-0000-0000-0000-000000000055` → v53 Fast(全币种,5m/30m)
|
|
||||||
|
|
||||||
**当前运行中策略(18个 BTC 对照组)**:
|
|
||||||
- 命名规则:`BTC_CVD{fast}x{slow}_TP{保守|平衡|激进}`
|
|
||||||
- 6个 CVD 组合 × 3个 TP 方案 = 18个策略
|
|
||||||
- symbol 全部 = `BTCUSDT`
|
|
||||||
- 权重统一:dir=38 / env=32 / aux=28 / mom=2 / entry_score=71
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.2 `paper_trades` — 模拟盘交易记录
|
|
||||||
|
|
||||||
每行代表一笔模拟交易(开仓→平仓)。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | bigint PK | 自增 |
|
|
||||||
| symbol | text | 交易对 |
|
|
||||||
| direction | text | `LONG` / `SHORT` |
|
|
||||||
| score | int | 开仓时评分 |
|
|
||||||
| tier | text | `light` / `standard` / `heavy`(仓位档位) |
|
|
||||||
| entry_price | float | 开仓价格(实时成交价快照) |
|
|
||||||
| entry_ts | bigint | 开仓时间戳(毫秒) |
|
|
||||||
| exit_price | float | 平仓价格 |
|
|
||||||
| exit_ts | bigint | 平仓时间戳(毫秒) |
|
|
||||||
| tp1_price | float | 止盈1价格 |
|
|
||||||
| tp2_price | float | 止盈2价格 |
|
|
||||||
| sl_price | float | 止损价格 |
|
|
||||||
| tp1_hit | bool | 是否曾触及 TP1 |
|
|
||||||
| status | text | `active` / `tp1_hit` / `tp` / `sl` / `sl_be` / `timeout` / `signal_flip` |
|
|
||||||
| pnl_r | float | 盈亏(以 R 计,1R = SL 距离) |
|
|
||||||
| atr_at_entry | float | 开仓时 ATR 值 |
|
|
||||||
| score_factors | jsonb | 四层评分详情 |
|
|
||||||
| strategy | varchar | 策略名(如 `custom_62047807`) |
|
|
||||||
| risk_distance | float | 1R 对应的价格距离(= sl_atr_multiplier × ATR) |
|
|
||||||
| calc_version | int | 计算版本(1=VWAP,2=last_trade) |
|
|
||||||
| price_source | text | 价格来源 |
|
|
||||||
| strategy_id | uuid FK | 关联 strategies 表 |
|
|
||||||
| strategy_name_snapshot | text | 开仓时的策略展示名快照 |
|
|
||||||
|
|
||||||
**status 含义**:
|
|
||||||
- `active`:持仓中,尚未触及 TP1
|
|
||||||
- `tp1_hit`:已触及 TP1,移动止损到保本价
|
|
||||||
- `tp`:全仓止盈出场(同时触及 TP2,或手动)
|
|
||||||
- `sl`:止损出场(pnl_r = -1.0)
|
|
||||||
- `sl_be`:保本止损出场(pnl_r ≈ +0.5 × tp1_ratio,小正收益)
|
|
||||||
- `timeout`:持仓超时平仓(240分钟)
|
|
||||||
- `signal_flip`:反向信号强制平仓
|
|
||||||
|
|
||||||
**pnl_r 计算规则**(以价格距离除以 1R = risk_distance 为基准):
|
|
||||||
- SL 触发:`pnl_r = -1.0`(恒定)
|
|
||||||
- TP1 触发后 SL_BE:`pnl_r = 0.5 × tp1_ratio`
|
|
||||||
- 全仓 TP2:`pnl_r = 0.5 × tp1_ratio + 0.5 × tp2_ratio`
|
|
||||||
- timeout/flip:`pnl_r = (exit_price - entry_price) / risk_distance`(LONG 方向,SHORT 取反)
|
|
||||||
- 实际实现中,以上结果再统一减去手续费折算:`fee_r = (2 × fee_rate × entry_price) / risk_distance`,当前 `fee_rate = 0.0005`(0.05% taker),即最终写入的 `pnl_r = 上述值 - fee_r`。
|
|
||||||
|
|
||||||
**胜率定义**(重要):`pnl_r > 0` 的笔数 / 总闭仓笔数。不用 status 字段判断。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.3 `signal_indicators` — 信号指标快照
|
|
||||||
|
|
||||||
每次信号引擎评估时写入一行,记录当时所有原始指标和评分结果。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | bigint PK | 自增 |
|
|
||||||
| ts | bigint | 时间戳(毫秒) |
|
|
||||||
| symbol | text | 交易对 |
|
|
||||||
| cvd_fast | float | 旧字段:30m CVD(别名,实际=cvd_30m) |
|
|
||||||
| cvd_mid | float | 旧字段:4h CVD(别名,实际=cvd_4h) |
|
|
||||||
| cvd_day | float | 24h CVD |
|
|
||||||
| cvd_fast_slope | float | CVD 快线斜率 |
|
|
||||||
| atr_5m | float | 5分钟粒度 ATR |
|
|
||||||
| atr_percentile | float | ATR 百分位(0~100) |
|
|
||||||
| vwap_30m | float | 30分钟 VWAP |
|
|
||||||
| price | float | 当前价格(实时最新成交价) |
|
|
||||||
| p95_qty | float | 过去1分钟成交量 P95 |
|
|
||||||
| p99_qty | float | 过去1分钟成交量 P99 |
|
|
||||||
| buy_vol_1m | float | 过去1分钟主动买入量 |
|
|
||||||
| sell_vol_1m | float | 过去1分钟主动卖出量 |
|
|
||||||
| score | int | 综合评分(0~100) |
|
|
||||||
| signal | text | `LONG` / `SHORT` / null(null=无信号) |
|
|
||||||
| factors | jsonb | 四层评分详情,格式见下 |
|
|
||||||
| strategy | varchar | 策略名 |
|
|
||||||
| atr_value | float | ATR 值(用于计算 SL/TP) |
|
|
||||||
| cvd_5m | float | 5分钟 CVD |
|
|
||||||
| cvd_15m | float | 15分钟 CVD |
|
|
||||||
| cvd_30m | float | 30分钟 CVD |
|
|
||||||
| cvd_1h | float | 1小时 CVD |
|
|
||||||
| cvd_4h | float | 4小时 CVD |
|
|
||||||
| strategy_id | uuid FK | 关联 strategies 表 |
|
|
||||||
| strategy_name_snapshot | text | 策略展示名快照 |
|
|
||||||
|
|
||||||
**factors jsonb 结构**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"direction": {"score": 38.0, "detail": "CVD共振LONG, cvd_fast=1234.5, cvd_slow=5678.9"},
|
|
||||||
"environment": {"score": 32.0, "detail": "ATR正常, VWAP上方"},
|
|
||||||
"auxiliary": {"score": 28.0, "detail": "OBI=0.45, 无否决"},
|
|
||||||
"momentum": {"score": 2.0, "detail": "P99成交量正常"},
|
|
||||||
"total": 75,
|
|
||||||
"gate_passed": true,
|
|
||||||
"block_reason": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.4 `agg_trades` — 逐笔成交数据(分区表)
|
|
||||||
|
|
||||||
主表 + 按月分区子表(agg_trades_202602, agg_trades_202603 等)。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| agg_id | bigint PK | 币安 agg trade ID |
|
|
||||||
| symbol | text | 交易对 |
|
|
||||||
| price | float | 成交价格 |
|
|
||||||
| qty | float | 成交量 |
|
|
||||||
| time_ms | bigint | 成交时间戳(毫秒) |
|
|
||||||
| is_buyer_maker | smallint | 0=主动买入,1=主动卖出 |
|
|
||||||
|
|
||||||
**数据量**:
|
|
||||||
- BTCUSDT:2026-02-05 起,约 8949 万条
|
|
||||||
- ETHUSDT:2026-02-25 起,约 3297 万条
|
|
||||||
- SOLUSDT/XRPUSDT:2026-02-28 起,约 400~500 万条
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.5 `market_indicators` — 市场宏观指标
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | int PK | 自增 |
|
|
||||||
| symbol | varchar | 交易对 |
|
|
||||||
| indicator_type | varchar | 指标类型(`funding_rate` / `open_interest` / `ls_ratio` 等) |
|
|
||||||
| timestamp_ms | bigint | 时间戳 |
|
|
||||||
| value | jsonb | 指标值(结构因类型而异) |
|
|
||||||
| created_at | timestamp | 写入时间 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.6 `rate_snapshots` — 资金费率快照
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| ts | 时间戳(毫秒) |
|
|
||||||
| btc_rate | BTC 资金费率 |
|
|
||||||
| eth_rate | ETH 资金费率 |
|
|
||||||
| btc_price | BTC 价格 |
|
|
||||||
| eth_price | ETH 价格 |
|
|
||||||
| btc_index_price | BTC 指数价格 |
|
|
||||||
| eth_index_price | ETH 指数价格 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.7 `liquidations` — 强平数据
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| symbol | 交易对 |
|
|
||||||
| side | `LONG` / `SHORT` |
|
|
||||||
| price | 强平价格 |
|
|
||||||
| qty | 数量 |
|
|
||||||
| usd_value | USD 价值 |
|
|
||||||
| trade_time | 时间戳(毫秒) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.8 `users` — 用户认证
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| id | bigint PK |
|
|
||||||
| email | 邮箱(唯一) |
|
|
||||||
| password_hash | bcrypt 哈希 |
|
|
||||||
| role | `admin` / `user` |
|
|
||||||
| discord_id | Discord 用户 ID |
|
|
||||||
| banned | 0=正常,1=封禁 |
|
|
||||||
|
|
||||||
Admin 账号:`fzq1228@gmail.com`,role=admin。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.9 `signal_feature_events` — 信号特征事件(机器学习数据集)
|
|
||||||
|
|
||||||
记录每次 gate 决策的全部原始特征,用于事后分析和 Optuna 优化。
|
|
||||||
|
|
||||||
重要字段:`gate_passed`(bool)、`score_direction`、`score_crowding`、`score_environment`、`score_aux`、`score_total`、`block_reason`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、信号引擎(signal_engine.py)详细逻辑
|
|
||||||
|
|
||||||
### 5.1 架构
|
|
||||||
|
|
||||||
- **单进程 Python 事件循环**(asyncio),每 ~15 秒运行一轮
|
|
||||||
- 每轮对 4 个币种 × N 个策略 进行评估
|
|
||||||
- 实时数据来源:`agg_trades` 表(由 agg-collector 写入)
|
|
||||||
- 附加数据来源:`market_indicators` 表(OBI、OI、资金费率等)
|
|
||||||
|
|
||||||
### 5.2 滑动窗口
|
|
||||||
|
|
||||||
每个币种维护 3 个滑动窗口(按 `agg_trades.time_ms` 切片):
|
|
||||||
- `win_fast`:30分钟窗口(`WINDOW_FAST = 30 * 60 * 1000 ms`)
|
|
||||||
- `win_mid`:4小时窗口(`WINDOW_MID = 4 * 3600 * 1000 ms`)
|
|
||||||
- `win_day`:24小时窗口(`WINDOW_DAY = 24 * 3600 * 1000 ms`)
|
|
||||||
|
|
||||||
每个窗口存储 `(time_ms, qty, price, is_buyer_maker)` 元组列表,定期淘汰过期数据。
|
|
||||||
|
|
||||||
**CVD 计算**:
|
|
||||||
`CVD = 主动买入量 - 主动卖出量`(在窗口时间范围内求和)
|
|
||||||
|
|
||||||
**动态切片**:
|
|
||||||
当策略配置了非标准窗口(如 15m、1h)时,从 `win_fast` 或 `win_mid` 的 trades 列表中按时间范围切片重算 CVD:
|
|
||||||
- fast 周期 ≤ 30m → 从 `win_fast.trades` 切片
|
|
||||||
- fast 周期 > 30m → 从 `win_mid.trades` 切片
|
|
||||||
- slow 周期 → 始终从 `win_mid.trades` 切片
|
|
||||||
|
|
||||||
### 5.3 评分模型(`evaluate_factory_strategy`,原 `_evaluate_v53`)
|
|
||||||
|
|
||||||
**四层线性评分**,总分 = 各层得分之和,满分 = 各层权重之和。
|
|
||||||
|
|
||||||
#### 门控系统(5个门,按顺序,任意一门否决则 gate_passed=false,不开仓)
|
|
||||||
|
|
||||||
| 门 | 条件 | 否决理由 |
|
|
||||||
|----|------|----------|
|
|
||||||
| 门1 波动率 | `ATR/price >= vol_atr_pct_min` | ATR 过小,市场太平静 |
|
|
||||||
| 门2 CVD共振 | `cvd_fast` 和 `cvd_slow` 同向(同正=LONG,同负=SHORT) | 快慢 CVD 不共振 |
|
|
||||||
| 门3 鲸鱼否决 | 大额成交(>whale_usd_threshold)的净流向与信号方向一致(或该门禁用) | 鲸鱼反向 |
|
|
||||||
| 门4 OBI否决 | OBI(订单簿不平衡)不与信号方向矛盾(或该门禁用) | OBI 反向 |
|
|
||||||
| 门5 期现背离 | 现货-永续价差在阈值内(或该门禁用) | 期现背离过大 |
|
|
||||||
|
|
||||||
门2(CVD共振)同时决定信号方向:两个 CVD 都正→LONG,都负→SHORT,不同向→HOLD(跳过)。
|
|
||||||
|
|
||||||
#### 评分计算(gate_passed=true 后)
|
|
||||||
|
|
||||||
**方向层**(weight_direction,默认 38):
|
|
||||||
- CVD共振强度 → 占方向层满分的比例
|
|
||||||
- 公式:`score_dir = weight_direction × min(1.0, abs(cvd_fast + cvd_slow) / normalization_factor)`
|
|
||||||
|
|
||||||
**环境层**(weight_env,默认 32):
|
|
||||||
- ATR 百分位(价格波动强度)
|
|
||||||
- VWAP 相对位置
|
|
||||||
- 资金费率方向
|
|
||||||
|
|
||||||
**辅助层**(weight_aux,默认 28):
|
|
||||||
- OBI 强度
|
|
||||||
- 期现背离程度
|
|
||||||
- 强平数据方向
|
|
||||||
|
|
||||||
**动量层**(weight_momentum,默认 2):
|
|
||||||
- P99 成交量异常
|
|
||||||
- 买卖量比
|
|
||||||
|
|
||||||
**开仓条件**:`total_score >= entry_score`(默认 75)
|
|
||||||
|
|
||||||
### 5.4 开仓逻辑
|
|
||||||
|
|
||||||
```
|
|
||||||
for each symbol:
|
|
||||||
update sliding windows with new agg_trades
|
|
||||||
for each strategy (status=running):
|
|
||||||
if strategy.symbol != symbol → skip
|
|
||||||
evaluate_signal(strategy_cfg) → result
|
|
||||||
if result.signal and score >= entry_score:
|
|
||||||
if no active position for this strategy:
|
|
||||||
if active_position_count < max_positions:
|
|
||||||
paper_open_trade(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
**开仓价格**:取 `signal_indicators.price`(实时最新成交价),不是 VWAP。
|
|
||||||
|
|
||||||
**SL/TP 计算**:
|
|
||||||
- `risk_distance = sl_atr_multiplier × ATR`(1R)
|
|
||||||
- LONG:`SL = price - risk_distance`,`TP1 = price + tp1_ratio × risk_distance`,`TP2 = price + tp2_ratio × risk_distance`
|
|
||||||
- SHORT:`SL = price + risk_distance`,`TP1 = price - tp1_ratio × risk_distance`,`TP2 = price - tp2_ratio × risk_distance`
|
|
||||||
|
|
||||||
**当前标准TP/SL配置(BTC 18组对照)**:
|
|
||||||
- 保守:`sl=2.0×ATR, tp1=0.75×ATR, tp2=1.5×ATR`(TP全到=+0.5625R)
|
|
||||||
- 平衡:`sl=2.0×ATR, tp1=1.0×ATR, tp2=2.0×ATR`(TP全到=+1.5R)
|
|
||||||
- 激进:`sl=2.0×ATR, tp1=1.5×ATR, tp2=3.0×ATR`(TP全到=+2.25R)
|
|
||||||
|
|
||||||
### 5.5 平仓逻辑(paper_monitor.py)
|
|
||||||
|
|
||||||
独立进程,通过币安 WebSocket 实时接收 mark price:
|
|
||||||
|
|
||||||
1. `price >= tp1_price`(LONG)或 `price <= tp1_price`(SHORT)→ 触发 TP1:status 改为 `tp1_hit`,移动止损到保本价
|
|
||||||
2. TP1 已触发后,`price >= tp2_price` → 全仓 TP2:status=`tp`
|
|
||||||
3. `price <= sl_price`(LONG)或 `price >= sl_price`(SHORT)→ 止损:status=`sl`,`pnl_r=-1.0`
|
|
||||||
4. 持仓超 240 分钟 → status=`timeout`
|
|
||||||
5. 反向信号强度 >= flip_threshold → signal_engine 触发 `signal_flip`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、FastAPI 后端接口(main.py)
|
|
||||||
|
|
||||||
### 6.1 认证
|
|
||||||
- `POST /api/auth/login` → 返回 JWT token
|
|
||||||
- `POST /api/auth/register` → 注册(需邀请码)
|
|
||||||
- 所有接口需 Bearer JWT,通过 `Depends(get_current_user)` 校验
|
|
||||||
|
|
||||||
### 6.2 行情数据
|
|
||||||
- `GET /api/rates` → 最新资金费率快照(rate_snapshots 最新一行)
|
|
||||||
- `GET /api/snapshots` → 多个时间点的资金费率历史
|
|
||||||
- `GET /api/kline?symbol=BTC&interval=1m&limit=100` → K线(从 agg_trades 聚合)
|
|
||||||
|
|
||||||
### 6.3 信号引擎
|
|
||||||
- `GET /api/signals/latest?strategy=v53` → 各币种最新一条信号指标(signal_indicators 每币种 LIMIT 1)
|
|
||||||
- `GET /api/signals/latest-v52?strategy=v52_8signals` → 同上,v52 字段
|
|
||||||
- `GET /api/signals/indicators?symbol=BTCUSDT&strategy=v53&limit=100` → 历史信号指标列表
|
|
||||||
- `GET /api/signals/signal-history?symbol=BTC&strategy=v53&limit=50` → 有信号(signal IS NOT NULL)的历史列表
|
|
||||||
- `GET /api/signals/market-indicators?symbol=BTCUSDT` → 最新市场宏观指标(OI/多空比/资金费率)
|
|
||||||
- `GET /api/signals/history?strategy=v53&limit=100` → 信号历史(含各层分数)
|
|
||||||
|
|
||||||
### 6.4 模拟盘(paper trading)
|
|
||||||
- `GET /api/paper/config` → 读取 paper_config.json
|
|
||||||
- `POST /api/paper/config` → 更新 paper_config.json(控制总开关和每策略开关)
|
|
||||||
- `GET /api/paper/summary?strategy=v53&strategy_id=uuid` → 总览(总盈亏R/USDT、胜率、余额、持仓数、盈亏比)
|
|
||||||
- `GET /api/paper/positions?strategy=v53&strategy_id=uuid` → 当前活跃持仓列表(含实时浮盈计算)
|
|
||||||
- `GET /api/paper/trades?strategy=v53&strategy_id=uuid&symbol=BTC&status=tp&limit=100` → 历史交易列表
|
|
||||||
- `GET /api/paper/equity-curve?strategy=v53&strategy_id=uuid` → 权益曲线(按时间累加 pnl_r)
|
|
||||||
- `GET /api/paper/stats?strategy=v53&strategy_id=uuid` → 详细统计(胜率/盈亏比/MDD/Sharpe/avg_win/avg_loss,按币种分组)
|
|
||||||
- `GET /api/paper/stats-by-strategy` → 所有策略的汇总统计(策略广场用)
|
|
||||||
|
|
||||||
### 6.5 策略广场(strategy plaza)
|
|
||||||
- `GET /api/strategy-plaza?status=running` → 策略列表(支持 status 过滤)
|
|
||||||
- `GET /api/strategy-plaza/{strategy_id}/summary` → 单策略总览卡
|
|
||||||
- `GET /api/strategy-plaza/{strategy_id}/signals` → 单策略最新信号
|
|
||||||
- `GET /api/strategy-plaza/{strategy_id}/trades` → 单策略交易记录
|
|
||||||
|
|
||||||
### 6.6 策略管理 CRUD
|
|
||||||
- `POST /api/strategies` → 创建新策略(写入 strategies 表,signal_engine 15秒内热重载)
|
|
||||||
- `GET /api/strategies` → 策略列表(含 open_positions 数量)
|
|
||||||
- `GET /api/strategies/{sid}` → 单策略详情
|
|
||||||
- `PATCH /api/strategies/{sid}` → 更新策略参数
|
|
||||||
- `POST /api/strategies/{sid}/pause` → 暂停(status=paused)
|
|
||||||
- `POST /api/strategies/{sid}/resume` → 恢复(status=running)
|
|
||||||
- `POST /api/strategies/{sid}/deprecate` → 停用(status=deprecated)
|
|
||||||
- `POST /api/strategies/{sid}/restore` → 恢复到 running
|
|
||||||
- `POST /api/strategies/{sid}/add-balance` → 补充模拟资金
|
|
||||||
|
|
||||||
### 6.7 服务器监控
|
|
||||||
- `GET /api/server/status` → 服务器资源(CPU/内存/磁盘/PM2进程状态)
|
|
||||||
|
|
||||||
### 6.8 实盘接口(暂未真实使用)
|
|
||||||
- `GET /api/live/*` → 实盘持仓、交易、权益曲线、账户余额、风控状态等
|
|
||||||
- `POST /api/live/emergency-close` → 紧急平仓
|
|
||||||
- `POST /api/live/block-new` → 暂停新开仓
|
|
||||||
- `POST /api/live/resume` → 恢复开仓
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、前端页面列表
|
|
||||||
|
|
||||||
### 7.1 主导航(Sidebar)
|
|
||||||
|
|
||||||
固定左侧 sidebar,包含所有页面入口和当前 BTC/ETH 资金费率实时显示。
|
|
||||||
|
|
||||||
### 7.2 页面详情
|
|
||||||
|
|
||||||
#### `/` (首页/Dashboard)
|
|
||||||
- 资金费率历史折线图(BTC/ETH 双轨)
|
|
||||||
- 资金费率 24h 统计(最大/最小/均值)
|
|
||||||
- 近期套利机会卡片
|
|
||||||
|
|
||||||
#### `/signals-v53` (信号引擎 v5.3,老页面)
|
|
||||||
- CVD 三轨卡片(cvd_fast/cvd_mid/cvd_day + 斜率 + 共振判断)
|
|
||||||
- ATR / VWAP / P95 / P99 市场环境指标
|
|
||||||
- 信号状态卡(LONG/SHORT/无信号 + 评分 + 四层分数进度条)
|
|
||||||
- Gate-Control 卡(5门详情:波动率/CVD共振/鲸鱼/OBI/期现背离)
|
|
||||||
- 信号历史列表(最近20条:时间/方向/评分)
|
|
||||||
- CVD 折线图(可切 1h/4h/12h/24h 时间范围)
|
|
||||||
- 币种切换:BTC/ETH/SOL/XRP
|
|
||||||
|
|
||||||
#### `/paper-v53` (模拟盘 v5.3,老页面)
|
|
||||||
- 总览卡片(总盈亏R/USDT/胜率/持仓数/盈亏比/余额)
|
|
||||||
- 最新信号(四币种最近信号+四层分+Gate状态)
|
|
||||||
- 控制面板(启动/停止按钮)
|
|
||||||
- 当前活跃持仓(实时价格WebSocket/浮盈R/入场TP SL价格)
|
|
||||||
- 权益曲线(面积图)
|
|
||||||
- 历史交易列表(筛选币种/盈亏;入场/出场价/状态/评分/持仓时长)
|
|
||||||
- 详细统计(胜率/盈亏比/avg_win/avg_loss/MDD/Sharpe,按币种分Tab)
|
|
||||||
|
|
||||||
#### `/strategy-plaza` (策略广场,主入口)
|
|
||||||
- 策略列表卡片视图(按 status 过滤:running/paused/deprecated)
|
|
||||||
- 每个卡片显示:策略名/币种/CVD周期/TP方案/胜率/净R/余额/状态
|
|
||||||
- 快速操作:暂停/恢复/停用
|
|
||||||
- 「新建策略」按钮 → 跳转 `/strategy-plaza/create`
|
|
||||||
|
|
||||||
#### `/strategy-plaza/create` (新建策略)
|
|
||||||
- 表单:策略名/symbol/CVD快慢周期/四层权重/五门阈值/TP-SL参数/初始余额
|
|
||||||
- 提交后 POST /api/strategies,signal_engine 15秒内热重载
|
|
||||||
|
|
||||||
#### `/strategy-plaza/[id]` (策略详情页,通用)
|
|
||||||
- Tab1: 信号引擎(SignalsGeneric 组件)
|
|
||||||
- 动态显示该策略配置的 CVD 周期、权重、Gate 阈值
|
|
||||||
- 实时信号评分
|
|
||||||
- 信号历史、CVD 图表
|
|
||||||
- Tab2: 模拟盘(PaperGeneric 组件)
|
|
||||||
- 总览统计、活跃持仓、权益曲线、交易列表、详细统计
|
|
||||||
- 启停控制
|
|
||||||
|
|
||||||
#### `/strategy-plaza/[id]/edit` (编辑策略)
|
|
||||||
- 同新建表单,预填当前参数,提交 PATCH /api/strategies/{id}
|
|
||||||
|
|
||||||
#### `/server` (服务器状态)
|
|
||||||
- CPU/内存/磁盘/PM2进程状态实时监控
|
|
||||||
|
|
||||||
#### `/kline` (K线图)
|
|
||||||
- 任意币种 K线,支持 1m/5m/15m/1h 粒度
|
|
||||||
|
|
||||||
#### `/login` / `/register`
|
|
||||||
- JWT 登录/邀请码注册
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、paper_config.json(模拟盘控制文件)
|
|
||||||
|
|
||||||
路径:`backend/paper_config.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"enabled_strategies": [],
|
|
||||||
"initial_balance": 10000,
|
|
||||||
"risk_per_trade": 0.02,
|
|
||||||
"max_positions": 100,
|
|
||||||
"tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `enabled_strategies` 为空列表 = 全部策略放行
|
|
||||||
- `max_positions` 当前设为 100(实际无限制)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、数据流(完整链路)
|
|
||||||
|
|
||||||
```
|
|
||||||
币安 WebSocket(逐笔成交)
|
|
||||||
↓ agg-collector.py
|
|
||||||
agg_trades 表(PostgreSQL 分区表)
|
|
||||||
↓ signal_engine.py(每15秒读取)
|
|
||||||
滑动窗口(win_fast 30m / win_mid 4h / win_day 24h)
|
|
||||||
↓ 计算CVD/ATR/VWAP
|
|
||||||
evaluate_factory_strategy() (原 _evaluate_v53)
|
|
||||||
↓ 5门检查 → 四层评分
|
|
||||||
signal_indicators 写入
|
|
||||||
↓ signal IS NOT NULL 且 score >= entry_score
|
|
||||||
paper_open_trade() → paper_trades 写入
|
|
||||||
↓
|
|
||||||
paper_monitor.py(WebSocket 实时价格监控)
|
|
||||||
↓ 触及 TP1/TP2/SL 或超时
|
|
||||||
paper_trades 平仓(status/exit_price/pnl_r 更新)
|
|
||||||
↓
|
|
||||||
FastAPI(main.py)← Next.js 前端查询展示
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十、已知缺陷(Codex 重写需改进)
|
|
||||||
|
|
||||||
1. **signal_engine.py 单体巨型文件**(1800+ 行),所有逻辑混在一起
|
|
||||||
2. **所有策略共享同一个 snapshot**,无法真正独立评估
|
|
||||||
3. **CVD 动态切片依赖 win_fast/win_mid 两个固定窗口**,扩展性受限
|
|
||||||
4. **开仓逻辑耦合在 signal_engine 主循环**,paper_monitor 只管平仓
|
|
||||||
5. **前端页面碎片化**:每个策略版本有独立页面,维护困难
|
|
||||||
6. **strategy_name 路由逻辑脆弱**:`custom_` 路由曾多次因 scp 覆盖丢失
|
|
||||||
7. **API 无分页**,大数据量接口可能超时
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十一、关键配置
|
|
||||||
|
|
||||||
```
|
|
||||||
数据库连接: postgresql://arb:arb_engine_2026@10.106.0.3/arb_engine
|
|
||||||
JWT Secret: arb-engine-jwt-secret-v2-2026
|
|
||||||
Admin: fzq1228@gmail.com (id=1, role=admin)
|
|
||||||
前端: https://arb.zhouyangclaw.com (端口 4333)
|
|
||||||
后端: 端口 4332
|
|
||||||
项目路径: /home/fzq1228/Projects/arbitrage-engine/
|
|
||||||
```
|
|
||||||
251
docs/arbitrage-engine/execution-state-machine.md
Normal file
251
docs/arbitrage-engine/execution-state-machine.md
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
# V5.4 Execution State Machine
|
||||||
|
|
||||||
|
本文档描述 V5.4 策略的执行层状态机设计,重点是 maker/taker 组合策略,确保在不牺牲入场/出场时效性的前提下,最大程度降低手续费与滑点摩擦。
|
||||||
|
|
||||||
|
设计目标:
|
||||||
|
- 将"信号质量"和"执行质量"解耦,执行层只负责:更便宜、更稳定地实现既定 TP/SL/flip/timeout 规则。
|
||||||
|
- 入场阶段在不丢失大行情的前提下尽量使用 maker;
|
||||||
|
- 出场阶段 TP 强制 maker 主路径,同时用 taker 做安全兜底;
|
||||||
|
- SL 始终使用 taker,优先保证风控。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 入场状态机(Entry State Machine)
|
||||||
|
|
||||||
|
### 1.1 状态定义
|
||||||
|
|
||||||
|
- `IDLE`:无持仓、无挂单,等待信号。
|
||||||
|
- `ENTRY_PENDING_MAKER`:已下入场限价挂单(post-only),等待成交或超时。
|
||||||
|
- `ENTRY_FILL`:入场成交完成(全仓或部分)。
|
||||||
|
- `ENTRY_FALLBACK_TAKER`:超时后使用 taker 市价单补齐未成交部分。
|
||||||
|
|
||||||
|
### 1.2 关键参数
|
||||||
|
|
||||||
|
- `entry_price_signal`:信号引擎给出的入场参考价(通常为最新价或中间价 mid)。
|
||||||
|
- `tick_size`:交易所最小价格步长。
|
||||||
|
- `entry_offset_ticks`:maker 入场挂单相对盘口的偏移(通常为 1–2 个 tick)。
|
||||||
|
- `entry_timeout_ms`:入场 maker 挂单最大等待时间(如 3000–5000ms)。
|
||||||
|
- `entry_fallback_slippage_bps`:fallback taker 允许的最大滑点(基础保护,超出则放弃补仓或缩小仓位)。
|
||||||
|
|
||||||
|
### 1.3 状态机伪代码
|
||||||
|
|
||||||
|
```pseudo
|
||||||
|
state = IDLE
|
||||||
|
|
||||||
|
on_signal_open(signal):
|
||||||
|
if state != IDLE:
|
||||||
|
return // 避免重复入场
|
||||||
|
|
||||||
|
// 计算 maker 挂单价格
|
||||||
|
side = signal.side // LONG or SHORT
|
||||||
|
ref_price = best_bid_ask_mid()
|
||||||
|
|
||||||
|
if side == LONG:
|
||||||
|
entry_price_maker = min(ref_price, best_bid() + entry_offset_ticks * tick_size)
|
||||||
|
else: // SHORT
|
||||||
|
entry_price_maker = max(ref_price, best_ask() - entry_offset_ticks * tick_size)
|
||||||
|
|
||||||
|
// 下 post-only 入场挂单
|
||||||
|
order_id = place_limit_post_only(side, entry_price_maker, target_size)
|
||||||
|
entry_start_ts = now()
|
||||||
|
state = ENTRY_PENDING_MAKER
|
||||||
|
|
||||||
|
|
||||||
|
on_timer():
|
||||||
|
if state == ENTRY_PENDING_MAKER:
|
||||||
|
if order_filled(order_id):
|
||||||
|
filled_size = get_filled_size(order_id)
|
||||||
|
if filled_size >= min_fill_ratio * target_size:
|
||||||
|
state = ENTRY_FILL
|
||||||
|
return
|
||||||
|
|
||||||
|
if now() - entry_start_ts >= entry_timeout_ms:
|
||||||
|
// 超时,取消剩余挂单
|
||||||
|
cancel_order(order_id)
|
||||||
|
remaining_size = target_size - get_filled_size(order_id)
|
||||||
|
|
||||||
|
if remaining_size <= 0:
|
||||||
|
state = ENTRY_FILL
|
||||||
|
return
|
||||||
|
|
||||||
|
// 兜底:按容忍滑点发市价单
|
||||||
|
mkt_price = best_bid_ask_mid()
|
||||||
|
theoretical_price = ref_price_at_signal
|
||||||
|
slippage_bps = abs(mkt_price - theoretical_price) / theoretical_price * 10000
|
||||||
|
|
||||||
|
if slippage_bps <= entry_fallback_slippage_bps:
|
||||||
|
place_market_order(side, remaining_size)
|
||||||
|
state = ENTRY_FILL
|
||||||
|
else:
|
||||||
|
// 滑点过大,放弃补仓或缩减仓位
|
||||||
|
state = ENTRY_FILL // 仅保留已成交部分
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 出场状态机(TP/SL/Flip/Timeout)
|
||||||
|
|
||||||
|
出场分为四类:TP(止盈)、SL(止损)、flip(信号翻转)、timeout(超时退出)。
|
||||||
|
|
||||||
|
### 2.1 通用状态
|
||||||
|
|
||||||
|
- `POSITION_OPEN`:持仓打开,已根据策略下好 TP/SL 限价单。
|
||||||
|
- `TP_PENDING_MAKER`:TP 限价挂单等待成交。
|
||||||
|
- `TP_FALLBACK_TAKER`:TP 越价未成交时,撤单+市价平仓兜底。
|
||||||
|
- `SL_PENDING`:止损触发,直接发送 taker 单。
|
||||||
|
- `FLIP_PENDING`:翻转触发,先平仓再反向开仓(可复用入场状态机)。
|
||||||
|
- `TIMEOUT_PENDING`:超时触发,按策略规则离场(可偏 maker)。
|
||||||
|
|
||||||
|
### 2.2 关键参数
|
||||||
|
|
||||||
|
- `tp1_r`, `tp2_r`:TP1/TP2 的目标 R 距离(如 1.0R / 2.0R)。
|
||||||
|
- `sl_r`:止损距离(如 -1.0R)。
|
||||||
|
- `tp_timeout_ms`:价格越过 TP 水平后,TP 限价未成交的允许时间窗口。
|
||||||
|
- `flip_threshold`:翻转触发条件(score + OBI + VWAP 等综合判断)。
|
||||||
|
- `timeout_seconds`:最大持仓时间,用于 timeout 出场。
|
||||||
|
|
||||||
|
### 2.3 TP 状态机(maker 主路径 + taker 兜底)
|
||||||
|
|
||||||
|
```pseudo
|
||||||
|
on_position_open(pos):
|
||||||
|
// 开仓后立即挂 TP1 限价单(maker)
|
||||||
|
tp1_price = pos.entry_price + pos.side * tp1_r * pos.risk_distance
|
||||||
|
tp2_price = pos.entry_price + pos.side * tp2_r * pos.risk_distance
|
||||||
|
|
||||||
|
// 半仓挂 TP1,半仓挂 TP2
|
||||||
|
tp1_id = place_limit_post_only(exit_side(pos.side), tp1_price, pos.size * 0.5)
|
||||||
|
tp2_id = place_limit_post_only(exit_side(pos.side), tp2_price, pos.size * 0.5)
|
||||||
|
pos.state = POSITION_OPEN
|
||||||
|
|
||||||
|
|
||||||
|
on_timer():
|
||||||
|
if pos.state == POSITION_OPEN:
|
||||||
|
current_price = best_bid_ask_mid()
|
||||||
|
|
||||||
|
// 检查 TP1 越价兜底
|
||||||
|
tp1_crossed = (pos.side == LONG and current_price >= tp1_price) or
|
||||||
|
(pos.side == SHORT and current_price <= tp1_price)
|
||||||
|
if tp1_crossed and not pos.tp1_cross_ts:
|
||||||
|
pos.tp1_cross_ts = now()
|
||||||
|
|
||||||
|
if pos.tp1_cross_ts:
|
||||||
|
if order_filled(tp1_id):
|
||||||
|
pos.tp1_cross_ts = None // 成交,清除计时
|
||||||
|
elif now() - pos.tp1_cross_ts >= tp_timeout_ms:
|
||||||
|
cancel_order(tp1_id)
|
||||||
|
remaining = size_tp1 - get_filled_size(tp1_id)
|
||||||
|
if remaining > 0:
|
||||||
|
place_market_order(exit_side(pos.side), remaining)
|
||||||
|
|
||||||
|
// 检查 TP2 越价兜底
|
||||||
|
tp2_crossed = (pos.side == LONG and current_price >= tp2_price) or
|
||||||
|
(pos.side == SHORT and current_price <= tp2_price)
|
||||||
|
if tp2_crossed and not pos.tp2_cross_ts:
|
||||||
|
pos.tp2_cross_ts = now()
|
||||||
|
|
||||||
|
if pos.tp2_cross_ts:
|
||||||
|
if order_filled(tp2_id):
|
||||||
|
pos.tp2_cross_ts = None
|
||||||
|
elif now() - pos.tp2_cross_ts >= tp_timeout_ms:
|
||||||
|
cancel_order(tp2_id)
|
||||||
|
remaining = size_tp2 - get_filled_size(tp2_id)
|
||||||
|
if remaining > 0:
|
||||||
|
place_market_order(exit_side(pos.side), remaining)
|
||||||
|
|
||||||
|
// 检查是否已全部平仓
|
||||||
|
if pos_size(pos) <= 0:
|
||||||
|
pos.state = CLOSED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 SL 状态机(纯 taker)
|
||||||
|
|
||||||
|
```pseudo
|
||||||
|
on_sl_trigger(pos, sl_price):
|
||||||
|
// 触发条件可以来自价格监控或止损订单触发
|
||||||
|
// 这里策略层只关心:一旦触发,立即使用 taker
|
||||||
|
close_size = pos_size(pos)
|
||||||
|
if close_size > 0:
|
||||||
|
place_market_order(exit_side(pos.side), close_size)
|
||||||
|
pos.state = CLOSED
|
||||||
|
```
|
||||||
|
|
||||||
|
SL 不做 maker 逻辑,避免在极端行情下挂单无法成交。
|
||||||
|
|
||||||
|
### 2.5 Flip 状态机(平旧仓 + 新开仓)
|
||||||
|
|
||||||
|
```pseudo
|
||||||
|
on_flip_signal(pos, new_side, flip_context):
|
||||||
|
if not flip_condition_met(flip_context):
|
||||||
|
return
|
||||||
|
|
||||||
|
// flip 条件:score < 85 AND OBI 翻转 AND 价格跌破 VWAP(三条件同时满足)
|
||||||
|
// flip_condition_met 由信号引擎判断
|
||||||
|
|
||||||
|
// 1) 先平旧仓(按 SL 逻辑,优先 taker)
|
||||||
|
close_size = pos_size(pos)
|
||||||
|
if close_size > 0:
|
||||||
|
place_market_order(exit_side(pos.side), close_size)
|
||||||
|
|
||||||
|
pos.state = CLOSED
|
||||||
|
|
||||||
|
// 2) 再按入场状态机开新仓
|
||||||
|
new_signal = build_signal_from_flip(new_side, flip_context)
|
||||||
|
entry_state_machine.on_signal_open(new_signal)
|
||||||
|
```
|
||||||
|
|
||||||
|
flip 的关键是:**门槛更高**(如 score < 85 且 OBI 翻转且价格跌破 VWAP),尽量减少在震荡行情中来回打脸。
|
||||||
|
|
||||||
|
### 2.6 Timeout 状态机(超时出场)
|
||||||
|
|
||||||
|
```pseudo
|
||||||
|
on_timer():
|
||||||
|
if pos.state == POSITION_OPEN and now() - pos.open_ts >= timeout_seconds:
|
||||||
|
// 可以偏 maker:先挂限价平仓,超时再 taker
|
||||||
|
timeout_price = best_bid_ask_mid()
|
||||||
|
size = pos_size(pos)
|
||||||
|
|
||||||
|
oid = place_limit_post_only(exit_side(pos.side), timeout_price, size)
|
||||||
|
pos.timeout_order_id = oid
|
||||||
|
pos.timeout_start_ts = now()
|
||||||
|
pos.state = TIMEOUT_PENDING
|
||||||
|
|
||||||
|
if pos.state == TIMEOUT_PENDING:
|
||||||
|
if order_filled(pos.timeout_order_id):
|
||||||
|
pos.state = CLOSED
|
||||||
|
elif now() - pos.timeout_start_ts >= timeout_grace_ms:
|
||||||
|
cancel_order(pos.timeout_order_id)
|
||||||
|
remaining = pos_size(pos)
|
||||||
|
if remaining > 0:
|
||||||
|
place_market_order(exit_side(pos.side), remaining)
|
||||||
|
pos.state = CLOSED
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 监控指标(执行层 KPI)
|
||||||
|
|
||||||
|
| 指标 | 说明 | 目标 |
|
||||||
|
|------|------|------|
|
||||||
|
| `maker_ratio_entry` | 入场成交中 maker 比例 | ≥ 50% |
|
||||||
|
| `maker_ratio_tp` | TP 成交中 maker 比例 | ≥ 80% |
|
||||||
|
| `avg_friction_cost_r` | 每笔平均摩擦成本(手续费+滑点,以 R 计) | ≤ 0.15R |
|
||||||
|
| `entry_timeout_rate` | 入场超时触发 taker 兜底比例 | ≤ 30% |
|
||||||
|
| `tp_overshoot_rate` | TP 越价后兜底比例 | ≤ 20% |
|
||||||
|
| `flip_frequency` | 每笔持仓中 flip 次数 | ≤ 1次/持仓 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 设计原则汇总
|
||||||
|
|
||||||
|
| 场景 | 主路径 | 兜底 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 入场 | limit post-only(盘口内侧 1-2 tick) | 超时 → taker(滑点容忍内) |
|
||||||
|
| TP1 / TP2 | limit post-only(预挂) | 越价 X ms 未成交 → 撤单 + taker |
|
||||||
|
| SL | — | 纯 taker,立即执行 |
|
||||||
|
| Flip | 平仓用 taker,新开仓复用入场逻辑 | — |
|
||||||
|
| Timeout | limit post-only | grace period 后 → taker |
|
||||||
|
|
||||||
|
> **标签**:`#EXECUTION-MAKER-TAKER`
|
||||||
|
> **状态**:V5.4 设计文档,待 3/11 A/B test 结束后进入实现阶段
|
||||||
|
> **作者**:小范(xiaofan)+ 露露(lulu)
|
||||||
|
> **日期**:2026-03-06
|
||||||
205
docs/arbitrage-engine/funding-rate-arbitrage-plan.md
Normal file
205
docs/arbitrage-engine/funding-rate-arbitrage-plan.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
title: Funding Rate Arbitrage Plan v2
|
||||||
|
---
|
||||||
|
|
||||||
|
> 初版日期:2026年2月
|
||||||
|
> v2更新:2026年2月24日(露露×小周15轮联合数据验证)
|
||||||
|
> 数据来源:Binance官方API实算,覆盖2019-2026全周期
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心定位
|
||||||
|
|
||||||
|
**自用低风险稳定收益策略**,不依赖行情判断,赚市场机制的钱。
|
||||||
|
|
||||||
|
- 目标年化:全周期均值 **11-14%**(PM模式净值)
|
||||||
|
- 风险等级:低(完全对冲,不暴露方向风险)
|
||||||
|
- 对标:大幅优于债基(年化2%)、银行理财(3-4%)
|
||||||
|
- 执行方式:**选好点位买入后长期持有不动**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 原理
|
||||||
|
|
||||||
|
永续合约每8小时结算一次资金费率:
|
||||||
|
- 多头多 → 多头付钱给空头(费率为正)
|
||||||
|
- 空头多 → 空头付钱给多头(费率为负)
|
||||||
|
|
||||||
|
**套利做法**:现货买入 + 永续做空,完全对冲币价风险,净收资金费率(USDT结算)。
|
||||||
|
|
||||||
|
```
|
||||||
|
买入1 BTC现货($96,000)
|
||||||
|
做空1 BTC永续($96,000,1倍杠杆)
|
||||||
|
|
||||||
|
BTC涨跌:两边对冲,净盈亏=0
|
||||||
|
资金费率:每8小时直接收USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 全周期真实数据(2019-2026,Binance实算)
|
||||||
|
|
||||||
|
### BTC(BTCUSDT永续,2019.9至今)
|
||||||
|
|
||||||
|
| 年份 | 年化毛收益 | 市场特征 |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 2019 | 7.48% | 起步期 |
|
||||||
|
| 2020 | 17.19% | 牛市前奏 |
|
||||||
|
| **2021** | **30.61%** | **大牛市** |
|
||||||
|
| 2022 | 4.16% | 熊市 |
|
||||||
|
| 2023 | 7.87% | 复苏 |
|
||||||
|
| 2024 | 11.92% | 牛市 |
|
||||||
|
| 2025 | 5.13% | 震荡 |
|
||||||
|
| 2026 YTD | 2.71% | 震荡 |
|
||||||
|
| **全周期均值** | **12.33%** | - |
|
||||||
|
| **PM净年化** | **11.67%** | 扣0.06%手续费 |
|
||||||
|
|
||||||
|
- 每8小时费率均值:0.011257%
|
||||||
|
- 负费率占比:13.07%
|
||||||
|
|
||||||
|
### ETH(ETHUSDT永续,2019.11至今)
|
||||||
|
|
||||||
|
| 年份 | 年化毛收益 | 市场特征 |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 2019 | 8.91% | 起步期 |
|
||||||
|
| 2020 | 27.41% | 牛市前奏 |
|
||||||
|
| **2021** | **37.54%** | **大牛市** |
|
||||||
|
| 2022 | 0.79% | 熊市 |
|
||||||
|
| 2023 | 8.26% | 复苏 |
|
||||||
|
| 2024 | 12.96% | 牛市 |
|
||||||
|
| 2025 | 4.93% | 震荡 |
|
||||||
|
| 2026 YTD | 0.83% | 震荡 |
|
||||||
|
| **全周期均值** | **14.87%** | - |
|
||||||
|
| **PM净年化** | **14.09%** | 扣0.06%手续费 |
|
||||||
|
|
||||||
|
- 每8小时费率均值:0.013584%
|
||||||
|
- 负费率占比:12.17%
|
||||||
|
|
||||||
|
### BTC+ETH 50/50组合
|
||||||
|
|
||||||
|
- **全周期组合年化毛收益:13.81%**
|
||||||
|
- **PM净年化:13.08%**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键结论(数据验证后确认)
|
||||||
|
|
||||||
|
1. **全周期PM净年化11-14%**,远超债基和银行理财
|
||||||
|
2. **收益是USDT**,不承受币价波动风险
|
||||||
|
3. **费率为负时不动(A方案)**:负费率仅占12-13%,长期均值为正
|
||||||
|
4. **只做BTC和ETH**:流动性最好、费率最稳定,不做山寨币
|
||||||
|
5. **年际波动大**:牛市30%+,熊市0-4%,需要耐心
|
||||||
|
6. **50万美金在BTC/ETH市场无滑点问题**
|
||||||
|
7. **策略已被机构验证**:Ethena(TVL数十亿)、Binance Smart Arbitrage、Presto Research回测
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Portfolio Margin(组合保证金)· 必须开通
|
||||||
|
|
||||||
|
**为什么必须用PM模式**:
|
||||||
|
|
||||||
|
| 维度 | 传统模式 | Portfolio Margin |
|
||||||
|
|------|---------|-----------------|
|
||||||
|
| $50万可做规模 | ~$25万(一半做保证金) | ~$47.5万(95%利用率) |
|
||||||
|
| 额外USDT需求 | 需$25万USDT | 不需要 |
|
||||||
|
| 年化收益 | ~6%(资金效率低) | ~12%(资金效率高) |
|
||||||
|
| 风险识别 | 不识别对冲 | 识别对冲,保证金需求低 |
|
||||||
|
|
||||||
|
**PM关键信息**:
|
||||||
|
- Binance 2024年10月已取消最低余额要求,所有用户可开
|
||||||
|
- BTC/ETH抵押率:0.95(5%折扣)
|
||||||
|
- 支持360+种加密货币作为抵押品
|
||||||
|
|
||||||
|
**PM风控线**:
|
||||||
|
- uniMMR < 1.35 → 内部预警
|
||||||
|
- uniMMR < 1.25 → 评估减仓
|
||||||
|
- uniMMR < 1.20 → Binance限制开新仓
|
||||||
|
- uniMMR < 1.05 → 触发强平
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 开仓
|
||||||
|
- 确认市场趋势(不急,等好点位)
|
||||||
|
- 开通Portfolio Margin
|
||||||
|
- 同时执行:现货买入BTC/ETH + 永续做空等值(1倍杠杆)
|
||||||
|
|
||||||
|
### 持仓
|
||||||
|
- **完全不动**,每8小时自动收取资金费率
|
||||||
|
- 费率为负不平仓,长期均值为正
|
||||||
|
- 只监控uniMMR安全垫
|
||||||
|
|
||||||
|
### 平仓条件(极端情况)
|
||||||
|
- 正常情况:**不平仓,长期持有**
|
||||||
|
- 极端情况:uniMMR接近1.25时评估是否减仓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 手续费说明(PM模式下影响极小)
|
||||||
|
|
||||||
|
| 操作 | 0.06%费率档 | $50万单次 |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| 现货买入 | 0.06% | $285 |
|
||||||
|
| 永续开空 | 0.02% | $95 |
|
||||||
|
| **开仓合计** | | **$380** |
|
||||||
|
|
||||||
|
因为"买入后不动",手续费只在开仓时发生一次。
|
||||||
|
$50万本金年收益约$6万(12%),$380手续费可忽略。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 风险清单
|
||||||
|
|
||||||
|
| 风险 | 严重程度 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 市场周期 | ⚠️ 中 | 熊市年化可降至0-4%,但不亏本金 |
|
||||||
|
| 费率持续为负 | 🟢 低 | 历史负费率占比仅12-13%,长期均值为正 |
|
||||||
|
| 交易所对手方 | ⚠️ 中 | FTX教训,建议未来考虑分散 |
|
||||||
|
| 爆仓(PM模式) | 🟢 低 | 1倍杠杆+对冲,理论需BTC翻倍才触发 |
|
||||||
|
| 基差波动 | 🟢 低 | 长期持有不影响,只影响平仓时机 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与替代方案对比
|
||||||
|
|
||||||
|
| 方案 | 年化 | 风险 | 操作难度 | 备注 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| **本方案(自建)** | 11-14% | 低 | 中 | 完全自主可控 |
|
||||||
|
| Ethena sUSDe | 4-15% | 中(合约风险) | 低 | DeFi,依赖协议安全 |
|
||||||
|
| Binance Smart Arbitrage | 未知 | 低 | 低 | 官方产品,黑盒 |
|
||||||
|
| 银行理财 | 3-4% | 极低 | 无 | 对标基准 |
|
||||||
|
| 债基 | 2% | 低 | 无 | 对标基准 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行计划
|
||||||
|
|
||||||
|
### 第一阶段:准备(现在)
|
||||||
|
- [ ] 开通Binance Portfolio Margin
|
||||||
|
- [ ] 确认VIP等级和手续费档位
|
||||||
|
- [ ] 准备资金(USDT入金)
|
||||||
|
|
||||||
|
### 第二阶段:模拟验证(可选,1-2个月)
|
||||||
|
- [ ] 搭建模拟系统,实时跑数据验证
|
||||||
|
- [ ] 对比模拟结果与历史回测
|
||||||
|
|
||||||
|
### 第三阶段:入场
|
||||||
|
- [ ] 等待合适入场时机(趋势确认)
|
||||||
|
- [ ] 同时开BTC+ETH对冲仓位
|
||||||
|
- [ ] 开始收费率
|
||||||
|
|
||||||
|
### 第四阶段:长期持有
|
||||||
|
- [ ] 每日检查uniMMR安全垫
|
||||||
|
- [ ] 每周记录累计收益
|
||||||
|
- [ ] 不主动平仓,长期持有
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据验证过程
|
||||||
|
|
||||||
|
本文档数据经过露露(Claude Opus 4.6)和小周(GPT-5.3-Codex)15轮交叉验证:
|
||||||
|
- 数据来源:Binance fapi/v1/fundingRate 官方API
|
||||||
|
- 覆盖周期:2019年9月至2026年2月(约6.5年)
|
||||||
|
- 验证内容:费率均值、年化收益、负费率占比、手续费敏感性、PM模式资金效率
|
||||||
|
- 外部参考:Presto Research、Ethena、CoinCryptoRank、FMZ量化
|
||||||
20
docs/arbitrage-engine/index.mdx
Normal file
20
docs/arbitrage-engine/index.mdx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Project ArbitrageEngine
|
||||||
|
---
|
||||||
|
|
||||||
|
套利引擎(ArbitrageEngine,简称 AE)— 资金费率自动化套利系统 + 短线交易信号系统。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
🟢 **V2-V4 已上线** — 权限管控 + aggTrades采集 + 成交流分析面板
|
||||||
|
🟡 **V5 方案定稿** — 短线交易信号系统,待开发
|
||||||
|
🔗 **面板地址**:https://arb.zhouyangclaw.com
|
||||||
|
⏳ **实盘阻塞**:等范总提供Binance API Key + PM + 资金
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [V5 短线交易信号系统方案](/docs/project-arbitrage-engine/v5-signal-system) — 信号体系、风控、回测方案、开发路线图 🆕
|
||||||
|
- [V2-V4 产品技术文档](/docs/project-arbitrage-engine/v2-v4-plan) — 权限管控 / aggTrades / 成交流面板
|
||||||
|
- [Phase 0 开发进度](/docs/project-arbitrage-engine/phase0-progress) — 已完成功能、Git结构
|
||||||
|
- [Funding Rate Arbitrage Plan v2](/docs/project-arbitrage-engine/funding-rate-arbitrage-plan) — 策略原理、数据验证、执行方案
|
||||||
|
- [Requirements v1.3.1](/docs/project-arbitrage-engine/requirements-v1.3) — 完整PRD(产品+技术+商业)
|
||||||
138
docs/arbitrage-engine/phase0-progress.md
Normal file
138
docs/arbitrage-engine/phase0-progress.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
title: Phase 0 开发进度
|
||||||
|
---
|
||||||
|
|
||||||
|
> 更新日期:2026年2月27日
|
||||||
|
> 状态:🟡 Phase 0 进行中(监控面板已上线,SaaS MVP已上线)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — 已完成
|
||||||
|
|
||||||
|
### 监控面板(arb.zhouyangclaw.com)✅
|
||||||
|
|
||||||
|
**上线时间**:2026-02-26
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- 后端:FastAPI(Python,端口4332)
|
||||||
|
- 前端:Next.js 16 + shadcn/ui + Tailwind + Recharts(端口4333)
|
||||||
|
- 部署:小周服务器 34.84.9.167,Caddy反代,HTTPS
|
||||||
|
|
||||||
|
**已实现功能**:
|
||||||
|
|
||||||
|
| 页面 | 功能 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 仪表盘(/) | BTC/ETH实时费率、标记价格、下次结算时间 | ✅ 2秒刷新 |
|
||||||
|
| 历史(/history) | 过去7天费率走势图(Recharts折线图)+ 历史记录表格 | ✅ |
|
||||||
|
| 信号(/signals) | 套利信号历史记录(触发时间/币种/年化) | ✅ |
|
||||||
|
| 说明(/about) | 策略原理、历史年化数据表、风险说明 | ✅ |
|
||||||
|
|
||||||
|
**API端点**:
|
||||||
|
```
|
||||||
|
GET /api/health — 健康检查
|
||||||
|
GET /api/rates — 实时资金费率(BTC/ETH)
|
||||||
|
GET /api/history — 7天历史费率数据
|
||||||
|
GET /api/stats — 7天统计(均值/年化/50-50组合)
|
||||||
|
```
|
||||||
|
|
||||||
|
**性能优化**:
|
||||||
|
- rates:3秒缓存(2秒前端刷新,不会触发限速)
|
||||||
|
- history/stats:60秒缓存(避免Binance API限速)
|
||||||
|
- User-Agent已设置(防403)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 信号推送系统 ✅
|
||||||
|
|
||||||
|
**逻辑**:BTC或ETH 7日年化 > 10% 时自动触发
|
||||||
|
|
||||||
|
**推送渠道**:Discord Bot API(#agent-team频道)
|
||||||
|
|
||||||
|
**信号格式**:
|
||||||
|
```
|
||||||
|
📊 套利信号
|
||||||
|
BTC 7日年化: 12.33%
|
||||||
|
ETH 7日年化: 8.17%
|
||||||
|
建议:BTC 现货+永续对冲可开仓
|
||||||
|
时间: 2026-02-26 14:00 UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
**定时任务**:每小时检查一次(crontab,小周服务器)
|
||||||
|
|
||||||
|
**数据库**:SQLite(arb.db),signal_logs表记录推送历史
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SaaS MVP 用户系统 ✅
|
||||||
|
|
||||||
|
**上线时间**:2026-02-26
|
||||||
|
|
||||||
|
**新增页面**:
|
||||||
|
|
||||||
|
| 页面 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| /register | 邮箱+密码注册,支持绑定Discord ID |
|
||||||
|
| /login | JWT登录 |
|
||||||
|
| /dashboard | 用户面板:订阅等级、Discord绑定、升级入口 |
|
||||||
|
|
||||||
|
**API端点**:
|
||||||
|
```
|
||||||
|
POST /api/auth/register — 注册
|
||||||
|
POST /api/auth/login — 登录(返回JWT)
|
||||||
|
POST /api/user/bind-discord — 绑定Discord ID
|
||||||
|
GET /api/user/me — 获取用户信息
|
||||||
|
GET /api/signals/history — 信号历史
|
||||||
|
```
|
||||||
|
|
||||||
|
**订阅等级预设**(支付接入前为占位):
|
||||||
|
| 等级 | 价格 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| 免费 | ¥0 | 实时费率面板 |
|
||||||
|
| Pro | ¥99/月 | 信号推送+历史数据 |
|
||||||
|
| Premium | ¥299/月 | Pro全部+定制阈值+优先客服 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git仓库
|
||||||
|
|
||||||
|
- **地址**:https://git.darkerilclaw.com/lulu/arbitrage-engine
|
||||||
|
- **主要目录结构**:
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
main.py — FastAPI主文件(rates/history/stats + 缓存)
|
||||||
|
auth.py — 用户注册/登录/JWT
|
||||||
|
subscriptions.py — 订阅管理
|
||||||
|
signal_pusher.py — 信号检测+Discord推送
|
||||||
|
frontend/
|
||||||
|
app/
|
||||||
|
page.tsx — 仪表盘
|
||||||
|
history/ — 历史页
|
||||||
|
signals/ — 信号历史页
|
||||||
|
about/ — 策略说明页
|
||||||
|
register/ — 注册页
|
||||||
|
login/ — 登录页
|
||||||
|
dashboard/ — 用户面板
|
||||||
|
components/
|
||||||
|
Navbar.tsx — 响应式导航(手机端汉堡菜单)
|
||||||
|
RateCard.tsx — 费率卡片
|
||||||
|
StatsCard.tsx — 统计卡片
|
||||||
|
FundingChart.tsx — 费率走势图
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — 待开发
|
||||||
|
|
||||||
|
> 需范总提供Binance API Key后开始
|
||||||
|
|
||||||
|
| 功能 | 依赖 | 预估工时 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 接入真实Binance账户余额/持仓 | Binance API Key(只读权限) | 1天 |
|
||||||
|
| 手动开仓/平仓界面 | 范总确认Portfolio Margin已开通 | 2天 |
|
||||||
|
| 自动再平衡(持仓期间) | Phase 1基础完成后 | 2天 |
|
||||||
|
| 风控熔断+自动告警 | — | 1天 |
|
||||||
|
|
||||||
|
**Phase 1开启条件(范总需提供)**:
|
||||||
|
1. Binance API Key(Read + Trade,禁止Withdraw)
|
||||||
|
2. 确认Portfolio Margin账户已开通
|
||||||
|
3. 初始资金就位(建议$500 = BTC$250 + ETH$250)
|
||||||
540
docs/arbitrage-engine/requirements-v1.3.md
Normal file
540
docs/arbitrage-engine/requirements-v1.3.md
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
---
|
||||||
|
title: Requirements v1.3.1
|
||||||
|
---
|
||||||
|
|
||||||
|
> 定稿日期:2026年2月24日
|
||||||
|
> 参与:露露(Claude Opus 4.6)、小周(GPT-5.3-Codex)、范总确认
|
||||||
|
> 状态:✅ 需求锁定,待开发
|
||||||
|
> 版本:v1.3.1(部署方案确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 文档定位
|
||||||
|
|
||||||
|
- **文档类型**:项目基石级 PRD + 技术需求定稿(用于研发、验收、后续商业化)
|
||||||
|
- **优先级**:安全性 > 可控性 > 稳定性 > 收益表现 > 开发速度
|
||||||
|
- **约束前提**:先需求锁定,再开发;先 API 全测通,再实盘;先 Dry Run 1 周,再 Real
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目定义与商业化定位
|
||||||
|
|
||||||
|
- **产品名**:套利引擎(ArbitrageEngine,简称 AE)
|
||||||
|
- **定位升级**:从 v1.2 的"范总单用户工具"升级为"可商业化产品雏形"
|
||||||
|
- Phase 1:服务范总实盘验证(单用户形态)
|
||||||
|
- 但架构上 Day1 预埋多租户和计费能力,避免二次重构
|
||||||
|
- **核心价值主张**:
|
||||||
|
- 对用户:低门槛执行资金费率套利(PM + 风控 + 可视化)
|
||||||
|
- 对团队:沉淀可复制交易基础设施,具备对外 SaaS 化能力
|
||||||
|
- **竞争差异化**:
|
||||||
|
- 完整前端(非CLI黑框)
|
||||||
|
- 安全第一(TOTP、审计、熔断)
|
||||||
|
- Dry Run验证(降低用户心理门槛)
|
||||||
|
- PM优化(资金效率提升)
|
||||||
|
- 自定义风控(非黑盒)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标与边界
|
||||||
|
|
||||||
|
**目标(Phase 1)**:
|
||||||
|
- Binance 上完成 BTC/ETH 对冲套利闭环
|
||||||
|
- 支持 PM 模式,执行"开/平人工确认,中间自动运行"
|
||||||
|
- 全链路审计、风控、告警、报表可用
|
||||||
|
|
||||||
|
**非目标(Phase 1 不做)**:
|
||||||
|
- 不做多交易所聚合
|
||||||
|
- 不做自动择时开平仓
|
||||||
|
- 不做资金托管与代客资管
|
||||||
|
- 不做公开注册与支付计费(仅预埋)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 已确认业务决策(锁定)
|
||||||
|
|
||||||
|
| 项目 | 确认结果 |
|
||||||
|
|------|---------|
|
||||||
|
| 执行模式 | C — 开仓/平仓需人工确认,持仓期间自动化 |
|
||||||
|
| 初始实盘资金 | $500(BTC $250 + ETH $250) |
|
||||||
|
| 收益处理 | 留账户复利,不自动提取 |
|
||||||
|
| 部署服务器 | 小周服务器 34.84.9.167(GCP东京,Binance延迟11ms) |
|
||||||
|
| 上线节奏 | Phase 0 API测通 → Dry Run 1周 → Real 2个月 |
|
||||||
|
| 费率为负 | 完全不动(A方案) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 系统状态机
|
||||||
|
|
||||||
|
### 模式状态
|
||||||
|
```
|
||||||
|
DRY_RUN ──(Phase 0 checklist 100%通过 + 范总确认)──→ REAL
|
||||||
|
REAL ──(手动降级)──→ DRY_RUN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 交易生命周期
|
||||||
|
```
|
||||||
|
IDLE ──(用户点"开始执行"+二次确认)──→ OPENING
|
||||||
|
OPENING ──(双腿成交+偏差≤1%)──→ HOLDING
|
||||||
|
OPENING ──(单边失败+补偿失败/超时/API连续失败)──→ HALTED
|
||||||
|
HOLDING ──(用户点"平仓"+确认)──→ CLOSING
|
||||||
|
HOLDING ──(uniMMR<1.25)──→ HALTED(不自动平仓,禁止新操作,立即告警)
|
||||||
|
CLOSING ──(双腿平完+仓位归零)──→ IDLE
|
||||||
|
任意状态 ──(硬风控触发/关键系统异常)──→ HALTED
|
||||||
|
HALTED ──(范总手动恢复)──→ IDLE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 风控参数(硬编码,不可前端修改)
|
||||||
|
|
||||||
|
| 参数 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 最大单次名义 | $600 | $500本金+20%缓冲 |
|
||||||
|
| 最大滑点 | 0.10% | 超过取消下单 |
|
||||||
|
| 最大对冲偏差 | 1.00% | \|spot-perp\|/target |
|
||||||
|
| 最大杠杆 | 1x | 不可修改 |
|
||||||
|
| uniMMR预警线 | 1.35 | 告警 |
|
||||||
|
| uniMMR危险线 | 1.25 | 自动HALTED |
|
||||||
|
| API失败熔断 | 连续5次 | 进入HALTED |
|
||||||
|
| 单腿超时(补单) | 8s | 触发补单逻辑 |
|
||||||
|
| 单腿超时(熔断) | 30s | 触发HALTED |
|
||||||
|
| 时钟漂移阈值 | >1000ms | 禁止交易请求 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Dry Run 对照机制
|
||||||
|
|
||||||
|
Dry Run 期间必须记录"拟交易账本":
|
||||||
|
- 拟下单价格、拟成交数量、拟手续费、拟持仓
|
||||||
|
- 拟资金费率收入(按实际市场费率与拟仓位估算)
|
||||||
|
|
||||||
|
**产出对照报告**:
|
||||||
|
- 如果 Real 执行,理论会得到的收益区间
|
||||||
|
- 引擎计算准确性与一致性验证
|
||||||
|
|
||||||
|
**通过标准**:Dry Run 1周内无关键错配、无状态机死锁、无风险漏报
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 功能模块清单(Phase 1)
|
||||||
|
|
||||||
|
### 认证与安全
|
||||||
|
- 账号密码 + TOTP(Google Authenticator)
|
||||||
|
- 会话超时(30分钟)
|
||||||
|
- 连续登录失败锁定(5次失败后锁定15分钟)
|
||||||
|
- 审计日志全记录(append-only)
|
||||||
|
|
||||||
|
### Binance 接入
|
||||||
|
- API权限自检(Read/Trade only,禁Withdraw)
|
||||||
|
- 行情、费率、账户、持仓、下单、回查
|
||||||
|
- 连接状态实时监控(心跳检测)
|
||||||
|
- API调用频率控制(避免触发限流)
|
||||||
|
|
||||||
|
### 执行引擎
|
||||||
|
- REST并发下单(现货买入 + 永续做空同时发出)
|
||||||
|
- WebSocket订阅成交回报、仓位变化
|
||||||
|
- 单边失败补偿、对冲偏差修复、熔断
|
||||||
|
- 下单前检查:余额、价格、滑点预估
|
||||||
|
|
||||||
|
### 监控告警
|
||||||
|
- uniMMR、对冲偏差、当前/预测费率、累计收益
|
||||||
|
- Discord实时告警 + 每日自动汇报
|
||||||
|
- 双通道告警预留(Discord + 邮件/短信)
|
||||||
|
|
||||||
|
### 报表
|
||||||
|
- 8小时/日/周/月收益统计
|
||||||
|
- 手续费统计、资金费率贡献
|
||||||
|
- 净值曲线图表
|
||||||
|
- Dry Run对照报告
|
||||||
|
|
||||||
|
### 运维
|
||||||
|
- PM2健康检查、自动拉起
|
||||||
|
- 重启后自动对账恢复
|
||||||
|
- 分api/worker进程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 多租户架构预埋(v1.3核心新增)
|
||||||
|
|
||||||
|
### 数据模型要求
|
||||||
|
- 所有核心业务表强制包含 `user_id`(或 `tenant_id`)
|
||||||
|
|
||||||
|
### 执行隔离要求
|
||||||
|
- Worker任务必须携带 `user_id` 上下文
|
||||||
|
- 严禁跨用户读取订单、仓位、密钥、日志
|
||||||
|
|
||||||
|
### 密钥管理
|
||||||
|
- 按用户分区加密存储(每用户独立密钥上下文)
|
||||||
|
- 主密钥与业务库分离,密钥不落盘
|
||||||
|
|
||||||
|
### 权限模型预留
|
||||||
|
- 用户表保留 `role` 字段(RBAC扩展位)
|
||||||
|
- 前端不写死"唯一管理员"
|
||||||
|
|
||||||
|
### 审计不可篡改
|
||||||
|
- 审计日志 append-only,禁止更新/删除业务接口
|
||||||
|
- 仅允许归档,不允许逻辑改写
|
||||||
|
- 预留哈希链字段(后续可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Binance API 接口清单
|
||||||
|
|
||||||
|
### 认证与账户(Phase 0 必测)
|
||||||
|
| 接口 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `GET /api/v3/account` | Spot账户资产、权限 |
|
||||||
|
| `GET /fapi/v2/account` | Futures账户信息 |
|
||||||
|
| `GET /fapi/v2/balance` | Futures余额 |
|
||||||
|
| `GET /fapi/v2/positionRisk` | 永续持仓风险 |
|
||||||
|
| `GET /fapi/v1/leverageBracket` | 杠杆/名义分档 |
|
||||||
|
| `GET /sapi/v1/portfolio/account` | PM账户信息 |
|
||||||
|
|
||||||
|
### 行情与费率
|
||||||
|
| 接口 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `GET /api/v3/ticker/bookTicker` | Spot最优买卖价 |
|
||||||
|
| `GET /fapi/v1/ticker/bookTicker` | Perp最优买卖价 |
|
||||||
|
| `GET /fapi/v1/premiumIndex` | 标记价格+当前/预测费率 |
|
||||||
|
| `GET /fapi/v1/fundingRate` | 历史费率 |
|
||||||
|
| `GET /api/v3/depth` | Spot深度(滑点估算) |
|
||||||
|
| `GET /fapi/v1/depth` | Perp深度(滑点估算) |
|
||||||
|
|
||||||
|
### 交易执行
|
||||||
|
| 接口 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `POST /api/v3/order` | Spot下单(市价/限价) |
|
||||||
|
| `GET /api/v3/order` | Spot订单回查 |
|
||||||
|
| `DELETE /api/v3/order` | Spot撤单 |
|
||||||
|
| `POST /fapi/v1/order` | Futures下单(开空/平空) |
|
||||||
|
| `GET /fapi/v1/order` | Futures订单回查 |
|
||||||
|
| `DELETE /fapi/v1/order` | Futures撤单 |
|
||||||
|
| `GET /fapi/v1/openOrders` | 未完成单查询 |
|
||||||
|
| WebSocket listenKey | 成交回报+仓位变化 |
|
||||||
|
|
||||||
|
### 资金与收益
|
||||||
|
| 接口 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `GET /fapi/v1/income` | 资金费率收入、手续费、盈亏 |
|
||||||
|
| `GET /fapi/v2/positionRisk` | 未实现PnL、保证金风险 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 前端页面清单
|
||||||
|
|
||||||
|
### `/login`
|
||||||
|
- 用户名/密码 + TOTP输入
|
||||||
|
- 登录审计记录
|
||||||
|
|
||||||
|
### `/dashboard`
|
||||||
|
- 当前模式(Dry Run / Real)+ 状态机状态
|
||||||
|
- BTC/ETH仓位卡片:持仓量、对冲偏差、当前/预测费率
|
||||||
|
- uniMMR + 风险灯(🟢 >1.35 / 🟡 1.25-1.35 / 🔴 <1.25)
|
||||||
|
- 当日收益、累计收益、手续费
|
||||||
|
- 净值收益曲线图
|
||||||
|
|
||||||
|
### `/execute`
|
||||||
|
- 参数展示(只读):BTC $250 / ETH $250
|
||||||
|
- 「开始执行(开仓)」按钮(二次确认弹窗)
|
||||||
|
- 「请求平仓」按钮(二次确认弹窗)
|
||||||
|
- 执行过程实时日志流(SSE)
|
||||||
|
|
||||||
|
### `/risk`
|
||||||
|
- 风控阈值参数表(只读展示)
|
||||||
|
- 告警历史列表
|
||||||
|
- 熔断记录 + 恢复操作按钮
|
||||||
|
|
||||||
|
### `/records`
|
||||||
|
- 订单记录(Spot/Futures,可筛选)
|
||||||
|
- 费率收入记录(8h维度,可按日/周/月汇总)
|
||||||
|
- 审计日志(全操作记录)
|
||||||
|
|
||||||
|
### `/settings`
|
||||||
|
- API Key配置(加密存储,显示为***)
|
||||||
|
- Discord Webhook配置
|
||||||
|
- 模式切换(Dry Run ↔ Real,需二次确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 数据库结构(PostgreSQL,v1.3)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 用户(多租户预埋)
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(256) NOT NULL,
|
||||||
|
totp_secret_enc VARCHAR(256) NOT NULL,
|
||||||
|
role VARCHAR(16) DEFAULT 'admin', -- RBAC预留
|
||||||
|
status VARCHAR(16) DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
last_login_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- API凭据(按用户分区)
|
||||||
|
CREATE TABLE api_credentials (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
account_name VARCHAR(64),
|
||||||
|
api_key_enc TEXT NOT NULL,
|
||||||
|
api_secret_enc TEXT NOT NULL,
|
||||||
|
perm_read BOOLEAN DEFAULT true,
|
||||||
|
perm_trade BOOLEAN DEFAULT true,
|
||||||
|
perm_withdraw BOOLEAN DEFAULT false,
|
||||||
|
ip_whitelist_ok BOOLEAN DEFAULT false,
|
||||||
|
pm_enabled BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 策略实例
|
||||||
|
CREATE TABLE strategy_instances (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
mode VARCHAR(16) NOT NULL, -- DRY_RUN / REAL
|
||||||
|
state VARCHAR(16) NOT NULL, -- IDLE / OPENING / HOLDING / CLOSING / HALTED
|
||||||
|
base_capital DECIMAL(18,2),
|
||||||
|
btc_alloc DECIMAL(18,2),
|
||||||
|
eth_alloc DECIMAL(18,2),
|
||||||
|
started_at TIMESTAMPTZ,
|
||||||
|
closed_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 订单
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
strategy_id INT REFERENCES strategy_instances(id),
|
||||||
|
venue VARCHAR(8) NOT NULL, -- SPOT / FUTURES
|
||||||
|
symbol VARCHAR(16) NOT NULL,
|
||||||
|
side VARCHAR(8) NOT NULL,
|
||||||
|
type VARCHAR(16) NOT NULL,
|
||||||
|
client_order_id VARCHAR(64),
|
||||||
|
exchange_order_id VARCHAR(64),
|
||||||
|
price DECIMAL(18,8),
|
||||||
|
orig_qty DECIMAL(18,8),
|
||||||
|
executed_qty DECIMAL(18,8),
|
||||||
|
status VARCHAR(16),
|
||||||
|
is_reduce_only BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 仓位快照
|
||||||
|
CREATE TABLE positions_snapshot (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
strategy_id INT REFERENCES strategy_instances(id),
|
||||||
|
symbol VARCHAR(16) NOT NULL,
|
||||||
|
spot_qty DECIMAL(18,8),
|
||||||
|
futures_qty DECIMAL(18,8),
|
||||||
|
spot_notional DECIMAL(18,2),
|
||||||
|
futures_notional DECIMAL(18,2),
|
||||||
|
hedge_deviation DECIMAL(8,4),
|
||||||
|
mark_price DECIMAL(18,2),
|
||||||
|
captured_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 资金费率收入
|
||||||
|
CREATE TABLE funding_income (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
symbol VARCHAR(16) NOT NULL,
|
||||||
|
funding_time TIMESTAMPTZ NOT NULL,
|
||||||
|
income_asset VARCHAR(8),
|
||||||
|
income_amount DECIMAL(18,8),
|
||||||
|
rate DECIMAL(18,8),
|
||||||
|
position_notional DECIMAL(18,2),
|
||||||
|
source_tx_id VARCHAR(64)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 风控事件
|
||||||
|
CREATE TABLE risk_events (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
strategy_id INT REFERENCES strategy_instances(id),
|
||||||
|
event_type VARCHAR(32) NOT NULL,
|
||||||
|
severity VARCHAR(16) NOT NULL,
|
||||||
|
metric_value DECIMAL(18,8),
|
||||||
|
threshold_value DECIMAL(18,8),
|
||||||
|
action_taken VARCHAR(64),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 审计日志(append-only,不可删改)
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id),
|
||||||
|
actor VARCHAR(64) NOT NULL,
|
||||||
|
action VARCHAR(64) NOT NULL,
|
||||||
|
target VARCHAR(128),
|
||||||
|
payload_json JSONB,
|
||||||
|
ip VARCHAR(64),
|
||||||
|
hash_chain VARCHAR(128), -- 预留哈希链字段
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 系统运行记录
|
||||||
|
CREATE TABLE system_runs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT REFERENCES users(id) NOT NULL,
|
||||||
|
mode VARCHAR(16) NOT NULL,
|
||||||
|
start_at TIMESTAMPTZ NOT NULL,
|
||||||
|
end_at TIMESTAMPTZ,
|
||||||
|
result VARCHAR(16),
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 三阶段产品路线图
|
||||||
|
|
||||||
|
| 阶段 | 目标 | 交付 | 预计周期 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| **Phase 1** | 范总$500跑通2个月 | 单用户验证版(含风控+审计+告警+报表) | 开发2-3周 + 实盘2月 |
|
||||||
|
| **Phase 2** | 开放小规模内测 | 注册体系+RBAC+订阅计费+运维扩容 | 2-4周 |
|
||||||
|
| **Phase 3** | 公开获客规模化 | 营销站点+渠道投放+客户成功+合规增强 | 持续 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Phase 2 新增模块预览
|
||||||
|
|
||||||
|
### 账户体系
|
||||||
|
- 注册、邮箱验证、找回密码、设备管理、2FA强制策略
|
||||||
|
|
||||||
|
### RBAC权限
|
||||||
|
- Owner / Admin / Operator / Viewer 角色与资源权限矩阵
|
||||||
|
|
||||||
|
### 订阅计费
|
||||||
|
- 套餐分层、试用期、续费、到期降级、账单与发票记录
|
||||||
|
|
||||||
|
### 多租户运维
|
||||||
|
- 队列隔离、限流配额、租户级熔断、租户级监控面板
|
||||||
|
|
||||||
|
### 客服与支持
|
||||||
|
- 工单、系统公告、状态页
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 商业模式设计
|
||||||
|
|
||||||
|
### 方案A:SaaS订阅制(推荐先做)
|
||||||
|
- 参考定价:$29 / $59 / $99 月费分层(按功能与账户规模)
|
||||||
|
- 优势:合规压力相对低、收入稳定、扩展快
|
||||||
|
- 用户资金在自己Binance账户,平台不碰钱
|
||||||
|
|
||||||
|
### 方案B:收益分成制(后评估)
|
||||||
|
- 管理费(1-2%/年)+ 超额收益分成(20%)
|
||||||
|
- 风险:合规、牌照、托管责任显著上升
|
||||||
|
|
||||||
|
### 建议路径
|
||||||
|
- 先SaaS,再评估收益分成
|
||||||
|
- 不在Phase 1/2落地资管模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. 技术选型(锁定)
|
||||||
|
|
||||||
|
| 层 | 技术 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 执行层 | 自建引擎 | 不fork Hummingbot |
|
||||||
|
| API层 | Binance官方SDK | `@binance/connector` |
|
||||||
|
| 后端 | Node.js + Express | 和灵镜统一 |
|
||||||
|
| 前端 | Next.js | 深色UI |
|
||||||
|
| 数据库 | PostgreSQL | 多租户友好 |
|
||||||
|
| 进程管理 | PM2 | 分api/worker |
|
||||||
|
| 部署 | 34.84.9.167 (GCP东京) | Binance延迟11ms |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15.1 部署与网络安全方案(v1.3.1 新增)
|
||||||
|
|
||||||
|
### 部署服务器
|
||||||
|
- **机器**:34.84.9.167(GCP asia-northeast1-b)
|
||||||
|
- **选择理由**:
|
||||||
|
- Binance API TCP延迟 11ms(露露服务器22ms的一半)
|
||||||
|
- 内存可用 6.4GB、磁盘可用 187GB
|
||||||
|
- 仅1个PM2服务运行,负载极低
|
||||||
|
- GCP基础设施安全性高于普通VPS
|
||||||
|
- 非root用户运行(fzq1228)
|
||||||
|
|
||||||
|
### 网络访问方案(域名 + HTTPS + 强认证)
|
||||||
|
- **访问方式**:子域名 + Caddy自动HTTPS
|
||||||
|
- **不使用IP白名单**(范总IP不固定)
|
||||||
|
- **安全靠认证层保障**:
|
||||||
|
|
||||||
|
| 安全层 | 措施 |
|
||||||
|
|--------|------|
|
||||||
|
| 传输层 | HTTPS(Caddy自动TLS证书) |
|
||||||
|
| 认证层 | 账号密码 + TOTP双因素 |
|
||||||
|
| 会话层 | 30分钟超时自动登出 |
|
||||||
|
| 防暴破 | 连续5次登录失败锁定15分钟 |
|
||||||
|
| 审计层 | 所有登录/操作记录,append-only |
|
||||||
|
| 数据库 | PostgreSQL仅监听localhost |
|
||||||
|
| 进程隔离 | 单独系统用户运行套利引擎 |
|
||||||
|
|
||||||
|
### GCP防火墙规则(需配置)
|
||||||
|
- 开放端口:仅HTTPS(443)
|
||||||
|
- 其他端口:全部关闭
|
||||||
|
- SSH:仅密钥登录
|
||||||
|
|
||||||
|
### Phase 2 安全升级路径
|
||||||
|
- 可选:Cloudflare Zero Trust(Tunnel + Access认证)
|
||||||
|
- 可选:WAF规则(防SQL注入/XSS)
|
||||||
|
- 可选:Cloudflare Access策略(邮箱OTP二次验证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Phase 0 验收Checklist
|
||||||
|
|
||||||
|
### 认证与权限
|
||||||
|
- [ ] API签名请求成功
|
||||||
|
- [ ] 读取余额成功
|
||||||
|
- [ ] 读取持仓成功
|
||||||
|
- [ ] 验证无提现权限
|
||||||
|
- [ ] PM状态可读
|
||||||
|
|
||||||
|
### 行情与费率
|
||||||
|
- [ ] Spot/Perp bookTicker可读
|
||||||
|
- [ ] premiumIndex可读
|
||||||
|
- [ ] fundingRate历史可拉取(BTC/ETH)
|
||||||
|
- [ ] 深度数据可拉取
|
||||||
|
|
||||||
|
### 交易闭环
|
||||||
|
- [ ] Spot下单/回查/撤单成功
|
||||||
|
- [ ] Futures下单/回查/撤单成功
|
||||||
|
- [ ] WebSocket成交推送正常
|
||||||
|
- [ ] 单腿失败补偿流程演练通过
|
||||||
|
- [ ] 熔断触发与恢复流程通过
|
||||||
|
|
||||||
|
### 风控机制
|
||||||
|
- [ ] 滑点超阈拦截通过
|
||||||
|
- [ ] 对冲偏差超阈拦截通过
|
||||||
|
- [ ] API连续失败熔断通过
|
||||||
|
- [ ] 重启后自动对账通过
|
||||||
|
- [ ] uniMMR预警/危险触发通过
|
||||||
|
|
||||||
|
### 报表与告警
|
||||||
|
- [ ] 8小时收益计算正确
|
||||||
|
- [ ] 每日汇报自动推送成功
|
||||||
|
- [ ] 异常告警实时送达成功
|
||||||
|
|
||||||
|
**验收标准**:Checklist 100%通过才允许进入 1周 Dry Run。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. 风险与原则
|
||||||
|
|
||||||
|
- **原则**:真金白银系统,宁慢勿错
|
||||||
|
- **风险优先级**:执行错误 > 权限泄露 > 可用性 > 收益偏差
|
||||||
|
- **处置原则**:触发风险先停机后恢复,先对账后继续
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. 下一步(文档后动作)
|
||||||
|
|
||||||
|
1. 独立服务器准备完成
|
||||||
|
2. API Key权限配置完成(Read+Trade,禁Withdraw,白名单)
|
||||||
|
3. Phase 0执行计划与验收人确认
|
||||||
|
4. 开始开发
|
||||||
203
docs/arbitrage-engine/strategy-plaza-data-contract.md
Normal file
203
docs/arbitrage-engine/strategy-plaza-data-contract.md
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# 策略广场 数据合约文档
|
||||||
|
|
||||||
|
> 版本:v1.0
|
||||||
|
> 日期:2026-03-07
|
||||||
|
> 状态:待范总确认 ✅
|
||||||
|
> 作者:露露 + 小范
|
||||||
|
> 分支:`feature/strategy-plaza`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 功能概述
|
||||||
|
|
||||||
|
**策略广场**(Strategy Plaza)将现有的 signals-v53 / paper-v53 / signals-v53fast 等分散页面整合为统一入口:
|
||||||
|
|
||||||
|
- 总览页:策略卡片列表,展示每个策略的核心指标,30 秒自动刷新
|
||||||
|
- 详情页:点击卡片进入,顶部 Tab 切换「信号引擎」和「模拟盘」视图
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 前端路由
|
||||||
|
|
||||||
|
| 路由 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `/strategy-plaza` | 策略广场总览(卡片列表) |
|
||||||
|
| `/strategy-plaza/[id]` | 策略详情页,默认「信号引擎」tab |
|
||||||
|
| `/strategy-plaza/[id]?tab=paper` | 策略详情页「模拟盘」tab |
|
||||||
|
|
||||||
|
**侧边栏变更:**
|
||||||
|
- 新增「策略广场」单一入口
|
||||||
|
- 原 `signals-v53` / `paper-v53` / `signals-v53fast` / `paper-v53fast` / `signals-v53middle` / `paper-v53middle` 页面:**保留但从侧边栏隐藏**(路由仍可访问)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 设计
|
||||||
|
|
||||||
|
### 3.1 `GET /api/strategy-plaza`
|
||||||
|
|
||||||
|
返回所有策略的卡片摘要数据。
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "v53",
|
||||||
|
"display_name": "V5.3 标准版",
|
||||||
|
"status": "running",
|
||||||
|
"started_at": 1741234567000,
|
||||||
|
"initial_balance": 10000,
|
||||||
|
"current_balance": 8693,
|
||||||
|
"net_usdt": -1307,
|
||||||
|
"net_r": -6.535,
|
||||||
|
"trade_count": 63,
|
||||||
|
"win_rate": 49.2,
|
||||||
|
"avg_win_r": 0.533,
|
||||||
|
"avg_loss_r": -0.721,
|
||||||
|
"open_positions": 0,
|
||||||
|
"pnl_usdt_24h": -320,
|
||||||
|
"pnl_r_24h": -1.6,
|
||||||
|
"std_r": 0.9,
|
||||||
|
"last_trade_at": 1741367890000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | string | 策略唯一标识,与 DB strategy 字段一致 |
|
||||||
|
| `display_name` | string | 展示名称 |
|
||||||
|
| `status` | string | `running` / `paused` / `error` |
|
||||||
|
| `started_at` | number (ms) | 策略启动时间(暂用 paper_trades 第一条 entry_ts,后续补 strategy_meta 表) |
|
||||||
|
| `initial_balance` | number | 初始余额 USDT,固定 10000 |
|
||||||
|
| `current_balance` | number | 当前余额 = initial_balance + net_usdt |
|
||||||
|
| `net_usdt` | number | 累计盈亏 USDT = SUM(pnl_r) × 200 |
|
||||||
|
| `net_r` | number | 累计净 R |
|
||||||
|
| `trade_count` | number | 已出场交易数 |
|
||||||
|
| `win_rate` | number | 胜率 % |
|
||||||
|
| `avg_win_r` | number | 平均赢单 R |
|
||||||
|
| `avg_loss_r` | number | 平均亏单 R(负数) |
|
||||||
|
| `open_positions` | number | 当前活跃持仓数(exit_ts IS NULL) |
|
||||||
|
| `pnl_usdt_24h` | number | 最近 24h 盈亏 USDT |
|
||||||
|
| `pnl_r_24h` | number | 最近 24h 净 R |
|
||||||
|
| `std_r` | number | 所有已出场交易的 pnl_r 标准差(风险感知) |
|
||||||
|
| `last_trade_at` | number (ms) | 最近一笔成交的 exit_ts |
|
||||||
|
|
||||||
|
**status 判断逻辑:**
|
||||||
|
- `running`:paper_config 中 enabled=true 且最近 signal_indicators 记录 < 5 分钟
|
||||||
|
- `paused`:paper_config 中 enabled=false
|
||||||
|
- `error`:paper_config 中 enabled=true 但 signal_indicators 最新记录 > 5 分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `GET /api/strategy-plaza/[id]/summary`
|
||||||
|
|
||||||
|
返回单个策略的完整摘要,包含卡片字段 + 详情字段。
|
||||||
|
|
||||||
|
**Response(在 3.1 基础上增加):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "v53",
|
||||||
|
"display_name": "V5.3 标准版",
|
||||||
|
"cvd_windows": "30m / 4h",
|
||||||
|
"description": "标准版:30分钟+4小时CVD双轨,适配1小时信号周期",
|
||||||
|
"symbols": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT"],
|
||||||
|
"weights": {
|
||||||
|
"direction": 55,
|
||||||
|
"crowding": 25,
|
||||||
|
"environment": 15,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"signal_threshold": 75,
|
||||||
|
"flip_threshold": 85
|
||||||
|
},
|
||||||
|
"...所有 3.1 字段..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `GET /api/strategy-plaza/[id]/signals`
|
||||||
|
|
||||||
|
复用现有 `/api/signals` 逻辑,增加 `strategy` 过滤。接口参数和返回格式与现有保持一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 `GET /api/strategy-plaza/[id]/trades`
|
||||||
|
|
||||||
|
复用现有 `/api/paper-trades` 逻辑,增加 `strategy` 过滤。接口参数和返回格式与现有保持一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据来源映射
|
||||||
|
|
||||||
|
| 字段 | 数据来源 |
|
||||||
|
|------|---------|
|
||||||
|
| `net_usdt`, `net_r`, `trade_count`, `win_rate`, `avg_win_r`, `avg_loss_r` | `paper_trades` WHERE strategy=id AND exit_ts IS NOT NULL |
|
||||||
|
| `open_positions` | `paper_trades` WHERE strategy=id AND exit_ts IS NULL |
|
||||||
|
| `pnl_usdt_24h`, `pnl_r_24h` | `paper_trades` WHERE strategy=id AND exit_ts > NOW()-24h |
|
||||||
|
| `std_r` | STDDEV(pnl_r) FROM paper_trades WHERE strategy=id AND exit_ts IS NOT NULL |
|
||||||
|
| `started_at` | MIN(entry_ts) FROM paper_trades WHERE strategy=id |
|
||||||
|
| `last_trade_at` | MAX(exit_ts) FROM paper_trades WHERE strategy=id AND exit_ts IS NOT NULL |
|
||||||
|
| `status` | paper_config.json + signal_indicators 最新记录时间 |
|
||||||
|
| `cvd_windows`, `weights`, `thresholds` | backend/strategies/[id].json |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 前端组件规划
|
||||||
|
|
||||||
|
### 5.1 总览页组件
|
||||||
|
|
||||||
|
```
|
||||||
|
StrategyPlaza
|
||||||
|
└── StrategyCardGrid
|
||||||
|
└── StrategyCard (×N)
|
||||||
|
├── 策略名 + status badge (running/paused/error)
|
||||||
|
├── 运行时长 (now - started_at)
|
||||||
|
├── 当前余额 / 初始余额
|
||||||
|
├── 净盈亏 USDT + 净R(带颜色)
|
||||||
|
├── 胜率
|
||||||
|
├── 最近24h盈亏(小字)
|
||||||
|
└── 点击 → /strategy-plaza/[id]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 详情页组件
|
||||||
|
|
||||||
|
```
|
||||||
|
StrategyDetail
|
||||||
|
├── 顶部:策略名 + status + 运行时长
|
||||||
|
├── Tab 切换:[信号引擎] [模拟盘]
|
||||||
|
│
|
||||||
|
├── Tab: 信号引擎
|
||||||
|
│ └── 复用 SignalsV53Page 内容
|
||||||
|
│
|
||||||
|
└── Tab: 模拟盘
|
||||||
|
└── 复用 PaperV53Page 内容
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 实现计划
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 负责 |
|
||||||
|
|------|------|------|
|
||||||
|
| P0 | 后端 API `/api/strategy-plaza` | 露露 |
|
||||||
|
| P1 | 后端 API `/api/strategy-plaza/[id]/summary` | 露露 |
|
||||||
|
| P2 | 前端总览页(StrategyCard × 3) | 露露 |
|
||||||
|
| P3 | 前端详情页(Tab + 复用现有组件) | 露露 |
|
||||||
|
| P4 | 侧边栏整合(新增入口,隐藏旧页面) | 露露 |
|
||||||
|
| Review | 代码审阅 + 逻辑验证 | 小范 |
|
||||||
|
|
||||||
|
> 开发前等范总确认数据结构,不提前动代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 变更记录
|
||||||
|
|
||||||
|
| 版本 | 日期 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| v1.0 | 2026-03-07 | 初版,露露起草 + 小范审阅 |
|
||||||
417
docs/arbitrage-engine/v2-v4-plan.mdx
Normal file
417
docs/arbitrage-engine/v2-v4-plan.mdx
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
---
|
||||||
|
title: Arbitrage Engine V2-V4 产品+技术文档
|
||||||
|
description: 权限管控、aggTrades全量采集、成交流分析面板的完整设计
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arbitrage Engine V2-V4 产品+技术文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 当前状态(V1.0 ✅)
|
||||||
|
- 实时BTC/ETH资金费率监控(2秒刷新)
|
||||||
|
- K线图(本地2秒粒度聚合,9个周期)
|
||||||
|
- 历史费率走势 + 明细表
|
||||||
|
- YTD年化统计
|
||||||
|
- 信号推送(Discord)
|
||||||
|
- 用户注册/登录框架(无鉴权保护)
|
||||||
|
- URL: https://arb.zhouyangclaw.com
|
||||||
|
|
||||||
|
### 1.2 战略升级方向
|
||||||
|
从「公开费率监控面板」升级为「私有交易数据研究平台」。
|
||||||
|
|
||||||
|
核心目标:
|
||||||
|
1. 收集全量逐笔成交数据,建立独家数据资产
|
||||||
|
2. 结合K线与成交流,研究价格变动的微观成因
|
||||||
|
3. 通过复盘标注→模式识别→模拟盘验证,逐步量化短线策略
|
||||||
|
4. 数据不公开,邀请制访问
|
||||||
|
|
||||||
|
### 1.3 核心定位
|
||||||
|
> 炒币是情绪盘,每一段价格背后都代表着集体情绪走向。K线是情绪的结果,成交流是情绪的过程。我们要看到过程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. V2.0 — 权限管控 + 邀请制
|
||||||
|
|
||||||
|
### 2.1 JWT鉴权设计
|
||||||
|
|
||||||
|
**Token体系:**
|
||||||
|
| Token | 有效期 | 用途 |
|
||||||
|
|-------|--------|------|
|
||||||
|
| Access Token | 24小时 | API请求鉴权(放header) |
|
||||||
|
| Refresh Token | 7天 | 刷新access token |
|
||||||
|
|
||||||
|
**实现策略:** 在现有`auth.py`基础上增量改造,不重写。
|
||||||
|
|
||||||
|
**新增接口:**
|
||||||
|
- `POST /api/auth/login` → 返回 `{access_token, refresh_token, expires_in}`
|
||||||
|
- `POST /api/auth/refresh` → 用refresh token换新access token
|
||||||
|
- `POST /api/auth/register` → 注册(必须提供有效邀请码)
|
||||||
|
- `GET /api/auth/me` → 当前用户信息
|
||||||
|
|
||||||
|
**依赖库:** `python-jose[cryptography]` 或 `PyJWT`
|
||||||
|
|
||||||
|
### 2.2 邀请码机制
|
||||||
|
|
||||||
|
**表结构:**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE invite_codes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT UNIQUE NOT NULL, -- 8位随机码
|
||||||
|
created_by INTEGER, -- admin user_id
|
||||||
|
max_uses INTEGER DEFAULT 1, -- 默认一码一用
|
||||||
|
used_count INTEGER DEFAULT 0,
|
||||||
|
status TEXT DEFAULT 'active', -- active/disabled/exhausted
|
||||||
|
expires_at TEXT, -- 可选过期时间
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE invite_usage (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
used_at TEXT DEFAULT (datetime('now')),
|
||||||
|
FOREIGN KEY (code_id) REFERENCES invite_codes(id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**注册流程:**
|
||||||
|
1. 用户填写邀请码 + 用户名 + 密码
|
||||||
|
2. 后端验证:邀请码存在 + status=active + used_count < max_uses + 未过期
|
||||||
|
3. 创建用户 → 记录使用 → used_count+1
|
||||||
|
4. 返回JWT token对
|
||||||
|
|
||||||
|
### 2.3 路由保护
|
||||||
|
|
||||||
|
**公开路由(无需登录):**
|
||||||
|
- `GET /api/health`
|
||||||
|
- `POST /api/auth/login`
|
||||||
|
- `POST /api/auth/register`
|
||||||
|
- `POST /api/auth/refresh`
|
||||||
|
- `GET /api/rates`(基础费率,作为引流)
|
||||||
|
|
||||||
|
**受保护路由(需登录):**
|
||||||
|
- `GET /api/kline`
|
||||||
|
- `GET /api/snapshots`
|
||||||
|
- `GET /api/history`
|
||||||
|
- `GET /api/stats`
|
||||||
|
- `GET /api/stats/ytd`
|
||||||
|
- `GET /api/signals/history`
|
||||||
|
- `GET /api/trades/*`(V3新增)
|
||||||
|
- `GET /api/analysis/*`(V4新增)
|
||||||
|
|
||||||
|
**Admin路由(需admin角色):**
|
||||||
|
- `POST /api/admin/invite-codes` — 生成邀请码
|
||||||
|
- `GET /api/admin/invite-codes` — 查看所有邀请码
|
||||||
|
- `DELETE /api/admin/invite-codes/:id` — 禁用邀请码
|
||||||
|
- `GET /api/admin/users` — 查看所有用户
|
||||||
|
- `PUT /api/admin/users/:id/ban` — 封禁用户
|
||||||
|
|
||||||
|
### 2.4 权限模型
|
||||||
|
|
||||||
|
| 资源 | Guest | User | Admin |
|
||||||
|
|------|-------|------|-------|
|
||||||
|
| 基础费率 | ✅ | ✅ | ✅ |
|
||||||
|
| K线/历史/统计 | ❌ | ✅ | ✅ |
|
||||||
|
| 成交流数据 | ❌ | ✅ | ✅ |
|
||||||
|
| 分析面板 | ❌ | ✅ | ✅ |
|
||||||
|
| 标注 | ❌ | ✅(自己) | ✅(全部) |
|
||||||
|
| 邀请码管理 | ❌ | ❌ | ✅ |
|
||||||
|
| 用户管理 | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
### 2.5 Admin CLI工具
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成邀请码
|
||||||
|
python3 admin_cli.py gen-invite --count 5
|
||||||
|
|
||||||
|
# 查看邀请码状态
|
||||||
|
python3 admin_cli.py list-invites
|
||||||
|
|
||||||
|
# 禁用邀请码
|
||||||
|
python3 admin_cli.py disable-invite CODE123
|
||||||
|
|
||||||
|
# 查看用户
|
||||||
|
python3 admin_cli.py list-users
|
||||||
|
|
||||||
|
# 封禁用户
|
||||||
|
python3 admin_cli.py ban-user 42
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 前端改造
|
||||||
|
|
||||||
|
- 未登录用户:仪表盘只显示实时费率卡片(引流),其他区域显示blur遮挡 + 「登录后查看」
|
||||||
|
- 登录页:用户名 + 密码 + 邀请码(注册时)
|
||||||
|
- Token存储:`localStorage`(access_token + refresh_token)
|
||||||
|
- 请求拦截器:自动带token、过期自动刷新
|
||||||
|
- 401处理:跳转登录页
|
||||||
|
|
||||||
|
### 2.7 验收标准
|
||||||
|
- [ ] 无邀请码无法注册
|
||||||
|
- [ ] 无token访问受保护API返回401
|
||||||
|
- [ ] Token过期后refresh成功
|
||||||
|
- [ ] Admin API非admin用户返回403
|
||||||
|
- [ ] 前端未登录正确遮挡数据区域
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. V3.0 — aggTrades全量采集
|
||||||
|
|
||||||
|
### 3.1 数据模型
|
||||||
|
|
||||||
|
**按月分表,单库(arb.db):**
|
||||||
|
```sql
|
||||||
|
-- 自动建表(每月1张)
|
||||||
|
CREATE TABLE IF NOT EXISTS agg_trades_202602 (
|
||||||
|
agg_id INTEGER PRIMARY KEY, -- Binance aggTradeId(天然唯一)
|
||||||
|
symbol TEXT NOT NULL, -- BTCUSDT / ETHUSDT
|
||||||
|
price REAL NOT NULL,
|
||||||
|
qty REAL NOT NULL,
|
||||||
|
first_trade_id INTEGER,
|
||||||
|
last_trade_id INTEGER,
|
||||||
|
time_ms INTEGER NOT NULL, -- 成交时间(毫秒)
|
||||||
|
is_buyer_maker INTEGER NOT NULL -- 0=主动买, 1=主动卖
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agg_trades_202602_time
|
||||||
|
ON agg_trades_202602(symbol, time_ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
**辅助表:**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE agg_trades_meta (
|
||||||
|
symbol TEXT PRIMARY KEY,
|
||||||
|
last_agg_id INTEGER NOT NULL, -- 最后写入的agg_id
|
||||||
|
last_time_ms INTEGER NOT NULL, -- 最后写入的时间
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 采集架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ WebSocket主链路 │ ← wss://fstream.binance.com/ws/btcusdt@aggTrade
|
||||||
|
│ (实时推送) │ ← wss://fstream.binance.com/ws/ethusdt@aggTrade
|
||||||
|
└──────┬──────────────┘
|
||||||
|
│ 攒200条 or 1秒
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 批量写入器 │ → INSERT OR IGNORE INTO agg_trades_YYYYMM
|
||||||
|
│ (去重+分表路由) │ → UPDATE agg_trades_meta
|
||||||
|
└──────┬──────────────┘
|
||||||
|
│
|
||||||
|
┌──────┴──────────────┐
|
||||||
|
│ 补洞巡检(每分钟) │ → 检查agg_id连续性
|
||||||
|
│ │ → REST /fapi/v1/aggTrades?fromId=X 补缺
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**断线重连流程:**
|
||||||
|
1. WS断线 → 立即重连
|
||||||
|
2. 读取`last_agg_id` → REST `fromId=last_agg_id+1` 批量拉取(每次1000条)
|
||||||
|
3. 追平后切回WS流
|
||||||
|
4. 全程记日志
|
||||||
|
|
||||||
|
### 3.3 写入优化
|
||||||
|
|
||||||
|
- 批量大小:200条 or 1秒(取先到者)
|
||||||
|
- SQLite WAL模式 + `PRAGMA synchronous=NORMAL`
|
||||||
|
- 单线程写入,避免锁竞争
|
||||||
|
- 月初自动建新表
|
||||||
|
|
||||||
|
### 3.4 去重策略
|
||||||
|
|
||||||
|
- `agg_id`作为PRIMARY KEY,`INSERT OR IGNORE`天然去重
|
||||||
|
- WS和REST可能有重叠,完全靠PK去重,零额外成本
|
||||||
|
|
||||||
|
### 3.5 监控与告警
|
||||||
|
|
||||||
|
| 指标 | 阈值 | 动作 |
|
||||||
|
|------|------|------|
|
||||||
|
| 采集中断 | >30秒无新数据 | Discord告警 |
|
||||||
|
| agg_id断档 | 缺口>10 | 自动REST补洞 |
|
||||||
|
| 写入延迟P95 | >500ms | 日志警告 |
|
||||||
|
| 磁盘占用 | >80% | Discord告警 |
|
||||||
|
| 日数据完整性 | 缺口率>0.1% | 日报标红 |
|
||||||
|
|
||||||
|
### 3.6 存储容量规划
|
||||||
|
|
||||||
|
| 时间跨度 | BTC | ETH | 合计 |
|
||||||
|
|---------|-----|-----|------|
|
||||||
|
| 1天 | ~200MB | ~150MB | ~350MB |
|
||||||
|
| 1个月 | ~6GB | ~4.5GB | ~10.5GB |
|
||||||
|
| 6个月 | ~36GB | ~27GB | ~63GB |
|
||||||
|
| 1年 | ~73GB | ~55GB | ~128GB |
|
||||||
|
|
||||||
|
200GB磁盘可存1年+。超出时按月归档到外部存储。
|
||||||
|
|
||||||
|
### 3.7 数据查询接口(需登录)
|
||||||
|
|
||||||
|
- `GET /api/trades/raw?symbol=BTC&start_ms=X&end_ms=Y&limit=10000` — 原始成交
|
||||||
|
- `GET /api/trades/summary?symbol=BTC&start_ms=X&end_ms=Y&interval=1m` — 分钟级聚合
|
||||||
|
- 返回:`{time, buy_vol, sell_vol, delta, trade_count, vwap, max_single_qty}`
|
||||||
|
|
||||||
|
### 3.8 验收标准
|
||||||
|
- [ ] BTC/ETH双流并行采集,PM2常驻
|
||||||
|
- [ ] agg_id连续性>99.9%
|
||||||
|
- [ ] 断线重连+补洞在60秒内完成
|
||||||
|
- [ ] 每日完整性报告自动生成
|
||||||
|
- [ ] 查询API响应时间<2秒(10万条范围内)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. V4.0 — 成交流分析面板
|
||||||
|
|
||||||
|
### 4.1 页面布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ [BTC▼] [时间范围选择] [1m 5m 15m] │ ← 全局控制栏
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ K线图 (lightweight-charts) │ ← 可框选时间段
|
||||||
|
│ │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 成交流热图 (ECharts heatmap) │ ← 时间×价格×密度
|
||||||
|
│ 绿=主动买 红=主动卖 │
|
||||||
|
│ │
|
||||||
|
├──────────────────────┬─────────────────────┤
|
||||||
|
│ 时段摘要 │ 标注面板 │
|
||||||
|
│ 总成交量: 1,234 BTC │ [上涨前兆] [下跌前兆] │
|
||||||
|
│ 买/卖比: 62%/38% │ [震荡] [放量突破] │
|
||||||
|
│ Delta: +427 BTC │ │
|
||||||
|
│ 最大单笔: 12.5 BTC │ [保存标注] │
|
||||||
|
│ 成交速率: 89笔/秒 │ [AI分析] [导出] │
|
||||||
|
└──────────────────────┴─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 共享时间轴
|
||||||
|
|
||||||
|
使用`zustand`或React Context管理全局时间范围:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TimeRange {
|
||||||
|
startMs: number;
|
||||||
|
endMs: number;
|
||||||
|
source: 'kline' | 'heatmap' | 'control' | 'manual';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
K线框选、热图缩放、控制栏切换都更新同一个state,所有组件响应式联动。
|
||||||
|
|
||||||
|
### 4.3 热图实现
|
||||||
|
|
||||||
|
- 库:ECharts heatmap(首版)
|
||||||
|
- 数据格式:`[timeMs, priceLevel, volume]`
|
||||||
|
- 价格分档:按0.1%粒度(BTC约$67 = 1档)
|
||||||
|
- 颜色映射:买量→绿色深度,卖量→红色深度
|
||||||
|
- 数据量控制:15分钟窗口 ≈ 几千个格子,ECharts轻松渲染
|
||||||
|
|
||||||
|
### 4.4 复盘工具交互
|
||||||
|
|
||||||
|
**核心流程:**
|
||||||
|
1. 选择日期和币种
|
||||||
|
2. 浏览K线,找到明显波动区间
|
||||||
|
3. 框选 → 下方热图+摘要自动聚焦
|
||||||
|
4. 观察波动前5-15分钟的成交流特征
|
||||||
|
5. 发现有意义的模式 → 标注保存
|
||||||
|
6. 可选:点「AI分析」让AI解读该时段特征
|
||||||
|
|
||||||
|
### 4.5 标注系统
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE annotations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
start_ms INTEGER NOT NULL,
|
||||||
|
end_ms INTEGER NOT NULL,
|
||||||
|
label TEXT NOT NULL, -- 上涨前兆/下跌前兆/震荡/突破/假突破/洗盘...
|
||||||
|
note TEXT, -- 用户备注
|
||||||
|
confidence INTEGER DEFAULT 3, -- 1-5 置信度
|
||||||
|
version INTEGER DEFAULT 1, -- 版本号(修改时递增)
|
||||||
|
features_json TEXT, -- 当时的特征快照(delta/速率/大单等)
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 AI辅助分析
|
||||||
|
|
||||||
|
**流程:**
|
||||||
|
```
|
||||||
|
原始成交数据(该时段)
|
||||||
|
↓ Python预处理(后端)
|
||||||
|
结构化摘要(~500字):
|
||||||
|
- 成交速率变化曲线(文字描述)
|
||||||
|
- Delta累计走势
|
||||||
|
- 大单列表(>平均5倍)
|
||||||
|
- 价格关键点(高低点+突破位)
|
||||||
|
↓ AI分析
|
||||||
|
结论+建议(~200字)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token消耗:** ~3000 token/次分析,成本忽略不计。
|
||||||
|
|
||||||
|
### 4.7 验收标准
|
||||||
|
- [ ] K线与热图时间同步,无偏移
|
||||||
|
- [ ] 热图渲染15分钟窗口<1秒
|
||||||
|
- [ ] 标注CRUD正常,支持版本回溯
|
||||||
|
- [ ] AI分析响应<10秒
|
||||||
|
- [ ] 手机端可用(响应式布局)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 开发排期
|
||||||
|
|
||||||
|
| 版本 | 内容 | 预估工期 | 依赖 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| V2.0 | 权限管控+邀请制 | 2天 | 无 |
|
||||||
|
| V3.0 | aggTrades全量采集 | 2天 | V2.0(受保护路由) |
|
||||||
|
| V4.0 | 成交流分析面板 | 3-5天 | V3.0(数据源) |
|
||||||
|
|
||||||
|
**里程碑:**
|
||||||
|
- V2.0完成:所有数据需登录访问,邀请码可用
|
||||||
|
- V3.0完成:数据开始积累,每日完整性报告
|
||||||
|
- V3.0+2周:积累足够数据,可以开始第一次复盘分析
|
||||||
|
- V4.0完成:完整的复盘研究工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 安全与隐私
|
||||||
|
|
||||||
|
1. **成交流数据不公开** — 所有`/api/trades/*`和`/api/analysis/*`必须登录
|
||||||
|
2. **邀请码范总一人控制** — Admin角色仅范总
|
||||||
|
3. **JWT密钥** — 32字节随机,存环境变量
|
||||||
|
4. **SQLite安全** — WAL模式,每日备份
|
||||||
|
5. **数据永久保留** — 不删不改,原始完整性最重要
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 回滚与故障预案
|
||||||
|
|
||||||
|
| 故障 | 预案 |
|
||||||
|
|------|------|
|
||||||
|
| V2上线后鉴权阻断 | git回退到V1 commit + PM2 restart |
|
||||||
|
| V3采集进程崩溃 | PM2自动重启 + REST补洞 |
|
||||||
|
| 磁盘满 | 按月归档旧数据到外部存储 |
|
||||||
|
| SQLite损坏 | 每日备份恢复 |
|
||||||
|
| 前端build失败 | 保持上一版本运行,不restart |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 数据库迁移规范
|
||||||
|
|
||||||
|
- 每次DDL变更写migration脚本,带版本号
|
||||||
|
- 命名:`migration_001_add_invite_codes.sql`
|
||||||
|
- 所有migration必须幂等(`IF NOT EXISTS`)
|
||||||
|
- 发布顺序:先跑migration → 再更新代码 → 再restart服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本: v1.0*
|
||||||
|
*最后更新: 2026-02-27*
|
||||||
|
*作者: 露露(Opus 4.6) × 小周(GPT-5.3-Codex)*
|
||||||
239
docs/arbitrage-engine/v5-signal-system.mdx
Normal file
239
docs/arbitrage-engine/v5-signal-system.mdx
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
title: V5 短线交易信号系统方案
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5 短线交易信号系统方案
|
||||||
|
|
||||||
|
> 版本:v5.0 | 日期:2026-02-27 | 状态:方案定稿,待开发
|
||||||
|
>
|
||||||
|
> 来源:露露(Opus 4.6)× 小周(GPT-5.3-Codex)10轮讨论
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
将 aggTrades 成交流数据转化为**可执行的短线做多/做空交易信号**,实现从"监控工具"到"交易工具"的升级。
|
||||||
|
|
||||||
|
## 2. 信号体系
|
||||||
|
|
||||||
|
### 2.1 核心门槛(3/3 必须全部满足)
|
||||||
|
|
||||||
|
| # | 条件 | 做多 | 做空 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | CVD_fast (30m 滚动) | > 0 且斜率正 | < 0 且斜率负 |
|
||||||
|
| 2 | CVD_mid (4h 滚动) | > 0 | < 0 |
|
||||||
|
| 3 | VWAP 位置 | price > VWAP_30m | price < VWAP_30m |
|
||||||
|
|
||||||
|
**说明**:CVD = Cumulative Volume Delta(累计买卖差额),是成交流分析最核心的指标。
|
||||||
|
|
||||||
|
- **CVD_fast**:30分钟滚动窗口,捕捉短线动量
|
||||||
|
- **CVD_mid**:4小时滚动窗口,确认大方向
|
||||||
|
- **CVD_day**:UTC日内重置,作为盘中强弱基线参考(不作为入场条件)
|
||||||
|
|
||||||
|
> ⚠️ CVD_fast 在剧烈波动时自适应:当1m成交量超过均值3倍时,窗口自动拉长到60m,防噪音误判。
|
||||||
|
|
||||||
|
### 2.2 加分条件(决定仓位大小,满分60分)
|
||||||
|
|
||||||
|
| 条件 | 分值 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| ATR 压缩→扩张 | +25 | 5m ATR分位从 <40% 突破 >60%,波动开始放大 |
|
||||||
|
| 无反向 P99 超大单 | +20 | 最近15分钟内无极端反向成交 |
|
||||||
|
| 资金费率配合 | +15 | 做多时费率<0.01%,做空时费率>0.01% |
|
||||||
|
|
||||||
|
### 2.3 仓位映射
|
||||||
|
|
||||||
|
| 加分总分 | 仓位等级 | 占总资金 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| 0-15 | 最小仓 | 2% |
|
||||||
|
| 20-40 | 中仓 | 5% |
|
||||||
|
| 45-60 | 满仓 | 8% |
|
||||||
|
|
||||||
|
### 2.4 预估信号频率
|
||||||
|
|
||||||
|
核心3条件同时满足概率约 20-30%,每5分钟检查一次。去掉冷却期重复后,预计**日均 5-15 个有效信号**。
|
||||||
|
|
||||||
|
## 3. 指标计算
|
||||||
|
|
||||||
|
### 3.1 CVD(Cumulative Volume Delta)
|
||||||
|
|
||||||
|
```
|
||||||
|
CVD = Σ(主动买量) - Σ(主动卖量)
|
||||||
|
|
||||||
|
三轨并行:
|
||||||
|
- CVD_fast:滚动30m窗口(入场信号)
|
||||||
|
- CVD_mid:滚动4h窗口(方向过滤)
|
||||||
|
- CVD_day:UTC日内重置(盘中基线)
|
||||||
|
```
|
||||||
|
|
||||||
|
入场优先看 fast,方向必须与 mid 同向。
|
||||||
|
|
||||||
|
### 3.2 大单阈值(动态分位数)
|
||||||
|
|
||||||
|
```
|
||||||
|
基于最近24h成交量分布:
|
||||||
|
- P99:超大单阈值
|
||||||
|
- P95:大单阈值
|
||||||
|
- 兜底下限:max(P95, 5 BTC)
|
||||||
|
|
||||||
|
区分"大单买"和"大单卖"的不对称性:
|
||||||
|
- 上涨趋势中出现P99大卖单,意义远大于P99大买单
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 ATR(Average True Range)
|
||||||
|
|
||||||
|
```
|
||||||
|
周期:5分钟K线,14根
|
||||||
|
用于:
|
||||||
|
1. 波动压缩→扩张判断(分位数)
|
||||||
|
2. 止损距离计算
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 VWAP(Volume Weighted Average Price)
|
||||||
|
|
||||||
|
```
|
||||||
|
滚动30分钟:VWAP_30m = Σ(price × qty) / Σ(qty)
|
||||||
|
用于:价格位置过滤(做多 price > VWAP,做空 price < VWAP)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 风控体系
|
||||||
|
|
||||||
|
### 4.1 止盈止损
|
||||||
|
|
||||||
|
| 参数 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 止损 (SL) | 1.2 × ATR(5m, 14) | 动态止损,适应波动 |
|
||||||
|
| 止盈1 (TP1) | 1.0R | 减仓50% |
|
||||||
|
| 止盈2 (TP2) | 2.0R | 剩余仓位移动止损 |
|
||||||
|
| 时间止损 | 30分钟 | 无延续即平仓,防磨损 |
|
||||||
|
|
||||||
|
> R = 1倍止损距离
|
||||||
|
|
||||||
|
### 4.2 冲突与冷却
|
||||||
|
|
||||||
|
- **信号冲突**:持仓中出现反向信号 → 先平仓 + 10分钟冷却再接受新信号
|
||||||
|
- **同方向冷却**:10分钟内不重复入场
|
||||||
|
- **单币限频**:每小时最多2次入场
|
||||||
|
|
||||||
|
### 4.3 多品种风控
|
||||||
|
|
||||||
|
- BTC / ETH 独立出信号
|
||||||
|
- 两个同时同向时,总仓位上限 10%
|
||||||
|
- 需要相关性过滤(ETH Delta 经常跟 BTC 走)
|
||||||
|
|
||||||
|
## 5. 回测达标线
|
||||||
|
|
||||||
|
| 指标 | 达标线 |
|
||||||
|
|------|--------|
|
||||||
|
| 胜率 | ≥ 45% |
|
||||||
|
| 盈亏比 (Avg Win / Avg Loss) | ≥ 1.5 |
|
||||||
|
| 最大回撤 (MDD) | ≤ 5% |
|
||||||
|
| 日均信号数 | 2-8 个 |
|
||||||
|
| 扣手续费后 | 正收益 |
|
||||||
|
|
||||||
|
**手续费模型**:
|
||||||
|
- Maker: 0.02%, Taker: 0.04%(Portfolio Margin 档位)
|
||||||
|
- 按 Taker 0.04% 双向估算(保守)
|
||||||
|
- 回测报告必须包含净收益/毛收益对比
|
||||||
|
|
||||||
|
**额外统计**:
|
||||||
|
- 持仓时长分布(验证30min时间止损合理性)
|
||||||
|
- Rate-limit 重试统计(回补脚本用)
|
||||||
|
|
||||||
|
## 6. 技术架构
|
||||||
|
|
||||||
|
### 6.1 进程拓扑
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
Binance WS ──────→ │ agg-collector │ ──→ agg_trades_YYYYMM
|
||||||
|
└─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ signal-engine │ ──→ signal_indicators (5s)
|
||||||
|
│ │ ──→ signal_indicators_1m (聚合)
|
||||||
|
│ │ ──→ signal_trades
|
||||||
|
│ │ ──→ Discord推送
|
||||||
|
└─────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┐
|
||||||
|
│ backtest.py │ ──→ 回测报告
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **agg-collector**:只做采集+落库,不动(已有)
|
||||||
|
- **signal-engine**:新建独立进程,指标计算 + 信号生成
|
||||||
|
- **backtest.py**:离线回测脚本
|
||||||
|
|
||||||
|
### 6.2 数据库新增表
|
||||||
|
|
||||||
|
| 表名 | 用途 | 保留策略 |
|
||||||
|
|------|------|----------|
|
||||||
|
| signal_indicators | 每5秒指标快照(CVD/ATR/VWAP/P95等) | 30天 |
|
||||||
|
| signal_indicators_1m | 1分钟聚合(前端默认读此表) | 长期 |
|
||||||
|
| signal_trades | 信号触发的开仓/平仓记录 | 长期 |
|
||||||
|
|
||||||
|
### 6.3 指标计算策略
|
||||||
|
|
||||||
|
- **内存滚动 + 增量更新**(不是每次SQL全量聚合)
|
||||||
|
- 启动时回灌历史窗口(30m/4h/24h)到内存
|
||||||
|
- 之后只处理新增 agg_id 增量
|
||||||
|
- 每5秒把快照落库(幂等)
|
||||||
|
|
||||||
|
### 6.4 冷启动处理
|
||||||
|
|
||||||
|
signal-engine 重启后:
|
||||||
|
1. 从DB回读最近4h的aggTrades重算所有指标
|
||||||
|
2. 前N根标记为 `warmup`,不出信号
|
||||||
|
3. warmup 完成后开始正常信号生成
|
||||||
|
|
||||||
|
## 7. 历史数据回补
|
||||||
|
|
||||||
|
### 7.1 回补脚本(backfill_agg_trades.py)
|
||||||
|
|
||||||
|
```
|
||||||
|
参数:--symbol BTCUSDT --days 7 --batch-size 1000
|
||||||
|
流程:
|
||||||
|
1. 查DB中最早的agg_id
|
||||||
|
2. 从最早agg_id向前REST分页补拉
|
||||||
|
3. 每次1000条,sleep 200ms防限流
|
||||||
|
4. INSERT OR IGNORE 写入agg_trades_YYYYMM
|
||||||
|
5. 断点续传:记录进度到meta表
|
||||||
|
6. 完成后输出统计+连续性检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 速率控制
|
||||||
|
|
||||||
|
- Binance aggTrades REST 限流:weight 20/min
|
||||||
|
- 每请求 sleep 200ms,实际约 3-5 req/s
|
||||||
|
- 带指数退避重试,429后等60s
|
||||||
|
- 记录 rate-limit 统计(429次数、退避次数)
|
||||||
|
|
||||||
|
## 8. 开发时间线
|
||||||
|
|
||||||
|
| Day | 任务 | 交付物 | 负责 |
|
||||||
|
|-----|------|--------|------|
|
||||||
|
| 1 | 回补脚本 + 1天小样本 | backfill跑通,BTC/ETH各1天入库 | 露露开发,小周部署 |
|
||||||
|
| 2 | 全量7天回补 + 连续性验证 | 完整7天aggTrades,缺口=0 | 小周跑+验收 |
|
||||||
|
| 3-4 | signal-engine + 前端指标展示 | CVD三轨/ATR/VWAP/大单标记实时可视化 | 露露开发,小周部署 |
|
||||||
|
| 5 | 回测框架 + 首版回测报告 | 胜率/盈亏比/MDD/持仓分布/净收益 | 露露开发 |
|
||||||
|
| 6+ | 调参优化 → 达标后模拟盘 | 模拟交易记录 | 协同 |
|
||||||
|
|
||||||
|
## 9. 前置依赖
|
||||||
|
|
||||||
|
| 依赖 | 状态 | 影响范围 |
|
||||||
|
|------|------|----------|
|
||||||
|
| aggTrades 实时采集 | ✅ 已运行 | Phase 1-3 已满足 |
|
||||||
|
| 历史数据回补 | ⏳ Day 1-2 | 回测需要 |
|
||||||
|
| Binance API Key | ⏳ 等范总 | 仅Phase 4实盘 |
|
||||||
|
| Portfolio Margin | ⏳ 等范总 | 仅Phase 4实盘 |
|
||||||
|
| 资金准备 | ⏳ 等范总 | 仅Phase 4实盘 |
|
||||||
|
|
||||||
|
> Phase 1-3 不依赖范总,可立即开工。
|
||||||
|
|
||||||
|
## 10. 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| v2-v4 | 2026-02-27 | 权限管控+aggTrades采集+成交流面板 |
|
||||||
|
| **v5.0** | **2026-02-27** | **短线交易信号系统方案定稿** |
|
||||||
146
docs/arbitrage-engine/v51-optimization-plan.md
Normal file
146
docs/arbitrage-engine/v51-optimization-plan.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
title: V5.1 优化方案
|
||||||
|
date: 2026-03-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.1 优化方案
|
||||||
|
|
||||||
|
> 基于:V5.1模拟盘执行分析报告(2026-03-03)
|
||||||
|
> 核心目标:在不重建信号系统的前提下,将净R从-96.98R拉回正值
|
||||||
|
> 策略:**降低手续费暴露 + 提升单笔期望值**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 优化原则
|
||||||
|
|
||||||
|
信号层毛R为+11.98R(微弱正收益),说明信号有效性存在但边际极薄。
|
||||||
|
改造优先级:**先降频降费 → 再提盈亏比 → 最后优化信号质量**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方向一:提高入场门槛(降频)
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
- 75分以上即可入场,触发频率过高(500笔/历史周期)
|
||||||
|
- 各分数段胜率差异不大(85+仅比75-79高1.7%),说明75-84大量交易性价比差
|
||||||
|
|
||||||
|
### 建议改动
|
||||||
|
| 参数 | 当前值 | 建议值 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 入场阈值 | 75 | **82** |
|
||||||
|
| 预期效果 | 500笔 | 约~200笔(减少约60%交易频次) |
|
||||||
|
| 手续费节省 | - | ~65R(108×60%) |
|
||||||
|
|
||||||
|
> 根据数据,82分以上样本约170笔,需重新统计。需要验证胜率是否提升。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方向二:时段过滤(砍亏损时段)
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
以下时段(北京时间)胜率<40%,是系统性亏损区:
|
||||||
|
|
||||||
|
| 时段 | 胜率 | 合计R |
|
||||||
|
|------|------|-------|
|
||||||
|
| 01:00 | 31.8% | -15.69R |
|
||||||
|
| 06:00 | 33.3% | -13.73R |
|
||||||
|
| 07:00 | 36.4% | -7.40R |
|
||||||
|
| 09:00 | 38.7% | -16.71R |
|
||||||
|
| 11:00 | 28.6% | -5.51R |
|
||||||
|
| 13:00 | 31.8% | -13.62R |
|
||||||
|
| 18:00 | 30.0% | -6.47R |
|
||||||
|
|
||||||
|
合计:约7个亏损时段,贡献约-79R亏损。
|
||||||
|
|
||||||
|
### 建议改动
|
||||||
|
禁止在以下北京时间开仓:**01:00, 06:00, 07:00, 09:00, 11:00, 13:00, 18:00**
|
||||||
|
→ 预计减少交易约~100笔,直接节省约79R亏损
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方向三:暂停BTC交易
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
| 币种 | 胜率 | 合计R |
|
||||||
|
|------|------|-------|
|
||||||
|
| BTCUSDT | 49.3% | -45.61R |
|
||||||
|
|
||||||
|
BTC胜率低于随机水平(49.3%<50%),是最大单一亏损来源,贡献总亏损47%。
|
||||||
|
|
||||||
|
### 建议改动
|
||||||
|
**暂停BTC交易**,等积累足够新数据(calc_version=2)后再评估是否恢复。
|
||||||
|
→ 直接避免-45.61R(历史口径),减少约27%交易频次。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方向四:拉大TP/SL比(提盈亏比)
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
- sl_multiplier=1.4, tp1=1.05, tp2=2.1
|
||||||
|
- tp1_r=0.75, tp2_r=1.5
|
||||||
|
- 平均TP净收益=0.90R,平均SL净亏损=-1.23R
|
||||||
|
- 盈亏比=0.73,手续费后需要胜率>58%才能打平
|
||||||
|
|
||||||
|
### 建议改动
|
||||||
|
| 参数 | 当前值 | 建议值 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| sl_multiplier | 1.4 | 2.0(扩大止损空间,减少噪声止损) |
|
||||||
|
| tp1_multiplier | 1.05 | 1.5 |
|
||||||
|
| tp2_multiplier | 2.1 | 3.0 |
|
||||||
|
|
||||||
|
> 注意:扩大止损会增大单笔手续费(fee_r=2×0.0005×entry/rd,rd变大则fee_r变小)
|
||||||
|
> 同时能减少被噪声打止损的次数(SL平均仅18分钟持仓)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 组合改动预期效果(粗估)
|
||||||
|
|
||||||
|
| 改动 | 预期节省R |
|
||||||
|
|------|----------|
|
||||||
|
| 提高入场门槛至82 | ~65R |
|
||||||
|
| 过滤7个亏损时段 | ~79R |
|
||||||
|
| 暂停BTC | ~46R |
|
||||||
|
| **合计** | **~190R** |
|
||||||
|
|
||||||
|
> 当前净亏损-96.98R,三项改动合计节省190R,理论上净R可到+93R(乐观估计,存在重叠)
|
||||||
|
> 实际效果需要在模拟盘上验证后才能确认
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施计划
|
||||||
|
|
||||||
|
### Phase 1:参数调整(立即可做,不改代码)
|
||||||
|
1. 修改 `backend/strategies/v51_baseline.json`:
|
||||||
|
- threshold: 75 → 82
|
||||||
|
- 添加 `forbidden_hours_bj: [1, 6, 7, 9, 11, 13, 18]`
|
||||||
|
- 添加 `disabled_symbols: ["BTCUSDT"]`
|
||||||
|
2. 修改 `backend/paper_config.json` 对应字段(如果有覆盖)
|
||||||
|
3. 重启 signal-engine
|
||||||
|
|
||||||
|
### Phase 2:TP/SL调整(需验证历史数据影响)
|
||||||
|
1. 模拟不同sl_multiplier在历史数据上的表现
|
||||||
|
2. 确认新参数下预期胜率和盈亏比
|
||||||
|
3. 更新 `v51_baseline.json`
|
||||||
|
|
||||||
|
### Phase 3:数据验证
|
||||||
|
1. 积累150-200笔新口径数据(calc_version=2)
|
||||||
|
2. 对比优化前后各项指标
|
||||||
|
3. 根据实际结果再次迭代
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **不要同时改太多参数**:每次只改1-2个变量,方便归因
|
||||||
|
2. **记录每次改动时间**:便于后续对比数据
|
||||||
|
3. **备份当前配置**:`v51_baseline.json` 改前先备份
|
||||||
|
4. **V5.2同步评估**:V5.2目前-15.94R,比V5.1好但仍亏损,后续需同步分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待讨论问题
|
||||||
|
|
||||||
|
- [ ] 入场门槛从75提到82合适吗?是否要先看82-84分的历史胜率数据?
|
||||||
|
- [ ] 时段过滤是全部禁止还是只禁BTC?
|
||||||
|
- [ ] TP/SL比调整是否应该先做回测再上模拟盘?
|
||||||
|
- [ ] 暂停BTC是否需要范总确认?
|
||||||
163
docs/arbitrage-engine/v51-performance-analysis.md
Normal file
163
docs/arbitrage-engine/v51-performance-analysis.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
title: V5.1 模拟盘执行分析报告
|
||||||
|
date: 2026-03-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.1 模拟盘执行分析报告
|
||||||
|
|
||||||
|
> 数据口径:真实成交价(agg_trades)+ 手续费扣除,calc_version=2
|
||||||
|
> 分析日期:2026-03-03
|
||||||
|
> 参与分析:露露(Sonnet 4.6)、小范(GPT-5.3-Codex)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、总体概况
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 总交易笔数 | 503笔(含3笔活跃) |
|
||||||
|
| 已闭合笔数 | 500笔 |
|
||||||
|
| 有效样本(score≥75)| 496笔 |
|
||||||
|
| 净R(含手续费)| **-96.98R** |
|
||||||
|
| 毛R(不含手续费)| **+11.98R** |
|
||||||
|
| 总手续费 | **108.97R** |
|
||||||
|
| 平均单笔手续费 | 0.218R |
|
||||||
|
| 胜率 | 55.4% |
|
||||||
|
| 平均每笔净R | -0.193R |
|
||||||
|
|
||||||
|
> 本金10,000 USD,1R=200 USD → 净亏损约19,396 USD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、出场类型分布
|
||||||
|
|
||||||
|
| 状态 | 笔数 | 平均R(净) | 合计R |
|
||||||
|
|------|------|-----------|-------|
|
||||||
|
| sl(止损)| 189 | -1.232R | **-232.85R** |
|
||||||
|
| tp(止盈)| 132 | +0.904R | +119.36R |
|
||||||
|
| sl_be(保本止损)| 118 | +0.161R | +19.04R |
|
||||||
|
| timeout(超时)| 41 | +0.073R | +2.99R |
|
||||||
|
| signal_flip(翻转)| 20 | -0.276R | -5.51R |
|
||||||
|
|
||||||
|
**关键发现**:SL次数(189)远超TP(132),SL吃掉232.85R,TP只回收119.36R,实际盈亏比=0.77:1。
|
||||||
|
|
||||||
|
### SL均值拆解
|
||||||
|
|
||||||
|
| 组成 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| SL基础R | -1.000R(止损公式正确) |
|
||||||
|
| 手续费 | -0.232R |
|
||||||
|
| 净SL | **-1.232R** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、方向分析
|
||||||
|
|
||||||
|
| 方向 | 笔数 | 胜率 | 合计R |
|
||||||
|
|------|------|------|-------|
|
||||||
|
| LONG | 281 | 54.7% | -46.32R |
|
||||||
|
| SHORT | 222 | 56.3% | -50.67R |
|
||||||
|
|
||||||
|
**结论**:多空双向均亏,非方向性问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、币种分析
|
||||||
|
|
||||||
|
| 币种 | 笔数 | 胜率 | 合计R |
|
||||||
|
|------|------|------|-------|
|
||||||
|
| BTCUSDT | 137 | **49.3%** | **-45.61R** |
|
||||||
|
| ETHUSDT | 119 | 54.2% | -19.37R |
|
||||||
|
| XRPUSDT | 129 | 62.0% | -16.05R |
|
||||||
|
| SOLUSDT | 118 | 56.4% | -15.95R |
|
||||||
|
|
||||||
|
**关键发现**:BTC胜率仅49.3%(低于随机),是最大亏损来源,亏损占总量47%。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、信号分数段分析
|
||||||
|
|
||||||
|
| 分数段 | 笔数 | 胜率 | 合计R |
|
||||||
|
|--------|------|------|-------|
|
||||||
|
| 75-79 | 179 | 57.3% | -30.54R |
|
||||||
|
| 80-84 | 214 | 54.7% | -45.21R |
|
||||||
|
| 85+ | 103 | 55.3% | -15.91R |
|
||||||
|
|
||||||
|
**关键发现**:高分(85+)胜率与低分段基本持平,评分体系对预测质量的区分度不足。
|
||||||
|
|
||||||
|
> 另有6笔score=70-72(早期历史数据,入场门槛未设75时),不计入有效样本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、时段分析(北京时间)
|
||||||
|
|
||||||
|
### 盈利时段(合计R>0)
|
||||||
|
| 时段 | R | 胜率 |
|
||||||
|
|------|---|------|
|
||||||
|
| 03:00 | +2.24R | 69.2% |
|
||||||
|
| 05:00 | +6.18R | 78.6% |
|
||||||
|
| 08:00 | +5.22R | 82.6% |
|
||||||
|
| 17:00 | +2.40R | 85.7% |
|
||||||
|
| 19:00 | +2.27R | 83.3% |
|
||||||
|
| 23:00 | +7.97R | 71.4% |
|
||||||
|
|
||||||
|
### 重度亏损时段(胜率<40%)
|
||||||
|
| 时段 | R | 胜率 |
|
||||||
|
|------|---|------|
|
||||||
|
| 01:00 | -15.69R | 31.8% |
|
||||||
|
| 06:00 | -13.73R | 33.3% |
|
||||||
|
| 07:00 | -7.40R | 36.4% |
|
||||||
|
| 09:00 | -16.71R | 38.7% |
|
||||||
|
| 11:00 | -5.51R | 28.6% |
|
||||||
|
| 13:00 | -13.62R | 31.8% |
|
||||||
|
| 18:00 | -6.47R | 30.0% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、持仓时间分析
|
||||||
|
|
||||||
|
| 出场类型 | 平均持仓 |
|
||||||
|
|----------|---------|
|
||||||
|
| timeout | 60.0分钟 |
|
||||||
|
| sl_be | 23.8分钟 |
|
||||||
|
| tp | 20.5分钟 |
|
||||||
|
| sl | **18.1分钟** |
|
||||||
|
| flip | 20.2分钟 |
|
||||||
|
|
||||||
|
**发现**:SL平均仅持仓18分钟即被打出,说明入场时机存在问题(短时噪声触发入场)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、风险统计
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 单笔最大亏损 | -1.47R |
|
||||||
|
| 单笔最大盈利 | +1.04R |
|
||||||
|
| 标准差 | 0.89R |
|
||||||
|
| 中位数 | +0.12R |
|
||||||
|
| P25 | -1.19R |
|
||||||
|
| P75 | +0.78R |
|
||||||
|
|
||||||
|
> 中位数为正(+0.12R)但均值为负(-0.19R),说明少数大亏拖累整体,分布右偏。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、核心结论
|
||||||
|
|
||||||
|
### 最关键发现:毛R为正,费用致亏
|
||||||
|
- **毛R(不含手续费):+11.98R** → 信号层有微弱预测优势,未完全失效
|
||||||
|
- **总手续费:108.97R** → 手续费将毛R从+12压到-97
|
||||||
|
- **结论:V5.1不是"不会预测",而是"预测优势太薄,被执行成本碾碎"**
|
||||||
|
|
||||||
|
### 四大结构性问题
|
||||||
|
1. **盈亏比天然劣势**:SL:TP=189:132(每次输更多,赢的次数更少)
|
||||||
|
2. **BTC信号质量差**:胜率49.3%,低于随机,应考虑暂停或单独优化BTC
|
||||||
|
3. **评分体系区分度不足**:85+高分与75-79低分胜率差不多,评分无效
|
||||||
|
4. **时段敏感**:约6-7个时段胜率<40%,是系统性亏损区间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、优化方向(待讨论)
|
||||||
|
|
||||||
|
详见:[V5.1优化方案](./v51-optimization-plan.md)
|
||||||
250
docs/arbitrage-engine/v51-signal-enhancement.md
Normal file
250
docs/arbitrage-engine/v51-signal-enhancement.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
title: V5.1 信号增强方案
|
||||||
|
description: V5.1 信号评分体系 + 仓位管理 + TP/SL + 风控 + 回测框架
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.1 信号增强方案
|
||||||
|
|
||||||
|
> 讨论参与:露露(Opus 4.6) + 小周(GPT-5.3-Codex) + 范总审核
|
||||||
|
> 定稿时间:2026-02-28
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
V5.0 以 aggTrades 原始成交流为核心(CVD三轨 + ATR + VWAP + P95/P99大单),V5.1 在此基础上增加 4 个数据维度 + 完善交易管理系统。
|
||||||
|
|
||||||
|
**核心理念**:aggTrades 是我们的独特优势(别人没有原始成交流),新增数据源作为方向确认和风控补充,不替代 aggTrades 的核心地位。
|
||||||
|
|
||||||
|
## 2. 信号评分体系(100分制)
|
||||||
|
|
||||||
|
### 2.1 权重分配
|
||||||
|
|
||||||
|
| 层级 | 数据源 | 权重 | 角色 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| **方向层** | aggTrades(CVD三轨 + P95/P99大单) | **45%** | 核心方向判断 |
|
||||||
|
| **拥挤层** | L/S Ratio + Top Trader Position | **20%** | 市场拥挤度 |
|
||||||
|
| **环境层** | Open Interest 变化 | **15%** | 资金活跃度/可交易性门槛 |
|
||||||
|
| **确认层** | 多时间框架一致性 | **15%** | 方向确认 |
|
||||||
|
| **辅助层** | Coinbase Premium | **5%** | 机构资金流向 |
|
||||||
|
|
||||||
|
### 2.2 各层级详细计算
|
||||||
|
|
||||||
|
#### 方向层(45分)
|
||||||
|
|
||||||
|
- **CVD_fast(30m滚动)方向**:与信号方向一致 +15
|
||||||
|
- **CVD_mid(4h滚动)方向**:与信号方向一致 +15
|
||||||
|
- **P95/P99 大单**:无反向 P99 大单 +10,有同向 P99 大单 +15
|
||||||
|
- **CVD_fast 斜率加速**:斜率 > 阈值 +5(额外加分)
|
||||||
|
|
||||||
|
#### 拥挤层(20分)
|
||||||
|
|
||||||
|
- **L/S Ratio**:
|
||||||
|
- L/S > 2.0(做空信号)或 L/S < 0.5(做多信号):+10
|
||||||
|
- L/S 1.5-2.0 / 0.5-0.67:+5
|
||||||
|
- 中性区间:0
|
||||||
|
- **Top Trader Position Ratio**:
|
||||||
|
- 大户方向与信号一致:+10
|
||||||
|
- 大户方向中性:+5
|
||||||
|
- 大户方向反向:0
|
||||||
|
|
||||||
|
#### 环境层(15分)
|
||||||
|
|
||||||
|
- **OI 变化**(不判断方向,判断活跃度):
|
||||||
|
- OI 15分钟变化率 > 阈值(活跃):+15
|
||||||
|
- OI 变化温和:+10
|
||||||
|
- OI 萎缩(市场冷清):+5
|
||||||
|
|
||||||
|
#### 确认层(15分)
|
||||||
|
|
||||||
|
- **多时间框架确认规则**:
|
||||||
|
- `1m` = 触发层(入场点)
|
||||||
|
- `5m/15m` = 方向确认层
|
||||||
|
- `1h` = 风险闸门
|
||||||
|
- **评分**:
|
||||||
|
- 5m AND 15m 同向:+15
|
||||||
|
- 5m OR 15m 同向:+10
|
||||||
|
- 无同向确认:+0
|
||||||
|
- **1h 反向处理**:不在评分里扣分,而是在仓位管理里降仓(见 3.2)
|
||||||
|
|
||||||
|
#### 辅助层(5分)
|
||||||
|
|
||||||
|
- **Coinbase Premium**:
|
||||||
|
- Premium 方向与信号一致且 > 阈值:+5
|
||||||
|
- 中性:+2
|
||||||
|
- 反向:0
|
||||||
|
|
||||||
|
### 2.3 开仓门槛
|
||||||
|
|
||||||
|
| 总分 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| < 60 | **不开仓** |
|
||||||
|
| 60 - 74 | 轻仓(基础仓位 × 0.5) |
|
||||||
|
| 75 - 84 | 标准仓位 |
|
||||||
|
| ≥ 85 | 允许加仓(基础仓位 × 1.3) |
|
||||||
|
|
||||||
|
## 3. 仓位管理
|
||||||
|
|
||||||
|
### 3.1 基础仓位
|
||||||
|
|
||||||
|
- **默认**:总资金的 10%
|
||||||
|
- **杠杆**:3X(可调)
|
||||||
|
- **单笔最大风险**:总资金的 2%
|
||||||
|
|
||||||
|
### 3.2 1h 时间框架降仓规则
|
||||||
|
|
||||||
|
| 1h 状态 | 仓位调整 |
|
||||||
|
|---------|---------|
|
||||||
|
| 1h 同向 | 正常仓位 |
|
||||||
|
| 1h 弱反向 | 仓位 × 0.7 |
|
||||||
|
| 1h 强反向(CVD + 趋势都反) | 仓位 × 0.5,且仅允许 ≥85 分信号 |
|
||||||
|
|
||||||
|
### 3.3 Funding Rate 偏置
|
||||||
|
|
||||||
|
- FR 不做触发因子,做"慢变量偏置"
|
||||||
|
- 计算 `FR z-score(7d)` + `FR 斜率(近3个结算点)`
|
||||||
|
- 映射为 `bias`(-1 ~ +1)叠加到总分
|
||||||
|
- FR 极端且与信号方向冲突时:仅降仓,不反向开仓
|
||||||
|
|
||||||
|
## 4. TP/SL 管理(双ATR融合)
|
||||||
|
|
||||||
|
### 4.1 ATR 计算
|
||||||
|
|
||||||
|
```
|
||||||
|
risk_atr = 0.7 × ATR_5m + 0.3 × ATR_1h
|
||||||
|
```
|
||||||
|
|
||||||
|
- ATR_5m:5分钟K线,14周期 → 管入场灵敏度
|
||||||
|
- ATR_1h:1小时K线,14周期 → 管极端波动保护
|
||||||
|
- 好处:分钟级不钝化(靠5m),又不被短时噪音洗掉(靠1h兜底)
|
||||||
|
|
||||||
|
### 4.2 止盈止损参数
|
||||||
|
|
||||||
|
| 参数 | 计算 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **SL** | Entry ± 2.0 × risk_atr | 初始止损 |
|
||||||
|
| **TP1** | Entry ∓ 1.5 × risk_atr | 第一目标 |
|
||||||
|
| **TP2** | Entry ∓ 3.0 × risk_atr | 第二目标 |
|
||||||
|
|
||||||
|
### 4.3 分批平仓逻辑
|
||||||
|
|
||||||
|
1. **TP1 触发**:平 50% 仓位,SL 移至成本价 + 手续费(Breakeven)
|
||||||
|
2. **TP2 触发**:平剩余 50%,信号标记 "tp"
|
||||||
|
3. **SL 触发(TP1 已达)**:标记 "sl_be"(保本止损,实际盈亏 ≈ +0.5R)
|
||||||
|
4. **SL 触发(TP1 未达)**:标记 "sl"(完整止损,亏损 -1.0R)
|
||||||
|
|
||||||
|
### 4.4 期望值计算
|
||||||
|
|
||||||
|
假设 60% 胜率(TP1 命中率):
|
||||||
|
- 60% × 2.0R = +1.2R
|
||||||
|
- 40% × -1.0R = -0.4R
|
||||||
|
- **期望值 = +0.8R/笔**
|
||||||
|
|
||||||
|
## 5. 风控系统
|
||||||
|
|
||||||
|
### 5.1 自适应冷却期
|
||||||
|
|
||||||
|
| 条件 | 冷却时间 |
|
||||||
|
|------|---------|
|
||||||
|
| 基础(同向信号开仓后) | 10 分钟 |
|
||||||
|
| 近 30min 同向连续 2 笔止损 | 升到 20 分钟 |
|
||||||
|
| 上一笔同向达到 TP1 | 缩短到 5 分钟 |
|
||||||
|
| 第 4 个同向信号 | 默认不开,除非上一笔已TP1 + 当前≥85分 + 1h不强反向 |
|
||||||
|
|
||||||
|
- **反向信号**不受同向冷却限制,但需过最小反转阈值(防止来回翻单)
|
||||||
|
|
||||||
|
### 5.2 清算瀑布检测
|
||||||
|
|
||||||
|
- **主通道(实时)**:aggTrades 异常成交密度 + 价格加速度 + 点差扩张 → 推断清算瀑布
|
||||||
|
- **辅通道(校验)**:Binance `forceOrders` API → 事后校验和阈值再训练
|
||||||
|
- 交易决策吃主通道,模型校准吃辅通道
|
||||||
|
|
||||||
|
### 5.3 盘口轻量监控(资源受限版)
|
||||||
|
|
||||||
|
- 仅采集 Top-of-Book + 前5档聚合(每 100-250ms 采样)
|
||||||
|
- 保留 3 个指标:`microprice`、`imbalance`、`spread`
|
||||||
|
- 只存特征,不存全量快照
|
||||||
|
- 后续评估是否升到10档
|
||||||
|
|
||||||
|
## 6. 数据源汇总
|
||||||
|
|
||||||
|
### 6.1 Binance 免费 API(V5.1 新增)
|
||||||
|
|
||||||
|
| 数据 | 接口 | 更新频率 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 多空比 | `GET /futures/data/globalLongShortAccountRatio` | 5min |
|
||||||
|
| 大户持仓比 | `GET /futures/data/topLongShortPositionRatio` | 5min |
|
||||||
|
| OI 历史 | `GET /futures/data/openInterestHist` | 5min |
|
||||||
|
| Funding Rate | `GET /fapi/v1/fundingRate` | 8h结算 |
|
||||||
|
|
||||||
|
### 6.2 Coinbase Premium
|
||||||
|
|
||||||
|
- 对比 Coinbase BTC/USD 与 Binance BTC/USDT 实时价差
|
||||||
|
- 正 Premium = 机构买入(看多信号)
|
||||||
|
- 负 Premium = 机构卖出(看空信号)
|
||||||
|
|
||||||
|
### 6.3 已有数据源(V5.0)
|
||||||
|
|
||||||
|
| 数据 | 来源 | 存储 |
|
||||||
|
|------|------|------|
|
||||||
|
| aggTrades | Binance WebSocket 实时 + REST 回补 | PostgreSQL agg_trades 表 |
|
||||||
|
| CVD三轨 | signal_engine 内存计算 | signal_indicators 表 |
|
||||||
|
| ATR/VWAP | signal_engine 内存计算 | signal_indicators 表 |
|
||||||
|
| P95/P99大单 | signal_engine 24h滚动统计 | signal_indicators 表 |
|
||||||
|
| Funding Rate | agg_trades_collector 定时采集 | rate_snapshots 表 |
|
||||||
|
|
||||||
|
## 7. 回测框架
|
||||||
|
|
||||||
|
### 7.1 架构:逐tick事件回放
|
||||||
|
|
||||||
|
**不用逐分钟K线回测**(会系统性高估策略),用 aggTrades 逐tick回放。
|
||||||
|
|
||||||
|
### 7.2 三层数据结构
|
||||||
|
|
||||||
|
```
|
||||||
|
FeatureStore
|
||||||
|
├── 按时间索引缓存 1m/5m/15m/1h 特征
|
||||||
|
├── CVD, L/S, OI, FR bias, 盘口因子
|
||||||
|
└── 滚动窗口自动过期
|
||||||
|
|
||||||
|
SignalEvent
|
||||||
|
├── ts, symbol, side, score, regime
|
||||||
|
├── factors (各层评分明细)
|
||||||
|
└── entry_rule_id
|
||||||
|
|
||||||
|
PositionState
|
||||||
|
├── entry_ts, entry_px, size
|
||||||
|
├── sl_px, tp1_px, tp2_px
|
||||||
|
├── status (active/tp1_hit/tp/sl/sl_be)
|
||||||
|
└── cooldown_until
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 撮合逻辑
|
||||||
|
|
||||||
|
1. 每个 tick 到来 → 先更新未平仓位(检查 TP/SL/时间止损)
|
||||||
|
2. 再评估新信号(检查冷却期、评分、仓位规则)
|
||||||
|
3. 输出交易记录
|
||||||
|
|
||||||
|
### 7.4 统计输出
|
||||||
|
|
||||||
|
- 胜率 (Win Rate)
|
||||||
|
- 总盈亏 (Total PnL in R)
|
||||||
|
- 盈亏比 (Profit Factor)
|
||||||
|
- 夏普比率 (Sharpe Ratio)
|
||||||
|
- 最大回撤 (MDD)
|
||||||
|
- 平均持仓时间 (Avg Hold)
|
||||||
|
- 滑点影响评估 (Slippage Impact)
|
||||||
|
|
||||||
|
## 8. 远期规划
|
||||||
|
|
||||||
|
### V5.2(远期备选)
|
||||||
|
|
||||||
|
- **Twitter 新闻情绪面**:监控关键账号,AI分析利好/利空
|
||||||
|
- **范总判断**:新闻最终反映在 aggTrades 里,信号跑通后不急
|
||||||
|
|
||||||
|
### V5.3(数据充足后)
|
||||||
|
|
||||||
|
- ML模型替换规则引擎(XGBoost/LightGBM集成)
|
||||||
|
- 需要足够回测数据训练
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本:V5.1-draft | 待范总最终确认*
|
||||||
132
docs/arbitrage-engine/v51-signal-system.md
Normal file
132
docs/arbitrage-engine/v51-signal-system.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: V5.1 信号系统文档
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.1 基线信号系统(v51_baseline)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
V5.1 是基于市场微观结构的短线交易信号系统,使用 5 层 100 分评分体系,通过 CVD(累积成交量差)、大单流、持仓结构等 6 个信号源综合判断交易方向和强度。
|
||||||
|
|
||||||
|
## 评分体系(5层100分)
|
||||||
|
|
||||||
|
| 层级 | 权重 | 信号源 | 逻辑 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 方向层 | 45分 | CVD_fast(30m) + CVD_mid(4h) + P99大单 + 加速度 | 三票共振定方向 |
|
||||||
|
| 拥挤层 | 20分 | 多空比 + 大户持仓 | 反向拥挤 = 机会 |
|
||||||
|
| 环境层 | 15分 | OI变化率 | 资金流入确认趋势 |
|
||||||
|
| 确认层 | 15分 | CVD_fast + CVD_mid 同向 | 双周期共振确认 |
|
||||||
|
| 辅助层 | 5分 | Coinbase Premium | 美国机构动向 |
|
||||||
|
|
||||||
|
### 方向层详解(45分 + 5分加速奖金)
|
||||||
|
|
||||||
|
信号方向由 CVD_fast 和 CVD_mid 综合判断:
|
||||||
|
|
||||||
|
```
|
||||||
|
CVD_fast > 0 且 CVD_mid > 0 → LONG
|
||||||
|
CVD_fast < 0 且 CVD_mid < 0 → SHORT
|
||||||
|
不一致 → 以 CVD_fast 方向为准,但标记 no_direction
|
||||||
|
```
|
||||||
|
|
||||||
|
评分项:
|
||||||
|
- CVD_fast 方向一致:+15
|
||||||
|
- CVD_mid 方向一致:+15
|
||||||
|
- P99 大单顺向流入:+15(无反向大单时+10)
|
||||||
|
- 加速度奖金:CVD_fast 加速度方向一致 → +5(可超过45)
|
||||||
|
|
||||||
|
### 拥挤层详解(20分)
|
||||||
|
|
||||||
|
| 子项 | 满分 | 逻辑 |
|
||||||
|
|------|------|------|
|
||||||
|
| 多空比(LSR) | 10分 | 做空+LSR>2.0=满分,做多+LSR<0.5=满分 |
|
||||||
|
| 大户持仓比 | 10分 | 做多+多头占比≥55%=满分 |
|
||||||
|
|
||||||
|
数据缺失时给中间分(5分),避免因采集失败误杀信号。
|
||||||
|
|
||||||
|
### 环境层详解(15分)
|
||||||
|
|
||||||
|
基于 OI(持仓量)变化率评分:
|
||||||
|
- OI 显著增长 → 趋势确认 → 满分
|
||||||
|
- OI 变化不大 → 中等分
|
||||||
|
- OI 下降 → 低分
|
||||||
|
|
||||||
|
### 确认层详解(15分)
|
||||||
|
|
||||||
|
CVD_fast 和 CVD_mid 同时为正(LONG)或同时为负(SHORT)→ 15分,否则 0分。
|
||||||
|
|
||||||
|
> **已知问题**:与方向层存在同源重复(两者都用 CVD_fast/CVD_mid)。两周后根据数据评估是否重构。
|
||||||
|
|
||||||
|
### 辅助层详解(5分)
|
||||||
|
|
||||||
|
Coinbase Premium = Coinbase 价格 vs Binance 价格差。
|
||||||
|
- 正溢价 + 做多 → 5分(美国机构买入)
|
||||||
|
- 负溢价 + 做空 → 5分
|
||||||
|
- 溢价绝对值 ≤ 0.05% → 2分(中性)
|
||||||
|
- 反向溢价 → 0分
|
||||||
|
|
||||||
|
## 开仓规则
|
||||||
|
|
||||||
|
| 档位 | 分数 | 行为 |
|
||||||
|
|------|------|------|
|
||||||
|
| 不开仓 | < 75 | 不触发 |
|
||||||
|
| 标准 | 75-84 | 正常开仓 |
|
||||||
|
| 加仓 | ≥ 85 | 加重仓位 |
|
||||||
|
|
||||||
|
- **冷却期**:同币种同策略 10 分钟
|
||||||
|
- **最大持仓**:4 笔/策略
|
||||||
|
- **反向翻转**:收到反向 ≥75 分信号 → 平旧仓 + 开新仓
|
||||||
|
|
||||||
|
## TP/SL 设置
|
||||||
|
|
||||||
|
| 参数 | 值 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| SL | 1.4 × ATR | 止损 |
|
||||||
|
| TP1 | 1.05 × ATR | 第一止盈(平50%仓位) |
|
||||||
|
| TP2 | 2.1 × ATR | 第二止盈(平剩余仓位) |
|
||||||
|
|
||||||
|
TP1命中后 SL 移到保本价(Break Even)。
|
||||||
|
|
||||||
|
> 对应R倍数:tp1_r=0.75, tp2_r=1.5(以SL为1R)
|
||||||
|
|
||||||
|
## 信号源
|
||||||
|
|
||||||
|
| 信号 | 数据源 | 更新频率 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| CVD_fast | aggTrades 30分钟窗口 | 15秒 |
|
||||||
|
| CVD_mid | aggTrades 4小时窗口 | 15秒 |
|
||||||
|
| P99 大单 | aggTrades P99分位数 | 实时 |
|
||||||
|
| 加速度 | CVD_fast 二阶导数 | 15秒 |
|
||||||
|
| 多空比/大户持仓 | Binance API | 5分钟 |
|
||||||
|
| OI | Binance API | 5分钟 |
|
||||||
|
| Coinbase Premium | 计算值 | 5分钟 |
|
||||||
|
|
||||||
|
## 策略配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "v51_baseline",
|
||||||
|
"version": "5.1",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 45,
|
||||||
|
"crowding": 20,
|
||||||
|
"environment": 15,
|
||||||
|
"confirmation": 15,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"accel_bonus": 5,
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 1.4,
|
||||||
|
"tp1_multiplier": 1.05,
|
||||||
|
"tp2_multiplier": 2.1
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 历史表现(修正后数据,截至 2026-03-02)
|
||||||
|
|
||||||
|
- 总交易:282+ 笔
|
||||||
|
- 胜率:~65%
|
||||||
|
- 盈亏比:~0.72
|
||||||
|
- 按档位:85+ 胜率73.6%,75-79 胜率78%(最佳档位)
|
||||||
175
docs/arbitrage-engine/v51_gemini_analysis.md
Normal file
175
docs/arbitrage-engine/v51_gemini_analysis.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
明白!既然马上要进入开发阶段,我们就不能只停留在“概念”上,必须精确到**参数配比、计算公式和逻辑边界**。
|
||||||
|
|
||||||
|
作为你的量化分析师,我将《V5.3 双轨信号系统设计案》中的高层设计,直接翻译成研发可以直接参考的**“伪代码级”拆解**。这份拆解去除了所有冗余,直指核心代码逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 一、 轨道 A:山寨币模型 (ALT Track - 适用于 ETH, XRP, SOL)
|
||||||
|
|
||||||
|
ALT 轨的核心是**线性评分加法模型**。满分 100 分。
|
||||||
|
|
||||||
|
#### 1. 方向层 (Direction) —— 总权重:55 分
|
||||||
|
|
||||||
|
**设计说明:** V5.1 中方向层(45分)和确认层(15分)存在多重共线性,V5.3 将其合并为 55 分。
|
||||||
|
|
||||||
|
* **子项 A:快慢期 CVD 共振 (30 分)**
|
||||||
|
* **逻辑:** `CVD_fast (30m)` 和 `CVD_mid (4h)` 必须同向。
|
||||||
|
* **打分:**
|
||||||
|
* `CVD_fast` > 0 且 `CVD_mid` > 0(做多共振) $\rightarrow$ 给 30 分。
|
||||||
|
* `CVD_fast` < 0 且 `CVD_mid` < 0(做空共振) $\rightarrow$ 给 30 分。
|
||||||
|
* 方向不一致 $\rightarrow$ 0 分(并且整个信号应标记为 `no_direction` 终止计算)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **子项 B:P99 大单流入 (20 分)**
|
||||||
|
* **逻辑:** 捕捉极值大单资金方向。
|
||||||
|
* **打分:**
|
||||||
|
* P99 大单净流入方向与 CVD 共振方向一致 $\rightarrow$ 给 20 分。
|
||||||
|
* 无明显反向大单阻击 $\rightarrow$ 给 10 分。
|
||||||
|
* 反向大单压制 $\rightarrow$ 0 分。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **子项 C:加速度奖金 (Accel Bonus) (5 分)**
|
||||||
|
* **逻辑:** `CVD_fast` 的二阶导数(动能正在增强)。
|
||||||
|
* **打分:** 加速度方向与共振方向一致 $\rightarrow$ 给 5 分。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 2. 拥挤层 (Crowding) —— 总权重:25 分
|
||||||
|
|
||||||
|
**设计说明:** 寻找散户的反向流动性(拔高了 V5.1 中该层的权重)。
|
||||||
|
|
||||||
|
* **子项 A:多空比 LSR (15 分)**
|
||||||
|
* **做多场景:** 如果 LSR < 0.5(散户极度看空) $\rightarrow$ 15 分满分。
|
||||||
|
* **做空场景:** 如果 LSR > 2.0(散户极度看多) $\rightarrow$ 15 分满分。
|
||||||
|
* *缺省/常态:* 数据缺失或在 0.8 - 1.2 之间 $\rightarrow$ 给 7.5 分中间分。
|
||||||
|
|
||||||
|
|
||||||
|
* **子项 B:大户持仓比例 (10 分)**
|
||||||
|
* **逻辑:** 跟着大户吃散户。
|
||||||
|
* **打分:** 做多且大户多头占比 $\ge$ 55% $\rightarrow$ 10 分满分(做空同理)。缺省给 5 分。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 3. 环境层 (Environment) —— 总权重:15 分
|
||||||
|
|
||||||
|
* **信号源:** OI (Open Interest) 5 分钟/30 分钟变化率。
|
||||||
|
* **打分:**
|
||||||
|
* OI 显著正增长(有新资金入场,趋势真实) $\rightarrow$ 15 分。
|
||||||
|
* OI 变化平缓 $\rightarrow$ 7.5 分。
|
||||||
|
* OI 显著下降(说明只是存量平仓导致的假突破) $\rightarrow$ 0 分。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 4. 辅助层 (Auxiliary) —— 总权重:5 分
|
||||||
|
|
||||||
|
* **信号源:** Coinbase Premium (Coinbase 现货价格 - Binance 现货价格)。
|
||||||
|
* **打分:**
|
||||||
|
* 做多且正溢价(美国资金在买) $\rightarrow$ 5 分。
|
||||||
|
* 做空且负溢价(美国资金在砸) $\rightarrow$ 5 分。
|
||||||
|
* 溢价绝对值 $\le$ 0.05% (中性) $\rightarrow$ 2 分。
|
||||||
|
* 反向溢价 $\rightarrow$ 0 分。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> ⚙️ **ALT 轨开发执行参数:**
|
||||||
|
> * `open_threshold`: **75** (总分 $\ge$ 75 触发开仓)
|
||||||
|
> * `flip_threshold`: **85** (反向信号总分 $\ge$ 85 才允许平旧开新)
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 二、 轨道 B:大饼专属模型 (BTC Track)
|
||||||
|
|
||||||
|
BTC 轨**抛弃了线性加分**,采用**“布尔逻辑门控 (Boolean Logic Gates)”**。必须同时满足所有通过条件,且不触发任何否决条件,才允许开仓。
|
||||||
|
|
||||||
|
#### 1. 门控特征一:波动率状态过滤 (atr_percent_1h) —— 决定“能不能做”
|
||||||
|
|
||||||
|
* **公式:** $Volatility = \frac{ATR(1h)}{Close Price}$
|
||||||
|
* **开发逻辑 (Veto 否决条件):**
|
||||||
|
设定一个最小波动率阈值(如 `min_vol_threshold = 0.002`,即 0.2%)。
|
||||||
|
`IF atr_percent_1h < min_vol_threshold THEN BLOCK_SIGNAL ("Garbage Time")`
|
||||||
|
*理由:BTC 在极低波动时,微观特征全是做市商噪音。*
|
||||||
|
|
||||||
|
#### 2. 门控特征二:巨鲸 CVD (tiered_cvd_whale) —— 决定“真实方向”
|
||||||
|
|
||||||
|
* **计算方式:** 在 aggTrades 聚合时,过滤掉单笔价值 $< \$100k$ 的成交。只对 $> \$100k$ 的大单计算 Net Flow。
|
||||||
|
* **开发逻辑:**
|
||||||
|
`IF tiered_cvd_whale > strong_positive_threshold THEN Direction = LONG`
|
||||||
|
|
||||||
|
#### 3. 门控特征三:前 10 档订单薄失衡 (obi_depth_10) —— 决定“有没有阻力”
|
||||||
|
|
||||||
|
* **公式:** $OBI = \frac{Bid Volume - Ask Volume}{Bid Volume + Ask Volume}$ (取盘口前 10 档挂单量)
|
||||||
|
* **开发逻辑 (Veto 否决条件):**
|
||||||
|
*做多场景:* 如果 `Direction = LONG`,但 $OBI < -0.3$(上方有极重的卖盘墙压制)。
|
||||||
|
`THEN BLOCK_SIGNAL ("Sell Wall Imbalance")`
|
||||||
|
|
||||||
|
#### 4. 门控特征四:期现背离 (spot_perp_divergence) —— 决定“是不是陷阱”
|
||||||
|
|
||||||
|
* **计算方式:** Binance BTCUSDT 现货 CVD 减去 BTCUSDT 永续合约 CVD。
|
||||||
|
* **开发逻辑 (Veto 否决条件):**
|
||||||
|
*做多场景:* 合约 CVD 在疯狂飙升(散户开多),但现货 CVD 为负(机构在现货抛售)。
|
||||||
|
`IF perp_cvd > 0 AND spot_cvd < 0 THEN BLOCK_SIGNAL ("Spot Selling Divergence")`
|
||||||
|
|
||||||
|
> ⚙️ **BTC 轨开发执行伪代码:**
|
||||||
|
> `IF (Passed Volatility Gate) AND (Whale CVD confirms Direction) AND NOT (Blocked by OBI) AND NOT (Blocked by Divergence) THEN Execute Trade`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 三、 执行引擎边界条件(影响所有轨)
|
||||||
|
|
||||||
|
这些是直接写死在 `signal_engine.py` 或订单执行模块里的硬性规则,用来保卫净收益。
|
||||||
|
|
||||||
|
1. **止损/止盈 (SL/TP) 基准:**
|
||||||
|
* `SL_multiplier`: **2.0** (止损设为入场价 $\pm 2.0 \times ATR$)
|
||||||
|
* `TP1_multiplier`: **1.5**
|
||||||
|
* `TP2_multiplier`: **3.0**
|
||||||
|
|
||||||
|
|
||||||
|
2. **Break-Even (BE) 滑点补偿:**
|
||||||
|
* 打到 TP1 后,SL 移动的位置不是 `Entry Price`。
|
||||||
|
* `New_SL = Entry_Price + (Direction * 0.2 * ATR)` (这里的 0.2 ATR 是预留给手续费和滑点的缓冲值)。
|
||||||
|
|
||||||
|
|
||||||
|
3. **TP 兜底状态机 (Fallback Logic):**
|
||||||
|
* *Step 1:* 信号触发,下市价单(Taker)开仓。
|
||||||
|
* *Step 2:* 立即挂出 TP1/TP2 的**限价单 (Maker)**。
|
||||||
|
* *Step 3 (Monitor):* 如果最新标记价格穿过了 TP 触发价,但限价单未成交,启动 `timeout` 计时器(例如 2 秒)。
|
||||||
|
* *Step 4 (Fallback):* 超时未成交 $\rightarrow$ 发送 `Cancel Order` $\rightarrow$ 发送 `Market Order (Taker)` 强平。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、开发注意事项(补充)
|
||||||
|
|
||||||
|
1. **缺失数据默认策略(必须写死)**
|
||||||
|
- BTC 四个门控特征(`atr_percent_1h`, `tiered_cvd_whale`, `obi_depth_10`, `spot_perp_divergence`)任一缺失时,默认 `BLOCK_SIGNAL`。
|
||||||
|
- 必须记录 `block_reason=missing_feature:<name>`,避免静默放行。
|
||||||
|
|
||||||
|
2. **阈值治理(避免拍脑袋改参)**
|
||||||
|
- `min_vol_threshold`、`obi_veto_threshold`、`whale_flow_threshold` 必须配置化(不可硬编码散落在代码中)。
|
||||||
|
- 文档标注为“初始值”,并明确回测校准窗口与更新频率。
|
||||||
|
|
||||||
|
3. **标签口径统一(防止回填偏差)**
|
||||||
|
- `Y_binary_60m` 使用 `Mark Price` 判定触发顺序。
|
||||||
|
- ATR 必须使用信号触发时快照 `atr_value`,禁止回填时二次重算 ATR。
|
||||||
|
|
||||||
|
4. **TP 兜底状态机补全部分成交分支**
|
||||||
|
- 触发兜底前先查询订单成交量。
|
||||||
|
- 若部分成交,只对剩余仓位执行 `Cancel -> Taker Market Close`,避免超平或漏平。
|
||||||
|
|
||||||
|
5. **并发与幂等保护**
|
||||||
|
- `Cancel -> Market` 流程增加订单状态锁(或行级锁)和幂等键。
|
||||||
|
- 防止重复撤单、重复平仓、双写成交记录。
|
||||||
|
|
||||||
|
6. **发布闸门指标字段统一**
|
||||||
|
- 统一报表输出字段:`maker_ratio`, `avg_friction_cost_r`, `flip_loss_r`。
|
||||||
|
- 发布闸门自动判断基于同一口径,避免人工解释偏差。
|
||||||
|
|
||||||
|
**给开发者的最终建议:**
|
||||||
|
你现在可以拿着这份拆解,直接去写 `v53_alt_config.json` 和 BTC 轨的条件判断代码了。建议你先从 **ALT 轨的 `v53_alt_config.json` 重写**开始,因为这个改动最小,见效最快。是否需要我帮你直接生成这个 JSON 文件的模板?
|
||||||
383
docs/arbitrage-engine/v52-development.md
Normal file
383
docs/arbitrage-engine/v52-development.md
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
---
|
||||||
|
title: V5.2 开发文档(完整版)
|
||||||
|
description: 套利引擎V5.2 — Bug修复 + 策略优化 + 8信号源 + 策略配置化 + AB测试
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.2 开发文档(完整版)
|
||||||
|
|
||||||
|
> **版本**:V5.2 | **状态**:待开发 | **负责人**:露露
|
||||||
|
> **创建**:2026-03-01 | **前置**:V5.1 tag `v5.1` commit `d8ad879`
|
||||||
|
> **V5.1-hotfix**:commits `45bad25` → `4b841bc`(P0修复已上线)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、V5.1 现状总结
|
||||||
|
|
||||||
|
### 模拟盘数据(截至2026-03-01)
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 总交易 | 181笔 |
|
||||||
|
| 总盈亏 | +37.07R |
|
||||||
|
| 每笔平均 | +0.20R |
|
||||||
|
| 初始资金 | $10,000 |
|
||||||
|
| 当前余额 | ~$17,414 |
|
||||||
|
|
||||||
|
### 按档位统计(关键数据)
|
||||||
|
|
||||||
|
| 档位 | 笔数 | 胜率 | 总PnL | 每笔平均 | 结论 |
|
||||||
|
|------|------|------|-------|---------|------|
|
||||||
|
| **85+ (heavy)** | 53 | 73.6% | +15.00R | +0.28R | ✅ 优质 |
|
||||||
|
| **80-84** | 81 | 65.4% | +4.11R | +0.05R | ⚠️ 平庸 |
|
||||||
|
| **75-79** | 41 | 78.0% | +17.05R | +0.42R | ✅ 最佳 |
|
||||||
|
| **70-74** | 6 | 50.0% | -2.65R | -0.44R | ❌ 亏钱 |
|
||||||
|
|
||||||
|
### 手续费分析(核心发现)
|
||||||
|
|
||||||
|
| 币种 | SL距离% | 仓位价值 | 隐含杠杆 | 手续费/R |
|
||||||
|
|------|---------|---------|---------|---------|
|
||||||
|
| BTC | 0.43% | $46,000 | 4.6x | 0.23R |
|
||||||
|
| ETH | 0.56% | $36,000 | 3.6x | 0.18R |
|
||||||
|
| XRP | 0.44% | $45,000 | 4.5x | 0.05R |
|
||||||
|
| SOL | 0.58% | $34,000 | 3.4x | 0.04R |
|
||||||
|
|
||||||
|
**手续费公式**:`fee_R = 2 × 0.05% × position_value / risk_usd`
|
||||||
|
|
||||||
|
**BTC盈亏比问题**:TP2净利+0.89R vs SL净亏-1.23R,盈亏比仅0.72,需55%以上胜率才保本。
|
||||||
|
|
||||||
|
### V5.1已修复的P0 Bug(hotfix已上线)
|
||||||
|
|
||||||
|
| Bug | 影响 | 修复Commit |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| pnl_r虚高2倍 | 统计数据全部失真 | `45bad25` |
|
||||||
|
| 冷却期阻断反向平仓 | 反向信号无法关仓 | `45bad25` |
|
||||||
|
| 分区月份Bug | 月底数据写入失败 | `45bad25` |
|
||||||
|
| SL/TP用市价不是限价 | SL超过1R | `2f9dce4` |
|
||||||
|
| 浮盈没算半仓 | 持仓盈亏虚高 | `4b841bc` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、V5.2 目标
|
||||||
|
|
||||||
|
### 核心目标
|
||||||
|
1. **修复所有已知Bug**(Claude Code审阅 + 实际使用发现的)
|
||||||
|
2. **FR+清算加入评分**(8信号源完整版)
|
||||||
|
3. **开仓阈值提到75分**(砍掉70-74垃圾信号)
|
||||||
|
4. **策略配置化框架**(一套代码多份配置)
|
||||||
|
5. **AB测试**(V5.1 vs V5.2并行对比)
|
||||||
|
6. **24小时warmup**(消除冷启动)
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
- **55%胜率必须盈利**:盈亏比至少0.82:1
|
||||||
|
- **无限趋近实盘**:模拟盘和实盘逻辑完全一致
|
||||||
|
- **数据驱动**:所有决策基于数据,不拍脑袋
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Bug修复清单
|
||||||
|
|
||||||
|
### 后端(15项)
|
||||||
|
|
||||||
|
| ID | 优先级 | 文件 | 问题 | 修复方案 |
|
||||||
|
|----|--------|------|------|---------|
|
||||||
|
| P0-3 | **P1** | signal_engine.py:285 | 开仓价用30分VWAP而非实时价 | `price = win_fast.trades[-1][2]` |
|
||||||
|
| P0-4 | P2 | signal_engine+paper_monitor | 双进程并发写paper_trades | `SELECT FOR UPDATE SKIP LOCKED` |
|
||||||
|
| P1-2 | P2 | signal_engine.py:143-162 | 浮点精度漂移(buy_vol/sell_vol) | 每10000次trim从deque重算sums |
|
||||||
|
| P1-3 | **P1** | market_data_collector.py:51 | 单连接无重连 | 改用`db.get_sync_conn()`连接池 |
|
||||||
|
| P1-4 | P3 | db.py:36-43 | 连接池初始化线程不安全 | `threading.Lock`双重检查 |
|
||||||
|
| P2-1 | P2 | market_data_collector.py:112 | XRP/SOL coinbase_premium KeyError | `if symbol not in pair_map: return` |
|
||||||
|
| P2-3 | P2 | agg_trades_collector.py:77 | flush_buffer每秒调ensure_partitions | 移到定时任务(每小时一次) |
|
||||||
|
| P2-4 | P3 | liquidation_collector.py:127 | elif条件冗余 | 改为`else` |
|
||||||
|
| P2-5 | P2 | signal_engine.py:209 | atr_percentile @property有写副作用 | 拆成`update_atr_history()`方法 |
|
||||||
|
| P2-6 | P2 | main.py:554 | 1R=$200硬编码 | 从paper_config.json动态读取 |
|
||||||
|
| P3-1 | P2 | auth.py:15 | JWT密钥硬编码默认值 | 启动时强制校验`JWT_SECRET`环境变量 |
|
||||||
|
| P3-2 | P3 | main.py:17 | CORS allow_origins=["*"] | 限制为`https://arb.zhouyangclaw.com` |
|
||||||
|
| P3-3 | P3 | auth.py:316 | refresh token刷新非原子 | `UPDATE...WHERE revoked=0 RETURNING` |
|
||||||
|
| P3-4 | P3 | auth.py:292 | 登录无频率限制 | slowapi或Redis计数器 |
|
||||||
|
| NEW-1 | **P1** | signal_engine.py:664 | 冷启动warmup只有4小时 | 分批加载24小时,加载完再出信号 |
|
||||||
|
|
||||||
|
### 前端(12项)
|
||||||
|
|
||||||
|
| ID | 优先级 | 文件 | 问题 | 修复方案 |
|
||||||
|
|----|--------|------|------|---------|
|
||||||
|
| FE-P1-1 | **P1** | lib/auth.tsx:113 | 并发401多次refresh竞态 | 单例Promise + `_refreshPromise` |
|
||||||
|
| FE-P1-2 | **P1** | lib/auth.tsx:127 | 刷新失败AuthContext未同步 | `window.dispatchEvent("auth:session-expired")` |
|
||||||
|
| FE-P1-3 | **P1** | 所有页面 | catch{}静默吞掉API错误 | 每个组件加`error` state + 红色提示 |
|
||||||
|
| FE-P1-4 | P2 | paper/page.tsx:119 | LatestSignals串行4请求 | `Promise.allSettled`并行 |
|
||||||
|
| FE-P2-1 | P3 | app/page.tsx:52 | MiniKChart每30秒销毁重建 | 只更新data不重建chart |
|
||||||
|
| FE-P2-3 | P2 | paper/page.tsx:20 | ControlPanel非admin可见 | 校验`isAdmin`,非admin隐藏 |
|
||||||
|
| FE-P2-4 | **P1** | paper/page.tsx:181 | WebSocket无断线重连 | 指数退避重连 + 断线提示 |
|
||||||
|
| FE-P2-5 | P2 | paper/page.tsx:217 | 1R=$200前端硬编码 | 从`/api/paper/config`读取 |
|
||||||
|
| FE-P2-6 | P2 | signals/page.tsx:101 | 5秒轮询5分钟数据 | 改为300秒间隔 |
|
||||||
|
| FE-P2-8 | P3 | paper/signals | 大量`any`类型 | 定义TypeScript interface |
|
||||||
|
| FE-P3-1 | P3 | lib/auth.tsx:33 | Token存localStorage | 评估httpOnly cookie方案 |
|
||||||
|
| FE-P3-3 | P3 | app/page.tsx:144 | Promise.all任一失败全丢 | 改`Promise.allSettled` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、新功能:8信号源评分
|
||||||
|
|
||||||
|
### 当前6信号源 → V5.2增加2个
|
||||||
|
|
||||||
|
| # | 信号源 | 层级 | 当前 | V5.2 |
|
||||||
|
|---|--------|------|------|------|
|
||||||
|
| 1 | CVD三轨(fast/mid) | 方向层 | ✅ 评分中 | 保持 |
|
||||||
|
| 2 | P99大单流 | 方向层 | ✅ 评分中 | 保持 |
|
||||||
|
| 3 | CVD加速度 | 方向层 | ✅ 评分中 | 保持 |
|
||||||
|
| 4 | 多空比+大户持仓比 | 拥挤层 | ✅ 评分中 | 保持 |
|
||||||
|
| 5 | OI变化率 | 环境层 | ✅ 评分中 | 保持 |
|
||||||
|
| 6 | Coinbase Premium | 辅助层 | ✅ 评分中 | 保持 |
|
||||||
|
| **7** | **资金费率(FR)** | **拥挤层** | ⬜ 仅采集 | **✅ 加入评分** |
|
||||||
|
| **8** | **清算数据** | **确认层** | ⬜ 仅采集 | **✅ 加入评分** |
|
||||||
|
|
||||||
|
### FR评分逻辑(草案)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 资金费率 → 拥挤层加分
|
||||||
|
# FR > 0.03% = 多头过度拥挤 → SHORT加分
|
||||||
|
# FR < -0.03% = 空头过度拥挤 → LONG加分
|
||||||
|
# |FR| > 0.1% = 极端值 → 反向强信号
|
||||||
|
|
||||||
|
funding_rate = self.market_indicators.get("funding_rate")
|
||||||
|
if funding_rate is not None:
|
||||||
|
if direction == "LONG" and funding_rate < -0.0003:
|
||||||
|
fr_score = 5 # 空头拥挤,做多有利
|
||||||
|
elif direction == "SHORT" and funding_rate > 0.0003:
|
||||||
|
fr_score = 5 # 多头拥挤,做空有利
|
||||||
|
elif direction == "LONG" and funding_rate > 0.001:
|
||||||
|
fr_score = -5 # 多头极度拥挤,做多危险(减分)
|
||||||
|
elif direction == "SHORT" and funding_rate < -0.001:
|
||||||
|
fr_score = -5 # 空头极度拥挤,做空危险
|
||||||
|
else:
|
||||||
|
fr_score = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 清算数据评分逻辑(草案)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 大额清算 → 确认层加分
|
||||||
|
# 大额空单清算 = 空头被清洗 → SHORT可能结束,LONG加分
|
||||||
|
# 大额多单清算 = 多头被清洗 → LONG可能结束,SHORT加分
|
||||||
|
# 5分钟内清算量 > 阈值 = 趋势加速信号
|
||||||
|
|
||||||
|
liq_long = self.market_indicators.get("long_liq_usd", 0)
|
||||||
|
liq_short = self.market_indicators.get("short_liq_usd", 0)
|
||||||
|
if direction == "LONG" and liq_short > 500000:
|
||||||
|
liq_score = 5 # 大量空单被清算,趋势确认
|
||||||
|
elif direction == "SHORT" and liq_long > 500000:
|
||||||
|
liq_score = 5
|
||||||
|
else:
|
||||||
|
liq_score = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### V5.2权重分配(草案)
|
||||||
|
|
||||||
|
| 层级 | V5.1权重 | V5.2权重 | 变化 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| 方向层 | 45+5 | 40+5 | -5 |
|
||||||
|
| 拥挤层 | 20 | 25(+FR 5分) | +5 |
|
||||||
|
| 环境层 | 15 | 15 | 不变 |
|
||||||
|
| 确认层 | 15 | 20(+清算 5分) | +5 |
|
||||||
|
| 辅助层 | 5 | 5 | 不变 |
|
||||||
|
| **总计** | **100+5** | **105+5** | +5 |
|
||||||
|
|
||||||
|
> 注:总分超过100不影响,阈值按绝对分数判断(75/80/85)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、策略调整
|
||||||
|
|
||||||
|
### 开仓阈值调整
|
||||||
|
|
||||||
|
| | V5.1 | V5.2 | 原因 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 最低开仓分 | 60分 | **75分** | 70-74档位50%胜率,扣手续费亏钱 |
|
||||||
|
| light档 | 60-74 | **取消** | 数据证明低分信号无价值 |
|
||||||
|
| standard档 | 75-84 | 75-84 | 保持 |
|
||||||
|
| heavy档 | 85+ | 85+ | 保持 |
|
||||||
|
|
||||||
|
### TP/SL倍数(待AB测试确认)
|
||||||
|
|
||||||
|
| 参数 | V5.1(方案A) | V5.2候选(方案B) |
|
||||||
|
|------|-------------|-----------------|
|
||||||
|
| SL | 2.0 × risk_atr | 3.0 × risk_atr |
|
||||||
|
| TP1 | 1.5 × risk_atr | 2.0 × risk_atr |
|
||||||
|
| TP2 | 3.0 × risk_atr | 4.0 × risk_atr |
|
||||||
|
| BTC手续费占比 | 0.23R | 0.15R |
|
||||||
|
| TP2净利 | +0.89R | +0.97R |
|
||||||
|
| SL净亏 | -1.23R | -1.15R |
|
||||||
|
| 盈亏比 | 0.72 | 0.84 |
|
||||||
|
| 保本胜率 | 58% | 54% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、策略配置化框架
|
||||||
|
|
||||||
|
### 设计目标
|
||||||
|
一个signal-engine进程,支持多套策略配置并行。
|
||||||
|
|
||||||
|
### 配置文件结构
|
||||||
|
```json
|
||||||
|
// strategies/v51.json
|
||||||
|
{
|
||||||
|
"name": "v51_baseline",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 45,
|
||||||
|
"crowding": 20,
|
||||||
|
"environment": 15,
|
||||||
|
"confirmation": 15,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 2.0,
|
||||||
|
"tp1_multiplier": 1.5,
|
||||||
|
"tp2_multiplier": 3.0
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// strategies/v52.json
|
||||||
|
{
|
||||||
|
"name": "v52_8signals",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 40,
|
||||||
|
"crowding": 25,
|
||||||
|
"environment": 15,
|
||||||
|
"confirmation": 20,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 3.0,
|
||||||
|
"tp1_multiplier": 2.0,
|
||||||
|
"tp2_multiplier": 4.0
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### paper_trades表新增字段
|
||||||
|
```sql
|
||||||
|
ALTER TABLE paper_trades ADD COLUMN strategy VARCHAR(32) DEFAULT 'v51_baseline';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、AB测试方案
|
||||||
|
|
||||||
|
### 架构
|
||||||
|
```
|
||||||
|
signal_engine.py
|
||||||
|
├── evaluate_signal(state, strategy="v51") → score_A, signal_A
|
||||||
|
├── evaluate_signal(state, strategy="v52") → score_B, signal_B
|
||||||
|
│
|
||||||
|
├── paper_open_trade(strategy="v51", ...) → paper_trades.strategy='v51'
|
||||||
|
└── paper_open_trade(strategy="v52", ...) → paper_trades.strategy='v52'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 独立资金池
|
||||||
|
- V5.1:$10,000虚拟资金,独立统计
|
||||||
|
- V5.2:$10,000虚拟资金,独立统计
|
||||||
|
|
||||||
|
### 对比指标
|
||||||
|
| 指标 | V5.1 | V5.2 |
|
||||||
|
|------|------|------|
|
||||||
|
| 总笔数 | ? | ? |
|
||||||
|
| 胜率 | ? | ? |
|
||||||
|
| 每笔平均R | ? | ? |
|
||||||
|
| 最大回撤 | ? | ? |
|
||||||
|
| 盈亏比 | ? | ? |
|
||||||
|
| PF | ? | ? |
|
||||||
|
|
||||||
|
### 测试周期
|
||||||
|
- **两周**(目标每策略100+笔)
|
||||||
|
- 结束后选表现更好的上实盘
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、24小时Warmup
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
signal-engine启动时只加载4小时数据,P99大单需要24小时窗口。
|
||||||
|
|
||||||
|
### 方案(范总确认)
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
states = {sym: SymbolState(sym) for sym in SYMBOLS}
|
||||||
|
|
||||||
|
# 分批加载24小时(每批100万条,避免OOM)
|
||||||
|
for sym, state in states.items():
|
||||||
|
load_historical_chunked(state, 24 * 3600 * 1000)
|
||||||
|
logger.info(f"[{sym}] warmup complete")
|
||||||
|
|
||||||
|
logger.info("=== 全部币种warmup完成,开始出信号 ===")
|
||||||
|
# 这之后才开始评估循环
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预估
|
||||||
|
- 4币种24小时约2000-3000万条
|
||||||
|
- 加载时间1-3分钟
|
||||||
|
- 启动后信号质量从第一笔就是100%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、V5.1-hotfix已修复清单(参考)
|
||||||
|
|
||||||
|
| Commit | 内容 |
|
||||||
|
|--------|------|
|
||||||
|
| `45bad25` | P0-1: 反向信号绕过冷却 |
|
||||||
|
| `45bad25` | P0-2: pnl_r统一(exit-entry)/risk_distance |
|
||||||
|
| `45bad25` | P1-1: 分区月份+UTC修复 |
|
||||||
|
| `2f9dce4` | TP/SL改限价单模式(趋近实盘) |
|
||||||
|
| `4b841bc` | 前端浮盈半仓计算 |
|
||||||
|
| `d351949` | 历史pnl_r修正脚本 |
|
||||||
|
| `8b73500` | Auth从SQLite迁移PG |
|
||||||
|
| `9528d69` | recent_large_trades去重 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、开发排期(草案)
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 内容 | 产出 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Phase 1 | Day 1-2 | Bug修复(P1全部 + P2重要的) | 代码+测试 |
|
||||||
|
| Phase 2 | Day 2-3 | FR+清算评分逻辑 | signal_engine.py |
|
||||||
|
| Phase 3 | Day 3-4 | 策略配置化框架 | strategies/*.json + 代码 |
|
||||||
|
| Phase 4 | Day 4-5 | AB测试机制 | paper_trades.strategy字段 |
|
||||||
|
| Phase 5 | Day 5 | 24h warmup | signal_engine.py |
|
||||||
|
| Phase 6 | Day 6-7 | 前端Bug修复 + 策略对比页面 | 前端代码 |
|
||||||
|
| **部署** | Day 7 | 全部写好测好,一次性部署 | 减少重启 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、数据需求
|
||||||
|
|
||||||
|
### 当前数据量
|
||||||
|
- aggTrades:7039万条(BTC 23天 + ETH 3天 + XRP/SOL 3天)
|
||||||
|
- signal_indicators:2.7万+条
|
||||||
|
- paper_trades:181笔
|
||||||
|
- market_indicators:5400+条
|
||||||
|
- liquidations:3600+条
|
||||||
|
|
||||||
|
### V5.2需要的数据量
|
||||||
|
- **AB测试**:每策略至少100笔 → 两周
|
||||||
|
- **权重优化(Phase 1统计分析)**:300+笔
|
||||||
|
- **回归分析**:500+笔
|
||||||
|
- **ML优化**:1000+笔
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、风险与注意事项
|
||||||
|
|
||||||
|
1. **手续费是盈亏关键** — BTC手续费0.23R/笔,必须控制交易频率
|
||||||
|
2. **样本量不足** — 当前181笔,按分数段分析可能不稳定
|
||||||
|
3. **过拟合风险** — 两周一次微调,每次±10-20%,不做大改
|
||||||
|
4. **AB测试期间** — 两套策略共享最大持仓4个,需要分配
|
||||||
|
5. **24h warmup** — 启动时间变长,需要告知运维(小周)
|
||||||
|
6. **策略配置化** — 改动signal_engine核心代码,必须充分测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档为V5.2开发的完整参考,开发过程中持续更新。*
|
||||||
|
*商业机密:策略细节不对外暴露。*
|
||||||
269
docs/arbitrage-engine/v52-evolution-roadmap.md
Normal file
269
docs/arbitrage-engine/v52-evolution-roadmap.md
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
---
|
||||||
|
title: V5.2 策略进化路线图
|
||||||
|
description: 信号引擎V5.2 — 数据驱动的策略迭代框架,将信号源视为特征、权重视为模型参数,通过模拟盘持续优化
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.2 策略进化路线图
|
||||||
|
|
||||||
|
> **优先级:P0(最高)** | 负责人:露露 | 创建:2026-02-28
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、核心思路 ⭐⭐⭐
|
||||||
|
|
||||||
|
### 信号源 = 特征(Feature)
|
||||||
|
|
||||||
|
每一个数据源都是模型的一个**输入特征**。特征越多、越有效,模型的预测能力越强。
|
||||||
|
|
||||||
|
当前V5.1的5层评分体系,本质上就是一个**手动设计的线性模型**:
|
||||||
|
|
||||||
|
```
|
||||||
|
总分 = W1×方向层 + W2×拥挤层 + W3×环境层 + W4×确认层 + W5×辅助层
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权重 = 模型参数(Parameter)
|
||||||
|
|
||||||
|
当前权重是人工拍的(45/20/15/15/5),不一定是最优的。
|
||||||
|
|
||||||
|
**目标**:用真实交易数据(模拟盘+实盘),自动学习每个特征的最优权重。
|
||||||
|
|
||||||
|
### 迭代循环(正向飞轮)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ① 加入新信号源(特征) │
|
||||||
|
│ ↓ │
|
||||||
|
│ ② 模拟盘跑数据(每笔记录5层分数+盈亏) │
|
||||||
|
│ ↓ │
|
||||||
|
│ ③ 分析数据(哪些特征有效、最优权重) │
|
||||||
|
│ ↓ │
|
||||||
|
│ ④ 调整权重 / 加减特征 │
|
||||||
|
│ ↓ │
|
||||||
|
│ ⑤ 实盘验证 │
|
||||||
|
│ ↓ │
|
||||||
|
│ ⑥ 数据反馈 → 回到① │
|
||||||
|
│ │
|
||||||
|
│ 数据越多 → 模型越准 → 赚越多 → 数据越多 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
这就是量化交易的**核心方法论**,和大模型训练思路一样:
|
||||||
|
- 大模型:数据 → 特征提取 → 权重训练 → 验证 → 迭代
|
||||||
|
- 信号引擎:行情数据 → 信号源/指标 → 评分权重 → 模拟盘验证 → 调参迭代
|
||||||
|
|
||||||
|
**护城河**:积累的数据和调优后的参数,是别人无法复制的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、当前架构(V5.1)
|
||||||
|
|
||||||
|
### 已有信号源(特征)
|
||||||
|
|
||||||
|
| 层 | 权重 | 信号源 | 数据来源 | 状态 |
|
||||||
|
|----|------|--------|---------|------|
|
||||||
|
| 方向层 | 45分 | CVD_fast(30m) | agg_trades计算 | ✅ 真实数据 |
|
||||||
|
| 方向层 | - | CVD_mid(4h) | agg_trades计算 | ✅ 真实数据 |
|
||||||
|
| 方向层 | - | P99大单流 | agg_trades计算 | ✅ 真实数据 |
|
||||||
|
| 方向层 | +5 | CVD加速度 | agg_trades计算 | ✅ 真实数据 |
|
||||||
|
| 拥挤层 | 20分 | 多空比 | 币安API globalLongShortAccountRatio | ✅ 真实数据 |
|
||||||
|
| 拥挤层 | - | 大户持仓比 | 币安API topLongShortAccountRatio | ✅ 真实数据 |
|
||||||
|
| 环境层 | 15分 | OI变化率 | 币安API openInterestHist | ✅ 真实数据 |
|
||||||
|
| 确认层 | 15分 | 多时间框架CVD一致性 | agg_trades计算 | ✅ 真实数据 |
|
||||||
|
| 辅助层 | 5分 | Coinbase Premium | 币安+CB价差 | ✅ 真实数据 |
|
||||||
|
|
||||||
|
### 每笔交易记录的数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"score": 85,
|
||||||
|
"score_factors": {
|
||||||
|
"direction": {"score": 45, "cvd_fast": 15, "cvd_mid": 15, "p99_flow": 15, "accel_bonus": 5},
|
||||||
|
"crowding": {"score": 15, "long_short_ratio": 10, "top_trader_position": 5},
|
||||||
|
"environment": {"score": 10, "open_interest_hist": 0.02},
|
||||||
|
"confirmation": {"score": 15},
|
||||||
|
"auxiliary": {"score": 5, "coinbase_premium": 0.0012}
|
||||||
|
},
|
||||||
|
"pnl_r": 2.25,
|
||||||
|
"status": "tp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、V5.2 待加入信号源(按优先级)
|
||||||
|
|
||||||
|
### 第一批(数据容易获取)
|
||||||
|
|
||||||
|
| 信号源 | 类型 | 获取方式 | 预期价值 |
|
||||||
|
|--------|------|---------|---------|
|
||||||
|
| 资金费率(Funding Rate) | 拥挤指标 | 币安API /fapi/v1/fundingRate | 高 — 极端FR是反转信号 |
|
||||||
|
| 清算数据(Liquidation) | 情绪指标 | 币安WS forceOrder | 高 — 大额清算=趋势加速 |
|
||||||
|
| 期权PCR(Put/Call Ratio) | 情绪指标 | Deribit API | 中 — 机构对冲意愿 |
|
||||||
|
| 波动率指数(DVOL) | 环境指标 | Deribit API | 中 — 波动率扩张/收缩 |
|
||||||
|
|
||||||
|
### 第二批(需要爬取/计算)
|
||||||
|
|
||||||
|
| 信号源 | 类型 | 获取方式 | 预期价值 |
|
||||||
|
|--------|------|---------|---------|
|
||||||
|
| Twitter/X情绪 | 情绪指标 | Agent-Reach xsearch | 中 — 散户情绪反指标 |
|
||||||
|
| 恐贪指数 | 情绪指标 | alternative.me API | 低 — 日级更新太慢 |
|
||||||
|
| 链上大额转账 | 鲸鱼行为 | Etherscan/Blockchain API | 中 — 鲸鱼动向 |
|
||||||
|
| 交易所净流入 | 资金流 | CryptoQuant/Glassnode | 高 — 抛压预警 |
|
||||||
|
|
||||||
|
### 第三批(高级)
|
||||||
|
|
||||||
|
| 信号源 | 类型 | 获取方式 | 预期价值 |
|
||||||
|
|--------|------|---------|---------|
|
||||||
|
| 订单簿深度不对称 | 微观结构 | 币安WS depth | 中 — 支撑/阻力判断 |
|
||||||
|
| 跨交易所价差 | 套利信号 | 多交易所API | 低 — 实现复杂 |
|
||||||
|
| 新闻事件检测 | 事件驱动 | LLM分析 | 中 — 黑天鹅预警 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、权重优化方法(数据足够后实施)
|
||||||
|
|
||||||
|
### Phase 1:统计分析(200+笔交易后)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 按各层分数分桶,看胜率
|
||||||
|
# 例如:方向层>=40分时胜率65%,<30分时胜率45%
|
||||||
|
# → 说明方向层有效,保持高权重
|
||||||
|
|
||||||
|
# 按拥挤层分桶
|
||||||
|
# 例如:拥挤层>=15分时胜率60%,<10分时胜率58%
|
||||||
|
# → 说明拥挤层区分度低,可以降权重
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2:回归分析(500+笔交易后)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 逻辑回归:各层分数 → 是否盈利
|
||||||
|
# 输出:每个特征的系数 = 最优权重
|
||||||
|
from sklearn.linear_model import LogisticRegression
|
||||||
|
model = LogisticRegression()
|
||||||
|
model.fit(X_factors, y_win) # X=5层分数, y=盈利/亏损
|
||||||
|
optimal_weights = model.coef_
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3:机器学习(1000+笔交易后)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# XGBoost/随机森林:自动发现非线性关系
|
||||||
|
# 例如:方向层高+拥挤层低 = 最佳组合(简单线性模型发现不了)
|
||||||
|
from xgboost import XGBClassifier
|
||||||
|
model = XGBClassifier()
|
||||||
|
model.fit(X_factors, y_win)
|
||||||
|
# 特征重要性排序 → 指导信号源增减
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、2026-02-28 开发记录
|
||||||
|
|
||||||
|
### V5.1完善
|
||||||
|
- **5层评分真实数据修复**:signal_engine之前没正确解析market_indicators的JSONB,拥挤/环境/辅助层全是默认中间分。修了fetch_market_indicators正确解析JSONB → commit `317031a`
|
||||||
|
- **前端UI全面压缩**:标题改"⚡ 信号引擎 V5.1",所有面板字体/间距/padding缩小 → commits `9382d35`, `271658c`
|
||||||
|
|
||||||
|
### 模拟盘上线(Paper Trading)
|
||||||
|
- **paper_trades表** + signal_engine集成 + 5个API → commit `e054db1`
|
||||||
|
- **开关机制**:默认关闭,前端按钮控制,API热更新 → commit `282aed1`
|
||||||
|
- **手续费**:Taker 0.05%×2=0.1%来回 → commit `47004ec`
|
||||||
|
- **反向信号翻转**:持多仓来空信号→先平后开 → commit `6681070`
|
||||||
|
- **WebSocket实时TP/SL**:独立paper_monitor.py进程,毫秒级平仓 → commit `7b901a2`
|
||||||
|
- **前端aggTrade实时价格**:逐笔成交推送 → commit `1d23042`
|
||||||
|
- **当前资金显示** → commit `d0e626a`
|
||||||
|
- **冷启动保护**:重启后跳过前3轮防重复开仓 → commit `95b45d0`
|
||||||
|
- **5层评分明细记录**:score_factors JSONB字段 → commit `022ead6`
|
||||||
|
|
||||||
|
### Bug修复
|
||||||
|
- arb-api缩进错误 → commit `cd17c76`
|
||||||
|
- Request未导入 → commit `b232270`
|
||||||
|
- useAuth登录检测 → commit `59910fe`
|
||||||
|
- 现价不准改币安API → commit `d177d28`
|
||||||
|
- 最新信号symbol参数修复 → commit `404cc68`
|
||||||
|
- 资金字体超框 → commit `95fec35`
|
||||||
|
- gitignore __pycache__ → commit `961cbc6`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、下一步行动
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| 现在 | 2026-02-28 ~ 03-14 | 模拟盘跑两周,积累200-300笔交易数据 |
|
||||||
|
| Phase 1 | 03-14 | 统计分析各层贡献,初步调权重 |
|
||||||
|
| Phase 2 | 03-21 | 加入资金费率+清算数据(第一批新信号源) |
|
||||||
|
| Phase 3 | 04-01 | 回归分析自动优化权重 |
|
||||||
|
| Phase 4 | 04-15 | 小仓实盘验证 |
|
||||||
|
| 持续 | 长期 | 不断加入新信号源,数据驱动迭代 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、行业竞品与信号源调研(2026-02-28)
|
||||||
|
|
||||||
|
### 与我们思路相似的项目
|
||||||
|
|
||||||
|
#### 1. Hyper-Alpha-Arena(GitHub开源)— 相似度85%
|
||||||
|
- **地址**:https://github.com/HammerGPT/Hyper-Alpha-Arena
|
||||||
|
- **核心**:监控Order Flow + OI变化 + Funding Rate极端值,触发自动交易
|
||||||
|
- 支持币安合约+Hyperliquid,用LLM(GPT-5/Claude/DeepSeek)做策略决策
|
||||||
|
- **区别**:他们用LLM自然语言描述策略做决策,我们用评分模型
|
||||||
|
- **我们的优势**:5层评分+权重训练更可量化、可回测、可优化
|
||||||
|
|
||||||
|
#### 2. FinRL-AlphaSeek(哥伦比亚大学,ACM竞赛)— 相似度80%
|
||||||
|
- **地址**:https://github.com/Open-Finance-Lab/FinRL_Contest_2025
|
||||||
|
- **论文**:https://arxiv.org/html/2504.02281v4
|
||||||
|
- **核心**:因子挖掘(Factor Mining) + 集成学习(Ensemble Learning)
|
||||||
|
- 两阶段:① 特征工程+因子选择 ② 多模型集成
|
||||||
|
- **映射关系**:他们的"因子"=我们的"信号源",他们的"集成权重"=我们的"评分权重"
|
||||||
|
- **区别**:他们用强化学习(RL)训练Agent,我们用统计回归优化权重
|
||||||
|
- **可借鉴**:遗传算法优化权重(比逻辑回归更强)
|
||||||
|
|
||||||
|
#### 3. ACM论文:ML驱动多因子量化模型(ETH市场)— 相似度75%
|
||||||
|
- **地址**:https://dl.acm.org/doi/10.1145/3766918.3766922
|
||||||
|
- **核心**:把交易因子分三类 — 传统技术因子 + 链上因子 + ML生成因子
|
||||||
|
- 用IC值(信息系数)衡量每个因子的预测力
|
||||||
|
- 用**遗传算法**自动优化因子权重
|
||||||
|
- 信号用Z-score阈值触发(>1买入,<-1卖出)
|
||||||
|
- **启发**:我们也可以用IC值来量化每个信号源的贡献
|
||||||
|
|
||||||
|
#### 4. CoinGlass CDRI(衍生品风险指数)— 相似度70%
|
||||||
|
- **地址**:https://www.coinglass.com/pro/i/CDRI
|
||||||
|
- **核心**:综合OI/FR/清算/CVD等多指标打分,评分>80或<20触发信号
|
||||||
|
- **区别**:他们只做风险预警展示,不做自动交易
|
||||||
|
|
||||||
|
### 行业信号源使用频率排名
|
||||||
|
|
||||||
|
| 排名 | 信号源 | 行业使用频率 | 我们状态 | 数据源 | 获取难度 |
|
||||||
|
|------|--------|------------|---------|--------|---------|
|
||||||
|
| 1 | CVD/Order Flow | ⭐⭐⭐⭐⭐ | ✅ 已有 | agg_trades | - |
|
||||||
|
| 2 | Open Interest | ⭐⭐⭐⭐⭐ | ✅ 已有 | 币安API | - |
|
||||||
|
| 3 | Funding Rate | ⭐⭐⭐⭐⭐ | ⬜ 待加 | 币安API(免费) | ⭐ |
|
||||||
|
| 4 | 清算数据 | ⭐⭐⭐⭐ | ⬜ 待加 | 币安WS forceOrder | ⭐⭐ |
|
||||||
|
| 5 | 多空比 | ⭐⭐⭐⭐ | ✅ 已有 | 币安API | - |
|
||||||
|
| 6 | 链上净流入/流出 | ⭐⭐⭐⭐ | ⬜ 待加 | CryptoQuant(付费) | ⭐⭐⭐ |
|
||||||
|
| 7 | Coinbase Premium | ⭐⭐⭐ | ✅ 已有 | 价差计算 | - |
|
||||||
|
| 8 | 社交情绪 | ⭐⭐⭐ | ⬜ 待加 | Santiment/LLM | ⭐⭐⭐ |
|
||||||
|
| 9 | 期权PCR/DVOL | ⭐⭐⭐ | ⬜ 待加 | Deribit API | ⭐⭐ |
|
||||||
|
| 10 | 鲸鱼钱包追踪 | ⭐⭐⭐ | ⬜ 待加 | Nansen(付费) | ⭐⭐⭐ |
|
||||||
|
| 11 | 清算热力图 | ⭐⭐ | ⬜ 待加 | CoinGlass API(付费) | ⭐⭐ |
|
||||||
|
| 12 | 订单簿深度 | ⭐⭐ | ⬜ 待加 | 币安WS depth | ⭐⭐ |
|
||||||
|
|
||||||
|
### 关键结论
|
||||||
|
|
||||||
|
1. **没有人做得和我们完全一样** — 大多数用传统技术指标或纯ML黑箱,用CVD+多空比+OI做多层评分的几乎没有
|
||||||
|
2. **行业趋势明确** — 多因子 + ML权重优化,和范总定的方向完全一致
|
||||||
|
3. **Funding Rate是最高优先级** — 行业使用率最高、免费获取、我们还没加
|
||||||
|
4. **权重优化可升级** — 从逻辑回归→IC值+遗传算法,参考ACM论文方法
|
||||||
|
|
||||||
|
### 数据供应商参考
|
||||||
|
|
||||||
|
| 平台 | 核心能力 | 价格 | 适合 |
|
||||||
|
|------|---------|------|------|
|
||||||
|
| CoinGlass | 衍生品数据(OI/FR/清算/热力图) | 免费基础+付费API | 清算数据 |
|
||||||
|
| CryptoQuant | 链上数据(净流入/矿工/交易所储备) | $29/月起 | 链上因子 |
|
||||||
|
| Santiment | 社交情绪+链上+开发活跃度 | 免费基础+付费 | 情绪因子 |
|
||||||
|
| Glassnode | 链上高级指标(SOPR/NUPL/STH-LTH) | $39/月起 | 深度链上 |
|
||||||
|
| Nansen | 鲸鱼钱包追踪+Smart Money | $100/月起 | 鲸鱼行为 |
|
||||||
193
docs/arbitrage-engine/v52-performance-analysis.md
Normal file
193
docs/arbitrage-engine/v52-performance-analysis.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
title: V5.2 模拟盘执行分析报告
|
||||||
|
date: 2026-03-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.2 模拟盘执行分析报告
|
||||||
|
|
||||||
|
> 数据口径:真实成交价(agg_trades)+ 手续费扣除,calc_version=2
|
||||||
|
> 分析日期:2026-03-03
|
||||||
|
> 策略名称:v52_8signals(8信号源)
|
||||||
|
> 参与分析:露露(Sonnet 4.6)、小范(GPT-5.3-Codex)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、总体概况
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 总交易笔数 | 156笔(含4笔活跃/tp1_hit) |
|
||||||
|
| 已闭合笔数 | 152笔 |
|
||||||
|
| 净R(含手续费)| **-25.07R** |
|
||||||
|
| 毛R(不含手续费)| **-3.27R** |
|
||||||
|
| 总手续费 | **21.80R** |
|
||||||
|
| 平均单笔手续费 | 0.143R |
|
||||||
|
| 胜率 | 51.3% |
|
||||||
|
| 平均每笔净R | -0.165R |
|
||||||
|
|
||||||
|
> 本金10,000 USD,1R=200 USD → 净亏损约5,014 USD
|
||||||
|
> 与V5.1对比:笔数少(152 vs 500),手续费率低(0.143R vs 0.218R),**但毛R为负(-3.27R)**,信号层本身无优势
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、V5.2 vs V5.1 对比
|
||||||
|
|
||||||
|
| 指标 | V5.1 | V5.2 |
|
||||||
|
|------|------|------|
|
||||||
|
| 已闭合笔数 | 500 | 152 |
|
||||||
|
| 净R | -99.73R | -25.07R |
|
||||||
|
| 毛R | **+10.73R** | **-3.27R** |
|
||||||
|
| 总手续费 | 110.46R | 21.80R |
|
||||||
|
| 平均单笔费 | 0.218R | 0.143R |
|
||||||
|
| 胜率 | 55.4% | 51.3% |
|
||||||
|
| 平均净R | -0.193R | -0.165R |
|
||||||
|
|
||||||
|
**关键差异:V5.1毛R为正(+11R),V5.2毛R为负(-3R)。V5.2交易频率低但信号质量更差。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、出场类型分布
|
||||||
|
|
||||||
|
| 状态 | 笔数 | 平均R(净) | 合计R | 平均手续费 |
|
||||||
|
|------|------|-----------|-------|----------|
|
||||||
|
| sl(止损)| 43 | -1.161R | **-49.90R** | 0.161R |
|
||||||
|
| timeout(超时)| 32 | +0.055R | +1.74R | 0.132R |
|
||||||
|
| sl_be(保本止损)| 30 | +0.179R | +5.38R | 0.154R |
|
||||||
|
| tp(止盈)| 29 | +0.964R | +27.97R | 0.119R |
|
||||||
|
| signal_flip(翻转)| 18 | -0.570R | -10.25R | 0.145R |
|
||||||
|
|
||||||
|
**关键发现**:
|
||||||
|
- SL 43笔亏49.90R,TP只29笔赚27.97R,差额-21.93R
|
||||||
|
- signal_flip出场损耗大(-0.570R均值),说明信号方向频繁切换
|
||||||
|
- **SL均值-1.161R,低于V5.1的-1.232R,单笔止损更小(SL更宽)**
|
||||||
|
|
||||||
|
### SL均值拆解
|
||||||
|
|
||||||
|
| 组成 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| SL基础R | -1.000R |
|
||||||
|
| 手续费 | -0.161R |
|
||||||
|
| 净SL | -1.161R |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、方向分析
|
||||||
|
|
||||||
|
| 方向 | 笔数 | 胜率 | 合计R |
|
||||||
|
|------|------|------|-------|
|
||||||
|
| LONG | 103 | 49.5% | -13.18R |
|
||||||
|
| SHORT | 53 | 54.9% | -11.88R |
|
||||||
|
|
||||||
|
**结论**:LONG胜率仅49.5%(低于随机),多空均亏。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、币种分析
|
||||||
|
|
||||||
|
| 币种 | 笔数 | 胜率 | 合计R |
|
||||||
|
|------|------|------|-------|
|
||||||
|
| BTCUSDT | 40 | **35.9%** | **-13.24R** |
|
||||||
|
| XRPUSDT | 37 | 52.8% | -9.17R |
|
||||||
|
| ETHUSDT | 37 | 58.3% | -3.40R |
|
||||||
|
| SOLUSDT | 42 | 58.5% | **+0.74R** |
|
||||||
|
|
||||||
|
**关键发现**:
|
||||||
|
- BTC胜率35.9%,严重低于随机,V5.2比V5.1更差(V5.1是49.3%)
|
||||||
|
- SOL是唯一正R币种(+0.74R),胜率58.5%
|
||||||
|
- ETH胜率58.3%但仍净亏(手续费拖累)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、信号分数段分析
|
||||||
|
|
||||||
|
| 分数段 | 笔数 | 胜率 | 合计R |
|
||||||
|
|--------|------|------|-------|
|
||||||
|
| 75-79 | 105 | 50.5% | -21.33R |
|
||||||
|
| 80-84 | 42 | 45.2% | -9.51R |
|
||||||
|
| 85+ | 9 | **88.9%** | **+5.77R** |
|
||||||
|
|
||||||
|
**重要发现**:
|
||||||
|
- V5.2的85+高分段胜率88.9%,合计+5.77R,**与V5.1完全相反**(V5.1高分无效)
|
||||||
|
- 但样本量太少(只有9笔),统计意义有限
|
||||||
|
- 75-84分段表现极差,80-84甚至只有45.2%胜率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、时段分析(北京时间)
|
||||||
|
|
||||||
|
### 盈利时段(合计R>0)
|
||||||
|
| 时段 | R | 胜率 |
|
||||||
|
|------|---|------|
|
||||||
|
| 05:00 | +0.94R | 75.0% |
|
||||||
|
| 08:00 | +2.93R | 85.7% |
|
||||||
|
| 22:00 | +4.76R | 80.0% |
|
||||||
|
| 23:00 | +7.62R | 85.7% |
|
||||||
|
|
||||||
|
### 重度亏损时段(胜率<30%)
|
||||||
|
| 时段 | R | 胜率 |
|
||||||
|
|------|---|------|
|
||||||
|
| 00:00 | -4.97R | 14.3% |
|
||||||
|
| 09:00 | -5.88R | 14.3% |
|
||||||
|
| 12:00 | -4.38R | 33.3% |
|
||||||
|
| 13:00 | -5.22R | 40.0% |
|
||||||
|
|
||||||
|
**V5.1和V5.2共同亏损时段**:09:00、13:00(两个策略均在这两个时段表现极差)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、持仓时间分析
|
||||||
|
|
||||||
|
| 出场类型 | 平均持仓 |
|
||||||
|
|----------|---------|
|
||||||
|
| timeout | 60.0分钟 |
|
||||||
|
| sl_be | 26.8分钟 |
|
||||||
|
| tp | 27.0分钟 |
|
||||||
|
| sl | **28.1分钟** |
|
||||||
|
| flip | 27.0分钟 |
|
||||||
|
|
||||||
|
**与V5.1对比**:V5.2的SL持仓时间更长(28min vs 18min),说明SL空间更宽(sl=2.1×ATR vs 1.4×ATR),但仍被打出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、风险统计
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 单笔最大亏损 | -1.29R |
|
||||||
|
| 单笔最大盈利 | +1.02R |
|
||||||
|
| 标准差 | 0.796R |
|
||||||
|
| 中位数 | +0.079R |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、核心结论
|
||||||
|
|
||||||
|
### V5.2 vs V5.1 关键差异
|
||||||
|
|
||||||
|
| 维度 | V5.1 | V5.2 | 解读 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 毛R | +10.73R | -3.27R | V5.2信号质量更差 |
|
||||||
|
| 胜率 | 55.4% | 51.3% | V5.2信号未改善胜率 |
|
||||||
|
| 单笔费 | 0.218R | 0.143R | V5.2手续费率更低(SL更宽) |
|
||||||
|
| BTC胜率 | 49.3% | 35.9% | V5.2在BTC上更差 |
|
||||||
|
| 85+分段 | 无效 | 88.9%(9笔)| 样本太少,不可靠 |
|
||||||
|
|
||||||
|
### V5.2失败原因
|
||||||
|
1. **信号质量比V5.1更差**:毛R从+11R变成-3R,8个信号源的叠加没有提升预测能力,反而带来更多噪声
|
||||||
|
2. **BTC更差**:35.9%胜率说明额外信号源对BTC的预测无帮助
|
||||||
|
3. **signal_flip损耗大**:-0.570R均值,方向频繁切换,每次翻转都有损耗
|
||||||
|
4. **统计样本不足**:152笔相对于策略评估太少,结论不确定性高
|
||||||
|
|
||||||
|
### 与Gemini分析对照
|
||||||
|
- CVD双重计分问题在V5.2同样存在
|
||||||
|
- V5.2增加的8个信号源(相比V5.1的6个)未能提升正交性
|
||||||
|
- V5.3应从根本上解决因子多重共线性问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、对V5.3设计的启示
|
||||||
|
|
||||||
|
1. **V5.1有微弱信号(毛R正),V5.2没有**:V5.3应保留V5.1的核心因子,不是简单增加信号
|
||||||
|
2. **BTC在两个版本都表现差**:V5.3可考虑完全不交易BTC,专注ETH/XRP/SOL
|
||||||
|
3. **08:00、22:00、23:00是两个策略共同盈利时段**:这些时段可能有结构性因素(美盘/亚盘交替)
|
||||||
|
4. **删除确认层(CVD重复)是最优先改动**:在V5.1和V5.2中均可验证这是评分失真的根源
|
||||||
151
docs/arbitrage-engine/v52-signal-system.md
Normal file
151
docs/arbitrage-engine/v52-signal-system.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
title: V5.2 信号系统文档
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.2 八信号源系统(v52_8signals)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
V5.2 在 V5.1 基础上新增 **资金费率(Funding Rate)** 和 **清算数据(Liquidation)** 两个信号源,形成 7 层 100 分评分体系。目标是提高信号精准度,减少无效开仓。
|
||||||
|
|
||||||
|
## 与 V5.1 的核心差异
|
||||||
|
|
||||||
|
| 对比项 | V5.1 | V5.2 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 信号源 | 6个 | 8个(+FR, +清算) |
|
||||||
|
| 评分层 | 5层 | 7层(+FR层, +清算层) |
|
||||||
|
| 方向权重 | 45分 | 40分 |
|
||||||
|
| TP/SL | SL=2.0×ATR | SL=3.0×ATR(更宽) |
|
||||||
|
| 盈亏比目标 | 0.72 | 0.84+ |
|
||||||
|
|
||||||
|
## 评分体系(7层100分)
|
||||||
|
|
||||||
|
| 层级 | 权重 | 信号源 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 方向层 | 40分 | CVD_fast + CVD_mid + P99大单 | 同V5.1但权重降低 |
|
||||||
|
| 拥挤层 | 18分 | 多空比 + 大户持仓 | 结构性仓位判断 |
|
||||||
|
| **FR层** | **5分** | **资金费率** | **持仓成本顺风度** |
|
||||||
|
| 环境层 | 12分 | OI变化率 | 同V5.1但权重降低 |
|
||||||
|
| 确认层 | 15分 | CVD双周期共振 | 同V5.1 |
|
||||||
|
| **清算层** | **5分** | **清算比率** | **市场清洗力度** |
|
||||||
|
| 辅助层 | 5分 | Coinbase Premium | 同V5.1 |
|
||||||
|
|
||||||
|
### FR层详解(5分)— 线性评分
|
||||||
|
|
||||||
|
**数据源**:Binance fundingRate,每5分钟采集,每8小时结算。
|
||||||
|
|
||||||
|
**评分公式**:
|
||||||
|
```
|
||||||
|
raw_score = (|当前FR| / 历史最大FR) × 5,上限5分
|
||||||
|
|
||||||
|
有利方向 → fr_score = raw_score(0~5)
|
||||||
|
不利方向 → fr_score = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**方向判断**:
|
||||||
|
- 做多 + FR为负(空头付费给多头)= 有利 → 给分
|
||||||
|
- 做空 + FR为正(多头付费给空头)= 有利 → 给分
|
||||||
|
- 其他 = 不利 → 0分(不扣分)
|
||||||
|
|
||||||
|
**历史最大FR**:从数据库实时计算,每小时缓存一次。数据越多越精准。
|
||||||
|
|
||||||
|
| 币种 | 历史最大FR | 说明 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| BTC | ~0.0046% | 波动最小 |
|
||||||
|
| ETH | ~0.0095% | 中等 |
|
||||||
|
| XRP | ~0.022% | 波动大 |
|
||||||
|
| SOL | ~0.022% | 波动大 |
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- 信号方向已确定,FR只评估"有多顺风"
|
||||||
|
- 不利方向不扣分(方向决策不由FR层负责)
|
||||||
|
- 线性映射,不分层不设阈值,灵活精准
|
||||||
|
- 分数带小数(如 +2.27分、+0.33分)
|
||||||
|
|
||||||
|
### 清算层详解(5分)— 梯度评分
|
||||||
|
|
||||||
|
**数据源**:liq-collector 实时采集清算事件。
|
||||||
|
|
||||||
|
**评分逻辑**:
|
||||||
|
```
|
||||||
|
ratio = 对手方清算USD / 己方清算USD
|
||||||
|
|
||||||
|
做多 → ratio = 空头清算 / 多头清算
|
||||||
|
做空 → ratio = 多头清算 / 空头清算
|
||||||
|
|
||||||
|
ratio ≥ 2.0 → 5分
|
||||||
|
ratio ≥ 1.5 → 3分
|
||||||
|
ratio ≥ 1.2 → 1分
|
||||||
|
ratio < 1.2 → 0分
|
||||||
|
```
|
||||||
|
|
||||||
|
**逻辑**:对手方爆仓越多,说明市场正在朝我们的方向清洗,有利信号。
|
||||||
|
|
||||||
|
## TP/SL 设置
|
||||||
|
|
||||||
|
| 参数 | V5.1 | V5.2 | 变化原因 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| SL | 2.0 × ATR | 3.0 × ATR | 更宽止损,减少噪声出局 |
|
||||||
|
| TP1 | 1.5 × ATR | 2.0 × ATR | 更远止盈,提高盈亏比 |
|
||||||
|
| TP2 | 3.0 × ATR | 4.5 × ATR | 大幅提升盈亏比目标 |
|
||||||
|
|
||||||
|
理论盈亏比从 0.72 提升到 0.84。
|
||||||
|
|
||||||
|
## 开仓规则
|
||||||
|
|
||||||
|
与 V5.1 相同:
|
||||||
|
- 阈值:75分(标准),85分(加仓)
|
||||||
|
- 冷却:10分钟
|
||||||
|
- 最大持仓:4笔
|
||||||
|
- 反向翻转:反向≥75分 → 平旧开新
|
||||||
|
|
||||||
|
## 策略配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "v52_8signals",
|
||||||
|
"version": "5.2",
|
||||||
|
"threshold": 75,
|
||||||
|
"weights": {
|
||||||
|
"direction": 40,
|
||||||
|
"crowding": 18,
|
||||||
|
"funding_rate": 5,
|
||||||
|
"environment": 12,
|
||||||
|
"confirmation": 15,
|
||||||
|
"liquidation": 5,
|
||||||
|
"auxiliary": 5
|
||||||
|
},
|
||||||
|
"accel_bonus": 0,
|
||||||
|
"tp_sl": {
|
||||||
|
"sl_multiplier": 3.0,
|
||||||
|
"tp1_multiplier": 2.0,
|
||||||
|
"tp2_multiplier": 4.5
|
||||||
|
},
|
||||||
|
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## AB测试观测清单(2026-03-02 ~ 03-16)
|
||||||
|
|
||||||
|
### 冻结期规则
|
||||||
|
- 不改权重、不改阈值、不改评分逻辑
|
||||||
|
- 单写入源(小周生产环境)
|
||||||
|
- 目标:V5.1 500+笔,V5.2 200+笔
|
||||||
|
|
||||||
|
### 两周后评审项
|
||||||
|
1. **确认层重复计分审计** — 方向层和确认层同源,看区分度
|
||||||
|
2. **拥挤层 vs FR相关性** — `corr(FR_score, crowd_score)`,>0.7则降一层
|
||||||
|
3. **OI持续性审计** — `oi_persist_n=1` vs `>=2` 胜率差异
|
||||||
|
4. **清算触发率审计** — 按币种,避免触发不均衡
|
||||||
|
5. **config_hash落库** — 权重调整前补版本标识
|
||||||
|
|
||||||
|
### 权重优化路径
|
||||||
|
- 200+笔 → 统计分析(各层分布+胜率关联)
|
||||||
|
- 500+笔 → 回归分析(哪些层对盈亏贡献最大)
|
||||||
|
- 1000+笔 → ML(XGBoost等)
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
1. **方向层与确认层同源重复** — 等数据验证
|
||||||
|
2. **清算样本少** — 才积累2天,ratio波动大
|
||||||
|
3. **币种间FR基准差异大** — BTC max=0.0046% vs SOL=0.022%,线性映射已自动处理
|
||||||
304
docs/arbitrage-engine/v53-design.md
Normal file
304
docs/arbitrage-engine/v53-design.md
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
---
|
||||||
|
title: V5.3 统一信号系统设计案
|
||||||
|
date: 2026-03-03
|
||||||
|
updated: 2026-03-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.3 统一信号系统设计案
|
||||||
|
|
||||||
|
> 目标:让策略从"手工打分规则"升级为"可持续训练和迭代的小模型系统"。统一架构覆盖 BTC/ETH/XRP/SOL,per-symbol 参数化门控,消除双轨维护成本。
|
||||||
|
|
||||||
|
## 1. 设计原则
|
||||||
|
|
||||||
|
1. **统一评分、差异化门控**:四层评分逻辑完全一致,通过 `symbol_gates` 参数化各币种的门控阈值。
|
||||||
|
2. **先数据、后调参**:先补齐特征与标签落库,再做参数优化。
|
||||||
|
3. **反过拟合优先**:任何优化必须先过样本外验证(OOS)。
|
||||||
|
4. **信号与执行解耦**:Alpha(信号)与成本(执行)分开归因。
|
||||||
|
5. **版本可追溯**:每次信号和交易都可回溯到 `strategy_version + config_hash + engine_instance`。
|
||||||
|
|
||||||
|
## 2. 现状问题归纳(V5.1/V5.2)
|
||||||
|
|
||||||
|
- V5.1:毛R为正、净R为负,说明有 Alpha 但被手续费和执行摩擦吞噬。
|
||||||
|
- V5.2:交易频率下降但毛R转负,说明新增因子未提升预测力,存在噪声与冗余。
|
||||||
|
- 评分结构存在共线性:方向层与确认层同源(重复使用 CVD_fast/CVD_mid)。
|
||||||
|
- BTC 与 ALT 使用同构逻辑,忽略了市场微结构差异。
|
||||||
|
|
||||||
|
## 3. V5.3 总体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Market Data → Feature Snapshot → _evaluate_v53() → Gate Check → Signal Decision → Execution → Label Backfill → Walk-Forward Eval
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 统一策略(v53)
|
||||||
|
|
||||||
|
单一策略文件 `backend/strategies/v53.json`,覆盖 BTC/ETH/XRP/SOL。
|
||||||
|
|
||||||
|
**四层评分(总分100)**
|
||||||
|
|
||||||
|
| 层 | 权重 | 子项 |
|
||||||
|
|---|---|---|
|
||||||
|
| Direction | 55 | CVD共振(30) + P99大单对齐(20) + 加速奖励(5) |
|
||||||
|
| Crowding | 25 | LSR反向(15) + 大户持仓(10) |
|
||||||
|
| Environment | 15 | OI变化率 |
|
||||||
|
| Auxiliary | 5 | Coinbase Premium |
|
||||||
|
|
||||||
|
**Per-symbol 四门控制(symbol_gates)**
|
||||||
|
|
||||||
|
| 门 | BTC | ETH | XRP | SOL |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 波动率下限 | 0.2% | 0.3% | 0.4% | 0.6% |
|
||||||
|
| 鲸鱼阈值/逻辑 | whale_cvd_ratio >$100k | 大单否决 $50k | 大单否决 $30k | 大单否决 $20k |
|
||||||
|
| OBI否决 | ±0.30 | ±0.35 | ±0.40 | ±0.45 |
|
||||||
|
| 期现背离否决 | ±0.3% | ±0.5% | ±0.6% | ±0.8% |
|
||||||
|
|
||||||
|
**开仓档位**
|
||||||
|
- < 75分:不开仓
|
||||||
|
- 75–84分:标准仓(1×R)
|
||||||
|
- ≥ 85分:加仓档(1.5×R)
|
||||||
|
- 冷却期:10分钟
|
||||||
|
|
||||||
|
### 3.2 实时数据流
|
||||||
|
|
||||||
|
| 数据 | 来源 | 频率 | 覆盖币种 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| OBI(订单簿失衡) | `@depth10@100ms` perp WS | 100ms | BTC/ETH/XRP/SOL |
|
||||||
|
| 期现背离 | `@bookTicker` spot + `@markPrice@1s` perp | 1s | BTC/ETH/XRP/SOL |
|
||||||
|
| 巨鲸CVD | aggTrades 流内计算(>$100k) | 实时 | BTC |
|
||||||
|
| 大单方向 | aggTrades 流内计算 | 实时 | ETH/XRP/SOL |
|
||||||
|
|
||||||
|
## 4. 训练数据飞轮(Phase 3)
|
||||||
|
|
||||||
|
```
|
||||||
|
signal_feature_events (raw features, 每轮评分写入)
|
||||||
|
↓ label_backfill.py (T+60m打标签)
|
||||||
|
signal_label_events (y_binary_60m, mfe_r_60m, mae_r_60m)
|
||||||
|
↓ walk_forward.py
|
||||||
|
权重优化 → v53.json 更新
|
||||||
|
```
|
||||||
|
|
||||||
|
**Walk-Forward 规则(严防过拟合)**
|
||||||
|
- 训练窗口:30天,步长:7天
|
||||||
|
- 验证集:永远在训练集之后,不交叉
|
||||||
|
- 评估指标:OOS 净R、胜率、MDD
|
||||||
|
|
||||||
|
## 5. 版本演进记录
|
||||||
|
|
||||||
|
| 版本 | 时间 | 变更摘要 |
|
||||||
|
|---|---|---|
|
||||||
|
| V5.1 | 2026-02 | 基础CVD评分,有毛Alpha,净R为负 |
|
||||||
|
| V5.2 | 2026-02 | 新增8信号层,频率下降但净R未改善 |
|
||||||
|
| V5.3 Phase0 | 2026-03-03 | 建立feature/label落库表,ATR列 |
|
||||||
|
| V5.3 Phase1 | 2026-03-03 | 四层评分+双轨(alt/btc),删确认层 |
|
||||||
|
| V5.3 Phase2 | 2026-03-03 | RT-WS接入(OBI+期现背离),覆盖所有symbol |
|
||||||
|
| V5.3 统一版 | 2026-03-03 | 合并alt/btc为单一v53策略,per-symbol门控 |
|
||||||
|
|
||||||
|
- **Feature Snapshot**:每次评估时落库原始特征和中间分数(含 `atr_value` 快照)。
|
||||||
|
- **Track Router**:按 symbol 路由到 ALT/BTC 模型。
|
||||||
|
- **Signal Decision**:输出开仓/不开仓/翻转决策和原因。
|
||||||
|
- **Execution**:独立处理 maker/taker、TP/SL、BE、flip。
|
||||||
|
- **Label Backfill**:按 15/30/60 分钟回填标签。
|
||||||
|
- **Walk-Forward Eval**:滚动训练与验证,驱动版本迭代。
|
||||||
|
|
||||||
|
## 4. 双轨模型定义
|
||||||
|
|
||||||
|
## 4.1 ALT 轨(ETH/XRP/SOL)
|
||||||
|
|
||||||
|
- 目标:保留 V5.1 有效微观结构 Alpha,去除冗余。
|
||||||
|
- 关键变更:取消独立 Confirmation 层(避免与 Direction 共线性重复计分)。
|
||||||
|
- 决策机制:线性加权评分(总分 100)+ 门控 + 阈值。
|
||||||
|
|
||||||
|
### 4.1.1 ALT 权重总表(V5.3 初版)
|
||||||
|
|
||||||
|
| 层级 | 权重 | 子特征 | 子特征权重 | 说明 |
|
||||||
|
|------|------|--------|------------|------|
|
||||||
|
| Direction | 55 | `cvd_resonance` | 30 | `cvd_fast` 与 `cvd_mid` 同向共振 |
|
||||||
|
| Direction | 55 | `p99_flow_alignment` | 20 | P99 大单方向与主方向一致 |
|
||||||
|
| Direction | 55 | `cvd_accel_bonus` | 5 | CVD 加速度同向奖励 |
|
||||||
|
| Crowding | 25 | `lsr_contrarian` | 15 | 多空比反向拥挤 |
|
||||||
|
| Crowding | 25 | `top_trader_position` | 10 | 大户持仓方向确认 |
|
||||||
|
| Environment | 15 | `oi_delta_regime` | 15 | OI 变化状态 |
|
||||||
|
| Auxiliary | 5 | `coinbase_premium` | 5 | 美系现货溢价辅助 |
|
||||||
|
|
||||||
|
### 4.1.2 ALT 子特征评分函数(标准化)
|
||||||
|
|
||||||
|
- `cvd_resonance`(0/30):
|
||||||
|
- LONG:`cvd_fast > 0 && cvd_mid > 0`
|
||||||
|
- SHORT:`cvd_fast < 0 && cvd_mid < 0`
|
||||||
|
- 否则 `0` 且 `gate_no_direction=true`
|
||||||
|
- `p99_flow_alignment`(0/10/20):
|
||||||
|
- 强同向净流:20
|
||||||
|
- 无明显反向压制:10
|
||||||
|
- 明显反向:0
|
||||||
|
- `cvd_accel_bonus`(0/5):
|
||||||
|
- 加速度方向与主方向一致:5
|
||||||
|
- `lsr_contrarian`(0~15):
|
||||||
|
- LONG:`lsr<=0.5` 高分,`0.5<lsr<1.0` 线性递减
|
||||||
|
- SHORT:`lsr>=2.0` 高分,`1.0<lsr<2.0` 线性递减
|
||||||
|
- `top_trader_position`(0~10):
|
||||||
|
- 与方向一致比例越高分越高
|
||||||
|
- `oi_delta_regime`(0/7.5/15):
|
||||||
|
- 资金显著流入:15
|
||||||
|
- 平稳:7.5
|
||||||
|
- 显著流出:0
|
||||||
|
- `coinbase_premium`(0/2/5):
|
||||||
|
- 顺向溢价:5
|
||||||
|
- 中性:2
|
||||||
|
- 反向:0
|
||||||
|
|
||||||
|
### 4.1.3 ALT 决策阈值
|
||||||
|
|
||||||
|
- `open_threshold = 75`
|
||||||
|
- `flip_threshold = 85`
|
||||||
|
- `max_positions_per_track = 4`
|
||||||
|
- `cooldown_seconds = 300`
|
||||||
|
|
||||||
|
### 4.1.4 ALT 信号总分公式
|
||||||
|
|
||||||
|
```text
|
||||||
|
score_alt = direction(55) + crowding(25) + environment(15) + auxiliary(5)
|
||||||
|
open if score_alt >= 75 and no veto
|
||||||
|
flip if reverse_score_alt >= 85 and no veto
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.2 BTC 轨(独立模型)
|
||||||
|
|
||||||
|
- 目标:针对机构主导盘口,提升信号有效性。
|
||||||
|
- 决策方式:先用"条件门控 + 否决条件",不与 ALT 共用线性总分。
|
||||||
|
|
||||||
|
### 4.2.1 BTC 核心特征
|
||||||
|
|
||||||
|
- `atr_percent_1h`:1小时 ATR 占当前价格百分比(波动率门控)
|
||||||
|
- `tiered_cvd_whale`:按成交额分层的大单净流(建议主桶 `>100k`)
|
||||||
|
- `obi_depth_10`:盘口前 10 档失衡
|
||||||
|
- `spot_perp_divergence`:现货与永续价量背离
|
||||||
|
|
||||||
|
### 4.2.2 BTC 门控与否决逻辑
|
||||||
|
|
||||||
|
- 波动率门控:`atr_percent_1h < min_vol_threshold` -> veto
|
||||||
|
- 方向门控:巨鲸净流未达阈值 -> veto
|
||||||
|
- 挂单墙否决:方向与 OBI 显著冲突 -> veto
|
||||||
|
- 期现背离否决:perp 强多但 spot 弱(或反向)-> veto
|
||||||
|
|
||||||
|
### 4.2.3 BTC 参数(初始值,可配置)
|
||||||
|
|
||||||
|
- `min_vol_threshold = 0.002`(0.2%)
|
||||||
|
- `obi_veto_threshold = 0.30`
|
||||||
|
- `whale_flow_threshold`:按币价与流动性分档配置
|
||||||
|
- 以上参数均定义为配置项,禁止散落硬编码。
|
||||||
|
|
||||||
|
### 4.2.4 BTC 决策伪代码
|
||||||
|
|
||||||
|
```text
|
||||||
|
if missing_any_feature:
|
||||||
|
block("missing_feature")
|
||||||
|
if atr_percent_1h < min_vol_threshold:
|
||||||
|
block("low_vol_regime")
|
||||||
|
if abs(tiered_cvd_whale) < whale_flow_threshold:
|
||||||
|
block("weak_whale_flow")
|
||||||
|
if direction_conflict_with_obi:
|
||||||
|
block("obi_imbalance_veto")
|
||||||
|
if spot_perp_divergence_is_trap:
|
||||||
|
block("spot_perp_divergence_veto")
|
||||||
|
otherwise:
|
||||||
|
allow_open
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 数据基建(ML Ready)
|
||||||
|
|
||||||
|
## 5.1 表设计
|
||||||
|
|
||||||
|
### `signal_feature_events`
|
||||||
|
|
||||||
|
- 用途:每次信号评估快照(无论是否开仓)。
|
||||||
|
- 关键字段:
|
||||||
|
- 元数据:`event_id, ts, symbol, track, side`
|
||||||
|
- 版本:`strategy, strategy_version, config_hash, engine_instance`
|
||||||
|
- 原始特征:`cvd_fast_raw, cvd_mid_raw, p99_flow_raw, accel_raw, ls_ratio_raw, top_pos_raw, oi_delta_raw, coinbase_premium_raw, fr_raw, liq_raw, obi_raw, tiered_cvd_whale_raw, atr_value`
|
||||||
|
- 决策:`score_total, score_direction, score_crowding, score_environment, score_aux, gate_passed, block_reason`
|
||||||
|
|
||||||
|
### `signal_label_events`
|
||||||
|
|
||||||
|
- 用途:延迟回填标签,评估信号纯预测能力。
|
||||||
|
- 字段:`event_id, y_binary_30m, y_binary_60m, y_return_15m, y_return_30m, y_return_60m, mfe_r_60m, mae_r_60m`
|
||||||
|
|
||||||
|
### `execution_cost_events`
|
||||||
|
|
||||||
|
- 用途:独立归因执行成本。
|
||||||
|
- 字段:`trade_id, entry_type, exit_type, fee_bps, slippage_bps, maker_ratio, flip_flag, hold_seconds, friction_cost_r`
|
||||||
|
|
||||||
|
## 5.2 标签定义
|
||||||
|
|
||||||
|
- `Y_binary_60m`(严格定义):从信号触发时间 `ts` 起 60 分钟内,使用 `Mark Price` 序列判定,若价格先触及 `+2.0 * atr_value`,且在该触发时刻之前从未触及 `-1.0 * atr_value`,则记为 `1`,否则记为 `0`。
|
||||||
|
- 时间顺序要求(Chronological Order):若 60 分钟窗口内先触及 `-1.0 * atr_value`,即使后续再触及 `+2.0 * atr_value`,也必须记为 `0`。
|
||||||
|
- `Y_return_t`:固定时间窗(15m/30m/60m)净收益率(含成本估计)。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 标签优先评价"信号有效性",而不是被具体 TP/SL 参数污染的最终交易结果。
|
||||||
|
- 统一使用 `Mark Price` + `atr_value` 快照,避免插针和重算偏差。
|
||||||
|
|
||||||
|
## 6. 执行引擎改造
|
||||||
|
|
||||||
|
1. **TP 优先 Maker + Taker 兜底**:入场后预挂 TP1/TP2 限价单;若价格已越过 TP 触发价且挂单在超时窗口(如 2 秒)内仍未成交,立即撤单并用 Taker 市价平仓兜底。
|
||||||
|
2. **部分成交分支**:兜底前查询成交量,仅对剩余仓位执行 `Cancel -> Taker Close`。
|
||||||
|
3. **Break-Even 费用感知**:BE 触发价需覆盖手续费与滑点缓冲,避免"名义保本、账户实亏"。
|
||||||
|
4. **Flip 双门槛**:开仓阈值 `75`,翻转阈值 `85`。
|
||||||
|
5. **并发和幂等**:`Cancel -> Market` 需要状态锁和幂等键,防止重复平仓。
|
||||||
|
6. **执行质量指标化**:持续监控 `maker_ratio / avg_friction_cost_r / flip_loss_r`。
|
||||||
|
|
||||||
|
## 7. 反过拟合协议(强制)
|
||||||
|
|
||||||
|
1. **Walk-Forward Optimization**:训练窗与验证窗严格时间隔离。
|
||||||
|
2. **参数冻结**:一个评估周期内禁止改权重、阈值。
|
||||||
|
3. **特征预算**:样本不足时严格限制特征数量,新增特征先 shadow 记录。
|
||||||
|
4. **升级门槛**:样本外结果不达标不得进入下一阶段。
|
||||||
|
5. **可解释性检查**:无金融逻辑支撑的"高胜率规则"禁止上线。
|
||||||
|
|
||||||
|
## 8. 模型权重训练与更新机制(新增)
|
||||||
|
|
||||||
|
## 8.1 参数分层
|
||||||
|
|
||||||
|
- `static_params`:交易风控硬约束(如最大仓位、最大回撤阈值)
|
||||||
|
- `tunable_params`:可训练参数(ALT 子特征权重、阈值、BTC 门控阈值)
|
||||||
|
- `release_params`:版本发布参数(`strategy_version`, `config_hash`)
|
||||||
|
|
||||||
|
## 8.2 ALT 权重训练流程
|
||||||
|
|
||||||
|
1. 用 `signal_feature_events + signal_label_events` 生成训练集。
|
||||||
|
2. 先做单变量稳定性审计(IC、分箱胜率、PSI)。
|
||||||
|
3. 再做有约束优化:
|
||||||
|
- 权重非负
|
||||||
|
- 总和固定为 100
|
||||||
|
- 单层权重变化设上限(如不超过上版的 30%)
|
||||||
|
4. 在 OOS 上评估,未达标不发布。
|
||||||
|
|
||||||
|
## 8.3 BTC 阈值训练流程
|
||||||
|
|
||||||
|
1. 针对每个门控特征做阈值网格搜索。
|
||||||
|
2. 以 OOS `net_r + drawdown` 共同评分。
|
||||||
|
3. 选择 Pareto 最优点,不追单一胜率最优。
|
||||||
|
|
||||||
|
## 8.4 参数更新节奏
|
||||||
|
|
||||||
|
- 建议频率:每 1-2 周滚动一次。
|
||||||
|
- 每次仅允许小步更新,避免参数跳变。
|
||||||
|
- 每次更新必须附带变更记录:`old -> new`、样本窗口、验证结果。
|
||||||
|
|
||||||
|
## 9. 发布与回滚机制
|
||||||
|
|
||||||
|
- 每次策略升级必须生成新 `strategy_version` 和 `config_hash`。
|
||||||
|
- 发版前必须附带:训练窗结果 + 验证窗(OOS)结果 + 执行成本变化。
|
||||||
|
- 任一核心指标触发阈值告警(如净R断崖、回撤超限)立即回滚到上一稳定版本。
|
||||||
|
|
||||||
|
## 10. 版本验收标准(V5.3)
|
||||||
|
|
||||||
|
- ALT 轨:样本外连续两个窗口净R为正。
|
||||||
|
- BTC 轨:样本外净R非负,且胜率不低于随机基线。
|
||||||
|
- 执行层:`maker_ratio >= 40%`,且 `avg_friction_cost_r`(滑点+手续费)较 V5.1 基线下降 >= 30%。
|
||||||
|
- 稳定性:最大回撤不显著劣化。
|
||||||
|
|
||||||
|
## 11. 里程碑
|
||||||
|
|
||||||
|
- M1:完成三张新表与事件落库。
|
||||||
|
- M2:完成 ALT/BTC 路由与首版决策逻辑。
|
||||||
|
- M3:完成执行成本改造(maker/BE/flip)。
|
||||||
|
- M4:跑通首轮 Walk-Forward 并产出 V5.3 首次评估报告。
|
||||||
108
docs/arbitrage-engine/v53-implementation-checklist.md
Normal file
108
docs/arbitrage-engine/v53-implementation-checklist.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: V5.3 实施清单
|
||||||
|
status: draft
|
||||||
|
updated: 2026-03-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# V5.3 实施清单
|
||||||
|
|
||||||
|
## Phase 0 - 数据与追溯基建(P0)
|
||||||
|
|
||||||
|
- [ ] 新增 `signal_feature_events` 表(含索引)
|
||||||
|
- [ ] 新增 `signal_label_events` 表(含索引)
|
||||||
|
- [ ] 新增 `execution_cost_events` 表(含索引)
|
||||||
|
- [ ] `signal_feature_events` 增加 `atr_value` 字段(信号触发时 ATR 绝对值快照)
|
||||||
|
- [ ] 在信号评估循环中落库 feature snapshot(每次评估都写)
|
||||||
|
- [ ] 打通统一追溯字段:`strategy_version/config_hash/engine_instance`
|
||||||
|
- [ ] 标签回填任务上线(15m/30m/60m)
|
||||||
|
- [ ] 标签回填强制使用 Mark Price,并按时间顺序判定先触发条件
|
||||||
|
- [ ] 标签计算强制使用快照 `atr_value`(禁止回填时重算 ATR)
|
||||||
|
|
||||||
|
## Phase 1 - 决策引擎重构(P0)
|
||||||
|
|
||||||
|
- [ ] 实现 ALT/BTC 路由器(按 symbol 分流)
|
||||||
|
- [ ] ALT 轨移除独立 confirmation 层,改为四层结构
|
||||||
|
- [ ] ALT 轨实现权重分配:`55/25/15/5`
|
||||||
|
- [ ] ALT 轨实现阈值:`open=75`, `flip=85`
|
||||||
|
- [ ] BTC 轨首版特征:`tiered_cvd_whale`, `obi_depth_10`, `spot_perp_divergence`, `atr_percent_1h`
|
||||||
|
- [ ] BTC 轨门控逻辑上线(含 veto 条件)
|
||||||
|
- [ ] BTC 轨缺失特征默认 `BLOCK_SIGNAL`,并写入 `block_reason`
|
||||||
|
- [ ] BTC 轨阈值配置化:`min_vol_threshold`, `obi_veto_threshold`, `whale_flow_threshold`
|
||||||
|
|
||||||
|
## Phase 2 - 执行层与摩擦成本优化(P0)
|
||||||
|
|
||||||
|
- [ ] 执行层支持 TP 预挂单(maker 优先)
|
||||||
|
- [ ] 增加 TP 未成交兜底:越价+超时后撤 maker 改 taker 强平
|
||||||
|
- [ ] 增加“部分成交”分支:仅对剩余仓位执行兜底
|
||||||
|
- [ ] Break-Even 改为费用感知(含手续费+滑点缓冲)
|
||||||
|
- [ ] `Cancel -> Market` 增加状态锁与幂等键
|
||||||
|
- [ ] 新增执行成本统计任务(fee/slippage/maker_ratio/friction_cost_r)
|
||||||
|
|
||||||
|
## Phase 3 - 评估与发布闸门(P1)
|
||||||
|
|
||||||
|
- [ ] 新增按 `config_hash` 分组报表接口
|
||||||
|
- [ ] 新增按 `track` 分组报表接口(ALT/BTC 分开看)
|
||||||
|
- [ ] 建立 Walk-Forward 评估脚本(训练窗+验证窗)
|
||||||
|
- [ ] 产出首版 V5.3 OOS 报告模板
|
||||||
|
- [ ] 新增参数变更记录模板(`old -> new` + 样本窗口 + OOS结果)
|
||||||
|
|
||||||
|
## Phase 4 - 持续优化(P2)
|
||||||
|
|
||||||
|
- [ ] 特征 shadow 机制(新因子先记录不参与决策)
|
||||||
|
- [ ] 自动化回滚钩子(核心KPI超阈值触发)
|
||||||
|
- [ ] 分层 CVD 桶参数自动校准
|
||||||
|
- [ ] OBI 深度档位自适应(5档/10档切换)
|
||||||
|
- [ ] 评估 XGBoost/LightGBM 离线实验管道
|
||||||
|
|
||||||
|
## 数据库建议(草案)
|
||||||
|
|
||||||
|
## `signal_feature_events`
|
||||||
|
|
||||||
|
- 主键:`event_id`
|
||||||
|
- 必要索引:
|
||||||
|
- `(ts)`
|
||||||
|
- `(symbol, ts DESC)`
|
||||||
|
- `(track, ts DESC)`
|
||||||
|
- `(strategy_version, config_hash, ts DESC)`
|
||||||
|
|
||||||
|
## `signal_label_events`
|
||||||
|
|
||||||
|
- 主键:`event_id`
|
||||||
|
- 必要索引:
|
||||||
|
- `(y_binary_60m, ts)`
|
||||||
|
- `(symbol, ts DESC)`
|
||||||
|
|
||||||
|
## `execution_cost_events`
|
||||||
|
|
||||||
|
- 主键:`trade_id`
|
||||||
|
- 必要索引:
|
||||||
|
- `(ts)`
|
||||||
|
- `(symbol, ts DESC)`
|
||||||
|
- `(entry_type, exit_type, ts DESC)`
|
||||||
|
|
||||||
|
## 上线前验证清单
|
||||||
|
|
||||||
|
- [ ] feature 事件写入无丢失,延迟可接受
|
||||||
|
- [ ] label 回填任务无时间错位
|
||||||
|
- [ ] ALT/BTC 路由正确(BTC 不落入 ALT)
|
||||||
|
- [ ] BTC 缺失特征不会静默放行
|
||||||
|
- [ ] maker 优先在真实成交中可观测
|
||||||
|
- [ ] TP 兜底分支含部分成交路径可复现
|
||||||
|
- [ ] BE 逻辑覆盖成本后,不再出现“保本但净亏”异常
|
||||||
|
- [ ] flip 频次和 flip 损耗下降
|
||||||
|
- [ ] OOS 报告通过预设阈值
|
||||||
|
|
||||||
|
## 发布闸门(量化指标)
|
||||||
|
|
||||||
|
- [ ] ALT:连续两个 OOS 窗口净R > 0
|
||||||
|
- [ ] BTC:OOS 净R >= 0 且胜率 >= 随机基线
|
||||||
|
- [ ] `maker_ratio >= 40%`
|
||||||
|
- [ ] `avg_friction_cost_r`(滑点+手续费)较 V5.1 下降 >= 30%
|
||||||
|
- [ ] 最大回撤不高于风险红线
|
||||||
|
|
||||||
|
## 任务分配建议
|
||||||
|
|
||||||
|
- 后端核心:`signal_engine.py`, `paper_monitor.py`, `main.py`, `db.py`
|
||||||
|
- 数据任务:新增 migration + 回填 job
|
||||||
|
- 评估任务:新增 `scripts/train_eval_walkforward.py`
|
||||||
|
- 文档任务:每次发版补充 `strategy_version` 变更记录
|
||||||
265
docs/arbitrage-engine/v54-requirements.md
Normal file
265
docs/arbitrage-engine/v54-requirements.md
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
# V5.4 Strategy Factory 需求文档
|
||||||
|
|
||||||
|
**版本**:v1.0
|
||||||
|
**日期**:2026-03-11
|
||||||
|
**作者**:露露
|
||||||
|
**状态**:待范总 + 小范 Review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
当前系统(V5.3)使用单体 `signal_engine.py`,所有策略逻辑耦合在一起,存在以下问题:
|
||||||
|
|
||||||
|
- 修改任意策略参数需重启整个引擎,中断数据采集
|
||||||
|
- 不同策略无法独立运行和对比,A/B 测试成本高
|
||||||
|
- 参数配置分散在 JSON 文件中,无法通过前端界面管理
|
||||||
|
- 无法按币种独立优化权重
|
||||||
|
|
||||||
|
V5.4 目标:构建 **Strategy Factory(策略工厂)**,将信号引擎解耦为数据总线 + 独立策略 Worker,支持前端可视化管理策略生命周期和参数配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心架构
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Signal Engine(数据总线)
|
||||||
|
├── 采集原始数据(aggTrades / OBI / 清算 / 市场数据)
|
||||||
|
├── 计算基础 Feature(CVD / ATR / VWAP / whale flow / OBI / spot-perp div)
|
||||||
|
└── 广播 feature_event(每15秒一次)
|
||||||
|
|
||||||
|
Strategy Workers(策略工厂)
|
||||||
|
├── Worker-1:我的BTC策略01(BTCUSDT,asyncio协程)
|
||||||
|
├── Worker-2:我的ETH策略01(ETHUSDT,asyncio协程)
|
||||||
|
├── Worker-N:...
|
||||||
|
└── 每个 Worker 订阅 feature_event,独立打分、开仓、管仓
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 关键设计原则
|
||||||
|
|
||||||
|
- **同一进程 + asyncio 协程**:所有 Worker 共享同一 Python 进程,feature_event 内存传递,省资源
|
||||||
|
- **独立资金池**:每个 Worker 有独立的 paper trading 余额,互不影响
|
||||||
|
- **15秒内热生效**:前端修改参数后,Worker 在下一个评估周期(≤15秒)自动从 DB 读取新参数
|
||||||
|
- **配置存 DB**:所有策略配置存入数据库,JSON 文件废弃
|
||||||
|
- **直接切换**:V5.4 上线后直接替换 V5.3 单体,不并行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 策略生命周期
|
||||||
|
|
||||||
|
### 3.1 状态定义
|
||||||
|
|
||||||
|
```
|
||||||
|
created(已创建)→ running(运行中)→ paused(已暂停)→ running
|
||||||
|
↓
|
||||||
|
deprecated(已废弃)→ running(重新启用)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **只有「废弃」,没有「删除」**
|
||||||
|
- 废弃的策略数据永久保留,可在废弃列表中检索
|
||||||
|
- 废弃策略可重新启用,继续使用原有余额和历史数据
|
||||||
|
|
||||||
|
### 3.2 策略标识
|
||||||
|
|
||||||
|
- 用户填写**显示名称**(自由命名,如"我的BTC激进策略")
|
||||||
|
- 后台自动生成**UUID**作为唯一标识,用户不感知
|
||||||
|
- `paper_trades` 等表通过 strategy_id(UUID)关联
|
||||||
|
|
||||||
|
### 3.3 余额管理
|
||||||
|
|
||||||
|
- 创建时设置初始资金(默认 10,000 USDT)
|
||||||
|
- 支持**追加余额**:追加后,`initial_balance` 同步增加,`current_balance` 同步增加
|
||||||
|
- 废弃后重新启用,继续使用废弃时的余额和历史数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 策略配置参数
|
||||||
|
|
||||||
|
每个策略实例(每个币种独立)包含以下可配置参数,均存入数据库,前端可编辑。
|
||||||
|
|
||||||
|
### 4.1 基础信息
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 范围 |
|
||||||
|
|------|------|------|--------|------|
|
||||||
|
| display_name | 策略显示名称 | string | — | 1-50字符 |
|
||||||
|
| symbol | 交易对 | enum | BTCUSDT | BTCUSDT / ETHUSDT / SOLUSDT / XRPUSDT |
|
||||||
|
| direction | 交易方向 | enum | both | long_only / short_only / both |
|
||||||
|
| initial_balance | 初始资金(USDT) | float | 10000 | 1000-1000000 |
|
||||||
|
|
||||||
|
### 4.2 CVD 窗口配置
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 可选项 |
|
||||||
|
|------|------|------|--------|--------|
|
||||||
|
| cvd_fast_window | 快线CVD窗口 | enum | 30m | 5m / 15m / 30m |
|
||||||
|
| cvd_slow_window | 慢线CVD窗口 | enum | 4h | 1h / 4h |
|
||||||
|
|
||||||
|
### 4.3 四层权重(合计必须 = 100)
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 | 范围 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| weight_direction | 方向得分权重 | 55 | 10-80 |
|
||||||
|
| weight_env | 环境得分权重 | 25 | 5-60 |
|
||||||
|
| weight_aux | 辅助因子权重 | 15 | 0-40 |
|
||||||
|
| weight_momentum | 动量权重 | 5 | 0-20 |
|
||||||
|
|
||||||
|
> 前端校验:四项之和必须 = 100,否则不允许保存
|
||||||
|
|
||||||
|
### 4.4 入场阈值
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 | 范围 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| entry_score | 入场最低总分 | 75 | 60-95 |
|
||||||
|
|
||||||
|
### 4.5 四道 Gate(过滤门)
|
||||||
|
|
||||||
|
每道 Gate 有独立开关和阈值:
|
||||||
|
|
||||||
|
| Gate | 说明 | 开关默认 | 阈值参数 | 默认值 | 范围 |
|
||||||
|
|------|------|----------|----------|--------|------|
|
||||||
|
| gate_obi | 订单簿失衡门 | ON | obi_threshold | 0.3 | 0.1-0.9 |
|
||||||
|
| gate_whale_cvd | 大单CVD门 | ON | whale_cvd_threshold | 0.0 | -1.0-1.0 |
|
||||||
|
| gate_vol_atr | 波动率ATR门 | ON | atr_percentile_min | 20 | 5-80 |
|
||||||
|
| gate_spot_perp | 现货/永续溢价门 | OFF | spot_perp_threshold | 0.002 | 0.0005-0.01 |
|
||||||
|
|
||||||
|
### 4.6 风控参数
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 | 范围 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| sl_atr_multiplier | SL宽度(×ATR) | 1.5 | 0.5-3.0 |
|
||||||
|
| tp1_ratio | TP1(×RD) | 0.75 | 0.3-2.0 |
|
||||||
|
| tp2_ratio | TP2(×RD) | 1.5 | 0.5-4.0 |
|
||||||
|
| timeout_minutes | 持仓超时(分钟) | 240 | 30-1440 |
|
||||||
|
| flip_threshold | 反转平仓阈值(分) | 80 | 60-95 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 前端功能需求
|
||||||
|
|
||||||
|
### 5.1 策略广场列表页(已有基础,新增以下)
|
||||||
|
|
||||||
|
- **右上角「+ 新增策略」按钮**:点击进入参数配置创建界面
|
||||||
|
- **每个卡片新增「调整参数」按钮**:点击进入参数配置编辑界面(预填当前参数)
|
||||||
|
- **每个卡片新增「废弃」按钮**:点击弹出二次确认,确认后策略进入废弃状态
|
||||||
|
- **每个卡片新增「追加余额」功能**:输入追加金额,确认后更新余额
|
||||||
|
|
||||||
|
### 5.2 参数配置界面(新建/编辑共用)
|
||||||
|
|
||||||
|
- 基础信息:名称、币种、交易方向、初始资金
|
||||||
|
- CVD窗口:快线/慢线各独立选择
|
||||||
|
- 四层权重:滑块或数字输入,实时显示合计,合计≠100时禁止保存
|
||||||
|
- 四道Gate:开关 + 阈值输入,各有说明文字和合理范围提示
|
||||||
|
- 风控参数:数字输入,有最小/最大值限制
|
||||||
|
- 底部:「保存并启动」(新建)/ 「保存」(编辑)按钮
|
||||||
|
|
||||||
|
### 5.3 策略详情页(已有基础,新增以下)
|
||||||
|
|
||||||
|
- 新增第三个 Tab:**「参数配置」**
|
||||||
|
- 展示当前所有参数,可点击编辑跳转到编辑界面
|
||||||
|
|
||||||
|
### 5.4 侧边栏新增入口
|
||||||
|
|
||||||
|
- **「废弃策略」**:点击进入废弃策略列表
|
||||||
|
- 展示格式与正常卡片相同,额外显示废弃时间
|
||||||
|
- 每个废弃策略有「重新启用」按钮
|
||||||
|
- 重新启用后恢复至运行中状态,继续原余额和数据
|
||||||
|
|
||||||
|
### 5.5 权限
|
||||||
|
|
||||||
|
- 所有页面需登录才能访问(复用现有 JWT)
|
||||||
|
- 登录后有全部操作权限,无角色区分
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 后端架构需求
|
||||||
|
|
||||||
|
### 6.1 数据库新增表
|
||||||
|
|
||||||
|
**`strategies` 表**(策略配置主表):
|
||||||
|
```sql
|
||||||
|
strategy_id UUID PRIMARY KEY
|
||||||
|
display_name TEXT NOT NULL
|
||||||
|
symbol TEXT NOT NULL
|
||||||
|
direction TEXT NOT NULL DEFAULT 'both'
|
||||||
|
status TEXT NOT NULL DEFAULT 'running' -- running/paused/deprecated
|
||||||
|
initial_balance FLOAT NOT NULL DEFAULT 10000
|
||||||
|
current_balance FLOAT NOT NULL DEFAULT 10000
|
||||||
|
cvd_fast_window TEXT NOT NULL DEFAULT '30m'
|
||||||
|
cvd_slow_window TEXT NOT NULL DEFAULT '4h'
|
||||||
|
weight_direction INT NOT NULL DEFAULT 55
|
||||||
|
weight_env INT NOT NULL DEFAULT 25
|
||||||
|
weight_aux INT NOT NULL DEFAULT 15
|
||||||
|
weight_momentum INT NOT NULL DEFAULT 5
|
||||||
|
entry_score INT NOT NULL DEFAULT 75
|
||||||
|
gate_obi_enabled BOOL NOT NULL DEFAULT TRUE
|
||||||
|
obi_threshold FLOAT NOT NULL DEFAULT 0.3
|
||||||
|
gate_whale_enabled BOOL NOT NULL DEFAULT TRUE
|
||||||
|
whale_cvd_threshold FLOAT NOT NULL DEFAULT 0.0
|
||||||
|
gate_vol_enabled BOOL NOT NULL DEFAULT TRUE
|
||||||
|
atr_percentile_min INT NOT NULL DEFAULT 20
|
||||||
|
gate_spot_perp_enabled BOOL NOT NULL DEFAULT FALSE
|
||||||
|
spot_perp_threshold FLOAT NOT NULL DEFAULT 0.002
|
||||||
|
sl_atr_multiplier FLOAT NOT NULL DEFAULT 1.5
|
||||||
|
tp1_ratio FLOAT NOT NULL DEFAULT 0.75
|
||||||
|
tp2_ratio FLOAT NOT NULL DEFAULT 1.5
|
||||||
|
timeout_minutes INT NOT NULL DEFAULT 240
|
||||||
|
flip_threshold INT NOT NULL DEFAULT 80
|
||||||
|
deprecated_at TIMESTAMP
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 现有表调整
|
||||||
|
|
||||||
|
- `paper_trades`:`strategy` 字段改为存 `strategy_id`(UUID),兼容现有数据(v53/v53_middle/v53_fast 保留字符串形式)
|
||||||
|
- `signal_indicators`:同上
|
||||||
|
|
||||||
|
### 6.3 API 新增端点
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/strategies 创建策略
|
||||||
|
GET /api/strategies 获取所有策略列表(含废弃)
|
||||||
|
GET /api/strategies/{id} 获取单个策略详情
|
||||||
|
PATCH /api/strategies/{id} 更新策略参数
|
||||||
|
POST /api/strategies/{id}/pause 暂停策略
|
||||||
|
POST /api/strategies/{id}/resume 恢复策略
|
||||||
|
POST /api/strategies/{id}/deprecate 废弃策略
|
||||||
|
POST /api/strategies/{id}/restore 重新启用
|
||||||
|
POST /api/strategies/{id}/add-balance 追加余额
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Signal Engine 改造
|
||||||
|
|
||||||
|
- Signal Engine 只负责 feature 计算和广播,不再包含评分/开仓逻辑
|
||||||
|
- 每个 Strategy Worker 作为 asyncio 协程,订阅 feature_event
|
||||||
|
- Worker 启动时从 DB 读取配置,每15秒评估时重新读取(捕获参数变更)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 迁移计划
|
||||||
|
|
||||||
|
V5.4 上线时:
|
||||||
|
1. 将现有 v53、v53_middle、v53_fast 三个策略迁移为 `strategies` 表中的三条记录
|
||||||
|
2. 历史 `paper_trades` 数据通过 strategy 名称映射到对应 strategy_id
|
||||||
|
3. 直接切换,不保留 V5.3 单体并行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 不在本期范围内
|
||||||
|
|
||||||
|
- 实盘交易(本期只做 paper trading)
|
||||||
|
- 多用户/多账户体系
|
||||||
|
- 策略算法类型选择(本期只支持四层评分算法)
|
||||||
|
- 自动化参数优化(Optuna 集成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Review 检查清单
|
||||||
|
|
||||||
|
- [ ] 范总确认需求无遗漏
|
||||||
|
- [ ] 小范审阅数据结构合理性
|
||||||
|
- [ ] 确认 `strategies` 表字段完整性
|
||||||
|
- [ ] 确认 API 端点覆盖所有前端操作
|
||||||
|
- [ ] 确认迁移方案不丢失历史数据
|
||||||
|
- [ ] 需求文档 Review 通过后,再开始写数据合约文档
|
||||||
@ -1,367 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { authFetch } from "@/lib/auth";
|
|
||||||
import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts";
|
|
||||||
|
|
||||||
function bjt(ms: number) {
|
|
||||||
const d = new Date(ms + 8 * 3600 * 1000);
|
|
||||||
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtPrice(p: number) {
|
|
||||||
return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function parseFactors(raw: any) {
|
|
||||||
if (!raw) return null;
|
|
||||||
if (typeof raw === "string") { try { return JSON.parse(raw); } catch { return null; } }
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
strategyId: string;
|
|
||||||
symbol: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterResult = "all" | "win" | "loss";
|
|
||||||
type FilterSymbol = "all" | string;
|
|
||||||
|
|
||||||
// ─── 控制面板(策略启停)─────────────────────────────────────────
|
|
||||||
|
|
||||||
function ControlPanel({ strategyId }: { strategyId: string }) {
|
|
||||||
const [status, setStatus] = useState<string | null>(null);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/strategies/${strategyId}`);
|
|
||||||
if (r.ok) { const j = await r.json(); setStatus(j.status); }
|
|
||||||
} catch {}
|
|
||||||
})();
|
|
||||||
}, [strategyId]);
|
|
||||||
|
|
||||||
const toggle = async () => {
|
|
||||||
setSaving(true);
|
|
||||||
const newStatus = status === "running" ? "paused" : "running";
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/strategies/${strategyId}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ status: newStatus }),
|
|
||||||
});
|
|
||||||
if (r.ok) setStatus(newStatus);
|
|
||||||
} catch {} finally { setSaving(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!status) return null;
|
|
||||||
return (
|
|
||||||
<div className={`rounded-xl border-2 ${status === "running" ? "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 ${status === "running" ? "bg-red-500 text-white hover:bg-red-600" : "bg-emerald-500 text-white hover:bg-emerald-600"}`}>
|
|
||||||
{saving ? "..." : status === "running" ? "⏹ 暂停" : "▶️ 启动"}
|
|
||||||
</button>
|
|
||||||
<span className={`text-xs font-medium ${status === "running" ? "text-emerald-700" : "text-slate-500"}`}>
|
|
||||||
{status === "running" ? "🟢 运行中" : "⚪ 已暂停"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 总览卡片 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function SummaryCards({ strategyId }: { strategyId: string }) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [data, setData] = useState<any>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const f = async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/paper/summary?strategy_id=${strategyId}`);
|
|
||||||
if (r.ok) setData(await r.json());
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
|
|
||||||
}, [strategyId]);
|
|
||||||
if (!data) return <div className="text-center text-slate-400 text-sm py-4">加载中...</div>;
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-3 lg:grid-cols-6 gap-1.5">
|
|
||||||
{[
|
|
||||||
{ label: "总盈亏(R)", value: `${data.total_pnl >= 0 ? "+" : ""}${data.total_pnl}R`, sub: `${data.total_pnl_usdt >= 0 ? "+" : ""}$${data.total_pnl_usdt}`, color: data.total_pnl >= 0 ? "text-emerald-600" : "text-red-500" },
|
|
||||||
{ label: "胜率", value: `${data.win_rate}%`, sub: `共${data.total_trades}笔`, color: "text-slate-800" },
|
|
||||||
{ label: "持仓中", value: data.active_positions, sub: "活跃仓位", color: "text-blue-600" },
|
|
||||||
{ label: "盈亏比", value: data.profit_factor, sub: "PF", color: "text-slate-800" },
|
|
||||||
{ label: "当前资金", value: `$${data.balance?.toLocaleString()}`, sub: "虚拟余额", color: data.balance >= 10000 ? "text-emerald-600" : "text-red-500" },
|
|
||||||
{ label: "状态", value: data.start_time ? "运行中 ✅" : "等待首笔", sub: "accumulating", color: "text-slate-600" },
|
|
||||||
].map(({ label, value, sub, color }) => (
|
|
||||||
<div key={label} className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">{label}</p>
|
|
||||||
<p className={`font-mono font-bold text-base ${color}`}>{value}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">{sub}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 当前持仓 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function ActivePositions({ strategyId }: { strategyId: string }) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [positions, setPositions] = useState<any[]>([]);
|
|
||||||
const [wsPrices, setWsPrices] = useState<Record<string, number>>({});
|
|
||||||
const RISK_USD = 200; // 1R = 200 USDT
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const f = async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/paper/positions?strategy_id=${strategyId}`);
|
|
||||||
if (r.ok) setPositions((await r.json()).data || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
|
|
||||||
}, [strategyId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const streams = ["btcusdt", "ethusdt", "xrpusdt", "solusdt"].map(s => `${s}@aggTrade`).join("/");
|
|
||||||
const ws = new WebSocket(`wss://fstream.binance.com/stream?streams=${streams}`);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.data) { const sym = msg.data.s; const price = parseFloat(msg.data.p); if (sym && price > 0) setWsPrices(prev => ({ ...prev, [sym]: price })); } } catch {} };
|
|
||||||
return () => ws.close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (positions.length === 0) return <div className="rounded-xl border border-slate-200 bg-white px-3 py-4 text-center text-slate-400 text-sm">暂无活跃持仓</div>;
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">当前持仓 <span className="text-[10px] text-emerald-500 font-normal">● 实时</span></h3></div>
|
|
||||||
<div className="divide-y divide-slate-100">
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{positions.map((p: any) => {
|
|
||||||
const sym = p.symbol?.replace("USDT", "") || "";
|
|
||||||
const holdMin = Math.round((Date.now() - p.entry_ts) / 60000);
|
|
||||||
const currentPrice = wsPrices[p.symbol] || p.current_price || 0;
|
|
||||||
const entry = p.entry_price || 0;
|
|
||||||
const riskDist = p.risk_distance || Math.abs(entry - (p.sl_price || entry)) || 1;
|
|
||||||
const tp1R = riskDist > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / riskDist : (entry - (p.tp1_price || 0)) / riskDist) : 0;
|
|
||||||
const fullR = riskDist > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / riskDist : (entry - currentPrice) / riskDist) : 0;
|
|
||||||
const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR;
|
|
||||||
const unrealUsdt = unrealR * RISK_USD;
|
|
||||||
return (
|
|
||||||
<div key={p.id} className="px-3 py-2 bg-emerald-50/60">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className={`text-xs font-bold ${p.direction === "LONG" ? "text-emerald-600" : "text-red-500"}`}>{p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction}</span>
|
|
||||||
<span className="text-[10px] text-slate-500">评分{p.score}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`font-mono text-sm font-bold ${unrealR >= 0 ? "text-emerald-600" : "text-red-500"}`}>{unrealR >= 0 ? "+" : ""}{unrealR.toFixed(2)}R</span>
|
|
||||||
<span className={`text-[10px] ${unrealUsdt >= 0 ? "text-emerald-500" : "text-red-400"}`}>${unrealUsdt.toFixed(0)}</span>
|
|
||||||
<span className="text-[10px] text-slate-400">{holdMin}m</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600 flex-wrap">
|
|
||||||
<span>入: ${fmtPrice(p.entry_price)}</span>
|
|
||||||
<span className="text-blue-600">现: ${currentPrice ? fmtPrice(currentPrice) : "-"}</span>
|
|
||||||
<span className="text-emerald-600">TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""}</span>
|
|
||||||
<span className="text-emerald-600">TP2: ${fmtPrice(p.tp2_price)}</span>
|
|
||||||
<span className="text-red-500">SL: ${fmtPrice(p.sl_price)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-[9px] text-slate-400">
|
|
||||||
入场: {p.entry_ts ? bjt(p.entry_ts) : "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 权益曲线 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function EquityCurve({ strategyId }: { strategyId: string }) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [data, setData] = useState<any[]>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const f = async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/paper/equity-curve?strategy_id=${strategyId}`);
|
|
||||||
if (r.ok) setData((await r.json()).data || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
|
|
||||||
}, [strategyId]);
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">权益曲线</h3></div>
|
|
||||||
{data.length < 2 ? <div className="px-3 py-6 text-center text-xs text-slate-400">数据积累中...</div> : (
|
|
||||||
<div className="p-2" style={{ height: 200 }}>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={data}>
|
|
||||||
<XAxis dataKey="ts" tickFormatter={(v) => bjt(Number(v))} tick={{ fontSize: 10 }} />
|
|
||||||
<YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => `${v}R`} />
|
|
||||||
<Tooltip labelFormatter={(v) => bjt(Number(v))} formatter={(v: unknown) => [`${v}R`, "累计PnL"]} />
|
|
||||||
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="3 3" />
|
|
||||||
<Area type="monotone" dataKey="pnl" stroke="#10b981" fill="#d1fae5" strokeWidth={2} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 历史交易 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function TradeHistory({ strategyId }: { strategyId: string }) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [trades, setTrades] = useState<any[]>([]);
|
|
||||||
const [filterResult, setFilterResult] = useState<FilterResult>("all");
|
|
||||||
const [filterSym, setFilterSym] = useState<FilterSymbol>("all");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const f = async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/paper/trades?strategy_id=${strategyId}&result=${filterResult}&symbol=${filterSym}&limit=50`);
|
|
||||||
if (r.ok) setTrades((await r.json()).data || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
|
|
||||||
}, [strategyId, filterResult, filterSym]);
|
|
||||||
|
|
||||||
const fmtTime = (ms: number) => ms ? bjt(ms) : "-";
|
|
||||||
const STATUS_LABEL: Record<string, string> = { tp: "止盈", sl: "止损", sl_be: "保本", timeout: "超时", signal_flip: "翻转" };
|
|
||||||
const STATUS_COLOR: Record<string, string> = { tp: "bg-emerald-100 text-emerald-700", sl: "bg-red-100 text-red-700", sl_be: "bg-amber-100 text-amber-700", signal_flip: "bg-purple-100 text-purple-700", timeout: "bg-slate-100 text-slate-600" };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">历史交易</h3>
|
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
|
||||||
{(["all", "BTC", "ETH", "XRP", "SOL"] as FilterSymbol[]).map(s => (
|
|
||||||
<button key={s} onClick={() => setFilterSym(s)} className={`px-2 py-0.5 rounded text-[10px] ${filterSym === s ? "bg-slate-800 text-white" : "text-slate-500 hover:bg-slate-100"}`}>{s === "all" ? "全部" : s}</button>
|
|
||||||
))}
|
|
||||||
<span className="text-slate-300">|</span>
|
|
||||||
{(["all", "win", "loss"] as FilterResult[]).map(r => (
|
|
||||||
<button key={r} onClick={() => setFilterResult(r)} className={`px-2 py-0.5 rounded text-[10px] ${filterResult === r ? "bg-slate-800 text-white" : "text-slate-500 hover:bg-slate-100"}`}>{r === "all" ? "全部" : r === "win" ? "盈利" : "亏损"}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-64 overflow-y-auto">
|
|
||||||
{trades.length === 0 ? <div className="text-center text-slate-400 text-sm py-6">暂无交易记录</div> : (
|
|
||||||
<table className="w-full text-[11px]">
|
|
||||||
<thead className="bg-slate-50 sticky top-0">
|
|
||||||
<tr className="text-slate-500">
|
|
||||||
<th className="px-2 py-1.5 text-left font-medium">币种</th>
|
|
||||||
<th className="px-2 py-1.5 text-left font-medium">方向</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">入场价</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">出场价</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">PnL(R)</th>
|
|
||||||
<th className="px-2 py-1.5 text-center font-medium">状态</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">分数</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">入场时间</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">出场时间</th>
|
|
||||||
<th className="px-2 py-1.5 text-right font-medium">持仓</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-50">
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{trades.map((t: any) => {
|
|
||||||
const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0;
|
|
||||||
return (
|
|
||||||
<tr key={t.id} className="hover:bg-slate-50">
|
|
||||||
<td className="px-2 py-1.5 font-mono">{t.symbol?.replace("USDT", "")}</td>
|
|
||||||
<td className={`px-2 py-1.5 font-bold ${t.direction === "LONG" ? "text-emerald-600" : "text-red-500"}`}>{t.direction === "LONG" ? "🟢" : "🔴"} {t.direction}</td>
|
|
||||||
<td className="px-2 py-1.5 text-right font-mono">{fmtPrice(t.entry_price)}</td>
|
|
||||||
<td className="px-2 py-1.5 text-right font-mono">{t.exit_price ? fmtPrice(t.exit_price) : "-"}</td>
|
|
||||||
<td className={`px-2 py-1.5 text-right font-mono font-bold ${t.pnl_r > 0 ? "text-emerald-600" : t.pnl_r < 0 ? "text-red-500" : "text-slate-500"}`}>{t.pnl_r > 0 ? "+" : ""}{t.pnl_r?.toFixed(2)}</td>
|
|
||||||
<td className="px-2 py-1.5 text-center"><span className={`px-1 py-0.5 rounded text-[9px] ${STATUS_COLOR[t.status] || "bg-slate-100 text-slate-600"}`}>{STATUS_LABEL[t.status] || t.status}</span></td>
|
|
||||||
<td className="px-2 py-1.5 text-right font-mono">{t.score}</td>
|
|
||||||
<td className="px-2 py-1.5 text-right text-slate-400 whitespace-nowrap">{fmtTime(t.entry_ts)}</td>
|
|
||||||
<td className="px-2 py-1.5 text-right text-slate-400 whitespace-nowrap">{fmtTime(t.exit_ts)}</td>
|
|
||||||
<td className="px-2 py-1.5 text-right text-slate-400">{holdMin}m</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 详细统计 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function StatsPanel({ strategyId }: { strategyId: string }) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [data, setData] = useState<any>(null);
|
|
||||||
const [tab, setTab] = useState("ALL");
|
|
||||||
useEffect(() => {
|
|
||||||
const f = async () => {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`/api/paper/stats?strategy_id=${strategyId}`);
|
|
||||||
if (r.ok) setData(await r.json());
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
|
|
||||||
}, [strategyId]);
|
|
||||||
if (!data || data.error) return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">详细统计</h3></div>
|
|
||||||
<div className="p-3 text-xs text-slate-400">等待交易记录积累...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const coinTabs = ["ALL", "BTC", "ETH", "XRP", "SOL"];
|
|
||||||
const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null);
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">详细统计</h3>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{coinTabs.map(t => (
|
|
||||||
<button key={t} onClick={() => setTab(t)} className={`px-2 py-0.5 rounded text-[10px] font-medium transition-colors ${tab === t ? "bg-slate-800 text-white" : "bg-slate-100 text-slate-500 hover:bg-slate-200"}`}>{t === "ALL" ? "总计" : t}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{st ? (
|
|
||||||
<div className="p-3">
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 text-xs">
|
|
||||||
<div><span className="text-slate-400">胜率</span><p className="font-mono font-bold">{st.win_rate}%</p></div>
|
|
||||||
<div><span className="text-slate-400">盈亏比</span><p className="font-mono font-bold">{st.win_loss_ratio}</p></div>
|
|
||||||
<div><span className="text-slate-400">平均盈利</span><p className="font-mono font-bold text-emerald-600">+{st.avg_win}R</p></div>
|
|
||||||
<div><span className="text-slate-400">平均亏损</span><p className="font-mono font-bold text-red-500">-{st.avg_loss}R</p></div>
|
|
||||||
<div><span className="text-slate-400">最大回撤</span><p className="font-mono font-bold">{st.mdd}R</p></div>
|
|
||||||
<div><span className="text-slate-400">夏普比率</span><p className="font-mono font-bold">{st.sharpe}</p></div>
|
|
||||||
<div><span className="text-slate-400">总盈亏</span><p className={`font-mono font-bold ${(st.total_pnl ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>{(st.total_pnl ?? 0) >= 0 ? "+" : ""}{st.total_pnl ?? "-"}R</p></div>
|
|
||||||
<div><span className="text-slate-400">总笔数</span><p className="font-mono font-bold">{st.total ?? data.total}</p></div>
|
|
||||||
<div><span className="text-slate-400">做多胜率</span><p className="font-mono">{st.long_win_rate}% ({st.long_count}笔)</p></div>
|
|
||||||
<div><span className="text-slate-400">做空胜率</span><p className="font-mono">{st.short_win_rate}% ({st.short_count}笔)</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : <div className="p-3 text-xs text-slate-400">该币种暂无数据</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 主组件 ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export default function PaperGeneric({ strategyId, symbol }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 p-1">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-bold text-slate-900">📈 模拟盘</h2>
|
|
||||||
<p className="text-[10px] text-slate-500">{symbol.replace("USDT", "")} · strategy_id: {strategyId.slice(0, 8)}...</p>
|
|
||||||
</div>
|
|
||||||
<ControlPanel strategyId={strategyId} />
|
|
||||||
<SummaryCards strategyId={strategyId} />
|
|
||||||
<ActivePositions strategyId={strategyId} />
|
|
||||||
<EquityCurve strategyId={strategyId} />
|
|
||||||
<TradeHistory strategyId={strategyId} />
|
|
||||||
<StatsPanel strategyId={strategyId} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,653 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { authFetch } from "@/lib/auth";
|
|
||||||
import {
|
|
||||||
ComposedChart, Area, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
|
|
||||||
ReferenceLine, CartesianGrid, Legend
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
interface IndicatorRow {
|
|
||||||
ts: number;
|
|
||||||
cvd_fast: number;
|
|
||||||
cvd_mid: number;
|
|
||||||
cvd_day: number;
|
|
||||||
atr_5m: number;
|
|
||||||
vwap_30m: number;
|
|
||||||
price: number;
|
|
||||||
score: number;
|
|
||||||
signal: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LatestIndicator {
|
|
||||||
ts: number;
|
|
||||||
cvd_fast: number;
|
|
||||||
cvd_mid: number;
|
|
||||||
cvd_day: number;
|
|
||||||
cvd_fast_slope: number;
|
|
||||||
atr_5m: number;
|
|
||||||
atr_percentile: number;
|
|
||||||
vwap_30m: number;
|
|
||||||
price: number;
|
|
||||||
p95_qty: number;
|
|
||||||
p99_qty: number;
|
|
||||||
score: number;
|
|
||||||
signal: string | null;
|
|
||||||
tier?: "light" | "standard" | "heavy" | null;
|
|
||||||
gate_passed?: boolean;
|
|
||||||
factors?: {
|
|
||||||
gate_passed?: boolean;
|
|
||||||
gate_block?: string;
|
|
||||||
block_reason?: string;
|
|
||||||
obi_raw?: number;
|
|
||||||
spot_perp_div?: number;
|
|
||||||
whale_cvd_ratio?: number;
|
|
||||||
atr_pct_price?: number;
|
|
||||||
direction?: { score?: number; max?: number };
|
|
||||||
crowding?: { score?: number; max?: number };
|
|
||||||
environment?: { score?: number; max?: number };
|
|
||||||
auxiliary?: { score?: number; max?: number };
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SignalRecord {
|
|
||||||
ts: number;
|
|
||||||
score: number;
|
|
||||||
signal: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AllSignalRow {
|
|
||||||
ts: number;
|
|
||||||
score: number;
|
|
||||||
signal: string | null;
|
|
||||||
price?: number;
|
|
||||||
// factors 结构与 LatestIndicator.factors 基本一致,兼容 string/json
|
|
||||||
factors?: LatestIndicator["factors"] | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Gates {
|
|
||||||
obi_threshold: number;
|
|
||||||
whale_usd_threshold: number;
|
|
||||||
whale_flow_pct: number;
|
|
||||||
vol_atr_pct_min: number;
|
|
||||||
spot_perp_threshold: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Weights {
|
|
||||||
direction: number;
|
|
||||||
env: number;
|
|
||||||
aux: number;
|
|
||||||
momentum: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
strategyId: string;
|
|
||||||
symbol: string;
|
|
||||||
cvdFastWindow: string;
|
|
||||||
cvdSlowWindow: string;
|
|
||||||
weights: Weights;
|
|
||||||
gates: Gates;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WINDOWS = [
|
|
||||||
{ label: "1h", value: 60 },
|
|
||||||
{ label: "4h", value: 240 },
|
|
||||||
{ label: "12h", value: 720 },
|
|
||||||
{ label: "24h", value: 1440 },
|
|
||||||
];
|
|
||||||
|
|
||||||
function bjtStr(ms: number) {
|
|
||||||
const d = new Date(ms + 8 * 3600 * 1000);
|
|
||||||
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bjtFull(ms: number) {
|
|
||||||
const d = new Date(ms + 8 * 3600 * 1000);
|
|
||||||
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(v: number, decimals = 1): string {
|
|
||||||
if (Math.abs(v) >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
|
|
||||||
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}K`;
|
|
||||||
return v.toFixed(decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) {
|
|
||||||
const ratio = Math.max(0, Math.min((score / max) * 100, 100));
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[10px] text-slate-500 w-6 shrink-0">{label}</span>
|
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-slate-100 overflow-hidden">
|
|
||||||
<div className={`h-full ${colorClass}`} style={{ width: `${ratio}%` }} />
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-mono text-slate-600 w-12 text-right">{score}/{max}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GateCard({ factors, gates }: { factors: LatestIndicator["factors"]; gates: Gates }) {
|
|
||||||
if (!factors) return null;
|
|
||||||
const passed = factors.gate_passed ?? true;
|
|
||||||
const blockReason = factors.gate_block || factors.block_reason;
|
|
||||||
return (
|
|
||||||
<div className={`rounded-xl border px-3 py-2 mt-2 ${passed ? "border-purple-200 bg-purple-50" : "border-red-200 bg-red-50"}`}>
|
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-purple-800">🔒 Gate-Control</p>
|
|
||||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded ${passed ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-600"}`}>
|
|
||||||
{passed ? "✅ Gate通过" : "❌ 否决"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">波动率</p>
|
|
||||||
<p className="text-xs font-mono text-slate-800">{((factors.atr_pct_price ?? 0) * 100).toFixed(3)}%</p>
|
|
||||||
<p className="text-[9px] text-slate-400">需 ≥{(gates.vol_atr_pct_min * 100).toFixed(2)}%</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">OBI</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.obi_raw ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.obi_raw ?? 0) * 100).toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">否决±{gates.obi_threshold}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">期现背离</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.spot_perp_div ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.spot_perp_div ?? 0) * 10000).toFixed(2)}bps
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">否决±{(gates.spot_perp_threshold * 100).toFixed(1)}%</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">鲸鱼</p>
|
|
||||||
<p className="text-xs font-mono text-slate-800">${(gates.whale_usd_threshold / 1000).toFixed(0)}k</p>
|
|
||||||
<p className="text-[9px] text-slate-400">{">"}{(gates.whale_flow_pct * 100).toFixed(0)}%占比</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{blockReason && (
|
|
||||||
<p className="text-[10px] text-red-600 mt-1.5 bg-red-50 rounded px-2 py-1">
|
|
||||||
否决原因: <span className="font-mono">{blockReason}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndicatorCards({ sym, strategyName, cvdFastWindow, cvdSlowWindow, weights, gates }: {
|
|
||||||
sym: string; strategyName: string; cvdFastWindow: string; cvdSlowWindow: string; weights: Weights; gates: Gates;
|
|
||||||
}) {
|
|
||||||
const [data, setData] = useState<LatestIndicator | null>(null);
|
|
||||||
const coin = sym.replace("USDT", "") as "BTC" | "ETH" | "XRP" | "SOL";
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetch_ = async () => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/latest?strategy=${strategyName}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json[coin] || null);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
fetch_();
|
|
||||||
const iv = setInterval(fetch_, 5000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [coin, strategyName]);
|
|
||||||
|
|
||||||
if (!data) return <div className="text-center text-slate-400 text-sm py-4">等待指标数据...</div>;
|
|
||||||
|
|
||||||
const priceVsVwap = data.price > data.vwap_30m ? "上方" : "下方";
|
|
||||||
const totalWeight = weights.direction + weights.env + weights.aux + weights.momentum;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* CVD双轨 */}
|
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD_fast ({cvdFastWindow})</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_fast >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{fmt(data.cvd_fast)}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">
|
|
||||||
斜率: <span className={data.cvd_fast_slope >= 0 ? "text-emerald-600" : "text-red-500"}>
|
|
||||||
{data.cvd_fast_slope >= 0 ? "↑" : "↓"}{fmt(Math.abs(data.cvd_fast_slope))}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD_slow ({cvdSlowWindow})</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_mid >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{fmt(data.cvd_mid)}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">{data.cvd_mid > 0 ? "多" : "空"}头占优</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD共振</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_fast >= 0 && data.cvd_mid >= 0 ? "text-emerald-600" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "text-red-500" : "text-slate-400"}`}>
|
|
||||||
{data.cvd_fast >= 0 && data.cvd_mid >= 0 ? "✅ 多头共振" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "✅ 空头共振" : "⚠️ 分歧"}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">双周期共振</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ATR + VWAP + P95/P99 */}
|
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">ATR</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">${fmt(data.atr_5m, 2)}</p>
|
|
||||||
<p className="text-[10px]">
|
|
||||||
<span className={data.atr_percentile > 60 ? "text-amber-600 font-semibold" : "text-slate-400"}>
|
|
||||||
{data.atr_percentile.toFixed(0)}%{data.atr_percentile > 60 ? "🔥" : ""}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">VWAP</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">${data.vwap_30m.toLocaleString("en-US", { maximumFractionDigits: 1 })}</p>
|
|
||||||
<p className="text-[10px]">价格在<span className={data.price > data.vwap_30m ? "text-emerald-600" : "text-red-500"}>{priceVsVwap}</span></p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">P95</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">{data.p95_qty?.toFixed(4) ?? "-"}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">大单阈值</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">P99</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-amber-600">{data.p99_qty?.toFixed(4) ?? "-"}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">超大单</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 信号状态 + 四层分 */}
|
|
||||||
<div className={`rounded-xl border px-3 py-2.5 ${
|
|
||||||
data.signal === "LONG" ? "border-emerald-300 bg-emerald-50" :
|
|
||||||
data.signal === "SHORT" ? "border-red-300 bg-red-50" :
|
|
||||||
"border-slate-200 bg-slate-50"
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-[10px] text-slate-500">四层评分 · {coin}</p>
|
|
||||||
<p className={`font-bold text-base ${
|
|
||||||
data.signal === "LONG" ? "text-emerald-700" :
|
|
||||||
data.signal === "SHORT" ? "text-red-600" :
|
|
||||||
"text-slate-400"
|
|
||||||
}`}>
|
|
||||||
{data.signal === "LONG" ? "🟢 做多" : data.signal === "SHORT" ? "🔴 做空" : "⚪ 无信号"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="font-mono font-bold text-lg text-slate-800">{data.score}/{totalWeight}</p>
|
|
||||||
<p className="text-[10px] text-slate-500">
|
|
||||||
{data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : "不开仓"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
<LayerScore label="方向" score={data.factors?.direction?.score ?? 0} max={weights.direction} colorClass="bg-blue-600" />
|
|
||||||
<LayerScore label="环境" score={data.factors?.environment?.score ?? 0} max={weights.env} colorClass="bg-emerald-600" />
|
|
||||||
<LayerScore label="辅助" score={data.factors?.auxiliary?.score ?? 0} max={weights.aux} colorClass="bg-violet-600" />
|
|
||||||
<LayerScore label="动量" score={data.factors?.crowding?.score ?? 0} max={weights.momentum} colorClass="bg-slate-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gate 卡片 */}
|
|
||||||
<GateCard factors={data.factors} gates={gates} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignalHistory({ coin, strategyName }: { coin: string; strategyName: string }) {
|
|
||||||
const [data, setData] = useState<SignalRecord[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/signal-history?symbol=${coin}&limit=20&strategy=${strategyName}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json.data || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
const iv = setInterval(fetchData, 15000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [coin, strategyName]);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">最近信号</h3>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-slate-100 max-h-48 overflow-y-auto">
|
|
||||||
{data.map((s, i) => (
|
|
||||||
<div key={i} className="px-3 py-1.5 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-xs font-bold ${s.signal === "LONG" ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{s.signal === "LONG" ? "🟢 LONG" : "🔴 SHORT"}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-slate-400">{bjtFull(s.ts)}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-mono text-xs text-slate-700">{s.score}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AllSignalsModal({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
symbol,
|
|
||||||
strategyName,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
symbol: string;
|
|
||||||
strategyName: string;
|
|
||||||
}) {
|
|
||||||
const [rows, setRows] = useState<AllSignalRow[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
const fetchAll = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const res = await authFetch(
|
|
||||||
`/api/signals/history?symbol=${symbol}&limit=200&strategy=${strategyName}`
|
|
||||||
);
|
|
||||||
if (!res.ok) {
|
|
||||||
setError(`加载失败 (${res.status})`);
|
|
||||||
setRows([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const json = await res.json();
|
|
||||||
setRows(json.items || []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setError("加载失败,请稍后重试");
|
|
||||||
setRows([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchAll();
|
|
||||||
}, [open, symbol, strategyName]);
|
|
||||||
|
|
||||||
const parseFactors = (r: AllSignalRow): LatestIndicator["factors"] | null => {
|
|
||||||
const f = r.factors;
|
|
||||||
if (!f) return null;
|
|
||||||
if (typeof f === "string") {
|
|
||||||
try {
|
|
||||||
return JSON.parse(f);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
||||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl max-h-[80vh] flex flex-col border border-slate-200">
|
|
||||||
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-slate-900">
|
|
||||||
所有历史信号(含未开仓)
|
|
||||||
</h3>
|
|
||||||
<p className="text-[11px] text-slate-500">
|
|
||||||
最近 200 条 · {symbol} · {strategyName}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-2 py-1 rounded-lg border border-slate-200 text-[11px] text-slate-600 hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{loading ? (
|
|
||||||
<div className="py-8 text-center text-slate-400 text-sm">
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="py-8 text-center text-red-500 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
) : rows.length === 0 ? (
|
|
||||||
<div className="py-8 text-center text-slate-400 text-sm">
|
|
||||||
暂无历史信号
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<table className="w-full text-[11px] text-left">
|
|
||||||
<thead className="bg-slate-50 border-b border-slate-200 sticky top-0">
|
|
||||||
<tr>
|
|
||||||
<th className="px-3 py-2 text-slate-500 font-medium">时间</th>
|
|
||||||
<th className="px-3 py-2 text-slate-500 font-medium">信号</th>
|
|
||||||
<th className="px-3 py-2 text-slate-500 font-medium">总分</th>
|
|
||||||
<th className="px-3 py-2 text-slate-500 font-medium">
|
|
||||||
四层评分
|
|
||||||
</th>
|
|
||||||
<th className="px-3 py-2 text-slate-500 font-medium">门控</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((r, idx) => {
|
|
||||||
const f = parseFactors(r);
|
|
||||||
const dirScore = f?.direction?.score ?? 0;
|
|
||||||
const envScore = f?.environment?.score ?? 0;
|
|
||||||
const auxScore = f?.auxiliary?.score ?? 0;
|
|
||||||
const momScore = f?.crowding?.score ?? 0;
|
|
||||||
const gateBlock =
|
|
||||||
(f?.gate_block as string | undefined) ||
|
|
||||||
(f?.block_reason as string | undefined) ||
|
|
||||||
"";
|
|
||||||
const gatePassed =
|
|
||||||
typeof f?.gate_passed === "boolean"
|
|
||||||
? f?.gate_passed
|
|
||||||
: !gateBlock;
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={`${r.ts}-${idx}`}
|
|
||||||
className="border-b border-slate-100 last:border-b-0 hover:bg-slate-50/60"
|
|
||||||
>
|
|
||||||
<td className="px-3 py-1.5 text-slate-500">
|
|
||||||
{bjtFull(r.ts)}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-1.5">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full border ${
|
|
||||||
r.signal === "LONG"
|
|
||||||
? "border-emerald-300 bg-emerald-50 text-emerald-600"
|
|
||||||
: r.signal === "SHORT"
|
|
||||||
? "border-red-300 bg-red-50 text-red-500"
|
|
||||||
: "border-slate-200 bg-slate-50 text-slate-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-[10px]">
|
|
||||||
{r.signal === "LONG"
|
|
||||||
? "多"
|
|
||||||
: r.signal === "SHORT"
|
|
||||||
? "空"
|
|
||||||
: "无"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-1.5 font-mono text-slate-800">
|
|
||||||
{r.score}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-1.5">
|
|
||||||
<div className="flex flex-wrap gap-1 text-[10px] text-slate-500">
|
|
||||||
<span>方:{dirScore}</span>
|
|
||||||
<span>环:{envScore}</span>
|
|
||||||
<span>辅:{auxScore}</span>
|
|
||||||
<span>动:{momScore}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-1.5">
|
|
||||||
{gatePassed ? (
|
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full bg-emerald-50 text-emerald-600 border border-emerald-200 text-[10px]">
|
|
||||||
通过
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full bg-red-50 text-red-500 border border-red-200 text-[10px]">
|
|
||||||
拒绝
|
|
||||||
{gateBlock ? ` · ${gateBlock}` : ""}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CVDChart({ sym, minutes, strategyName, cvdFastWindow, cvdSlowWindow }: {
|
|
||||||
sym: string; minutes: number; strategyName: string; cvdFastWindow: string; cvdSlowWindow: string;
|
|
||||||
}) {
|
|
||||||
const [data, setData] = useState<IndicatorRow[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const coin = sym.replace("USDT", "");
|
|
||||||
|
|
||||||
const fetchData = useCallback(async (silent = false) => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/indicators?symbol=${coin}&minutes=${minutes}&strategy=${strategyName}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json.data || []);
|
|
||||||
if (!silent) setLoading(false);
|
|
||||||
} catch {}
|
|
||||||
}, [coin, minutes, strategyName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
fetchData();
|
|
||||||
const iv = setInterval(() => fetchData(true), 30000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const chartData = data.map(d => ({
|
|
||||||
time: bjtStr(d.ts),
|
|
||||||
fast: parseFloat(d.cvd_fast?.toFixed(2) || "0"),
|
|
||||||
mid: parseFloat(d.cvd_mid?.toFixed(2) || "0"),
|
|
||||||
price: d.price,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const prices = chartData.map(d => d.price).filter(v => v > 0);
|
|
||||||
const pMin = prices.length ? Math.min(...prices) : 0;
|
|
||||||
const pMax = prices.length ? Math.max(...prices) : 0;
|
|
||||||
const pPad = (pMax - pMin) * 0.3 || pMax * 0.001;
|
|
||||||
|
|
||||||
if (loading) return <div className="flex items-center justify-center h-48 text-slate-400 text-sm">加载指标数据...</div>;
|
|
||||||
if (data.length === 0) return <div className="flex items-center justify-center h-48 text-slate-400 text-sm">暂无指标数据,等待积累...</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
|
||||||
<ComposedChart data={chartData} margin={{ top: 4, right: 60, bottom: 0, left: 8 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
|
||||||
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
|
||||||
<YAxis yAxisId="cvd" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} axisLine={false} width={55} />
|
|
||||||
<YAxis yAxisId="price" orientation="right" tick={{ fill: "#f59e0b", fontSize: 10 }} tickLine={false} axisLine={false} width={65}
|
|
||||||
domain={[Math.floor(pMin - pPad), Math.ceil(pMax + pPad)]}
|
|
||||||
tickFormatter={(v: number) => v >= 1000 ? `$${(v / 1000).toFixed(1)}k` : `$${v.toFixed(0)}`}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
formatter={(v: any, name: any) => {
|
|
||||||
if (name === "price") return [`$${Number(v).toLocaleString()}`, "币价"];
|
|
||||||
if (name === "fast") return [fmt(Number(v)), `CVD_fast(${cvdFastWindow})`];
|
|
||||||
return [fmt(Number(v)), `CVD_slow(${cvdSlowWindow})`];
|
|
||||||
}}
|
|
||||||
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }}
|
|
||||||
/>
|
|
||||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
|
||||||
<ReferenceLine yAxisId="cvd" y={0} stroke="#94a3b8" strokeDasharray="4 2" />
|
|
||||||
<Area yAxisId="cvd" type="monotone" dataKey="fast" name="fast" stroke="#2563eb" fill="#eff6ff" strokeWidth={1.5} dot={false} connectNulls />
|
|
||||||
<Line yAxisId="cvd" type="monotone" dataKey="mid" name="mid" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="6 3" />
|
|
||||||
<Line yAxisId="price" type="monotone" dataKey="price" name="price" stroke="#f59e0b" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="4 2" />
|
|
||||||
</ComposedChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SignalsGeneric({ strategyId, symbol, cvdFastWindow, cvdSlowWindow, weights, gates }: Props) {
|
|
||||||
const [minutes, setMinutes] = useState(240);
|
|
||||||
const coin = symbol.replace("USDT", "");
|
|
||||||
const strategyName = `custom_${strategyId.slice(0, 8)}`;
|
|
||||||
const [showAllSignals, setShowAllSignals] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 p-1">
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-bold text-slate-900">⚡ 信号引擎</h2>
|
|
||||||
<p className="text-slate-500 text-[10px]">
|
|
||||||
CVD {cvdFastWindow}/{cvdSlowWindow} · 权重 {weights.direction}/{weights.env}/{weights.aux}/{weights.momentum} · {coin}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAllSignals(true)}
|
|
||||||
className="px-2 py-0.5 rounded-lg border border-slate-200 text-[10px] text-slate-600 hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
所有信号
|
|
||||||
</button>
|
|
||||||
<span className="px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-100 text-blue-700 border border-blue-200">
|
|
||||||
{coin}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IndicatorCards
|
|
||||||
sym={symbol}
|
|
||||||
strategyName={strategyName}
|
|
||||||
cvdFastWindow={cvdFastWindow}
|
|
||||||
cvdSlowWindow={cvdSlowWindow}
|
|
||||||
weights={weights}
|
|
||||||
gates={gates}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SignalHistory coin={coin} strategyName={strategyName} />
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">CVD双轨 + 币价</h3>
|
|
||||||
<p className="text-[10px] text-slate-400">蓝=fast({cvdFastWindow}) · 紫=slow({cvdSlowWindow}) · 橙=价格</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{WINDOWS.map(w => (
|
|
||||||
<button key={w.value} onClick={() => setMinutes(w.value)}
|
|
||||||
className={`px-2 py-1 rounded border text-xs transition-colors ${minutes === w.value ? "bg-slate-800 text-white border-slate-800" : "border-slate-200 text-slate-500 hover:border-slate-400"}`}>
|
|
||||||
{w.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<CVDChart sym={symbol} minutes={minutes} strategyName={strategyName} cvdFastWindow={cvdFastWindow} cvdSlowWindow={cvdSlowWindow} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AllSignalsModal
|
|
||||||
open={showAllSignals}
|
|
||||||
onClose={() => setShowAllSignals(false)}
|
|
||||||
symbol={symbol}
|
|
||||||
strategyName={strategyName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useAuth, authFetch } from "@/lib/auth";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import StrategyForm, { StrategyFormData } from "@/components/StrategyForm";
|
|
||||||
|
|
||||||
export default function EditStrategyPage() {
|
|
||||||
useAuth();
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const sid = params?.id as string;
|
|
||||||
const [formData, setFormData] = useState<StrategyFormData | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sid) return;
|
|
||||||
authFetch(`/api/strategies/${sid}`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
|
||||||
const s = d.strategy;
|
|
||||||
if (!s) throw new Error("策略不存在");
|
|
||||||
setFormData({
|
|
||||||
display_name: s.display_name,
|
|
||||||
symbol: s.symbol,
|
|
||||||
direction: s.direction,
|
|
||||||
initial_balance: s.initial_balance,
|
|
||||||
cvd_fast_window: s.cvd_fast_window,
|
|
||||||
cvd_slow_window: s.cvd_slow_window,
|
|
||||||
weight_direction: s.weight_direction,
|
|
||||||
weight_env: s.weight_env,
|
|
||||||
weight_aux: s.weight_aux,
|
|
||||||
weight_momentum: s.weight_momentum,
|
|
||||||
entry_score: s.entry_score,
|
|
||||||
// 门1 波动率
|
|
||||||
gate_vol_enabled: s.gate_vol_enabled,
|
|
||||||
vol_atr_pct_min: s.vol_atr_pct_min,
|
|
||||||
// 门2 CVD共振
|
|
||||||
gate_cvd_enabled: s.gate_cvd_enabled ?? true,
|
|
||||||
// 门3 鲸鱼否决
|
|
||||||
gate_whale_enabled: s.gate_whale_enabled,
|
|
||||||
whale_usd_threshold: s.whale_usd_threshold,
|
|
||||||
whale_flow_pct: s.whale_flow_pct,
|
|
||||||
// 门4 OBI否决
|
|
||||||
gate_obi_enabled: s.gate_obi_enabled,
|
|
||||||
obi_threshold: s.obi_threshold,
|
|
||||||
// 门5 期现背离
|
|
||||||
gate_spot_perp_enabled: s.gate_spot_perp_enabled,
|
|
||||||
spot_perp_threshold: s.spot_perp_threshold,
|
|
||||||
sl_atr_multiplier: s.sl_atr_multiplier,
|
|
||||||
tp1_ratio: s.tp1_ratio,
|
|
||||||
tp2_ratio: s.tp2_ratio,
|
|
||||||
timeout_minutes: s.timeout_minutes,
|
|
||||||
flip_threshold: s.flip_threshold,
|
|
||||||
description: s.description || "",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => setError(e.message))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [sid]);
|
|
||||||
|
|
||||||
if (loading) return <div className="p-8 text-slate-400 text-sm animate-pulse">加载中...</div>;
|
|
||||||
if (error) return <div className="p-8 text-red-500 text-sm">{error}</div>;
|
|
||||||
if (!formData) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 max-w-2xl mx-auto">
|
|
||||||
<div className="mb-5">
|
|
||||||
<h1 className="text-lg font-bold text-slate-800">调整策略参数</h1>
|
|
||||||
<p className="text-slate-500 text-xs mt-0.5">修改保存后15秒内生效,不影响当前持仓</p>
|
|
||||||
</div>
|
|
||||||
<StrategyForm
|
|
||||||
mode="edit"
|
|
||||||
initialData={formData}
|
|
||||||
strategyId={sid}
|
|
||||||
isBalanceEditable={false}
|
|
||||||
onSuccess={() => router.push(`/strategy-plaza/${sid}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
PauseCircle,
|
PauseCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Clock,
|
Clock,
|
||||||
Settings,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
// ─── Dynamic imports for each strategy's pages ───────────────────
|
// ─── Dynamic imports for each strategy's pages ───────────────────
|
||||||
@ -21,20 +20,10 @@ const SignalsV53Middle = dynamic(() => import("@/app/signals-v53middle/page"), {
|
|||||||
const PaperV53 = dynamic(() => import("@/app/paper-v53/page"), { ssr: false });
|
const PaperV53 = dynamic(() => import("@/app/paper-v53/page"), { ssr: false });
|
||||||
const PaperV53Fast = dynamic(() => import("@/app/paper-v53fast/page"), { ssr: false });
|
const PaperV53Fast = dynamic(() => import("@/app/paper-v53fast/page"), { ssr: false });
|
||||||
const PaperV53Middle = dynamic(() => import("@/app/paper-v53middle/page"), { ssr: false });
|
const PaperV53Middle = dynamic(() => import("@/app/paper-v53middle/page"), { ssr: false });
|
||||||
const SignalsGeneric = dynamic(() => import("./SignalsGeneric"), { ssr: false });
|
|
||||||
const PaperGeneric = dynamic(() => import("./PaperGeneric"), { ssr: false });
|
|
||||||
|
|
||||||
// ─── UUID → legacy strategy name map ─────────────────────────────
|
|
||||||
const UUID_TO_LEGACY: Record<string, string> = {
|
|
||||||
"00000000-0000-0000-0000-000000000053": "v53",
|
|
||||||
"00000000-0000-0000-0000-000000000054": "v53_middle",
|
|
||||||
"00000000-0000-0000-0000-000000000055": "v53_fast",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────
|
||||||
interface StrategySummary {
|
interface StrategySummary {
|
||||||
strategy_id?: string;
|
id: string;
|
||||||
id?: string;
|
|
||||||
display_name: string;
|
display_name: string;
|
||||||
status: string;
|
status: string;
|
||||||
started_at: number;
|
started_at: number;
|
||||||
@ -50,42 +39,7 @@ interface StrategySummary {
|
|||||||
pnl_usdt_24h: number;
|
pnl_usdt_24h: number;
|
||||||
pnl_r_24h: number;
|
pnl_r_24h: number;
|
||||||
cvd_windows?: string;
|
cvd_windows?: string;
|
||||||
cvd_fast_window?: string;
|
|
||||||
cvd_slow_window?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
symbol?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StrategyDetail {
|
|
||||||
weight_direction: number;
|
|
||||||
weight_env: number;
|
|
||||||
weight_aux: number;
|
|
||||||
weight_momentum: number;
|
|
||||||
entry_score: number;
|
|
||||||
// 门1 波动率
|
|
||||||
gate_vol_enabled: boolean;
|
|
||||||
vol_atr_pct_min: number;
|
|
||||||
// 门2 CVD共振
|
|
||||||
gate_cvd_enabled: boolean;
|
|
||||||
// 门3 鲸鱼否决
|
|
||||||
gate_whale_enabled: boolean;
|
|
||||||
whale_usd_threshold: number;
|
|
||||||
whale_flow_pct: number;
|
|
||||||
// 门4 OBI否决
|
|
||||||
gate_obi_enabled: boolean;
|
|
||||||
obi_threshold: number;
|
|
||||||
// 门5 期现背离
|
|
||||||
gate_spot_perp_enabled: boolean;
|
|
||||||
spot_perp_threshold: number;
|
|
||||||
sl_atr_multiplier: number;
|
|
||||||
tp1_ratio: number;
|
|
||||||
tp2_ratio: number;
|
|
||||||
timeout_minutes: number;
|
|
||||||
flip_threshold: number;
|
|
||||||
symbol: string;
|
|
||||||
direction: string;
|
|
||||||
cvd_fast_window: string;
|
|
||||||
cvd_slow_window: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────
|
||||||
@ -100,127 +54,24 @@ function fmtDur(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: string }) {
|
function StatusBadge({ status }: { status: string }) {
|
||||||
if (status === "running") return <span className="flex items-center gap-1 text-xs text-emerald-600 font-medium"><CheckCircle size={12} />运行中</span>;
|
if (status === "running") return <span className="flex items-center gap-1 text-xs text-emerald-400"><CheckCircle size={12} />运行中</span>;
|
||||||
if (status === "paused") return <span className="flex items-center gap-1 text-xs text-yellow-600 font-medium"><PauseCircle size={12} />已暂停</span>;
|
if (status === "paused") return <span className="flex items-center gap-1 text-xs text-yellow-400"><PauseCircle size={12} />已暂停</span>;
|
||||||
return <span className="flex items-center gap-1 text-xs text-red-500 font-medium"><AlertCircle size={12} />异常</span>;
|
return <span className="flex items-center gap-1 text-xs text-red-400"><AlertCircle size={12} />异常</span>;
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Config Tab ───────────────────────────────────────────────────
|
|
||||||
function ConfigTab({ detail, strategyId }: { detail: StrategyDetail; strategyId: string }) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const row = (label: string, value: string | number | boolean) => (
|
|
||||||
<div className="flex items-center justify-between py-2 border-b border-slate-100 last:border-0">
|
|
||||||
<span className="text-xs text-slate-500">{label}</span>
|
|
||||||
<span className="text-xs font-medium text-slate-800">{String(value)}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const gateRow = (label: string, enabled: boolean, threshold: string) => (
|
|
||||||
<div className="flex items-center justify-between py-2 border-b border-slate-100 last:border-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`inline-block w-2 h-2 rounded-full ${enabled ? "bg-emerald-400" : "bg-slate-300"}`} />
|
|
||||||
<span className="text-xs text-slate-500">{label}</span>
|
|
||||||
</div>
|
|
||||||
<span className={`text-xs font-medium ${enabled ? "text-slate-800" : "text-slate-400"}`}>
|
|
||||||
{enabled ? threshold : "已关闭"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => router.push(`/strategy-plaza/${strategyId}/edit`)}
|
|
||||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<Settings size={13} />
|
|
||||||
编辑参数
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* 基础配置 */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h4 className="text-xs font-semibold text-slate-600 mb-2">基础配置</h4>
|
|
||||||
{row("交易对", detail.symbol)}
|
|
||||||
{row("交易方向", detail.direction === "both" ? "多空双向" : detail.direction === "long_only" ? "只做多" : "只做空")}
|
|
||||||
{row("CVD 快线", detail.cvd_fast_window)}
|
|
||||||
{row("CVD 慢线", detail.cvd_slow_window)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 四层权重 */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h4 className="text-xs font-semibold text-slate-600 mb-2">四层权重</h4>
|
|
||||||
{row("方向权重", `${detail.weight_direction}%`)}
|
|
||||||
{row("环境权重", `${detail.weight_env}%`)}
|
|
||||||
{row("辅助权重", `${detail.weight_aux}%`)}
|
|
||||||
{row("动量权重", `${detail.weight_momentum}%`)}
|
|
||||||
{row("入场阈值", `${detail.entry_score} 分`)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 五道 Gate */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h4 className="text-xs font-semibold text-slate-600 mb-2">过滤门控 (Gate)</h4>
|
|
||||||
{gateRow("门1 波动率", detail.gate_vol_enabled, `ATR% ≥ ${((detail.vol_atr_pct_min ?? 0) * 100).toFixed(2)}%`)}
|
|
||||||
{gateRow("门2 CVD共振", detail.gate_cvd_enabled ?? true, "快慢CVD同向")}
|
|
||||||
{gateRow("门3 鲸鱼否决", detail.gate_whale_enabled, `USD ≥ $${((detail.whale_usd_threshold ?? 50000) / 1000).toFixed(0)}k`)}
|
|
||||||
{gateRow("门4 OBI否决", detail.gate_obi_enabled, `阈值 ${detail.obi_threshold}`)}
|
|
||||||
{gateRow("门5 期现背离", detail.gate_spot_perp_enabled, `溢价 ≤ ${((detail.spot_perp_threshold ?? 0.005) * 100).toFixed(2)}%`)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 风控参数 */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h4 className="text-xs font-semibold text-slate-600 mb-2">风控参数</h4>
|
|
||||||
{row("SL 宽度", `${detail.sl_atr_multiplier} × ATR`)}
|
|
||||||
{row("TP1 目标", `${detail.tp1_ratio} × RD`)}
|
|
||||||
{row("TP2 目标", `${detail.tp2_ratio} × RD`)}
|
|
||||||
{row("超时", `${detail.timeout_minutes} 分钟`)}
|
|
||||||
{row("反转阈值", `${detail.flip_threshold} 分`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Content router ───────────────────────────────────────────────
|
// ─── Content router ───────────────────────────────────────────────
|
||||||
function SignalsContent({ strategyId, symbol, detail }: { strategyId: string; symbol?: string; detail?: StrategyDetail | null }) {
|
function SignalsContent({ strategyId }: { strategyId: string }) {
|
||||||
const legacy = UUID_TO_LEGACY[strategyId] || strategyId;
|
if (strategyId === "v53") return <SignalsV53 />;
|
||||||
if (legacy === "v53") return <SignalsV53 />;
|
if (strategyId === "v53_fast") return <SignalsV53Fast />;
|
||||||
if (legacy === "v53_fast") return <SignalsV53Fast />;
|
if (strategyId === "v53_middle") return <SignalsV53Middle />;
|
||||||
if (legacy === "v53_middle") return <SignalsV53Middle />;
|
return <div className="p-8 text-gray-400">未知策略: {strategyId}</div>;
|
||||||
const weights = detail ? {
|
|
||||||
direction: detail.weight_direction,
|
|
||||||
env: detail.weight_env,
|
|
||||||
aux: detail.weight_aux,
|
|
||||||
momentum: detail.weight_momentum,
|
|
||||||
} : { direction: 38, env: 32, aux: 28, momentum: 2 };
|
|
||||||
const gates = detail ? {
|
|
||||||
obi_threshold: detail.obi_threshold,
|
|
||||||
whale_usd_threshold: detail.whale_usd_threshold,
|
|
||||||
whale_flow_pct: detail.whale_flow_pct,
|
|
||||||
vol_atr_pct_min: detail.vol_atr_pct_min,
|
|
||||||
spot_perp_threshold: detail.spot_perp_threshold,
|
|
||||||
} : { obi_threshold: 0.3, whale_usd_threshold: 100000, whale_flow_pct: 0.5, vol_atr_pct_min: 0.002, spot_perp_threshold: 0.003 };
|
|
||||||
return (
|
|
||||||
<SignalsGeneric
|
|
||||||
strategyId={strategyId}
|
|
||||||
symbol={symbol || "BTCUSDT"}
|
|
||||||
cvdFastWindow={detail?.cvd_fast_window || "15m"}
|
|
||||||
cvdSlowWindow={detail?.cvd_slow_window || "1h"}
|
|
||||||
weights={weights}
|
|
||||||
gates={gates}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaperContent({ strategyId, symbol }: { strategyId: string; symbol?: string }) {
|
function PaperContent({ strategyId }: { strategyId: string }) {
|
||||||
const legacy = UUID_TO_LEGACY[strategyId] || strategyId;
|
if (strategyId === "v53") return <PaperV53 />;
|
||||||
if (legacy === "v53") return <PaperV53 />;
|
if (strategyId === "v53_fast") return <PaperV53Fast />;
|
||||||
if (legacy === "v53_fast") return <PaperV53Fast />;
|
if (strategyId === "v53_middle") return <PaperV53Middle />;
|
||||||
if (legacy === "v53_middle") return <PaperV53Middle />;
|
return <div className="p-8 text-gray-400">未知策略: {strategyId}</div>;
|
||||||
return <PaperGeneric strategyId={strategyId} symbol={symbol || "BTCUSDT"} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Main Page ────────────────────────────────────────────────────
|
// ─── Main Page ────────────────────────────────────────────────────
|
||||||
@ -232,67 +83,9 @@ export default function StrategyDetailPage() {
|
|||||||
const tab = searchParams?.get("tab") || "signals";
|
const tab = searchParams?.get("tab") || "signals";
|
||||||
|
|
||||||
const [summary, setSummary] = useState<StrategySummary | null>(null);
|
const [summary, setSummary] = useState<StrategySummary | null>(null);
|
||||||
const [detail, setDetail] = useState<StrategyDetail | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchSummary = useCallback(async () => {
|
||||||
try {
|
|
||||||
// Try new /api/strategies/{id} first for full detail
|
|
||||||
const r = await authFetch(`/api/strategies/${strategyId}`);
|
|
||||||
if (r.ok) {
|
|
||||||
const d = await r.json();
|
|
||||||
const s = d.strategy;
|
|
||||||
setSummary({
|
|
||||||
strategy_id: s.strategy_id,
|
|
||||||
display_name: s.display_name,
|
|
||||||
status: s.status,
|
|
||||||
started_at: s.started_at || s.created_at || Date.now(),
|
|
||||||
initial_balance: s.initial_balance,
|
|
||||||
current_balance: s.current_balance,
|
|
||||||
net_usdt: s.net_usdt || 0,
|
|
||||||
net_r: s.net_r || 0,
|
|
||||||
trade_count: s.trade_count || 0,
|
|
||||||
win_rate: s.win_rate || 0,
|
|
||||||
avg_win_r: s.avg_win_r || 0,
|
|
||||||
avg_loss_r: s.avg_loss_r || 0,
|
|
||||||
open_positions: s.open_positions || 0,
|
|
||||||
pnl_usdt_24h: s.pnl_usdt_24h || 0,
|
|
||||||
pnl_r_24h: s.pnl_r_24h || 0,
|
|
||||||
cvd_fast_window: s.cvd_fast_window,
|
|
||||||
cvd_slow_window: s.cvd_slow_window,
|
|
||||||
description: s.description,
|
|
||||||
symbol: s.symbol,
|
|
||||||
});
|
|
||||||
setDetail({
|
|
||||||
weight_direction: s.weight_direction,
|
|
||||||
weight_env: s.weight_env,
|
|
||||||
weight_aux: s.weight_aux,
|
|
||||||
weight_momentum: s.weight_momentum,
|
|
||||||
entry_score: s.entry_score,
|
|
||||||
gate_vol_enabled: s.gate_vol_enabled,
|
|
||||||
vol_atr_pct_min: s.vol_atr_pct_min,
|
|
||||||
gate_cvd_enabled: s.gate_cvd_enabled,
|
|
||||||
gate_whale_enabled: s.gate_whale_enabled,
|
|
||||||
whale_usd_threshold: s.whale_usd_threshold,
|
|
||||||
whale_flow_pct: s.whale_flow_pct,
|
|
||||||
gate_obi_enabled: s.gate_obi_enabled,
|
|
||||||
obi_threshold: s.obi_threshold,
|
|
||||||
gate_spot_perp_enabled: s.gate_spot_perp_enabled,
|
|
||||||
spot_perp_threshold: s.spot_perp_threshold,
|
|
||||||
sl_atr_multiplier: s.sl_atr_multiplier,
|
|
||||||
tp1_ratio: s.tp1_ratio,
|
|
||||||
tp2_ratio: s.tp2_ratio,
|
|
||||||
timeout_minutes: s.timeout_minutes,
|
|
||||||
flip_threshold: s.flip_threshold,
|
|
||||||
symbol: s.symbol,
|
|
||||||
direction: s.direction,
|
|
||||||
cvd_fast_window: s.cvd_fast_window,
|
|
||||||
cvd_slow_window: s.cvd_slow_window,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
// Fallback to legacy /api/strategy-plaza/{id}/summary
|
|
||||||
try {
|
try {
|
||||||
const r = await authFetch(`/api/strategy-plaza/${strategyId}/summary`);
|
const r = await authFetch(`/api/strategy-plaza/${strategyId}/summary`);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@ -304,10 +97,10 @@ export default function StrategyDetailPage() {
|
|||||||
}, [strategyId]);
|
}, [strategyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData().finally(() => setLoading(false));
|
fetchSummary();
|
||||||
const iv = setInterval(fetchData, 30000);
|
const iv = setInterval(fetchSummary, 30000);
|
||||||
return () => clearInterval(iv);
|
return () => clearInterval(iv);
|
||||||
}, [fetchData]);
|
}, [fetchSummary]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -318,20 +111,17 @@ export default function StrategyDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isProfit = (summary?.net_usdt ?? 0) >= 0;
|
const isProfit = (summary?.net_usdt ?? 0) >= 0;
|
||||||
const cvdLabel = summary?.cvd_fast_window
|
|
||||||
? `${summary.cvd_fast_window}/${summary.cvd_slow_window}`
|
|
||||||
: summary?.cvd_windows || "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 max-w-full">
|
<div className="p-4 max-w-full">
|
||||||
{/* Back + Strategy Header */}
|
{/* Back + Strategy Header */}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Link href="/strategy-plaza" className="flex items-center gap-1 text-slate-500 hover:text-slate-800 text-sm transition-colors">
|
<Link href="/strategy-plaza" className="flex items-center gap-1 text-gray-400 hover:text-white text-sm transition-colors">
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
策略广场
|
策略广场
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-slate-300">/</span>
|
<span className="text-gray-600">/</span>
|
||||||
<span className="text-slate-800 font-medium">{summary?.display_name ?? strategyId}</span>
|
<span className="text-white font-medium">{summary?.display_name ?? strategyId}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Bar */}
|
{/* Summary Bar */}
|
||||||
@ -341,8 +131,8 @@ export default function StrategyDetailPage() {
|
|||||||
<span className="text-xs text-slate-400 flex items-center gap-1">
|
<span className="text-xs text-slate-400 flex items-center gap-1">
|
||||||
<Clock size={10} />运行 {fmtDur(summary.started_at)}
|
<Clock size={10} />运行 {fmtDur(summary.started_at)}
|
||||||
</span>
|
</span>
|
||||||
{cvdLabel && (
|
{summary.cvd_windows && (
|
||||||
<span className="text-xs text-blue-600 bg-blue-50 border border-blue-100 px-2 py-0.5 rounded">CVD {cvdLabel}</span>
|
<span className="text-xs text-blue-600 bg-blue-50 border border-blue-100 px-2 py-0.5 rounded">CVD {summary.cvd_windows}</span>
|
||||||
)}
|
)}
|
||||||
<span className="ml-auto flex items-center gap-4 text-xs">
|
<span className="ml-auto flex items-center gap-4 text-xs">
|
||||||
<span className="text-slate-500">胜率 <span className={summary.win_rate >= 50 ? "text-emerald-600 font-bold" : "text-amber-600 font-bold"}>{summary.win_rate}%</span></span>
|
<span className="text-slate-500">胜率 <span className={summary.win_rate >= 50 ? "text-emerald-600 font-bold" : "text-amber-600 font-bold"}>{summary.win_rate}%</span></span>
|
||||||
@ -358,7 +148,6 @@ export default function StrategyDetailPage() {
|
|||||||
{[
|
{[
|
||||||
{ key: "signals", label: "📊 信号引擎" },
|
{ key: "signals", label: "📊 信号引擎" },
|
||||||
{ key: "paper", label: "📈 模拟盘" },
|
{ key: "paper", label: "📈 模拟盘" },
|
||||||
{ key: "config", label: "⚙️ 参数配置" },
|
|
||||||
].map(({ key, label }) => (
|
].map(({ key, label }) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
@ -374,13 +163,12 @@ export default function StrategyDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content — direct render of existing pages */}
|
||||||
<div>
|
<div>
|
||||||
{tab === "signals" && <SignalsContent strategyId={strategyId} symbol={summary?.symbol} detail={detail} />}
|
{tab === "signals" ? (
|
||||||
{tab === "paper" && <PaperContent strategyId={strategyId} symbol={summary?.symbol} />}
|
<SignalsContent strategyId={strategyId} />
|
||||||
{tab === "config" && detail && <ConfigTab detail={detail} strategyId={strategyId} />}
|
) : (
|
||||||
{tab === "config" && !detail && (
|
<PaperContent strategyId={strategyId} />
|
||||||
<div className="text-center text-slate-400 text-sm py-16">暂无配置信息</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useAuth } from "@/lib/auth";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import StrategyForm, { DEFAULT_FORM } from "@/components/StrategyForm";
|
|
||||||
|
|
||||||
export default function CreateStrategyPage() {
|
|
||||||
useAuth();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 max-w-2xl mx-auto">
|
|
||||||
<div className="mb-5">
|
|
||||||
<h1 className="text-lg font-bold text-slate-800">新增策略</h1>
|
|
||||||
<p className="text-slate-500 text-xs mt-0.5">配置策略参数,创建后立即开始运行</p>
|
|
||||||
</div>
|
|
||||||
<StrategyForm
|
|
||||||
mode="create"
|
|
||||||
initialData={DEFAULT_FORM}
|
|
||||||
onSuccess={(id) => router.push(`/strategy-plaza/${id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { authFetch, useAuth } from "@/lib/auth";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
TrendingUp, TrendingDown, Clock, Activity, RotateCcw
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface DeprecatedStrategy {
|
|
||||||
strategy_id: string;
|
|
||||||
display_name: string;
|
|
||||||
symbol: string;
|
|
||||||
status: string;
|
|
||||||
started_at: number;
|
|
||||||
deprecated_at: number | null;
|
|
||||||
initial_balance: number;
|
|
||||||
current_balance: number;
|
|
||||||
net_usdt: number;
|
|
||||||
net_r: number;
|
|
||||||
trade_count: number;
|
|
||||||
win_rate: number;
|
|
||||||
avg_win_r: number;
|
|
||||||
avg_loss_r: number;
|
|
||||||
pnl_usdt_24h: number;
|
|
||||||
last_trade_at: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ms: number | null): string {
|
|
||||||
if (!ms) return "—";
|
|
||||||
return new Date(ms).toLocaleString("zh-CN", {
|
|
||||||
timeZone: "Asia/Shanghai",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DeprecatedStrategiesPage() {
|
|
||||||
useAuth();
|
|
||||||
const [strategies, setStrategies] = useState<DeprecatedStrategy[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [restoring, setRestoring] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch("/api/strategies?include_deprecated=true");
|
|
||||||
const data = await res.json();
|
|
||||||
const deprecated = (data.strategies || []).filter(
|
|
||||||
(s: DeprecatedStrategy) => s.status === "deprecated"
|
|
||||||
);
|
|
||||||
setStrategies(deprecated);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const handleRestore = async (sid: string, name: string) => {
|
|
||||||
if (!confirm(`确认重新启用策略「${name}」?将继续使用原有余额和历史数据。`)) return;
|
|
||||||
setRestoring(sid);
|
|
||||||
try {
|
|
||||||
await authFetch(`/api/strategies/${sid}/restore`, { method: "POST" });
|
|
||||||
await fetchData();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
setRestoring(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-64">
|
|
||||||
<div className="text-slate-400 text-sm animate-pulse">加载中...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 max-w-5xl mx-auto">
|
|
||||||
<div className="flex items-center justify-between mb-5">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-bold text-slate-800">废弃策略</h1>
|
|
||||||
<p className="text-slate-500 text-xs mt-0.5">数据永久保留,可随时重新启用</p>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/strategy-plaza"
|
|
||||||
className="text-xs text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
← 返回策略广场
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{strategies.length === 0 ? (
|
|
||||||
<div className="text-center text-slate-400 text-sm py-16">暂无废弃策略</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
||||||
{strategies.map((s) => {
|
|
||||||
const isProfit = s.net_usdt >= 0;
|
|
||||||
const is24hProfit = s.pnl_usdt_24h >= 0;
|
|
||||||
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
|
|
||||||
return (
|
|
||||||
<div key={s.strategy_id} className="rounded-xl border border-slate-200 bg-white overflow-hidden opacity-80">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-slate-600 text-sm">{s.display_name}</h3>
|
|
||||||
<span className="text-[10px] text-slate-400 bg-slate-200 px-1.5 py-0.5 rounded-full">已废弃</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-slate-400">{s.symbol.replace("USDT", "")}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PnL */}
|
|
||||||
<div className="px-4 pt-3 pb-2">
|
|
||||||
<div className="flex items-end justify-between mb-2">
|
|
||||||
<div>
|
|
||||||
<div className="text-[10px] text-slate-400 mb-0.5">废弃时余额</div>
|
|
||||||
<div className="text-xl font-bold text-slate-700">
|
|
||||||
{s.current_balance.toLocaleString()}
|
|
||||||
<span className="text-xs font-normal text-slate-400 ml-1">USDT</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-[10px] text-slate-400 mb-0.5">累计盈亏</div>
|
|
||||||
<div className={`text-lg font-bold ${isProfit ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Balance bar */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="flex justify-between text-[10px] text-slate-400 mb-1">
|
|
||||||
<span>{balancePct}%</span>
|
|
||||||
<span>{s.initial_balance.toLocaleString()} USDT 初始</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
|
||||||
<div
|
|
||||||
className={`h-1.5 rounded-full ${isProfit ? "bg-emerald-400" : "bg-red-300"}`}
|
|
||||||
style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">胜率</div>
|
|
||||||
<div className="text-sm font-bold text-slate-600">{s.win_rate}%</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">净R</div>
|
|
||||||
<div className={`text-sm font-bold ${s.net_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">交易数</div>
|
|
||||||
<div className="text-sm font-bold text-slate-700">{s.trade_count}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="px-4 py-2.5 border-t border-slate-100 flex items-center justify-between bg-slate-50/60">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{is24hProfit ? <TrendingUp size={12} className="text-emerald-500" /> : <TrendingDown size={12} className="text-red-400" />}
|
|
||||||
<span className="text-[10px] text-slate-500">
|
|
||||||
废弃于 {formatTime(s.deprecated_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRestore(s.strategy_id, s.display_name)}
|
|
||||||
disabled={restoring === s.strategy_id}
|
|
||||||
className="flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 disabled:opacity-50 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<RotateCcw size={11} />
|
|
||||||
{restoring === s.strategy_id ? "启用中..." : "重新启用"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ import { useEffect, useState, useCallback } from "react";
|
|||||||
import { authFetch } from "@/lib/auth";
|
import { authFetch } from "@/lib/auth";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import {
|
import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
@ -13,16 +12,11 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
Plus,
|
|
||||||
Settings,
|
|
||||||
Trash2,
|
|
||||||
PlusCircle,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
interface StrategyCard {
|
interface StrategyCard {
|
||||||
strategy_id: string;
|
id: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
symbol: string;
|
|
||||||
status: "running" | "paused" | "error";
|
status: "running" | "paused" | "error";
|
||||||
started_at: number;
|
started_at: number;
|
||||||
initial_balance: number;
|
initial_balance: number;
|
||||||
@ -88,250 +82,128 @@ function StatusBadge({ status }: { status: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AddBalanceModal ────────────────────────────────────────────────────────────
|
function StrategyCardComponent({ s }: { s: StrategyCard }) {
|
||||||
function AddBalanceModal({
|
|
||||||
strategy,
|
|
||||||
onClose,
|
|
||||||
onSuccess,
|
|
||||||
}: {
|
|
||||||
strategy: StrategyCard;
|
|
||||||
onClose: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}) {
|
|
||||||
const [amount, setAmount] = useState(1000);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (amount <= 0) { setError("金额必须大于0"); return; }
|
|
||||||
setSubmitting(true);
|
|
||||||
setError("");
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/strategies/${strategy.strategy_id}/add-balance`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ amount }),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("追加失败");
|
|
||||||
onSuccess();
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : "未知错误");
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
|
||||||
<div className="bg-white rounded-xl shadow-xl p-5 w-80">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-sm mb-1">追加余额</h3>
|
|
||||||
<p className="text-[11px] text-slate-500 mb-3">策略:{strategy.display_name}</p>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="text-xs text-slate-600 mb-1 block">追加金额 (USDT)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={amount}
|
|
||||||
min={100}
|
|
||||||
step={100}
|
|
||||||
onChange={(e) => setAmount(parseFloat(e.target.value) || 0)}
|
|
||||||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] text-slate-400 mt-1">
|
|
||||||
追加后初始资金:{(strategy.initial_balance + amount).toLocaleString()} USDT /
|
|
||||||
余额:{(strategy.current_balance + amount).toLocaleString()} USDT
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{error && <p className="text-xs text-red-500 mb-2">{error}</p>}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button onClick={onClose} className="flex-1 py-2 rounded-lg border border-slate-200 text-sm text-slate-600 hover:bg-slate-50">取消</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={submitting}
|
|
||||||
className="flex-1 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{submitting ? "追加中..." : "确认追加"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── StrategyCardComponent ──────────────────────────────────────────────────────
|
|
||||||
function StrategyCardComponent({
|
|
||||||
s,
|
|
||||||
onDeprecate,
|
|
||||||
onAddBalance,
|
|
||||||
}: {
|
|
||||||
s: StrategyCard;
|
|
||||||
onDeprecate: (s: StrategyCard) => void;
|
|
||||||
onAddBalance: (s: StrategyCard) => void;
|
|
||||||
}) {
|
|
||||||
const isProfit = s.net_usdt >= 0;
|
const isProfit = s.net_usdt >= 0;
|
||||||
const is24hProfit = s.pnl_usdt_24h >= 0;
|
const is24hProfit = s.pnl_usdt_24h >= 0;
|
||||||
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
|
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
|
||||||
const symbolShort = s.symbol?.replace("USDT", "") || "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden hover:border-blue-300 hover:shadow-md transition-all group">
|
<Link href={`/strategy-plaza/${s.id}`}>
|
||||||
{/* Header */}
|
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden hover:border-blue-300 hover:shadow-md transition-all cursor-pointer group">
|
||||||
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||||||
<Link href={`/strategy-plaza/${s.strategy_id}`}>
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-slate-800 text-sm group-hover:text-blue-700 transition-colors cursor-pointer hover:underline">
|
<h3 className="font-semibold text-slate-800 text-sm group-hover:text-blue-700 transition-colors">
|
||||||
{s.display_name}
|
{s.display_name}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
<StatusBadge status={s.status} />
|
||||||
<StatusBadge status={s.status} />
|
|
||||||
{symbolShort && (
|
|
||||||
<span className="text-[10px] text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded-full">{symbolShort}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-slate-400 flex items-center gap-1">
|
|
||||||
<Clock size={9} />
|
|
||||||
{formatDuration(s.started_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main PnL */}
|
|
||||||
<div className="px-4 pt-3 pb-2">
|
|
||||||
<div className="flex items-end justify-between mb-2">
|
|
||||||
<div>
|
|
||||||
<div className="text-[10px] text-slate-400 mb-0.5">当前余额</div>
|
|
||||||
<div className="text-xl font-bold text-slate-800">
|
|
||||||
{s.current_balance.toLocaleString()}
|
|
||||||
<span className="text-xs font-normal text-slate-400 ml-1">USDT</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<span className="text-[10px] text-slate-400 flex items-center gap-1">
|
||||||
<div className="text-[10px] text-slate-400 mb-0.5">累计盈亏</div>
|
<Clock size={9} />
|
||||||
<div className={`text-lg font-bold ${isProfit ? "text-emerald-600" : "text-red-500"}`}>
|
{formatDuration(s.started_at)}
|
||||||
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Balance Bar */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="flex justify-between text-[10px] text-slate-400 mb-1">
|
|
||||||
<span>{balancePct}%</span>
|
|
||||||
<span>{s.initial_balance.toLocaleString()} USDT 初始</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
|
||||||
<div
|
|
||||||
className={`h-1.5 rounded-full ${isProfit ? "bg-emerald-400" : "bg-red-400"}`}
|
|
||||||
style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Row */}
|
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">胜率</div>
|
|
||||||
<div className={`text-sm font-bold ${s.win_rate >= 50 ? "text-emerald-600" : s.win_rate >= 45 ? "text-amber-600" : "text-red-500"}`}>
|
|
||||||
{s.win_rate}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">净R</div>
|
|
||||||
<div className={`text-sm font-bold ${s.net_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
|
||||||
<div className="text-[10px] text-slate-400">交易数</div>
|
|
||||||
<div className="text-sm font-bold text-slate-700">{s.trade_count}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Avg win/loss */}
|
|
||||||
<div className="flex gap-2 mb-3">
|
|
||||||
<div className="flex-1 bg-emerald-50 rounded-lg px-2.5 py-1.5">
|
|
||||||
<span className="text-[10px] text-emerald-600">平均赢</span>
|
|
||||||
<span className="float-right text-[10px] font-bold text-emerald-600">+{s.avg_win_r}R</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 bg-red-50 rounded-lg px-2.5 py-1.5">
|
|
||||||
<span className="text-[10px] text-red-500">平均亏</span>
|
|
||||||
<span className="float-right text-[10px] font-bold text-red-500">{s.avg_loss_r}R</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="px-4 py-2.5 border-t border-slate-100 flex items-center justify-between bg-slate-50/60">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{is24hProfit ? (
|
|
||||||
<TrendingUp size={12} className="text-emerald-500" />
|
|
||||||
) : (
|
|
||||||
<TrendingDown size={12} className="text-red-500" />
|
|
||||||
)}
|
|
||||||
<span className={`text-[10px] font-medium ${is24hProfit ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
24h {is24hProfit ? "+" : ""}{s.pnl_usdt_24h.toLocaleString()} U
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-[10px] text-slate-400">
|
|
||||||
<Activity size={9} />
|
{/* Main PnL */}
|
||||||
{s.open_positions > 0 ? (
|
<div className="px-4 pt-3 pb-2">
|
||||||
<span className="text-amber-600 font-medium">{s.open_positions}仓持仓中</span>
|
<div className="flex items-end justify-between mb-2">
|
||||||
) : (
|
<div>
|
||||||
<span>上次: {formatTime(s.last_trade_at)}</span>
|
<div className="text-[10px] text-slate-400 mb-0.5">当前余额</div>
|
||||||
)}
|
<div className="text-xl font-bold text-slate-800">
|
||||||
|
{s.current_balance.toLocaleString()}
|
||||||
|
<span className="text-xs font-normal text-slate-400 ml-1">USDT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-[10px] text-slate-400 mb-0.5">累计盈亏</div>
|
||||||
|
<div className={`text-lg font-bold ${isProfit ? "text-emerald-600" : "text-red-500"}`}>
|
||||||
|
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance Bar */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex justify-between text-[10px] text-slate-400 mb-1">
|
||||||
|
<span>{balancePct}%</span>
|
||||||
|
<span>{s.initial_balance.toLocaleString()} USDT 初始</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full ${isProfit ? "bg-emerald-400" : "bg-red-400"}`}
|
||||||
|
style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Row */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||||
|
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||||||
|
<div className="text-[10px] text-slate-400">胜率</div>
|
||||||
|
<div className={`text-sm font-bold ${s.win_rate >= 50 ? "text-emerald-600" : s.win_rate >= 45 ? "text-amber-600" : "text-red-500"}`}>
|
||||||
|
{s.win_rate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||||||
|
<div className="text-[10px] text-slate-400">净R</div>
|
||||||
|
<div className={`text-sm font-bold ${s.net_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||||||
|
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||||||
|
<div className="text-[10px] text-slate-400">交易数</div>
|
||||||
|
<div className="text-sm font-bold text-slate-700">{s.trade_count}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avg win/loss */}
|
||||||
|
<div className="flex gap-2 mb-3">
|
||||||
|
<div className="flex-1 bg-emerald-50 rounded-lg px-2.5 py-1.5">
|
||||||
|
<span className="text-[10px] text-emerald-600">平均赢</span>
|
||||||
|
<span className="float-right text-[10px] font-bold text-emerald-600">+{s.avg_win_r}R</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 bg-red-50 rounded-lg px-2.5 py-1.5">
|
||||||
|
<span className="text-[10px] text-red-500">平均亏</span>
|
||||||
|
<span className="float-right text-[10px] font-bold text-red-500">{s.avg_loss_r}R</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-4 py-2.5 border-t border-slate-100 flex items-center justify-between bg-slate-50/60">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{is24hProfit ? (
|
||||||
|
<TrendingUp size={12} className="text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown size={12} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className={`text-[10px] font-medium ${is24hProfit ? "text-emerald-600" : "text-red-500"}`}>
|
||||||
|
24h {is24hProfit ? "+" : ""}{s.pnl_usdt_24h.toLocaleString()} U
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-[10px] text-slate-400">
|
||||||
|
<Activity size={9} />
|
||||||
|
{s.open_positions > 0 ? (
|
||||||
|
<span className="text-amber-600 font-medium">{s.open_positions}仓持仓中</span>
|
||||||
|
) : (
|
||||||
|
<span>上次: {formatTime(s.last_trade_at)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="px-4 py-2.5 border-t border-slate-100 flex gap-2">
|
|
||||||
<Link
|
|
||||||
href={`/strategy-plaza/${s.strategy_id}/edit`}
|
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-lg border border-slate-200 text-[11px] text-slate-600 hover:bg-slate-50 transition-colors"
|
|
||||||
>
|
|
||||||
<Settings size={11} />
|
|
||||||
调整参数
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={() => onAddBalance(s)}
|
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-lg border border-blue-200 text-[11px] text-blue-600 hover:bg-blue-50 transition-colors"
|
|
||||||
>
|
|
||||||
<PlusCircle size={11} />
|
|
||||||
追加余额
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onDeprecate(s)}
|
|
||||||
className="flex items-center justify-center gap-1 px-2.5 py-1.5 rounded-lg border border-red-200 text-[11px] text-red-500 hover:bg-red-50 transition-colors"
|
|
||||||
>
|
|
||||||
<Trash2 size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
|
||||||
export default function StrategyPlazaPage() {
|
export default function StrategyPlazaPage() {
|
||||||
useAuth();
|
useAuth();
|
||||||
const router = useRouter();
|
|
||||||
const [strategies, setStrategies] = useState<StrategyCard[]>([]);
|
const [strategies, setStrategies] = useState<StrategyCard[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
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 PositionFilter = "all" | "with_open" | "no_open";
|
|
||||||
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 [positionFilter, setPositionFilter] = useState<PositionFilter>("all");
|
|
||||||
const [sortKey, setSortKey] = useState<SortKey>("net_usdt_desc");
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch("/api/strategies");
|
const res = await authFetch("/api/strategy-plaza");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setStrategies(data.strategies || []);
|
setStrategies(data.strategies || []);
|
||||||
setLastUpdated(new Date());
|
setLastUpdated(new Date());
|
||||||
@ -348,49 +220,6 @@ export default function StrategyPlazaPage() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const handleDeprecate = async (s: StrategyCard) => {
|
|
||||||
if (!confirm(`确认废弃策略「${s.display_name}」?\n\n废弃后策略停止运行,数据永久保留,可在废弃列表中重新启用。`)) return;
|
|
||||||
try {
|
|
||||||
await authFetch(`/api/strategies/${s.strategy_id}/deprecate`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ confirm: true }),
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
if (positionFilter === "with_open" && s.open_positions <= 0) return false;
|
|
||||||
if (positionFilter === "no_open" && s.open_positions > 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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-64">
|
<div className="flex items-center justify-center min-h-64">
|
||||||
@ -405,172 +234,25 @@ export default function StrategyPlazaPage() {
|
|||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-slate-800">策略广场</h1>
|
<h1 className="text-lg font-bold text-slate-800">策略广场</h1>
|
||||||
<p className="text-slate-500 text-xs mt-0.5">点击策略名查看信号引擎和模拟盘详情</p>
|
<p className="text-slate-500 text-xs mt-0.5">点击卡片查看信号引擎和模拟盘详情</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
{lastUpdated && (
|
||||||
{lastUpdated && (
|
<div className="text-[10px] text-slate-400 flex items-center gap-1">
|
||||||
<div className="text-[10px] text-slate-400 flex items-center gap-1">
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
{lastUpdated.toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })}
|
||||||
{lastUpdated.toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => router.push("/strategy-plaza/create")}
|
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<Plus size={13} />
|
|
||||||
新增策略
|
|
||||||
</button>
|
|
||||||
</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>
|
||||||
<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>
|
|
||||||
{[
|
|
||||||
{ key: "all" as PositionFilter, label: "全部" },
|
|
||||||
{ key: "with_open" as PositionFilter, label: "有持仓" },
|
|
||||||
{ key: "no_open" as PositionFilter, label: "无持仓" },
|
|
||||||
].map((opt) => {
|
|
||||||
const active = positionFilter === opt.key;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={opt.key}
|
|
||||||
onClick={() => setPositionFilter(opt.key)}
|
|
||||||
className={`px-2 py-0.5 rounded-full border text-[11px] ${
|
|
||||||
active
|
|
||||||
? "border-amber-500 bg-amber-50 text-amber-600"
|
|
||||||
: "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>
|
</div>
|
||||||
|
|
||||||
{/* Strategy Cards */}
|
{/* Strategy Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
{filteredStrategies.map((s) => (
|
{strategies.map((s) => (
|
||||||
<StrategyCardComponent
|
<StrategyCardComponent key={s.id} s={s} />
|
||||||
key={s.strategy_id}
|
|
||||||
s={s}
|
|
||||||
onDeprecate={handleDeprecate}
|
|
||||||
onAddBalance={setAddBalanceTarget}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredStrategies.length === 0 && (
|
{strategies.length === 0 && (
|
||||||
<div className="text-center text-slate-400 text-sm py-16">
|
<div className="text-center text-slate-400 text-sm py-16">暂无策略数据</div>
|
||||||
<p className="mb-3">暂无运行中的策略</p>
|
|
||||||
<button
|
|
||||||
onClick={() => router.push("/strategy-plaza/create")}
|
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 text-white text-sm hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
<Plus size={14} />
|
|
||||||
创建第一个策略
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add Balance Modal */}
|
|
||||||
{addBalanceTarget && (
|
|
||||||
<AddBalanceModal
|
|
||||||
strategy={addBalanceTarget}
|
|
||||||
onClose={() => setAddBalanceTarget(null)}
|
|
||||||
onSuccess={fetchData}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useAuth } from "@/lib/auth";
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard, Info,
|
LayoutDashboard, Info,
|
||||||
Menu, X, Zap, LogIn, UserPlus,
|
Menu, X, Zap, LogIn, UserPlus,
|
||||||
ChevronLeft, ChevronRight, Activity, LogOut, Monitor, LineChart, Bolt, Archive
|
ChevronLeft, ChevronRight, Activity, LogOut, Monitor, LineChart, Bolt
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@ -15,7 +15,6 @@ const navItems = [
|
|||||||
{ href: "/trades", label: "成交流", icon: Activity },
|
{ href: "/trades", label: "成交流", icon: Activity },
|
||||||
{ href: "/live", label: "⚡ 实盘交易", icon: Bolt, section: "── 实盘 ──" },
|
{ href: "/live", label: "⚡ 实盘交易", icon: Bolt, section: "── 实盘 ──" },
|
||||||
{ href: "/strategy-plaza", label: "策略广场", icon: Zap, section: "── 策略 ──" },
|
{ href: "/strategy-plaza", label: "策略广场", icon: Zap, section: "── 策略 ──" },
|
||||||
{ href: "/strategy-plaza/deprecated", label: "废弃策略", icon: Archive },
|
|
||||||
{ href: "/server", label: "服务器", icon: Monitor },
|
{ href: "/server", label: "服务器", icon: Monitor },
|
||||||
{ href: "/about", label: "说明", icon: Info },
|
{ href: "/about", label: "说明", icon: Info },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,541 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { authFetch } from "@/lib/auth";
|
|
||||||
import { useAuth } from "@/lib/auth";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
ComposedChart, Area, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
|
|
||||||
ReferenceLine, CartesianGrid, Legend
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
type Symbol = "BTC" | "ETH" | "XRP" | "SOL";
|
|
||||||
|
|
||||||
interface IndicatorRow {
|
|
||||||
ts: number;
|
|
||||||
cvd_fast: number;
|
|
||||||
cvd_mid: number;
|
|
||||||
cvd_day: number;
|
|
||||||
atr_5m: number;
|
|
||||||
vwap_30m: number;
|
|
||||||
price: number;
|
|
||||||
score: number;
|
|
||||||
signal: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LatestIndicator {
|
|
||||||
ts: number;
|
|
||||||
cvd_fast: number;
|
|
||||||
cvd_mid: number;
|
|
||||||
cvd_day: number;
|
|
||||||
cvd_fast_slope: number;
|
|
||||||
atr_5m: number;
|
|
||||||
atr_percentile: number;
|
|
||||||
vwap_30m: number;
|
|
||||||
price: number;
|
|
||||||
p95_qty: number;
|
|
||||||
p99_qty: number;
|
|
||||||
score: number;
|
|
||||||
display_score?: number;
|
|
||||||
gate_passed?: boolean;
|
|
||||||
signal: string | null;
|
|
||||||
tier?: "light" | "standard" | "heavy" | null;
|
|
||||||
factors?: {
|
|
||||||
track?: string;
|
|
||||||
direction?: { score?: number; max?: number; cvd_resonance?: number; p99_flow?: number; accel_bonus?: number };
|
|
||||||
crowding?: { score?: number; max?: number; lsr_contrarian?: number; top_trader_position?: number };
|
|
||||||
environment?: { score?: number; max?: number };
|
|
||||||
auxiliary?: { score?: number; max?: number; coinbase_premium?: number };
|
|
||||||
gate_passed?: boolean;
|
|
||||||
block_reason?: string;
|
|
||||||
gate_block?: string;
|
|
||||||
obi_raw?: number;
|
|
||||||
spot_perp_div?: number;
|
|
||||||
whale_cvd_ratio?: number;
|
|
||||||
atr_pct_price?: number;
|
|
||||||
alt_score_ref?: number;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WINDOWS = [
|
|
||||||
{ label: "1h", value: 60 },
|
|
||||||
{ label: "4h", value: 240 },
|
|
||||||
{ label: "12h", value: 720 },
|
|
||||||
{ label: "24h", value: 1440 },
|
|
||||||
];
|
|
||||||
|
|
||||||
function bjtStr(ms: number) {
|
|
||||||
const d = new Date(ms + 8 * 3600 * 1000);
|
|
||||||
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bjtFull(ms: number) {
|
|
||||||
const d = new Date(ms + 8 * 3600 * 1000);
|
|
||||||
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(v: number, decimals = 1): string {
|
|
||||||
if (Math.abs(v) >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
|
|
||||||
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}K`;
|
|
||||||
return v.toFixed(decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) {
|
|
||||||
const ratio = Math.max(0, Math.min((score / max) * 100, 100));
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[10px] text-slate-500 w-6 shrink-0">{label}</span>
|
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-slate-100 overflow-hidden">
|
|
||||||
<div className={`h-full ${colorClass}`} style={{ width: `${ratio}%` }} />
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-mono text-slate-600 w-8 text-right">{score}/{max}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALT_GATE_THRESHOLDS: Record<string, { vol: string; obi: string; spd: string; whale: string }> = {
|
|
||||||
ETH: { vol: "0.3%", obi: "0.35", spd: "0.5%", whale: "$50k" },
|
|
||||||
XRP: { vol: "0.25%", obi: "0.40", spd: "0.6%", whale: "$30k" },
|
|
||||||
SOL: { vol: "0.4%", obi: "0.45", spd: "0.8%", whale: "$20k" },
|
|
||||||
};
|
|
||||||
|
|
||||||
function ALTGateCard({ symbol, factors }: { symbol: Symbol; factors: LatestIndicator["factors"] }) {
|
|
||||||
if (!factors || symbol === "BTC") return null;
|
|
||||||
const thresholds = ALT_GATE_THRESHOLDS[symbol] ?? ALT_GATE_THRESHOLDS["ETH"];
|
|
||||||
const passed = factors.gate_passed ?? true;
|
|
||||||
const blockReason = factors.gate_block;
|
|
||||||
return (
|
|
||||||
<div className={`rounded-xl border px-3 py-2 mt-2 ${passed ? "border-purple-200 bg-purple-50" : "border-red-200 bg-red-50"}`}>
|
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-purple-800">🔒 {symbol} Gate-Control</p>
|
|
||||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded ${passed ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-600"}`}>
|
|
||||||
{passed ? "✅ Gate通过" : "❌ 否决"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">波动率</p>
|
|
||||||
<p className="text-xs font-mono text-slate-800">{((factors.atr_pct_price ?? 0) * 100).toFixed(3)}%</p>
|
|
||||||
<p className="text-[9px] text-slate-400">需 ≥{thresholds.vol}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">OBI</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.obi_raw ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.obi_raw ?? 0) * 100).toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">否决±{thresholds.obi}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">期现背离</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.spot_perp_div ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.spot_perp_div ?? 0) * 10000).toFixed(2)}bps
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">否决±{thresholds.spd}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">鲸鱼阈值</p>
|
|
||||||
<p className="text-xs font-mono text-slate-800">{thresholds.whale}</p>
|
|
||||||
<p className="text-[9px] text-slate-400">大单门槛</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{blockReason && (
|
|
||||||
<p className="text-[10px] text-red-600 mt-1.5 bg-red-50 rounded px-2 py-1">
|
|
||||||
否决原因: <span className="font-mono">{blockReason}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BTCGateCard({ factors }: { factors: LatestIndicator["factors"] }) {
|
|
||||||
if (!factors) return null;
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 mt-2">
|
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-amber-800">⚡ BTC Gate-Control</p>
|
|
||||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded ${factors.gate_passed ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-600"}`}>
|
|
||||||
{factors.gate_passed ? "✅ Gate通过" : "❌ 否决"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">波动率</p>
|
|
||||||
<p className="text-xs font-mono text-slate-800">{((factors.atr_pct_price ?? 0) * 100).toFixed(3)}%</p>
|
|
||||||
<p className="text-[9px] text-slate-400">需 ≥0.2%</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">OBI</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.obi_raw ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.obi_raw ?? 0) * 100).toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">盘口失衡</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">期现背离</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.spot_perp_div ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.spot_perp_div ?? 0) * 10000).toFixed(2)}bps
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">spot-perp</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded px-2 py-1">
|
|
||||||
<p className="text-[10px] text-slate-400">巨鲸CVD</p>
|
|
||||||
<p className={`text-xs font-mono ${(factors.whale_cvd_ratio ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{((factors.whale_cvd_ratio ?? 0) * 100).toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
<p className="text-[9px] text-slate-400">>$100k</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{factors.block_reason && (
|
|
||||||
<p className="text-[10px] text-red-600 mt-1.5 bg-red-50 rounded px-2 py-1">
|
|
||||||
否决原因: <span className="font-mono">{factors.block_reason}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndicatorCards({ symbol, strategy }: { symbol: Symbol; strategy: string }) {
|
|
||||||
const [data, setData] = useState<LatestIndicator | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetch = async () => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/latest?strategy=${strategy}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json[symbol] || null);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
fetch();
|
|
||||||
const iv = setInterval(fetch, 5000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [symbol, strategy]);
|
|
||||||
|
|
||||||
if (!data) return <div className="text-center text-slate-400 text-sm py-4">等待指标数据...</div>;
|
|
||||||
|
|
||||||
const isBTC = symbol === "BTC";
|
|
||||||
const priceVsVwap = data.price > data.vwap_30m ? "上方" : "下方";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD_fast (30m)</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_fast >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{fmt(data.cvd_fast)}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">
|
|
||||||
斜率: <span className={data.cvd_fast_slope >= 0 ? "text-emerald-600" : "text-red-500"}>
|
|
||||||
{data.cvd_fast_slope >= 0 ? "↑" : "↓"}{fmt(Math.abs(data.cvd_fast_slope))}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD_mid (4h)</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_mid >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{fmt(data.cvd_mid)}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">{data.cvd_mid > 0 ? "多" : "空"}头占优</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">CVD共振</p>
|
|
||||||
<p className={`font-mono font-bold text-sm ${data.cvd_fast >= 0 && data.cvd_mid >= 0 ? "text-emerald-600" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "text-red-500" : "text-slate-400"}`}>
|
|
||||||
{data.cvd_fast >= 0 && data.cvd_mid >= 0 ? "✅ 多头共振" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "✅ 空头共振" : "⚠️ 分歧"}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-400">V5.3核心信号</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">ATR</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">${fmt(data.atr_5m, 2)}</p>
|
|
||||||
<p className="text-[10px]">
|
|
||||||
<span className={data.atr_percentile > 60 ? "text-amber-600 font-semibold" : "text-slate-400"}>
|
|
||||||
{data.atr_percentile.toFixed(0)}%{data.atr_percentile > 60 ? "🔥" : ""}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">VWAP</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">${data.vwap_30m.toLocaleString("en-US", { maximumFractionDigits: 1 })}</p>
|
|
||||||
<p className="text-[10px]">
|
|
||||||
价格在<span className={data.price > data.vwap_30m ? "text-emerald-600" : "text-red-500"}>{priceVsVwap}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">P95</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-slate-800">{data.p95_qty.toFixed(4)}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">大单阈值</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
|
|
||||||
<p className="text-[10px] text-slate-400">P99</p>
|
|
||||||
<p className="font-mono font-semibold text-sm text-amber-600">{data.p99_qty.toFixed(4)}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">超大单</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`rounded-xl border px-3 py-2.5 ${
|
|
||||||
data.signal === "LONG" ? "border-emerald-300 bg-emerald-50" :
|
|
||||||
data.signal === "SHORT" ? "border-red-300 bg-red-50" :
|
|
||||||
"border-slate-200 bg-slate-50"
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-[10px] text-slate-500">
|
|
||||||
{isBTC ? "BTC Gate-Control" : "ALT 四层评分"}
|
|
||||||
{" · "}{"v53"}
|
|
||||||
</p>
|
|
||||||
<p className={`font-bold text-base ${
|
|
||||||
data.signal === "LONG" ? "text-emerald-700" :
|
|
||||||
data.signal === "SHORT" ? "text-red-600" :
|
|
||||||
"text-slate-400"
|
|
||||||
}`}>
|
|
||||||
{data.signal === "LONG" ? "🟢 做多" : data.signal === "SHORT" ? "🔴 做空" : "⚪ 无信号"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
{isBTC ? (
|
|
||||||
<>
|
|
||||||
<p className="font-mono font-bold text-lg text-slate-800">
|
|
||||||
{data.display_score ?? data.factors?.alt_score_ref ?? data.score}/100
|
|
||||||
<span className="text-[10px] font-normal text-slate-400 ml-1">参考分</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-slate-500">
|
|
||||||
{(data.gate_passed ?? data.factors?.gate_passed) ? (data.tier === "standard" ? "标准" : "不开仓") : "Gate否决"}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="font-mono font-bold text-lg text-slate-800">{data.score}/100</p>
|
|
||||||
<p className="text-[10px] text-slate-500">{data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : "不开仓"}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
<LayerScore label="方向" score={data.factors?.direction?.score ?? 0} max={55} colorClass="bg-blue-600" />
|
|
||||||
<LayerScore label="拥挤" score={data.factors?.crowding?.score ?? 0} max={25} colorClass="bg-violet-600" />
|
|
||||||
<LayerScore label="环境" score={data.factors?.environment?.score ?? 0} max={15} colorClass="bg-emerald-600" />
|
|
||||||
<LayerScore label="辅助" score={data.factors?.auxiliary?.score ?? 0} max={5} colorClass="bg-slate-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isBTC && data.factors && <ALTGateCard symbol={symbol} factors={data.factors} />}
|
|
||||||
{isBTC && data.factors && <BTCGateCard factors={data.factors} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SignalRecord {
|
|
||||||
ts: number;
|
|
||||||
score: number;
|
|
||||||
signal: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignalHistory({ symbol, strategy }: { symbol: Symbol; strategy: string }) {
|
|
||||||
const [data, setData] = useState<SignalRecord[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20&strategy=${strategy}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json.data || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
const iv = setInterval(fetchData, 15000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [symbol, strategy]);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">最近信号 ({strategy})</h3>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-slate-100 max-h-48 overflow-y-auto">
|
|
||||||
{data.map((s, i) => (
|
|
||||||
<div key={i} className="px-3 py-1.5 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-xs font-bold ${s.signal === "LONG" ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{s.signal === "LONG" ? "🟢 LONG" : "🔴 SHORT"}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-slate-400">{bjtFull(s.ts)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="font-mono text-xs text-slate-700">{s.score}</span>
|
|
||||||
<span className={`text-[10px] px-1 py-0.5 rounded ${
|
|
||||||
s.score >= 85 ? "bg-red-100 text-red-700" :
|
|
||||||
s.score >= 75 ? "bg-blue-100 text-blue-700" :
|
|
||||||
"bg-slate-100 text-slate-600"
|
|
||||||
}`}>
|
|
||||||
{s.score >= 85 ? "加仓" : s.score >= 75 ? "标准" : "不开仓"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CVDChart({ symbol, minutes, strategy }: { symbol: Symbol; minutes: number; strategy: string }) {
|
|
||||||
const [data, setData] = useState<IndicatorRow[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async (silent = false) => {
|
|
||||||
try {
|
|
||||||
const res = await authFetch(`/api/signals/indicators?symbol=${symbol}&minutes=${minutes}&strategy=${strategy}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
setData(json.data || []);
|
|
||||||
if (!silent) setLoading(false);
|
|
||||||
} catch {}
|
|
||||||
}, [symbol, minutes, strategy]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
fetchData();
|
|
||||||
const iv = setInterval(() => fetchData(true), 30000);
|
|
||||||
return () => clearInterval(iv);
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const chartData = data.map(d => ({
|
|
||||||
time: bjtStr(d.ts),
|
|
||||||
fast: parseFloat(d.cvd_fast?.toFixed(2) || "0"),
|
|
||||||
mid: parseFloat(d.cvd_mid?.toFixed(2) || "0"),
|
|
||||||
price: d.price,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const prices = chartData.map(d => d.price).filter(v => v > 0);
|
|
||||||
const pMin = prices.length ? Math.min(...prices) : 0;
|
|
||||||
const pMax = prices.length ? Math.max(...prices) : 0;
|
|
||||||
const pPad = (pMax - pMin) * 0.3 || pMax * 0.001;
|
|
||||||
|
|
||||||
if (loading) return <div className="flex items-center justify-center h-48 text-slate-400 text-sm">加载指标数据...</div>;
|
|
||||||
if (data.length === 0) return <div className="flex items-center justify-center h-48 text-slate-400 text-sm">暂无 V5.3 指标数据,signal-engine 需运行积累</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
|
||||||
<ComposedChart data={chartData} margin={{ top: 4, right: 60, bottom: 0, left: 8 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
|
||||||
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
|
||||||
<YAxis yAxisId="cvd" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} axisLine={false} width={55} />
|
|
||||||
<YAxis yAxisId="price" orientation="right" tick={{ fill: "#f59e0b", fontSize: 10 }} tickLine={false} axisLine={false} width={65}
|
|
||||||
domain={[Math.floor(pMin - pPad), Math.ceil(pMax + pPad)]}
|
|
||||||
tickFormatter={(v: number) => v >= 1000 ? `$${(v / 1000).toFixed(1)}k` : `$${v.toFixed(0)}`}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={(v: any, name: any) => {
|
|
||||||
if (name === "price") return [`$${Number(v).toLocaleString()}`, "币价"];
|
|
||||||
if (name === "fast") return [fmt(Number(v)), "CVD_fast(30m)"];
|
|
||||||
return [fmt(Number(v)), "CVD_mid(4h)"];
|
|
||||||
}}
|
|
||||||
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }}
|
|
||||||
/>
|
|
||||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
|
||||||
<ReferenceLine yAxisId="cvd" y={0} stroke="#94a3b8" strokeDasharray="4 2" />
|
|
||||||
<Area yAxisId="cvd" type="monotone" dataKey="fast" name="fast" stroke="#2563eb" fill="#eff6ff" strokeWidth={1.5} dot={false} connectNulls />
|
|
||||||
<Line yAxisId="cvd" type="monotone" dataKey="mid" name="mid" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="6 3" />
|
|
||||||
<Line yAxisId="price" type="monotone" dataKey="price" name="price" stroke="#f59e0b" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="4 2" />
|
|
||||||
</ComposedChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SignalsView({ strategy }: { strategy: string }) {
|
|
||||||
const { isLoggedIn, loading } = useAuth();
|
|
||||||
const [symbol, setSymbol] = useState<Symbol>("ETH");
|
|
||||||
const [minutes, setMinutes] = useState(240);
|
|
||||||
|
|
||||||
if (loading) return <div className="flex items-center justify-center h-64 text-slate-400">加载中...</div>;
|
|
||||||
if (!isLoggedIn) return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
||||||
<div className="text-5xl">🔒</div>
|
|
||||||
<p className="text-slate-600 font-medium">请先登录查看信号数据</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Link href="/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm">登录</Link>
|
|
||||||
<Link href="/register" className="border border-slate-300 text-slate-600 px-4 py-2 rounded-lg text-sm">注册</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-bold text-slate-900">⚡ 信号引擎 V5.3</h1>
|
|
||||||
<p className="text-slate-500 text-[10px]">
|
|
||||||
四层评分 55/25/15/5 · ALT双轨 + BTC gate-control ·
|
|
||||||
{symbol === "BTC" ? " 🔵 BTC轨(gate-control)" : " 🟣 ALT轨(ETH/XRP/SOL)"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{(["BTC", "ETH", "XRP", "SOL"] as Symbol[]).map(s => (
|
|
||||||
<button key={s} onClick={() => setSymbol(s)}
|
|
||||||
className={`px-3 py-1 rounded-lg border text-xs font-medium transition-colors ${symbol === s ? (s === "BTC" ? "bg-amber-500 text-white border-amber-500" : "bg-blue-600 text-white border-blue-600") : "border-slate-200 text-slate-600 hover:border-blue-400"}`}>
|
|
||||||
{s}{s === "BTC" ? " 🔵" : ""}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IndicatorCards symbol={symbol} strategy={strategy} />
|
|
||||||
<SignalHistory symbol={symbol} strategy={strategy} />
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">CVD三轨 + 币价</h3>
|
|
||||||
<p className="text-[10px] text-slate-400">蓝=fast(30m) · 紫=mid(4h) · 橙=价格</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{WINDOWS.map(w => (
|
|
||||||
<button key={w.value} onClick={() => setMinutes(w.value)}
|
|
||||||
className={`px-2 py-1 rounded border text-xs transition-colors ${minutes === w.value ? "bg-slate-800 text-white border-slate-800" : "border-slate-200 text-slate-500 hover:border-slate-400"}`}>
|
|
||||||
{w.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<CVDChart symbol={symbol} minutes={minutes} strategy={strategy} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
|
||||||
<div className="px-3 py-2 border-b border-slate-100">
|
|
||||||
<h3 className="font-semibold text-slate-800 text-xs">📖 V5.3 双轨信号说明</h3>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-2 space-y-2 text-[11px] text-slate-600">
|
|
||||||
<div className="p-2 bg-purple-50 rounded-lg border border-purple-100">
|
|
||||||
<span className="font-bold text-purple-800">🟣 ALT轨(ETH/XRP/SOL)— 四层线性评分</span>
|
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
<p><span className="font-semibold">1️⃣ 方向层(55分)</span> — CVD共振30分(fast+mid同向)+ P99大单对齐20分 + 加速奖励5分。删除独立确认层,解决CVD双重计分问题。</p>
|
|
||||||
<p><span className="font-semibold">2️⃣ 拥挤层(25分)</span> — LSR反向拥挤15分(散户过度拥挤=信号)+ 大户持仓方向10分。</p>
|
|
||||||
<p><span className="font-semibold">3️⃣ 环境层(15分)</span> — OI变化率,新资金进场vs撤离,判断趋势持续性。</p>
|
|
||||||
<p><span className="font-semibold">4️⃣ 辅助层(5分)</span> — Coinbase Premium,美系机构动向。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-2 bg-amber-50 rounded-lg border border-amber-100">
|
|
||||||
<span className="font-bold text-amber-800">🔵 BTC轨 — Gate-Control逻辑</span>
|
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
<p><span className="font-semibold">波动率门控</span>:ATR/Price ≥ 0.2%,低波动行情拒绝开仓</p>
|
|
||||||
<p><span className="font-semibold">OBI否决</span>:订单簿失衡超阈值且与信号方向冲突时否决(实时100ms)</p>
|
|
||||||
<p><span className="font-semibold">期现背离否决</span>:spot与perp价差超阈值时否决(实时1s)</p>
|
|
||||||
<p><span className="font-semibold">巨鲸CVD</span>:>$100k成交额净CVD,15分钟滚动窗口实时计算</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-1 border-t border-slate-100">
|
|
||||||
<span className="text-blue-600 font-medium">档位:</span><75不开仓 · 75-84标准 · ≥85加仓 · 冷却10分钟
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,534 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { authFetch } from "@/lib/auth";
|
|
||||||
import { useAuth } from "@/lib/auth";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowLeft, Save, Info } from "lucide-react";
|
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
||||||
export interface StrategyFormData {
|
|
||||||
display_name: string;
|
|
||||||
symbol: string;
|
|
||||||
direction: string;
|
|
||||||
initial_balance: number;
|
|
||||||
cvd_fast_window: string;
|
|
||||||
cvd_slow_window: string;
|
|
||||||
weight_direction: number;
|
|
||||||
weight_env: number;
|
|
||||||
weight_aux: number;
|
|
||||||
weight_momentum: number;
|
|
||||||
entry_score: number;
|
|
||||||
// 门1 波动率
|
|
||||||
gate_vol_enabled: boolean;
|
|
||||||
vol_atr_pct_min: number;
|
|
||||||
// 门2 CVD共振
|
|
||||||
gate_cvd_enabled: boolean;
|
|
||||||
// 门3 鲸鱼否决
|
|
||||||
gate_whale_enabled: boolean;
|
|
||||||
whale_usd_threshold: number;
|
|
||||||
whale_flow_pct: number;
|
|
||||||
// 门4 OBI否决
|
|
||||||
gate_obi_enabled: boolean;
|
|
||||||
obi_threshold: number;
|
|
||||||
// 门5 期现背离
|
|
||||||
gate_spot_perp_enabled: boolean;
|
|
||||||
spot_perp_threshold: number;
|
|
||||||
sl_atr_multiplier: number;
|
|
||||||
tp1_ratio: number;
|
|
||||||
tp2_ratio: number;
|
|
||||||
timeout_minutes: number;
|
|
||||||
flip_threshold: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_FORM: StrategyFormData = {
|
|
||||||
display_name: "",
|
|
||||||
symbol: "BTCUSDT",
|
|
||||||
direction: "both",
|
|
||||||
initial_balance: 10000,
|
|
||||||
cvd_fast_window: "30m",
|
|
||||||
cvd_slow_window: "4h",
|
|
||||||
weight_direction: 55,
|
|
||||||
weight_env: 25,
|
|
||||||
weight_aux: 15,
|
|
||||||
weight_momentum: 5,
|
|
||||||
entry_score: 75,
|
|
||||||
gate_vol_enabled: true,
|
|
||||||
vol_atr_pct_min: 0.002,
|
|
||||||
gate_cvd_enabled: true,
|
|
||||||
gate_whale_enabled: true,
|
|
||||||
whale_usd_threshold: 50000,
|
|
||||||
whale_flow_pct: 0.5,
|
|
||||||
gate_obi_enabled: true,
|
|
||||||
obi_threshold: 0.35,
|
|
||||||
gate_spot_perp_enabled: false,
|
|
||||||
spot_perp_threshold: 0.005,
|
|
||||||
sl_atr_multiplier: 1.5,
|
|
||||||
tp1_ratio: 0.75,
|
|
||||||
tp2_ratio: 1.5,
|
|
||||||
timeout_minutes: 240,
|
|
||||||
flip_threshold: 80,
|
|
||||||
description: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Per-symbol 推荐值 ────────────────────────────────────────────────────────
|
|
||||||
// 来自 v53.json symbol_gates,与 signal_engine.py 默认值保持一致
|
|
||||||
export const SYMBOL_RECOMMENDED: Record<string, Partial<StrategyFormData>> = {
|
|
||||||
BTCUSDT: {
|
|
||||||
vol_atr_pct_min: 0.002, // ATR需>价格0.2%
|
|
||||||
whale_usd_threshold: 100000, // 鲸鱼单>10万USD
|
|
||||||
whale_flow_pct: 0.5, // BTC鲸鱼流量>50%才否决
|
|
||||||
obi_threshold: 0.30, // OBI阈值宽松(BTC流动性好)
|
|
||||||
spot_perp_threshold: 0.003, // 期现溢价<0.3%
|
|
||||||
},
|
|
||||||
ETHUSDT: {
|
|
||||||
vol_atr_pct_min: 0.003, // ETH波动需更大
|
|
||||||
whale_usd_threshold: 50000,
|
|
||||||
whale_flow_pct: 0.5,
|
|
||||||
obi_threshold: 0.35,
|
|
||||||
spot_perp_threshold: 0.005,
|
|
||||||
},
|
|
||||||
SOLUSDT: {
|
|
||||||
vol_atr_pct_min: 0.004, // SOL波动更剧烈,需更高阈值
|
|
||||||
whale_usd_threshold: 20000,
|
|
||||||
whale_flow_pct: 0.5,
|
|
||||||
obi_threshold: 0.45, // SOL OBI噪音多,需更严
|
|
||||||
spot_perp_threshold: 0.008,
|
|
||||||
},
|
|
||||||
XRPUSDT: {
|
|
||||||
vol_atr_pct_min: 0.0025,
|
|
||||||
whale_usd_threshold: 30000,
|
|
||||||
whale_flow_pct: 0.5,
|
|
||||||
obi_threshold: 0.40,
|
|
||||||
spot_perp_threshold: 0.006,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function applySymbolDefaults(form: StrategyFormData, symbol: string): StrategyFormData {
|
|
||||||
const rec = SYMBOL_RECOMMENDED[symbol] || SYMBOL_RECOMMENDED["BTCUSDT"];
|
|
||||||
return { ...form, symbol, ...rec };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Helper Components ────────────────────────────────────────────────────────
|
|
||||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1 mb-1">
|
|
||||||
<span className="text-xs font-medium text-slate-600">{label}</span>
|
|
||||||
{hint && (
|
|
||||||
<span className="group relative">
|
|
||||||
<Info size={11} className="text-slate-400 cursor-help" />
|
|
||||||
<span className="hidden group-hover:block absolute left-4 top-0 z-10 w-48 text-[10px] bg-slate-800 text-white rounded px-2 py-1">
|
|
||||||
{hint}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NumberInput({
|
|
||||||
value, onChange, min, max, step = 1, disabled = false
|
|
||||||
}: {
|
|
||||||
value: number; onChange: (v: number) => void;
|
|
||||||
min: number; max: number; step?: number; disabled?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = parseFloat(e.target.value);
|
|
||||||
if (!isNaN(v)) onChange(Math.min(max, Math.max(min, v)));
|
|
||||||
}}
|
|
||||||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300 disabled:bg-slate-50 disabled:text-slate-400"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectInput({
|
|
||||||
value, onChange, options
|
|
||||||
}: {
|
|
||||||
value: string; onChange: (v: string) => void;
|
|
||||||
options: { label: string; value: string }[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300 bg-white"
|
|
||||||
>
|
|
||||||
{options.map((o) => (
|
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GateRow({
|
|
||||||
label, hint, enabled, onToggle, children
|
|
||||||
}: {
|
|
||||||
label: string; hint: string; enabled: boolean; onToggle: () => void; children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={`border rounded-lg p-3 transition-colors ${enabled ? "border-blue-200 bg-blue-50/30" : "border-slate-200 bg-slate-50/50"}`}>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-xs font-medium text-slate-700">{label}</span>
|
|
||||||
<span className="group relative">
|
|
||||||
<Info size={11} className="text-slate-400 cursor-help" />
|
|
||||||
<span className="hidden group-hover:block absolute left-4 top-0 z-10 w-52 text-[10px] bg-slate-800 text-white rounded px-2 py-1">
|
|
||||||
{hint}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onToggle}
|
|
||||||
className={`relative inline-flex items-center w-10 h-5 rounded-full transition-colors flex-shrink-0 ${enabled ? "bg-blue-500" : "bg-slate-300"}`}
|
|
||||||
>
|
|
||||||
<span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${enabled ? "translate-x-5" : "translate-x-0"}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{enabled && <div className="mt-2">{children}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Main Form Component ──────────────────────────────────────────────────────
|
|
||||||
interface StrategyFormProps {
|
|
||||||
mode: "create" | "edit";
|
|
||||||
initialData: StrategyFormData;
|
|
||||||
strategyId?: string;
|
|
||||||
onSuccess?: (id: string) => void;
|
|
||||||
isBalanceEditable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function StrategyForm({ mode, initialData, strategyId, onSuccess, isBalanceEditable = true }: StrategyFormProps) {
|
|
||||||
useAuth();
|
|
||||||
const router = useRouter();
|
|
||||||
const [form, setForm] = useState<StrategyFormData>(initialData);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const set = <K extends keyof StrategyFormData>(key: K, value: StrategyFormData[K]) => {
|
|
||||||
setForm((prev) => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const weightsTotal = form.weight_direction + form.weight_env + form.weight_aux + form.weight_momentum;
|
|
||||||
const weightsOk = weightsTotal === 100;
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!weightsOk) {
|
|
||||||
setError(`权重合计必须等于 100,当前为 ${weightsTotal}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!form.display_name.trim()) {
|
|
||||||
setError("策略名称不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError("");
|
|
||||||
setSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload: Record<string, unknown> = { ...form };
|
|
||||||
if (!payload.description) delete payload.description;
|
|
||||||
|
|
||||||
let res: Response;
|
|
||||||
if (mode === "create") {
|
|
||||||
res = await authFetch("/api/strategies", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Edit: only send changed fields (excluding symbol & initial_balance)
|
|
||||||
const { symbol: _s, initial_balance: _b, ...editPayload } = payload;
|
|
||||||
void _s; void _b;
|
|
||||||
res = await authFetch(`/api/strategies/${strategyId}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(editPayload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json();
|
|
||||||
throw new Error(err.detail?.[0]?.msg || err.detail || "请求失败");
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
const newId = data.strategy?.strategy_id || strategyId || "";
|
|
||||||
if (onSuccess) onSuccess(newId);
|
|
||||||
else router.push(`/strategy-plaza/${newId}`);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setError(err instanceof Error ? err.message : "未知错误");
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* ── 基础信息 ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">基础信息</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="策略名称" hint="自由命名,最多50字符" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.display_name}
|
|
||||||
onChange={(e) => set("display_name", e.target.value)}
|
|
||||||
placeholder="例如:我的BTC激进策略"
|
|
||||||
maxLength={50}
|
|
||||||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="交易对" />
|
|
||||||
<SelectInput
|
|
||||||
value={form.symbol}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (mode === "create") {
|
|
||||||
// 新建时切换币种自动填入推荐值
|
|
||||||
setForm((prev) => applySymbolDefaults(prev, v));
|
|
||||||
} else {
|
|
||||||
set("symbol", v);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{ label: "BTC/USDT", value: "BTCUSDT" },
|
|
||||||
{ label: "ETH/USDT", value: "ETHUSDT" },
|
|
||||||
{ label: "SOL/USDT", value: "SOLUSDT" },
|
|
||||||
{ label: "XRP/USDT", value: "XRPUSDT" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="交易方向" />
|
|
||||||
<SelectInput
|
|
||||||
value={form.direction}
|
|
||||||
onChange={(v) => set("direction", v)}
|
|
||||||
options={[
|
|
||||||
{ label: "多空双向", value: "both" },
|
|
||||||
{ label: "只做多", value: "long_only" },
|
|
||||||
{ label: "只做空", value: "short_only" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="初始资金 (USDT)" hint="最少 1,000 USDT" />
|
|
||||||
<NumberInput
|
|
||||||
value={form.initial_balance}
|
|
||||||
onChange={(v) => set("initial_balance", v)}
|
|
||||||
min={1000}
|
|
||||||
max={1000000}
|
|
||||||
step={1000}
|
|
||||||
disabled={mode === "edit" && !isBalanceEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<FieldLabel label="策略描述(可选)" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.description}
|
|
||||||
onChange={(e) => set("description", e.target.value)}
|
|
||||||
placeholder="简短描述这个策略的思路"
|
|
||||||
maxLength={200}
|
|
||||||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── CVD 窗口 ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">CVD 窗口</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="快线 CVD" hint="短周期CVD,捕捉近期买卖力量" />
|
|
||||||
<SelectInput
|
|
||||||
value={form.cvd_fast_window}
|
|
||||||
onChange={(v) => set("cvd_fast_window", v)}
|
|
||||||
options={[
|
|
||||||
{ label: "5m(超短线)", value: "5m" },
|
|
||||||
{ label: "15m(短线)", value: "15m" },
|
|
||||||
{ label: "30m(中短线)", value: "30m" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="慢线 CVD" hint="长周期CVD,反映趋势方向" />
|
|
||||||
<SelectInput
|
|
||||||
value={form.cvd_slow_window}
|
|
||||||
onChange={(v) => set("cvd_slow_window", v)}
|
|
||||||
options={[
|
|
||||||
{ label: "30m", value: "30m" },
|
|
||||||
{ label: "1h(推荐)", value: "1h" },
|
|
||||||
{ label: "4h(趋势)", value: "4h" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 四层权重 ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700">四层评分权重</h3>
|
|
||||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${weightsOk ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-600"}`}>
|
|
||||||
合计: {weightsTotal}/100
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="方向得分权重" hint="CVD方向信号的权重,建议50-65" />
|
|
||||||
<NumberInput value={form.weight_direction} onChange={(v) => set("weight_direction", v)} min={10} max={80} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="环境得分权重" hint="市场环境(OI/FR/资金费率)的权重" />
|
|
||||||
<NumberInput value={form.weight_env} onChange={(v) => set("weight_env", v)} min={5} max={60} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="辅助因子权重" hint="清算/现货溢价等辅助信号的权重" />
|
|
||||||
<NumberInput value={form.weight_aux} onChange={(v) => set("weight_aux", v)} min={0} max={40} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="动量权重" hint="短期价格动量信号的权重" />
|
|
||||||
<NumberInput value={form.weight_momentum} onChange={(v) => set("weight_momentum", v)} min={0} max={20} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 入场阈值 ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">入场阈值</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="入场最低总分" hint="信号总分超过此值才开仓,默认75,越高越严格" />
|
|
||||||
<NumberInput value={form.entry_score} onChange={(v) => set("entry_score", v)} min={60} max={95} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 五道 Gate ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">过滤门控(Gate)</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* 推荐值提示 */}
|
|
||||||
{(() => {
|
|
||||||
const rec = SYMBOL_RECOMMENDED[form.symbol];
|
|
||||||
if (!rec) return null;
|
|
||||||
return (
|
|
||||||
<div className="text-xs text-slate-500 bg-slate-50 rounded-lg px-3 py-2">
|
|
||||||
📌 {form.symbol.replace("USDT","")} 推荐值:ATR%≥{((rec.vol_atr_pct_min??0)*100).toFixed(2)}%,鲸鱼≥${((rec.whale_usd_threshold??50000)/1000).toFixed(0)}k,OBI阈值{rec.obi_threshold}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
<GateRow
|
|
||||||
label="门1:波动率门 (ATR%价格)"
|
|
||||||
hint="ATR占价格比例低于阈值时不开仓,过滤低波动时段"
|
|
||||||
enabled={form.gate_vol_enabled}
|
|
||||||
onToggle={() => set("gate_vol_enabled", !form.gate_vol_enabled)}
|
|
||||||
>
|
|
||||||
<FieldLabel label="ATR% 最低阈值" hint={`推荐:BTC=0.002, ETH=0.003, SOL=0.004, XRP=0.0025(当前${form.symbol.replace("USDT","")}推荐${SYMBOL_RECOMMENDED[form.symbol]?.vol_atr_pct_min??0.002})`} />
|
|
||||||
<NumberInput value={form.vol_atr_pct_min} onChange={(v) => set("vol_atr_pct_min", v)} min={0.0001} max={0.02} step={0.0005} />
|
|
||||||
</GateRow>
|
|
||||||
<GateRow
|
|
||||||
label="门2:CVD共振方向门"
|
|
||||||
hint="要求快慢两条CVD同向(双线共振),否则视为无方向不开仓"
|
|
||||||
enabled={form.gate_cvd_enabled}
|
|
||||||
onToggle={() => set("gate_cvd_enabled", !form.gate_cvd_enabled)}
|
|
||||||
/>
|
|
||||||
<GateRow
|
|
||||||
label="门3:鲸鱼否决门"
|
|
||||||
hint="检测大单方向:ALT用USD金额阈值,BTC用鲸鱼CVD流量比例"
|
|
||||||
enabled={form.gate_whale_enabled}
|
|
||||||
onToggle={() => set("gate_whale_enabled", !form.gate_whale_enabled)}
|
|
||||||
>
|
|
||||||
<FieldLabel label="大单USD阈值 (ALT)" hint={`推荐:BTC=10万, ETH=5万, SOL=2万, XRP=3万(当前推荐$${((SYMBOL_RECOMMENDED[form.symbol]?.whale_usd_threshold??50000)/1000).toFixed(0)}k)`} />
|
|
||||||
<NumberInput value={form.whale_usd_threshold} onChange={(v) => set("whale_usd_threshold", v)} min={1000} max={1000000} step={5000} />
|
|
||||||
<FieldLabel label="鲸鱼CVD流量阈值 (BTC)" hint="0~1,BTC鲸鱼净方向比例超过此值才否决,推荐0.5" />
|
|
||||||
<NumberInput value={form.whale_flow_pct} onChange={(v) => set("whale_flow_pct", v)} min={0} max={1} step={0.05} />
|
|
||||||
</GateRow>
|
|
||||||
<GateRow
|
|
||||||
label="门4:订单簿失衡门 (OBI)"
|
|
||||||
hint="要求订单簿方向与信号一致,OBI绝对值超过阈值才否决反向信号"
|
|
||||||
enabled={form.gate_obi_enabled}
|
|
||||||
onToggle={() => set("gate_obi_enabled", !form.gate_obi_enabled)}
|
|
||||||
>
|
|
||||||
<FieldLabel label="OBI 否决阈值" hint={`推荐:BTC=0.30(宽松), ETH=0.35, SOL=0.45(严格)(当前推荐${SYMBOL_RECOMMENDED[form.symbol]?.obi_threshold??0.35})`} />
|
|
||||||
<NumberInput value={form.obi_threshold} onChange={(v) => set("obi_threshold", v)} min={0.1} max={0.9} step={0.05} />
|
|
||||||
</GateRow>
|
|
||||||
<GateRow
|
|
||||||
label="门5:期现背离门"
|
|
||||||
hint="要求现货与永续溢价低于阈值,过滤套利异常时段(默认关闭)"
|
|
||||||
enabled={form.gate_spot_perp_enabled}
|
|
||||||
onToggle={() => set("gate_spot_perp_enabled", !form.gate_spot_perp_enabled)}
|
|
||||||
>
|
|
||||||
<FieldLabel label="溢价率阈值" hint={`推荐:BTC=0.003, ETH=0.005, SOL=0.008(当前推荐${SYMBOL_RECOMMENDED[form.symbol]?.spot_perp_threshold??0.005})`} />
|
|
||||||
<NumberInput value={form.spot_perp_threshold} onChange={(v) => set("spot_perp_threshold", v)} min={0.0005} max={0.01} step={0.0005} />
|
|
||||||
</GateRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 风控参数 ── */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
|
||||||
<h3 className="text-sm font-semibold text-slate-700 mb-3">风控参数</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="SL 宽度 (× ATR)" hint="止损距离 = SL倍数 × ATR,默认1.5" />
|
|
||||||
<NumberInput value={form.sl_atr_multiplier} onChange={(v) => set("sl_atr_multiplier", v)} min={0.5} max={3.0} step={0.1} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="TP1 目标 (× RD)" hint="第一止盈 = TP1倍数 × 风险距离,默认0.75" />
|
|
||||||
<NumberInput value={form.tp1_ratio} onChange={(v) => set("tp1_ratio", v)} min={0.3} max={2.0} step={0.05} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="TP2 目标 (× RD)" hint="第二止盈 = TP2倍数 × 风险距离,默认1.5" />
|
|
||||||
<NumberInput value={form.tp2_ratio} onChange={(v) => set("tp2_ratio", v)} min={0.5} max={4.0} step={0.1} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="超时时间 (分钟)" hint="持仓超过此时间自动平仓,默认240min" />
|
|
||||||
<NumberInput value={form.timeout_minutes} onChange={(v) => set("timeout_minutes", v)} min={30} max={1440} step={30} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FieldLabel label="反转平仓阈值 (分)" hint="对手方向信号分≥此值时平仓,默认80" />
|
|
||||||
<NumberInput value={form.flip_threshold} onChange={(v) => set("flip_threshold", v)} min={60} max={95} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Error & Submit ── */}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg px-4 py-2 text-sm text-red-600">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Link
|
|
||||||
href="/strategy-plaza"
|
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-slate-200 text-sm text-slate-600 hover:bg-slate-50 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={15} />
|
|
||||||
返回
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={submitting || !weightsOk}
|
|
||||||
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
<Save size={15} />
|
|
||||||
{submitting ? "保存中..." : mode === "create" ? "创建并启动" : "保存参数"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
清理 signal_indicators 中 strategy_id 对应 symbol 不一致的历史脏数据。
|
|
||||||
|
|
||||||
默认 dry-run;加 --apply 才会真正删除。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
|
|
||||||
try:
|
|
||||||
import psycopg2
|
|
||||||
except Exception:
|
|
||||||
psycopg2 = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
if psycopg2 is None:
|
|
||||||
raise RuntimeError("缺少 psycopg2 依赖,请先安装:pip install psycopg2-binary")
|
|
||||||
host = os.getenv("PG_HOST") or os.getenv("DB_HOST") or "127.0.0.1"
|
|
||||||
port = int(os.getenv("PG_PORT") or os.getenv("DB_PORT") or 5432)
|
|
||||||
dbname = os.getenv("PG_DB") or os.getenv("DB_NAME") or "arb_engine"
|
|
||||||
user = os.getenv("PG_USER") or os.getenv("DB_USER") or "arb"
|
|
||||||
password = os.getenv("PG_PASS") or os.getenv("DB_PASS") or "arb_engine_2026"
|
|
||||||
return psycopg2.connect(host=host, port=port, dbname=dbname, user=user, password=password)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--apply", action="store_true", help="执行删除")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
conn = get_conn()
|
|
||||||
conn.autocommit = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS mismatch_rows,
|
|
||||||
to_timestamp(min(si.ts)/1000.0) AS first_ts_utc,
|
|
||||||
to_timestamp(max(si.ts)/1000.0) AS last_ts_utc
|
|
||||||
FROM signal_indicators si
|
|
||||||
JOIN strategies s ON s.strategy_id = si.strategy_id
|
|
||||||
WHERE si.symbol <> s.symbol
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
mismatch_rows, first_ts, last_ts = cur.fetchone()
|
|
||||||
print(f"mismatch_rows={mismatch_rows}, range={first_ts} -> {last_ts}")
|
|
||||||
|
|
||||||
if mismatch_rows == 0:
|
|
||||||
print("无需清理")
|
|
||||||
conn.rollback()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if not args.apply:
|
|
||||||
print("dry-run 模式,仅输出统计;加 --apply 执行删除")
|
|
||||||
conn.rollback()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
DELETE FROM signal_indicators si
|
|
||||||
USING strategies s
|
|
||||||
WHERE si.strategy_id = s.strategy_id
|
|
||||||
AND si.symbol <> s.symbol
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
print(f"deleted_rows={cur.rowcount}")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
return 0
|
|
||||||
except Exception:
|
|
||||||
conn.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@ -239,7 +239,7 @@ def replay_trade(cur, tid, symbol, direction, strategy, entry_ts, atr):
|
|||||||
|
|
||||||
|
|
||||||
def main(dry_run=False):
|
def main(dry_run=False):
|
||||||
conn = psycopg2.connect(host='127.0.0.1', dbname='arb_engine', user='arb', password='arb_engine_2026')
|
conn = psycopg2.connect(host='10.106.0.3', dbname='arb_engine', user='arb', password='arb_engine_2026')
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
|
|||||||
196
signal-engine.log
Normal file
196
signal-engine.log
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
2026-03-01 23:06:35,118 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-01 23:06:37,990 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载425,636条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:06:41,185 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载474,707条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:06:41,583 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载63,246条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:06:42,041 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载70,472条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:06:42,041 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-01 23:06:42,270 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=65362.8
|
||||||
|
2026-03-01 23:06:42,270 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=65362.8
|
||||||
|
2026-03-01 23:06:42,726 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=82.8
|
||||||
|
2026-03-01 23:06:42,726 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=92 price=82.8
|
||||||
|
2026-03-01 23:07:14,105 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-01 23:14:12,791 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-01 23:14:15,845 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载446,015条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:14:19,122 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载486,995条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:14:19,543 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载64,234条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:14:20,034 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载72,230条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:14:20,034 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-01 23:14:20,277 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=65517.8
|
||||||
|
2026-03-01 23:14:20,277 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=65517.8
|
||||||
|
2026-03-01 23:14:20,717 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.2
|
||||||
|
2026-03-01 23:14:20,717 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=97 price=83.2
|
||||||
|
2026-03-01 23:14:52,024 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-01 23:23:48,162 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-01 23:23:51,129 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载454,017条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:23:54,321 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载489,974条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:23:54,744 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载65,115条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:23:55,256 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载72,235条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:23:55,257 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-01 23:23:55,497 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=95 price=65616.4
|
||||||
|
2026-03-01 23:23:55,497 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=95 price=65616.4
|
||||||
|
2026-03-01 23:23:55,960 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.4
|
||||||
|
2026-03-01 23:23:55,961 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=92 price=83.4
|
||||||
|
2026-03-01 23:24:27,328 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-01 23:32:18,630 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: SHORT score=82 price=1.4
|
||||||
|
2026-03-01 23:32:18,630 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: SHORT score=82 price=1.4
|
||||||
|
2026-03-01 23:32:18,652 [INFO] signal-engine: [XRPUSDT] 📝 模拟开仓: SHORT @ 1.35 score=82 tier=standard strategy=v52_8signals TP1=1.34 TP2=1.33 SL=1.36
|
||||||
|
2026-03-01 23:34:08,143 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=65781.9
|
||||||
|
2026-03-01 23:34:08,144 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=85 price=65781.9
|
||||||
|
2026-03-01 23:34:08,165 [INFO] signal-engine: [BTCUSDT] 📝 模拟开仓: LONG @ 65781.85 score=85 tier=heavy strategy=v52_8signals TP1=66122.21 TP2=66547.66 SL=65271.31
|
||||||
|
2026-03-01 23:34:08,664 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=83.5
|
||||||
|
2026-03-01 23:34:08,664 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=83.5
|
||||||
|
2026-03-01 23:34:08,686 [INFO] signal-engine: [SOLUSDT] 📝 模拟开仓: LONG @ 83.54 score=87 tier=heavy strategy=v52_8signals TP1=84.10 TP2=84.80 SL=82.70
|
||||||
|
2026-03-01 23:35:46,904 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-01 23:35:50,048 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载459,950条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:35:53,307 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载486,670条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:35:53,760 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载64,640条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:35:54,229 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载71,505条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:35:54,229 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-01 23:35:54,463 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=95 price=65794.8
|
||||||
|
2026-03-01 23:35:54,464 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=95 price=65794.8
|
||||||
|
2026-03-01 23:35:54,823 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: SHORT score=92 price=1.4
|
||||||
|
2026-03-01 23:35:54,823 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: SHORT score=92 price=1.4
|
||||||
|
2026-03-01 23:35:54,901 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.6
|
||||||
|
2026-03-01 23:35:54,901 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=92 price=83.6
|
||||||
|
2026-03-01 23:36:26,291 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-01 23:45:20,597 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: SHORT score=85 price=1937.2
|
||||||
|
2026-03-01 23:45:20,598 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: SHORT score=90 price=1937.2
|
||||||
|
2026-03-01 23:45:20,621 [INFO] signal-engine: [ETHUSDT] 📝 模拟开仓: SHORT @ 1937.23 score=90 tier=heavy strategy=v52_8signals TP1=1923.04 TP2=1905.29 SL=1958.53
|
||||||
|
2026-03-01 23:46:07,484 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=65708.5
|
||||||
|
2026-03-01 23:46:07,484 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=85 price=65708.5
|
||||||
|
2026-03-01 23:46:07,850 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: SHORT score=82 price=1.3
|
||||||
|
2026-03-01 23:46:07,850 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: SHORT score=82 price=1.3
|
||||||
|
2026-03-01 23:46:07,939 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=83.5
|
||||||
|
2026-03-01 23:46:07,939 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=83.5
|
||||||
|
2026-03-01 23:51:22,298 [INFO] signal-engine: [BTCUSDT] 状态: CVD_fast=417.5 CVD_mid=3139.9 ATR=262.21 (11%) VWAP=65702.8
|
||||||
|
2026-03-01 23:51:22,298 [INFO] signal-engine: [ETHUSDT] 状态: CVD_fast=-892.3 CVD_mid=-33690.7 ATR=8.38 (11%) VWAP=1937.7
|
||||||
|
2026-03-01 23:51:22,298 [INFO] signal-engine: [XRPUSDT] 状态: CVD_fast=-517373.6 CVD_mid=-3295080.7 ATR=0.01 (11%) VWAP=1.4
|
||||||
|
2026-03-01 23:51:22,298 [INFO] signal-engine: [SOLUSDT] 状态: CVD_fast=829.6 CVD_mid=204333.9 ATR=0.39 (11%) VWAP=83.6
|
||||||
|
2026-03-01 23:55:33,622 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: SHORT score=80 price=1937.9
|
||||||
|
2026-03-01 23:55:33,622 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: SHORT score=80 price=1937.9
|
||||||
|
2026-03-01 23:56:20,475 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=65700.6
|
||||||
|
2026-03-01 23:56:20,475 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=85 price=65700.6
|
||||||
|
2026-03-01 23:56:20,867 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: SHORT score=82 price=1.4
|
||||||
|
2026-03-01 23:56:20,867 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: SHORT score=82 price=1.4
|
||||||
|
2026-03-01 23:56:20,955 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=83.6
|
||||||
|
2026-03-01 23:56:20,955 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=83.6
|
||||||
|
2026-03-01 23:57:56,842 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-01 23:58:00,054 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载469,368条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:58:03,491 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载480,675条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:58:03,940 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载64,627条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:58:04,405 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载69,923条历史数据 (窗口=4h)
|
||||||
|
2026-03-01 23:58:04,405 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-01 23:58:04,670 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=65701.1
|
||||||
|
2026-03-01 23:58:04,671 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=65701.1
|
||||||
|
2026-03-01 23:58:05,068 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.6
|
||||||
|
2026-03-01 23:58:05,069 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=92 price=83.6
|
||||||
|
2026-03-01 23:58:36,358 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-02 00:02:15,179 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: SHORT score=87 price=1.4
|
||||||
|
2026-03-02 00:02:15,180 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: SHORT score=87 price=1.4
|
||||||
|
2026-03-02 00:08:14,751 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=65785.2
|
||||||
|
2026-03-02 00:08:14,751 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=65785.2
|
||||||
|
2026-03-02 00:08:15,137 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=83.6
|
||||||
|
2026-03-02 00:08:15,138 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=82 price=83.6
|
||||||
|
2026-03-02 00:12:25,824 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=1.4
|
||||||
|
2026-03-02 00:12:25,824 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=1.4
|
||||||
|
2026-03-02 00:12:57,084 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1942.8
|
||||||
|
2026-03-02 00:12:57,085 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=95 price=1942.8
|
||||||
|
2026-03-02 00:13:28,619 [INFO] signal-engine: [BTCUSDT] 状态: CVD_fast=492.8 CVD_mid=4524.8 ATR=344.15 (100%) VWAP=65886.1
|
||||||
|
2026-03-02 00:13:28,619 [INFO] signal-engine: [ETHUSDT] 状态: CVD_fast=12835.6 CVD_mid=5292.2 ATR=10.87 (100%) VWAP=1942.9
|
||||||
|
2026-03-02 00:13:28,619 [INFO] signal-engine: [XRPUSDT] 状态: CVD_fast=561858.0 CVD_mid=1078138.6 ATR=0.01 (100%) VWAP=1.4
|
||||||
|
2026-03-02 00:13:28,620 [INFO] signal-engine: [SOLUSDT] 状态: CVD_fast=61604.1 CVD_mid=357198.7 ATR=0.51 (100%) VWAP=83.7
|
||||||
|
2026-03-02 00:18:25,810 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=65894.7
|
||||||
|
2026-03-02 00:18:25,810 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=82 price=65894.7
|
||||||
|
2026-03-02 00:18:25,829 [INFO] signal-engine: [BTCUSDT] 📝 模拟开仓: LONG @ 65894.67 score=82 tier=standard strategy=v52_8signals TP1=66258.64 TP2=66713.60 SL=65348.71
|
||||||
|
2026-03-02 00:18:26,217 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=83.7
|
||||||
|
2026-03-02 00:18:26,217 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=83.7
|
||||||
|
2026-03-02 00:18:26,235 [INFO] signal-engine: [SOLUSDT] 📝 模拟开仓: LONG @ 83.72 score=87 tier=heavy strategy=v52_8signals TP1=84.44 TP2=85.34 SL=82.65
|
||||||
|
2026-03-02 00:22:37,394 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=77 price=1.4
|
||||||
|
2026-03-02 00:22:37,395 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=77 price=1.4
|
||||||
|
2026-03-02 00:23:08,756 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=1943.5
|
||||||
|
2026-03-02 00:23:08,756 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=1943.5
|
||||||
|
2026-03-02 00:25:13,809 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-02 00:25:17,084 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载467,836条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:25:20,192 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载457,043条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:25:20,634 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载65,387条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:25:21,081 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载67,363条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:25:21,081 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-02 00:25:21,333 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=65913.4
|
||||||
|
2026-03-02 00:25:21,334 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=65913.4
|
||||||
|
2026-03-02 00:25:21,612 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1943.7
|
||||||
|
2026-03-02 00:25:21,612 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=90 price=1943.7
|
||||||
|
2026-03-02 00:25:21,693 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=1.4
|
||||||
|
2026-03-02 00:25:21,693 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=82 price=1.4
|
||||||
|
2026-03-02 00:25:21,780 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.8
|
||||||
|
2026-03-02 00:25:21,780 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=83.8
|
||||||
|
2026-03-02 00:25:53,073 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-02 00:34:24,612 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-02 00:34:27,756 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载459,517条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:34:30,633 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载438,434条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:34:31,056 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载64,941条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:34:31,510 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载66,780条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:34:31,511 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-02 00:34:31,763 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=65953.9
|
||||||
|
2026-03-02 00:34:31,763 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=65953.9
|
||||||
|
2026-03-02 00:34:32,046 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=95 price=1945.0
|
||||||
|
2026-03-02 00:34:32,046 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=87 price=1945.0
|
||||||
|
2026-03-02 00:34:32,138 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=1.4
|
||||||
|
2026-03-02 00:34:32,235 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=83.8
|
||||||
|
2026-03-02 00:34:32,236 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=84 price=83.8
|
||||||
|
2026-03-02 00:35:03,725 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-02 00:38:28,367 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=1.4
|
||||||
|
2026-03-02 00:44:45,649 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=66018.3
|
||||||
|
2026-03-02 00:44:45,649 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=66018.3
|
||||||
|
2026-03-02 00:44:45,939 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1947.1
|
||||||
|
2026-03-02 00:44:45,940 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=1947.1
|
||||||
|
2026-03-02 00:44:46,036 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=1.4
|
||||||
|
2026-03-02 00:44:46,128 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=84.0
|
||||||
|
2026-03-02 00:44:46,129 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=80 price=84.0
|
||||||
|
2026-03-02 00:48:42,855 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=1.4
|
||||||
|
2026-03-02 00:50:01,898 [INFO] signal-engine: [BTCUSDT] 状态: CVD_fast=587.6 CVD_mid=5583.3 ATR=230.66 (69%) VWAP=66328.7
|
||||||
|
2026-03-02 00:50:01,898 [INFO] signal-engine: [ETHUSDT] 状态: CVD_fast=13374.7 CVD_mid=74135.1 ATR=7.58 (69%) VWAP=1953.2
|
||||||
|
2026-03-02 00:50:01,899 [INFO] signal-engine: [XRPUSDT] 状态: CVD_fast=686323.2 CVD_mid=4294786.9 ATR=0.00 (66%) VWAP=1.4
|
||||||
|
2026-03-02 00:50:01,899 [INFO] signal-engine: [SOLUSDT] 状态: CVD_fast=159663.5 CVD_mid=524572.1 ATR=0.37 (38%) VWAP=84.4
|
||||||
|
2026-03-02 00:55:01,241 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=66391.1
|
||||||
|
2026-03-02 00:55:01,241 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=66391.1
|
||||||
|
2026-03-02 00:55:01,565 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1954.9
|
||||||
|
2026-03-02 00:55:01,565 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=1954.9
|
||||||
|
2026-03-02 00:55:01,681 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=77 price=1.4
|
||||||
|
2026-03-02 00:55:01,797 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=84.5
|
||||||
|
2026-03-02 00:55:01,797 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=84.5
|
||||||
|
2026-03-02 00:56:24,512 [INFO] signal-engine: 已加载策略配置: v51_baseline, v52_8signals
|
||||||
|
2026-03-02 00:56:27,834 [INFO] signal-engine: [BTCUSDT] 冷启动完成: 加载487,314条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:56:30,636 [INFO] signal-engine: [ETHUSDT] 冷启动完成: 加载423,049条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:56:31,186 [INFO] signal-engine: [XRPUSDT] 冷启动完成: 加载63,935条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:56:31,669 [INFO] signal-engine: [SOLUSDT] 冷启动完成: 加载67,314条历史数据 (窗口=4h)
|
||||||
|
2026-03-02 00:56:31,669 [INFO] signal-engine: === Signal Engine (PG) 启动完成 ===
|
||||||
|
2026-03-02 00:56:31,916 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=95 price=66401.1
|
||||||
|
2026-03-02 00:56:31,916 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=87.0 price=66401.1
|
||||||
|
2026-03-02 00:56:32,159 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=95 price=1955.3
|
||||||
|
2026-03-02 00:56:32,159 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=87.0 price=1955.3
|
||||||
|
2026-03-02 00:56:32,227 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=1.4
|
||||||
|
2026-03-02 00:56:32,296 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=92 price=84.6
|
||||||
|
2026-03-02 00:56:32,296 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=79.0 price=84.6
|
||||||
|
2026-03-02 00:57:03,538 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
|
||||||
|
2026-03-02 01:06:43,805 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=66469.7
|
||||||
|
2026-03-02 01:06:43,805 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83.0 price=66469.7
|
||||||
|
2026-03-02 01:06:44,079 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1959.2
|
||||||
|
2026-03-02 01:06:44,079 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=83.0 price=1959.2
|
||||||
|
2026-03-02 01:06:44,158 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=1.4
|
||||||
|
2026-03-02 01:06:44,232 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=84.8
|
||||||
|
2026-03-02 01:06:44,232 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=80.0 price=84.8
|
||||||
|
2026-03-02 01:06:44,253 [INFO] signal-engine: [SOLUSDT] 📝 模拟开仓: LONG @ 84.81 score=80.0 tier=standard strategy=v52_8signals TP1=85.30 TP2=85.91 SL=84.08
|
||||||
|
2026-03-02 01:10:55,723 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=75.0 price=1.4
|
||||||
|
2026-03-02 01:11:58,908 [INFO] signal-engine: [BTCUSDT] 状态: CVD_fast=952.8 CVD_mid=5720.3 ATR=235.00 (100%) VWAP=66586.5
|
||||||
|
2026-03-02 01:11:58,908 [INFO] signal-engine: [ETHUSDT] 状态: CVD_fast=31499.2 CVD_mid=101911.4 ATR=7.67 (100%) VWAP=1966.7
|
||||||
|
2026-03-02 01:11:58,908 [INFO] signal-engine: [XRPUSDT] 状态: CVD_fast=763930.9 CVD_mid=3518537.2 ATR=0.01 (100%) VWAP=1.4
|
||||||
|
2026-03-02 01:11:58,908 [INFO] signal-engine: [SOLUSDT] 状态: CVD_fast=173330.9 CVD_mid=509498.7 ATR=0.38 (46%) VWAP=85.1
|
||||||
|
2026-03-02 01:16:57,856 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=66677.4
|
||||||
|
2026-03-02 01:16:57,856 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=78.0 price=66677.4
|
||||||
|
2026-03-02 01:16:58,164 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=1971.5
|
||||||
|
2026-03-02 01:16:58,164 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=78.0 price=1971.5
|
||||||
|
2026-03-02 01:16:58,185 [INFO] signal-engine: [ETHUSDT] 📝 模拟开仓: LONG @ 1971.50 score=78.0 tier=standard strategy=v52_8signals TP1=1984.82 TP2=2001.47 SL=1951.52
|
||||||
|
2026-03-02 01:16:58,264 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=77 price=1.4
|
||||||
|
2026-03-02 01:16:58,346 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=85.3
|
||||||
|
2026-03-02 01:16:58,346 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=75.0 price=85.3
|
||||||
|
2026-03-02 01:16:58,368 [INFO] signal-engine: [SOLUSDT] 📝 模拟开仓: LONG @ 85.32 score=75.0 tier=standard strategy=v52_8signals TP1=85.98 TP2=86.80 SL=84.34
|
||||||
Loading…
Reference in New Issue
Block a user