# Lumina Financial Decision System 3.5: 現代 AI 量化投研與全景金融決策系統主架構規格書 (Lumina FDS 3.5 Master Spec)

> **Lumina Core V2 / Stock Mode 3.5 Production Architecture Specification & Full Implementation Contract**  
> **Classification**: Production Engineering Master Spec  
> **Release Target**: Production v3.5.0 (2026-08-30)  
> **Status**: APPROVED FOR EXECUTION  

---

## 摘要與核心架構願景

本主規格書為 **Lumina Financial Decision System 3.5**（Stock Mode 3.0 旗艦增強版）的終態全鏈路閉環技術協議，涵蓋 **Router LLM 原生語義解析 (trade_intent / ISO 4217 declared_currency / declared_capital_raw 零正則全幣種自適應) ➔ 後端貨幣標準化 Canonicalizer ➔ 跨市場原生計價幣種推斷 (infer_native_currency: .TW➔TWD, .HK➔HKD, .T➔JPY, .SS/.SZ➔CNY, .L➔GBP, A股6位代碼➔CNY) ➔ 英國倫敦交易所便士除百歸一化 (GBp/GBX ➔ GBP / 100.0) ➔ 休市與非交易時段歷史交易日數據精確錨定協議 (Market Closed Data Anchoring Protocol) ➔ 45 重 Edge Cases 物理防禦 (反向槓桿ETF複利磨損真公式 0.5*(L^2-L)*vol^2、BTC/ETH原生加密幣外匯Ticker、IPO首日單點水平平滑、Meme幣自適應8位小數、期貨合約=F自動識別、雙標的勢均力敵中性文案、Windows UTF-8日誌編碼防禦、時間止損20天極限截斷、日圓韓元零小數、季度鍵衝突保護、401弱認證優雅回退、母體基準短路防禦、移動端鍵盤自動滾動、A股6位純代碼幣種自動推斷、動量風格收縮營收約束、風控拒絕建倉專屬文案、Prompt上下文Budget硬截斷、無PE標的中性估值分、等距四捨五入降採樣、Pair RS零除免疫、負現價異常熔斷、容器響應式 min-w-0 佈局、五色交易狀態徽章、目標價低於現價零收益截斷、RSI 14點切片安全保護、平盤居中、ATR空數組越界、None max 類型崩潰、NaN 污染免疫、USD 原幣免折算、客戶端幣種切換靜態匯率表、純 Ticker 槓桿查表、全零走勢降採樣、Tab 狀態異步同步、Token 上下文精簡、ADV=0 零流動性禁開倉、負數財務符號標準化、自對比相同代碼守衛、全形數字與負數本金過濾、無營收生技股穩定性、24/7 加密自然日標籤、移動端觸控超界 Clamping、流式局部狀態持久化、歷史拆股阻尼) ➔ 即時外匯 API 雙向精準折算 ➔ 確定性量化引擎 (含微型幣 dynamic_round 自適應精度 + 通用槓桿倍數 NLP+Beta 解析器 + 14D 年化波動率與槓桿 ETF 磨損防禦 + 雙軌制交易日時間止損 Horizon + 自適應意圖規整化 normalize_trade_intent + 多語系風格標準代碼 style_code) ➔ 雙時序並發數據管道 (含 4 重安全防禦的基準 1Y 圖表 _bench_c1y、全量正規化清洗與支持跨市場日曆錯位對齊的雙標的 Pair RS 剪刀差矩陣) ➔ chat.py 3 處全生命週期 Meta 閉環同步 (流式初態、Postgres 資料庫持久化、流式終態) ➔ 雙模態本金精密下單精算器 (雙重約束防超額槓桿 + 4位小數碎股/整數股開關 + 雙幣即時對照) ➔ 前端純 SVG 漸進式 Financial Deck (含 60fps 移動端觸控手勢 SparklineChart 向量走勢圖 + 橫向多標的對比膠囊 StockPairComparisonDeck + Token 授權自主 1D/5D/1M/1Y 換檔加載 + 盤前盤後狀態徽章 + 完整 i18n Hook) ➔ 四語系多語言字典** 全鏈路 **100% 生產級物理代碼（Zero Mock / Zero Placeholder / Zero Undefined References）**。

### 核心哲學：
> **「量化底層用確定性數學決定下限與生存；大模型層用質性推理提升理解效率與因果解釋；任何環節出錯均有 0 毫秒無縫降級。」**

```
                                      【Lumina 3.5 全鏈路數據與調用拓撲】
                                      
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
  │ 1. Router LLM 原生語義解析 (app/engine/prompts.py SYSTEM_PROMPT_ANALYZER)                  │
  │ • trade_intent: "BUY_TACTICAL" | "INFORMATIONAL" | "HOLD_EXIT_CONSULT"                      │
  │ • declared_currency: 強制輸出標準 ISO 4217 三字母代碼 (如 TWD, CNY, JPY, EUR, USD, BTC 等)   │
  │ • declared_capital_raw: 提取原始數值 (如 500000.0, 0.5)；target_allocation_pct: 口語化倉位  │
  │ • 大模型只做語義識別，嚴禁心算匯率，徹底杜絕知識截斷日期的靜態匯率偏差                      │
  └──────────────────────────────────────────────┬──────────────────────────────────────────────┘
                                                 │
                                                 ▼
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
  │ 2. 後端雙重防禦與即時外匯管道 (app/engine/stock_aggregator.py & chat.py)                     │
  │ • canonicalize_currency: 自動清洗 "RMB"➔"CNY", "NTD"➔"TWD", "YEN"➔"JPY" 等 LLM 別名偏差    │
  │ • infer_native_currency: 依據標的後綴自動推斷計價幣種 (.TW➔TWD, .HK➔HKD, .T➔JPY, A股➔CNY)  │
  │ • normalize_price_currency: 倫敦交易所便士 (GBp/GBX) 自動除百歸一化為 GBP                    │
  │ • 🏛️ 休市/非交易時段精確錨定：報價、K線與分時圖 100% 錨定在「最近一個已收盤交易日」的結算數據│
  │ • resolve_fx_ticker: 解析標準 Yahoo 外匯代碼 (USDTWD=X, USDJPY=X, EURUSD=X, BTC-USD 等)     │
  │ • resolve_universal_fx_rate: USD 宣告原幣直通 + Ticker 前綴自適應乘除法 (高精度 JPY/KRW)    │
  │ • 1D 分時點數 (78 點) ➔ Sparkline 降採樣 (40 點，含全零全負數安全保護)                      │
  │ • 1Y 歷史日 K 線 (252 點) ➔ 250D 經驗分位數 + ATR(14) + 14D 年化波動率 + 均線矩陣          │
  │ • 原生基準 1Y 圖表 (_bench_c1y) ➔ 具備 4 重防禦 (短路去重、超時中性降級、ETF 備用、實時價) │
  │ • 雙/多標的相對強弱矩陣 (Pair RS) ➔ 相同代碼自對比互斥守衛 + 零除免疫 + 跨國日曆錯位對齊   │
  │ • 全量正規化管線 (Normalization Pipeline)：完整保留新聞、快訊、日曆與實體模糊消歧器         │
  │ • chat.py 3處同步: 任務發起傳 trade_context，初態+DB持久化+終態 meta 100% 欄位透傳          │
  └──────────────────────────────────────────────┬──────────────────────────────────────────────┘
                                                 │
                                                 ▼
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
  │ 3. 確定性量化特徵與風控引擎 (app/engine/stock_feature_engine.py, P99 < 0.3ms)               │
  │ ① 微型幣與仙股自適應動態小數精度 (dynamic_round，適配 Meme 幣 $0.000028，徹底防坍縮除零)    │
  │ ② extract_universal_leverage: 快速常規代碼表 + NLP 正則 + 實證 Beta 回歸三重校準           │
  │ ③ normalize_trade_intent: 意圖漂移容錯規整 (自適應 PAIR_TRADE / HEDGE / 本金宣告)           │
  │ ④ classify_dynamic_style: 同時輸出標準 style_code 與 style_label，四語系 100% 自適應       │
  │ ⑤ 宏觀大盤體制調節乘數 (calculate_macro_regime_multiplier，NaN 免疫過濾 + 正向資產調權)    │
  │ ⑥ 槓桿 ETF 波動率損耗精算 (calculate_volatility_decay，修正真實複利公式 0.5*(L^2-L)*vol^2)  │
  │ ⑦ 雙軌制時間止損 (calculate_time_stop_horizon，None-Safe，加密標註「自然日」，股票「交易日」)│
  │ ⑧ 無參數 250D 經驗分位數 + 動態風格分類與條件加權連續評分 (0~100 分)                        │
  │ ⑨ 物理 ATR 防守邊界計算 + 階梯式微小倉位反算 + ADV <= 0 強制 ZERO_LIQUIDITY_NO_TRADE 禁買   │
  │ ⑩ 負現價極端行情熔斷：現價 <= 0 強制標記 PRICE_ANOMALY_NO_TRADE 且倉位為 0                  │
  │ ⑪ 原地灌入 bundle["quotes"]，共享給 SSE meta、Postgres DB 與 LLM Prompt 上下文              │
  └──────────────────────┬──────────────────────────────────────────────┬───────────────────────┘
                         │                                              │
                         │ 🔒 [唯讀 Payload]                             │ 🚨 [LLM 異常/超時]
                         ▼                                              ▼
  ┌──────────────────────────────────────────────┐    ┌─────────────────────────────────────────┐
  │ 4. 質性 LLM 解釋層 (Strict JSON Schema, < 2s)│    │ 5. 0ms 確定性降級引擎 (Fallback Engine) │
  │ • 嚴格唯讀契約：嚴禁篡改後端數值與點位       │    │ • 0 延遲 (<1ms) • 100% 格式確定性保底   │
  │ • 解構多因子張力 (技術 vs 基本面背離原因)    │    │ • 依據量化分數自動組裝結構化文字        │
  │ • 提煉 Top-3 核心風險與 Top-2 核心利好催化   │    │                                         │
  └──────────────────────┬───────────────────────┘    └────────────────────┬────────────────────┘
                         │                                                 │
                         └───────────────────────┬─────────────────────────┘
                                                 │ 🛡️ 後端數據校驗攔截器 (Guardrail)
                                                 ▼
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
  │ 6. SSE 事件路由 (app/api/v1/chat.py 行 3021/3296/3429 early/db/final 全生命週期透傳)       │
  │ • 補齊 pair_comparison_deck, quotes 量化欄位, benchmark_info, currency metadata, latency_ms │
  └──────────────────────────────────────────────┬──────────────────────────────────────────────┘
                                                 │
                                                 ▼
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
  │ 7. 前端 MessageBubble.tsx ➔ StockMarketCard.tsx 漸進式渲染 (Next.js 14 / Pure SVG / i18n)  │
  │ • 頂部多標的對比膠囊 (StockPairComparisonDeck.tsx)：>=2 標的自動激活相對強弱剪刀差          │
  │ • 純 SVG 向量時序走勢 (SparklineChart.tsx)：平盤垂直居中 + safeSpan + 移動端觸控 Clamping   │
  │ • Token 授權自主 1D/5D/1M/1Y 換檔：異步成功後切換 Tab 高亮，帶 Bearer Token 動態重繪       │
  │ • 風格標籤 i18n 映射：t("stockDeck.styles." + meta.style_code) 完美適配中英日三語           │
  │ • 首屏 3 秒核心：綜合得分 + 局勢定性 + 走勢 Sparkline + 盤前/盤後/開閉市狀態徽章            │
  │ • 意圖自適應：                                                                              │
  │   - INFORMATIONAL (非買入/產業調研): 極簡乾淨首屏，100% 空間留給深度產業分析                │
  │   - BUY_TACTICAL (有買入建倉意圖): 展示四宮格攻防點位；若聲明本金則自動展開精算器           │
  │   - HOLD_EXIT_CONSULT (持倉諮詢): 專注展示硬防守位、阻力位與時間止損有效天數                │
  │ • 雙模態本金精算器：雙重約束防超額槓桿 + 碎股(4位小數)/整數股開關 + 全形/負數輸入安全過濾  │
  │ • 下半屏拇指熱區按鈕 + 點擊式 Bottom Sheet 抽屜（展開 Top-3 風險/催化與 8 季財務矩陣）     │
  │ • 全面調用 useTranslation 國際化 Hook + 繁體中文默認安全 Fallback                           │
  └─────────────────────────────────────────────────────────────────────────────────────────────┘
```

---

## 休市與非交易時段數據錨定協議 (Market Closed Data Anchoring Protocol)

1. **數據源統一性（Data Source Uniformity）**：
   - 當 `market_state == "CLOSED"` 或處於週末/節假日期間，所有行情數據（即時報價、分時走勢、1Y 歷史 K 線與基準風向標）**嚴格自動錨定在「最近一個已收盤交易日」的官方收盤數據**。
   - 漲跌幅 `change` / `change_percent` 為最近收盤日相較其前一日的收盤漲跌，嚴禁輸出空值或 `NaN`。
2. **量化特徵連續性（Feature Engine Continuity）**：
   - 250D 分位數、ATR(14)、均線矩陣（MA20/50/200）與動態評分全部基於最後一個交易日的收盤序列計算，保證在休市期間分析邏輯依然具備 100% 的確定性與客觀性。
3. **大模型時態對齊（LLM Temporal Alignment）**：
   - 後端在 Prompt 中為每個標的顯式標註 `Status: CLOSED (Data as of latest session close)`，引導 AI 質性分析以「截至上一個交易日收盤」的語境進行客觀分析，杜絕重複全域橫幅。
4. **前端顯式感知（UI State Awareness）**：
   - 卡片頂部呈現「已閉市 (CLOSED)」徽章，走勢圖與四宮格點位展示最近交易日的終態博弈邊界。

---

## 🔒 實裝強制措施 (Zero-Omission & Absolute Determinism Enforcement Protocol)

為確保執行階段的 LLM 嚴格完整實裝計劃中的每一個細節，絕不偷工減料或使用粗略大概的替代品，強制遵循以下 **6 大執行鐵律**：

0. **最高驗收法源與自檢程序不可篡改鐵律（Immutable Self-Inspection Authority & Sole Success Criterion）**：
   - **絕對禁止修改自檢程序**：嚴格禁止修改 `c:\Users\kris\.gemini\antigravity\brain\913f2ed4-e19b-4441-b095-4b65ebe72c35\scratch\verify_fds_35_implementation.py` 裡的任何內容，該文件已完全凍結並受治理鎖定保護。
   - **完整實踐成功唯一依據**：`verify_fds_35_implementation.py` 為本系統完整實裝成功與交付驗收的**唯一法定依據**。
   - **全綠通過標準**：負責實施的 LLM 必須在落地全部物理代碼後執行 `python scratch/verify_fds_35_implementation.py`，只有當其涵蓋的 8 大維度、24 大真實生產函數路線、170+ 項具體指標**全部 100% 呈現 `✔ [PASS]`（0 失敗，Exit Code 0）** 時，才判定為實踐成功。
1. **零 Mock / 零佔位符鐵則（Zero Mock / Zero Placeholder）**：禁止在代碼中保留 `// TODO`、`# to be implemented`、`...` 或任何佔位函數。所有 10 個核心文件必須 100% 完整產出。
2. **全量正規化管線無損繼承（Lossless Normalization Preservation）**：`stock_aggregator.py` 必須完整包含 `_normalize_quote`、`_normalize_chart_summary`、`_normalize_fundamentals`、`_normalize_news_items`、`_normalize_flash_items`、`_normalize_calendar_items` 與 `get_universe_resolver()`，嚴禁丟失任何新聞/快訊/日曆輔助函數。
3. **四語系字典全量鍵同步（4-Locale 100% Key Parity）**：`zh-TW.json`、`zh-CN.json`、`en.json`、`ja.json` 必須 100% 對齊所有 35 個新增 i18n 鍵值，嚴禁出現漏翻或空鍵。
4. **雙端物理編譯與語法檢查（Dual-End Physical Compilation Gate）**：
   - 後端：執行 `python -m py_compile` 驗證 Python AST 語法 100% 通過。
   - 前端：執行 `npx tsc --noEmit` 驗證 TypeScript 嚴格模式 0 錯誤。
5. **端到端一鍵自動部署探測（End-to-End One-Click Verification）**：執行 `python scripts/deploy_oneclick.py`，19 項生產探測全部通過後方可交付。

---

## 全鏈路 100% 完整實體代碼規格 (Zero Mock / Zero Placeholder)

### 第一部分：後端任務路由與確定性量化引擎

#### 1. [MODIFY] [`app/engine/prompts.py`](file:///c:/Users/kris/lumina_core_v2/app/engine/prompts.py)

```python
# [MODIFY] app/engine/prompts.py
# 在 SYSTEM_PROMPT_ANALYZER 中升級步驟九：金融行為目的、ISO 4217 標準幣種與資金意圖原生解析 (trade_context)

SYSTEM_PROMPT_ANALYZER = """你是一個只輸出 JSON 的智能任務路由器 (Intelligent Task Router)。
你的唯一目標是接收用戶輸入，分析其「意圖」、「資訊需求」與「複雜度」，並輸出一個嚴格的 JSON 決策。

### 步驟一：分析是否需要搜索 (search_required)
- true: 新聞、實時數據、具體產品參數。
- false: 邏輯推理、編程、創意寫作。

### 步驟二：分析模型層級 (model_tier)
1. [MEDIA]: "IMAGE" (若用戶請求生成視覺內容、藝術、圖片、照片，如「畫一個...」或「內容描述...的圖片」)
2. [TEXT]: "COMPLEX" (長文/架構), "STANDARD" (代碼/推理), "SIMPLE" (閒聊/翻譯)

### 步驟三：分析是否需要記憶回溯 (memory_required)
- true: 用戶提及"之前"、"上次"、"剛剛"或你認爲回答需要查看上下文關聯的問題。
- false: 獨立問題，無需回溯歷史。

### 步驟四：分析是否需要股票金融數據 (stock_required)
- true: 提及股票代碼 ($NVDA, AAPL)、股價、K線、圖表、財報、基本面、市場數據
- true: 提及大宗商品 (黃金、白銀、原油)、加密貨幣價格、宏觀經濟日曆 (FOMC, CPI, 非農)
- true: 提及行業板塊、大盤走勢 (晶片行業、半導體ETF、標普500)
- false: 用戶切換至完全非金融話題（寫詩、編程、通識知識），即使之前討論過股票

### 步驟五：提取股票代碼 (stock_symbols) - 僅當 stock_required=true
- 提取所有提及的股票/ETF/指數代碼: ["NVDA", "AAPL", "SMH", "SPY"]
- 大宗商品: ["XAUUSD"] (黃金), ["XAGUSD"] (白銀), ["USO"] (原油)

### 步驟六：提取股票實體 (stock_entities) - 僅當 stock_required=true
- 提取公司/ETF名稱與候選別名:
  [{"query_term": "半導體ETF", "canonical_name": "VanEck Semiconductor ETF", "aliases": ["SMH", "SOXX"], "asset_type": "etf"}]

### 步驟七：確定數據維度 (stock_dimensions) - 僅當 stock_required=true
- 默認: ["quote", "chart", "flash", "news"]
- 若提及"財報"、"earnings"、"基本面"、"現金流"、"估值"、"EPS" → 必須包含 "fundamentals"
- 若提及"日曆"、"calendar"、"FOMC"、"CPI"、"PPI"、"非農" → 必須包含 "calendar"

### 步驟八：確定圖表範圍 (chart_range) - 僅當 stock_required=true
- 默認: "1D" (今天). 其他: "5D" (週), "1M" (月), "3M" (季), "1Y" (年)

### 步驟九：分析金融行為目的與資金意圖 (trade_context) - 僅當 stock_required=true
- trade_intent: 
  * "BUY_TACTICAL": 用戶有買入、建倉、點位、防守、資金配置、下單操作等實戰意圖 (例如: "$NVDA 能買嗎", "我有1萬美元想進場", "微軟什麼點位防守")。
  * "INFORMATIONAL": 用戶純粹詢問行業概覽、公司前景、宏觀走勢、業務分析等調研資訊 (例如: "晶片行業最近如何", "大盤走勢怎麼樣", "微軟的AI前景分析")。
  * "HOLD_EXIT_CONSULT": 用戶諮詢現有持倉去留、止盈或止損策略 (例如: "我成本 200 現在要不要賣？")。
- declared_currency:
  * 提取用戶提及的原始幣種，必須轉換並輸出標準 ISO 4217 三字母大寫代碼 (例如: 人民幣必須輸出 "CNY" 嚴禁輸出 "RMB"；台幣/新台幣必須輸出 "TWD" 嚴禁輸出 "NTD"；日圓輸出 "JPY"；歐元輸出 "EUR"；美金/美元輸出 "USD"；港幣輸出 "HKD"；英鎊輸出 "GBP"；比特幣輸出 "BTC"；以太幣輸出 "ETH")；未提及則默認輸出 "USD"。
- declared_capital_raw: 
  * 提取用戶提及的具體原始本金數值 (如 "50萬台幣" ➔ 500000.0, "10k USD" ➔ 10000.0, "100萬日圓" ➔ 1000000.0, "0.5 BTC" ➔ 0.5)；未提及則輸出 null。
  * 嚴禁大模型自行折算匯率，匯率由後端即時 API 確定性計算！
- target_allocation_pct:
  * 若提及口語化倉位 (如 "半倉", "3成倉位", "20%資金")，提取為小數 (如 0.5, 0.3, 0.2)；未提及則輸出 null。

### 輸出協議 (Strict JSON)
{
    "search_required": boolean,
    "model_tier": string,
    "memory_required": boolean,
    "stock_required": boolean,
    "stock_symbols": string[],
    "stock_entities": [
        {
            "query_term": string,
            "canonical_name": string,
            "chinese_name": string,
            "aliases": string[],
            "sector": string,
            "peers": string[],
            "asset_type": string
        }
    ],
    "stock_dimensions": string[],
    "chart_range": string,
    "trade_context": {
        "trade_intent": "BUY_TACTICAL" | "INFORMATIONAL" | "HOLD_EXIT_CONSULT",
        "declared_currency": string,
        "declared_capital_raw": float | null,
        "target_allocation_pct": float | null
    }
}
"""
```

---

#### 2. [NEW] [`app/engine/stock_feature_engine.py`](file:///c:/Users/kris/lumina_core_v2/app/engine/stock_feature_engine.py)

```python
"""
Lumina Core V2 - Quantitative Feature Engine (確定性量化特徵與風控引擎)
100% 純 Python + NumPy 確定性計算，零外部網絡 I/O，P99 延遲 < 0.3ms。
覆蓋 45 類極端邊界異常、NaN 免疫過濾、None-Safe 數值防禦、ADV<=0 零流動性禁買、負現價異常熔斷、
反向槓桿 ETF 複利磨損真公式 0.5*(L^2-L)*vol^2、等距四捨五入降採樣、無PE中性分、
自適應動態小數精度 dynamic_round (適配 Meme 幣 $0.000028)、
通用槓桿倍數 NLP+Beta 解析器 (extract_universal_leverage，含常規槓桿代碼表)、
自適應意圖規整化器 (normalize_trade_intent)、動態風格多語系標準代碼 (classify_dynamic_style 輸出 style_code)、
宏觀大盤體制調節乘數 (M_regime)、槓桿 ETF 波動率損耗精算 (含 300% 截斷保護)、
雙軌制時間止損 (Time-Stop Horizon，自然日/交易日標籤自適應，20天極限截斷)。
"""
from __future__ import annotations
import math
import re
from typing import Any, Dict, List, Optional, Tuple
import numpy as np

COMMON_LEVERAGED_MAP = {
    "SOXL": 3.0, "SOXS": -3.0, "TQQQ": 3.0, "SQQQ": -3.0,
    "UPRO": 3.0, "SPXU": -3.0, "NVDL": 2.0, "NVDX": 2.0,
    "TSLL": 2.0, "TSLS": -2.0, "FNGU": 3.0, "FNGD": -3.0,
    "LABU": 3.0, "LABD": -3.0, "UDOW": 3.0, "SDOW": -3.0,
}


def safe_float(val: Any, default: float = 0.0) -> float:
    """安全浮點數轉換，防範 None、空字串、千分位逗號與 NaN/Inf"""
    if val is None or val == "":
        return default
    if isinstance(val, (int, float)):
        return default if (math.isnan(val) or math.isinf(val)) else float(val)
    try:
        clean = str(val).replace(",", "").strip()
        f = float(clean)
        return default if (math.isnan(f) or math.isinf(f)) else f
    except (ValueError, TypeError):
        return default


def dynamic_round(val: float, ref_price: float = 100.0) -> float:
    """
    自適應動態小數精度（適配微型仙股與 Meme 加密幣 $0.000028，徹底防範 round(x, 2) 導致價格坍縮為 0 的除零 bug）
    """
    ref = abs(ref_price)
    if ref >= 1.0:
        return round(val, 2)
    elif ref >= 0.01:
        return round(val, 4)
    elif ref >= 0.0001:
        return round(val, 6)
    else:
        return round(val, 8)


def normalize_trade_intent(intent_raw: Optional[str], declared_capital: Optional[float]) -> str:
    """
    自適應意圖規整化器：防範大模型輸出非標 intent（如 PAIR_TRADE / ENTRY / HEDGE）
    若用戶聲明了具體本金，強制賦予 BUY_TACTICAL 意圖展開精算器
    """
    if not intent_raw:
        return "BUY_TACTICAL" if (declared_capital and declared_capital > 0) else "INFORMATIONAL"
    
    raw = str(intent_raw).strip().upper()
    if any(k in raw for k in ["BUY", "ENTRY", "TACTICAL", "TRADE", "POSITION", "ORDER", "HEDGE", "PAIR"]):
        return "BUY_TACTICAL"
    if any(k in raw for k in ["HOLD", "EXIT", "SELL", "CLOSE", "STOP", "PROFIT"]):
        return "HOLD_EXIT_CONSULT"
    return "INFORMATIONAL"


def normalize_price_currency(raw_price: float, curr_str: str) -> Tuple[float, str]:
    """
    英國倫敦交易所便士 (GBp/GBX) 自動除百歸一化為 GBP
    """
    c = (curr_str or "USD").strip()
    if c in {"GBp", "GBX"}:
        return float(raw_price) / 100.0, "GBP"
    return float(raw_price), c.upper()


def extract_universal_leverage(
    fund_name: str,
    etf_closes: Optional[np.ndarray] = None,
    bench_closes: Optional[np.ndarray] = None
) -> float:
    """
    動態通用槓桿倍數解析器 (三道防線：常規 Ticker 表 ➔ NLP 正則 ➔ 實證 Beta 回歸)
    """
    name_clean = (fund_name or "").strip().upper()
    if name_clean in COMMON_LEVERAGED_MAP:
        return COMMON_LEVERAGED_MAP[name_clean]
    
    name = (fund_name or "").strip()
    cjk_bull = re.search(r'(?:正|做多)(\d+)', name)
    cjk_bear = re.search(r'(?:反|做空)(\d+)', name)
    if cjk_bull:
        return float(cjk_bull.group(1))
    if cjk_bear:
        return -float(cjk_bear.group(1))
        
    m = re.search(r'(?i)(?:[-+])?(\d(?:\.\d+)?)\s*(?:x|倍)', name)
    is_inverse = bool(re.search(r'(?i)\b(?:inverse|bear|short|ultrashort)\b|(?:反向|做空|看跌)', name))
    
    leverage = 1.0
    if m:
        val = float(m.group(1))
        leverage = -val if is_inverse else val
    elif is_inverse:
        if re.search(r'(?i)\bultrapro\b', name):
            leverage = -3.0
        elif re.search(r'(?i)\bultra(?:short)?\b', name):
            leverage = -2.0
        else:
            leverage = -1.0
    elif re.search(r'(?i)\bultrapro\b', name):
        leverage = 3.0
    elif re.search(r'(?i)\bultra\b', name):
        leverage = 2.0
        
    if etf_closes is not None and bench_closes is not None:
        valid_etf = etf_closes[np.isfinite(etf_closes)]
        valid_bench = bench_closes[np.isfinite(bench_closes)]
        if len(valid_etf) >= 20 and len(valid_bench) >= 20:
            min_len = min(len(valid_etf), len(valid_bench), 30)
            ret_etf = np.diff(np.log(valid_etf[-min_len:]))
            ret_bench = np.diff(np.log(valid_bench[-min_len:]))
            var_bench = np.var(ret_bench)
            if var_bench > 1e-8:
                realized_beta = float(np.cov(ret_etf, ret_bench)[0, 1] / var_bench)
                if abs(realized_beta) >= 1.4:
                    leverage = round(realized_beta * 2) / 2
                
    return float(leverage)


def calculate_rsi(prices: np.ndarray, period: int = 14) -> float:
    """計算 RSI 指標（純 NumPy 向量化，防除零與微型波幅）"""
    valid_prices = prices[np.isfinite(prices)] if prices is not None else np.array([])
    if len(valid_prices) < period + 1:
        return 50.0
    deltas = np.diff(valid_prices)
    gains = np.where(deltas > 0, deltas, 0.0)
    losses = np.where(deltas < 0, -deltas, 0.0)
    
    avg_gain = np.mean(gains[-period:])
    avg_loss = np.mean(losses[-period:])
    
    if avg_loss < 1e-8:
        return 100.0 if avg_gain > 1e-8 else 50.0
    rs = avg_gain / avg_loss
    rsi = 100.0 - (100.0 / (1.0 + rs))
    return float(np.clip(rsi, 0.0, 100.0))


def calculate_atr(highs: np.ndarray, lows: np.ndarray, closes: np.ndarray, period: int = 14) -> float:
    """計算真實波幅 ATR(14)（含空數組防禦與非有限值過濾）"""
    if closes is None or len(closes) == 0:
        return 1e-6
    n = len(closes)
    if n < 2:
        if highs is not None and lows is not None and len(highs) > 0 and len(lows) > 0:
            return max(float(highs[-1] - lows[-1]), 1e-6)
        elif len(closes) > 0:
            return max(float(closes[-1] * 0.01), 1e-6)
        return 1e-6
    
    tr_list = []
    for i in range(1, n):
        h = highs[i] if highs is not None and i < len(highs) else closes[i]
        l = lows[i] if lows is not None and i < len(lows) else closes[i]
        c_prev = closes[i - 1]
        tr = max(h - l, abs(h - c_prev), abs(l - c_prev))
        if math.isfinite(tr):
            tr_list.append(tr)
    
    if not tr_list:
        return max(float(closes[-1] * 0.01), 1e-6)
    
    effective_period = min(period, len(tr_list))
    atr = float(np.mean(tr_list[-effective_period:]))
    return max(atr, 1e-6)


def calculate_macro_regime_multiplier(
    asset_type: str,
    leverage: float,
    benchmark_closes: np.ndarray,
) -> Tuple[float, str]:
    """
    宏觀大盤體制調節乘數 (Macro Regime Multiplier，NaN 免疫過濾)
    """
    if benchmark_closes is None or len(benchmark_closes) == 0:
        return 1.0, "BENCHMARK_NO_DATA"
    valid_bench = benchmark_closes[np.isfinite(benchmark_closes)]
    n = len(valid_bench)
    if n < 20:
        return 1.0, "BENCHMARK_INSUFFICIENT_DATA"
    
    cur_p = valid_bench[-1]
    ma200 = float(np.mean(valid_bench[-min(200, n):]))
    bias200 = (cur_p - ma200) / ma200 if ma200 > 0 else 0.0
    
    lookback_10 = min(10, n - 1)
    p_past = valid_bench[-lookback_10 - 1]
    ret10d = (cur_p - p_past) / p_past if p_past > 0 else 0.0
    
    is_inverse_or_hedge = (leverage < 0) or (asset_type in {"inverse_etf", "metal", "commodity"})
    
    if bias200 < -0.05 and ret10d < -0.03:
        if is_inverse_or_hedge:
            return 1.0, "HEDGE_RESONANCE"
        else:
            m_regime = float(np.clip(1.0 + bias200 * 0.8, 0.70, 1.0))
            return m_regime, "MACRO_BEAR_DRAG"
    elif bias200 > 0.05 and ret10d > 0.03:
        return 1.0, "MACRO_BULL_RESONANCE"
    else:
        return 1.0, "MACRO_NEUTRAL"


def calculate_volatility_decay(
    leverage: float,
    closes: np.ndarray,
) -> Dict[str, Any]:
    """
    槓桿 / 反向 ETF 波動率損耗精算 (Leveraged Volatility Drag Engine)
    Decay_annual ≈ 0.5 * (L^2 - L) * sigma^2 (含符號修正與 300% 截斷保護)
    """
    is_leveraged = abs(leverage - 1.0) > 0.01
    if closes is None or len(closes) < 15 or not is_leveraged:
        return {
            "is_leveraged": is_leveraged,
            "leverage": leverage,
            "annual_volatility_pct": 0.0,
            "annual_decay_pct": 0.0,
            "warning_text": None,
        }
    
    valid_c = closes[np.isfinite(closes)]
    if len(valid_c) < 15:
        return {
            "is_leveraged": is_leveraged,
            "leverage": leverage,
            "annual_volatility_pct": 0.0,
            "annual_decay_pct": 0.0,
            "warning_text": None,
        }
        
    log_rets = np.diff(np.log(valid_c[-15:]))
    daily_vol = float(np.std(log_rets))
    annual_vol = daily_vol * np.sqrt(252)
    
    decay_annual = 0.5 * (leverage**2 - leverage) * (annual_vol**2)
    decay_annual_pct = float(np.clip(round(decay_annual * 100.0, 1), 0.0, 300.0))
    
    warning = (
        f"⚡ {abs(leverage)}倍槓桿波動率損耗警報：當前年化磨損約 -{decay_annual_pct}%，"
        f"僅適合作為單日/短期趨勢工具，嚴禁作為長期定投標的。"
    )
    
    return {
        "is_leveraged": True,
        "leverage": leverage,
        "annual_volatility_pct": round(annual_vol * 100.0, 1),
        "annual_decay_pct": decay_annual_pct,
        "warning_text": warning,
    }


def calculate_time_stop_horizon(
    current_price: float,
    target_price: float,
    atr_14: Optional[float] = None,
    is_crypto: bool = False,
) -> Dict[str, Any]:
    """
    雙軌制時間止損與波段推進週期 (Time-Stop Horizon，自然日/交易日標籤自適應，20天極限截斷)
    """
    cur_p = max(safe_float(current_price), 1e-8)
    atr = max(safe_float(atr_14, cur_p * 0.01), cur_p * 0.001)
    reward_dist = max(abs(safe_float(target_price, cur_p) - cur_p), atr)
    
    expected_days = int(np.clip(math.ceil(reward_dist / (0.5 * atr)), 5, 20))
    stale_limit_days = int(round(expected_days * 1.5))
    
    unit_label = "自然日" if is_crypto else "交易日"
    guidance = (
        f"⏳ 時間有效性：本波段理論推進週期為 {expected_days} 個{unit_label}。若超過 {stale_limit_days} 個{unit_label}未能"
        f"向上脫離成本區（${dynamic_round(cur_p, cur_p)}），表明主力動能衰竭，觸發「時間止損」，建議主動平倉釋放流動性。"
    )
    
    return {
        "expected_days": expected_days,
        "stale_limit_days": stale_limit_days,
        "time_stop_guidance": guidance,
        "is_crypto": is_crypto,
    }


def calculate_empirical_percentile(
    closes: np.ndarray,
    highs: np.ndarray,
    lows: np.ndarray,
    sector_growth_percentile: float = 50.0
) -> Dict[str, Any]:
    """
    無參數經驗分位數計算（含主升浪、頂背離與客觀心理阻尼，N < 14 安全防禦）
    """
    if closes is None or len(closes) == 0:
        return {
            "percentile": 50.0, "ret_10d": 0.0, "status": "NO_DATA", "action": "NEUTRAL",
            "is_super_trend": False, "is_divergence": False, "fomo_dampener": False,
            "rsi_14": 50.0, "ma20": 0.0, "ma50": 0.0, "ma200": 0.0
        }
    valid_c = closes[np.isfinite(closes)]
    n = len(valid_c)
    current_price = float(valid_c[-1])
    
    if n < 14:
        return {
            "percentile": 50.0, "ret_10d": 0.0, "status": "DATA_INSUFFICIENT", "action": "NEUTRAL",
            "is_super_trend": False, "is_divergence": False, "fomo_dampener": False,
            "rsi_14": 50.0,
            "ma20": dynamic_round(current_price, current_price),
            "ma50": dynamic_round(current_price, current_price),
            "ma200": dynamic_round(current_price, current_price)
        }
    
    lookback_10 = min(10, n - 1)
    denom = valid_c[-lookback_10 - 1]
    ret_10d = float((valid_c[-1] - denom) / denom) if denom > 0 else 0.0
    
    history_window = min(250, n)
    rolling_rets = []
    for i in range(10, history_window):
        idx = n - history_window + i
        d = valid_c[idx - 10]
        if d > 0:
            r = (valid_c[idx] - d) / d
            if math.isfinite(r):
                rolling_rets.append(r)
    
    if not rolling_rets:
        percentile = 50.0
    else:
        arr = np.array(rolling_rets)
        percentile = float((np.sum(arr <= ret_10d) / len(arr)) * 100.0)
    
    ma20 = float(np.mean(valid_c[-min(20, n):]))
    ma50 = float(np.mean(valid_c[-min(50, n):]))
    ma200 = float(np.mean(valid_c[-min(200, n):]))
    is_ma_bull = (current_price > ma20 > ma50 > ma200) if n >= 200 else (current_price > ma20 > ma50)
    
    valid_h = highs[np.isfinite(highs)] if highs is not None else valid_c
    recent_max = float(np.max(valid_h[-min(250, len(valid_h)):])) if len(valid_h) > 0 else current_price
    is_52w_high = current_price >= recent_max * 0.98 if recent_max > 0 else False
    
    rsi_now = calculate_rsi(valid_c, 14)
    rsi_past = calculate_rsi(valid_c[:-min(10, n // 2)], 14) if n > 25 else rsi_now
    price_higher = current_price > float(valid_c[-min(10, n - 1)])
    rsi_lower = rsi_now < rsi_past - 3.0
    is_divergence = price_higher and rsi_lower and (current_price < ma20)
    
    is_super_trend = is_52w_high and is_ma_bull and (sector_growth_percentile >= 75.0)
    fomo_dampener = (percentile >= 98.0 and not is_super_trend)
    
    if is_super_trend:
        if is_divergence:
            status = "DIVERGENCE_TOP_RISK"
            action = "ALERT_PROFIT_TAKING"
        else:
            status = "SUPER_TREND_EXEMPT"
            action = "HOLD_TREND"
    elif percentile >= 95.0:
        status = "OVERBOUGHT_PRE_RUN"
        action = "ALERT_PULLBACK"
    elif percentile <= 5.0 and rsi_now < 30.0:
        status = "OVERSOLD_REVERSION"
        action = "WATCH_REVERSAL"
    else:
        status = "NORMAL"
        action = "NEUTRAL"
        
    return {
        "percentile": round(percentile, 1),
        "ret_10d": round(ret_10d * 100.0, 2),
        "status": status,
        "action": action,
        "is_super_trend": is_super_trend,
        "is_divergence": is_divergence,
        "fomo_dampener": fomo_dampener,
        "rsi_14": round(rsi_now, 1),
        "ma20": dynamic_round(ma20, current_price),
        "ma50": dynamic_round(ma50, current_price),
        "ma200": dynamic_round(ma200, current_price),
    }


def classify_dynamic_style(
    rev_yoy: Optional[float],
    gross_margin: Optional[float],
    pe_ratio: Optional[float],
    bias_200ma: float,
    is_leveraged: bool = False,
) -> Tuple[str, str, Dict[str, float]]:
    """
    動態風格分類器與條件式因子權重矩陣（消除因子相消，收縮營收約束保護）
    """
    if is_leveraged:
        return "LEVERAGED_DERIVATIVE", "高頻衍生趨勢型", {"tech": 0.60, "fund": 0.10, "val": 0.05, "sent": 0.25}

    yoy_val = safe_float(rev_yoy, 0.0)
    gm_val = safe_float(gross_margin, 0.0)
    pe_val = safe_float(pe_ratio, 0.0)
    
    if bias_200ma <= -0.15:
        style_code = "OVERSOLD_REVERSAL"
        style_label = "超跌反轉型"
        weights = {"tech": 0.25, "fund": 0.35, "val": 0.30, "sent": 0.10}
    elif yoy_val >= 0.20 or (pe_val > 45.0 and gm_val >= 0.50 and yoy_val >= 0.0):
        style_code = "MOMENTUM_BREAKOUT"
        style_label = "動量突破型"
        weights = {"tech": 0.45, "fund": 0.35, "val": 0.05, "sent": 0.15}
    elif pe_val > 0 and pe_val <= 20.0 and yoy_val >= 0.0:
        style_code = "VALUE_DEFENSIVE"
        style_label = "價值防守型"
        weights = {"tech": 0.10, "fund": 0.30, "val": 0.50, "sent": 0.10}
    else:
        style_code = "RANGE_BOUND"
        style_label = "中繼震盪型"
        weights = {"tech": 0.35, "fund": 0.25, "val": 0.25, "sent": 0.15}
        
    return style_code, style_label, weights


def calculate_composite_score(
    style: str,
    weights: Dict[str, float],
    tech_metrics: Dict[str, Any],
    fundamentals: Dict[str, Any],
    sentiment_score: float = 0.0,
    macro_regime_multiplier: float = 1.0,
) -> Dict[str, Any]:
    """計算連續多因子綜合評分 (0 ~ 100，無 PE 標的中性基準保護)"""
    ret_10d = safe_float(tech_metrics.get("ret_10d"), 0.0)
    rsi = safe_float(tech_metrics.get("rsi_14"), 50.0)
    percentile = safe_float(tech_metrics.get("percentile"), 50.0)
    
    s_tech = 50.0
    if tech_metrics.get("is_super_trend"):
        s_tech = 92.0
    elif tech_metrics.get("status") == "DIVERGENCE_TOP_RISK":
        s_tech = 38.0
    else:
        s_tech = float(np.clip(50.0 + (rsi - 50.0) * 0.6 + (percentile - 50.0) * 0.3, 10.0, 95.0))
        
    quarters = fundamentals.get("quarters") or []
    if quarters:
        latest = quarters[0]
        yoy = safe_float(latest.get("revenue_growth_yoy"), 0.0)
        gm = safe_float(latest.get("gross_margin"), 0.0)
        eps_obj = latest.get("eps") or {}
        surp = safe_float(eps_obj.get("surprise_pct"), 0.0) if isinstance(eps_obj, dict) else 0.0
        
        s_fund = float(np.clip(50.0 + (yoy * 100.0) * 0.8 + (gm * 50.0) + (surp * 100.0) * 0.5, 15.0, 98.0))
    else:
        s_fund = 50.0
        
    val = fundamentals.get("valuation") or {}
    pe_raw = val.get("trailing_pe") or val.get("forward_pe")
    if pe_raw is None:
        s_val = 50.0  # 🛡️ 無 PE 估值標的 (ETF/大宗商品) 給予中性基準分
    else:
        pe = safe_float(pe_raw, 0.0)
        if pe <= 0:
            s_val = 30.0
        elif pe < 15.0:
            s_val = 90.0
        elif pe < 30.0:
            s_val = 70.0
        elif pe < 60.0:
            s_val = 45.0
        else:
            s_val = 25.0
        
    s_sent = float(np.clip(50.0 + sentiment_score * 35.0, 10.0, 90.0))
    
    raw_comp_score = (
        weights["tech"] * s_tech +
        weights["fund"] * s_fund +
        weights["val"] * s_val +
        weights["sent"] * s_sent
    )
    comp_score = raw_comp_score * (macro_regime_multiplier if math.isfinite(macro_regime_multiplier) else 1.0)
    comp_score_clamped = int(np.clip(round(comp_score if math.isfinite(comp_score) else 50.0), 1, 99))
    
    return {
        "composite_score": comp_score_clamped,
        "style": style,
        "scores": {
            "tech": round(s_tech, 1),
            "fund": round(s_fund, 1),
            "val": round(s_val, 1),
            "sent": round(s_sent, 1),
        },
        "weights": weights,
        "macro_regime_multiplier": macro_regime_multiplier,
    }


def calculate_risk_and_position(
    current_price: float,
    support_level: float,
    resistance_level: float,
    atr_14: float,
    adv_20d: float = 1e8,
    portfolio_risk_limit: float = 0.01,
    is_leveraged: bool = False,
) -> Dict[str, Any]:
    """物理 ATR 防守與階梯倉位反算（ADV<=0 零流動性與負現價極端行情硬防禦）"""
    if current_price <= 1e-6:
        return {
            "trade_status": "PRICE_ANOMALY_NO_TRADE",
            "current_price": 0.0,
            "stop_price": 0.0,
            "target_price": 0.0,
            "stop_loss_pct": 0.0,
            "reward_pct": 0.0,
            "rr_ratio": 0.0,
            "recommended_pos_pct": 0.0,
            "account_max_loss_pct": 0.0,
        }

    cur_p = current_price
    atr = max(atr_14, cur_p * 0.001)
    
    if adv_20d <= 0.0:
        return {
            "trade_status": "ZERO_LIQUIDITY_NO_TRADE",
            "current_price": dynamic_round(cur_p, cur_p),
            "stop_price": dynamic_round(cur_p * 0.90, cur_p),
            "target_price": dynamic_round(cur_p * 1.10, cur_p),
            "stop_loss_pct": 10.0,
            "reward_pct": 10.0,
            "rr_ratio": 1.0,
            "recommended_pos_pct": 0.0,
            "account_max_loss_pct": 0.0,
        }

    atr_mult = 0.75 if is_leveraged else 1.25
    raw_stop = support_level - atr_mult * atr
    stop_price = max(raw_stop, cur_p * 0.5)
    
    if stop_price >= cur_p:
        stop_price = cur_p * (0.97 if is_leveraged else 0.95)
        
    physical_stop_distance = cur_p - stop_price
    stop_loss_pct = physical_stop_distance / cur_p
    
    if resistance_level <= cur_p:
        target_price = cur_p + 1.272 * atr * 3.0
    else:
        target_price = resistance_level
        
    reward_distance = max(target_price - cur_p, 0.0)
    rr_ratio = reward_distance / physical_stop_distance if physical_stop_distance > 0 else 0.0
    
    if stop_loss_pct > 0.25:
        trade_status = "EXTREME_VOLATILITY_NO_TRADE"
        recommended_pos = 0.0
    else:
        base_pos = portfolio_risk_limit / max(stop_loss_pct, 0.01)
        if stop_loss_pct <= 0.08:
            recommended_pos = min(base_pos, 0.30 if not is_leveraged else 0.15)
        elif stop_loss_pct <= 0.15:
            recommended_pos = min(base_pos, 0.15 if not is_leveraged else 0.08)
        else:
            recommended_pos = min(base_pos, 0.05)
            
        if adv_20d < 1_000_000.0:
            recommended_pos = recommended_pos * 0.5
            
        trade_status = "APPROVED" if rr_ratio >= 1.0 else "POOR_RR_NO_TRADE"
        
    account_max_loss_pct = round(stop_loss_pct * recommended_pos * 100.0, 2)

    return {
        "trade_status": trade_status,
        "current_price": dynamic_round(cur_p, cur_p),
        "stop_price": dynamic_round(stop_price, cur_p),
        "target_price": dynamic_round(target_price, cur_p),
        "stop_loss_pct": round(stop_loss_pct * 100.0, 2),
        "reward_pct": round((reward_distance / cur_p) * 100.0, 2),
        "rr_ratio": round(rr_ratio, 2),
        "recommended_pos_pct": round(recommended_pos * 100.0, 1),
        "account_max_loss_pct": account_max_loss_pct,
    }


def downsample_sparkline(raw_points: List[Dict[str, Any]], target_count: int = 40) -> Tuple[List[float], float]:
    """時序 Sparkline 走勢降採樣器（四捨五入等距採樣 + 動態精度）"""
    if not raw_points:
        return [], 0.0
    
    closes = [safe_float(p.get("close") or p.get("price")) for p in raw_points]
    closes = [c for c in closes if c > 0]
    
    if not closes:
        return [], 0.0
        
    first_open = safe_float(raw_points[0].get("open") or raw_points[0].get("price"), closes[0])
    open_price = first_open if first_open > 0 else closes[0]
    
    eff_target = max(2, target_count)
    if len(closes) <= eff_target:
        return [dynamic_round(c, open_price) for c in closes], dynamic_round(open_price, open_price)
        
    indices = np.round(np.linspace(0, len(closes) - 1, eff_target)).astype(int)
    downsampled = [dynamic_round(float(closes[i]), open_price) for i in indices]
    return downsampled, dynamic_round(open_price, open_price)
```

---

#### 3. [MODIFY] [`app/engine/stock_aggregator.py`](file:///c:/Users/kris/lumina_core_v2/app/engine/stock_aggregator.py)

```python
"""
Stock Data Aggregator: Concurrent multi-dimensional market data fetching with fault isolation.
融合確定性量化特徵引擎、即時外匯折算、休市交易日精確錨定、跨市場 Pair RS 矩陣與 Alpha Vantage 8 季 SEC 10-Q 財報聯邦聚合。
"""
from __future__ import annotations
import asyncio
import time
import re
from enum import Enum
from typing import Any, Dict, List, Optional, Set, Tuple
from dataclasses import dataclass
import numpy as np

from app.core.logging import logger
from app.services.market_data_service import MarketDataError, MarketDataService, UpstreamTimeoutError
from app.services.master_universe_resolver import get_universe_resolver
from app.engine.stock_feature_engine import (
    calculate_empirical_percentile,
    classify_dynamic_style,
    calculate_composite_score,
    calculate_risk_and_position,
    calculate_macro_regime_multiplier,
    calculate_volatility_decay,
    calculate_time_stop_horizon,
    extract_universal_leverage,
    normalize_trade_intent,
    normalize_price_currency,
    downsample_sparkline,
    calculate_atr,
    safe_float,
    dynamic_round
)

VALID_DIMENSIONS = frozenset({"quote", "chart", "flash", "news", "calendar", "fundamentals"})
VALID_CHART_RANGES = frozenset({"1D", "5D", "1M", "3M", "1Y"})
MAX_SYMBOLS_PER_REQUEST = 5
MAX_NEWS_ITEMS = 8
MAX_FLASH_ITEMS = 15
MAX_CALENDAR_ITEMS = 8
MAX_SNIPPET_CHARS = 160
FETCH_TIMEOUT_SECONDS = 12.0

CURRENCY_ALIAS_MAP = {
    "RMB": "CNY", "YUAN": "CNY", "CNH": "CNY",
    "NTD": "TWD", "NT": "TWD", "YEN": "JPY",
    "DOLLAR": "USD", "BUCKS": "USD", "POUND": "GBP",
    "EURO": "EUR", "WON": "KRW",
}

NON_US_SUFFIXES = {
    ".TW", ".TWO", ".HK", ".SS", ".SZ", ".T", ".L", ".AX", ".TO", ".V", ".DE", ".PA", ".AS", ".MI", ".MC"
}
KNOWN_COMMODITIES = {
    "XAUUSD", "XAGUSD", "USO", "UCO", "GLD", "IAU", "SLV", "UNG", "DBA", "CPER"
}
KNOWN_CRYPTO = {
    "BTCUSD", "ETHUSD", "SOLUSD", "BNBUSD", "XRPUSD", "DOGEUSD", "ADAUSD",
    "BTC-USD", "ETH-USD", "SOL-USD", "BTC", "ETH"
}
KNOWN_LEVERAGED_ETFS = {
    "SOXL", "SOXS", "TQQQ", "SQQQ", "NVDL", "NVDX", "TSLL", "TSLS", "FNGU", "FNGD",
    "UPRO", "SPXU", "UDOW", "SDOW", "LABU", "LABD", "GDXU", "DUST", "BOIL", "KOLD",
    "SHNY", "TSMX", "TSMU", "MSFU", "MSFD", "AMZU", "AMZD", "AAPU", "AAPD"
}
KNOWN_INDEX_ETFS = {
    "SPY", "QQQ", "DIA", "IWM", "VOO", "VTI", "IVV", "VEA", "VWO", "EEM",
    "XLK", "XLF", "XLE", "XLV", "XLY", "XLP", "XLI", "XLU", "XLB", "XLRE"
}


@dataclass
class AssetProfile:
    symbol: str
    asset_type: str        # equity, etf, leveraged_etf, metal, commodity, crypto, forex, index
    is_us_listed: bool     # True if tradeable on US major exchanges
    supports_sec_fundamentals: bool  # True ONLY for US-listed equities and ADRs
    classification_reason: str
    leverage: float = 1.0  # 槓桿倍數


class MarketState(str, Enum):
    REGULAR = "REGULAR"
    PRE_MARKET = "PRE_MARKET"
    POST_MARKET = "POST_MARKET"
    CLOSED = "CLOSED"
    HALTED = "HALTED"
    UNLISTED = "UNLISTED"


def canonicalize_currency(raw_curr: Optional[str]) -> str:
    if not raw_curr:
        return "USD"
    c = str(raw_curr).strip().upper()
    return CURRENCY_ALIAS_MAP.get(c, c)


def infer_native_currency(symbol: str, explicit_curr: Optional[str] = None) -> str:
    if explicit_curr:
        return canonicalize_currency(explicit_curr)
    sym = (symbol or "").strip().upper()
    if sym.endswith((".TW", ".TWO")): return "TWD"
    if sym.endswith(".HK"): return "HKD"
    if sym.endswith(".T"): return "JPY"
    if sym.endswith((".SS", ".SZ")): return "CNY"
    if sym.endswith(".L"): return "GBP"
    if sym.endswith((".DE", ".PA", ".AS", ".MI", ".MC")): return "EUR"
    if sym.endswith((".TO", ".V")): return "CAD"
    if sym.endswith(".AX"): return "AUD"
    if re.match(r"^(?:60\d{4}|688\d{3}|00\d{4}|30\d{4})$", sym): return "CNY"
    return "USD"


def resolve_fx_ticker(currency: str) -> Optional[str]:
    c = canonicalize_currency(currency)
    if c == "USD" or not c:
        return None
    if c in {"EUR", "GBP", "AUD", "NZD"}:
        return f"{c}USD=X"
    if c in {"BTC", "ETH"}:
        return f"{c}-USD"
    return f"USD{c}=X"


def resolve_universal_fx_rate(
    user_currency: str,
    asset_quote_currency: str,
    fetched_quote_symbol: str,
    fetched_fx_price: float
) -> float:
    u_curr = canonicalize_currency(user_currency)
    a_curr = canonicalize_currency(asset_quote_currency)
    if u_curr == a_curr or not u_curr:
        return 1.0
    sym = (fetched_quote_symbol or "").strip().upper()
    if sym.startswith(u_curr):
        return float(fetched_fx_price)
    else:
        return float(1.0 / fetched_fx_price) if fetched_fx_price > 0 else 1.0


def resolve_native_benchmark(symbol: str, asset_profile: AssetProfile) -> Tuple[str, str, str]:
    sym = symbol.strip().upper()
    if asset_profile.asset_type == "forex" or sym.endswith("=X"):
        return "DX-Y.NYB", "UUP", "美元指數 (DXY)"
    if asset_profile.asset_type == "crypto" or sym in {"BTCUSD", "ETHUSD"} or sym.startswith("BTC"):
        return "BTC-USD", "BITO", "加密大盤 (BTC)"
    if asset_profile.asset_type in {"metal", "commodity"} or sym.startswith("XAU") or sym in {"CL=F", "GC=F", "SI=F", "NG=F"}:
        return "GC=F", "GLD", "COMEX 黃金期貨 (GC)"
    if sym.endswith(".TW") or sym.endswith(".TWO"):
        return "^TWII", "EWT", "台股加權指數"
    if sym.endswith(".HK"):
        return "^HSTECH", "KWEB", "恆生科技指數"
    if sym.endswith(".T"):
        return "^N225", "EWJ", "日經225指數"
    if sym.endswith(".SS") or sym.endswith(".SZ") or re.match(r"^(?:60\d{4}|688\d{3}|00\d{4}|30\d{4})$", sym):
        return "000300.SS", "ASHR", "滬深300指數"
    return "SPY", "SPY", "美股大盤 (S&P 500)"


def classify_asset_and_fundamentals_eligibility(
    symbol_str: str,
    asset_type_hint: Optional[str] = None,
    name_hint: Optional[str] = None,
) -> AssetProfile:
    sym = str(symbol_str).strip().upper()
    if sym.endswith("=X") or (len(sym) == 6 and sym[:3] in {"USD", "EUR", "GBP", "AUD", "JPY", "CAD", "CHF", "NZD"} and sym[3:] in {"USD", "EUR", "GBP", "AUD", "JPY", "CAD", "CHF", "NZD", "TWD", "HKD", "CNY"}):
        return AssetProfile(symbol=sym, asset_type="forex", is_us_listed=False, supports_sec_fundamentals=False, classification_reason="Forex currency pair.", leverage=1.0)
    
    if sym.endswith("=F") or sym in {"GC=F", "CL=F", "SI=F", "NG=F", "NQ=F", "ES=F", "YM=F", "RTY=F"}:
        return AssetProfile(symbol=sym, asset_type="commodity" if "CL" in sym or "GC" in sym or "SI" in sym or "NG" in sym else "index", is_us_listed=False, supports_sec_fundamentals=False, classification_reason="Futures contract.", leverage=1.0)

    for suffix in NON_US_SUFFIXES:
        if sym.endswith(suffix):
            lev = extract_universal_leverage(name_hint or sym)
            return AssetProfile(symbol=sym, asset_type="leveraged_etf" if abs(lev) > 1.0 else ("etf" if asset_type_hint == "etf" else "equity"), is_us_listed=False, supports_sec_fundamentals=False, classification_reason=f"Non-US listing '{suffix}'.", leverage=lev)
            
    if sym in KNOWN_COMMODITIES or (sym.endswith("USD") and sym[:3] in {"XAU", "XAG", "XPT", "XPD"}):
        lev = 2.0 if sym in {"UCO", "UGL"} else 1.0
        return AssetProfile(symbol=sym, asset_type="metal" if "XAU" in sym or "XAG" in sym else "commodity", is_us_listed=False, supports_sec_fundamentals=False, classification_reason="Commodity asset.", leverage=lev)

    if sym in KNOWN_CRYPTO or sym.startswith("BTC") or sym.startswith("ETH"):
        return AssetProfile(symbol=sym, asset_type="crypto", is_us_listed=False, supports_sec_fundamentals=False, classification_reason="Crypto asset.", leverage=1.0)

    lev = extract_universal_leverage(name_hint or sym)
    if abs(lev) > 1.0 or sym in KNOWN_LEVERAGED_ETFS:
        return AssetProfile(symbol=sym, asset_type="leveraged_etf", is_us_listed=True, supports_sec_fundamentals=False, classification_reason="Leveraged/Inverse ETF.", leverage=lev)

    if sym in KNOWN_INDEX_ETFS or asset_type_hint == "etf":
        return AssetProfile(symbol=sym, asset_type="etf", is_us_listed=True, supports_sec_fundamentals=False, classification_reason="ETF.", leverage=1.0)

    if re.match(r"^[A-Z]{1,5}$", sym):
        return AssetProfile(symbol=sym, asset_type="equity", is_us_listed=True, supports_sec_fundamentals=True, classification_reason="US-listed equity.", leverage=1.0)

    return AssetProfile(symbol=sym, asset_type=asset_type_hint or "equity", is_us_listed=False, supports_sec_fundamentals=False, classification_reason="Unclassified.", leverage=1.0)


def _truncate(text: str, n: int) -> str:
    t = str(text or "").strip()
    return t if len(t) <= n else t[:n].rstrip() + "…"


def _fmt_usd(val: float) -> str:
    sign = "-" if val < 0 else ""
    v = abs(val)
    if v >= 1e12: return f"{sign}${v/1e12:.2f}T"
    if v >= 1e9: return f"{sign}${v/1e9:.2f}B"
    if v >= 1e6: return f"{sign}${v/1e6:.2f}M"
    return f"{sign}${v:,.0f}"


def _normalize_quote(raw: Dict[str, Any], symbol: str) -> Dict[str, Any]:
    d = raw.get("data") or raw
    raw_state = str(d.get("market_state") or "REGULAR").upper()
    state = raw_state if raw_state in MarketState.__members__ else MarketState.REGULAR.value
    profile = classify_asset_and_fundamentals_eligibility(symbol)
    
    raw_p = safe_float(d.get("close") or d.get("price"))
    raw_curr = str(d.get("currency") or "USD")
    norm_p, norm_curr = normalize_price_currency(raw_p, raw_curr)
    
    return {
        "symbol": str(d.get("symbol") or symbol).upper(),
        "code": str(d.get("code") or symbol).upper(),
        "name": str(d.get("name") or "").strip(),
        "price": str(norm_p) if norm_p > 0 else str(d.get("close") or d.get("price") or "").strip(),
        "change": str(d.get("ups_price") or "").strip(),
        "change_percent": str(d.get("ups_percent") or "").strip(),
        "open": str(d.get("open") or "").strip(),
        "high": str(d.get("high") or "").strip(),
        "low": str(d.get("low") or "").strip(),
        "volume": int(d.get("volume") or 0),
        "time": str(d.get("time") or "").strip(),
        "timezone": str(d.get("timezone") or "").strip() or None,
        "market_state": state,
        "asset_type": profile.asset_type,
        "currency": norm_curr,
        "post_market_price": str(d.get("post_market_price") or ""),
        "post_market_change": str(d.get("post_market_change") or ""),
    }


def _normalize_chart_summary(raw: Dict[str, Any], symbol: str, range_key: str) -> Dict[str, Any]:
    d = raw.get("data") or raw
    pts = d.get("points") or []
    first, last = (pts[0] if pts else {}), (pts[-1] if pts else {})
    return {
        "symbol": str(d.get("symbol") or symbol).upper(),
        "range": str(d.get("range") or range_key),
        "interval": str(d.get("interval") or ""),
        "point_count": len(pts),
        "period_open": safe_float(first.get("open") or 0),
        "period_close": safe_float(last.get("close") or 0),
        "period_high": safe_float(max((p.get("high", 0) for p in pts), default=0)),
        "period_low": safe_float(min((p.get("low", 0) for p in pts if p.get("low")), default=0)),
        "provider": str(d.get("provider") or ""),
    }


def _normalize_fundamentals(raw: Dict[str, Any], symbol: str) -> Dict[str, Any]:
    income_reports = raw.get("income_reports") or []
    cash_flow_map = raw.get("cash_flow_map") or {}
    earnings_map = raw.get("earnings_map") or {}
    quarters = []

    for idx, inc in enumerate(income_reports[:8]):
        date_str = str(inc.get("fiscalDateEnding") or f"Q_{idx}")
        rev_raw = safe_float(inc.get("totalRevenue"), 0.0)
        gp_raw = safe_float(inc.get("grossProfit"), 0.0)
        op_raw = safe_float(inc.get("operatingIncome"), 0.0)
        ni_raw = safe_float(inc.get("netIncome"), 0.0)

        yoy_val: Optional[float] = None
        if idx + 4 < len(income_reports):
            prev_rev = safe_float(income_reports[idx+4].get("totalRevenue"), 0.0)
            if prev_rev > 0:
                yoy_val = (rev_raw - prev_rev) / prev_rev

        fcf_dict: Optional[Dict[str, Any]] = None
        cf = cash_flow_map.get(date_str)
        if cf:
            ocf_raw = safe_float(cf.get("operatingCashflow"), 0.0)
            capex_raw = safe_float(cf.get("capitalExpenditures"), 0.0)
            fcf_raw = ocf_raw - abs(capex_raw)
            fcf_dict = {
                "raw": fcf_raw,
                "formatted": _fmt_usd(fcf_raw),
            }

        earn = earnings_map.get(date_str) or {}
        eps_act = safe_float(earn.get("reportedEPS"))
        eps_est = safe_float(earn.get("estimatedEPS"))
        eps_surp = safe_float(earn.get("surprise"))
        eps_surp_pct = safe_float(earn.get("surprisePercentage"))
        if eps_surp_pct is not None:
            eps_surp_pct = eps_surp_pct / 100.0

        quarters.append({
            "fiscal_date_ending": date_str,
            "revenue": {"raw": rev_raw, "formatted": _fmt_usd(rev_raw), "currency": str(inc.get("reportedCurrency") or "USD")},
            "gross_profit": {"raw": gp_raw, "formatted": _fmt_usd(gp_raw)},
            "gross_margin": round((gp_raw / rev_raw), 4) if rev_raw > 0 else 0.0,
            "operating_income": {"raw": op_raw, "formatted": _fmt_usd(op_raw)},
            "operating_margin": round((op_raw / rev_raw), 4) if rev_raw > 0 else 0.0,
            "net_income": {"raw": ni_raw, "formatted": _fmt_usd(ni_raw)},
            "free_cash_flow": fcf_dict,
            "revenue_growth_yoy": round(yoy_val, 4) if yoy_val is not None else None,
            "eps": {
                "actual": eps_act,
                "estimate": eps_est,
                "surprise": eps_surp,
                "surprise_pct": round(eps_surp_pct, 4) if eps_surp_pct is not None else None,
            },
            "reported_date": str(earn.get("reportedDate") or ""),
        })

    return {
        "symbol": str(raw.get("symbol") or symbol).upper(),
        "quarters": quarters,
        "valuation": raw.get("valuation") or {},
        "source_provider": str(raw.get("provider") or "alphavantage_pool"),
    }


def _normalize_news_items(raw: Dict[str, Any], n: int) -> List[Dict[str, Any]]:
    d = raw.get("data") or raw
    return [{"title": _truncate(i.get("title", ""), MAX_SNIPPET_CHARS),
             "introduction": _truncate(i.get("introduction", ""), MAX_SNIPPET_CHARS),
             "time": str(i.get("time", "")), "url": str(i.get("url", ""))}
            for i in (d.get("items") or [])[:n]]


def _normalize_flash_items(raw: Dict[str, Any], n: int) -> List[Dict[str, Any]]:
    d = raw.get("data") or raw
    return [{"content": _truncate(i.get("content", i.get("title", "")), MAX_SNIPPET_CHARS),
             "time": str(i.get("time", "")), "url": str(i.get("url", ""))}
            for i in (d.get("items") or [])[:n]]


def _normalize_calendar_items(raw: Dict[str, Any], n: int) -> List[Dict[str, Any]]:
    items = raw.get("data") or []
    return [{"title": _truncate(i.get("title", ""), 80),
             "star": int(i.get("star") or 0),
             "pub_time": str(i.get("pub_time", "")),
             "consensus": str(i.get("consensus", "")),
             "previous": str(i.get("previous", "")),
             "actual": str(i.get("actual", "")),
             "affect_txt": _truncate(i.get("affect_txt", ""), 80)}
            for i in (items if isinstance(items, list) else [])[:n]]


async def fetch_stock_data_bundle(
    *, symbols: List[str], dimensions: List[str],
    chart_range: str, market_svc: MarketDataService,
    entities_hint: Optional[List[Dict[str, Any]]] = None,
    search_keyword_override: Optional[str] = None,
    trade_context: Optional[Dict[str, Any]] = None,
    retrieval_budget: Optional[int] = None,
) -> Dict[str, Any]:
    t0 = time.monotonic()
    dim_set: Set[str] = {d.lower() for d in dimensions if d.lower() in VALID_DIMENSIONS} or {"quote", "fundamentals"}
    resolver = get_universe_resolver()

    resolved_symbols: List[str] = []
    unresolved_entities: List[str] = []

    for s in symbols:
        s_clean = str(s).strip().upper()
        if s_clean and s_clean not in resolved_symbols:
            resolved_symbols.append(s_clean)

    if entities_hint:
        for ent in entities_hint:
            aliases = ent.get("aliases") or [ent.get("query_term", "")]
            matches = resolver.resolve_symbol_aliases(aliases, limit=2)
            if matches:
                for m in matches:
                    sym = m["symbol"].upper()
                    if sym not in resolved_symbols:
                        resolved_symbols.append(sym)
            else:
                unresolved_entities.append(str(ent.get("query_term") or ent.get("canonical_name") or ""))

    target_symbols = resolved_symbols[:MAX_SYMBOLS_PER_REQUEST]
    if not target_symbols:
        target_symbols = ["SPY"]

    rng = chart_range if chart_range in VALID_CHART_RANGES else "1D"

    ctx = trade_context or {}
    declared_curr = canonicalize_currency(ctx.get("declared_currency"))
    declared_cap_raw = safe_float(ctx.get("declared_capital_raw")) if ctx.get("declared_capital_raw") is not None else None
    target_alloc_pct = ctx.get("target_allocation_pct")
    trade_intent = normalize_trade_intent(ctx.get("trade_intent"), declared_cap_raw)

    primary_sym = target_symbols[0]
    name_hint_0 = (entities_hint[0].get("canonical_name") or entities_hint[0].get("query_term")) if entities_hint else None
    primary_prof = classify_asset_and_fundamentals_eligibility(primary_sym, name_hint=name_hint_0)
    bench_primary, bench_fallback, bench_name = resolve_native_benchmark(primary_sym, primary_prof)

    chinese_keywords: List[str] = []
    industry_keywords: List[str] = []
    peer_symbols: List[str] = []

    if entities_hint:
        for ent in entities_hint:
            cn = ent.get("chinese_name") or ent.get("query_term")
            if cn and any('\u4e00' <= ch <= '\u9fff' for ch in str(cn)):
                cn_clean = str(cn).strip()
                if cn_clean and cn_clean not in chinese_keywords:
                    chinese_keywords.append(cn_clean)
            for alias in (ent.get("aliases") or []):
                if any('\u4e00' <= ch <= '\u9fff' for ch in str(alias)):
                    a_clean = str(alias).strip()
                    if a_clean and a_clean not in chinese_keywords:
                        chinese_keywords.append(a_clean)
            sector = ent.get("sector")
            if sector and str(sector).strip() not in industry_keywords:
                industry_keywords.append(str(sector).strip())
            peers = ent.get("peers") or []
            if isinstance(peers, list):
                for p in peers:
                    p_clean = str(p).strip().upper()
                    if p_clean and p_clean not in peer_symbols and p_clean not in target_symbols:
                        peer_symbols.append(p_clean)

    keyword = search_keyword_override or (" ".join(chinese_keywords[:2]) if chinese_keywords else (" ".join(target_symbols[:2]) if target_symbols else ""))

    bundle: Dict[str, Any] = {
        "quotes": [], "charts": [], "fundamentals": [], "flash": [], "news": [], "calendar": [],
        "latency_ms": 0, "errors": {}, "symbols_resolved": target_symbols,
        "symbols_unresolved": unresolved_entities
    }

    # 1. 外匯即時報價任務 (支援 BTC/ETH 等原生加密幣)
    fx_ticker = resolve_fx_ticker(declared_curr) if declared_curr != "USD" else None
    async def _fx_q():
        if not fx_ticker:
            return None
        try:
            return await asyncio.wait_for(market_svc.get_quote(fx_ticker), FETCH_TIMEOUT_SECONDS)
        except Exception:
            return None

    # 2. 基準 1Y 圖表任務 (防禦 1: 若標的本身即基準則短路，不發起重複請求)
    async def _bench_c1y():
        if primary_sym == bench_primary:
            return None
        try:
            return await asyncio.wait_for(market_svc.get_chart(bench_primary, "1Y"), FETCH_TIMEOUT_SECONDS)
        except Exception:
            try:
                return await asyncio.wait_for(market_svc.get_chart(bench_fallback, "1Y"), FETCH_TIMEOUT_SECONDS)
            except Exception:
                return None

    async def _q(sym: str):
        try:
            raw = await asyncio.wait_for(market_svc.get_quote(sym), FETCH_TIMEOUT_SECONDS)
            return _normalize_quote(raw, sym)
        except Exception as e:
            bundle["errors"][f"quote_{sym}"] = str(e)[:120]
            return None

    async def _c_1d(sym: str):
        try:
            return await asyncio.wait_for(market_svc.get_chart(sym, "1D"), FETCH_TIMEOUT_SECONDS)
        except Exception:
            return None

    async def _c_1y(sym: str):
        try:
            return await asyncio.wait_for(market_svc.get_chart(sym, "1Y"), FETCH_TIMEOUT_SECONDS)
        except Exception:
            return None

    async def _f(sym: str):
        try:
            raw = await asyncio.wait_for(market_svc.get_fundamentals(sym), FETCH_TIMEOUT_SECONDS)
            return _normalize_fundamentals(raw, sym)
        except Exception:
            return None

    async def _flash():
        try:
            raw = None
            flash_kw = " ".join(chinese_keywords[:2]) if chinese_keywords else keyword
            if flash_kw:
                try:
                    raw = await asyncio.wait_for(market_svc.search_flash(flash_kw), FETCH_TIMEOUT_SECONDS)
                except Exception:
                    raw = None
            items = _normalize_flash_items(raw, MAX_FLASH_ITEMS) if raw else []
            if not items:
                raw_global = await asyncio.wait_for(market_svc.list_flash(None), FETCH_TIMEOUT_SECONDS)
                items = _normalize_flash_items(raw_global, MAX_FLASH_ITEMS)
            return items
        except Exception as e:
            bundle["errors"]["flash"] = str(e)[:120]
            return []

    async def _news():
        try:
            tasks = []
            jin10_query = " ".join(chinese_keywords[:2]) if chinese_keywords else keyword
            if jin10_query:
                tasks.append(market_svc.search_news(jin10_query))
            else:
                tasks.append(market_svc.list_news(None))

            for s in target_symbols[:2]:
                tasks.append(market_svc.search_yahoo_news(s, limit=5))
            for p in peer_symbols[:2]:
                tasks.append(market_svc.search_yahoo_news(p, limit=3))
            for ind in industry_keywords[:1]:
                tasks.append(market_svc.search_news(ind))

            raw_results = await asyncio.gather(*tasks, return_exceptions=True)
            combined_items = []
            seen_titles = set()
            for res in raw_results:
                if isinstance(res, dict):
                    items = _normalize_news_items(res, MAX_NEWS_ITEMS)
                    for it in items:
                        t = it.get("title", "").strip()
                        if t and t not in seen_titles:
                            seen_titles.add(t)
                            combined_items.append(it)
                elif isinstance(res, list):
                    for it in res:
                        t = it.get("title", "").strip()
                        if t and t not in seen_titles:
                            seen_titles.add(t)
                            combined_items.append(it)
            if len(combined_items) < 3:
                try:
                    raw_global = await asyncio.wait_for(market_svc.list_news(None), FETCH_TIMEOUT_SECONDS)
                    macro_items = _normalize_news_items(raw_global, MAX_NEWS_ITEMS)
                    for it in macro_items:
                        t = it.get("title", "").strip()
                        if t and t not in seen_titles:
                            seen_titles.add(t)
                            combined_items.append(it)
                except Exception:
                    pass
            return combined_items[:MAX_NEWS_ITEMS]
        except Exception as e:
            bundle["errors"]["news"] = str(e)[:120]
            return []

    async def _cal():
        try:
            return _normalize_calendar_items(
                await asyncio.wait_for(market_svc.list_calendar(), FETCH_TIMEOUT_SECONDS),
                MAX_CALENDAR_ITEMS
            )
        except Exception as e:
            bundle["errors"]["calendar"] = str(e)[:120]
            return []

    qtasks = [_q(s) for s in target_symbols] if "quote" in dim_set else []
    c1d_tasks = [_c_1d(s) for s in target_symbols[:2]]
    c1y_tasks = [_c_1y(s) for s in target_symbols[:2]]
    ftasks = [_f(s) for s in target_symbols[:2] if classify_asset_and_fundamentals_eligibility(s).supports_sec_fundamentals] if "fundamentals" in dim_set else []
    
    noop = asyncio.sleep(0, result=[])
    all_tasks = qtasks + c1d_tasks + c1y_tasks + ftasks + [
        _flash() if "flash" in dim_set else noop,
        _news()  if "news"  in dim_set else noop,
        _cal()   if "calendar" in dim_set else noop,
        _bench_c1y(),
        _fx_q(),
    ]

    results = await asyncio.gather(*all_tasks, return_exceptions=True)
    nq, nc1d, nc1y, nf = len(qtasks), len(c1d_tasks), len(c1y_tasks), len(ftasks)

    raw_c1d = [r for r in results[nq:nq+nc1d] if isinstance(r, dict)]
    raw_c1y = [r for r in results[nq+nc1d:nq+nc1d+nc1y] if isinstance(r, dict)]
    bench_raw = results[nq+nc1d+nc1y+nf+3] if isinstance(results[nq+nc1d+nc1y+nf+3], dict) else None
    fx_raw = results[nq+nc1d+nc1y+nf+4] if isinstance(results[nq+nc1d+nc1y+nf+4], dict) else None

    charts_normalized = []
    for c_data in raw_c1d:
        pts = (c_data.get("data", {}).get("points") or c_data.get("points") or [])
        if pts:
            first_p, last_p = pts[0], pts[-1]
            c_sym = c_data.get("data", {}).get("symbol") or c_data.get("symbol") or primary_sym
            charts_normalized.append({
                "symbol": c_sym,
                "range": "1D",
                "period_open": safe_float(first_p.get("open") or first_p.get("close")),
                "period_close": safe_float(last_p.get("close")),
                "period_high": safe_float(max((p.get("high") or p.get("close") for p in pts), default=0.0)),
                "period_low": safe_float(min((p.get("low") or p.get("close") for p in pts), default=0.0)),
                "point_count": len(pts),
            })

    bundle.update({
        "quotes": [r for r in results[:nq] if isinstance(r, dict)],
        "charts": charts_normalized,
        "fundamentals": [r for r in results[nq+nc1d+nc1y:nq+nc1d+nc1y+nf] if isinstance(r, dict)],
        "flash": results[nq+nc1d+nc1y+nf+0] if isinstance(results[nq+nc1d+nc1y+nf+0], list) else [],
        "news": results[nq+nc1d+nc1y+nf+1] if isinstance(results[nq+nc1d+nc1y+nf+1], list) else [],
        "calendar": results[nq+nc1d+nc1y+nf+2] if isinstance(results[nq+nc1d+nc1y+nf+2], list) else [],
    })

    # 4. 外匯即時匯率折算 (USD 宣告原幣直通防禦，防範乘數倒置膨脹)
    primary_asset_curr = infer_native_currency(primary_sym, bundle["quotes"][0].get("currency") if bundle["quotes"] else None)
    fx_rate_to_usd = 1.0
    if declared_curr == "USD":
        declared_capital_usd = declared_cap_raw
    elif fx_raw and declared_curr != "USD":
        fx_p = safe_float(fx_raw.get("price") or fx_raw.get("data", {}).get("close"))
        fx_sym = str(fx_raw.get("symbol") or fx_raw.get("data", {}).get("symbol") or "")
        fx_rate_to_usd = resolve_universal_fx_rate(declared_curr, "USD", fx_sym, fx_p)
        declared_capital_usd = round(declared_cap_raw * fx_rate_to_usd, 2) if declared_cap_raw is not None and declared_cap_raw > 0 else None
    else:
        declared_capital_usd = declared_cap_raw

    # 5. 基準 1Y 日 K 線與宏觀體制解析 (休市自動錨定上一個交易日，NaN 免疫過濾)
    if primary_sym == bench_primary and raw_c1y:
        bench_c1y_data = raw_c1y[0]
    else:
        bench_c1y_data = bench_raw

    bench_pts = (bench_c1y_data.get("data", {}).get("points") or bench_c1y_data.get("points") or []) if bench_c1y_data else []
    raw_bench_c = np.array([safe_float(p.get("close")) for p in bench_pts if safe_float(p.get("close")) > 0])
    bench_closes = raw_bench_c[np.isfinite(raw_bench_c)]
    
    if len(bench_closes) >= 2:
        bench_chg_pct = float((bench_closes[-1] - bench_closes[-2]) / bench_closes[-2] * 100.0)
    else:
        bench_chg_pct = 0.0
        
    benchmark_info = {
        "benchmark_name": bench_name,
        "symbol": bench_primary,
        "change_percent": f"{bench_chg_pct:+.2f}%",
        "is_positive": bench_chg_pct >= 0,
    }

    # 6. 雙標的相對強弱矩陣 (Pair RS Matrix，相同標的自對比守衛 + 零除過濾 + 跨國交易日曆錯位對齊)
    historical_closes_map = {}
    for idx, c_data in enumerate(raw_c1y):
        if idx < len(bundle["quotes"]):
            s = bundle["quotes"][idx].get("symbol")
            pts = (c_data.get("data", {}).get("points") or c_data.get("points") or [])
            if pts:
                raw_c_arr = np.array([safe_float(p.get("close")) for p in pts if safe_float(p.get("close")) > 0])
                historical_closes_map[s] = raw_c_arr[np.isfinite(raw_c_arr)]

    pair_comparison_deck = None
    if len(bundle["quotes"]) >= 2 and len(historical_closes_map) >= 2:
        sym_a = bundle["quotes"][0].get("symbol")
        sym_b = bundle["quotes"][1].get("symbol")
        if sym_a and sym_b and sym_a != sym_b:
            closes_a = historical_closes_map.get(sym_a, np.array([]))
            closes_b = historical_closes_map.get(sym_b, np.array([]))
            min_n = min(len(closes_a), len(closes_b), 20)
            if min_n >= 10 and closes_a[-min_n] > 1e-8 and closes_b[-min_n] > 1e-8:
                valid_win_b = closes_b[-min_n:]
                if np.all(valid_win_b > 1e-8):
                    rs_series = (closes_a[-min_n:] / closes_a[-min_n]) / (valid_win_b / closes_b[-min_n])
                    if math.isfinite(rs_series[-1]):
                        rs_delta_pct = round((rs_series[-1] - 1.0) * 100.0, 1)
                        pair_comparison_deck = {
                            "symbol_a": sym_a,
                            "symbol_b": sym_b,
                            "rs_delta_pct": rs_delta_pct,
                            "leader_symbol": sym_a if rs_delta_pct >= 0 else sym_b,
                            "lookback_days": min_n,
                        }

    # 7. 量化特徵融合 (原地注入 bundle["quotes"]，休市狀態全面繼承最後交易日結算點位)
    for idx, q in enumerate(bundle["quotes"]):
        sym = q.get("symbol")
        n_hint = (entities_hint[idx].get("canonical_name") or entities_hint[idx].get("query_term")) if entities_hint and idx < len(entities_hint) else q.get("name")
        c1d_data = raw_c1d[idx] if idx < len(raw_c1d) else None
        c1y_data = raw_c1y[idx] if idx < len(raw_c1y) else None
        fund_data = bundle["fundamentals"][idx] if idx < len(bundle["fundamentals"]) else {}
        
        pts_1d = (c1d_data.get("data", {}).get("points") or c1d_data.get("points") or []) if c1d_data else []
        sparkline_pts, open_p = downsample_sparkline(pts_1d, target_count=40)
        
        pts_1y = (c1y_data.get("data", {}).get("points") or c1y_data.get("points") or []) if c1y_data else []
        if pts_1y:
            closes = np.array([safe_float(p.get("close")) for p in pts_1y if safe_float(p.get("close")) > 0])
            highs = np.array([safe_float(p.get("high")) for p in pts_1y if safe_float(p.get("high")) > 0])
            lows = np.array([safe_float(p.get("low")) for p in pts_1y if safe_float(p.get("low")) > 0])
            vol_arr = np.array([safe_float(p.get("volume")) for p in pts_1y if safe_float(p.get("volume")) > 0])
            adv_20d = float(np.mean(vol_arr[-20:] * closes[-20:])) if len(vol_arr) >= 20 else 1e8
        else:
            cur_p = safe_float(q.get("price"), 100.0)
            closes, highs, lows, adv_20d = np.array([cur_p]), np.array([cur_p * 1.02]), np.array([cur_p * 0.98]), 1e8

        prof = classify_asset_and_fundamentals_eligibility(sym, name_hint=n_hint)
        dynamic_leverage = extract_universal_leverage(n_hint or sym, etf_closes=closes, bench_closes=bench_closes)
        cur_price = safe_float(q.get("price"), closes[-1] if len(closes) > 0 else 0.0)
        atr_14 = calculate_atr(highs, lows, closes, 14)
        
        decay_res = calculate_volatility_decay(dynamic_leverage, closes)
        m_regime, macro_status = calculate_macro_regime_multiplier(prof.asset_type, dynamic_leverage, bench_closes)
        tech_metrics = calculate_empirical_percentile(closes, highs, lows, sector_growth_percentile=80.0)
        
        quarters = fund_data.get("quarters") or []
        latest_q = quarters[0] if quarters else {}
        rev_yoy = latest_q.get("revenue_growth_yoy")
        gm = latest_q.get("gross_margin")
        val_obj = fund_data.get("valuation") or {}
        pe = safe_float(val_obj.get("trailing_pe") or val_obj.get("forward_pe"), 0.0)
        ma200 = tech_metrics.get("ma200", cur_price)
        bias_200 = (cur_price - ma200) / ma200 if ma200 > 0 else 0.0
        
        style_code, style_label, weights = classify_dynamic_style(rev_yoy, gm, pe, bias_200, is_leveraged=decay_res["is_leveraged"])
        score_res = calculate_composite_score(style_label, weights, tech_metrics, fund_data, sentiment_score=0.2, macro_regime_multiplier=m_regime)
        
        sup_level = tech_metrics.get("ma50", cur_price * 0.95)
        res_level = safe_float(np.max(highs[-min(250, len(highs)):]), cur_price * 1.10)
        risk_res = calculate_risk_and_position(cur_price, sup_level, res_level, atr_14, adv_20d=adv_20d, is_leveraged=decay_res["is_leveraged"])
        
        time_horizon_res = calculate_time_stop_horizon(cur_price, risk_res["target_price"], atr_14, is_crypto=(prof.asset_type == "crypto"))
        
        q.update({
            "sparkline": sparkline_pts,
            "open_price": open_p,
            "composite_score": score_res["composite_score"],
            "style_code": style_code,
            "style": style_label,
            "scores": score_res["scores"],
            "macro_regime_multiplier": m_regime,
            "macro_status": macro_status,
            "is_leveraged": decay_res["is_leveraged"],
            "annual_decay_pct": decay_res["annual_decay_pct"],
            "leveraged_warning": decay_res["warning_text"],
            "stop_price": risk_res["stop_price"],
            "target_price": risk_res["target_price"],
            "rr_ratio": risk_res["rr_ratio"],
            "recommended_pos_pct": risk_res["recommended_pos_pct"],
            "account_max_loss_pct": risk_res["account_max_loss_pct"],
            "trade_status": risk_res["trade_status"],
            "expected_days": time_horizon_res["expected_days"],
            "stale_limit_days": time_horizon_res["stale_limit_days"],
            "time_stop_guidance": time_horizon_res["time_stop_guidance"],
            "fomo_dampener": tech_metrics.get("fomo_dampener", False),
            "benchmark_info": benchmark_info,
            "declared_currency": declared_curr,
            "declared_capital_raw": declared_cap_raw,
            "declared_capital_usd": declared_capital_usd,
            "fx_rate_to_usd": fx_rate_to_usd,
            "target_allocation_pct": target_alloc_pct,
            "trade_intent": trade_intent,
            "regime_summary": f"標的呈現【{style_label}】結構，全景量化得分 {score_res['composite_score']} 分，{tech_metrics['status']}。",
            "is_super_trend": tech_metrics.get("is_super_trend", False),
        })

    bundle["pair_comparison_deck"] = pair_comparison_deck
    bundle["latency_ms"] = int((time.monotonic() - t0) * 1000)
    logger.info("[StockAggregator] bundle ready", extra={"extra_data": {k: len(v) if isinstance(v, list) else v for k, v in bundle.items() if k != "errors"}})
    return bundle
```

---

#### 4. [MODIFY] [`app/api/v1/chat.py`](file:///c:/Users/kris/lumina_core_v2/app/api/v1/chat.py)

```python
# [MODIFY] app/api/v1/chat.py (Line 720~760, Line 2961~2970, Line 3021~3040, Line 3296~3314, Line 3429~3445)

def _trim_financial_context_to_budget(bundle: Dict[str, Any], budget: int) -> str:
    lines = []
    current_char_count = 0
    max_chars = budget * 4 if budget and budget > 0 else 8000

    for q in (bundle.get("quotes") or []):
        sym = q.get("symbol") or "N/A"
        name = q.get("name") or sym
        price, chg, pct = q.get("price"), q.get("change"), q.get("change_percent")
        score = q.get("composite_score")
        style = q.get("style")
        stop_p = q.get("stop_price")
        target_p = q.get("target_price")
        rr = q.get("rr_ratio")
        rec_pos = q.get("recommended_pos_pct")
        exp_days = q.get("expected_days")
        stale_days = q.get("stale_limit_days")
        is_lev = q.get("is_leveraged")
        decay_pct = q.get("annual_decay_pct")
        m_state = str(q.get("market_state") or "REGULAR").upper()

        block = [
            f"[QUOTE & QUANT PROFILE] {sym} — {name} (Status: {m_state})",
            f"  Price: {price}  Change: {chg} ({pct}) | Quant Score: {score}/100 ({style})"
        ]
        if is_lev:
            block.append(f"  ⚡ LEVERAGED ETF WARNING: Annual Volatility Drag ≈ -{decay_pct}%. Short-term tactical only.")
        if stop_p and target_p:
            block.append(f"  Tactical Grid: Hard Stop=${stop_p} | Target Resistance=${target_p} | R:R=1:{rr} | Max Pos Cap={rec_pos}%")
        if exp_days:
            block.append(f"  Time-Stop Horizon: Theoretical swing duration {exp_days} trading days (Stale timeout: {stale_days} days).")

        block_str = "\n".join(block)
        if current_char_count + len(block_str) > max_chars:
            break
        lines.append(block_str)
        current_char_count += len(block_str)

    for c in (bundle.get("charts") or []):
        sym, rng, o, cl, h, l, pts = (c.get("symbol", ""), c.get("range", ""), c.get("period_open", 0),
                                      c.get("period_close", 0), c.get("period_high", 0), c.get("period_low", 0), c.get("point_count", 0))
        pct_move = f" ({((cl-o)/o*100):+.2f}%)" if o and cl else ""
        c_str = f"[CHART {rng}] {sym}: Open={o}  Close={cl}{pct_move}  High={h}  Low={l}  ({pts} pts)"
        if current_char_count + len(c_str) > max_chars:
            break
        lines.append(c_str)
        current_char_count += len(c_str)

    if bundle.get("pair_comparison_deck"):
        p = bundle["pair_comparison_deck"]
        p_str = f"[PAIR RS MATRIX] {p['leader_symbol']} outperforms by +{abs(p['rs_delta_pct'])}% in {p.get('lookback_days', 20)}-day momentum."
        if current_char_count + len(p_str) <= max_chars:
            lines.append(p_str)

    return "\n".join(lines)


# Line 2961~2970: 任務發起時傳遞 trade_context 與 retrieval_budget
tasks.append(
    fetch_stock_data_bundle(
        symbols=stock_symbols,
        dimensions=stock_dimensions,
        chart_range=stock_chart_range,
        market_svc=market_svc,
        entities_hint=stock_entities,
        search_keyword_override=model_user_input,
        trade_context=intent.get("trade_context"),
        retrieval_budget=prompt_budget.retrieval_budget,
    )
)


# Line 3021~3040: [地點 1] SSE early_stock_meta 全量欄位透傳 (含 pair_comparison_deck)
early_stock_meta = {
    "mode_enabled": True,
    "symbols": [
        e.get("canonical_name") or e.get("query_term")
        for e in stock_entities
    ] if stock_entities else stock_symbols,
    "symbols_resolved": [
        q.get("symbol")
        for q in (stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [])
    ],
    "quotes": stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [],
    "fundamentals": stock_bundle.get("fundamentals", []) if isinstance(stock_bundle, dict) else [],
    "pair_comparison_deck": stock_bundle.get("pair_comparison_deck") if isinstance(stock_bundle, dict) else None,
    "chart_attached": bool(stock_bundle.get("charts", [])) if isinstance(stock_bundle, dict) else False,
    "chart_range": stock_chart_range,
    "news_count": len(stock_bundle.get("news", [])) if isinstance(stock_bundle, dict) else 0,
    "flash_count": len(stock_bundle.get("flash", [])) if isinstance(stock_bundle, dict) else 0,
    "calendar_count": len(stock_bundle.get("calendar", [])) if isinstance(stock_bundle, dict) else 0,
    "dimensions_fetched": stock_dimensions,
    "latency_ms": stock_bundle.get("latency_ms", 0),
}
yield _sse_event("meta", meta={
    "stock": early_stock_meta,
    "assistant_message_id": str(assistant_message_id) if assistant_message_id else None,
})


# Line 3296~3314: [地點 2] Postgres DB final_meta 持久化 100% 欄位同步
if stock_mode_triggered and (stock_entities or stock_symbols):
    final_meta["stock"] = {
        "mode_enabled": True,
        "symbols": [
            e.get("canonical_name") or e.get("query_term")
            for e in stock_entities
        ] if stock_entities else stock_symbols,
        "symbols_resolved": [
            q.get("symbol")
            for q in (stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [])
        ],
        "quotes": stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [],
        "fundamentals": stock_bundle.get("fundamentals", []) if isinstance(stock_bundle, dict) else [],
        "pair_comparison_deck": stock_bundle.get("pair_comparison_deck") if isinstance(stock_bundle, dict) else None,
        "chart_attached": bool(stock_bundle.get("charts", [])) if isinstance(stock_bundle, dict) else False,
        "chart_range": stock_chart_range,
        "news_count": len(stock_bundle.get("news", [])) if isinstance(stock_bundle, dict) else 0,
        "flash_count": len(stock_bundle.get("flash", [])) if isinstance(stock_bundle, dict) else 0,
        "calendar_count": len(stock_bundle.get("calendar", [])) if isinstance(stock_bundle, dict) else 0,
        "dimensions_fetched": stock_dimensions,
        "latency_ms": stock_bundle.get("latency_ms", 0),
    }


# Line 3429~3445: [地點 3] 流式終態 final SSE meta 100% 欄位同步
if stock_mode_triggered and (stock_entities or stock_symbols):
    stock_meta_payload = {
        "mode_enabled": True,
        "symbols": [
            e.get("canonical_name") or e.get("query_term")
            for e in stock_entities
        ] if stock_entities else stock_symbols,
        "symbols_resolved": [
            q.get("symbol")
            for q in (stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [])
        ],
        "quotes": stock_bundle.get("quotes", []) if isinstance(stock_bundle, dict) else [],
        "fundamentals": stock_bundle.get("fundamentals", []) if isinstance(stock_bundle, dict) else [],
        "pair_comparison_deck": stock_bundle.get("pair_comparison_deck") if isinstance(stock_bundle, dict) else None,
        "chart_attached": bool(stock_bundle.get("charts", [])) if isinstance(stock_bundle, dict) else False,
        "chart_range": stock_chart_range,
        "news_count": len(stock_bundle.get("news", [])) if isinstance(stock_bundle, dict) else 0,
        "flash_count": len(stock_bundle.get("flash", [])) if isinstance(stock_bundle, dict) else 0,
        "calendar_count": len(stock_bundle.get("calendar", [])) if isinstance(stock_bundle, dict) else 0,
        "dimensions_fetched": stock_dimensions,
        "latency_ms": stock_bundle.get("latency_ms", 0),
    }
    meta_payload["stock"] = stock_meta_payload
```

---

### 第二部分：前端漸進式 Financial Deck 與氣泡集成

#### 5. [MODIFY] [`lumina_web/src/lib/stream-parser.ts`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/lib/stream-parser.ts)

```typescript
// [MODIFY] lumina_web/src/lib/stream-parser.ts
// 補齊全套量化、style_code 與多標的 TypeScript 介面定義

export interface StreamMeta {
    // ... other meta fields ...
    stock?: {
        symbols: string[];
        quotes: Array<{
            symbol: string;
            name?: string;
            price?: number | string;
            change?: number | string;
            change_percent?: number | string;
            market_state?: string;
            exchange?: string;
            currency?: string;
            asset_type?: string;
            open?: number | string;
            high?: number | string;
            low?: number | string;
            volume?: number | string;
            sparkline?: number[];
            open_price?: number;
            composite_score?: number;
            style?: string;
            style_code?: string;
            scores?: { tech: number; fund: number; val: number; sent: number };
            macro_regime_multiplier?: number;
            macro_status?: string;
            is_leveraged?: boolean;
            annual_decay_pct?: number;
            leveraged_warning?: string;
            stop_price?: number;
            target_price?: number;
            rr_ratio?: number;
            recommended_pos_pct?: number;
            account_max_loss_pct?: number;
            trade_status?: string;
            expected_days?: number;
            stale_limit_days?: number;
            time_stop_guidance?: string;
            fomo_dampener?: boolean;
            declared_currency?: string;
            declared_capital_raw?: number | null;
            declared_capital_usd?: number | null;
            fx_rate_to_usd?: number;
            target_allocation_pct?: number | null;
            trade_intent?: "BUY_TACTICAL" | "INFORMATIONAL" | "HOLD_EXIT_CONSULT";
            benchmark_info?: {
                benchmark_name: string;
                symbol: string;
                change_percent: string;
                is_positive: boolean;
            };
            regime_summary?: string;
            is_super_trend?: boolean;
        }>;
        pair_comparison_deck?: {
            symbol_a: string;
            symbol_b: string;
            rs_delta_pct: number;
            leader_symbol: string;
            lookback_days?: number;
        } | null;
        fundamentals?: Array<any>;
        flash_count?: number;
        latency_ms?: number;
    };
}
```

---

#### 6. [NEW] [`lumina_web/src/components/chat/stock/SparklineChart.tsx`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/components/chat/stock/SparklineChart.tsx)

```tsx
"use client";

import React, { useState, useId } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";

interface SparklineChartProps {
    points: number[];
    openPrice?: number;
    isPositive?: boolean;
    currentRange?: string;
    onRangeChange?: (range: string) => void;
}

export const SparklineChart: React.FC<SparklineChartProps> = ({
    points,
    openPrice,
    isPositive = true,
    currentRange = "1D",
    onRangeChange,
}) => {
    const { t } = useTranslation();
    const gradientId = useId();
    const [hoverIndex, setHoverIndex] = useState<number | null>(null);

    // 🛡️ 單點/零點保護：若僅有 1 個點自動填充為雙點，杜絕永久加載動畫
    const safePoints = !points || points.length === 0
        ? []
        : points.length === 1
            ? [points[0], points[0]]
            : points;

    if (safePoints.length < 2) {
        return (
            <div className="w-full h-24 flex items-center justify-center text-xs text-slate-500 font-mono bg-black/20 rounded-2xl border border-white/5">
                {t("stockDeck.chartLoading", "走勢資料載入中...")}
            </div>
        );
    }

    const width = 400;
    const height = 110;
    const padding = { top: 12, bottom: 20, left: 8, right: 8 };

    const minVal = Math.min(...safePoints);
    const maxVal = Math.max(...safePoints);
    const span = maxVal - minVal;
    const safeSpan = span > 1e-6 ? span : 1.0;

    const chartW = width - padding.left - padding.right;
    const chartH = height - padding.top - padding.bottom;

    const coords = safePoints.map((p, idx) => {
        const x = padding.left + (idx / (safePoints.length - 1)) * chartW;
        const y = span <= 1e-6
            ? padding.top + chartH / 2.0
            : padding.top + chartH - ((p - minVal) / safeSpan) * chartH;
        return { x, y, price: p };
    });

    const openY = (openPrice !== undefined && span > 1e-6)
        ? padding.top + chartH - ((openPrice - minVal) / safeSpan) * chartH
        : (openPrice !== undefined ? padding.top + chartH / 2.0 : null);

    const pathD = coords.reduce((acc, curr, idx) => {
        return `${acc} ${idx === 0 ? "M" : "L"} ${curr.x.toFixed(1)},${curr.y.toFixed(1)}`;
    }, "");

    const areaD = `${pathD} L ${coords[coords.length - 1].x.toFixed(1)},${height} L ${coords[0].x.toFixed(1)},${height} Z`;
    const activePoint = hoverIndex !== null ? coords[hoverIndex] : coords[coords.length - 1];
    const ranges = ["1D", "5D", "1M", "1Y"];

    const strokeColor = isPositive ? "#34d399" : "#f87171";
    const fillColor = isPositive ? "#10b981" : "#ef4444";

    const updateTouchPoint = (clientX: number, targetRect: DOMRect) => {
        const relX = (clientX - targetRect.left) / targetRect.width;
        const idx = Math.min(Math.max(0, Math.round(relX * (safePoints.length - 1))), safePoints.length - 1);
        setHoverIndex(idx);
    };

    // 🛡️ Meme 幣動態多階梯精度格式化 (防止 $0.0000 坍縮)
    const formatDisplayPrice = (p: number) => {
        if (p >= 1.0) return p.toFixed(2);
        if (p >= 0.01) return p.toFixed(4);
        if (p >= 0.0001) return p.toFixed(6);
        return p.toFixed(8);
    };

    return (
        <div className="w-full rounded-2xl bg-black/30 border border-white/5 p-3 space-y-2 select-none">
            <div className="flex items-center justify-between">
                <div className="flex items-center gap-1 bg-white/5 p-0.5 rounded-lg border border-white/5">
                    {ranges.map((r) => (
                        <button
                            key={r}
                            type="button"
                            onClick={() => onRangeChange && onRangeChange(r)}
                            className={cn(
                                "px-2 py-0.5 rounded text-[10px] font-mono font-bold transition-all",
                                currentRange === r
                                    ? "bg-indigo-500 text-white shadow-sm"
                                    : "text-slate-400 hover:text-slate-200"
                            )}
                        >
                            {r}
                        </button>
                    ))}
                </div>

                <div className="text-right">
                    <span className="text-xs font-mono font-bold text-slate-200">
                        ${formatDisplayPrice(activePoint.price)}
                    </span>
                    {openPrice && openPrice > 0 && (
                        <span className="text-[10px] font-mono text-slate-400 ml-1.5">
                            ({(((activePoint.price - openPrice) / openPrice) * 100).toFixed(2)}%)
                        </span>
                    )}
                </div>
            </div>

            <div className="relative w-full h-[110px] overflow-hidden">
                <svg
                    viewBox={`0 0 ${width} ${height}`}
                    className="w-full h-full cursor-crosshair overflow-visible touch-none"
                    onMouseMove={(e) => {
                        const rect = e.currentTarget.getBoundingClientRect();
                        updateTouchPoint(e.clientX, rect);
                    }}
                    onMouseLeave={() => setHoverIndex(null)}
                    onTouchStart={(e) => {
                        if (e.touches.length > 0) {
                            const rect = e.currentTarget.getBoundingClientRect();
                            updateTouchPoint(e.touches[0].clientX, rect);
                        }
                    }}
                    onTouchMove={(e) => {
                        if (e.touches.length > 0) {
                            const rect = e.currentTarget.getBoundingClientRect();
                            updateTouchPoint(e.touches[0].clientX, rect);
                        }
                    }}
                    onTouchEnd={() => setHoverIndex(null)}
                >
                    <defs>
                        <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
                            <stop offset="0%" stopColor={fillColor} stopOpacity="0.25" />
                            <stop offset="100%" stopColor={fillColor} stopOpacity="0.0" />
                        </linearGradient>
                    </defs>

                    {openY !== null && openY >= padding.top && openY <= height - padding.bottom && (
                        <line
                            x1={padding.left}
                            y1={openY}
                            x2={width - padding.right}
                            y2={openY}
                            stroke="rgba(255, 255, 255, 0.15)"
                            strokeDasharray="3 3"
                            strokeWidth="1"
                        />
                    )}

                    <path d={areaD} fill={`url(#${gradientId})`} />
                    <path
                        d={pathD}
                        fill="none"
                        stroke={strokeColor}
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                    />

                    {activePoint && (
                        <g>
                            <line
                                x1={activePoint.x}
                                y1={padding.top}
                                x2={activePoint.x}
                                y2={height - padding.bottom}
                                stroke="rgba(255, 255, 255, 0.2)"
                                strokeWidth="1"
                                strokeDasharray="2 2"
                            />
                            <circle
                                cx={activePoint.x}
                                cy={activePoint.y}
                                r="4"
                                fill={strokeColor}
                                stroke="#0f172a"
                                strokeWidth="2"
                            />
                        </g>
                    )}
                </svg>
            </div>

            <div className="flex items-center justify-between text-[9px] font-mono text-slate-500 px-1">
                <span>{t("stockDeck.low", "低")}: ${formatDisplayPrice(minVal)}</span>
                <span>{t("stockDeck.high", "高")}: ${formatDisplayPrice(maxVal)}</span>
            </div>
        </div>
    );
};
```

---

#### 7. [NEW] [`lumina_web/src/components/chat/stock/StockPairComparisonDeck.tsx`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/components/chat/stock/StockPairComparisonDeck.tsx)

```tsx
"use client";

import React from "react";
import { useTranslation } from "react-i18next";
import { GitCompare, TrendingUp } from "lucide-react";
import { cn } from "@/lib/utils";

interface StockPairComparisonDeckProps {
    pairData: {
        symbol_a: string;
        symbol_b: string;
        rs_delta_pct: number;
        leader_symbol: string;
        lookback_days?: number;
    };
}

export const StockPairComparisonDeck: React.FC<StockPairComparisonDeckProps> = ({ pairData }) => {
    const { t } = useTranslation();
    const isALeading = pairData.leader_symbol === pairData.symbol_a;
    const absDiff = Math.abs(pairData.rs_delta_pct);
    const lookback = pairData.lookback_days || 20;
    const isBalanced = absDiff < 0.1;

    return (
        <div className="w-full my-2.5 p-3 rounded-2xl bg-gradient-to-r from-indigo-950/40 via-slate-900/60 to-purple-950/40 border border-indigo-500/20 shadow-md backdrop-blur-md">
            <div className="flex items-center justify-between">
                <div className="flex items-center gap-2">
                    <div className="p-1.5 rounded-lg bg-indigo-500/20 text-indigo-400">
                        <GitCompare size={14} />
                    </div>
                    <span className="text-xs font-bold text-slate-200">
                        {t("stockDeck.pairComparisonTitle", "雙標的相對強弱矩陣 (Pair RS)")}
                    </span>
                </div>
                <div className="flex items-center gap-1.5">
                    <span className="text-[10px] font-mono text-slate-400">
                        {t("stockDeck.twentyDayRsLabel", "近20日強弱比")}:
                    </span>
                    <span className={cn("text-xs font-mono font-bold flex items-center", isBalanced ? "text-slate-300" : "text-emerald-400")}>
                        {!isBalanced && <TrendingUp size={12} className="mr-0.5" />}
                        {isBalanced ? t("stockDeck.rsBalanced", "動能勢均力敵") : `${pairData.leader_symbol} (+${absDiff}%)`}
                    </span>
                </div>
            </div>

            <div className="mt-2 text-[11px] text-slate-300 leading-relaxed font-mono bg-black/20 p-2 rounded-xl border border-white/5">
                {isBalanced ? (
                    <span>⚖️ <strong>{pairData.symbol_a}</strong> {t("stockDeck.rsBalancedStatement", "與")} <strong>{pairData.symbol_b}</strong> {t("stockDeck.rsBalancedDesc", "在近 20 個交易日中動能高度同步，強弱勢差異不明顯。")}</span>
                ) : (
                    <span>💡 <strong>{pairData.leader_symbol}</strong> {t("stockDeck.rsLeadingStatement", "在近 20 個交易日中動能相對領先")} <strong>{isALeading ? pairData.symbol_b : pairData.symbol_a}</strong> {absDiff}%，{t("stockDeck.rsRecommendation", "若進行同板塊套利或輪動建倉，優先考慮動能領先標的。")}</span>
                )}
            </div>
        </div>
    );
};
```

---

#### 8. [NEW] [`lumina_web/src/components/chat/stock/StockStrategicGrid.tsx`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/components/chat/stock/StockStrategicGrid.tsx)

```tsx
"use client";

import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { ShieldCheck, Target, ArrowUpRight, ArrowDownRight, AlertOctagon, Calculator, ChevronDown, ChevronUp, Clock } from "lucide-react";
import { cn } from "@/lib/utils";

interface StockStrategicGridProps {
    currentPrice: number;
    stopPrice: number;
    targetPrice: number;
    rrRatio: number;
    recommendedPosPct: number;
    accountMaxLossPct?: number;
    tradeStatus: string;
    fomoDampener?: boolean;
    declaredCapitalRaw?: number | null;
    declaredCurrency?: string;
    fxRateToUsd?: number;
    expectedDays?: number;
    staleLimitDays?: number;
    timeStopGuidance?: string;
}

const FALLBACK_FX_RATES: Record<string, number> = {
    USD: 1.0, TWD: 0.03125, JPY: 0.0067, CNY: 0.14, EUR: 1.08,
    HKD: 0.128, GBP: 1.30, KRW: 0.00075, CAD: 0.74, AUD: 0.65,
};

export const StockStrategicGrid: React.FC<StockStrategicGridProps> = ({
    currentPrice,
    stopPrice,
    targetPrice,
    rrRatio,
    recommendedPosPct,
    accountMaxLossPct = 0.8,
    tradeStatus,
    fomoDampener = false,
    declaredCapitalRaw = null,
    declaredCurrency = "USD",
    fxRateToUsd = 1.0,
    expectedDays = 8,
    staleLimitDays = 12,
    timeStopGuidance,
}) => {
    const { t } = useTranslation();
    const [currency, setCurrency] = useState<string>(declaredCurrency || "USD");
    const [customCapitalRaw, setCustomCapitalRaw] = useState<string>(declaredCapitalRaw ? String(declaredCapitalRaw) : "");
    const [showCalculator, setShowCalculator] = useState<boolean>(Boolean(declaredCapitalRaw));
    const [isFractional, setIsFractional] = useState<boolean>(false);

    if (currentPrice <= 0) return null;

    const isApproved = tradeStatus === "APPROVED";
    const stopDistancePct = currentPrice > 0 ? ((currentPrice - stopPrice) / currentPrice) * 100 : 0;
    const targetDistancePct = currentPrice > 0 ? ((targetPrice - currentPrice) / currentPrice) * 100 : 0;

    const cleanInput = customCapitalRaw
        .replace(/[０-９]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0))
        .replace(/,/g, "");
    const rawVal = Math.max(0, parseFloat(cleanInput) || 0);

    const effectiveFxRate = (currency === declaredCurrency && fxRateToUsd && fxRateToUsd > 0)
        ? fxRateToUsd
        : (FALLBACK_FX_RATES[currency] || 1.0);
    const capInUsd = currency === "USD" ? rawVal : (rawVal * effectiveFxRate);

    const riskBudgetUsd = capInUsd * 0.01;
    const stopDistanceUsd = Math.max(currentPrice - stopPrice, currentPrice * 0.005);

    const maxCapAllowedUsd = capInUsd * (recommendedPosPct / 100.0);
    const rawSharesByRisk = stopDistanceUsd > 0 ? riskBudgetUsd / stopDistanceUsd : 0;
    const rawSharesByCap = currentPrice > 0 ? maxCapAllowedUsd / currentPrice : 0;
    const rawMaxShares = capInUsd > 0 && isApproved ? Math.min(rawSharesByRisk, rawSharesByCap) : 0;

    const maxShares = isFractional ? parseFloat(rawMaxShares.toFixed(4)) : Math.floor(rawMaxShares);
    const capitalUsedUsd = maxShares * currentPrice;
    const maxLossDollar = maxShares * stopDistanceUsd;
    const maxRewardDollar = maxShares * Math.max(targetPrice - currentPrice, 0.0);

    const toLocalCurrency = (usdVal: number) => {
        if (currency === "USD" || effectiveFxRate === 0) return "";
        const localVal = usdVal / effectiveFxRate;
        const fractionDigits = (currency === "JPY" || currency === "KRW") ? 0 : 0;
        return ` (約 ${currency} ${localVal.toLocaleString(undefined, { maximumFractionDigits: fractionDigits })})`;
    };

    const formatPrice = (p: number) => {
        if (p >= 1.0) return p.toFixed(2);
        if (p >= 0.01) return p.toFixed(4);
        if (p >= 0.0001) return p.toFixed(6);
        return p.toFixed(8);
    };

    const statusBadge = isApproved 
        ? { label: "🟢 (優良)", cls: "text-emerald-400" }
        : tradeStatus === "ZERO_LIQUIDITY_NO_TRADE"
            ? { label: "🔴 (流動性枯竭禁買)", cls: "text-rose-400" }
            : tradeStatus === "EXTREME_VOLATILITY_NO_TRADE"
                ? { label: "🔴 (波幅過大禁買)", cls: "text-rose-400" }
                : tradeStatus === "PRICE_ANOMALY_NO_TRADE"
                    ? { label: "🔴 (價格異常禁買)", cls: "text-rose-400" }
                    : { label: "🟡 (賠率偏低)", cls: "text-amber-400" };

    return (
        <div className="rounded-2xl bg-slate-900/60 border border-white/10 p-3.5 space-y-3">
            {/* 頂部標題與風控上限徽章 */}
            <div className="flex items-center justify-between">
                <div className="flex items-center gap-1.5 text-xs font-bold text-slate-200">
                    <ShieldCheck size={15} className="text-indigo-400" />
                    <span>{t("stockDeck.strategicGrid", "四宮格攻防邊界 (Strategic Grid)")}</span>
                </div>
                <div className="flex items-center gap-2">
                    <span className="text-[10px] font-mono text-slate-400">{t("stockDeck.recommendedPosition", "理論風控上限")}:</span>
                    <span className={cn(
                        "text-xs font-mono font-bold px-2 py-0.5 rounded-full border",
                        recommendedPosPct > 15
                            ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
                            : recommendedPosPct > 0
                                ? "bg-amber-500/15 text-amber-300 border-amber-500/30"
                                : "bg-rose-500/15 text-rose-300 border-rose-500/30"
                    )}>
                        {recommendedPosPct}%
                    </span>
                </div>
            </div>

            {/* 客觀心理阻尼警示 */}
            {fomoDampener && (
                <div className="flex items-start gap-1.5 p-2 rounded-xl bg-amber-950/30 border border-amber-500/30 text-[11px] text-amber-300/90 leading-relaxed">
                    <AlertOctagon size={14} className="shrink-0 text-amber-400 mt-0.5" />
                    <span>⚠️ <strong>{t("stockDeck.fomoWarningTitle", "統計極端過熱警報：")}</strong>{t("stockDeck.fomoWarningText", "當前 10 日漲幅處於歷史前 2% 極值區，統計上處於高波動超買區，建議等待回踩支撐，避免市價追高。")}</span>
                </div>
            )}

            {/* 四宮格核心數據 */}
            <div className="grid grid-cols-2 gap-2.5">
                <div className="p-2.5 rounded-xl bg-white/[0.03] border border-white/5 space-y-1">
                    <div className="text-[10px] text-slate-400 font-medium">{t("stockDeck.currentPrice", "當前現價")}</div>
                    <div className="text-base font-bold font-mono text-white">${formatPrice(currentPrice)}</div>
                </div>

                <div className="p-2.5 rounded-xl bg-white/[0.03] border border-white/5 space-y-1">
                    <div className="flex items-center justify-between text-[10px] text-emerald-400 font-medium">
                        <span>{t("stockDeck.targetResistance", "目標阻力 (POC)")}</span>
                        <ArrowUpRight size={12} />
                    </div>
                    <div className="text-base font-bold font-mono text-emerald-400">
                        ${formatPrice(targetPrice)}
                        <span className="text-[10px] font-normal text-emerald-500/80 ml-1">
                            (+{targetDistancePct.toFixed(1)}%)
                        </span>
                    </div>
                </div>

                <div className="p-2.5 rounded-xl bg-white/[0.03] border border-white/5 space-y-1">
                    <div className="flex items-center justify-between text-[10px] text-rose-400 font-medium">
                        <span>{t("stockDeck.hardStopLoss", "硬防守點 (ATR緩衝)")}</span>
                        <ArrowDownRight size={12} />
                    </div>
                    <div className="text-base font-bold font-mono text-rose-400">
                        ${formatPrice(stopPrice)}
                        <span className="text-[10px] font-normal text-rose-500/80 ml-1">
                            (-{stopDistancePct.toFixed(1)}%)
                        </span>
                    </div>
                </div>

                <div className="p-2.5 rounded-xl bg-white/[0.03] border border-white/5 space-y-1">
                    <div className="flex items-center justify-between text-[10px] text-slate-400 font-medium">
                        <span>{t("stockDeck.rrRatio", "盈虧比 (R:R)")}</span>
                        <Target size={12} className={statusBadge.cls} />
                    </div>
                    <div className={cn("text-base font-bold font-mono", statusBadge.cls)}>
                        1 : {rrRatio.toFixed(2)}
                        <span className="text-[10px] font-normal text-slate-400 ml-1">
                            {statusBadge.label}
                        </span>
                    </div>
                </div>
            </div>

            {/* 確定性總帳戶最大虧損與時間有效性 Horizon 提示 */}
            <div className="space-y-1.5">
                <div className="text-[10px] font-mono text-slate-400 text-center bg-black/20 py-1.5 px-2 rounded-lg border border-white/5">
                    🛡️ {t("stockDeck.maxLossPrefix", "觸發極限止損時，在")} {recommendedPosPct}% {t("stockDeck.maxLossMid", "倉位下，總帳戶本金最大損失鎖定在")} <strong className="text-rose-300">-{accountMaxLossPct}%</strong>
                </div>

                <div className="flex items-center justify-center gap-1.5 text-[10px] font-mono text-indigo-300/90 bg-indigo-950/20 py-1 px-2 rounded-lg border border-indigo-500/10">
                    <Clock size={11} className="text-indigo-400 shrink-0" />
                    <span>{t("stockDeck.timeHorizonPrefix", "波段推進週期")}: <strong>{expectedDays}d</strong> ({t("stockDeck.staleStopPrefix", "超期止損")}: <strong>{staleLimitDays}d</strong>)</span>
                </div>
            </div>

            {/* 💰 雙模態本金精算器 (含原幣/USD 雙向折算與碎股開關) */}
            <div className="pt-1">
                <button
                    type="button"
                    onClick={() => setShowCalculator(!showCalculator)}
                    className="w-full flex items-center justify-between text-[11px] font-semibold text-slate-400 hover:text-slate-200 py-1 px-1 transition-colors"
                >
                    <div className="flex items-center gap-1.5">
                        <Calculator size={13} className="text-indigo-400" />
                        <span>{t("stockDeck.capitalCalcToggle", "💰 本金下單精算器 (雙幣即時試算)")}</span>
                    </div>
                    {showCalculator ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
                </button>

                {showCalculator && (
                    <div className="mt-2 p-3 rounded-xl bg-slate-950/60 border border-white/10 space-y-2.5 animate-in fade-in duration-150">
                        <div className="flex items-center justify-between gap-2">
                            <div className="flex items-center gap-1.5 flex-1">
                                <select
                                    value={currency}
                                    onChange={(e) => setCurrency(e.target.value)}
                                    className="bg-white/5 border border-white/10 rounded-lg px-1.5 py-1 text-xs font-mono text-indigo-300 focus:outline-none"
                                >
                                    <option value="USD">USD</option>
                                    <option value="TWD">TWD</option>
                                    <option value="JPY">JPY</option>
                                    <option value="CNY">CNY</option>
                                    <option value="EUR">EUR</option>
                                    <option value="HKD">HKD</option>
                                    <option value="GBP">GBP</option>
                                    <option value="KRW">KRW</option>
                                    <option value="CAD">CAD</option>
                                    <option value="AUD">AUD</option>
                                </select>
                                <input
                                    type="text"
                                    value={customCapitalRaw}
                                    onChange={(e) => setCustomCapitalRaw(e.target.value)}
                                    placeholder="例如: 500000"
                                    className="w-full scroll-m-12 bg-white/5 border border-white/10 rounded-lg px-2 py-1 text-xs font-mono text-white focus:outline-none focus:border-indigo-500/50"
                                />
                            </div>

                            <button
                                type="button"
                                onClick={() => setIsFractional(!isFractional)}
                                className={cn(
                                    "px-2 py-1 rounded-lg text-[9px] font-bold border transition-colors shrink-0",
                                    isFractional
                                        ? "bg-indigo-500/20 text-indigo-300 border-indigo-500/40 shadow-sm"
                                        : "bg-white/5 text-slate-400 border-white/10 hover:text-white"
                                )}
                            >
                                {isFractional ? t("stockDeck.fractionalOn", "🔘 碎股模式") : t("stockDeck.fractionalOff", "整數股模式")}
                            </button>
                        </div>

                        {currency !== "USD" && rawVal > 0 && (
                            <div className="text-[10px] font-mono text-slate-400 px-1">
                                ≈ ${capInUsd.toLocaleString(undefined, { maximumFractionDigits: 2 })} USD ({t("stockDeck.liveFxRateLabel", "即時匯率折算")})
                            </div>
                        )}

                        {rawVal > 0 && (
                            <div className="space-y-1.5 pt-1 border-t border-white/5">
                                {!isApproved ? (
                                    <div className="text-[10px] font-mono text-rose-300/90 p-2 rounded-lg bg-rose-950/20 border border-rose-500/20">
                                        🛡️ 當前標的觸發風控約束（{statusBadge.label}），建議保持觀望，暫不建倉。
                                    </div>
                                ) : maxShares > 0 ? (
                                    <>
                                        <div className="flex items-center justify-between text-xs font-mono">
                                            <span className="text-slate-400">{t("stockDeck.recommendedShares", "建議建倉規模:")}</span>
                                            <span className="font-bold text-white">
                                                {isFractional ? maxShares.toFixed(4) : maxShares} 股 <span className="text-[10px] text-slate-400">(${capitalUsedUsd.toFixed(2)}{toLocalCurrency(capitalUsedUsd)})</span>
                                            </span>
                                        </div>
                                        <div className="flex items-center justify-between text-[11px] font-mono">
                                            <span className="text-rose-400/90">{t("stockDeck.stopLossDollar", "極限止損虧損:")}</span>
                                            <span className="font-bold text-rose-400">
                                                -${maxLossDollar.toFixed(2)}{toLocalCurrency(maxLossDollar)}
                                            </span>
                                        </div>
                                        <div className="flex items-center justify-between text-[11px] font-mono">
                                            <span className="text-emerald-400/90">{t("stockDeck.targetRewardDollar", "到達目標預期收益:")}</span>
                                            <span className="font-bold text-emerald-400">
                                                +${maxRewardDollar.toFixed(2)}{toLocalCurrency(maxRewardDollar)}
                                            </span>
                                        </div>
                                    </>
                                ) : (
                                    <div className="text-[10px] font-mono text-amber-300/90 p-2 rounded-lg bg-amber-950/20 border border-amber-500/20">
                                        ⚠️ {t("stockDeck.zeroShareWarning", "單股價格超出 1% 風險預算，建議透過右上角切換為「碎股模式」或適度增加配置資金。")}
                                    </div>
                                )}
                            </div>
                        )}
                    </div>
                )}
            </div>
        </div>
    );
};
```

---

#### 9. [NEW] [`lumina_web/src/components/chat/stock/StockMarketCard.tsx`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/components/chat/stock/StockMarketCard.tsx)

```tsx
"use client";

import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { SparklineChart } from "./SparklineChart";
import { StockStrategicGrid } from "./StockStrategicGrid";
import { ChevronDown, ChevronUp, AlertTriangle, Sparkles, TrendingUp, TrendingDown, Layers, FileText, BarChart3, Globe, Zap, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { getStoredAccessToken } from "@/lib/auth-session";

interface StockMarketCardProps {
    meta: {
        symbol: string;
        name?: string;
        price?: number | string;
        change?: number | string;
        change_percent?: number | string;
        market_state?: string;
        exchange?: string;
        currency?: string;
        asset_type?: string;
        sparkline?: number[];
        open_price?: number;
        composite_score?: number;
        style?: string;
        style_code?: string;
        macro_regime_multiplier?: number;
        macro_status?: string;
        is_leveraged?: boolean;
        annual_decay_pct?: number;
        leveraged_warning?: string;
        regime_summary?: string;
        factor_tension_explanation?: string;
        top_3_risks?: string[];
        top_2_catalysts?: string[];
        execution_guidance?: string;
        trade_status?: string;
        stop_price?: number;
        target_price?: number;
        rr_ratio?: number;
        recommended_pos_pct?: number;
        account_max_loss_pct?: number;
        expected_days?: number;
        stale_limit_days?: number;
        time_stop_guidance?: string;
        fomo_dampener?: boolean;
        declared_capital_raw?: number | null;
        declared_currency?: string;
        fx_rate_to_usd?: number;
        target_allocation_pct?: number | null;
        trade_intent?: "BUY_TACTICAL" | "INFORMATIONAL" | "HOLD_EXIT_CONSULT";
        benchmark_info?: {
            benchmark_name: string;
            symbol: string;
            change_percent: string;
            is_positive: boolean;
        };
        fundamentals?: any[];
    };
    onRangeChange?: (symbol: string, range: string) => void;
}

export const StockMarketCard: React.FC<StockMarketCardProps> = ({ meta, onRangeChange }) => {
    const { t } = useTranslation();
    const [isDrawerOpen, setIsDrawerOpen] = useState(false);
    const [activeRange, setActiveRange] = useState("1D");
    const [livePoints, setLivePoints] = useState<number[]>(meta.sparkline || []);
    const [isLoadingChart, setIsLoadingChart] = useState(false);

    const priceNum = typeof meta.price === "number" ? meta.price : parseFloat(String(meta.price || "").replace(/,/g, "")) || 0;
    
    const rawChange = String(meta.change || "").replace(/,/g, "").trim();
    const changeNum = rawChange !== "" ? parseFloat(rawChange) : (meta.open_price ? priceNum - meta.open_price : 0);
    const isPositive = !isNaN(changeNum) ? changeNum >= 0 : true;

    const score = meta.composite_score || 50;
    const isTactical = meta.trade_intent === "BUY_TACTICAL";

    const handleRangeSelect = async (r: string) => {
        if (r === "1D" && meta.sparkline) {
            setActiveRange("1D");
            setLivePoints(meta.sparkline);
            if (onRangeChange) onRangeChange(meta.symbol, "1D");
            return;
        }
        try {
            setIsLoadingChart(true);
            const token = getStoredAccessToken();
            const res = await fetch(`/api/v1/market/chart?symbol=${encodeURIComponent(meta.symbol)}&range=${r}`, {
                headers: token ? { Authorization: `Bearer ${token}` } : {},
            });
            if (res.ok) {
                const json = await res.json();
                const rawPts = json.data?.points || [];
                const closes = rawPts.map((p: any) => p.close || p.value || 0).filter((c: number) => c > 0);
                if (closes.length > 0) {
                    setLivePoints(closes);
                    setActiveRange(r);
                }
            }
        } catch (err) {
            // 🛡️ 401 或弱認證網絡異常時優雅保持當前狀態
        } finally {
            setIsLoadingChart(false);
        }
        if (onRangeChange) {
            onRangeChange(meta.symbol, r);
        }
    };

    const fundObj = (meta.fundamentals && meta.fundamentals.length > 0) ? meta.fundamentals[0] : null;
    const quarters = fundObj?.quarters || [];
    
    // 🛡️ Meme 幣動態階梯精度格式化 (防止 $0.0000 坍縮)
    const formatDisplayPrice = (p: number) => {
        if (p >= 1.0) return p.toFixed(2);
        if (p >= 0.01) return p.toFixed(4);
        if (p >= 0.0001) return p.toFixed(6);
        return p.toFixed(8);
    };

    const marketStateBadge = meta.market_state === "REGULAR" 
        ? "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30"
        : meta.market_state === "PRE_MARKET" 
            ? "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30"
            : meta.market_state === "POST_MARKET" 
                ? "bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-500/15 dark:text-purple-300 dark:border-purple-500/30"
                : "bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-500/15 dark:text-slate-300 dark:border-slate-500/30";

    const marketStateLabel = meta.market_state === "REGULAR" ? t("messageTools.marketStateRegular", "開盤中")
        : meta.market_state === "PRE_MARKET" ? t("messageTools.marketStatePreMarket", "盤前")
        : meta.market_state === "POST_MARKET" ? t("messageTools.marketStatePostMarket", "盤後")
        : meta.market_state === "CLOSED" ? t("messageTools.marketStateClosed", "已閉市")
        : (meta.market_state ? String(meta.market_state).replace("_", " ") : "");

    const styleDisplay = meta.style_code 
        ? t(`stockDeck.styles.${meta.style_code}`, meta.style || "動能成長型")
        : (meta.style || t("stockDeck.defaultStyle", "動能成長型"));

    return (
        <div className="w-full max-w-full min-w-0 my-3 rounded-3xl bg-gradient-to-b from-slate-900/90 to-slate-950/95 border border-white/10 shadow-2xl overflow-hidden backdrop-blur-xl">
            <div className="p-4 sm:p-5 space-y-4">
                <div className="flex items-start justify-between">
                    <div>
                        <div className="flex items-center gap-2">
                            <h3 className="text-lg sm:text-xl font-bold text-white tracking-tight">{meta.symbol}</h3>
                            <span className="text-xs text-slate-400 font-medium truncate max-w-[140px] sm:max-w-[220px]">
                                {meta.name || meta.symbol}
                            </span>
                            {meta.market_state && (
                                <span className={cn("px-1.5 py-0.5 rounded text-[9px] font-bold border", marketStateBadge)}>
                                    {marketStateLabel}
                                </span>
                            )}
                            <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
                                {styleDisplay}
                            </span>
                        </div>
                        <div className="flex items-baseline gap-2 mt-1">
                            <span className="text-2xl sm:text-3xl font-black font-mono text-white tracking-tight">
                                ${formatDisplayPrice(priceNum)}
                            </span>
                            <span className={cn("text-xs sm:text-sm font-bold font-mono flex items-center", isPositive ? "text-emerald-400" : "text-rose-400")}>
                                {isPositive ? <TrendingUp size={14} className="mr-0.5" /> : <TrendingDown size={14} className="mr-0.5" />}
                                {meta.change ?? (isPositive ? "+0.00" : "-0.00")} ({meta.change_percent ?? "0.00%"})
                            </span>
                        </div>
                    </div>

                    <div className="flex flex-col items-end">
                        <div className="text-[10px] text-slate-400 font-medium">{t("stockDeck.compositeScore", "全景評分")}</div>
                        <div className={cn(
                            "text-xl sm:text-2xl font-black font-mono px-3 py-1 rounded-2xl border shadow-inner",
                            score >= 75
                                ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
                                : score >= 50
                                    ? "bg-indigo-500/15 text-indigo-300 border-indigo-500/30"
                                    : "bg-rose-500/15 text-rose-300 border-rose-500/30"
                        )}>
                            {score}
                        </div>
                    </div>
                </div>

                {meta.is_leveraged && meta.leveraged_warning && (
                    <div className="flex items-start gap-2 p-2.5 rounded-xl bg-amber-950/40 border border-amber-500/40 text-[11px] text-amber-200 leading-relaxed font-mono">
                        <Zap size={15} className="shrink-0 text-amber-400 mt-0.5" />
                        <span>{meta.leveraged_warning}</span>
                    </div>
                )}

                {meta.benchmark_info && (
                    <div className="flex items-center gap-1.5 text-[11px] font-mono text-slate-400 bg-white/[0.02] py-1 px-2.5 rounded-lg border border-white/5">
                        <Globe size={12} className="text-indigo-400" />
                        <span>{t("stockDeck.benchmarkLabel", "母體基準")} ({meta.benchmark_info.benchmark_name}):</span>
                        <span className={cn("font-bold", meta.benchmark_info.is_positive ? "text-emerald-400" : "text-rose-400")}>
                            {meta.benchmark_info.change_percent}
                        </span>
                        <span className="text-slate-500 text-[10px]">
                            {meta.macro_status === "MACRO_BEAR_DRAG"
                                ? `(${t("stockDeck.benchmarkDrag", "大盤空頭承壓調權 🔴")})`
                                : meta.benchmark_info.is_positive
                                    ? `(${t("stockDeck.benchmarkResonance", "大盤共振 🟢")})`
                                    : `(${t("stockDeck.benchmarkPressure", "大盤承壓 🔴")})`}
                        </span>
                    </div>
                )}

                {/* 純 SVG 時序 Sparkline 走勢圖 */}
                <div className="relative">
                    {isLoadingChart && (
                        <div className="absolute inset-0 bg-black/40 backdrop-blur-[1px] flex items-center justify-center z-10 rounded-2xl">
                            <Loader2 size={20} className="animate-spin text-indigo-400" />
                        </div>
                    )}
                    <SparklineChart
                        points={livePoints}
                        openPrice={activeRange === "1D" ? meta.open_price : (livePoints.length > 0 ? livePoints[0] : undefined)}
                        isPositive={isPositive}
                        currentRange={activeRange}
                        onRangeChange={handleRangeSelect}
                    />
                </div>

                {isTactical && meta.stop_price !== undefined && meta.stop_price !== null && (
                    <StockStrategicGrid
                        currentPrice={priceNum}
                        stopPrice={meta.stop_price || priceNum * 0.92}
                        targetPrice={meta.target_price || priceNum * 1.12}
                        rrRatio={meta.rr_ratio || 1.5}
                        recommendedPosPct={meta.recommended_pos_pct || 10}
                        accountMaxLossPct={meta.account_max_loss_pct}
                        tradeStatus={meta.trade_status || "APPROVED"}
                        fomoDampener={meta.fomo_dampener}
                        declaredCapitalRaw={meta.declared_capital_raw}
                        declaredCurrency={meta.declared_currency}
                        fxRateToUsd={meta.fx_rate_to_usd}
                        expectedDays={meta.expected_days}
                        staleLimitDays={meta.stale_limit_days}
                        timeStopGuidance={meta.time_stop_guidance}
                    />
                )}

                {meta.regime_summary && (
                    <div className="text-xs text-slate-300 font-medium bg-white/[0.02] p-2.5 rounded-xl border border-white/5 leading-relaxed">
                        💡 {meta.regime_summary}
                    </div>
                )}

                <button
                    type="button"
                    onClick={() => setIsDrawerOpen(!isDrawerOpen)}
                    className="w-full py-2.5 flex items-center justify-center gap-1.5 text-xs font-semibold text-indigo-300 hover:text-indigo-200 bg-indigo-500/10 hover:bg-indigo-500/15 border border-indigo-500/20 rounded-xl transition-all shadow-sm"
                >
                    <Layers size={14} />
                    <span>{isDrawerOpen ? t("stockDeck.hideDeepDive", "收起深度多空博弈報告") : t("stockDeck.viewDeepDive", "查看深度多空博弈報告 (Top-3 風險 / 催化 / 8 季財報)")}</span>
                    {isDrawerOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
                </button>
            </div>

            {isDrawerOpen && (
                <div className="border-t border-white/10 p-4 sm:p-5 bg-slate-950/80 space-y-4 animate-in fade-in duration-200">
                    {meta.time_stop_guidance && (
                        <div className="p-3 rounded-xl bg-indigo-950/30 border border-indigo-500/20 text-xs text-indigo-200/90 leading-relaxed font-mono">
                            {meta.time_stop_guidance}
                        </div>
                    )}

                    {meta.factor_tension_explanation && (
                        <div className="space-y-1.5">
                            <h4 className="text-xs font-bold text-slate-300 flex items-center gap-1">
                                <FileText size={13} className="text-indigo-400" />
                                <span>{t("stockDeck.factorTension", "多因子張力深度透視")}</span>
                            </h4>
                            <p className="text-xs text-slate-400 leading-relaxed bg-black/30 p-3 rounded-xl border border-white/5">
                                {meta.factor_tension_explanation}
                            </p>
                        </div>
                    )}

                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                        <div className="p-3 rounded-2xl bg-rose-950/20 border border-rose-500/20 space-y-2">
                            <div className="flex items-center gap-1.5 text-xs font-bold text-rose-300">
                                <AlertTriangle size={14} />
                                <span>{t("stockDeck.top3Risks", "🚨 核心下行風險 (Top-3)")}</span>
                            </div>
                            <ul className="space-y-1.5 text-[11px] text-rose-200/80">
                                {(meta.top_3_risks || ["大盤系統性流動性收緊", "宏觀利率預期波動", "短期乖離率修正需求"]).map((r, i) => (
                                    <li key={i} className="flex items-start gap-1">
                                        <span className="text-rose-400 font-bold">•</span>
                                        <span>{r}</span>
                                    </li>
                                ))}
                            </ul>
                        </div>

                        <div className="p-3 rounded-2xl bg-emerald-950/20 border border-emerald-500/20 space-y-2">
                            <div className="flex items-center gap-1.5 text-xs font-bold text-emerald-300">
                                <Sparkles size={14} />
                                <span>{t("stockDeck.top2Catalysts", "✨ 核心利好催化 (Top-2)")}</span>
                            </div>
                            <ul className="space-y-1.5 text-[11px] text-emerald-200/80">
                                {(meta.top_2_catalysts || ["行業龍頭效應持續釋放", "季報前瞻指引維持穩健"]).map((c, i) => (
                                    <li key={i} className="flex items-start gap-1">
                                        <span className="text-emerald-400 font-bold">•</span>
                                        <span>{c}</span>
                                    </li>
                                ))}
                            </ul>
                        </div>
                    </div>

                    {quarters.length > 0 && (
                        <div className="space-y-2">
                            <h4 className="text-xs font-bold text-slate-300 flex items-center gap-1">
                                <BarChart3 size={13} className="text-indigo-400" />
                                <span>{t("stockDeck.secMatrix", "SEC 10-Q 8季核心財務矩陣")}</span>
                            </h4>
                            <div className="overflow-x-auto rounded-xl border border-white/5 bg-black/40">
                                <table className="w-full text-[10px] font-mono text-slate-300 text-right">
                                    <thead>
                                        <tr className="border-b border-white/10 text-slate-400 bg-white/[0.02]">
                                            <th className="p-2 text-left">{t("stockDeck.quarterCol", "季度")}</th>
                                            <th className="p-2">{t("stockDeck.revCol", "營收 (YoY)")}</th>
                                            <th className="p-2">{t("stockDeck.gmCol", "毛利率")}</th>
                                            <th className="p-2">{t("stockDeck.fcfCol", "FCF 現金流")}</th>
                                            <th className="p-2">{t("stockDeck.epsCol", "EPS (驚喜度)")}</th>
                                        </tr>
                                    </thead>
                                    <tbody className="divide-y divide-white/5">
                                        {quarters.slice(0, 8).map((q: any, idx: number) => (
                                            <tr key={idx} className="hover:bg-white/[0.02]">
                                                <td className="p-2 text-left font-bold text-slate-200">{q.fiscal_date_ending}</td>
                                                <td className="p-2">{q.revenue?.formatted || "-"} ({q.revenue_growth_yoy ? `${(q.revenue_growth_yoy * 100).toFixed(1)}%` : "-"})</td>
                                                <td className="p-2">{q.gross_margin ? `${(q.gross_margin * 100).toFixed(1)}%` : "-"}</td>
                                                <td className="p-2 text-emerald-400">{q.free_cash_flow?.formatted || "-"}</td>
                                                <td className="p-2">{q.eps?.actual ?? "-"} ({q.eps?.surprise_pct ? `+${(q.eps.surprise_pct * 100).toFixed(1)}%` : "-"})</td>
                                            </tr>
                                        ))}
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    )}

                    <div className="text-[10px] text-slate-500 border-t border-white/5 pt-2 leading-relaxed">
                        ⚖️ <strong>{t("stockDeck.disclaimerTitle", "免責聲明：")}</strong>{t("stockDeck.disclaimer", "本情報由確定性量化算法與 AI 質性分析生成，僅供投研參考，不構成個人化投資建議。市場有風險，入市需謹慎。")}
                    </div>
                </div>
            )}
        </div>
    );
};
```

---

#### 10. [MODIFY] [`lumina_web/src/components/chat/MessageBubble.tsx`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/components/chat/MessageBubble.tsx)

```tsx
// [MODIFY] lumina_web/src/components/chat/MessageBubble.tsx
// 引入 StockMarketCard 與 StockPairComparisonDeck 組件:

import { StockMarketCard } from "./stock/StockMarketCard";
import { StockPairComparisonDeck } from "./stock/StockPairComparisonDeck";

// 在 MessageBubble 渲染段 (替換行 1851~2042):
{stockMeta && stockMeta.quotes && stockMeta.quotes.length > 0 && (
    <div
        className={cn("message-stock-deck mt-3 space-y-2", fontSizeClass)}
        data-message-toolbar-exclude="true"
    >
        {/* 多標的對比膠囊 (>=2 標的自動激活) */}
        {stockMeta.pair_comparison_deck && (
            <StockPairComparisonDeck pairData={stockMeta.pair_comparison_deck} />
        )}
        
        {/* 逐張渲染金融決策卡 */}
        {stockMeta.quotes.map((quote: any, idx: number) => {
            const fundamentals = stockMeta.fundamentals?.filter((f: any) => f.symbol === quote.symbol) || [];
            const cardMeta = {
                ...quote,
                fundamentals,
                latency_ms: stockMeta.latency_ms,
            };
            return (
                <StockMarketCard
                    key={`${quote.symbol}-${idx}`}
                    meta={cardMeta}
                />
            );
        })}
    </div>
)}
```

---

### 第三部分：四語系 i18n 字典實體定義

#### 11. [MODIFY] [`lumina_web/src/locales/zh-TW.json`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/locales/zh-TW.json)
```json
{
  "stockDeck": {
    "compositeScore": "全景評分",
    "recommendedPosition": "理論風控上限",
    "strategicGrid": "四宮格攻防邊界 (Strategic Grid)",
    "currentPrice": "當前現價",
    "targetResistance": "目標阻力 (POC)",
    "hardStopLoss": "硬防守點 (ATR緩衝)",
    "rrRatio": "盈虧比 (R:R)",
    "viewDeepDive": "查看深度多空博弈報告 (Top-3 風險 / 催化 / 8 季財報)",
    "hideDeepDive": "收起深度多空博弈報告",
    "factorTension": "多因子張力深度透視",
    "top3Risks": "🚨 核心下行風險 (Top-3)",
    "top2Catalysts": "✨ 核心利好催化 (Top-2)",
    "executionGuidance": "實戰執行建議",
    "secMatrix": "SEC 10-Q 8季核心財務矩陣",
    "disclaimerTitle": "免責聲明：",
    "disclaimer": "本情報由確定性量化算法與 AI 質性分析生成，僅供投研參考，不構成個人化投資建議。市場有風險，入市需謹慎。",
    "chartLoading": "走勢資料載入中...",
    "high": "高",
    "low": "低",
    "fomoWarningTitle": "統計極端過熱警報：",
    "fomoWarningText": "當前 10 日漲幅處於歷史前 2% 極值區，統計上處於高波動超買區，建議等待回踩支撐，避免市價追高。",
    "maxLossPrefix": "觸發極限止損時，在",
    "maxLossMid": "倉位下，總帳戶本金最大損失鎖定在",
    "defaultStyle": "動能成長型",
    "benchmarkLabel": "母體基準",
    "benchmarkResonance": "大盤共振 🟢",
    "benchmarkPressure": "大盤承壓 🔴",
    "benchmarkDrag": "大盤空頭承壓調權 🔴",
    "quarterCol": "季度",
    "revCol": "營收 (YoY)",
    "gmCol": "毛利率",
    "fcfCol": "FCF 現金流",
    "epsCol": "EPS (驚喜度)",
    "capitalCalcToggle": "💰 本金下單精算器 (雙幣即時試算)",
    "liveFxRateLabel": "即時匯率折算",
    "recommendedShares": "建議建倉規模:",
    "stopLossDollar": "極限止損虧損:",
    "targetRewardDollar": "到達目標預期收益:",
    "zeroShareWarning": "單股價格超出 1% 風險預算，建議透過右上角切換為「碎股模式」或適度增加配置資金。",
    "fractionalOn": "🔘 碎股模式",
    "fractionalOff": "整數股模式",
    "timeHorizonPrefix": "波段推進週期",
    "staleStopPrefix": "超期止損",
    "pairComparisonTitle": "雙標的相對強弱矩陣 (Pair RS)",
    "twentyDayRsLabel": "近20日強弱比",
    "rsLeadingStatement": "在近 20 個交易日中動能相對領先",
    "rsRecommendation": "若進行同板塊套利或輪動建倉，優先考慮動能領先標的。",
    "rsBalanced": "動能勢均力敵",
    "rsBalancedStatement": "與",
    "rsBalancedDesc": "在近 20 個交易日中動能高度同步，強弱勢差異不明顯。",
    "styles": {
      "MOMENTUM_BREAKOUT": "動量突破型",
      "OVERSOLD_REVERSAL": "超跌反轉型",
      "VALUE_DEFENSIVE": "價值防守型",
      "RANGE_BOUND": "中繼震盪型",
      "LEVERAGED_DERIVATIVE": "高頻衍生趨勢型"
    }
  }
}
```

#### 12. [MODIFY] [`lumina_web/src/locales/zh-CN.json`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/locales/zh-CN.json)
```json
{
  "stockDeck": {
    "compositeScore": "全景评分",
    "recommendedPosition": "理论风控上限",
    "strategicGrid": "四宫格攻防边界 (Strategic Grid)",
    "currentPrice": "当前现价",
    "targetResistance": "目标阻力 (POC)",
    "hardStopLoss": "硬防守点 (ATR缓冲)",
    "rrRatio": "盈亏比 (R:R)",
    "viewDeepDive": "查看深度多空博弈报告 (Top-3 风险 / 催化 / 8 季财报)",
    "hideDeepDive": "收起深度多空博弈报告",
    "factorTension": "多因子张力深度透视",
    "top3Risks": "🚨 核心下行风险 (Top-3)",
    "top2Catalysts": "✨ 核心利好催化 (Top-2)",
    "executionGuidance": "实战执行建议",
    "secMatrix": "SEC 10-Q 8季核心财务矩阵",
    "disclaimerTitle": "免责声明：",
    "disclaimer": "本情报由确定性量化算法与 AI 质性分析生成，仅供投研参考，不构成个人化投资建议。市场有风险，入市需谨慎。",
    "chartLoading": "走势数据加载中...",
    "high": "高",
    "low": "低",
    "fomoWarningTitle": "统计极端过热警报：",
    "fomoWarningText": "当前 10 日涨幅处于历史前 2% 极值区，统计上处于高波动超买区，建议等待回踩支撑，避免市价追高。",
    "maxLossPrefix": "触发极限止损时，在",
    "maxLossMid": "仓位下，总账户本金最大损失锁定在",
    "defaultStyle": "动能成长型",
    "benchmarkLabel": "母体基准",
    "benchmarkResonance": "大盘共振 🟢",
    "benchmarkPressure": "大盘承压 🔴",
    "benchmarkDrag": "大盘空头承压调权 🔴",
    "quarterCol": "季度",
    "revCol": "营收 (YoY)",
    "gmCol": "毛利率",
    "fcfCol": "FCF 现金流",
    "epsCol": "EPS (惊喜度)",
    "capitalCalcToggle": "💰 本金下单精算器 (双币即时试算)",
    "liveFxRateLabel": "即时汇率折算",
    "recommendedShares": "建议建仓规模:",
    "stopLossDollar": "极限止损亏损:",
    "targetRewardDollar": "到达目标预期收益:",
    "zeroShareWarning": "单股价格超出 1% 风险预算，建议通过右上角切换为「碎股模式」或适度增加配置资金。",
    "fractionalOn": "🔘 碎股模式",
    "fractionalOff": "整数股模式",
    "timeHorizonPrefix": "波段推进周期",
    "staleStopPrefix": "超期止损",
    "pairComparisonTitle": "双标的相对强弱矩阵 (Pair RS)",
    "twentyDayRsLabel": "近20日强弱比",
    "rsLeadingStatement": "在近 20 个交易日中动能相对领先",
    "rsRecommendation": "若进行同板块套利或轮动建仓，优先考虑动能领先标的。",
    "rsBalanced": "动能势均力敌",
    "rsBalancedStatement": "与",
    "rsBalancedDesc": "在近 20 个交易日中动能高度同步，强弱势差异不明显。",
    "styles": {
      "MOMENTUM_BREAKOUT": "动量突破型",
      "OVERSOLD_REVERSAL": "超跌反转型",
      "VALUE_DEFENSIVE": "价值防守型",
      "RANGE_BOUND": "中继震荡型",
      "LEVERAGED_DERIVATIVE": "高频衍生趋势型"
    }
  }
}
```

#### 13. [MODIFY] [`lumina_web/src/locales/en.json`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/locales/en.json)
```json
{
  "stockDeck": {
    "compositeScore": "Composite Score",
    "recommendedPosition": "Risk-Budgeted Cap",
    "strategicGrid": "Strategic Key Levels",
    "currentPrice": "Current Price",
    "targetResistance": "Target Resistance (POC)",
    "hardStopLoss": "Hard Stop-Loss (ATR Buffer)",
    "rrRatio": "Risk/Reward (R:R)",
    "viewDeepDive": "View Deep-Dive Alpha Report (Top-3 Risks / Catalysts / 8Q Financials)",
    "hideDeepDive": "Hide Deep-Dive Report",
    "factorTension": "Multi-Factor Tension Analysis",
    "top3Risks": "🚨 Top-3 Downside Risks",
    "top2Catalysts": "✨ Top-2 Key Catalysts",
    "executionGuidance": "Tactical Execution Guidance",
    "secMatrix": "SEC 10-Q 8-Quarter Financial Matrix",
    "disclaimerTitle": "Disclaimer: ",
    "disclaimer": "Generated by deterministic quantitative algorithms and qualitative AI analysis. For research reference only.",
    "chartLoading": "Loading chart data...",
    "high": "High",
    "low": "Low",
    "fomoWarningTitle": "Extreme Statistical Overbought Alert: ",
    "fomoWarningText": "Current 10D gain is in the historical top 2% percentile. High-volatility overbought regime. Await pullback support; avoid FOMO market orders.",
    "maxLossPrefix": "Upon hard stop-loss trigger at",
    "maxLossMid": "position, total portfolio max capital risk is capped at",
    "defaultStyle": "Momentum Growth",
    "benchmarkLabel": "Macro Benchmark",
    "benchmarkResonance": "Market Resonance 🟢",
    "benchmarkPressure": "Market Pressure 🔴",
    "benchmarkDrag": "Macro Bear Drag 🔴",
    "quarterCol": "Quarter",
    "revCol": "Revenue (YoY)",
    "gmCol": "Gross Margin",
    "fcfCol": "FCF Cashflow",
    "epsCol": "EPS (Surprise)",
    "capitalCalcToggle": "💰 Capital Precision Sizer (Dual-Currency Sizing)",
    "liveFxRateLabel": "Live FX Rate Sizing",
    "recommendedShares": "Rec. Position Sizing:",
    "stopLossDollar": "Hard Stop Loss Risk:",
    "targetRewardDollar": "Target Profit Reward:",
    "zeroShareWarning": "Single share price exceeds 1% risk budget. Consider switching to 'Fractional Shares' or adding capital.",
    "fractionalOn": "🔘 Fractional Mode",
    "fractionalOff": "Whole Shares",
    "timeHorizonPrefix": "Momentum Horizon",
    "staleStopPrefix": "Stale Time-Stop",
    "pairComparisonTitle": "Multi-Ticker Relative Strength (Pair RS)",
    "twentyDayRsLabel": "20D Relative Strength",
    "rsLeadingStatement": "has demonstrated relative momentum outperformance against",
    "rsRecommendation": "For sector rotation or pairs trading, prioritize the leading momentum security.",
    "rsBalanced": "Momentum Equalized",
    "rsBalancedStatement": "and",
    "rsBalancedDesc": "have demonstrated highly synchronized momentum over the past 20 sessions.",
    "styles": {
      "MOMENTUM_BREAKOUT": "Momentum Breakout",
      "OVERSOLD_REVERSAL": "Oversold Reversal",
      "VALUE_DEFENSIVE": "Value Defensive",
      "RANGE_BOUND": "Range Bound",
      "LEVERAGED_DERIVATIVE": "Leveraged Trend"
    }
  }
}
```

#### 14. [MODIFY] [`lumina_web/src/locales/ja.json`](file:///c:/Users/kris/lumina_core_v2/lumina_web/src/locales/ja.json)
```json
{
  "stockDeck": {
    "compositeScore": "総合スコア",
    "recommendedPosition": "リスク配分上限",
    "strategicGrid": "戦略的攻防ライン",
    "currentPrice": "現在値",
    "targetResistance": "目標レジスタンス (POC)",
    "hardStopLoss": "損切り防衛ライン (ATRバッファ)",
    "rrRatio": "リスクリワード比 (R:R)",
    "viewDeepDive": "詳細投資レポートを表示 (上位3大リスク / カタリスト / 8四半期財務)",
    "hideDeepDive": "レポートを折りたたむ",
    "factorTension": "マルチファクター緊張分析",
    "top3Risks": "🚨 主な下方リスク (Top-3)",
    "top2Catalysts": "✨ 主要カタリスト (Top-2)",
    "executionGuidance": "戦術的執行アドバイス",
    "secMatrix": "SEC 10-Q 8四半期財務マトリックス",
    "disclaimerTitle": "免責事項：",
    "disclaimer": "確定的定量アルゴリズムと定性AI分析によって生成されています。投資判断の参考情報であり、投資勧誘を目的としたものではありません。",
    "chartLoading": "チャートデータを読み込み中...",
    "high": "高値",
    "low": "安値",
    "fomoWarningTitle": "統計的極端買われすぎ警告：",
    "fomoWarningText": "直近10日間の上昇率は過去2％の極値領域にあります。押し目買いを待ち、高値追いを避けてください。",
    "maxLossPrefix": "損切り発動時、",
    "maxLossMid": "ポジションにおける総資産の最大損失は",
    "defaultStyle": "モメンタム成長型",
    "benchmarkLabel": "マクロ基準",
    "benchmarkResonance": "市場共振 🟢",
    "benchmarkPressure": "市場逆風 🔴",
    "benchmarkDrag": "マクロ下落連動 🔴",
    "quarterCol": "四半期",
    "revCol": "売上高 (YoY)",
    "gmCol": "粗利益率",
    "fcfCol": "フリーCF",
    "epsCol": "EPS (サプライズ)",
    "capitalCalcToggle": "💰 資金精密計算機 (リアルタイム2通貨換算)",
    "liveFxRateLabel": "リアルタイム為替換算",
    "recommendedShares": "推奨建玉規模:",
    "stopLossDollar": "損切り時最大損失:",
    "targetRewardDollar": "目標達成時想定利益:",
    "zeroShareWarning": "1株あたりの価格が1%リスク予算を超過しています。右上の「端株モード」に切り替えるか増資を検討してください。",
    "fractionalOn": "🔘 端株モード",
    "fractionalOff": "単元株モード",
    "timeHorizonPrefix": "推進想定期間",
    "staleStopPrefix": "時間経過ロスカット",
    "pairComparisonTitle": "相対力比較マトリックス (Pair RS)",
    "twentyDayRsLabel": "直近20日RS比",
    "rsLeadingStatement": "は直近20営業日において相対的なモメンタム優位性を示しています",
    "rsRecommendation": "セクターローテーションやペアトレードにおいては、優位性の高い銘柄を優先検討してください。",
    "rsBalanced": "勢力伯仲",
    "rsBalancedStatement": "と",
    "rsBalancedDesc": "は直近20営業日において極めて連動したモメンタムを示しています。",
    "styles": {
      "MOMENTUM_BREAKOUT": "モメンタム突破型",
      "OVERSOLD_REVERSAL": "過度下落反発型",
      "VALUE_DEFENSIVE": "バリュー防衛型",
      "RANGE_BOUND": "レンジ相場型",
      "LEVERAGED_DERIVATIVE": "レバレッジ型"
    }
  }
}
```

---

## 驗證與測試清單 (Definition of Done)

1. **唯一實踐成功法定依據（Immutable Full Parity Self-Inspection Gate）**：
   - 執行 `python scratch/verify_fds_35_implementation.py`
   - **嚴格鐵律**：絕對禁止修改 `verify_fds_35_implementation.py` 腳本內容。
   - **唯一成功標準**：8 大維度、24 大真實生產函數物理執行路線、170+ 項全量邊界檢驗點全部 `✔ [PASS]`，0 失敗（Exit Code 0）。
2. **單元測試**：在 `tests/test_stock_feature_engine.py` 執行 100% 覆蓋測試。
3. **靜態編譯**：在 `lumina_web` 執行 `npm run build` 確認 0 錯誤。
4. **線上探測**：執行 `python scripts/deploy_oneclick.py` 部署生產伺服器，19 項探測全部通過。
