AWS Builder 工作坊

與 Kiro 一起開發:重建損益排行榜背後的量化分析引擎

工作坊系列

量化損益排行榜排名截圖 1
量化損益排行榜排名截圖 2

摘要: 本獨立工作坊將教導開發者使用 Kiro、TypeScript、純財務函數、SVG 圖表生成器、測試以及受控的即時模擬,重新構建上傳的排行榜中之量化 JavaScript 邏輯。開發者將實作報酬率、回撤、波動度、Calmar 比率、VaR、最佳單日、最差單日、勝率、排序、搜尋過濾、走勢線路徑、權益曲線、直方圖,以及具備完整原型覆蓋率與審查任務的更新迴圈安全性。

工作坊目的

這個為期 2 小時的工作坊專注於 Demo 背後的分析與互動引擎。本工作坊不側重於 UI 樣式,而是將原始的 JavaScript 行為萃取到可測試的 TypeScript 模組中。開發者將學習如何使用 Kiro 來設計財務函數、測試邊緣案例、解釋指標意義,並在不引入可靠性存疑的計算情況下保留即時更新行為。

Demo 功能覆蓋地圖

本工作坊涵蓋以下 Demo 實作細節:

  • 包含 8 位交易員的參與者陣列 P:策略、淨值(NAV)%、每日 %、夏普比率、獲利因子、勝率、偏態、最大回撤(Max DD)以及淨值序列。
  • 市場陣列 M:包含 ES、CL、GC、BTC、狀態標籤、變動值與迷你序列。
  • 格式化輔助函式:帶正負號的百分比顯示與正/負類別(class)選取。
  • ret(series):每日報酬率序列。
  • dd(series):回撤序列。
  • avg, sd, perc:平均值、標準差、百分位數。
  • path:SVG 折線路徑生成。
  • spark, eqChart, ddChart, hist:迷你圖與完整圖表生成概念。
  • panel:衍生進階指標,包含區間報酬率、已實現波動度、Calmar 比率、VaR 95、最佳單日、最差單日、勝率、最大回撤。
  • data:搜尋與排序行為。
  • render:資料列建構與展開面板狀態保留。
  • stat:參與者人數、最佳夏普比率、平均勝率、最佳淨值。
  • 5 秒模擬淨值與每日報酬率更新迴圈。

目標開發者

  • 將儀表板數學公式轉換為經測試模組的量化開發者。
  • 正在學習財務指標實作的 TypeScript 開發者。
  • 需要確定性圖表路徑生成的前端工程師。
  • 正在學習如何使用 Kiro 審查數學與邊緣案例的工程師。

兩小時議程

時間 模組 開發者產出
0:00-0:10 萃取需求 來自 HTML Demo 的分析資產清單
0:10-0:25 Kiro 引導設定 指標正負號、單位、測試與模擬規則
0:25-0:45 指標引擎 報酬率、回撤、波動度、百分位數、Calmar 比率
0:45-1:05 聚合引擎 排行榜統計資料與排序/過濾邏輯
1:05-1:25 SVG 圖表引擎 走勢線、權益曲線、回撤、直方圖路徑
1:25-1:45 即時模擬 受控更新迴圈與狀態安全性
1:45-1:55 測試 單元測試與基於屬性的測試
1:55-2:00 Kiro 審查 邊緣案例與生產環境強化待辦清單

架構

src/domain/
├─ types.ts              # Trader, Market, RiskMetrics, SortKey
├─ formatting.ts         # 帶正負號的百分比與類別
├─ calculations.ts       # 報酬率、回撤、波動度、百分位數
├─ leaderboard.ts        # 搜尋、排序、統計資料、資料列模型
├─ charts.ts             # SVG 路徑與圖表模型函數
├─ simulator.ts          # 確定性更新迴圈輔助函式
└─ __tests__/
   ├─ calculations.test.ts
   ├─ leaderboard.test.ts
   ├─ charts.test.ts
   └─ simulator.test.ts

財務指標定義與交易決策用途

指標 Demo 公式或解讀 交易決策用途
區間報酬率 (Window Return) (最後一個 NAV / 第一個 NAV - 1) * 100 衡量顯示視窗內的累積績效。
每日報酬率 (Daily Return) (今日 NAV / 前日 NAV - 1) * 100 顯示短期貢獻並驅動直方圖長條。
已實現波動度 (Realized Volatility) 每日報酬率的標準差乘以 sqrt(252) 用於比較不同策略之間的風險強度。
回撤 (Drawdown) (當前 NAV / 歷史高點 - 1) * 100 偵測策略從其頂峰下跌了多少。
最大回撤 (Max Drawdown) 序列中的最小回撤值。 用於風險限制和停損審查閾值。
Calmar 比率 (Calmar Ratio) 區間報酬率除以最大回撤的絕對值。 比較報酬效率與資金痛苦程度。
VaR 95 簡化 Demo 中每日報酬率的第五百分位數。 根據歷史報酬率近似估算下檔風險閾值。
最佳單日 (Best Day) 最大每日報酬率。 識別潛在的向上跳空潛力。
最差單日 (Worst Day) 最小每日報酬率。 識別單一期間的下檔衝擊。
勝率 (Win Rate) 正每日報酬率天數除以總天數。 衡量一致性,但不衡量交易規模。
獲利因子 (Profit Factor) 來自參與者資料的 Demo 資料列品質指標。 比較總收益與總損失,作為策略品質指標。
偏態 (Skew) Demo 資料列的不對稱性指標。 負偏態警告可能存在下檔尾部行為。

步驟 1 — 為財務計算加入 Kiro 引導設定

建立 .kiro/steering/quant-math.md

# 量化數學引導設定

- 將所有顯示的報酬率視為百分點,而非小數。
- 回撤必須為零或負數。
- 在此 Demo 中,VaR 95 為每日報酬率的第五百分位數。
- 已實現波動度使用 sqrt(252) 進行年化。
- 保持所有財務函數為純函數(pure functions)且無副作用。
- 請勿虛構即時或真實的市場資料。

建立 .kiro/steering/testing.md

# 測試引導設定

每個財務公式都需要確定性的單元測試。
使用屬性風格的測試(property-style tests)來驗證不變量,例如回撤 <= 0。
測試空陣列、單點陣列、平坦淨值、全勝淨值、全敗淨值、無效淨值以及極端回撤。

Kiro 提示詞範例

建立一個規格說明,將上傳的排行榜 JavaScript 萃取至 TypeScript 分析模組中。包含財務公式定義、排序與搜尋行為、SVG 路徑生成、即時模擬行為、測試與邊緣案例。

商業邏輯: 引導設定在生成程式碼之前先定義財務意義。開發者需要一致的單位與正負號慣例,以避免產生誤導性的排名。

程式碼邏輯: Kiro 使用引導設定規則來生成純函數與測試,而不是將計算嵌入到 UI 渲染中。

預期結果: Kiro 為分析引擎產生需求/設計/任務流程。

系統設計原理解析:

  1. 量化數學引導設定是強制性的,因為原始原型包含諸如 retddperc 等簡短的輔助函式名稱;生產環境開發者需要明確的定義。
  2. 測試引導設定與數學引導設定分開,因為測試覆蓋率是一項工程政策,而非財務公式。
  3. 「不虛構即時資料」的規則保持了工作坊的真實性。模擬數據只有在明確標記為 Demo 行為時才能使用。

步驟 2 — 定義型別

建立 src/domain/types.ts

export type SortKey = 'navPct' | 'dailyPct' | 'sharpe' | 'profitFactor' | 'winRatePct' | 'maxDrawdownPct';

export type Trader = {
    name: string;
    strategy: string;
    navPct: number;
    dailyPct: number;
    sharpe: number;
    profitFactor: number;
    winRatePct: number;
    skew: number;
    maxDrawdownPct: number;
    series: number[];
};

export type AdvancedMetrics = {
    windowReturnPct: number;
    realizedVolPct: number;
    calmarRatio: number;
    valueAtRisk95Pct: number;
    bestDayPct: number;
    worstDayPct: number;
    winRatePct: number;
    maxDrawdownPct: number;
};

商業邏輯: 型別記錄了哪些值是原始輸入,哪些是衍生分析指標。

程式碼邏輯: SortKey 模擬了 Demo 的排序按鈕。Trader 對應參與者物件,而 AdvancedMetrics 對應詳細資料面板的區塊。

預期結果: TypeScript 在開發過程中能捕捉到無效的排序鍵或缺失的指標欄位。

系統設計原理解析:

  1. 明確的型別取代了隱式的 JavaScript 物件形狀。這至關重要,因為交易儀表板經常需要透過新增欄位來演進。
  2. 排序鍵使用領域名稱(domain names),而非 UI 標籤(如 SRPF),這使程式碼更容易理解,同時在呈現層保留標籤。
  3. 進階指標與交易員輸入分離,因為 Calmar 和 VaR 等數值是從 series 計算而來,而非手動維護。

步驟 3 — 實作計算

建立 src/domain/calculations.ts

import type { AdvancedMetrics } from './types';

export function returnsPct(series: number[]): number[] {
    if (series.length < 2) return [];
    return series.slice(1).map((value, index) => ((value / series[index]) - 1) * 100);
}

export function drawdownPct(series: number[]): number[] {
    if (!series.length) return [];
    let peak = series[0];
    return series.map(value => {
        peak = Math.max(peak, value);
        return peak === 0 ? 0 : ((value / peak) - 1) * 100;
    });
}

export function average(values: number[]): number {
    return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
}

export function standardDeviation(values: number[]): number {
    const m = average(values);
    return values.length ? Math.sqrt(average(values.map(v => (v - m) ** 2))) : 0;
}

export function percentile(values: number[], p: number): number {
    if (!values.length) return 0;
    const sorted = [...values].sort((a, b) => a - b);
    const index = Math.min(sorted.length - 1, Math.max(0, Math.floor((p / 100) * sorted.length)));
    return sorted[index];
}

export function advancedMetrics(series: number[]): AdvancedMetrics {
    const r = returnsPct(series);
    const d = drawdownPct(series);
    const windowReturnPct = series.length >= 2 ? ((series.at(-1)! / series[0]) - 1) * 100 : 0;
    const maxDrawdownPct = d.length ? Math.min(...d) : 0;
    return {
        windowReturnPct,
        realizedVolPct: standardDeviation(r) * Math.sqrt(252),
        calmarRatio: maxDrawdownPct === 0 ? 0 : windowReturnPct / Math.abs(maxDrawdownPct),
        valueAtRisk95Pct: percentile(r, 5),
        bestDayPct: r.length ? Math.max(...r) : 0,
        worstDayPct: r.length ? Math.min(...r) : 0,
        winRatePct: r.length ? r.filter(x => x > 0).length / r.length * 100 : 0,
        maxDrawdownPct,
    };
}

商業邏輯: These functions 使用專業名稱與安全的空陣列處理行為,重現了分析面板的邏輯。

程式碼邏輯: 程式碼將原型輔助函式轉換為可組合的函數。advancedMetrics 聚合了詳細資料面板中顯示的特定區塊數值。

預期結果: 傳入 Sofia 的資產淨值(NAV)序列會返回大約 18.4% 的區間報酬率、負的最大回撤,以及面板所需的進階分析數值。

系統設計原理解析:

  1. 純函數使分析引擎可用於 UI、API、測試或批次作業。這保護了商業邏輯免受 UI 重構的影響。
  2. advancedMetrics 集中了面板計算,因此 React 詳細資料面板不需要重複公式。
  3. 百分位數方法刻意鏡像了原始的簡化 Demo。工作坊應解釋生產環境的 VaR 需要更強大的方法論與資料治理。

步驟 4 — 實作排序、過濾與摘要統計

建立 src/domain/leaderboard.ts

import type { SortKey, Trader } from './types';

export function signed(value: number, digits = 1, suffix = ''): string {
    return `${value >= 0 ? '+' : ''}${value.toFixed(digits)}${suffix}`;
}

export function searchAndSort(traders: Trader[], query: string, sortKey: SortKey): Trader[] {
    const q = query.trim().toLowerCase();
    return traders
        .filter(t => !q || t.name.toLowerCase().includes(q) || t.strategy.toLowerCase().includes(q))
        .sort((a, b) => sortKey === 'maxDrawdownPct' ? a.maxDrawdownPct - b.maxDrawdownPct : b[sortKey] - a[sortKey]);
}

export function boardStats(traders: Trader[]) {
    return {
        participants: traders.length,
        bestSharpe: Math.max(...traders.map(t => t.sharpe)),
        avgWinRatePct: traders.reduce((s, t) => s + t.winRatePct, 0) / traders.length,
        bestNavPct: Math.max(...traders.map(t => t.navPct)),
    };
}

商業邏輯: 使用者可以按績效或品質進行排名,並按人員或策略進行搜尋。摘要統計資料支援快速的交易桌層級讀取。

程式碼邏輯: 該函數重現了 Demo 的 data()stat() 行為。最大回撤採升序排序,因為在原始邏輯中,較低的回撤代表較好的風險控制。

預期結果: 按資產淨值(NAV)排序會將 Sofia 排在第一位。搜尋 crypto 會返回 Lucia Fernandez 和 Carmen Lopez。

系統設計原理解析:

  1. 排序從渲染中分離出來,因此測試可以獨立於 UI 證明排名行為。
  2. 搜尋同時使用名稱與策略,因為原始 Demo 允許使用者透過策略風格來發現交易員,而不僅僅是看名字。
  3. 最大回撤排序經過特殊處理,因為風險指標通常與報酬指標的方向相反。

步驟 5 — 實作 SVG 路徑輔助函式

建立 src/domain/charts.ts

export function svgPath(values: number[], width: number, height: number, padding = 18): string {
    if (!values.length) return '';
    const min = Math.min(...values);
    const max = Math.max(...values);
    const range = max - min || 1;
    const innerWidth = width - padding * 2;
    const innerHeight = height - padding * 2;
    return values.map((value, index) => {
        const x = padding + index * innerWidth / Math.max(1, values.length - 1);
        const y = padding + innerHeight - ((value - min) / range) * innerHeight;
        return `${index ? 'L' : 'M'}${x.toFixed(1)} ${y.toFixed(1)}`;
    }).join(' ');
}

export function histogramBars(returns: number[], width: number, height: number, padding = 18) {
    const maxAbs = Math.max(...returns.map(Math.abs), 1);
    const barWidth = (width - padding * 2) / Math.max(1, returns.length);
    const mid = height / 2;
    return returns.map((value, index) => {
        const barHeight = Math.abs(value) / maxAbs * (height / 2 - padding);
        return {
            x: padding + index * barWidth + 1,
            y: value >= 0 ? mid - barHeight : mid,
            width: Math.max(2, barWidth - 2),
            height: barHeight,
            positive: value >= 0,
        };
    });
}

商業邏輯: 圖表協助交易員比僅看數字更快理解趨勢、回撤壓力與每日報酬率分佈。

程式碼邏輯: svgPath 鏡像了原型的路徑生成器。histogramBars 將報酬率轉換為正/負長條的矩形幾何形狀。

預期結果: 相同的資產淨值(NAV)序列可以生成走勢線、權益曲線、回撤線與報酬率直方圖。

系統設計原理解析:

  1. 圖表幾何形狀在純函數中計算,因此無需瀏覽器渲染即可進行測試。
  2. 路徑函數將數值正規化到視窗(view box)。這使得同一個輔助函式可以同時支援小型走勢線與大型圖表。
  3. 直方圖長條帶有 positive 旗標,因此 UI 可以直接應用綠色或紅色樣式,而無需重新計算正負號邏輯。

步驟 6 — 實作受控的即時模擬

建立 src/domain/simulator.ts

import type { Trader } from './types';

export function nextDemoTick(traders: Trader[], random = Math.random): Trader[] {
    return traders.map(trader => {
        const bump = (random() - 0.48) * 0.18;
        const nextDailyPct = Number((trader.dailyPct + bump).toFixed(2));
        const nextNavPct = Number((trader.navPct + bump * 0.25).toFixed(2));
        return {
            ...trader,
            dailyPct: nextDailyPct,
            navPct: nextNavPct,
            series: [...trader.series.slice(1), 100 + nextNavPct],
        };
    });
}

商業邏輯: Demo 每 5 秒更新一次以模擬即時看板。開發者必須明確將其標記為模擬數據,而非真實市場數據。

程式碼邏輯: 該函數鏡像了原始的更新迴圈,但使隨機性可注入(injectable)以進行確定性測試。

預期結果: 每個 Tick 都會微調每日報酬率、微調資產淨值(NAV)、移動序列視窗,並附加一個新的合成淨值點。

系統設計原理解析:

  1. 隨機性是注入的,因為測試需要確定性的行為。這保留了即時 Demo 的感覺,又不會使測試變得不穩定(flaky)。
  2. 序列透過捨棄最舊的點來使用滾動視窗(rolling window)。這保留了圖表長度並避免了記憶體無限制增長。
  3. 該函數返回新物件,而不是修改(mutating)原始陣列,符合 React 狀態管理預期。

步驟 7 — 新增測試

建立 src/domain/__tests__/calculations.test.ts

import { describe, expect, it } from 'vitest';
import { advancedMetrics, drawdownPct, returnsPct } from '../calculations';

describe('量化計算', () => {
    it('從 NAV 計算報酬率', () => {
        expect(returnsPct([100, 110, 99]).map(x => Number(x.toFixed(2)))).toEqual([10, -10]);
    });

    it('回撤絕不為正數', () => {
        expect(drawdownPct([100, 120, 90, 130]).every(x => x <= 0)).toBe(true);
    });

    it('建立進階面板指標', () => {
        const metrics = advancedMetrics([100, 105, 99.75]);
        expect(Number(metrics.windowReturnPct.toFixed(2))).toBe(-0.25);
        expect(Number(metrics.maxDrawdownPct.toFixed(2))).toBe(-5.00);
    });
});

Kiro 提示詞範例

審查分析模組與測試。為每個 Demo 參與者序列、淨值(NAV)與最大回撤(Max DD)的排序行為、按策略關鍵字搜尋、SVG 路徑邊界、直方圖正負長條,以及注入隨機值的確定性模擬新增測試案例。

商業邏輯: 測試保護了驅動交易解讀的計算公式。

程式碼邏輯: 單元測試驗證了已知的數值範例,並可擴展以覆蓋所有參與者。

預期結果: npx vitest run 通過且 Kiro 提出了額外的覆蓋建議。

系統設計原理解析:

  1. 已知值測試可快速捕捉公式退化(regressions)。財務審查人員很容易理解這些測試。
  2. 要求 Kiro 新增特定參與者的回歸測試,使資料或公式的變更不會意外更改顯示的分析結果。
  3. 圖表測試專注於幾何邊界,因為 SVG 視覺測試成本高昂且對此工作坊而言並非必要。

最終實驗室挑戰

詢問 Kiro:

生成一份完整的分析差異報告,對比 TypeScript 模組與原始 Demo 的 JavaScript 函數:sg、cl、id、ret、dd、avg、sd、perc、path、spark、eqChart、ddChart、hist、panel、data、render、tog、stat、markets、clock 以及 5 秒更新迴圈。識別哪些已實現、哪些刻意移至 React,以及哪些仍需要測試。

完成檢查清單

  • 所有參與者與市場資料皆已定義型別。
  • 報酬率、回撤、波動度、百分位數、Calmar 比率、VaR、最佳/最差單日以及勝率皆已實作。
  • 搜尋與排序重現了 Demo 的行為。
  • SVG 路徑與直方圖幾何形狀為純函數。
  • 即時模擬已明確標記並在測試中具備確定性。
  • Kiro 已審查數學邊緣案例。
  • 測試涵蓋了計算、排序、圖表與模擬。

附錄 — 用於分析測試的完整參與者與市場覆蓋範圍

使用此檢查清單以確保分析引擎覆蓋完整的 HTML Demo,而不僅僅是範例資料列:

  • Sofia Garcia — 跨資產凸性總經阿爾法 (Cross-Asset Convex Macro Alpha):NAV +18.4%, 每日 +0.42%, 夏普比率 0.73, 獲利因子 1.8, 勝率 58%, 偏態 +0.44, 最大回撤 18。
  • Lucia Fernandez — 加密貨幣動能輪動 (Crypto Momentum Rotation):NAV +16.9%, 每日 +0.88%, 夏普比率 0.91, 獲利因子 1.7, 勝率 61%, 偏態 +0.31, 最大回撤 22。
  • Carmen Lopez — 加密貨幣套利與波動度 (Crypto Carry & Volatility):NAV +14.2%, 每日 -0.31%, 夏普比率 0.68, 獲利因子 1.6, 勝率 56%, 偏態 +0.22, 最大回撤 25。
  • Elena Martin — 全球總經趨勢跟隨 (Global Macro Trend Rider):NAV +11.8%, 每日 +0.17%, 夏普比率 0.62, 獲利因子 1.5, 勝率 54%, 偏態 +0.18, 最大回撤 17。
  • Marta Sanchez — 利率與外匯相對價值 (Rates & FX Relative Value):NAV +9.6%, 每日 +0.09%, 夏普比率 0.57, 獲利因子 1.4, 勝率 53%, 偏態 +0.09, 最大回撤 15。
  • Paula Romero — 權益因子集合 (Equity Factor Ensemble):NAV +7.1%, 每日 -0.12%, 夏普比率 0.49, 獲利因子 1.3, 勝率 52%, 偏態 -0.04, 最大回撤 14。
  • Ana Torres — 大宗商品突破系統 (Commodity Breakout System):NAV +5.4%, 每日 +0.28%, 夏普比率 0.42, 獲利因子 1.2, 勝率 51%, 偏態 +0.12, 最大回撤 19。
  • Laura Navarro — 多資產均值回歸 (Multi-Asset Mean Reversion):NAV +3.8%, 每日 -0.06%, 夏普比率 0.35, 獲利因子 1.1, 勝率 49%, 偏態 -0.11, 最大回撤 16。

市場圖卡回歸案例:

  • ES: +0.38%, 買方堆疊 (BID STACK), 序列 [20,21,20,22,23,23,24,25,24,26]
  • CL: -0.22%, 賣方成交 (OFFER HIT), 序列 [30,29,31,28,27,26,25,24,23,22]
  • GC: +0.62%, 買方堆疊 (BID STACK), 序列 [18,18.5,19,18.7,20,21,20.5,22,23,23.5]
  • BTC: +2.18%, 買方堆疊 (BID STACK), 序列 [20,22,21,24,26,25,29,28,32,34]

Kiro 完整覆蓋提示詞:

生成一個回歸測試套件,載入所有 8 個參與者資料列與所有 4 個市場圖卡。驗證摘要統計資料、每個排序鍵的排序順序、按每個策略關鍵字搜尋、每個序列的 SVG 路徑,以及每個參與者的進階面板指標。

原始 Demo 參考

本工作坊基於上傳的 aws_quant_pnl_leaderboard_v3.html Demo。該 Demo 包含一個 CME Direct 風格的深色工作空間、參與者排行榜、市場卡片、可排序/可搜尋的損益板、進階分析面板、圖表函數、響應式 CSS、即時 HKT 時鐘以及模擬的週期性資產淨值更新。所有資料均視為 Demo 預留位置資料,不構成投資建議。


進階開發者附加動手實作實驗室 — HTML 圖形分析

這些實驗室透過分析上傳的 HTML 檔案的 SVG 圖表圖形來擴展量化分析工作坊。它們專注於原型如何將資產淨值(NAV)序列、回撤陣列與每日報酬率轉化為視覺標記。它們不會重複基礎財務公式或 UI 元件遷移。

進階圖形分析目標

到本節結束時,進階開發者將能夠:

  • 解釋 HTML 如何將數值序列轉換為 SVG 路徑、區域、線條與長條。
  • 透過確定性測試驗證圖表幾何形狀。
  • 將視覺編碼邏輯與財務計算分離。
  • 建構協助審查人員理解圖形意義的圖表審查中繼資料(chart-audit metadata)。
  • 偵測由尺度、填補(padding)或邊緣案例資料引起的誤導性圖表輸出。

來自 HTML 檔案的圖表圖形清冊

上傳的 HTML 使用了幾個 JavaScript 函數來產生 SVG 圖形:

  • path(a,w,h,p) 將序列正規化為 SVG ML 指令。
  • spark(a) 渲染緊湊的資料列與市場卡片走勢線。
  • grid(w,h,p) 建立水平網格線與底部軸線。
  • eqChart(a) 渲染帶有填滿區域與 NAV 起始/結束標籤的權益曲線。
  • ddChart(a) 將回撤渲染為零軸下方的紅色水位線形狀。
  • hist(a) 圍繞中線渲染每日報酬率長條,使用獨立的正、負類別(classes)。

進階實驗室 1 — 圖表幾何契約測試

目標: 建立測試以證明 SVG 幾何形狀保持在圖表邊界內,並安全地處理平坦、過短以及劇烈波動的序列。

建立 src/domain/__tests__/chartGeometry.test.ts

import { describe, expect, it } from 'vitest';
import { svgPath, histogramBars } from '../charts';

function extractNumbers(path: string): number[] {
  return path.match(/-?\d+(\.\d+)?/g)?.map(Number) ?? [];
}

describe('SVG 圖表幾何契約', () => {
  it('將路徑座標保持在視窗填補邊界之內', () => {
    const d = svgPath([100, 102, 101, 104], 120, 32, 2);
    const numbers = extractNumbers(d);
    const xs = numbers.filter((_, index) => index % 2 === 0);
    const ys = numbers.filter((_, index) => index % 2 === 1);

    expect(Math.min(...xs)).toBeGreaterThanOrEqual(2);
    expect(Math.max(...xs)).toBeLessThanOrEqual(118);
    expect(Math.min(...ys)).toBeGreaterThanOrEqual(2);
    expect(Math.max(...ys)).toBeLessThanOrEqual(30);
  });

  it('渲染平坦序列時不會發生除以零的幾何錯誤', () => {
    const d = svgPath([100, 100, 100], 120, 32, 2);
    expect(d).toContain('M');
    expect(d).toContain('L');
    expect(d).not.toContain('NaN');
    expect(d).not.toContain('Infinity');
  });

  it('圍繞中線建立正向和負向的直方圖長條', () => {
    const bars = histogramBars([1, -2, 0.5], 680, 120, 18);
    expect(bars.some((bar) => bar.positive)).toBe(true);
    expect(bars.some((bar) => !bar.positive)).toBe(true);
  });
});

Kiro 提示詞:

為上傳的 HTML 之路徑、走勢線、權益曲線、回撤與直方圖行為生成圖表幾何測試。驗證座標邊界、無 NaN 或 Infinity、平坦序列行為、短序列行為、正/負直方圖標記,以及各個 Demo 參與者序列的繪圖一致性。

預期結果: 開發者可以重構圖表程式碼,而不會意外產生損壞的 SVG。

進階實驗室 2 — SVG 視覺編碼文件

目標: 記錄財務概念與圖形標記之間的關係,使圖表行為可供審查。

建立 docs/svg-visual-encoding.md

# SVG 視覺編碼筆記

## 走勢線 (Sparkline)

- 資料輸入:NAV 序列或市場迷你序列。
- 標記類型:單一綠線。
- 用途:緊湊的趨勢預覽。
- 風險:未顯示 Y 軸尺度,因此不應視為精確測量。

## 權益曲線 (Equity curve)

- 資料輸入:指數化(indexed)的 NAV 序列。
- 標記類型:綠線加上半透明填滿區域。
- 用途:可視化的累積績效路徑。
- 標籤:首個 NAV 值與最後一個 NAV 值。

## 回撤水位線 (Drawdown waterline)

- 資料輸入:從 NAV 衍生出的回撤百分比序列。
- 標記類型:紅線以及零軸下方的紅色填滿區域。
- 用途:顯示波峰到波谷的痛苦與復原壓力。

## 每日損益直方圖 (Daily P&L histogram)

- 資料輸入:每日報酬率序列。
- 標記類型:圍繞水平中線的垂直長條。
- 正向編碼:中線之上的綠色長條。
- 負向編碼:中線之下的紅色長條。

Kiro 提示詞:

為 HTML 圖表建立視覺編碼文件。使用資料輸入、SVG 標記類型、顏色編碼、尺度限制與審查注意事項來解釋走勢線、權益曲線、回撤水位線與每日損益直方圖。

預期結果: 圖表圖形對開發者、設計師與風險審查人員變得易於解釋。

進階實驗室 3 — 圖表審查中繼資料生成器

目標: 為每張圖表生成中繼資料,以便審查人員檢查尺度、極值、範圍、正負長條計數與標籤文字。

建立 src/domain/chartAudit.ts

import { drawdownPct, returnsPct } from './calculations';

export type SeriesAudit = {
  pointCount: number;
  min: number;
  max: number;
  range: number;
  first: number;
  last: number;
};

export type ChartAudit = {
  nav: SeriesAudit;
  drawdown: SeriesAudit;
  returns: SeriesAudit & {
    positiveCount: number;
    negativeCount: number;
    zeroCount: number;
  };
};

function auditSeries(values: number[]): SeriesAudit {
  if (!values.length) {
    return { pointCount: 0, min: 0, max: 0, range: 0, first: 0, last: 0 };
  }

  const min = Math.min(...values);
  const max = Math.max(...values);

  return {
    pointCount: values.length,
    min,
    max,
    range: max - min,
    first: values[0],
    last: values.at(-1)!,
  };
}

export function auditChartSeries(navSeries: number[]): ChartAudit {
  const drawdown = drawdownPct(navSeries);
  const returns = returnsPct(navSeries);
  const returnsAudit = auditSeries(returns);

  return {
    nav: auditSeries(navSeries),
    drawdown: auditSeries(drawdown),
    returns: {
      ...returnsAudit,
      positiveCount: returns.filter((value) => value > 0).length,
      negativeCount: returns.filter((value) => value < 0).length,
      zeroCount: returns.filter((value) => value === 0).length,
    },
  };
}

Kiro 提示詞:

為每個淨值(NAV)序列新增圖表審查中繼資料。包含點數、最小值、最大值、範圍、首/尾值、回撤範圍、報酬率範圍,以及正/負/零報酬率計數。為所有 8 個參與者序列新增測試。

預期結果: 分析審查人員無需開啟瀏覽器即可檢查圖表輸入與尺度風險。

進階實驗室 4 — 誤導性圖形邊緣案例實驗室

目標: 教導開發者識別圖形在技術上正確但視覺上具誤導性的邊緣案例。

建立 docs/misleading-chart-edge-cases.md

# 誤導性圖表邊緣案例

## 平坦序列 (Flat series)

平坦的 NAV 序列會產生一條直線,但如果忽略了標籤,視覺範圍的退化可能會使極微小的變動看起來比實際更大。

## 單點序列 (Single-point series)

單個點無法代表趨勢。圖表應渲染安全的預留位置或顯示資料不足的訊息。

## 極端離群值 (Extreme outlier)

一個巨大的跳空可能會壓縮所有其他變動,使正常的波動看起來像消失了一樣。

## 短報酬率視窗 (Short return window)

包含 14 個報酬率的直方圖具有教育意義,但不足以得出穩健的分佈結論。

## 缺少尺度標籤 (Missing scale labels)

走勢線對於快速形狀識別很有用,但不應被視為精確的風險證據。

Kiro 提示詞:

為 HTML 圖表函數建立誤導性圖表邊緣案例指南。涵蓋平坦序列、單點序列、極端離群值、短報酬率視窗、缺少 Y 軸尺度以及直方圖解讀限制。

進階實驗室 5 — 圖表渲染驗收標準

目標: 在生產環境重構驗收之前,定義圖表圖形的驗收標準。

建立 docs/chart-rendering-acceptance-criteria.md

# 圖表渲染驗收標準

## 權益曲線 (Equity curve)

- 使用與 HTML 原型相同之正規化座標邏輯。
- 包含可見的線條與填滿區域。
- 標註第一個和最後一個 NAV 值。
- 不渲染 NaN 或 Infinity。

## 回撤水位線 (Drawdown waterline)

- 渲染前回撤值必須為零或負數。
- 零軸清晰可見。
- 紅色區域隨回撤加深而向下增長。
- 最大回撤標籤與計算出的最小回撤相符。

## 每日報酬率直方圖 (Daily return histogram)

- 正向長條顯示在中線之上。
- 負向長條顯示在中線之下。
- 數值為零的長條不會引發視覺錯誤。
- 在所有支援的圖表尺寸下,長條寬度保持清晰可見。

## 走勢線 (Sparkline)

- 緊湊的圖表不包含未支援的尺度宣告。
- 保留所有參與者與市場迷你序列的趨勢形狀。

Kiro 提示詞:

為 React/SVG 重構生成圖表渲染驗收標準。包含權益曲線、回撤水位線、每日報酬率直方圖與走勢線檢查。盡可能將每個視覺斷言與確定性測試相連結。

進階最終挑戰 — SVG 圖表保真度審查

詢問 Kiro:

對上傳的 HTML 檔案進行 SVG 圖表保真度審查。對比路徑正規化、填補(padding)、權益區域閉合、網格線、回撤零軸、直方圖中線、正負長條放置、標籤以及邊緣案例行為。產出優先順序修復待辦清單。

進階圖形分析完成檢查清單

  • 圖表幾何測試驗證了邊界與無效數字保護。
  • 視覺編碼文件解釋了每種 SVG 圖表類型。
  • 圖表審查中繼資料總結了 NAV、回撤與報酬率序列。
  • 誤導性圖表的邊緣案例已記錄成冊。
  • 渲染驗收標準保護了圖表重構。