AI Trading PK
我是怎麼蓋起來的
這份文件是寫給未來的自己。哪天有同事問「這個怎麼做的?技術棧長怎樣?」 時,可以打開最後一節「講解話術」掃 60 秒,每句話都是現成可以講的。 全文都是我(用 Claude Code)真的蓋出來的東西,不是 AI 隨便講講。
里程碑(最新在上)
每出一個有意義的版本就在這留一筆。下面各章節若有變更,會就地更新。
正式上線 · 真實盤比賽 · live-edge 引擎修復
- 全套上雲:後端 Cloud Run(SQLite 放 GCS volume、鎖單一 instance = 單一寫入者)、 前端 Cloudflare Workers(OpenNext,順手把 Next 14 升到 15 + React 19)、 Cloud Scheduler 每個交易日 14:15 收盤後自動 tick(18:00 第二發當保險 — endpoint 冪等,重打無害)。 盤中個股頁照舊走 TWSE MIS 近即時報價。
- 最關鍵的引擎修復:歷史回放時「明天的 K 棒」永遠在快取裡,但真實前進時明天根本還沒發生 —
原本引擎遇到「下一個交易日沒資料」就跳過決策,而每一天又是冪等、不會重決 → 真實盤比賽會永遠 MTM、永遠不下單。
改成:用估計的
execute_date(Dk+1 日曆日)掛單,成交端改用execute_date <= 當天撈單並回寫實際成交日 — 週五掛的單自然在週一開盤成交,no-look-ahead 不變。回放模式測不到這隻 bug:「回放會騙人」的活教材。 - Live PK 真實盤:四個 persona(動能 / 技術線仙 / 少年股神 / 逆勢抄底)每個交易日收盤後 真的呼叫 Gemini 做決策、隔日開盤成交,在真實資料上往前跑。LLM 的 reasoning 與每筆單的 rationale 全面改為繁體中文輸出。
- 新功能四連發:0050 買進持有「幽靈跑道」直接插進賽道排名(一眼看出誰真的贏過大盤);
每日戰報卡(成交 / 領先 / 最大波動三句話,模板生成、零 LLM 成本);
策略線上編輯 + 版本歷史逐欄 diff;
管理操作加上
ADMIN_SECRET防護(網址可以公開分享了)。 - 後端測試 99 → 110 全綠。即時報價維持 display-only — 引擎仍然每日結算,餵盤中價給決策會破壞 no-look-ahead。
LIVE 強化 · 新聞整合 · 中文版本文件
- LIVE 欄位補強:原本只有 ~30% 的股票顯示即時價(因為 TWSE MIS 的
z最新成交價在沒新撮合時是空的)。改成優先用z,沒有時退回最佳買賣中價 (mid), 最後 fallback 到今日開盤價 (open)。現在交易時段幾乎 100% 覆蓋。畫面會用顏色 + 小標籤標示資料來源。 - 新聞整合:每檔個股新增「新聞」欄位,點開跳出 modal 顯示最近 10 則 Google News 中文新聞, 附發布時間 + 媒體名稱 + 原文連結。Server side 快取 4 小時,10 個人同時看 = 4 小時只打 1 次 RSS。
- 中文版建構筆記:現在你正在看的這份。和英文版同步維護。
三大法人 · 風險調整排名 · 強化 LLM prompt
- 引入 三大法人:TWSE T86 + TPEx 3insti 抓取每日 外資 / 投信 / 自營 淨買賣,
存進
InstitutionalFlow表,並寫進 LLM 的 snapshot 裡。 個股頁加上四個欄位。 - 實證有效:2454 聯發科 5/29 顯示 外資 -2,325 張 / 投信 -1,021 張 / 自營 -130 張 = 三大法人 -3,476 張, 完美對應到當天 -2.27% 的跌幅。訊號是真的有用。
- 新增 Sharpe / 年化波動 / 最大回撤:每個 agent 用 EquitySnapshot 算指標。 排行榜可切換排序:報酬率 / Sharpe / 最大回撤。Hero 加上三個 metric pills。
- 實證:航海王 報酬率 +8.75% 但最大回撤 7.5%;價值老司機 報酬率 +4.58% 但最大回撤只有 2.25%。 按 Sharpe 排,價值老司機贏。這才是真正定義「會交易」的方式。
- 強化
app/llm/prompt.py:明確的 5 步思考流程(state → research → decide → size → emit), 欄位 schema 寫清楚(包含 三大法人),提醒 LLM 訂單會在下一個交易日 open 成交。 - 10 個新測試(5 個 institutional parser、5 個 metrics math)。總計 57 個後端測試全綠。
資料新鮮度 — 收盤後自動更新 + 顯眼的「資料截至」
- Jazz 抓包:聯發科顯示 4410(昨日收盤)而不是 4310(今日收盤)。 因為 seed 只到 5/28,13:30 收盤後沒人主動 pull。
- 新增
app/services/refresh.py:expected_latest_finalized()知道 「週一到週五 14:00 TPE 之後 → 今日;其他時候 → 上個交易日」。refresh_universe()只 pull 缺的日期區間。 - 新 endpoint
POST /competitions/{id}/refresh-data。Stocks endpoint 改回傳{as_of, expected_latest, is_stale, rows}讓前端判斷要不要更新。 - APScheduler 加排程:週一到週五 14:00 TPE 自動更新所有比賽的 universe。引擎決策還是維持 18:00。
- 個股頁顯眼的「資料截至 YYYY-MM-DD」badge。資料過舊時自動背景更新。手動「立即更新」按鈕隨時可用。
近即時 TWSE 報價 — TWSE MIS · 約 5 秒延遲
- 接入 TWSE MIS(
mis.twse.com.tw),這是台灣券商「即時報價」用的同一個 source。 交易時段(週一到週五 9:00-13:30 TPE)約 5 秒一次更新。 - 新 endpoint
GET /competitions/{id}/stocks/realtime。Server 端 5 秒快取, 10 個人開頁面也只打一次 TWSE。 - 個股頁加上 LIVE 欄位 + 日內 %,每 30 秒自動更新,非交易時段顯示「MARKET CLOSED」。
- 架構紅線:LLM 決策永遠只用日線資料。即時資料是 display-layer overlay 而已 — no-look-ahead invariant 不會破。
個股看盤頁 · Hero 字級修復 · Agent 建立流程說明
- 新增
/stocks個股 universe 瀏覽頁 — 可排序的 194 檔 TWSE 一覽表,欄位和 LLM 看到的 snapshot 一致。點任一檔看 90 日歷史走勢。 - 修掉 Hero 文字重疊:標題字級
clamp(2.5rem,5vw,5rem)改成clamp(1.75rem,3.6vw,3.25rem), stats 在窄螢幕改成垂直堆疊。 - 新增「agent 不是 .md 檔,是從 UI 建立的 DB row」說明(第 8b 節)。
賽馬場視覺改版 · BYOK Claude · 194 檔 universe
- 拿掉 server 端共用的
ANTHROPIC_API_KEY。改成 BYOK Claude,每位 owner 自己連結,金鑰 Fernet 加密儲存。 - UI 全面改版:Hero frontrunner card、橫向 race track、Framer Motion 動畫數字、排名變動有過渡。
- 把壞掉的
yfinancePython lib 換成直接打 Yahoo Finance Chart API。Seed universe 從 137 檔擴充到 194 檔。 - 引擎 bug fix:
advance_one_trading_day第一天起算改成start_date − 1,不會再從歷史回溯資料的最早日子開始 tick。
初版 · 引擎 + LLM clients + 第一版 UI
- SQLite 11 張表。引擎 tick 有 idempotency + no-look-ahead invariant。
- FastAPI routers + Pydantic v2 strict。每日 tick:本機開發用 APScheduler;正式環境改由 Cloud Scheduler 打冪等的 cron endpoint(14:15 + 18:00 雙保險)。
- 6 個 personas + 4 題小測驗,策略 version-lock + 每週修改上限。
- Next.js + Tailwind 第一版排行榜、agent detail、新增 agent wizard、admin 後台。
- 38 個測試,包含 no-lookahead probe。
30 秒講完版本
有人只給你 30 秒,你就講這段。
為什麼有這個東西
需求
內部團隊小工具,第一階段 10 人、設計上撐到 50 人。 是會議室裡放著看的小玩具,讓沒寫過程式的同事用自然語言設定 agent, 然後每週看排名變化。
明確不做的
真錢、即時資料、盤中串流、統計顯著性那種研究級工具。 也沒有真的 SSO(用 owner_name 當身分就夠了)。 日線解析度已足夠。
五個你該知道的觀念
把這五個搞懂,幾乎所有同事問的問題你都答得出來。
Paper trading(紙上模擬交易)
用真實價格、虛擬資金。系統紀錄你「如果真的這樣交易,會賺多少」。 不涉及券商,不下真的單。
日線 OHLCV
四個價格摘要一檔股票的一個交易日:Open(開)、 High(高)、Low(低)、 Close(收)加上 Volume(量)。 這整個系統的最小單位。
Look-ahead bias(前視偏誤)
不小心用了「未來的資料」做「過去的決策」。 回測世界最大的原罪:策略看起來超強,一上線就崩。 我這個 codebase 有個測試專門守這條 invariant, 只要程式碼偷看未來,測試就會掛。
Engine as source of truth
來自真實交易系統的 pattern:策略/模型「提議」訂單; 引擎驗證、套入成本、修改投組。兩層架構各司其職, 模型怎麼亂講都動不了規則底線。
BYOK(Bring Your Own Key)
不用 server 統一付一把 LLM API key,每個人連結自己的。 用 Fernet(對稱式 AES + HMAC)加密儲存。 用多少自己付,不用爭 quota。
未調整 vs 調整後價格
「調整後收盤」會把過去的股利和分割隱性地融入價格 — 適合畫線圖、但用來算 P&L 會錯。 這個系統用未調整價格,公司行動(除權息、分割)會明確套用: 除息 → 加現金;分割 → 股數 × 比例、均價 ÷ 比例。
一張圖看完架構
五層,依賴方向永遠由上往下。沒什麼花俏的。
技術棧逐層說明
每樣東西為什麼選它,一兩句講完。
| 層 | 選擇 | 為什麼是它,不是別的 |
|---|---|---|
| 後端框架 | FastAPI 0.115 | 原生 async、Pydantic v2、自動 OpenAPI。比 Django 蓋純 API 更快。 |
| Schema 驗證 | Pydantic v2 strict | Strict mode 不會自動把 "1" 轉成 1。bugs 在邊界就被擋住,不會跑到引擎裡才出包。 |
| ORM | SQLAlchemy 2.0 | Typed Mapped[...] 跟 mypy / Pylance 合得來。查詢時 ORM 不會擋路。 |
| 資料庫 | SQLite(檔案) | 零維運、原子寫入。≤50 人完全不會撞牆。第二階段換 Postgres 只是改一個 env var。 |
| 排程 | APScheduler 3.10 | Process 內 cron。不需要 Redis 或 Celery 那一套。目前就一個每日 tick + 一個午後 refresh。 |
| 市場資料 | Yahoo Chart API(直接 HTTP) | yfinance Python lib 對 TWSE tickers 莫名其妙回傳空的。直接打 endpoint 反而穩。 |
| 即時報價 | TWSE MIS | 台灣券商「即時報價」用的同一個。約 5 秒延遲、免費、不用註冊。 |
| 三大法人 | TWSE T86 + TPEx 3insti | 官方每日法人買賣超公開資料。免費、每天收盤後 30 分內更新。 |
| 新聞 | Google News RSS(zh-TW) | 免費、不用 API key、可以針對 ticker + 名稱查詢、中文支援好。 |
| LLM SDKs | anthropic, google-genai | 兩家都有現役 Python SDK。包成一個 DecisionProvider Protocol,引擎不在乎是哪家。 |
| 加密 | cryptography.Fernet | 認證式對稱加密,「at-rest secret」的標準解。Master key 從 SERVER_SECRET 經 SHA-256 衍生。 |
| 前端框架 | Next.js 14 App Router | File-based routing、RSC 處理靜態殼、client components 只用在需要的地方。TS strict、不用 any。 |
| 樣式 | Tailwind 3 | Design tokens 寫在 tailwind.config.ts,utility classes 寫在組件裡。比 CSS-in-JS 快上鏡。 |
| 動畫 | Framer Motion 11 | 賽馬道排名變動的 layout animation、AnimatedNumber 平滑計數。 |
| 測試 | pytest + TestClient | 62 個後端測試。最重要的是 test_engine_no_lookahead — 守住核心 invariant。 |
引擎 tick
backend/app/engine/tick.py 裡面只有一個函數 advance_one_trading_day,
每天跑同樣的 6 個步驟,順序不變。Idempotent。沒有特例。
六步驟在做什麼
- 定 Dk — universe 裡下一個有資料的交易日
- 填昨日掛單 — 在 Dk 的 OPEN 成交,重新驗證每個 guardrail
- 套用公司行動 — 除息 → 加現金;分割 → 股數 × 因子
- 收盤 MTM — Dk 收盤價 × 持股 = 市值;寫 EquitySnapshot
- 跑決策 — 該決策的 agent 呼叫 LLM,產生明日掛單
- 更新指標 —
current_trading_date = Dk
為什麼是這個順序
每一步只負責一件事。「填單」要在「決策」之前,因為新決策需要看到填完之後的投組狀態。 公司行動排在填單跟 MTM 之間,因為分割會改變股數,要先處理。 決策放最後,它讀所有其他東西的結果 — 然後產生的單是給「明天」用,不是今天。
不能偷看未來,否則一切都是假的
業餘回測最常見的單一錯誤:不小心讓「過去的決策」用到了「未來的資料」。 這個系統有個測試,只要踩到這條線就會炸給你看。
規則
Dk 那天做的決策,只能讀 ≤ Dk 收盤的資料。 它產生的訂單會在 Dk+1 的 OPEN 成交 — 絕對不會用 Dk 的收盤填單。
理由:真實市場裡,你在 13:30 收盤前不會知道收盤價。 你只能在明天早上開盤時動手。
怎麼守
build_decision_context 裡每個查詢都加
date <= decision_date。
填單函數明確讀 Dk 的 PriceBar.open,不是 .close。
測試套件裡有一個 probe provider,記錄它「看到」的每個日期。 只要有任何日期 > Dk,測試就 fail。
# backend/tests/test_engine_no_lookahead.py — 守在這裡
def test_decision_never_reads_future_data(db):
# 第 1-3 天股價 600..602;第 4 天跳到 999(這是「未來」)
seed_path_bars(db, ticker="2330.TW", start=date(2026, 5, 4),
opens=[600, 601, 602, 999, 1000, 1001])
snoop = SnoopProvider()
advance_one_trading_day(db, comp, snoop, _snoop_snapshot)
advance_one_trading_day(db, comp, snoop, _snoop_snapshot)
advance_one_trading_day(db, comp, snoop, _snoop_snapshot)
# 每次決策看到的日期,都不能 > 自己的 decision_date
for dk_observed, seen in snoop.observations:
assert all(d <= dk_observed for d in seen), \
f"Decision on {dk_observed} saw future dates {seen}"
每次重構引擎都會跑這個測試。哪天有人「優化」程式碼結果偷讀了明天的 open, 測試就斷給你看。bug 進不了生產環境。
策略 schema:兩半
同一個 shape,一半是程式硬性守的、一半是自然語言。Personas 就是這個 shape 的預設值。
structured(引擎強制執行)
{
"max_position_pct": 0.30,
"max_holdings": 4,
"max_trades_per_decision": 3,
"cash_floor_pct": 0.10,
"stop_loss_pct": 0.08,
"take_profit_pct": 0.20
}
這些是硬上限。LLM 提議的每個訂單,引擎都會驗證一次。 超過任何一條都會被 rejected 並記錄理由 — 看 agent 的訂單紀錄就看得到。
free_text(LLM 讀)
"鎖定 5 日與 20 日動能最強、且
above_ma20 的股票。持倉跌破 -8%
出場,獲利 +20% 起逐步停利。
動能消退立刻換股。"
這段是 LLM 每次決策時讀的,是 agent 的「個性」。 引擎不解析這段,只把它和市場 snapshot 包進 prompt 裡。
為什麼要兩半:純 structured 太死板,沒個性;純 free_text 容易亂下單浪費手續費。 切兩半同時拿到「自然語言設定」的好處跟「程式守底線」的安全。
「agent 怎麼建立?」
同事常常以為 agent 是 markdown 設定檔。不是。 這節把心理模型講清楚,下次有人問你就能秒答。
這個系統的 agent 是什麼
agents 表裡的一筆 row。欄位:
id, competition_id, owner_name, name, model_provider,
persona_id, decision_cadence_days + 連結的 StrategyVersion
(存 structured + free_text 策略)。
從 UI(/new-agent)或 API(POST /competitions/{id}/agents)建立。
全程不用碰任何檔案。
它「不」是什麼
不是某個資料夾裡的 markdown 檔,也不是要 commit 的 YAML 設定。
混淆來自 Claude Code 的 subagent — 那個確實是 .claude/agents/ 底下的 md 檔。
完全不一樣的東西。
也跟 LangChain 或 AutoGen 的 agent 定義不一樣。 這個系統的 agent 比較像「交易者 profile」+「LLM client 連結」。
回答:「是的,全程在 UI — quiz、選 persona、調策略、連 Claude、推上場。不用碰任何檔案。」 — 現成可講
個股 / Stocks tab
排行榜告訴你「誰在贏」,個股頁告訴你「該叫你的 agent 看哪些股票」。 讓使用者在調策略之前可以先逛 universe。
看得到什麼
整個比賽 universe(~194 檔 TWSE)的可排序表格。 欄位:代號、名稱、收盤、LIVE、日內 %、1d/5d/20d 報酬、MA20 cross、量比、 外資/投信/自營/三大法人 淨買賣(張)、新聞、30 日 sparkline。
任何欄位可排序、可搜尋(ticker 或公司名)。 點 row → 90 日歷史 K 線 modal。點「新聞」按鈕 → 最新中文新聞 modal。
關鍵設計
這個頁面顯示的就是 LLM 決策時看到的同一份 snapshot。
用同一個 build_snapshot 函數、同一份數字。
使用者瀏覽看到的就是 agent 會看到的,
不會出現「看的人跟做的人各說各話」這種事。
對 PK 來說為什麼重要:沒寫過程式的同事寫 free_text 策略時,
很容易寫「買漲的股票」這種模糊指令。給他看到 LLM 看到的實際 snapshot,
他就會學會寫「pct_5d > 10% 且 volume_ratio > 1.5 才買」這種具體可執行的指令。
三大法人
每個台股 trader 都會先看三大法人,才下單。如果我們的 agent 看不到,等於對台股市場最大的推力視而不見。
外資
外資及陸資 — 台股最大的單一推力。 含外資自營商。 外資對 2330 一張買 5000 張的衝擊力,比任何散戶都大。
投信
共同基金與投信公司。流量比較小,但持續 — 投信一旦開始買某檔,常常一連買好幾天。 抓「投信開始進場的第一天」是經典台股訊號。
自營商
券商自營部。流量更小,分自行買賣(directional)跟避險(hedge)。 方向性訊號比較弱,但能補齊整體面貌。
LLM 怎麼看到:snapshot 的每個 row 現在都帶著
foreign_net_lots、trust_net_lots、dealer_net_lots、
total_inst_net_lots,跟價量欄位並排。單位是「張」(1 張 = 1000 股)。
正數 = 淨買、負數 = 淨賣。系統 prompt 也明確跟 LLM 講該怎麼看 —
「三大法人買超 + 股價上漲 = 強訊號;賣超 + 股價反彈 = 警訊;
投信持續買進 = 多日行情的開始」。
# 實證:2454 聯發科 2026-05-29
ticker: 2454.TW
last_close: 4310.0 # 前一天 4410
pct_1d: -2.27% # 跌
foreign_net_lots: -2325 # 外資倒貨 2,325 張
trust_net_lots: -1021 # 投信賣 1,021 張
dealer_net_lots: -130 # 自營小賣
total_inst_net_lots: -3476 # 三大法人合計賣 3,476 張
資料來源:https://www.twse.com.tw/rwd/zh/fund/T86(上市)跟
https://www.tpex.org.tw/web/stock/3insti/daily_trade/3itrade_hedge_result.php(上櫃)。
兩個都免費、都能用,每天收盤後 30 分內更新。一次 HTTP 拿回整個市場 —
所以更新 194 檔 universe 的三大法人,一天只要 2 個 request。
風險調整後的排行榜
三個指標把排行榜從「誰運氣好」變成「誰真的會做交易」。可以切換排序:報酬率 / Sharpe / 最大回撤。
Sharpe Ratio
(每日平均報酬 / 每日報酬標準差) × √252。
越高 = 用更少的波動賺到同樣多的錢。
Sharpe 2 意思是「我承擔的波動,有 2 倍報酬回來」。
真實基金界 >1 算好、>2 算很好、>3 可能是運氣或樣本太短。
沒做無風險利率調整 — 這只是內部 PK,不是研究級工具。
最大回撤 Max Drawdown
淨值曲線上「最高點到之後最低點」的最大跌幅。 越小 = 風險控制越好。 賺 20% 又跌到 5%,最大回撤約 12.5%。
排序時取負值,這樣所有排序模式都是「分數越高排越前面」。
為什麼重要:看看我們自己 demo 切換排序時發生什麼事:
| Agent | 報酬率 | 最大回撤 | Sharpe | 用什麼贏 |
|---|---|---|---|---|
| 航海王 · Momentum | +8.75% | -7.50% | 3.88 | 報酬率 |
| 價值老司機 · Contrarian | +4.58% | -2.25% | 4.14 | Sharpe 最大回撤 |
Momentum 的報酬將近兩倍,但 Contrarian 用三分之一的回撤拿到一半的報酬 — 所以以風險調整後的角度,Contrarian 才是贏家。 這種比較讓排行榜從「吃角子老虎機」變成「面對懂金融的同事也站得住腳」的工具。
實作在 backend/app/services/metrics.py。用 252 個交易日年化。
資料不足(< 2 個 snapshot 或波動為零)時回傳 None,前端顯示「—」而不是亂湊。
「即時」到底有多即時
兩條資料流,分得清清楚楚。這個邊界僅次於 engine-as-source-of-truth,是整個架構的第二根脊椎。
日線(決策用)
從 Yahoo Finance Chart API 拉的 OHLCV,bar 確定之後永久快取。 這是 LLM 看到的。 Dk 那天的決策只能讀 ≤ Dk 收盤的資料 — no-look-ahead 測試守住,盤中資料無法滲入。
檔案:backend/app/data/prices.py
即時(只給人看)
TWSE MIS — 台灣券商「即時報價」用的同一個 source。 週一到週五 9:00-13:30 TPE 約 5 秒一次更新。 餵給個股頁的 LIVE 欄位。引擎絕對不會讀這個。
檔案:backend/app/data/realtime.py
| 來源 | 延遲 | 免費? | 用在哪 |
|---|---|---|---|
| TWSE MIS | 交易時段約 5 秒 | 是 | 個股頁 LIVE 欄位。優先 z (match) → midpoint (b/a 中價) → o (今日開盤)。 |
| Yahoo Finance Chart API | 收盤後日線 | 是 | 引擎跟決策 snapshot 用的日線快取。 |
| TWSE T86 + TPEx 3insti | 收盤後 30 分 | 是 | 三大法人每日淨買賣。 |
| Google News RSS | 幾分鐘到幾小時 | 是 | 每檔個股最新中文新聞。Server 快取 4 小時。 |
| GOOGLEFINANCE | 15-20 分 | 是 | 沒用 — TWSE MIS 更快。 |
| 券商 API(元大 / 富邦) | tick-by-tick | 否(要開戶) | 小工具不用。要做真實 fill 才需要。 |
一句話講邊界:即時資料只是 UI 上的塗料。 任何會改投組的東西(訂單、填單、MTM snapshot)都只讀日線資料 — 引擎保持決定性、no-look-ahead 測試依然能跑。
BYOK Claude:使用者金鑰怎麼運作
沒有共用的 Anthropic key。每個人連自己的。
為什麼 BYOK
市面上沒有第三方 app 能透過 Claude.ai 訂閱呼叫 Claude API 的公開 OAuth — Claude Code 是特例。 現實選項只有兩個:共用 API key(一個用量大戶吃光大家的 rate limit) 或 BYOK(每個人自己付)。BYOK 贏。
怎麼儲存
使用者在「連結 Claude 帳號」面板貼一次金鑰。
Server 用 SERVER_SECRET SHA-256 衍生出 Fernet key,
加密之後才寫進 owner_keys.encrypted_key(二進位)。
只有 last4 跟 label 會回到前端顯示。
# backend/app/services/crypto.py — 加密邊界
def _derive_key(secret: str) -> bytes:
digest = hashlib.sha256(secret.encode("utf-8")).digest()
return base64.urlsafe_b64encode(digest)
def encrypt(plaintext: str) -> bytes:
return _fernet().encrypt(plaintext.encode("utf-8"))
def decrypt(token: bytes) -> str:
return _fernet().decrypt(token).decode("utf-8")
使用者解除連結時 row 就刪。如果存的金鑰失效,LLM 呼叫會優雅失敗 — 決策記錄為「claude not connected」並回傳 0 個訂單。引擎絕不會在 tick 中崩潰。
賽馬場視覺改版的六個動作
第一版乾淨的 dark theme 表格 — 可讀但無聊。改版有意識地借用了「賽馬」的能量,靠這六個動作就完成大半。
1 · Hero frontrunner
頁面上 40% 完全給冠軍。皇冠 SVG、金色光暈帶 pulse 動畫、 巨大的 Space Grotesk 名字(96px+)、亞軍 "chasing" 副卡。 還沒往下滑就看見焦點。
2 · 橫向賽馬道
每個 agent 一條 lane。token 的橫向位置 = 該 agent 報酬率 vs 領先者。 lane 背景是 sparkline。最右邊是格仔旗終點線。
3 · 動畫計數
Framer Motion 的 animate() 套在 useMotionValue 上。
每次 poll 數字會平滑跳到新值,不會 jump cut。
在 AnimatedNumber.tsx 寫一次到處用。
4 · 排名變動的 layout 動畫
每個 lane 包在 <motion.div layout>。
排行榜重新排序時 lane 會平滑換位,不會啪一下跳。
整個「賽馬感」靠的就是這個細節。
5 · 顏色階層
前三名金 / 銀 / 銅(配色 + glow),漲跌綠 / 紅,中間排淡化。
眼睛自動鎖定到前段。色碼從 tailwind.config.ts 拿,不直接寫 inline。
6 · 字型
Display:Space Grotesk(粗、緊)。
數字:JetBrains Mono + tabular-nums — 計數時數字寬度不會跳動。
內文:Inter。中文用 Noto Sans TC。
都從 Google Fonts 在 app/globals.css 載。
講解話術
可以直接講出來的句子。每句都在 30 個字內。要 paraphrase,不要逐字念。 這是你蓋的東西 — 講起來要像你蓋的。
怎麼用這份文件
每次對話之前
掃一下第 14 節「講解話術」。60 秒。挑兩三句最切題的。
有人問「這怎麼運作?」
打開第 4 節(架構圖)+ 第 6 節(引擎 tick)。從左到右講過去。
有人問「準不準?」
第 7 節 — no-look-ahead 不變式。測試在那裡,把那段程式碼秀給他看。
有人問「下一步呢?」
第 13 節 — 下一步要做什麼。別吹太大,就講兩個真的可以做的方向。