Add Hyperliquid fee tier selector — 7 VIP levels × 7 staking tiers
config/fee_tiers.py: complete Hyperliquid fee schedule with perps and spot
base rates plus staking discount multipliers. effective_rate() computes
the actual fee after staking discount. get_perp_fees() returns the
effective rate for a given VIP tier, staking tier, and fee model.
Backtest runner: added --fee-tier (0-6) and --staking-tier flags.
Regenerated all 12 backtests at VIP 0 baseline. Runner now shows fee tier
info at startup.
Server: /api/backtest/{name}/recalc endpoint accepts ?fee_tier=X&staking_tier=Y
and returns recalculated PnL with the new fee structure. On-the-fly
recalculation — no need to re-run the backtest.
Dashboard: VIP tier dropdown (VIP 0-6) and staking tier dropdown
(None/Wood/Bronze/Silver/Gold/Platinum/Diamond) in backtest detail panel.
Changing either instantly recalculates PnL via the API.
Key finding: Cartea-Jaimungal goes from -5.58% net at VIP0 to +2.39% net
at VIP6+Diamond (maker rebate: exchange pays YOU -0.0024% to provide
liquidity). Fee structure completely changes strategy viability assessment.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+13
-4
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
from common.metrics import sharpe, sortino, max_drawdown, win_rate
|
from common.metrics import sharpe, sortino, max_drawdown, win_rate
|
||||||
|
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS
|
||||||
|
|
||||||
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
||||||
os.makedirs(RESULTS_DIR, exist_ok=True)
|
os.makedirs(RESULTS_DIR, exist_ok=True)
|
||||||
@@ -30,14 +31,15 @@ CONFIGS = {
|
|||||||
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
|
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
|
||||||
}
|
}
|
||||||
|
|
||||||
def simulate(key, periods=720, include_fees=True):
|
def simulate(key, periods=720, include_fees=True, fee_tier=0, staking_tier="none"):
|
||||||
# Deterministic seed per strategy (hash() is randomized per Python process)
|
# Deterministic seed per strategy (hash() is randomized per Python process)
|
||||||
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
|
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
|
||||||
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
|
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
|
||||||
"cartea":51,"queue_imb":52,"gueant":53}
|
"cartea":51,"queue_imb":52,"gueant":53}
|
||||||
random.seed(_fixed_seeds.get(key, 42))
|
random.seed(_fixed_seeds.get(key, 42))
|
||||||
cfg = CONFIGS[key]
|
cfg = CONFIGS[key]
|
||||||
fee_rate = MAKER_FEE if cfg.get("fee_model") == "maker" else TAKER_FEE
|
fee_model = cfg.get("fee_model", "taker")
|
||||||
|
fee_rate = get_perp_fees(fee_tier, staking_tier, fee_model)
|
||||||
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
|
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
|
||||||
eq_gross=100.0; eq_net=100.0; curve_gross=[]; curve_net=[]; rets=[]; trades=[]
|
eq_gross=100.0; eq_net=100.0; curve_gross=[]; curve_net=[]; rets=[]; trades=[]
|
||||||
total_fees=0.0
|
total_fees=0.0
|
||||||
@@ -92,16 +94,23 @@ def main():
|
|||||||
p=argparse.ArgumentParser()
|
p=argparse.ArgumentParser()
|
||||||
p.add_argument("--strategy","-s",choices=list(CONFIGS)+["all"],default="all")
|
p.add_argument("--strategy","-s",choices=list(CONFIGS)+["all"],default="all")
|
||||||
p.add_argument("--no-fees",action="store_true",help="Exclude simulated fees from PnL")
|
p.add_argument("--no-fees",action="store_true",help="Exclude simulated fees from PnL")
|
||||||
|
p.add_argument("--fee-tier",type=int,default=0,choices=range(7),help="VIP fee tier 0-6 (default: 0)")
|
||||||
|
p.add_argument("--staking-tier",default="none",choices=list(STAKING_TIERS.keys()),help="Staking discount tier (default: none)")
|
||||||
a=p.parse_args()
|
a=p.parse_args()
|
||||||
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
|
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
|
||||||
include_fees=not a.no_fees
|
include_fees=not a.no_fees
|
||||||
|
ft_info = PERPS_TIERS[a.fee_tier]
|
||||||
|
st_info = STAKING_TIERS[a.staking_tier]
|
||||||
print("="*60)
|
print("="*60)
|
||||||
print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)")
|
print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)")
|
||||||
print(f" Fees: {'INCLUDED (default)' if include_fees else 'EXCLUDED (--no-fees)'}")
|
print(f" Fee Tier: {ft_info['name']} (taker={ft_info['taker']*100:.3f}%, maker={ft_info['maker']*100:.3f}%)")
|
||||||
|
print(f" Staking: {st_info['name']} ({st_info['multiplier']*100:.0f}% multiplier)")
|
||||||
|
print(f" Effective taker: {get_perp_fees(a.fee_tier,a.staking_tier,'taker')*100:.4f}%")
|
||||||
|
print(f" Effective maker: {get_perp_fees(a.fee_tier,a.staking_tier,'maker')*100:.4f}%")
|
||||||
print("="*60)
|
print("="*60)
|
||||||
for k in keys:
|
for k in keys:
|
||||||
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
|
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
|
||||||
r=simulate(k, include_fees=include_fees); save(r)
|
r=simulate(k, include_fees=include_fees, fee_tier=a.fee_tier, staking_tier=a.staking_tier); save(r)
|
||||||
print(f" Net PnL: {r['pnl_pct']:+.2f}% | Gross: {r['pnl_gross_pct']:+.2f}% | Fees: ${r['fees_total']:.2f} | Sharpe: {r['sharpe']:.2f} | Win: {r['win_rate']:.0%}")
|
print(f" Net PnL: {r['pnl_pct']:+.2f}% | Gross: {r['pnl_gross_pct']:+.2f}% | Fees: ${r['fees_total']:.2f} | Sharpe: {r['sharpe']:.2f} | Win: {r['win_rate']:.0%}")
|
||||||
print("\n"+"="*60); print(" Results in backtests/results/"); print(" View at: https://ftdt.io/cv (Backtest tab)"); print("="*60)
|
print("\n"+"="*60); print(" Results in backtests/results/"); print(" View at: https://ftdt.io/cv (Backtest tab)"); print("="*60)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Hyperliquid fee tiers — perps and spot, base rates + staking discounts.
|
||||||
|
|
||||||
|
Source: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees
|
||||||
|
|
||||||
|
Fee = base_rate × staking_multiplier
|
||||||
|
Staking tiers are based on staked HYPE tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ── Perps fee tiers (base rates) ──
|
||||||
|
PERPS_TIERS = {
|
||||||
|
0: {"name": "VIP 0", "volume": 0, "taker": 0.00045, "maker": 0.00015},
|
||||||
|
1: {"name": "VIP 1", "volume": 5_000_000, "taker": 0.00040, "maker": 0.00012},
|
||||||
|
2: {"name": "VIP 2", "volume": 25_000_000, "taker": 0.00035, "maker": 0.00008},
|
||||||
|
3: {"name": "VIP 3", "volume": 100_000_000,"taker": 0.00030, "maker": 0.00004},
|
||||||
|
4: {"name": "VIP 4", "volume": 250_000_000,"taker": 0.00025, "maker": 0.00000},
|
||||||
|
5: {"name": "VIP 5", "volume": 750_000_000,"taker": 0.00020, "maker": -0.00002},
|
||||||
|
6: {"name": "VIP 6", "volume": 2_500_000_000,"taker":0.00015,"maker": -0.00004},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Spot fee tiers (base rates) ──
|
||||||
|
SPOT_TIERS = {
|
||||||
|
0: {"name": "VIP 0", "volume": 0, "taker": 0.00070, "maker": 0.00040},
|
||||||
|
1: {"name": "VIP 1", "volume": 100_000, "taker": 0.00060, "maker": 0.00030},
|
||||||
|
2: {"name": "VIP 2", "volume": 1_000_000, "taker": 0.00050, "maker": 0.00020},
|
||||||
|
3: {"name": "VIP 3", "volume": 10_000_000, "taker": 0.00040, "maker": 0.00010},
|
||||||
|
4: {"name": "VIP 4", "volume": 50_000_000, "taker": 0.00030, "maker": 0.00005},
|
||||||
|
5: {"name": "VIP 5", "volume": 200_000_000, "taker": 0.00020, "maker": 0.00000},
|
||||||
|
6: {"name": "VIP 6", "volume": 1_000_000_000,"taker":0.00010,"maker": -0.00005},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Staking discount multipliers ──
|
||||||
|
STAKING_TIERS = {
|
||||||
|
"none": {"name": "No Stake", "multiplier": 1.00},
|
||||||
|
"wood": {"name": "Wood", "multiplier": 0.95},
|
||||||
|
"bronze": {"name": "Bronze", "multiplier": 0.90},
|
||||||
|
"silver": {"name": "Silver", "multiplier": 0.85},
|
||||||
|
"gold": {"name": "Gold", "multiplier": 0.80},
|
||||||
|
"platinum": {"name": "Platinum", "multiplier": 0.70},
|
||||||
|
"diamond": {"name": "Diamond", "multiplier": 0.60},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def effective_rate(base_rate: float, staking_tier: str = "none") -> float:
|
||||||
|
"""Calculate effective fee rate after staking discount."""
|
||||||
|
mult = STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])["multiplier"]
|
||||||
|
return base_rate * mult
|
||||||
|
|
||||||
|
|
||||||
|
def get_perp_fees(vip_tier: int, staking_tier: str = "none", fee_model: str = "taker") -> float:
|
||||||
|
"""Get effective perp fee for a given VIP tier and staking tier."""
|
||||||
|
tier = PERPS_TIERS.get(vip_tier, PERPS_TIERS[0])
|
||||||
|
base = tier[fee_model] if fee_model in ("taker", "maker") else tier["taker"]
|
||||||
|
return effective_rate(base, staking_tier)
|
||||||
|
|
||||||
|
|
||||||
|
def get_spot_fees(vip_tier: int, staking_tier: str = "none", fee_model: str = "taker") -> float:
|
||||||
|
"""Get effective spot fee for a given VIP tier and staking tier."""
|
||||||
|
tier = SPOT_TIERS.get(vip_tier, SPOT_TIERS[0])
|
||||||
|
base = tier[fee_model] if fee_model in ("taker", "maker") else tier["taker"]
|
||||||
|
return effective_rate(base, staking_tier)
|
||||||
|
|
||||||
|
|
||||||
|
def fee_tier_from_volume(volume_14d: float, market: str = "perps") -> int:
|
||||||
|
"""Determine fee tier from 14-day rolling volume."""
|
||||||
|
tiers = PERPS_TIERS if market == "perps" else SPOT_TIERS
|
||||||
|
current = 0
|
||||||
|
for t in sorted(tiers.keys()):
|
||||||
|
if volume_14d >= tiers[t]["volume"]:
|
||||||
|
current = t
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
# ── Strategy-specific defaults (matching existing classification) ──
|
||||||
|
STRATEGY_FEE_MODELS = {
|
||||||
|
"Order Book Imbalance": "taker",
|
||||||
|
"Iceberg Detection": "taker",
|
||||||
|
"Funding Rate Arb": "taker",
|
||||||
|
"Pairs Trading": "taker",
|
||||||
|
"Avellaneda-Stoikov": "maker",
|
||||||
|
"Momentum Breakout": "taker",
|
||||||
|
"Mean Reversion": "taker",
|
||||||
|
"Hawkes OFI": "taker",
|
||||||
|
"Deep LOB": "maker",
|
||||||
|
"Cartea-Jaimungal": "maker",
|
||||||
|
"Queue Imbalance": "taker",
|
||||||
|
"Guéant Market Making": "maker",
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ from typing import Optional
|
|||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@@ -195,6 +198,65 @@ async def get_backtest(name: str):
|
|||||||
return JSONResponse({"error": "not found"}, status_code=404)
|
return JSONResponse({"error": "not found"}, status_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/backtest/{name}/recalc")
|
||||||
|
async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "none"):
|
||||||
|
"""Recalculate backtest PnL with different fee tier."""
|
||||||
|
fpath = os.path.join(BACKTEST_DIR, f"{name}.json")
|
||||||
|
if not os.path.exists(fpath):
|
||||||
|
return JSONResponse({"error": "not found"}, status_code=404)
|
||||||
|
with open(fpath) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
fee_model = STRATEGY_FEE_MODELS.get(data.get("strategy", ""), "taker")
|
||||||
|
new_fee_rate = get_perp_fees(fee_tier, staking_tier, fee_model)
|
||||||
|
|
||||||
|
# Get original gross PnL and trades
|
||||||
|
pnl_gross = data.get("pnl_gross", data.get("pnl", 0))
|
||||||
|
trades = data.get("trades", [])
|
||||||
|
|
||||||
|
# Recalculate fees with new rate
|
||||||
|
new_fees = 0.0
|
||||||
|
new_trades = []
|
||||||
|
for t in trades:
|
||||||
|
sz = t.get("size", 0)
|
||||||
|
px = t.get("price", 0)
|
||||||
|
orig_fee = t.get("fee", 0)
|
||||||
|
new_fee = sz * px * new_fee_rate * 2 # entry + exit
|
||||||
|
new_fees += new_fee
|
||||||
|
new_trades.append({**t, "fee": round(new_fee, 6),
|
||||||
|
"pnl_net": round(t.get("pnl_gross", t.get("pnl", 0)) - new_fee, 4)})
|
||||||
|
|
||||||
|
new_pnl_net = pnl_gross - new_fees
|
||||||
|
new_pnl_pct = new_pnl_net
|
||||||
|
|
||||||
|
ft = PERPS_TIERS.get(fee_tier, PERPS_TIERS[0])
|
||||||
|
st = STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])
|
||||||
|
eff_taker = get_perp_fees(fee_tier, staking_tier, "taker")
|
||||||
|
eff_maker = get_perp_fees(fee_tier, staking_tier, "maker")
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"strategy": data.get("strategy"),
|
||||||
|
"fee_tier": ft["name"],
|
||||||
|
"staking_tier": st["name"],
|
||||||
|
"effective_taker_pct": round(eff_taker * 100, 4),
|
||||||
|
"effective_maker_pct": round(eff_maker * 100, 4),
|
||||||
|
"fee_model": fee_model,
|
||||||
|
"pnl_gross": round(pnl_gross, 4),
|
||||||
|
"pnl_gross_pct": round(pnl_gross, 4),
|
||||||
|
"pnl_net": round(new_pnl_net, 4),
|
||||||
|
"pnl_net_pct": round(new_pnl_pct, 4),
|
||||||
|
"fees_total": round(new_fees, 4),
|
||||||
|
"total_trades": len(new_trades),
|
||||||
|
"equity_curve": data.get("equity_curve", []),
|
||||||
|
"trades": new_trades[-100:],
|
||||||
|
"sharpe": data.get("sharpe", 0),
|
||||||
|
"sortino": data.get("sortino", 0),
|
||||||
|
"max_dd": data.get("max_dd", 0),
|
||||||
|
"win_rate": data.get("win_rate", 0),
|
||||||
|
"num_periods": data.get("num_periods", 720),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/backtest/{name}/csv")
|
@app.get("/api/backtest/{name}/csv")
|
||||||
async def get_backtest_csv(name: str):
|
async def get_backtest_csv(name: str):
|
||||||
"""Download backtest trades as CSV."""
|
"""Download backtest trades as CSV."""
|
||||||
|
|||||||
@@ -126,10 +126,28 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
|||||||
<div class="detail-panel" id="detail-panel">
|
<div class="detail-panel" id="detail-panel">
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<h2 id="det-name">Strategy Detail</h2>
|
<h2 id="det-name">Strategy Detail</h2>
|
||||||
<div style="display:flex;align-items:center;gap:10px">
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||||
<label id="fee-toggle-wrap" style="display:none;font-size:11px;color:var(--tx);cursor:pointer;user-select:none">
|
<label id="fee-toggle-wrap" style="display:none;font-size:11px;color:var(--tx);cursor:pointer;user-select:none">
|
||||||
<input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees
|
<input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees
|
||||||
</label>
|
</label>
|
||||||
|
<select id="fee-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
|
||||||
|
<option value="0">VIP 0 (0.045/0.015%)</option>
|
||||||
|
<option value="1">VIP 1 (0.040/0.012%)</option>
|
||||||
|
<option value="2">VIP 2 (0.035/0.008%)</option>
|
||||||
|
<option value="3">VIP 3 (0.030/0.004%)</option>
|
||||||
|
<option value="4">VIP 4 (0.025/0.000%)</option>
|
||||||
|
<option value="5">VIP 5 (0.020/-0.002%)</option>
|
||||||
|
<option value="6">VIP 6 (0.015/-0.004%)</option>
|
||||||
|
</select>
|
||||||
|
<select id="stake-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
|
||||||
|
<option value="none">No Stake</option>
|
||||||
|
<option value="wood">Wood (×0.95)</option>
|
||||||
|
<option value="bronze">Bronze (×0.90)</option>
|
||||||
|
<option value="silver">Silver (×0.85)</option>
|
||||||
|
<option value="gold">Gold (×0.80)</option>
|
||||||
|
<option value="platinum">Platinum (×0.70)</option>
|
||||||
|
<option value="diamond">Diamond (×0.60)</option>
|
||||||
|
</select>
|
||||||
<a id="dl-csv" href="#" style="display:none;font-size:11px;color:var(--bl);text-decoration:none;padding:4px 10px;border:1px solid var(--ln);border-radius:5px" download>↓ CSV</a>
|
<a id="dl-csv" href="#" style="display:none;font-size:11px;color:var(--bl);text-decoration:none;padding:4px 10px;border:1px solid var(--ln);border-radius:5px" download>↓ CSV</a>
|
||||||
<button class="close-btn" onclick="closeDetail()">✕ Close</button>
|
<button class="close-btn" onclick="closeDetail()">✕ Close</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,10 +223,23 @@ function renCards(sgridId,ss,baseEq,tab,statsRowId){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════ Fee toggle ═══════════
|
// ═══════════ Fee toggle ═══════════
|
||||||
|
var currentBTName=null;
|
||||||
function toggleFees(){
|
function toggleFees(){
|
||||||
feeOn=document.getElementById('fee-toggle').checked;
|
feeOn=document.getElementById('fee-toggle').checked;
|
||||||
if(lastBTFull){renderBTDetail(lastBTFull)}
|
if(lastBTFull){renderBTDetail(lastBTFull)}
|
||||||
}
|
}
|
||||||
|
function onFeeTierChange(){
|
||||||
|
if(!currentBTName)return;
|
||||||
|
var ft=document.getElementById('fee-tier-sel').value;
|
||||||
|
var st=document.getElementById('stake-tier-sel').value;
|
||||||
|
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Recalculating with '+document.getElementById('fee-tier-sel').selectedOptions[0].text+'…</td></tr>';
|
||||||
|
fetch('/cv/api/backtest/'+encodeURIComponent(currentBTName)+'/recalc?fee_tier='+ft+'&staking_tier='+st)
|
||||||
|
.then(function(r){return r.json()}).then(function(full){
|
||||||
|
lastBTFull=full; renderBTDetail(full);
|
||||||
|
}).catch(function(e){
|
||||||
|
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--rd);padding:20px">Recalc failed: '+e.message+'</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════ Render backtest detail with fee toggle ──
|
// ═══════════ Render backtest detail with fee toggle ──
|
||||||
function renderBTDetail(full){
|
function renderBTDetail(full){
|
||||||
@@ -255,8 +286,11 @@ function openDetail(name,tab){
|
|||||||
ss=lastData.strategies||{};
|
ss=lastData.strategies||{};
|
||||||
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
|
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
|
||||||
var b=lastBT[name];
|
var b=lastBT[name];
|
||||||
|
currentBTName=b.name;
|
||||||
document.getElementById('fee-toggle-wrap').style.display='inline';
|
document.getElementById('fee-toggle-wrap').style.display='inline';
|
||||||
document.getElementById('fee-toggle').checked=true; feeOn=true;
|
document.getElementById('fee-toggle').checked=true; feeOn=true;
|
||||||
|
document.getElementById('fee-tier-sel').style.display='inline';
|
||||||
|
document.getElementById('stake-tier-sel').style.display='inline';
|
||||||
document.getElementById('dl-csv').style.display='inline';
|
document.getElementById('dl-csv').style.display='inline';
|
||||||
document.getElementById('dl-csv').href='/cv/api/backtest/'+encodeURIComponent(b.name)+'/csv';
|
document.getElementById('dl-csv').href='/cv/api/backtest/'+encodeURIComponent(b.name)+'/csv';
|
||||||
document.getElementById('det-desc').textContent='';
|
document.getElementById('det-desc').textContent='';
|
||||||
@@ -305,7 +339,7 @@ function openDetail(name,tab){
|
|||||||
setTimeout(function(){if(detChart){detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280});detChart.timeScale().fitContent()}},300);
|
setTimeout(function(){if(detChart){detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280});detChart.timeScale().fitContent()}},300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on');document.getElementById('fee-toggle-wrap').style.display='none';document.getElementById('dl-csv').style.display='none';lastBTFull=null}
|
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on');document.getElementById('fee-toggle-wrap').style.display='none';document.getElementById('fee-tier-sel').style.display='none';document.getElementById('stake-tier-sel').style.display='none';document.getElementById('dl-csv').style.display='none';lastBTFull=null;currentBTName=null}
|
||||||
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
|
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
|
||||||
|
|
||||||
// ═══════════ WebSocket + render ═══════════
|
// ═══════════ WebSocket + render ═══════════
|
||||||
|
|||||||
Reference in New Issue
Block a user