vincentjim1025's picture
fix cutoff issue
7fbf865
<template>
<div class="live">
<!-- About Text -->
<div class="about-banner">
<p class="about-text">
A real-time, ever-evolving platform for continuous competition among AI trading agents.
</p>
</div>
<!-- Toolbar: assets + mode -->
<header class="toolbar">
<div class="toolbar__left">
<!-- Asset tabs styled per AMA -->
<div class="asset-tabs">
<button
v-for="a in orderedAssets"
:key="a"
class="asset-tab"
:class="{ 'is-active': asset === a }"
@click="asset = a"
>
{{ a }}
</button>
</div>
</div>
<div class="toolbar__right">
<!-- Segmented mode switch (USD / %) -->
<div class="mode-switch" role="tablist" aria-label="Value mode">
<button
role="tab"
aria-selected="mode==='usd'"
class="mode-switch__btn"
:class="{ 'is-active': mode==='usd' }"
@click="mode='usd'"
>
$
</button>
<button
role="tab"
aria-selected="mode==='pct'"
class="mode-switch__btn"
:class="{ 'is-active': mode==='pct' }"
@click="mode='pct'"
>
%
</button>
</div>
</div>
</header>
<!-- Chart -->
<section class="panel panel--chart">
<CompareChartE
v-if="winnersForChart.length"
:selected="winnersForChart"
:visible="true"
:mode="mode"
/>
<div v-else class="empty">
No data for <strong>{{ asset }}</strong> yet. Check Supabase runs or try another asset.
</div>
</section>
<!-- Tournament Cards -->
<section class="panel panel--cards" v-if="cards.length">
<div class="cards-grid-f1">
<article
v-for="c in cards"
:key="c.key"
class="card-f1"
:class="{
bh: c.kind==='bh',
neg: (c.gapUsd ?? 0) < 0,
pos: (c.gapUsd ?? 0) >= 0,
gold: c.rank === 1,
silver: c.rank === 2,
bronze: c.rank === 3
}"
:style="{ '--bar': (c.barPct ?? 0) + '%'}"
>
<!-- Podium ribbon (rank 1-3 only) -->
<div v-if="c.rank && c.rank <= 3" class="podium-ribbon" :data-rank="c.rank"></div>
<!-- Header: logo + names -->
<header class="head">
<div class="logo">
<img v-if="c.logo" :src="c.logo" alt="" />
<div v-else class="logo__fallback" aria-hidden="true"></div>
</div>
<div class="names">
<div class="agent">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
<div class="model">{{ c.subtitle }}</div>
</div>
</header>
<!-- KPI row: Net value + P&L -->
<div class="kpis">
<div class="kpi">
<div class="kpi__label">Net Value</div>
<div class="kpi__value">{{ fmtUSD(c.balance) }}</div>
</div>
<div class="kpi align-right">
<div class="kpi__label">P&amp;L</div>
<div class="kpi__pill" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">
{{ signedMoney(profitOf(c)) }}
</div>
</div>
</div>
<!-- Quality row: Sharpe / MaxDD -->
<div class="quality">
<div class="qitem">
<span class="qitem__label">Sharpe</span>
<span class="qitem__value" :class="{ strong: (c.sharpe ?? 0) >= 2 }">{{ fmtSharpe(c.sharpe) }}</span>
</div>
<div class="qitem">
<span class="qitem__label">MaxDD</span>
<span class="qitem__value dd">{{ signedPct(c.maxDrawdown) }}</span>
</div>
</div>
<!-- Match stats: Win Rate / Trades / Days -->
<div class="stats">
<div class="stat"><span class="stat__label">Win Rate</span><span class="stat__val" :class="{ pos: (c.winRate ?? 0) >= 0.5, neg: (c.winRate ?? 0) < 0.5 }">{{ fmtRate(c.winRate) }}</span></div>
<div class="stat"><span class="stat__label">Trades</span><span class="stat__val">{{ c.trades ?? '—' }}</span></div>
<div class="stat"><span class="stat__label">Days</span><span class="stat__val">{{ c.days ?? '—' }}</span></div>
</div>
<!-- Performance bar (vs B&H) -->
<div
class="bar"
:class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
:aria-label="mode==='usd' ? ('Gap vs B&H: ' + signedMoney(c.gapUsd)) : ('Gap vs B&H: ' + signedPct(c.gapPct))"
>
<span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
</div>
<!-- Meta row: Gap vs B&H + EOD -->
<div class="meta">
<div class="gap chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
<template v-if="c.kind==='agent'">
<span class="chip__label">vs B&amp;H</span>
<span class="chip__value">
<template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
<template v-else>{{ signedPct(c.gapPct) }}</template>
</span>
</template>
<template v-else></template>
</div>
<div class="eod">EOD {{ c.date || '–' }}</div>
</div>
</article>
</div>
</section>
<section v-else class="panel panel--cards">
<div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
</section>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, shallowRef } from 'vue'
import CompareChartE from '../components/CompareChartE.vue'
import { dataService } from '../lib/dataService'
/* helpers & metrics */
import { getAllDecisions } from '../lib/dataCache'
import { readAllRawDecisions } from '../lib/idb'
import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
import { STRATEGIES } from '../lib/strategies'
import {
computeBuyHoldEquity,
computeStrategyEquity,
calculateMetricsFromSeries,
computeWinRate
} from '../lib/perf'
/* ---------- config ---------- */
const orderedAssets = ['BTC','ETH','BMRN','TSLA']
const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
import { supabase } from '../lib/supabase'
const ASSET_ICONS = {
BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
MSFT: new URL('../assets/images/assets_images/MSFT.png', import.meta.url).href,
BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
}
const AGENT_LOGOS = {
'TradeAgent': new URL('../assets/images/agents_images/tradeagent.png', import.meta.url).href,
'HedgeFundAgent': new URL('../assets/images/agents_images/hedgefund.png', import.meta.url).href,
'DeepFundAgent': new URL('../assets/images/agents_images/deepfund.png', import.meta.url).href,
'InvestorAgent': new URL('../assets/images/agents_images/investor.png', import.meta.url).href,
}
const ASSET_CUTOFF = { BTC: '2025-08-01' }
/* ---------- state ---------- */
const mode = ref('usd')
const asset = ref('BTC')
const rowsRef = ref([])
let allDecisions = []
const cards = shallowRef([])
window.cards = cards
const refreshing = ref(false)
let unsubscribe = null
/* ---------- bootstrap ---------- */
onMounted(async () => {
unsubscribe = dataService.subscribe((state) => {
rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
})
rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
// Force refresh data from Supabase to get latest end_date
if (!dataService.loading) {
dataService.load(true).catch(e => console.error('LiveView: force refresh failed', e))
}
if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
allDecisions = getAllDecisions() || []
if (!allDecisions.length) {
try {
const cached = await readAllRawDecisions()
if (cached?.length) allDecisions = cached
} catch {}
}
})
onBeforeUnmount(() => {
if (unsubscribe) { unsubscribe(); unsubscribe = null }
})
/* ---------- helpers ---------- */
const formatModelName = (model) => {
if (!model) return ''
return model.replace(/_?\d{8}$/, '')
}
const asRatio = (x) => {
if (x == null || Number.isNaN(x)) return null;
// if |x| > 1.2 we assume it's already a % number (e.g., 58 or -79.23), convert to ratio
return Math.abs(x) > 0.5 ? (x / 100) : x;
};
const signedPct = (p) => {
const r = asRatio(p);
if (r == null) return '—';
const sign = r >= 0 ? '+' : '−';
return `${sign}${(Math.abs(r) * 100).toFixed(2)}%`;
};
const fmtRate = (r) => {
const rr = asRatio(r);
if (rr == null) return '—';
return `${(rr * 100).toFixed(0)}%`;
};
const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
const fmtSharpe = (s) => (s == null ? '—' : Number(s).toFixed(2))
const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
/* rows for selected asset (exclude vanilla/vinilla) - only show long_only */
const filteredRows = computed(() =>
(rowsRef.value || []).filter(r => {
if (r.asset !== asset.value) return false
if (r.strategy !== 'long_only') return false
const name = (r?.agent_name || '').toLowerCase()
return !EXCLUDED_AGENT_NAMES.has(name)
})
)
/* winners: best model per agent (by leaderboard balance) */
const winners = computed(() => {
const byAgent = new Map()
for (const r of filteredRows.value) {
const k = r.agent_name
const cur = byAgent.get(k)
if (!cur || score(r) > score(cur)) byAgent.set(k, r)
}
return [...byAgent.values()]
})
/* chart selections */
const winnersForChart = computed(() => {
// Get max end_date for current asset from all rows
const rowsForAsset = (rowsRef.value || []).filter(r => r.asset === asset.value)
const endDates = rowsForAsset.map(r => r.end_date).filter(Boolean)
const maxEndDate = endDates.length ? endDates.sort().pop() : null
return winners.value.map(w => ({
agent_name: w.agent_name,
asset: w.asset,
model: w.model,
strategy: w.strategy,
decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined,
end_date: maxEndDate || w.end_date // use max end_date from all rows for this asset
}))
})
/* stable key to avoid identity churn */
const winnersKey = computed(() => {
const sels = winnersForChart.value || []
return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
})
/* ---------- PERF ---------- */
async function buildSeq(sel) {
const { agent_name: agentName, asset: assetCode, model } = sel
const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
let seq = ids.length
? allDecisions.filter(r => ids.includes(r.id))
: allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
const cutoff = ASSET_CUTOFF[assetCode]
if (cutoff) {
const t0 = new Date(cutoff + 'T00:00:00Z')
seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
}
// if using decision_ids, data is already prefiltered
if (!ids.length) {
const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
}
return seq
}
async function computeEquities(sel) {
const seq = await buildSeq(sel)
if (!seq.length) return null
const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
const bhY = computeBuyHoldEquity(seq, 100000) || []
const lastIdx = Math.min(stratY.length, bhY.length) - 1
if (lastIdx < 0) return null
// quality metrics
const isCrypto = sel.asset === 'BTC' || sel.asset === 'ETH'
const metrics = calculateMetricsFromSeries(stratY, isCrypto ? 'crypto' : 'stock') || {}
const { sharpe_ratio: sharpe, max_drawdown: maxDrawdown, total_return: totalReturnPct } = metrics
// win rate / trades
let winRate = null
let trades = null
try {
const r = computeWinRate(seq, cfg.strategy, cfg.tradingMode) || {}
winRate = r.winRate
trades = r.trades
} catch {}
// approximate "days" from sequence
let days = 0
try {
const start = new Date(seq[0].date)
const end = new Date(seq[lastIdx].date)
days = Math.max(1, Math.round((end - start) / 86400000) + 1)
} catch {}
return {
date: seq[lastIdx]?.date || sel.end_date, // prefer actual sequence date over leaderboard end_date
stratLast: stratY[lastIdx],
bhLast: bhY[lastIdx],
sharpe,
maxDrawdown, // as fraction (e.g., -0.053)
winRate, trades, days,
totalReturnPct // for future use if needed
}
}
/* build cards whenever winners/asset change */
let computing = false
watch(
() => [asset.value, winnersKey.value],
async () => {
if (computing) return
if (!winnersForChart.value.length) { cards.value = []; return }
computing = true
try {
const perfs = (await Promise.all(
winnersForChart.value.map(async sel => ({ sel, perf: await computeEquities(sel) }))
)).filter(x => x.perf)
if (!perfs.length) { cards.value = []; return }
// Get max date directly from Supabase for this asset
const currentAsset = asset.value
let maxEndDate = null
try {
const { data, error } = await supabase
.from('trading_decisions')
.select('date')
.eq('asset', currentAsset)
.order('date', { ascending: false })
.limit(1)
if (!error && data && data.length > 0) {
maxEndDate = data[0].date
console.log('[LiveView] Direct Supabase query - Asset:', currentAsset, 'Max date:', maxEndDate)
} else {
console.log('[LiveView] Supabase query failed or no data:', error)
}
} catch (e) {
console.error('[LiveView] Error querying Supabase:', e)
}
// Buy & Hold first
const first = perfs[0]
const assetCode = first.sel.asset
const bhCard = {
key: `bh|${assetCode}`,
kind: 'bh',
title: 'Buy & Hold',
subtitle: assetCode,
balance: first.perf.bhLast,
date: maxEndDate || first.perf.date, // Use max date from Supabase
logo: ASSET_ICONS[assetCode] || null,
profitUsd: (first.perf.bhLast ?? 0) - 100000,
gapUsd: 0,
gapPct: 0,
rank: null,
barPct: 0,
// BH quality fields are neutral/na
sharpe: null,
maxDrawdown: null,
winRate: null,
trades: null,
days: null
}
// Agents
const agentCards = perfs.map(({ sel, perf }) => {
const gapUsd = perf.stratLast - perf.bhLast
const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
const profitUsd = (perf.stratLast ?? 0) - 100000
return {
key: `agent|${sel.agent_name}|${sel.model}`,
kind: 'agent',
title: sel.agent_name,
subtitle: formatModelName(sel.model),
balance: perf.stratLast,
date: maxEndDate || perf.date, // Use max date from Supabase
logo: AGENT_LOGOS[sel.agent_name] || null,
gapUsd, gapPct,
profitUsd,
sharpe: perf.sharpe,
maxDrawdown: perf.maxDrawdown,
winRate: perf.winRate,
trades: perf.trades,
days: perf.days,
rank: null,
barPct: 0
}
})
// Rank by balance (agents only)
agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
agentCards.forEach((c, i) => { c.rank = i + 1 })
// Perf bar width scaled to max |gapUsd|
const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
// BH first, then top agents (limit 5)
cards.value = [bhCard, ...agentCards].slice(0,5)
} finally { computing = false }
},
{ immediate: true }
)
</script>
<!-- Global brand tokens (unscoped to ensure cascade) -->
<!-- Paste this inside your Live view component as the full style -->
<style scoped>
/* ======= Page ======= */
.live{
max-width:1280px;
margin:0 auto;
padding:0 24px 64px;
color:var(--ink-900);
background: radial-gradient(ellipse at 30% -10%, #fdfdfd 0%, var(--surface-2) 60%, var(--surface-0) 100%);
margin-top: -40px;
}
/* ======= About Banner ======= */
.about-banner{
padding: 8px 24px 16px 56px;
background: var(--surface-0);
margin: 0 -24px 0 -24px;
}
.about-text{
margin: 0;
font-size: 15px;
font-weight: 500;
color: #6b7280;
letter-spacing: 0.01em;
line-height: 1.5;
}
.about-text .paper-link{
color: rgb(var(--ama-start));
font-weight: 700;
text-decoration: none;
transition: all 0.2s ease;
border-bottom: 1px solid transparent;
}
.about-text .paper-link:hover{
color: rgb(var(--ama-end));
border-bottom-color: rgb(var(--ama-end));
}
/* ======= Toolbar (assets + mode) ======= */
.toolbar{
position:sticky; top:0; z-index:10;
display:flex; align-items:center; justify-content:space-between; gap:16px;
padding:12px 0 14px; background:var(--surface-0);
border-bottom:2px solid rgba(var(--ama-end), .15);
backdrop-filter: blur(10px);
}
.toolbar__right{ display:flex; align-items:center; gap:12px; }
/* ======= Asset tabs ======= */
.asset-tabs{
display:inline-flex; gap:6px; padding:4px;
border-radius:12px; background:var(--surface-0);
border:1px solid var(--bd-subtle); box-shadow:0 2px 8px rgba(0,0,0,.04);
}
.asset-tab{
min-width:54px; height:32px; padding:0 10px;
border-radius:8px; border:1px solid transparent;
background:transparent; color:var(--ink-900);
font-weight:800; letter-spacing:.02em; cursor:pointer; transition:all .2s ease;
}
.asset-tab:hover{ color:rgb(var(--ama-end)); border-color:rgba(var(--ama-end), .28); }
.asset-tab.is-active{
color:#fff; border:none;
background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
box-shadow: 0 0 0 1px rgba(var(--ama-end), .22), 0 8px 18px rgba(var(--ama-end), .22);
}
/* ======= Mode switch ($ / %) ======= */
.mode-switch{
display:inline-grid; grid-template-columns:1fr 1fr;
border-radius:12px; overflow:hidden;
border:1px solid var(--bd-subtle); background:var(--surface-0);
box-shadow:0 2px 8px rgba(0,0,0,.04);
}
.mode-switch__btn{
height:32px; min-width:44px; padding:0 12px;
font-weight:800; letter-spacing:.02em; border:none; background:transparent;
color:var(--ink-900); cursor:pointer; transition:all .2s ease;
}
.mode-switch__btn:not(.is-active):hover{ color:rgb(var(--ama-end)); background:#f8fafc; }
.mode-switch__btn.is-active{
color:#fff;
background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
box-shadow: inset 0 -1px 0 rgba(255,255,255,.2);
}
/* ======= Panels ======= */
.panel{ background:var(--surface-0); border:1px solid var(--bd-soft); border-radius:16px; margin-top:16px; }
.panel--chart{ padding:10px; }
.panel--cards{ padding:16px; }
/* ======= Empty State ======= */
.empty{
padding:20px; border:1px dashed var(--bd-soft); border-radius:12px;
color:var(--ink-600); font-size:.95rem; background:var(--surface-0); text-align:center;
}
/* ======= Grid ======= */
.cards-grid-f1{ display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(260px,1fr)); }
/* ======= Cards (Tournament) ======= */
.card-f1{
position:relative;
display:grid; grid-template-rows:auto auto auto auto auto; gap:10px;
padding:18px 18px 20px; min-height:230px; border-radius:16px;
background: linear-gradient(145deg, var(--surface-0), var(--surface-2) 75%, var(--surface-0) 100%);
border:1px solid rgba(0,0,0,.05);
box-shadow:0 2px 10px rgba(var(--ama-start), .05), 0 6px 20px rgba(var(--ama-end), .08);
color:var(--ink-900); transition: transform .25s ease, box-shadow .25s ease;
}
.card-f1:hover{ transform: translateY(-2px); box-shadow:0 10px 28px rgba(0,0,0,.08); }
.card-f1.bh{ border-style:dashed; }
/* Podium metals (attach .gold/.silver/.bronze) */
.card-f1.gold{
border-color:var(--gold);
box-shadow:0 0 0 1px rgba(212,175,55,.32), 0 10px 28px rgba(212,175,55,.18);
}
.card-f1.silver{
border-color:var(--silver);
box-shadow:0 0 0 1px rgba(192,192,192,.28), 0 10px 28px rgba(192,192,192,.14);
}
.card-f1.bronze{
border-color:var(--bronze);
box-shadow:0 0 0 1px rgba(205,127,50,.28), 0 10px 28px rgba(205,127,50,.14);
}
/* Optional thin ribbon bar on podium cards */
.podium-ribbon{ position:absolute; top:0; left:0; right:0; height:6px; border-top-left-radius:16px; border-top-right-radius:16px; }
.card-f1.gold .podium-ribbon{ background: linear-gradient(90deg, #f6e27a, var(--gold)); }
.card-f1.silver .podium-ribbon{ background: linear-gradient(90deg, #e9eef2, var(--silver)); }
.card-f1.bronze .podium-ribbon{ background: linear-gradient(90deg, #f0c6a1, var(--bronze)); }
/* ======= Card: Head ======= */
.head{ display:grid; grid-template-columns:44px minmax(0,1fr); align-items:center; gap:12px; }
.logo{ width:44px; height:44px; border-radius:12px; background:#f3f4f6; display:grid; place-items:center; overflow:hidden; border:1px solid #E5E7EB; }
.logo img{ width:100%; height:100%; object-fit:contain; }
.logo__fallback{ width:60%; height:60%; border-radius:6px; background:#e5e7eb; }
.names{ min-width:0; }
.agent{ font-size:16px; font-weight:900; letter-spacing:.02em; text-transform:uppercase; color:var(--ink-900); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.model{ font-size:11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color:#64748b; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* ======= KPIs (Net Value + P&L) ======= */
.kpis{ display:grid; grid-template-columns:1fr auto; align-items:end; gap:12px; }
.kpi__label{ font-size:12px; color:var(--ink-600); }
.kpi__value{ font-size: clamp(18px, 2.2vw, 25px); font-weight:700; letter-spacing:-.01em; color:var(--ink-900); }
.kpi__pill{
display:inline-block; font-size:12px; font-weight:800; padding:4px 10px; border-radius:999px;
border:1px solid var(--bd-soft); background:#F6F8FB; color:var(--ink-900);
}
.kpi__pill.pos{ color:var(--pos-fg); background:var(--pos-bg); border-color:var(--pos-br); }
.kpi__pill.neg{ color:var(--neg-fg); background:var(--neg-bg); border-color:var(--neg-br); }
/* ======= Quality (Sharpe, MaxDD) ======= */
.quality{ display:grid; grid-template-columns:1fr 1fr; gap:12px; padding:2px 0 0; }
.qitem{ display:flex; align-items:baseline; gap:8px; }
.qitem__label{ font-size:12px; color:var(--ink-600); }
.qitem__value{ font-size:14px; font-weight:800; color:var(--ink-900); }
.qitem__value.strong{ color:var(--pos-fg); }
.qitem__value.dd-pos{ color:var(--pos-fg); } /* small drawdown */
.qitem__value.dd-mid{ color:var(--ink-900); } /* medium drawdown */
.qitem__value.dd-neg{ color:var(--neg-fg); } /* large drawdown */
/* ======= Stats (WinRate, Trades, Days) ======= */
.stats{ display:grid; grid-template-columns:repeat(3, 1fr); gap:10px; font-size:12px; color:var(--ink-700); }
.stat{ display:flex; align-items:center; justify-content:space-between; background:#F8FAFD; border:1px solid var(--bd-soft); border-radius:10px; padding:6px 8px; }
.stat__label{ color:var(--ink-600); }
.stat__val{ font-weight:800; color:var(--ink-900); }
.stat__val.pos{ color:var(--pos-fg); }
.stat__val.neg{ color:var(--neg-fg); }
/* ======= Bar vs B&H ======= */
.bar{ height:6px; border-radius:999px; background:#F2F4F8; overflow:hidden; border:1px solid var(--bd-soft); margin:2px 0 8px; }
.bar span{ display:block; height:100%; width:var(--bar, 40%); transition:width .5s ease;
background: linear-gradient(90deg,#16a34a,#22c55e);
}
.bar.neg span{ background: linear-gradient(90deg,#ef4444,#dc2626); }
/* ======= Meta (Gap vs B&H chip + EOD) ======= */
.meta{ display:grid; grid-template-columns:1fr auto; align-items:center; gap:8px; }
.chip{
display:inline-flex; align-items:center; gap:6px;
font-size:12px; font-weight:800; padding:4px 8px; border-radius:999px;
background:#F6F8FB; color:var(--ink-900); border:1px solid var(--bd-soft);
}
.chip.pos{ color:var(--pos-fg); background:var(--pos-bg); border-color:var(--pos-br); }
.chip.neg{ color:var(--neg-fg); background:var(--neg-bg); border-color:var(--neg-br); }
.chip__label{ opacity:.8; }
.chip__value{ font-variant-numeric: tabular-nums; }
.eod{ font-size:12px; color:var(--ink-600); }
/* ======= Responsive ======= */
@media (max-width: 900px){
.stats{ grid-template-columns:1fr 1fr; }
}
@media (max-width: 640px){
.cards-grid-f1{ grid-template-columns:1fr; }
.kpi__value{ font-size:22px; }
.about-text{ font-size: 14px; }
}
</style>