Hallmark redesign + QuantReport fix
Header/Tabs: Hallmark Cobalt aesthetic - Hairlines, cool paper bg, JetBrains Mono + Inter - Electric cobalt accent on active tab - No branding, no purple badges, no gradients QuantReport: inline in strategy detail view - Renders below trade history on every tab - API maps strategy name -> file prefix - Proper backtestId from historical data Server: strategy-name-to-prefix lookup ofi, avellaneda, iceberg, momentum, mean_rev, funding_arb, kalman_pairs, pairs
This commit is contained in:
@@ -13,6 +13,7 @@ import { PositionsPanel } from "@/components/positions-panel";
|
|||||||
import { OBIDetail } from "@/components/obi-detail";
|
import { OBIDetail } from "@/components/obi-detail";
|
||||||
import OrderBookDepthMap from "@/components/orderbook-depth-map";
|
import OrderBookDepthMap from "@/components/orderbook-depth-map";
|
||||||
import L2Terminal from "@/components/L2Terminal";
|
import L2Terminal from "@/components/L2Terminal";
|
||||||
|
import QuantReport from "@/components/QuantReport";
|
||||||
import { useLiveMetrics, usePaperMetrics, fetchHistorical, fetchBacktestDetail, recalcBacktest } from "@/lib/api";
|
import { useLiveMetrics, usePaperMetrics, fetchHistorical, fetchBacktestDetail, recalcBacktest } from "@/lib/api";
|
||||||
import type { Strategy, BacktestSummary, BacktestFull, Trade, Position, Order } from "@/lib/types";
|
import type { Strategy, BacktestSummary, BacktestFull, Trade, Position, Order } from "@/lib/types";
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
if (detailOpen) {
|
if (detailOpen) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-[#f8f9fb]">
|
||||||
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-xl">
|
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-xl">
|
||||||
<div className="flex items-center justify-between px-6 py-3 max-w-[1440px] mx-auto">
|
<div className="flex items-center justify-between px-6 py-3 max-w-[1440px] mx-auto">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -277,7 +278,16 @@ export default function Dashboard() {
|
|||||||
<p className="text-xs text-muted-foreground text-center py-12">No trades recorded yet</p>
|
<p className="text-xs text-muted-foreground text-center py-12">No trades recorded yet</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Live L2 Order Book + Trade Tape (all strategies, live tab only) */}
|
|
||||||
|
{/* QF-Lib Quant Report — Hallmark Cobalt inline */}
|
||||||
|
<div className="mt-6 border-t border-[#e0e4ec] pt-4">
|
||||||
|
<QuantReport
|
||||||
|
strategyName={detailName}
|
||||||
|
backtestId={historical[detailName]?.name || `${detailName.replace(/\s+/g, "_").toLowerCase()}.json`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live L2 Order Book + Trade Tape */}
|
||||||
{detailTab === "live" && (
|
{detailTab === "live" && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<OrderBookDepthMap coin="BTC" height={480} topRatio={0.55} />
|
<OrderBookDepthMap coin="BTC" height={480} topRatio={0.55} />
|
||||||
@@ -292,7 +302,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-[#f8f9fb]">
|
||||||
{/* Header — Hallmark Cobalt */}
|
{/* Header — Hallmark Cobalt */}
|
||||||
<header className="sticky top-0 z-50 border-b border-[#e0e4ec] bg-[#f8f9fb]/95 backdrop-blur-sm">
|
<header className="sticky top-0 z-50 border-b border-[#e0e4ec] bg-[#f8f9fb]/95 backdrop-blur-sm">
|
||||||
<div className="flex items-center justify-between px-6 h-12 max-w-[1440px] mx-auto">
|
<div className="flex items-center justify-between px-6 h-12 max-w-[1440px] mx-auto">
|
||||||
|
|||||||
+24
-18
@@ -441,40 +441,46 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|||||||
@app.get("/api/quant-report/{name}")
|
@app.get("/api/quant-report/{name}")
|
||||||
async def get_quant_report(name: str):
|
async def get_quant_report(name: str):
|
||||||
"""Compute full QF-Lib quant report from a backtest file.
|
"""Compute full QF-Lib quant report from a backtest file.
|
||||||
Accepts either an exact filename or a strategy-name prefix.
|
Accepts strategy name and auto-maps to filename prefix.
|
||||||
"""
|
"""
|
||||||
|
# Strategy name → file prefix mapping
|
||||||
|
NAME_MAP = {
|
||||||
|
"order book imbalance": "ofi",
|
||||||
|
"avellaneda-stoikov": "avellaneda",
|
||||||
|
"funding rate arb": "funding_arb",
|
||||||
|
"iceberg detection": "iceberg",
|
||||||
|
"momentum breakout": "momentum",
|
||||||
|
"mean reversion": "mean_rev",
|
||||||
|
"kalman pairs": "kalman_pairs",
|
||||||
|
"pairs trading": "pairs",
|
||||||
|
}
|
||||||
|
|
||||||
|
name_lower = name.lower()
|
||||||
|
prefix = NAME_MAP.get(name_lower, name_lower.replace(" ", "_"))
|
||||||
|
|
||||||
# Build candidate paths
|
# Build candidate paths
|
||||||
candidates = []
|
candidates = []
|
||||||
exact_path = os.path.join(BACKTEST_DIR, name)
|
exact_path = os.path.join(BACKTEST_DIR, name)
|
||||||
hist_exact = os.path.join(HISTORICAL_DIR, name)
|
hist_exact = os.path.join(HISTORICAL_DIR, name)
|
||||||
candidates.extend([exact_path, hist_exact])
|
candidates.extend([exact_path, hist_exact])
|
||||||
|
|
||||||
# Try exact match first
|
# Try exact match
|
||||||
for path in candidates:
|
for path in candidates:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
backtest_path = path
|
backtest_path = path
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Fuzzy match: look for files containing the name
|
# Fuzzy match: find files starting with the mapped prefix
|
||||||
|
fuzzy = []
|
||||||
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
||||||
if not os.path.exists(d): continue
|
if not os.path.exists(d): continue
|
||||||
for f in os.listdir(d):
|
for f in os.listdir(d):
|
||||||
# Match: name is a substring of filename (case insensitive)
|
|
||||||
name_clean = name.lower().replace(" ", "_").replace(".json", "")
|
|
||||||
f_clean = f.lower()
|
f_clean = f.lower()
|
||||||
if name_clean in f_clean or f_clean.startswith(name_clean):
|
# Match by prefix, then prefer BTC/ETH files
|
||||||
candidates.append(os.path.join(d, f))
|
if f_clean.startswith(f"{prefix}_"):
|
||||||
if not candidates:
|
fuzzy.append(os.path.join(d, f))
|
||||||
# Try partial match on strategy name
|
if fuzzy:
|
||||||
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
backtest_path = fuzzy[0]
|
||||||
if not os.path.exists(d): continue
|
|
||||||
for f in os.listdir(d):
|
|
||||||
parts = f.lower().replace(".json", "").split("_")
|
|
||||||
name_parts = name.lower().replace(" ", "_").split("_")
|
|
||||||
if all(p in parts for p in name_parts):
|
|
||||||
candidates.append(os.path.join(d, f))
|
|
||||||
if candidates:
|
|
||||||
backtest_path = candidates[0]
|
|
||||||
else:
|
else:
|
||||||
return JSONResponse({"error": f"Backtest '{name}' not found"}, status_code=404)
|
return JSONResponse({"error": f"Backtest '{name}' not found"}, status_code=404)
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user