- Sidebar: 信号/模拟盘 section headers - Three paper trade entries: 全部持仓, V5.1模拟盘, V5.2模拟盘 (NEW badge) - Paper page reads strategy from URL query params - Suspense boundary for useSearchParams
225 lines
7.4 KiB
Markdown
225 lines
7.4 KiB
Markdown
# 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
|