summaryrefslogtreecommitdiff
path: root/frontend/src/lib/resultsAccounting.test.ts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
commit5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch)
tree948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/lib/resultsAccounting.test.ts
downloadstrategy-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.ts99
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('成交次数');
+ });
+});