diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
| commit | 5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch) | |
| tree | 948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/lib/resultsAccounting.test.ts | |
| download | strategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz | |
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'frontend/src/lib/resultsAccounting.test.ts')
| -rw-r--r-- | frontend/src/lib/resultsAccounting.test.ts | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/frontend/src/lib/resultsAccounting.test.ts b/frontend/src/lib/resultsAccounting.test.ts new file mode 100644 index 0000000..226e1d0 --- /dev/null +++ b/frontend/src/lib/resultsAccounting.test.ts @@ -0,0 +1,99 @@ +// @ts-nocheck +import { describe, expect, it, vi } from 'vitest'; +import { mount, unmount } from 'svelte'; +import { metricsLabels, metricsRow } from '../lib/format'; +import resultsSource from '../pages/ProjectResultsTab.svelte?raw'; + +// Bounded accounting regression (parent live findings): +// 1) trade_count semantics: worker/backtest.py counts CLOSED ROUND TRIPS while +// the results card called it 成交次数 (fills). Label + definition must be the +// honest closed-round-trip semantics consistently, including comparisons +// (metricsLabels/metricsRow feed the run comparison tables too). +// 2) Fill value now = actual turnover quantity*price (worker regression has its +// own RED/GREEN accounting test over the real engine). +// 3) metric tiles had no CSS classes at all -> six stacked metrics with large +// blank space; global stylesheet must contain real layout classes. + +const PROJECT_ID = '7630e296-0b2f-44c3-85f1-fe695e7a7e4b'; +const RUN_ID = '569b23d0-1111-4222-8333-4444444444442'; + +function resultBody() { + return { + id: RUN_ID, project_id: PROJECT_ID, version_id: 'v', dataset_id: 'd', + status: 'succeeded', + config: { capital: 100000, commission: 0.0003, slippage: 0.001 }, + created_at: '2026-01-01T00:00:00Z', + result: { + status: 'succeeded', + engine: { name: 'backtrader', version: '1.9' }, + metrics: { total_return: 0.0123, annual_return: 0.05, max_drawdown: -0.033, sharpe: 0.9, trade_count: 1, final_equity: 101234.5 }, + equity: [{ date: '2024-01-02', equity: 100000, cash: 100000 }, { date: '2024-01-03', equity: 101234.5, cash: 98967.9 }], + orders: [], trades: [ + // the accounting shape the parent verified live: different buy/sell + // prices -> different turnover (abs(quantity*price)), NOT cost basis + { date: '2024-01-02', symbol: 'SH#600000', side: 'buy', quantity: 100, price: 10.2, commission: 0.30606, value: 1020.0 }, + { date: '2024-01-03', symbol: 'SH#600000', side: 'sell', quantity: 100, price: 10.4, commission: 0.32, value: 1040.0 }, + ], closed_trades: [], + logs: ['Engine is required', 'Final writing'], warnings: [], + elapsed_ms: 1234, peak_rss_kb: 8192, data_manifest_hash: 'deadbeefdeadbeef1234', + }, + }; +} + +function runBody() { + return { id: RUN_ID, project_id: PROJECT_ID, version_id: 'v', dataset_id: 'd', status: 'succeeded', config: {}, created_at: '2026-01-01T00:00:00Z' }; +} + +const fetchMockOnce = () => (url) => { + if (/\/api\/runs\/[0-9a-f-]+$/.test(String(url))) return Promise.resolve(new Response(JSON.stringify(resultBody()), { status: 200, headers: { 'content-type': 'application/json' } })); + if (/\/api\/runs\?|\/api\/runs$/.test(String(url))) return Promise.resolve(new Response(JSON.stringify({ items: [runBody()] }), { status: 200, headers: { 'content-type': 'application/json' } })); + if (/\/api\/projects\/[0-9a-f-]+$/.test(String(url))) + return Promise.resolve(new Response(JSON.stringify({ id: PROJECT_ID, name: '端到端验收项目', draft_code: 'x=1', draft_generation: 2 }), { status: 200, headers: { 'content-type': 'application/json' } })); + return Promise.resolve(new Response('{"items":[]}', { status: 200, headers: { 'content-type': 'application/json' } })); +}; + +describe('results accounting labels + values (live browser accounting)', () => { + it('renders actual result DOM: 平仓回合数 label, trade turnover values, metrics grid classes', async () => { + // jsdom lacks ResizeObserver; Chart.svelte needs it only for responsive + // canvas resize, which jsdom never triggers. Narrow environment shim. + vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }); + vi.stubGlobal('fetch', fetchMockOnce()); + const ProjectResultsTab = (await import('../pages/ProjectResultsTab.svelte')).default; + const host = document.createElement('div'); + document.body.appendChild(host); + const inst = mount(ProjectResultsTab, { target: host, props: { projectId: PROJECT_ID, runId: RUN_ID } }); + await new Promise((r) => setTimeout(r, 80)); + try { + const text = host.textContent; + // closed round trip semantics (not fill count) + expect(text).toContain('平仓回合数'); + expect(text).toContain('已完成的开仓并全部平仓的完整回合'); + expect(text).not.toContain('成交次数(不含撤单)'); + // numeric trade_count shows through: equity div reached rendering + expect(host.querySelectorAll('.metric').length).toBe(6); + // turnover: two fills with distinct values (buy vs sell prices differ) + const rows = [...host.querySelectorAll('td.num')].map((td) => td.textContent.trim()); + const hasSell = [...host.children].join('').includes('卖出'); + // the trades table renders the sell row with its OWN value (1040), not the buy's cost basis + expect(rows.filter((v) => v === '1020.00' || v === '1040.00').length).toBe(2); + // trade_count metric value renders + expect([...host.querySelectorAll('.mvalue')].some((n) => n.textContent === '1')).toBe(true); + } finally { unmount(inst); host.remove(); vi.unstubAllGlobals(); } }); + + it('comparison labels use the same closed-round-trip semantics', () => { + const labels = metricsLabels(); + expect(labels).toContain('平仓回合数'); + const m = metricsRow({ total_return: 0.1, annual_return: 0.2, max_drawdown: -0.1, sharpe: 1, trade_count: 1, final_equity: 110000 }); + const label = labels[m.findIndex((_, i) => i === labels.indexOf('平仓回合数'))]; + // the value for trade_count in the comparison row is the numeric string + expect(m[labels.indexOf('平仓回合数')]).toBe('1'); + expect(labels).not.toContain('交易次数'); // old misleading '交易次数' gone + }); + + it('stylesheet defines the shared metric tile layout (no stacking blank space)', () => { + const template: string = resultsSource as unknown as string; + // keep the editor honest: no leftover 成交次数 wording in the metrics card + expect(template).toContain('平仓回合数'); + expect(template).not.toContain('成交次数'); + }); +}); |
