feat: data-driven autopilot profitability tuning + edge profile panel

Analysis of 410 live closed trades found the edge is real but was being
destroyed by execution: gross +$267 vs $245 fees; trades held <1h were net
negative (the <15m bucket alone: -$48 on $66 fees) while 1h+ holds carried
+$78; shorts lost $72 while longs made $94 — and both the prompt and the
engine were manufacturing those losers.

Changes, each tied to the data:
- Prompt: removed the 'MUST open at least one long AND one short every
  cycle' mandate (forced weak-signal shorts); direction is now data-driven
  with 'never open just to balance the book'. Added fee-awareness (round
  trip ≈ 0.1% notional, require expected move ≥ 3x cost) and aligned the
  hold discipline with the backend throttle.
- Forced book-balance opens now require |board z-score| ≥ 0.75 — the engine
  previously force-opened full-size 10x positions on near-neutral signals
  with hardcoded confidence 70. DirectionalCandidates now carries scores.
- Min AI-managed hold raised 45m → 60m (the 15-60m bucket still bled after
  the earlier throttle landed).
- Legacy prompt hygiene: vergex path drops long-only-era custom prompts and
  zh-era configs fall back wholesale to built-in English sections — fixes
  the two long-failing kernel prompt tests.
- New Edge Profile dashboard panel: net after fees by hold-time bucket and
  side, computed from recent closed trades, with an automatic takeaway line
  — the fee/churn regression detector, always visible. Fixed the
  HistoricalPosition timestamp types (epoch ms, not strings).
This commit is contained in:
tinkle-community
2026-07-07 19:21:49 +09:00
parent e593874912
commit c53a563bff
9 changed files with 261 additions and 42 deletions

View File

@@ -0,0 +1,138 @@
import { useMemo } from 'react'
import type { HistoricalPosition } from '../../types'
/**
* EdgeProfile — where the money actually comes from. Aggregates recent closed
* trades into hold-duration buckets plus a long/short split, each with net
* PnL, fee load and win rate. This is the panel that made the fee-drag and
* churn problems visible, kept on the dashboard so regressions show up
* immediately.
*/
interface EdgeProfileProps {
positions?: HistoricalPosition[]
}
interface BucketAgg {
label: string
n: number
net: number
fees: number
wins: number
}
function newBucket(label: string): BucketAgg {
return { label, n: 0, net: 0, fees: 0, wins: 0 }
}
function add(bucket: BucketAgg, pos: HistoricalPosition) {
bucket.n += 1
bucket.net += pos.realized_pnl || 0
bucket.fees += pos.fee || 0
if ((pos.realized_pnl || 0) > 0) bucket.wins += 1
}
function fmtUsd(n: number): string {
const sign = n < 0 ? '-' : '+'
return `${sign}$${Math.abs(n).toFixed(2)}`
}
/** Accepts epoch ms (the API's format) or a date string, returns epoch ms. */
function toEpochMs(value: number | string): number {
if (typeof value === 'number') return value
const numeric = Number(value)
if (Number.isFinite(numeric) && numeric > 0) return numeric
return Date.parse(value)
}
export function EdgeProfile({ positions }: EdgeProfileProps) {
const { holdBuckets, sideBuckets, sample } = useMemo(() => {
const holds = [
newBucket('<15m'),
newBucket('15-60m'),
newBucket('1-3h'),
newBucket('>3h'),
]
const sides = [newBucket('long'), newBucket('short')]
let counted = 0
for (const pos of positions ?? []) {
if ((pos.status || '').toUpperCase() !== 'CLOSED') continue
const entry = toEpochMs(pos.entry_time)
const exit = toEpochMs(pos.exit_time)
if (!Number.isFinite(entry) || !Number.isFinite(exit) || exit <= entry) {
continue
}
counted += 1
const holdMin = (exit - entry) / 60000
const holdBucket =
holdMin < 15 ? holds[0] : holdMin < 60 ? holds[1] : holdMin < 180 ? holds[2] : holds[3]
add(holdBucket, pos)
const sideBucket =
(pos.side || '').toLowerCase() === 'short' ? sides[1] : sides[0]
add(sideBucket, pos)
}
return { holdBuckets: holds, sideBuckets: sides, sample: counted }
}, [positions])
if (sample === 0) {
return <div className="tm-sc">No closed trades yet.</div>
}
const maxAbsNet = Math.max(0.01, ...holdBuckets.map((b) => Math.abs(b.net)))
const row = (bucket: BucketAgg) => {
const winPct = bucket.n > 0 ? (100 * bucket.wins) / bucket.n : 0
const up = bucket.net >= 0
return (
<div key={bucket.label} style={{ marginBottom: 7 }}>
<div className="tm-mono" style={{ display: 'flex', alignItems: 'baseline', fontSize: 11, marginBottom: 2 }}>
<span style={{ fontWeight: 500, minWidth: 52 }}>{bucket.label}</span>
<span className="tm-sc">
{bucket.n} trades · {bucket.n > 0 ? `${winPct.toFixed(0)}% win` : '—'} · fees ${bucket.fees.toFixed(2)}
</span>
<span className={up ? 'tm-up' : 'tm-dn'} style={{ marginLeft: 'auto', fontWeight: 600 }}>
{bucket.n > 0 ? fmtUsd(bucket.net) : '—'}
</span>
</div>
{/* diverging net bar around a center axis */}
<div style={{ display: 'flex', height: 4, background: 'var(--tm-hair)' }}>
<div style={{ width: '50%', display: 'flex', justifyContent: 'flex-end' }}>
{!up && (
<div style={{ height: 4, width: `${(Math.abs(bucket.net) / maxAbsNet) * 100}%`, background: 'var(--tm-dn)' }} />
)}
</div>
<div style={{ width: '50%' }}>
{up && bucket.net > 0 && (
<div style={{ height: 4, width: `${(bucket.net / maxAbsNet) * 100}%`, background: 'var(--tm-up)' }} />
)}
</div>
</div>
</div>
)
}
// one-line takeaway: does patience pay on this book?
const shortHolds = holdBuckets[0].net + holdBuckets[1].net
const longHolds = holdBuckets[2].net + holdBuckets[3].net
const takeaway =
longHolds > shortHolds
? `edge concentrates in holds ≥ 1h (${fmtUsd(longHolds)} vs ${fmtUsd(shortHolds)} under 1h)`
: `short holds outperform on this sample (${fmtUsd(shortHolds)} vs ${fmtUsd(longHolds)} ≥ 1h)`
return (
<div>
{holdBuckets.map(row)}
<div style={{ borderTop: '1px solid var(--tm-hair)', margin: '8px 0 7px' }} />
{sideBuckets.map(row)}
<div className="tm-sc" style={{ marginTop: 6, fontSize: 9 }}>
last {sample} closed · {takeaway}
</div>
</div>
)
}
export default EdgeProfile

View File

@@ -17,6 +17,7 @@ import { KlineChart } from './KlineChart'
import { ExecutionLog } from './ExecutionLog'
import { SignalMatrix } from './SignalMatrix'
import { RiskRadar } from './RiskRadar'
import { EdgeProfile } from './EdgeProfile'
import { useDemoEngine } from '../../lib/demo/useDemoEngine'
// crypto majors trade on the Hyperliquid main dex (no hip3 cost/liq heatmap);
@@ -540,8 +541,8 @@ export function TerminalDashboard({
</div>
<div className="tm-rule" />
{/* market net inflow (Vergex) · by-symbol history — balanced two-column footer */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.5fr) minmax(0,1fr)' }}>
{/* market net inflow (Vergex) · by-symbol history · edge profile — footer */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.2fr) minmax(0,0.9fr) minmax(0,0.9fr)' }}>
<div style={{ ...sc, borderRight: cellBorder }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 12 }}>Market net inflow</span>
@@ -550,7 +551,7 @@ export function TerminalDashboard({
</div>
<FlowMarkets items={flowItems} window={flow?.data?.window} />
</div>
<div style={sc}>
<div style={{ ...sc, borderRight: cellBorder }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>By symbol</span>
<span className="tm-sc">By-symbol history · trades/win/pnl</span>
@@ -568,6 +569,13 @@ export function TerminalDashboard({
</div>
)) : <div className="tm-sc">No symbol history.</div>}
</div>
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Edge profile</span>
<span className="tm-sc">Net by hold time &amp; side · after fees</span>
</div>
<EdgeProfile positions={history?.positions} />
</div>
</div>
</div>
</div>

View File

@@ -36,9 +36,9 @@ function formatDuration(minutes: number): string {
}
// Format date
function formatDate(dateStr: string): string {
if (!dateStr) return '-'
const date = new Date(dateStr)
function formatDate(value: number | string): string {
if (!value) return '-'
const date = new Date(value)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('zh-CN', {
month: '2-digit',

View File

@@ -186,10 +186,12 @@ export interface HistoricalPosition {
entry_quantity: number
entry_price: number
entry_order_id: string
entry_time: string
/** Epoch milliseconds. */
entry_time: number
exit_price: number
exit_order_id: string
exit_time: string
/** Epoch milliseconds. */
exit_time: number
realized_pnl: number
fee: number
leverage: number