diff --git a/common/metrics.py b/common/metrics.py index 0417478..7adcee1 100644 --- a/common/metrics.py +++ b/common/metrics.py @@ -40,4 +40,5 @@ def max_drawdown(equity: list[float]) -> float: def win_rate(trades: list[dict]) -> float: if not trades: return 0.0 - return sum(1 for t in trades if t.get("pnl", 0) > 0) / len(trades) + tp = sum(1 for t in trades if (t.get("pnl_net") or t.get("pnl_gross") or t.get("pnl", 0)) > 0) + return tp / len(trades) diff --git a/dashboard/server.py b/dashboard/server.py index d8bcf88..7e5ce41 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -175,7 +175,7 @@ async def get_paper_metrics_rest(): # Backtest endpoints # ═══════════════════════════════════════════════════════════ -@app.get("/api/backtests") +@app.get("/backtests") async def list_backtests(): """List all saved backtest results.""" results = [] @@ -195,7 +195,7 @@ async def list_backtests(): "sortino": data.get("sortino", 0), "pnl_pct": data.get("pnl_pct", 0), "max_dd": data.get("max_dd", 0), - "win_rate": data.get("win_rate", 0), + "win_rate": data.get("win_rate", 0) or recalc_win_rate(data.get("trades", [])) or 0, "total_trades": data.get("total_trades", 0), }) except (json.JSONDecodeError, IOError): @@ -203,7 +203,7 @@ async def list_backtests(): return JSONResponse(results) -@app.get("/api/backtest/{name}") +@app.get("/backtest/{name}") async def get_backtest(name: str): """Get full backtest result data.""" fpath = os.path.join(BACKTEST_DIR, f"{name}.json") @@ -214,6 +214,14 @@ async def get_backtest(name: str): + +def recalc_win_rate(trades): + """Fallback win rate when stored value is 0.""" + if not trades: + return 0.0 + wins = sum(1 for t in trades if (t.get("pnl_net") or t.get("pnl_gross") or t.get("pnl", 0)) > 0) + return round(wins / len(trades), 4) if trades else 0.0 + def recalc_equity_curve(equity_curve, trades, new_fee_rate, fee_model): """Rebuild equity curve with new fee rates, preserving gross PnL.""" if not equity_curve or not trades: @@ -241,7 +249,7 @@ def recalc_equity_curve(equity_curve, trades, new_fee_rate, fee_model): new_curve.append({"t": pt_time, "v": round(pt.get("v", 0) + cum, 6)}) return new_curve -@app.get("/api/backtest/{name}/recalc") +@app.get("/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") @@ -302,12 +310,12 @@ async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "non "sharpe": data.get("sharpe", 0), "sortino": data.get("sortino", 0), "max_dd": data.get("max_dd", 0), - "win_rate": data.get("win_rate", 0), + "win_rate": data.get("win_rate", 0) or recalc_win_rate(data.get("trades", [])) or 0, "num_periods": data.get("num_periods", 720), }) -@app.get("/api/backtests/historical") +@app.get("/backtests/historical") async def list_historical_backtests(): """List historical (real data) backtest results.""" results = [] @@ -329,7 +337,7 @@ async def list_historical_backtests(): "sortino": data.get("sortino", 0), "pnl_pct": data.get("pnl_pct", 0), "max_dd": data.get("max_dd", 0), - "win_rate": data.get("win_rate", 0), + "win_rate": data.get("win_rate", 0) or recalc_win_rate(data.get("trades", [])) or 0, "total_trades": data.get("total_trades", 0), "data_source": "Hyperliquid Mainnet", }) @@ -338,7 +346,7 @@ async def list_historical_backtests(): return JSONResponse(results) -@app.get("/api/backtest/historical/{name}") +@app.get("/backtest/historical/{name}") async def get_historical_backtest(name: str): """Get full historical backtest result.""" fpath = os.path.join(HISTORICAL_DIR, f"{name}.json") @@ -348,7 +356,7 @@ async def get_historical_backtest(name: str): return JSONResponse({"error": "not found"}, status_code=404) -@app.get("/api/backtest/{name}/csv") +@app.get("/backtest/{name}/csv") async def get_backtest_csv(name: str): """Download backtest trades as CSV.""" from fastapi.responses import Response @@ -371,7 +379,7 @@ async def get_backtest_csv(name: str): ) -@app.get("/api/risk") +@app.get("/risk") async def get_risk_metrics(): """Compute risk analytics from the latest paper metrics.""" paper = read_paper_metrics() diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 406066f..e96a638 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -7,7 +7,7 @@ -
-

FTDT Quant LabProfessional Quant Dashboard

+

FTDT Quant LabPer-Strategy Dashboard

-
-
Portfolio Equity
-
$0.00
-
+
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
+ +
+
No open positions
+
No open orders
- -
- - - -
+

Strategy Detail

@@ -179,7 +134,6 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)} -
@@ -192,47 +146,13 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
- - + \ No newline at end of file diff --git a/dashboard/static/index.html.old b/dashboard/static/index.html.old new file mode 100644 index 0000000..406066f --- /dev/null +++ b/dashboard/static/index.html.old @@ -0,0 +1,514 @@ + + + + + +FTDT Quant Lab — Professional Dashboard + + + + + + +
+
+

FTDT Quant LabProfessional Quant Dashboard

+
+
+
Portfolio Equity
+
$0.00
+
+
+
+ +
+ + + + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + + + + + +
+
+
+

Strategy Detail

+
+ + + + + +
+
+
+
+
+
+

Trade History

+
TimeSideSizePricePnLFeeReason / Signal
+
+
+
+ + + + diff --git a/live/node.py b/live/node.py index 81acfc7..689eed4 100644 --- a/live/node.py +++ b/live/node.py @@ -26,6 +26,7 @@ log = logging.getLogger("ftdt-quant") METRICS_FILE = "/tmp/ftdt-metrics.json" TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +MAINNET_INFO = "https://api.hyperliquid.xyz/info" TOTAL_EQUITY = 898.0 RESERVE = 398.0 MAKER_FEE = 0.0002 @@ -42,6 +43,7 @@ STRATEGIES = { trades_log: list[dict] = [] equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} seen_fills: set[int] = set() btc_prices: deque = deque(maxlen=60) eth_prices: deque = deque(maxlen=60) @@ -97,7 +99,9 @@ def write_metrics(addr): "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, "reserve":RESERVE,"equity_history":equity_history[-600:], - "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running" + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running", + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] } try: with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) @@ -168,10 +172,48 @@ async def main(): addr = client.get_user_address() client.set_account_id("HYPERLIQUID-"+addr) - insts = await client.load_instrument_definitions(include_perps=True) - perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} - for inst in perps.values(): client.cache_instrument(inst) - btc_perp = perps["BTC-USD-PERP"]; eth_perp = perps["ETH-USD-PERP"] + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + # Fallback: load from raw API (mainnet) if SDK failed or returned empty + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(MAINNET_INFO, json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) prices = get_mark_prices() btc_bid, btc_ask, btc_mid = get_orderbook("BTC") @@ -201,6 +243,7 @@ async def main(): log.info(f"Tracking {len(seen_fills)} existing fills") for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] write_metrics(addr) tick=0; names=list(STRATEGIES.keys()); idx=0 @@ -233,6 +276,7 @@ async def main(): STRATEGIES[strat]["fee_paid"]+=abs(fee) if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) new_fills+=1 @@ -242,7 +286,15 @@ async def main(): # Place/refresh orders every 3-5 ticks if tick>=3 and tick%random.randint(3,5)==0: btc_bid, btc_ask, btc_mid = get_orderbook("BTC") - eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + try: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + except Exception as e: + log.debug(f"OB BTC error: {e}") + btc_bid = btc_ask = btc_mid = 0 + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 name = names[idx%7]; idx+=1; cfg=STRATEGIES[name] coin="BTC" if "BTC" in cfg["instrument"] else "ETH"