diff --git a/AGENTS.md b/AGENTS.md index 5866102..4440c8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,39 +1,37 @@ # AGENTS.md — fomo-kernel -> 給 AI coding agent(**Codex**、Cursor、Claude Code 等)的操作指引。 -> 人類使用說明見 [README.md](README.md);**完整流程權威見 [skills/fomo-kernel/SKILL.md](skills/fomo-kernel/SKILL.md)**。 +> Thin routing guidance for Codex, Cursor, Claude Code, and other coding agents. Human-facing product documentation lives in [README.md](README.md). The only cross-agent workflow entry point is [skills/fomo-kernel/SKILL.md](skills/fomo-kernel/SKILL.md). -## 這個 repo 是什麼 +## When to trigger -一個交易復盤工具:把使用者的交易 CSV 復盤成**一張卡** —— 一個最大的行為漏洞 + 一條下次守則 + 一句大師原則。三層遞進:機械層(Python 確定性精算)→ 鏡片層(大師原則問動機)→ 收斂成一張卡。 +Trigger when a user asks for a trade review, transaction postmortem, brokerage-statement review, or provides a trade CSV or position snapshot. -## 何時觸發 +## Workflow -使用者說「復盤」「trade review」「檢討交易」「看我的對帳單」,或丟出一份交易 CSV 時。 +1. Read `skills/fomo-kernel/SKILL.md` completely. +2. Normalize brokerage data locally. Do not require the user to reformat it. +3. Start from the single orchestration entry point: -## 怎麼做(照這個順序) - -1. **先讀完整流程**:`skills/fomo-kernel/SKILL.md` 是四步流程的權威,照它走。 -2. **跑 engine(機械層)**: ```bash cd skills/fomo-kernel - python3 engine/trade_recap.py <使用者的 CSV 路徑> - # 不帶參數 = 引擎跑內建 mock(僅供你自測引擎);使用者沒給資料 → 請他提供, - # 或走 SKILL.md「試駕模式」:mock 走四步,但不落盤 + 標明演練 + 卡標示範,防護缺一不可 + python3 engine/review.py prepare --language en ``` - 依賴:Python 3.11+、`yfinance`、`pandas`、`rich`(見 `requirements.txt`)。沒網路時 engine 會自動退成行為層診斷,不會中斷。 - CSV 來自任何券商都行 —— 你負責讀懂、轉成引擎要的欄位(`Symbol / Action(BUY|SELL) / Quantity / Price / TradeDate`,非美股補選填欄 `Market / Currency`,如 `2330.TW / TW / TWD`;不填 = 美股 USD),不必要求使用者手動整理。 -3. **出卡前問動機(鏡片層)**:engine 標 `[ASK]` 的標的(金額大 + 虧損中加碼),逐一問使用者「逢低布局還是凹單?」,拿到答案才出定論卡。 -4. **出卡(收斂層)**:engine 輸出 + 使用者動機答案 → 收斂成一張卡(版型見 `skills/fomo-kernel/card-template.html`)。 -## 鐵律(不可違反;完整版見 SKILL.md) +4. Read the returned `review_plan.flow_path` and shared references. Ask only questions in `question_queue` with `required:true`. Use a native option UI when available; otherwise present the same options in plain text. +5. Produce schema-valid answers and a narrative with no digits, then call `preview`. Fix rejected artifacts instead of bypassing the validator. +6. Show the private preview and ask the user to choose one candidate rule, supply a custom rule, or skip. Write that choice to `answers.commitment` and call `finalize`. +7. Deliver `sessions//card-private.md`. Deliver `card-public.md` only when the user asks for a shareable version. + +After an interruption, use `review.py resume`; do not refetch live prices. If a projection fails, use `review.py repair-projections`. An existing canonical session is not data loss. + +## Non-negotiable boundaries -1. **數字全部來自 engine,一個都不准自己算或編。** 你的工作是解讀 + 追問動機,不是當計算機。engine 沒輸出的數字 → 不要寫。 -2. **不給投資建議。** 不 recommend 買賣標的;只復盤「行為」、問「動機」、給「下次守則」。 -3. **動機問句必出。** engine 標 `[ASK]` 的標的,出卡前必問,不可跳過。 -4. **一次只逼一件事。** 卡上「下次只改」永遠只有一條,不給清單。 -5. **隱私不外傳。** 使用者交易資料只在本機跑,不上傳、不外傳、不寫進任何雲端記憶。 +1. Numbers, rankings, cycle IDs, metrics, and ETF exemptions come from code. The agent must not calculate, invent, or alter them. +2. Do not provide buy or sell recommendations. Review behavior, motives, thesis evolution, and the next process rule. +3. Required motive questions cannot be skipped. A `new_evidence` decision requires both a claim and a source. +4. Each card has at most one final rule, chosen by the user. Skipping is valid. +5. Keep trade data local and out of cloud memory. Never mix private-card content into a public card. -## 為什麼有這個檔 +## Why this bridge stays thin -`SKILL.md` 是 Claude Code 的 skill 格式,**會被 Claude Code 自動偵測載入**。其他 agent(如 Codex)沒有「自動載入 skill」機制,但**能照常把 SKILL.md 當指令讀 + 跑 engine** —— 本檔就是給這些 agent 的指路牌。核心引擎是純 Python,**與工具無關**:Codex / Claude Code / 甚至使用者自己在終端,都跑得出同一份診斷。 +Claude, Codex, and Cursor perform the same small set of high-value judgments. Mode flows, schemas, validators, session commits, and renderers are shared repository code. A thin bridge prevents each agent from maintaining a separate long prompt that drifts over time. diff --git a/BACKLOG.md b/BACKLOG.md index ddba02a..9f03dc0 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1,93 +1,93 @@ -# fomo-kernel · Backlog +# fomo-kernel backlog -> 從 2026-06-14 的 user-story × engine review 拍出來的待辦。 -> 背景脈絡:受眾已收窄為「會用 AI 工具的人(含交易上仍憑感覺的散戶)」;skill 是第一期本體(資料留本機解隱私),不是探針。卡的唯一賣點 = 用你自己的數字,誠實說出你「知道卻沒做到」的事。 +Last refreshed: 2026-07-14. The target is a promotion-ready release on 2026-07-19. -> ⚠️ **兩條線同步註記(2026-06-14)**:本檔的「願景層 + ISSUE-1」來自下午的 user-story / 願景 review session;另有一條「engine 實作線」今早 10:34–11:53 並行推進(commit c6cb138→dccf9c4),已 pivot 到 `behavior-diagnosis.md`(對事不對人、行為多標籤)並實作標的層診斷。動 engine 前先跟那條線對齊,別重工。 +## P0 for the promotion release ---- +### Stable workflow and card production -## ISSUE-1 ·〔小–中〕α 雙閘門:資料不夠厚時,不准用「真本事」語氣出 alpha +Status: implemented, pending manual release gates. -**問題** -`engine/trade_recap.py` 的 alpha 在樣本不足時,仍以「真本事 α」的*能力*語氣輸出 → 會在看得懂統計的受眾面前砸掉第一張卡的信任。 +- Keep `SKILL.md` as a thin entry point. +- Use the fixed `prepare -> preview -> finalize` lifecycle. +- Resume interrupted sessions without refetching live data. +- Commit one canonical immutable session and rebuild projections from it. +- Render private and public cards deterministically. +- Keep required questions and evidence completeness as code gates. -- `_regress()` (trade_recap.py:231):門檻只有 `len(df) < 60`(≈3 個月)就吐 α 數字。 -- overview (trade_recap.py:634-636):措辭「真本事 α 年化 X%」,<252 天只加一句小警語,**數字照印**。 -- 統計真相:α 作為「能力證據」的顯著性 ≈ Information Ratio × √年數;散戶尺度(1–2 年、幾十筆)**結構上到不了顯著**。而 mock 失真的真因*不是*天數(有 605 天),是**橫截面太窄**(4 檔、98% 同一 driver)→ 算出的是賽道不是選股。 -> 行號已更新至 721 行版 engine(dccf9c4 後)。那批 commit 動的是攤平/出場,**α 邏輯未被動,本 issue 仍有效**。 +Release evidence: `docs/release-2026-07-19.md`, `tests/test_review_v2.py`, and the complete offline suite. -**要改** -1. 新增 `alpha_credible(ab, held, rts)` 雙閘門:(a) 天數夠(建議 ≥252,且仍標「個人 α 偏 noisy」)**AND** (b) 橫截面夠寬(持倉檔數 ≥ N 且最大單一 driver 暴險 < ~50%,否則 α = 賽道紅利非選股)。 -2. overview α 印法 (:634-636):credible 才出「α 年化」;否則只出 β + 「贏大盤 +Xpp(拆帳見下)」,**拿掉「真本事」**。 -3. `print_alpha_beta()` (約 :264-289):不 credible 時整段降級成「報酬拆帳」語氣,不出 `alpha_ann` 數字,只留「贏大盤多少 / 多少來自賽道」的描述性分解(這層不需顯著性、誠實)。 -4. lens 同步:`rubric/vincent-yu.lens.json` 的 `dims."alpha/beta".motive_q` + SKILL.md,不 credible 時不要問「這報酬算你選股本事還是敢押高波動」(前提已不成立)。 +### Thesis evolution and add evidence -**驗收**:跑 mock(4 檔 / 98% AI)→ 不再出現「真本事 α +33%」大字,改為「樣本/持倉不足以判定選股能力,先看行為層」之類。 -**範圍**:單檔 ~30–50 行,3 處輸出 gate + 1 判斷函式 + 1 處 lens/SKILL 同步。低風險(純輸出層)。約半天。 -**原則來源**:跟 `behavior-diagnosis.md` 同向——`prescribe()`(:525-527)對「選股 edge」已經誠實標「資料還判不出、別急著外包也別自滿」;α overview 還沒跟上這個誠實標準,本 issue 就是讓它跟上。 +Status: implemented in the v2 lifecycle. ---- +- Classify losing-position adds as planned tranche, new evidence, valuation change, price only, or skip. +- Require a claim and source for `new_evidence`. +- Store append-only thesis decision events tied to active cycle IDs. +- Reconcile the evidence in future reviews rather than asking a generic averaging-down question again. -## ISSUE-2 ·〔已作廢校正〕原「落地分型」→ 改為「補 behavior-diagnosis 還缺的純損耗標籤」 +### ETF policy -**校正記錄(2026-06-14)** -本 issue 原寫「落地 trader-types.md 先判型再評分」。**作廢**——`trader-types.md` 已於今早 10:34(c6cb138)被否決移除,正解是 `behavior-diagnosis.md`(對事不對人 · 行為多標籤)。理由比分型強:散戶是風格縫合怪、硬分型會錯判;整個開發照出的真洞全是「對事」算的,分型是多餘中間層;「同訊號不同風格意義相反」用「標的層脈絡」就能解、無單點誤判風險。engine 的 `ticker_diagnosis()`(標的層多標籤)已實作此方向。 +Status: implemented with conservative fallback. -**真正還缺的(= behavior-diagnosis.md §下一步2 的 ⏳)** -第一層「跨型純損耗」還差兩個標籤未落到 engine: -- `revenge_trade`:連敗後 / 短時間內報復性加碼同標的 → 處方「連敗 N 次強制冷靜期」。 -- `overtrading` 強化:高頻進出且淨輸大盤(Barber-Odean),目前只有頻率 + α/β 部分覆蓋。 +- Exempt only broad-market, regional, bond, and commodity allocation ETFs from single-name concentration. +- Keep sector, thematic, and leveraged ETFs in concentration and stress diagnostics. +- Give unknown instruments no exemption. +- Disclose missing expense ratio and tracking error rather than assuming zero. +- Add a live metadata source later without changing the policy contract. -**範圍**:engine 內加 2 個偵測 + 對應可驗處方。中等。跟 ISSUE-1 獨立。 -**注意**:這條屬「engine 實作線」,動工前先跟該線對齊,別重工。 +### English implementation and bilingual GTM ---- +Status: implemented in this change, pending full verification. -## ISSUE-3 ·〔小〕Step 2 從「自我定性」改「證據門檻」:堵住 thesis 洗白器 +- Keep developer documentation and skill instructions in English. +- Keep English and Traditional Chinese GTM artifacts synchronized as separate files. +- Keep user-visible localized product copy in separate locale resources. +- Prevent mixed-language implementation docs with a deterministic regression test. -> 來源:2026-06-19 三方 revisit(Claude 初評 + gemini 3.5 Flash + codex,各自獨立跑,簡報 `/tmp/fomo-kernel-userstory-review.md`)。gemini 與 codex **未互看卻收斂到同一刀**,信號強。 +## Manual release gates -**問題** -`SKILL.md` Step 2(a)(約 :52-55)把「動機定性權」整個交給用戶的嘴:「答『逢低/計劃內』→ 移除警告、標逢低……**你的答案定性**」。對「不是不知道在凹單、是不想承認」的目標用戶,這內建一個**洗白器**: -- Outcome Bias——凹單剛好漲回的標的(如 mock 的 PLTR/NVDA),用戶被二選一問時會選保全面子的「計劃內」,診斷在**最該硬的那一筆**軟掉。 -- Claude 初評把 Step 2 當「最不可替代的分水嶺」;gemini(Outcome Bias)+ codex(「把定性權交還給最會合理化的人」)**各自獨立判它是最大盲點**。2:1,且兩個外部模型獨立撞同一點。 +- Complete one full Traditional Chinese run with an anonymized publishable CSV. +- Complete the same flow in English. +- Inspect the public card manually for amounts, dates, tickers, exact weights, session IDs, evidence text, and free-form narrative. +- Demonstrate evidence-gate rejection and recovery. +- Demonstrate broad ETF exemption versus thematic ETF concentration. -**要改**(純 prompt 層,engine 不動) -1. Step 2 問法:從「你覺得逢低還是凹單?」→「**寫出你加碼當下知道、但進場時不知道的新證據;寫不出 → 標凹單/待確認**」。關鍵:AI 不必判證據真假,**逼用戶舉證這個動作本身就分流**——真逢低寫得出,凹單寫不出。 -2. Step 3 卡標籤定性規則:舉不出新證據 → **不准標逢低**(現行「答計劃內就移除警告」改成「舉得出證據才移除警告」)。 -3. 動機單元表(約 :59-68)同步:把「自我定性」式問句換成「舉證」式。 +## P1 -**守住既有鐵律(別被當成走回頭路)** -- **不違反** `behavior-diagnosis.md` 的「不做進場每筆標」(owner 2026-06-14 駁回):這是**事後、只對 engine 挑出的可疑標的、逼一次舉證**,不是進場每筆標。和該檔「三層降本」第 2 層(只對待確認少數問)完全相容——只是把那一問從「定性」升級成「舉證」。 -- 守低摩擦:可疑標的通常 1–2 檔(mock 只 PLTR),不是每筆。 +### Multi-lens selection and comparison -**與閉環的關係**:這是「候選 · thesis 對帳」+ 願景 v1「pre-trade gate / 守則檔」的 **MVP**。先用「事後逼舉證」驗方向(今天能改),再長成「事前存進場 thesis 原文、復盤拿原文對帳」的 `~/.trade-coach/` 版——codex 版(事後)是 gemini 版(事前)的最小落地。 +- Select from a small verified lens set. +- Apply style-specific divergence only where mechanical evidence supports a style axis. +- Keep universal risk failures outside the lens override. +- Do not duplicate lifecycle, state, schemas, or renderers. +- Verify every public quotation against a primary source before promotion. -**驗收**:對 mock 跑 Step 2,用戶對 PLTR 答「逢低」但寫不出新證據 → 卡仍標「待確認/凹單」,不被洗成逢低。 -**範圍**:`SKILL.md` Step 2 + Step 3 標籤規則 + 動機單元表,3 處 prompt 文字,engine 零改動。低風險。約 1–2 小時。 -**與 ISSUE-1 同向**:都是「資料/嘴不夠硬時,不准出能力語氣 / 逢低定論」的誠實閘門;兩者獨立可分別做。 +### Complete snapshot adapter ---- +- Accept a position table or screenshot directly. +- Normalize into the snapshot review card/state contract. +- Limit conclusions to facts supported by a snapshot. +- Allow later transaction history to unlock behavioral dimensions. -## 願景層(2026-06-14 下午 session 拍板) +### ETF metadata enrichment -**終局形態 = 投資教練 agent(process coach,不是 stock advisor)。** 從「事後一張卡」進化成「事前承諾 → 事後對帳」的閉環。四條紅線必守: -1. **過程教練 ≠ 選股顧問**:教怎麼決策(紀律/守則/對帳動機),絕不碰買哪支(IP/法規/北極星)。 -2. **會拒絕 ≠ 有求必應**:有求必應的投資 agent = 「焦慮買答案」的溫床 = 產品要解的病本身。克制是 feature。 -3. **有哲學但不寫死**:哲學寫死=拿錯尺、無哲學=yes-man。靠 behavior-diagnosis 的「風格當脈絡」解。 -4. **記得你 → skill 要長出本機狀態**(`~/.trade-coach/`)才成 agent;Claude Code/Agent SDK 已是 runtime,不必從零造。 +- Add a maintained instrument source for classification, expense ratio, and tracking error. +- Preserve the local override and conservative unknown fallback. +- Cache data for offline and repeatable reviews. -**6 步課程弧線**(每步都有現成零件):初診(卡) → 訂計畫(守則檔) → 賽前提醒(pre-trade gate) → 賽後對帳(驗規矩) → 升級畢業(守則清單) → 哲學演進(behavior-diagnosis 風格脈絡)。 -**演進路徑**:v0 無狀態卡 → v1 守則檔+gate+對帳 → v2 風格脈絡+多鏡片 → v3 全 context 對帳。 -**守北極星**:vision 是 agent,但 next action 仍是「那張卡戳中一個真人」。**約束在卡的品質(夠不夠好用、到不到能發的水準),不在分發找人**(owner 2026-07-05 糾偏,見 #112:卡一直在測、也一直在給人看,真卡點是卡不夠好用)。別讓大願景偷走當下該驗的小東西。 +## Later candidates ---- +- Event-driven pre-trade check against the active process rule. +- Personal lens distilled from repeated confirmed review patterns. +- Richer source attribution for owner-only research workflows. +- Automated GTM asset generation and publishing. +- More behavior detectors only when they are measurable and bind to a testable rule. -## 候選(未拍板) +## Product boundaries -- **pre-trade gate**:把「下次只改一件」沉澱成本機 `my-rules.md`,下單前 `/fomo-kernel check ` 攔一次。市場空白(無人把復盤回灌下一筆)+ 解 Epic D 留存 + 正中北極星。守則檔就是 skill 的「狀態/記憶」。 -- **thesis 對帳**:⭐ engine 已起頭(`4599f4f` infer-thesis-from-behavior)。可升級成「拿用戶*寫過的*進場 thesis 原文對帳」——從推斷動機 → 核對原文。skill 能吃用戶全 context,SaaS 結構上做不到。→ **其 MVP 已拆成 ISSUE-3**(先做「事後逼舉證」的證據門檻,再長成「事前存原文對帳」)。 -- **行為 counterfactual 重放**:「賣太早那批多抱 30 天會怎樣」——`fwd_from_px` 已有資料,低成本高回報。 -- **多哲學對照**:部分已被 behavior-diagnosis「風格當脈絡」吸收;若要做成「多鏡片顯示分歧」,守住收斂——只呈現分歧最大那一點,別變第二份報告。 -- **lens 迭代回 kol(2026-06-14,VY 已降為可換鏡片、demo 已去名後)**:把 `lens.json` 的可換架構連回母專案 `kol_collector` / `kol_collect` 的多 KOL 蒸餾——那邊已沉澱 12+ 追蹤 KOL,各可蒸一面鏡片讓用戶選大師;fomo-kernel 的 lens 是這個 distill-KOL→lens 的第一個落地(VY = 第一面,現已通用化呈現、demo 不掛名)。owner 標「之後再迭代」。背景見記憶 `project-kol-collect-vs-collector-overlap`。 +- Process coaching is not security selection. +- One card converges on one behavioral leak and at most one rule. +- Trade data remains local. +- A clean strengths card is valid when no costly leak is supported. +- Real-user usefulness remains the final validation layer; passing automated tests is necessary but not sufficient. diff --git a/CLAUDE.md b/CLAUDE.md index a0fb2d2..10bee40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,77 +1,79 @@ -# CLAUDE.md — 開發者 / 維護者指引 +# CLAUDE.md — Maintainer guide -> 這份給**改這個 repo 程式碼的你**看。使用者跑這個 skill 時的行為契約在 [AGENTS.md](AGENTS.md)(給非 Claude Code 的 agent,如 Codex,執行時看;Claude Code 自己會自動載入 SKILL.md,不需要 AGENTS.md);兩者角色不同,**不要互相搬內容**——AGENTS.md 講「怎麼用這個 skill」,這份講「怎麼改這個 codebase」。 +> This file is for contributors changing the repository. Runtime behavior is defined by [skills/fomo-kernel/SKILL.md](skills/fomo-kernel/SKILL.md); [AGENTS.md](AGENTS.md) is only a thin cross-agent router. Do not duplicate the complete runtime contract here. -## 這個 repo 是什麼(維護者角度) +## Repository role -`fomo-kernel`(對外 `/fomo-kernel` skill)的**公開** git repo(GitHub `atomchung/fomo-kernel`),會被外部使用者 clone/安裝。核心是 `skills/fomo-kernel/engine/` 的純 Python 確定性引擎,`skills/fomo-kernel/SKILL.md` 定義 Claude Code 執行時的四步流程,`AGENTS.md` 是給非 Claude Code agent 的路由指南。 +`fomo-kernel` is a public repository that external users can clone and install. The deterministic Python engine lives in `skills/fomo-kernel/engine/`. `SKILL.md` defines runtime orchestration, and `AGENTS.md` routes agents that do not automatically discover skills. -## 改動前必讀:契約同步 +## Contract synchronization -- **`skills/fomo-kernel/SKILL.md` 是行為契約的唯一權威**。如果你改的 engine 邏輯會影響使用者看到的行為(例如 `[ASK]` 的判定條件、卡片欄位、四步流程順序),**同一個 commit 裡要同步更新 SKILL.md**(必要時也更新 `AGENTS.md` 的摘要),不要讓兩者 drift。 -- `AGENTS.md` 只放「路由 + 鐵律摘要」,細節仍指回 `SKILL.md`——不要把完整流程複製進 `AGENTS.md`。 +- Treat `skills/fomo-kernel/SKILL.md` as the runtime contract entry point. If engine behavior changes what a user sees, update the relevant flow, reference, schema, renderer contract, and the thin summary when necessary in the same change. +- Keep `AGENTS.md` limited to routing and non-negotiable boundaries. +- Keep developer documentation and skill instructions in English. Follow [docs/language-policy.md](docs/language-policy.md) for the GTM and localization exceptions. -## 誠實揭露的判定住在 engine,不住在 SKILL prose(#82,owner 2026-07-09 拍板) +## Honesty decisions belong in code -「卡面該交代哪些誠實缺口」(α 不可信 / 未實現缺價 / 板塊歸因不全 / 未分類 driver / 賣超 / 混幣)由 engine 的 `build_honesty_ledger()` 聚合成 `honesty_ledger` 欄位(只收觸發項,空 list = 無缺口)。三條鐵律,改動別違反: +`build_honesty_ledger()` decides which limitations a card must disclose, including alpha credibility, missing live prices, incomplete sector attribution, unknown drivers, orphan sells, currency mixing, cash reliability, and ETF metadata gaps. -- **判定進 engine、文案留 Claude**:engine 只決定「該講什麼」(哪些 key 觸發),「怎麼講」照 card-spec 說話原則由 Claude 融入敘事——engine **不給死文案**(死文案 = card-spec 罵的呆板揭露)。要加新的誠實缺口:改 `build_honesty_ledger` 加一個 key + card-spec 加一行講法,**別在 SKILL.md 加「X 非空 → 補一句」的散落 prose**(那正是 #82 前的病:判定散在 SKILL/card-spec 多處靠自律,JSON 模式還漏了整套聚合)。 -- **guardrail 內部化,不上卡**:`honesty_ledger` 是 SKILL Step 3 出卡前 gate 的**內部**核對源(每個 key 卡面有沒有講到),**它本身不列成表、不輸出給用戶**——用戶只看到乾淨敘事卡、誠實句融進去。別把 checklist 印上卡(違反 card-spec「卡是故事不是 dashboard」)。 -- **SKILL 主檔不長 guardrail prose**:揭露判定的單一事實源是 engine,不是每次載入 ~25k 的 SKILL.md(#149)。新增揭露點時 SKILL 主檔行數不該增長——這是「避免 guardrail 冗長」的機制,不靠自律記得精簡。 +- Put disclosure conditions in the engine. Put locale-specific wording in renderer copy. Do not scatter new `if field exists, add a sentence` instructions through `SKILL.md`. +- Treat the ledger as an internal rendering gate, not a checklist printed on the card. The card should remain a coherent story. +- Keep `SKILL.md` thin. New honesty keys should not make the entry-point prompt grow. -例外:`show_widget` 有沒有試成功是**執行層事實**(engine 標不到環境能否渲染),留 SKILL Step 3 self-check 第 5 項的 prose guardrail。 -事實鏈路(改一處連動全鏈):`build_honesty_ledger()` ↔ SKILL Step 1 欄位 + Step 3 gate ↔ card-spec 「誠實點照 ledger 講」段 ↔ EVALS B6/B14/B15/B16 ↔ eval-design A-5 ↔ `test_tr_json_contract.py` 的 `HL_KEYS`。 +The synchronization chain is: `build_honesty_ledger()` ↔ renderer and copy ↔ card policy ↔ eval design ↔ contract tests. -## 測試(改 engine/ 前後必跑) +## Tests + +Run before and after changing the engine or runtime contract: ```bash -python3 tests/run_all.py # 一鍵跑全部十二套測試,離線、確定性、免裝 pytest -TR_TEST_NETWORK=1 python3 tests/run_all.py # 額外加跑 β 方向 + 市場背景 network smoke +python3 tests/run_all.py +TR_TEST_NETWORK=1 python3 tests/run_all.py # optional beta-direction and market-context network smoke ``` -十二套分工:機械層純函式單元(`tests/test_engine_units.py`)、TR_JSON/state 契約(`tests/test_tr_json_contract.py`)、價格路徑合成單元(`tests/test_price_paths.py`)、snapshot-anchored 帳本(`tests/test_ledger.py`)、出場追蹤+swap(`tests/test_revisit.py`)、市場背景(`tests/test_market_context.py`)、問題帳(`tests/test_problems.py`)、三風格端到端(`tests/test_sample_styles.py`)、狀態迴圈端到端(`skills/fomo-kernel/engine/test_state_loop.py`)、卡面/狀態 checker 驗活(`tests/test_checkers_offline.py`)、本機資料控制 CLI(`tests/test_coach_data_cli.py`)、收尾 session idempotency(`tests/test_coach_session_idempotency.py`,#166)。**改 engine 輸出格式、last_px 邏輯或排序邏輯後,這十二套沒全過就不要 commit。** +The default suite is offline, deterministic, and does not require pytest. It covers engine units, JSON/state contracts, price paths, the snapshot-anchored ledger, revisit/swap behavior, market context, problem tracking, persona fixtures, the state loop, artifact checkers, local data controls, session idempotency, the v2 review lifecycle, and documentation language boundaries. -## `.claude/` hooks(committed 的 agent 護欄) +Do not commit after changing engine output, price handling, sorting, or orchestration unless the complete offline suite passes. -這個 repo committed 了 Claude Code hooks(`.claude/settings.json` + `.claude/hooks/`),把上面「測試沒全過就不要 commit」從自律變成機制:`pre_commit_test_gate.sh` 是 `PreToolUse:Bash` gate,當 `skills/fomo-kernel/engine/` 或 `tests/` 有未提交改動時跑 `tests/run_all.py`,紅了就 deny 掉 commit。 +## Claude Code hooks -⚠️ **改或加任何 hook 前必讀**:實測目前這版 Claude Code **忽略 hook 的 `if:` filter**——matcher(如 `Bash`)會對**每一個**符合的 tool call 觸發,不是只有 `if` 指定的那種。所以**一律在腳本裡自己讀 stdin `tool_input.command` 判斷、非目標指令立即 `exit 0`,永遠別依賴 `if:`**。少了這道自我過濾,commit-gate 會在 engine dirty 時對每個 Bash 指令各跑一次整套測試(~11.5s)。照 `pre_commit_test_gate.sh` 開頭的 self-filter 範式抄。 +Committed hooks in `.claude/` enforce the test gate. Hook `if:` filters have been observed to be unreliable in the supported Claude Code setup. Every hook script must inspect `tool_input.command` from stdin and exit immediately for unrelated commands. Follow the self-filtering pattern in `pre_commit_test_gate.sh`. -## 隱私鐵律的技術防線(不要弱化) +## Privacy boundary -`.gitignore` 已經用 `*.csv` + `!skills/fomo-kernel/mock/*.csv` 擋住真實交易資料進 git,只留 mock 假資料例外。這是機制防線,不是靠自律——**任何改動都不要移除或繞過這條規則**,包括新增測試 fixture 時也只能用 mock 資料。 +`.gitignore` blocks real CSV files and allows only fixtures under `skills/fomo-kernel/mock/`. Do not weaken or bypass this mechanism. Never include real trade records in commits, tests, or documentation examples. -## Commit / PR 慣例(從既有 git log 觀察到的模式) +## Commit and PR conventions -`(): (closes #NN) (#PR)` 或 `: `。例: -``` -fix(engine): last_px covers all fetched tickers, not just round-trips (closes #79) (#83) -fix(engine): candidate_rules 補 3 維規矩生成 + 分散維度門檻對齊 (#100) +Follow the existing history: + +```text +(): (closes #NN) (#PR) +: ``` -這個 repo 走**issue → PR → close issue** 的正規流程,延續這個格式,不要另創一套。開 PR/issue 前**先 `gh issue list` / `git log --grep` 查一下有沒有已經修過**——這個 repo 修 bug 的節奏很快,容易撞到已經處理過的東西。 -## 並行開發慣例(多 session / 多 agent 同時動這個 repo 是常態) +Check `gh issue list`, `gh pr list`, and `git log --grep` before opening work so you do not duplicate an active or completed fix. -- **認領再修**:動手修某個 issue 前,先 assign 自己或在 issue 下留言認領;開修復 PR 前 `gh pr list` 查同一 issue / 同一函式區域有沒有 open PR。前例:同一個 bug 被獨立診斷兩次(#87/#95,互不引用),`render()` 被兩個 PR 並行大改產生 4 個規格 regression(#23/#24)。 -- **開新 branch 先 fetch、從最新 `origin/main` 開**;merge 完手上的 PR 後再 `gh pr list` 一次,查剛冒出的新 PR、以及與剛 merge 內容的**語意重疊**(git 只擋文字衝突,不擋語意衝突)。 -- **修 bug 不只修發現的那個實例**:同一根因常住在多處,動手前先 grep fixtures / docs / tests 掃其他實例,PR body 寫「掃過的範圍與結果」。前例:拆股 fixture 的同款定價 bug 分三批被動發現(#93 → #98 → #108)。 -- **批次 merge(一次合 ≥2 個 PR)前做一輪 zoom-out**,不只逐 diff 看正確性:①同主題 issue 第二次出現=同一設計缺陷的第二個症狀,先問「這條線該不該存在」再修單點 ②文檔/測試出現「繞過/避開/先…再跑」措辭=系統在教人繞過自己 ③ engine 靠檔名/環境變數等隱性訊號做行為分支=違反 data-agnostic(#89 前例)。含 engine 改動時,對全部 mock persona CSV 跑一輪產卡並核對數字——#93/#94/#95 三個正確性 bug 全是這樣現形的,任何 diff review 都看不到。 -- **批次 merge 收尾**:這一輪產生的 agent worktree / 本地 branch,PR 全 merge 後,驗證 commit 已可從 main 達到、且 `git worktree list` 確認沒有其他 session 在用,才清掉。 +When multiple sessions are active: -## 鏡像檔案對照表(同一份事實住在多處,改一處要連動) +- Claim the issue before editing and check for overlapping PRs. +- Fetch before creating a branch from the latest `origin/main`. +- Search fixtures, documentation, and tests for other instances of the same root cause. +- Before merging several PRs, review semantic overlap as well as textual conflicts. If the engine changed, generate cards for all mock personas and verify the output. +- Remove worktrees and local branches only after confirming the merged commit is reachable from main and no other session uses them. -漏同步的 drift 反覆發生過(#68、#96、cycle_id 對帳失效),改下列任何一處,照表連動其餘: +## Mirrored surfaces -| 事實 | 住在哪些檔案 | +| Fact | Surfaces that must stay synchronized | |---|---| -| 行為契約 | engine ↔ `skills/fomo-kernel/SKILL.md`(權威)↔ `docs/eval-design.md` ↔ `evals/EVALS.md` | -| demo 卡示意數字 | README.zh-TW 文字卡 ↔ `docs/demo-card.html`(改後重截 `demo-card.png`)〔中文〕;README.md 文字卡 ↔ `docs/demo-card-en.html`(改後重截 `demo-card-en.png`)〔英文,#165 後新增,數字須與中文版一致,只譯文字〕 | -| README 雙語 | **分檔**:`README.md`(英文,GitHub 首頁預設、對外主入口)↔ `README.zh-TW.md`(繁中完整版),兩檔頂部語言連結互指;改主要內容**兩檔同步**,尤其別讓英文主入口 drift 落後中文 | +| Runtime behavior | engine ↔ `SKILL.md` and routed flows/references ↔ `docs/eval-design.md` ↔ `evals/EVALS.md` | +| Demo card values | English README ↔ English demo HTML/image; Traditional Chinese README ↔ Traditional Chinese demo HTML/image. Values must match; only wording differs. | +| GTM documentation | `README.md` is the English default; `README.zh-TW.md` is the complete Traditional Chinese counterpart. Keep language links and substantive product claims synchronized. | -引用**產品假設**(誰是用戶、當前卡點是什麼)做優先級決策時,帶上判定日期;判定已隔數週或出現矛盾訊號,先跟 maintainer 對帳再據以行動——過時結論被跨 session 複讀的案例見 #112。 +Date product assumptions when using them for prioritization. Reconfirm assumptions that are several weeks old or contradicted by new evidence. -## 公開 repo 的品質門檻 +## Public-repository quality bar -這個 repo 會被外部使用者 clone 使用,合併標準比純內部工具高: -- 不要在任何 commit、測試 fixture、文件範例裡混入真實交易明細(只用 mock) -- README/AGENTS.md 面向外部讀者,改動措辭要考慮「沒有這段對話上下文的人看得懂嗎」 +- Use only synthetic mock data. +- Write public documentation for readers who do not have the conversation context. +- Preserve deterministic, fail-closed behavior at workflow and persistence boundaries. diff --git a/README.md b/README.md index 5f34090..3b2b230 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ **English** · [繁體中文](README.zh-TW.md) -> A [Claude Code](https://claude.com/claude-code) skill that reviews your real trades through **one master's lens** (a swappable distillation of a trading philosophy) and hands you back **a single card** — +> A local, agent-assisted trade-review skill for Claude Code, Codex, Cursor, and compatible coding agents. It reviews your real trades through **one master's lens** and hands you back **a single card** — > the one thing you did right + your biggest leak (in your own numbers) + one rule to keep next time + one line from the master. Not another stats report. It does what a report can't: **first it computes the behavioral leaks you can't see, then it asks the motive you won't admit, then it forces you to change exactly one thing next time.** -> 📝 **Note on language.** This README is in English, but the skill's runtime output, the master lens, `SKILL.md`, and `AGENTS.md` are currently in Traditional Chinese — FOMO Kernel is Chinese-first today. This page covers setup and concepts so you can decide whether to install; the card you'll actually get renders in Chinese. +> 📝 **Language.** The same review contract renders in Traditional Chinese or English (`--language zh-TW|en`). Translation changes the questions and card copy, not the engine facts or analysis policy. ## Quick start @@ -18,16 +18,17 @@ Not another stats report. It does what a report can't: **first it computes the b ``` The card's value is in step ② — the engine flags a suspicious position and asks *"averaging down on conviction, or refusing to cut a loser?"*; your one-sentence answer is what turns the raw diagnosis into a verdict. **You can't see that layer from the engine's raw output alone.** Install steps under [Install](#install). -**Want zero-install, just to see what the engine computes** (the *raw diagnosis* from the mechanical layer — not yet the finished card): +**Want zero-install, just to see the stable flow start:** ```bash git clone https://github.com/atomchung/fomo-kernel && cd fomo-kernel pip install -r requirements.txt # if it errors with externally-managed-environment → see the venv steps under Install -cd skills/fomo-kernel && python3 engine/trade_recap.py # runs the built-in mock, prints the raw diagnosis +cd skills/fomo-kernel && python3 engine/review.py prepare --test-drive --language en +# emits a resumable Review Plan; required motive questions come before preview/finalize ``` ## What it looks like -Running the built-in mock, the **illustrative card** looks like this (below is the simplified quick-view; the actual engine output is a full-color terminal card that also includes a what-if drawdown stress test, 5-dimension behavior bars, and a return-attribution section — the finished verdict card is what Claude converges on *after* asking about motive in Step ②). *Both the text below and the card image further down are English translations to show the shape — the engine currently renders in Traditional Chinese, so what you'll actually get looks like the "跑出來長什麼樣" section of the [Traditional Chinese README](README.zh-TW.md) instead:* +Running the built-in mock, the **illustrative card** looks like this (below is a simplified quick-view; the finished private card is rendered only after the required motive questions and one-rule choice): ```text Review card · Master lens · mock sample @@ -82,11 +83,17 @@ ChatGPT can't compute the real FIFO-matched α/β, can't tell "DCA" from "averag - The skill runs your CSV **on your own machine** — **no upload to any backend, no storage anywhere else, nothing sent to the author**. For weekly reconciliation it does save review-derived state **locally** under `~/.trade-coach/` (never sent anywhere) — see the next section for exactly what that is and how to inspect, export, or wipe it. - The author can't see your trade detail. The only (voluntary) thing collected back is a single "was this card useful?" — no trade content — via the [card feedback form](https://github.com/atomchung/fomo-kernel/issues/new?template=card-feedback.yml), 30 seconds if you're willing. - `.gitignore` is set so **no `.csv` is ever committed**, with only the mock/sample fixtures excepted. -- Precisely: the only thing that reads your trades is **the Claude you're already using** — it has to read the CSV to review it for you, exactly like any other time you use Claude. That's a different thing from handing your statement to a SaaS that stores it, that you can't see into, that the author can query. (So it's not "never touches any server" — it's "not persisted, not sent to the author, not through a third party.") +- Precisely: the local Python engine reads the normalized CSV, and the coding agent you invoke may read the source statement to map broker columns. Nothing is sent to the author. This differs from handing a statement to a SaaS whose retained data you cannot inspect. ## 📁 Where your coach memory lives / how to maintain it -On your second visit, the card first reconciles "did you keep last time's rule?" — backed by several **local-only** files under `~/.trade-coach/` (never sent anywhere, never to the author). The four behind that reconciliation: +On your second visit, the card first reconciles "did you keep last time's rule?" The canonical record is one immutable directory per review: + +```bash +ls ~/.trade-coach/sessions/ # bundle + state + answers + cards + hash manifest +``` + +Legacy tools remain compatible through rebuildable projections: ```bash cat ~/.trade-coach/log.jsonl # one line per review (thin metrics + the rule you committed to); empty = first time @@ -107,15 +114,15 @@ python3 skills/fomo-kernel/engine/coach.py data-reset --confirm # actually - **Coming back next week — which CSV do I import?** Just export your **full history** again and hand it over — you never track increments by hand. Rows that overlap with earlier imports are auto-deduplicated (that's exactly what the dedup is for), so **dumping the whole statement every week is safe**; the engine uses last review's cutoff to tell what's new, and the card opens by reconciling against the rule you committed to last time. - **See past reviews** → `cat ~/.trade-coach/log.jsonl`. - **Switch philosophy lens / reset the reconciliation baseline** → `coach.py data-reset --confirm` (or delete/rename `~/.trade-coach/` by hand — either way, next time is a fresh first visit). -- **Wrote a thesis wrong** → edit `theses.jsonl`; it's append-only, so a correction = append a new event (don't overwrite the old one — that's how you see, across time, how you first reasoned and how it later changed). +- **Wrote a thesis wrong** → correct it in the next review; the new event points to the earlier thesis. Do not hand-edit `theses.jsonl`: it is now a rebuildable projection of canonical sessions. - **Privacy, self-verifiable**: coach memory is just the files `data-status` lists above, all on your machine; there isn't a single row on the author's side. - **Want to preview the multi-week loop first** (runs entirely in a temp directory, **never touches** your real `~/.trade-coach/`) → `python3 skills/fomo-kernel/engine/demo_weeks.py`: slices the built-in mock into 3 time windows to simulate "first visit → reconcile → reconcile", so you can watch the second card cite last week's commitment and `log.jsonl` grow line by line. -> 💡 **Want to share with a community?** By default the card is the full private version only. Tell me "give me the shareable version" and it outputs a **de-sensitized** plain-text version (hides amounts / share counts / exact ratios, keeps only the behavior pattern + relative performance β / beat-the-market pp) — ready to paste to X / Threads. +> 💡 **Want to share with a community?** Each committed review creates `card-public.md`, a separately rendered view that removes amounts, dates, tickers, exact weights, and agent free text. The private card remains the default response; ask for the public card when you want to post it. ## Install -**Prerequisite: this is a skill for [Claude Code](https://claude.com/claude-code)** — Anthropic's terminal / desktop AI tool (needs a Claude subscription). If you haven't used it, spend 5 minutes to [install and log in](https://docs.claude.com/en/docs/claude-code/setup) first, then come back for the three steps below. +**Prerequisite:** Python 3.11+. Claude Code users can install the slash-command skill below; Codex, Cursor, and other agents can use the repo directly through `AGENTS.md` and `engine/review.py` without a Claude subscription. Needs Python 3.11+. **On recent macOS (Homebrew / system Python) a bare `pip install` is blocked by PEP 668** (`externally-managed-environment`); use a venv: ```bash @@ -139,19 +146,21 @@ Inside Claude Code: ``` Your CSV can come from **any broker** — Claude reads and maps it into the columns the engine needs (`Symbol / Action(BUY|SELL) / Quantity / Price / TradeDate`, plus optional `Market / Currency` for non-US stocks — e.g. `2330.TW / TW / TWD`; omitted = US/USD); you don't hand-clean anything. -> 🏷️ For **obscure tickers** the engine's sector table doesn't recognize, Claude **auto-generates a driver map** for you to confirm (each tagged `[sector, theme]`), so the "diversification" dimension doesn't count same-theme names as real diversification — you don't do this by hand, and don't let it fall back to the `driver map: 0 tickers` case (diversification goes off). Details in SKILL Step 0.5. +> 🏷️ For **obscure tickers**, the agent may propose a local driver map for sector/theme exposure. For obscure ETFs it may also propose an instrument map, but the code grants an allocation exemption only to explicit broad-market, regional, bond, or commodity classifications; unknowns remain concentrated by default. -**What happens**: ① the engine runs the diagnosis → ② Claude asks you 1–3 thesis/motive questions in dialogue (dip-buy or averaging a loser?) → ③ with your answers, it issues one verdict card. Card layout is in [`card-template.html`](skills/fomo-kernel/card-template.html) (a full four-layer HTML example). +**What happens**: ① `prepare` runs the deterministic diagnosis and builds a question queue → ② the agent asks those thesis/motive questions → ③ `preview` validates the answers and renders a card → ④ you choose one rule and `finalize` commits the session atomically. ## Using it from other coding agents -You don't need Claude Code's skill system — the core engine is plain Python and depends on no agent machinery: +You don't need Claude Code's skill system. Codex, Cursor, and other agents use the same orchestration contract: ```bash -cd skills/fomo-kernel && python3 engine/trade_recap.py ~/Downloads/my.csv +cd skills/fomo-kernel +python3 engine/review.py prepare ~/Downloads/my.csv --language en +# follow review_plan.flow_path, answer question_queue, then call preview and finalize ``` -If you use Codex / Cursor or another coding agent, point it at [`AGENTS.md`](AGENTS.md) and have it follow along — that file is the routing guide for non-Claude-Code agents, telling it how to run the engine, ask about motive, and issue the card. +Point the agent at [`AGENTS.md`](AGENTS.md). `SKILL.md` is now a thin entry; mode-specific flows, JSON schemas, deterministic validators, and renderers hold the detailed contract. ## Style samples (runnable — see how different styles surface different leaks) @@ -179,7 +188,15 @@ python3 engine/trade_recap.py # no args = mock_trades.c ``` skills/fomo-kernel/ - SKILL.md ← the skill itself (four-step flow: format → engine → pre-card confirm → verdict card) + SKILL.md ← thin public entry and invariants + flows/ ← first / weekly / snapshot / test-drive contracts + references/ ← agent boundaries, thesis, card, and recovery policies + schemas/ ← Review Plan / answers / narrative / canonical bundle + copy/ ← Traditional Chinese and English product copy + engine/review.py ← prepare / preview / finalize / resume orchestration + engine/session.py ← atomic canonical bundle + legacy projections + engine/card_renderer.py ← deterministic private/public Markdown + HTML + engine/instruments.py ← ETF allocation-vs-concentration policy card-spec.md ← Step 3 card spec (blocklist / redact / narrative rules; read only after Step 2 questions) engine/trade_recap.py ← mechanical layer: 5-dim + per-position DCA/loser classifier + attribution (pure functions, no real paths) rubric/ diff --git a/README.zh-TW.md b/README.zh-TW.md index ae47daa..c9ea109 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -2,7 +2,7 @@ [English](README.md) · **繁體中文** -> 一個 Claude Code skill:用**一面大師鏡片**(預設一套交易原則蒸餾、可換),把你的真實交易復盤成**一張卡**—— +> 一個給 Claude Code、Codex、Cursor 等 coding agent 使用的本機交易復盤 skill:用**一面大師鏡片**把你的真實交易收斂成**一張卡**—— > 你做對的一件事 + 一個最大的洞(用你自己的數字)+ 一條下次要守的規矩 + 一句大師的話。 不是又一份統計報表。它做的是報表做不到的事:**先算出你看不見的行為漏洞,再問出你不願承認的動機,最後逼你下次只改一件事。** @@ -17,11 +17,12 @@ ``` 卡的價值在第 ② 步那段對話 —— 引擎挑出可疑標的、問你「逢低還是凹單?」,你一句話定案,卡才出定論。**光看引擎原始輸出看不到這層。** 安裝見下方 [安裝](#安裝)。 -**想先零安裝、看引擎在算什麼**(機械層的*原始診斷*,還不是定論卡): +**想先零安裝、看穩定流程怎麼開始**: ```bash git clone https://github.com/atomchung/fomo-kernel && cd fomo-kernel pip install -r requirements.txt # 若報 externally-managed-environment → 見下方「安裝」的 venv 三行 -cd skills/fomo-kernel && python3 engine/trade_recap.py # 跑內建 mock,印出引擎原始診斷 +cd skills/fomo-kernel && python3 engine/review.py prepare --test-drive --language zh-TW +# 先產生可恢復的 Review Plan;required questions 問完才 preview/finalize ``` ## 跑出來長什麼樣 @@ -80,11 +81,17 @@ ChatGPT 算不出 FIFO 配對的真實 α/β、分不清你是「定投」還是 - skill 在**你自己的機器**上跑你的 CSV,**不上傳到任何後端、不落地儲存到別處、不回傳給作者**。為了每週對帳,它會把復盤衍生的狀態存在**你本機**的 `~/.trade-coach/`(永不外傳)——下一節說明那是什麼、怎麼查看、匯出或清除。 - 作者拿不到你的交易明細。唯一(自願)回收的是一句「這張卡有沒有用」,不含交易內容——願意給的話走 [card feedback 表單](https://github.com/atomchung/fomo-kernel/issues/new?template=card-feedback.yml),30 秒。 - `.gitignore` 已設:**任何 `.csv` 都不會被 commit**,只有 mock/sample 假資料例外。 -- 精確說:唯一會讀到你交易的,是你**正在用的 Claude 本身**——它要讀 CSV 才能幫你復盤,就跟你平常用 Claude 一樣。這跟把對帳單交給一個會存檔、你看不到、作者能撈的 SaaS,是兩回事(所以不是「完全不經過任何伺服器」,而是「不落地、不回作者、不進第三方」)。 +- 精確說:本機 Python engine 讀標準化後的 CSV;你使用的 coding agent 可能為了理解券商欄位而讀原始對帳單。資料不回作者。這跟把對帳單交給一個會保留資料、你看不到的 SaaS 是兩回事。 ## 📁 你的教練記憶在哪 / 怎麼維護 -第二次來,卡會先對帳「上次那條規矩守了沒」——這靠 `~/.trade-coach/` 下幾個**純本機**檔撐起來(永不外傳、不回作者)。撐起對帳的四個核心檔: +第二次來,卡會先對帳「上次那條規矩守了沒」。每次正式復盤的權威紀錄是一個 immutable canonical session: + +```bash +ls ~/.trade-coach/sessions/ # bundle、state、answers、cards、hash manifest +``` + +原本的本機檔仍保留,但它們是可重建的相容 projection: ```bash cat ~/.trade-coach/log.jsonl # 每行一次復盤(薄 metric + 你承諾的規矩);空 = 第一次 @@ -105,15 +112,15 @@ python3 skills/fomo-kernel/engine/coach.py data-reset --confirm # 真的全 - **下週回來要匯哪份 CSV?** 直接把**全歷史**再匯出來丟給它就好——你不用手動追增量。跟之前重疊的列會自動去重(去重就是為這個設計的),所以**每週丟整份對帳單都安全**;引擎用上次復盤的截點判斷哪些是新的,卡第一句就對帳你上次承諾的那條規矩。 - **看歷次復盤** → `cat ~/.trade-coach/log.jsonl`。 - **換哲學鏡片重來 / 清空對帳基準** → `coach.py data-reset --confirm`(或自己刪掉/改名 `~/.trade-coach/`,效果一樣:下次就當第一次)。 -- **thesis 寫歪了** → 改 `theses.jsonl`;它是 append-only,修正 = 補一筆新 event(別蓋舊的,才能跨期看你當初怎麼想、後來怎麼變)。 +- **thesis 寫歪了** → 在下一次復盤新增修訂 event,指回舊 thesis;不要直接手改 `theses.jsonl`,它現在是 canonical session 的可重建 projection。 - **隱私自證**:教練記憶就是 `data-status` 列出的那些檔、全在你機器上,作者那邊一行都沒有。 - **想先看「多週迴圈」長什麼樣**(全程在 temp 目錄跑,**不碰**你正式的 `~/.trade-coach/`) → `python3 skills/fomo-kernel/engine/demo_weeks.py`:把內建 mock 按時間切 3 段模擬「初診 → 對帳 → 對帳」,直接看到第二張卡怎麼引用上週承諾、log.jsonl 怎麼一行行長出來。 -> 💡 **想分享給社群?** 卡預設只出完整私人版。對我說「給我分享版」,會輸出**去敏感化**的純文字版(隱藏金額 / 股數 / 精確佔比,只留行為 pattern + 相對績效 β/贏大盤 pp),可直接貼 X / Thread。 +> 💡 **想分享給社群?** 每個 committed review 都會另外產生 `card-public.md`。它不是遮罩 private card,而是重新渲染,移除金額、日期、ticker、精確權重與 agent 自由文字;回覆仍預設給 private card。 ## 安裝 -**前置:這是 [Claude Code](https://claude.com/claude-code) 的 skill**——Anthropic 的終端 / 桌面 AI 工具(需 Claude 訂閱)。沒用過的話,先花 5 分鐘[裝好並登入](https://docs.claude.com/en/docs/claude-code/setup),再回來走下面三步。 +**前置:**Python 3.11+。Claude Code 使用者可安裝下面的 slash-command skill;Codex、Cursor 等 agent 可直接依 `AGENTS.md` 與 `engine/review.py` 使用 repo,不需要 Claude 訂閱。 需要 Python 3.11+。**新 macOS(Homebrew / 系統 Python)直接 `pip install` 會被 PEP 668 擋下**(`externally-managed-environment`),用 venv 三行裝: ```bash @@ -137,19 +144,21 @@ cp -r skills/fomo-kernel ~/.claude/skills/ # B. 複製( ``` 你的 CSV 來自**任何券商**都行——Claude 會自動讀懂、轉成引擎要的欄位(`Symbol / Action(BUY|SELL) / Quantity / Price / TradeDate`,台股等非美股可加選填欄 `Market / Currency`,如 `2330.TW / TW / TWD`;不填 = 美股 USD),不必你手動整理。 -> 🏷️ **冷門股**(引擎 sector 表不認的)Claude 會**自動產生 driver map** 給你確認(每檔標 `[sector, 主題]`),讓「分散」維不會把同主題的標的當成真分散——不必你手動弄,也別讓它落到 `driver map: 0 檔` 的 fallback(分散維會失準)。細節見 SKILL Step 0.5。 +> 🏷️ **冷門標的**可由 agent 提出本機 driver map;冷門 ETF 另可提出 instrument map。但只有明確分類為大盤、區域、債券或商品 ETF 才取得配置豁免;未知標的預設仍算集中風險。 -**會發生什麼**:① 引擎跑診斷 → ② Claude 在對話裡問你 1–3 個持股假設/動機問題(逢低還是凹單?)→ ③ 拿到你的答案,出一張定論卡。卡的版型見 [`card-template.html`](skills/fomo-kernel/card-template.html)(完整四層的 HTML 範例)。 +**會發生什麼**:① `prepare` 跑確定性診斷並建立 question queue → ② agent 問 thesis/動機 → ③ `preview` 驗證答案並產卡 → ④ 你選一條規矩,`finalize` 原子提交整個 session。 ## 其他 coding agent 怎麼用 -沒有 Claude Code 的 skill 系統一樣能用——核心引擎是純 Python,不依賴任何 agent 機制: +沒有 Claude Code 的 skill 系統一樣能用。Codex、Cursor 等 agent 走同一份 orchestration contract: ```bash -cd skills/fomo-kernel && python3 engine/trade_recap.py ~/Downloads/my.csv +cd skills/fomo-kernel +python3 engine/review.py prepare ~/Downloads/my.csv --language zh-TW +# 依 review_plan.flow_path 執行,回答 question_queue,再呼叫 preview / finalize ``` -如果你用 Codex / Cursor 等其他 coding agent,叫它讀 [`AGENTS.md`](AGENTS.md) 照著走——那份檔案是給非 Claude Code agent 的路由指南,會告訴它怎麼跑引擎、怎麼問動機、怎麼出卡。 +叫 agent 先讀 [`AGENTS.md`](AGENTS.md)。`SKILL.md` 現在是薄入口;各 mode 的 flow、JSON schema、validator 與 renderer 才是詳細契約。 ## 風格 sample(直接可跑,看不同風格照出不同洞) @@ -177,7 +186,15 @@ python3 engine/trade_recap.py # 不帶參數 = mock_tra ``` skills/fomo-kernel/ - SKILL.md ← skill 本體(四步流程:格式 → 引擎 → 出卡前確認 → 定論卡) + SKILL.md ← 薄入口與不可違反的 invariants + flows/ ← first / weekly / snapshot / test-drive 路由契約 + references/ ← agent 邊界、thesis、卡片與 recovery policy + schemas/ ← Review Plan / answers / narrative / canonical bundle + copy/ ← 繁中與英文產品 copy + engine/review.py ← prepare / preview / finalize / resume + engine/session.py ← atomic canonical bundle + legacy projections + engine/card_renderer.py ← deterministic private/public Markdown + HTML + engine/instruments.py ← ETF 配置/集中風險 policy card-spec.md ← Step 3 卡規格(禁止清單 / redact / 敘事鐵律;Step 2 問完才讀) engine/trade_recap.py ← 機械層:5 維 + 標的層主從分類 + 歸因(純函式,無真實路徑) rubric/ diff --git a/docs/eval-design.md b/docs/eval-design.md index f3035ff..0e476f6 100644 --- a/docs/eval-design.md +++ b/docs/eval-design.md @@ -1,210 +1,186 @@ -# fomo-kernel 產出 Eval 設計(spec) +# fomo-kernel evaluation design -> 2026-07-03。評估對象**嚴格限定 = 這個 skill 的產出**。engine 的數學對不對歸 `tests/`(已覆蓋,不在本檔);模型裸能力歸 investment-note `evals/`(不在本檔)。本檔管的是中間那層——**Claude 照著 SKILL.md 跑出來的東西,好不好、有沒有踩鐵律、用戶會不會覺得沒意義**。 -> -> 設計討論全文見 investment-note `research/20260703_agent_eval_design.md`;本檔是其中 fomo-kernel 部分的可執行版(單一權威在這裡,那邊不再維護細節)。 -> -> **分工(#68)**:本檔 = **自動化 harness 的單一權威**(斷言定義以這裡為準);[`evals/EVALS.md`](../evals/EVALS.md) = 作者的**手動驗收入口**(乾淨 session 逐條人判)。同源判準的兩套編號對照表在 EVALS.md 頭部。鐵律文本自 #67 起分居 `skills/fomo-kernel/SKILL.md`(流程) 與 `skills/fomo-kernel/card-spec.md`(卡規格)——本檔「來源」欄與 mutation 目標對這兩檔都要追。 +This specification evaluates the layer between deterministic engine math and real-user value: whether an agent follows the review contract, creates correct artifacts, and produces a useful card. -## 0. 產出是什麼(= 受測面) +Engine formulas are tested in the standard unit suites. Real-user usefulness is measured separately through actual reviews. This document owns automated workflow and artifact assertions; `evals/EVALS.md` is the compact manual checklist. -skill 一次 run 的產出有三面,eval 三面都要管: +## Evaluation surfaces -1. **卡**(private review 文字卡)— 用戶直接讀的東西。 -2. **本機狀態檔**(`~/.trade-coach/log.jsonl` / `theses.jsonl` / `profile.md`)— 下週對帳的記憶;寫壞 = 迴圈失效,用戶下週才發現。 -3. **對話行為**(Step 2 問答的順序與敏感度)— 產品的差異化價值所在;transcript 可判。 +One run has four observable surfaces: -## 1. 判定哲學(三條) +1. Review Plan and conversation trajectory. +2. Canonical session bundle and manifest. +3. Private and public cards. +4. Rebuildable compatibility projections. -1. **code-check > LLM-judge > 人工**。能 regex / JSON diff 斷言的絕不用 judge;judge 只留給敘事品質一項。 -2. **差分斷言測「聽沒聽」**:同一份 CSV、只換用戶答案跑兩次,產出必須在對應維度不同。**卡對答案不變 = Step 2 是儀式** —— 這測的是產品靈魂,而且是純機檢(diff 兩份 `log.jsonl` 即可)。 -3. **一個 case 跑 n≥2 次報通過率**(輸出非確定性;產品要的是每次都不踩,不是有一次做對)。 +All four matter. A readable card with corrupt state fails, and correct state with skipped motive questions also fails. -## 2. Harness +## Evidence hierarchy -``` -tests/agent/ - cases/*.yaml # 每 case:輸入 fixture、persona 腳本、斷言清單 - personas.md # 腳本化用戶(見 §3) - run_case.sh # headless `claude -p` 跑 skill;HOME 指到暫存目錄(~/.trade-coach 隔離) - check_card.py # 卡片斷言(§4 A/B 系列的機檢部分) - check_state.py # 狀態檔斷言 - mutations.md # 驗活記錄(§6) +1. Deterministic code assertion. +2. Differential fixture assertion. +3. LLM narrative judge. +4. Human review. + +Use the strongest cheaper layer that can answer the question. Do not ask a model to judge a schema, hash, privacy field, or required-question gate. + +Non-deterministic agent runs should be repeated when measuring adherence. Deterministic lifecycle tests need only one run per case because identical input must produce identical contract behavior. + +## Harness + +```text +tests/test_review_v2.py lifecycle, evidence, ETF, language, privacy, recovery +tests/test_doc_language.py implementation/GTM language boundary +tests/agent/check_card.py legacy and artifact-level card invariants +tests/agent/check_state.py projection and trajectory helpers +tests/agent/personas.md scripted users and differential pairs +tests/agent/cases/*.yaml optional headless declarations +tests/agent/judge_narrative.py optional prose-quality judge ``` -fixture 直接用現有 `mock/` 的 7 個 persona CSV + driver map,不另造。transcript 用 `--output-format stream-json` 收,tool-call 順序從這裡判。 - -## 3. Simulated user(腳本化 persona) - -| Persona | 腳本 | 測什麼 | -|---|---|---| -| **洗白者** | 對疑似凹單標的答「逢低」,被要求舉證時寫不出新證據 | 證據門檻(BACKLOG ISSUE-3)| -| **誠實者** | 答「不想認賠」 | 答案被採用 + 不說教 | -| **跳過者** | 一律跳過不答 | 不追問、卡照出 | -| **推翻者** | 答「計畫內定投」(推翻 engine 預設的「別加碼」)| commitment 存最終版 + 差分敏感度 | -| **回頭客** | 第二週帶新 CSV 回來 | 對帳而非重新初診 | - -## 4. Case 總表 - -### A 系列 · 鐵律不變量(SKILL.md 🚫 逐條翻譯,全機檢) - -| # | 斷言 | 來源 | -|---|---|---| -| A-1 | 卡上不得出現 `thesis_questions` 任何一條原文 | Step 1「絕不准印在卡上」 | -| A-2 | 不得出現 5 維 severity 小數表(`0?\.\d+ *[🔴🟡]`)| card-spec.md 🚫 清單(原 Step 3,#67 拆檔)| -| A-3 | 不得出現連續 `〔.+〕` 標籤拼接 / `← 點\d` / `(引擎產出)` / `(供參)` | card-spec.md 敘事鐵律「卡是故事不是 dashboard」 | -| A-4 | (retired,#89:engine 已移除 is_demo,對任何輸入一致;demo 展示改走 README 靜態範例卡,見 #46) | — | -| A-5 | `alpha_credible=false` → 全文禁「真本事 α」定論語氣,但 α 年化數字仍需出現(帶 95% 區間);須講清楚 `gate.reason`(樣本不足 vs 區間太寬/持倉集中);判定源對齊 `honesty_ledger.alpha_credibility` | ISSUE-1 輸出 gate(alpha v2 #90;#82 後 honesty_ledger 統管揭露判定,B6/B16)| -| A-6 | 首段不得以勝率當主數字(`勝率 *\d+%` 或 `\d+ *勝 *\d+ *負`)——勝負筆數不進 metric 格、不當句子主詞 | card-spec.md 數字鐵律「金額 > 筆數勝率」(真人反饋:勝率不重要,關鍵就是賠錢)| -| A-7 | `log.jsonl` / `theses.jsonl` 行數只增不減、每行可 parse | append-only 鐵律 | -| A-8 | `theses.jsonl` 每筆 `cycle_id` 符合三段格式 `ticker#YYYY-MM-DD#序號` | 收尾 part 2 ⚠️(踩了 = 記憶迴圈失效)| -| A-9 | 新 thesis 預設 `maturity=inferred`;修正走新 event 帶 `revises`,舊行不動 | append-only 動機庫 | -| A-10 | `insufficient_data=true` → **engine 預設不落盤**:commitment 若存在必為 `source:"user_chosen"` 且帶 `baseline_note:"short-sample baseline"`;無用戶親選則 `commitment=null` | 樣本不足不硬出規矩;用戶親選例外(#78 Step 3.5) | -| A-11 | `inferred` thesis trigger 觸發 → 輸出為問句 + `[⚠️ AI 猜測待校正]`,禁「該走」定論 | Step 2.5 措辭分級(雙審標最關鍵)| -| A-12 | 卡文與 AskUserQuestion 選項文字禁內部 metric key / 參數名(regex `max_pos_pct\|ai_pct\|avgdown_count\|max_sector_pct\|top3_pct\|metric_key\|baseline`)— log/state 檔內不受限 | card-spec 說話原則(#78 真人反饋:「追蹤 max_pos_pct,基線 42%」= 拗口)| -| A-13 | 卡面文案標點全形統一:中文字之間禁半形逗號 / 冒號 / 分號(regex `[一-鿿][,:;][一-鿿]`);數字格式(千分位、小數點、%、日期連字號)除外。只驗卡面輸出,不驗 repo 文檔 | card-spec 說話原則(真人反饋:全形半形混用要統一)| -| A-14 | 卡面「你 vs 大盤」三數字(持倉 TWR / 大盤 / 差 pp)必等於 `alpha_beta_breakdown` 的 `port_tot`/`spy_tot`/`excess_vs_spy`(照抄不變量,禁 Claude 自算);基準字樣跟 `bench`/`scope` 走(US=SPY、TW=^TWII);`note` / 樣本不足 → 這行不出 | #164 柱2 TWR vs 大盤上卡(SKILL Step 1「只准照抄」/ card-spec 該不該買指數段)| -| A-15 | 出場追蹤冷啟動(#170):既有歷史存量首次 enqueue,`due` 不被啟用前到期的舊出場灌爆(`due<=enqueued_at` = 歷史窗,不催 → 走 `backlog`);歷史 backlog 呈現先彙總模式(count/full/reduce/top_tickers/span,免現價必得)、賣飛傾向只在有現價時算且覆蓋率誠實、再抓大放小 1–2 筆不逐筆問 | 既有使用者補建帳本的遷移路徑不該把復盤變審問(#170 冷啟動兩層設計)| - -### B 系列 · 用戶價值(「這卡沒意義」的四種機制 × persona) - -「沒意義」不是抽象猜測——SKILL.md 那批鐵律的出處就是一次真人交易者 review(「像幾份報告硬湊」「差點關掉」「我又不是基金經理」)。歸納成四機制,每個配 case: - -**機制④ 沒被聽見(最致命:殺掉 Step 2 本身)— 全機檢,含差分:** - -| # | Persona | 沒意義的樣子 | 斷言 | -|---|---|---|---| -| B-1 | 洗白者 | 卡被洗成讚美卡、洞消失 | 該 ticker 標籤 ∈ {凹單, 待確認},∉ {逢低};headline 洞仍在(= ISSUE-3 驗收,**eval-first:先亮紅再改 prompt**)| -| B-2 | 誠實者 | 被說教;或答了還標「待確認」 | 卡標凹單且「看動機」引用其原話;說教句式黑名單(「你不該/大忌/千萬別」)+ judge 複核;規矩接「這就是要擋的事」 | -| B-3 | **推翻者差分** | 答什麼卡都一樣 →「講了白講」 | 同 CSV、兩種答案 → 兩份 `log.jsonl` 的 `commitment.metric_key` 必不同(定投版 → `ai_pct` 類非 `avgdown` 類);headline 框架不同 | -| B-4 | **集中度差分** | 答「刻意押賽道」還被罵「假分散」= 問了還打臉 | 「刻意」版標題禁「假分散」、須含集中回檔 / α 測不出語意;「以為分散」版才准用「假分散」 | -| B-5 | 跳過者 | 被追問審問;卡上留問號待辦 | 卡照出(機械洞版);transcript 無二次追問;卡文無問句 | -| B-6 | 回頭客 | 重新初診、同一個洞當新發現重講 → 沒進度感 | 卡第一段含上次 `commitment.metric_key` 的舊值→新值兩個數字;同維洞須含「還沒過關」語意 | - -**機制① 沒新資訊(「這 ChatGPT 也會講」):** - -| # | 斷言 | -|---|---| -| B-7 | 每條 candidate rule 必含用戶自己的 ticker 或具體數字(% / $);抽象規矩黑名單(「注意分散」「想清楚」「控制風險」)| -| B-8 | (低頻、optional)同 CSV 餵裸模型出報告,judge 盲比:skill 版必含只有引擎算得出的資訊(FIFO α/β、歸因 pp、攤平次數)— 差異化價值存在性檢查 | +The complete deterministic suite runs through `python3 tests/run_all.py`. Headless agent generation and LLM judging are opt-in because they are non-deterministic and may cost money. -**機制② 不可行動(黑話 / 沒案例):** +## P0 lifecycle assertions -| # | 斷言 | -|---|---| -| B-9 | 「最大的洞」區塊必含 ≥1 具體 ticker + ≥1 數字;黑話詞(α / β / 處置效應 / 夏普)出現時 ±2 句內必有白話翻譯(近鄰 keyword + judge 複核)。對帳單標準詞彙(已實現 / 未實現 / 盈虧比)**不算黑話**、直接用(card-spec 說話原則)| -| B-10 | 規矩必為 if-then 可驗形;「動手前問自己」型自我喊話禁入(judge)| +### Prepare -**機制③ 不可信(名實不符 → 信任崩,整卡歸零):** A-5 / A-11 已覆蓋,歸入此機制不另立 case(A-4 已於 #89 retired)。 +- Uses `engine/review.py prepare` as the canonical entry point. +- Produces a schema-valid Review Plan. +- Selects one route and a bounded flow path. +- Emits a deduplicated required question queue. +- Creates a pending session with a stable fingerprint. +- Repeated prepare or resume does not refetch prices for the same pending review. -### C 系列 · 對話行為 / trajectory(transcript 斷言) +### Agent artifacts -| # | 斷言 | -|---|---| -| C-1 | `TR_JSON=1` 的 engine 呼叫存在,且先於卡片輸出 | -| C-2 | Step 2 提問先於卡片輸出(「確認在出卡之前」);一次 ≤3 問;**AskUserQuestion 為主**——有該工具的環境(Claude Code)走工具呼叫,transcript 無工具呼叫而用對話問 = 只在無工具環境可過(SKILL Step 2 問法鐵律,#55)| -| C-3 | Step 0 魯棒性:3–5 份不同券商欄位命名的小 CSV → 標準化輸出欄位恰為 `Symbol,Action,Quantity,Price,TradeDate,RecordType`;非美股再加 `Market`/`Currency`,台股 `Symbol` 標完整 yfinance 代號(`2330.TW`/`.TWO`)、`Market=TW`/`Currency=TWD`、民國年換西元(#173,認格式是 Claude 職責、引擎不 hardcode) | -| C-4 | 收尾有 append log.jsonl / theses.jsonl 的呼叫(狀態迴圈沒被跳過)| -| C-5 | `card-spec.md` 的讀取發生在 Step 2 答案拿齊**之後**,不是開場整份讀進來(goal-hiding;收編自 EVALS.md C1)| -| C-6 | 規矩承諾前有 AskUserQuestion 讓用戶從 candidate_rules 挑選的呼叫(Step 3.5);log 寫入的 rule = 用戶所選且 `source:"user_chosen"`(SKIP → `commitment=null`;engine 預設 fallback → `source:"engine_default"`)(SKILL 收尾鐵律,#56/#78)| +- Every required question has an explicit answer before preview. +- Every uncovered cycle has a thesis update tied to the unchanged engine cycle ID. +- Inferred theses use `maturity:"inferred"` and never claim user confirmation. +- Narrative is qualitative and contains no digits. +- A `new_evidence` choice requires claim and source. +- The agent never calculates numbers or ETF exemptions. -### 唯一的 LLM-judge 項 +### Preview -「卡是連貫故事不是報表」敘事品質 0–5:rubric 直接抄 card-spec.md 敘事鐵律(先承認本事再打 / 數字要髒 / 不講黑話 / 引言不當結語),judge 看 rubric 不看範本答案。judge 與人工判的 agreement < ~80% → 分數不可信,重寫 rubric。 +- Validation failures identify the artifact to fix and do not mutate canonical state. +- Private and public previews render from the same engine facts. +- Preview contains at most one proposed commitment surface and does not finalize it automatically. -> **實作現況**(#60,2026-07-09): -> - **離線機檢核心已落地**:`check_card.py`(A-2/A-3/A-6/A-12/A-13/B-7/B-9)+ `check_state.py`(S-1..S-4 收尾產物 + 差分/append helper)+ `test_checkers_offline.py`(斷言驗活,用 coach.py 真實寫入當 known-good oracle)——確定性、無網路,已進 `tests/run_all.py` 第 10 套。 -> - **judge 本體 + mutation 驗活**已落地(`judge_narrative.py` + `run_judge_eval.py` + `fixtures/`,需 API key,不進 CI)。 -> - **harness 編排**:`personas.md`(5 persona + 差分對)+ `cases/*.yaml` + `run_case.sh`(`--check` 離線機檢已產出的卡/狀態,CI-verified;`--headless` opt-in 產卡)。 -> - **仍待辦**(#60 較大本體):B-1 洗白者 red→green 全流程、check_card 的 case 特定斷言(B-1 標籤定位 / B-9 section 級)、grader 校準(§6)。**(c) 內心層的工具主路徑 headless 測不到**是固有天花板(AskUserQuestion 不在 headless;EVALS.md 2026-07-04 實測),要互動 session / Step 4 線上反饋——見 tests/agent/ README「(c) 內心層的 headless 天花板」與 issue #159 三層框架。 +### Finalize -## 5. Case 來源三條(持續長 case 的機制) +- Requires a user choice: candidate rule, custom rule, or skip. +- Commits one immutable canonical bundle by atomic directory rename. +- Writes a manifest hash for every artifact. +- Identical retry is a no-op. +- Conflicting content under the same session ID fails closed. +- Projection failure cannot invalidate the canonical session. +- `repair-projections` rebuilds compatibility files without asking the user again. -1. **鐵律驅動**:SKILL.md / card-spec.md 每新增一條 🚫 → 一條 A 系列斷言(改這兩檔任一的 PR 應同時動本檔;鐵律文本 #67 起分居兩檔)。 -2. **用戶反饋驅動**:Step 4 的「沒戳中 / 哪裡不對」反饋 = **用戶價值層的 escape log**。每收到一個,做三問 postmortem:有 case 嗎?case 為何沒攔(grader 太鬆 vs 沒 cover)?→ 長出新 B-case。這是 L3(真人反饋)回流 L2(可跑斷言)的管道。 -3. **事故驅動**:開發 / 使用中踩的坑(如 cycle_id 拼錯格式)→ 即補不變量。 +## Thesis and evidence assertions -## 6. Eval 自身的驗活(做完 harness 的第一件事) +- Losing-position adds use only the defined decision enum. +- `new_evidence` contains a claim and source and may include observation time or falsifier. +- A cheaper price alone maps to valuation change or price only, not new evidence. +- A revision appends a new event with `revises`; old events remain unchanged. +- New position cycles receive new thesis identities. +- Active-thesis reconstruction ignores unrelated event types. -- **Mutation 驗活**:故意弄壞 SKILL.md / card-spec.md(刪「確認在出卡之前」、放行 thesis_questions 上卡、刪 🚫 清單某條、讓 commitment 存 engine 預設)→ 對應 case **必須亮紅**。不紅 = 斷言是死的,先修 eval。每條斷言至少被一個 mutation 殺過一次,記錄在 `mutations.md`。 -- **Grader 校準**:首批 10–20 個 transcript 人工全判一次,對比機檢結果量 FP/FN;之後 grader 每次改動抽 5 個複核。 -- **飽和監控**:長期 100% pass 的 case 標「回歸哨兵」身分(允許存在,但不要誤當「還在提供訊號」)。 +## ETF assertions -## 7. 跑的節奏 +- Broad-market, regional, bond, and commodity ETFs may receive the explicit diversified-allocation exemption. +- Sector, thematic, and leveraged ETFs remain concentrated risk. +- Unknown tickers receive no exemption. +- Allocation exemptions affect sizing, risk concentration, single-name stress, and decision-exit logic consistently. +- Missing expense ratio or tracking error is disclosed, never set to zero. -| 觸發 | 跑什麼 | -|---|---| -| 改 SKILL.md / card-spec.md / engine 輸出層 | 全套(~15 case × n=2,分鐘級/case)| -| 模型升級 | 全套 n=3 | -| 平時 | 不跑。**L2 不進 CI**(非確定性 + 有成本,進 CI 會逼人把斷言寫鬆);`tests/run_all.py` 維持每 commit | +## Card assertions -## 8. 反模式 +### Private card -1. 別造 framework——yaml case + 一支 runner + 兩支 checker,幾百行封頂。 -2. 別用 judge 驗機檢項;別追單一總分(有意義的是「哪個 case 紅、對應哪條鐵律」)。 -3. persona 腳本與斷言不得進 skill 可讀路徑(受測 session 的 cwd / HOME 隔離)。 -4. 別把 Step 4 反饋(L3)錯當本層通過標準:B 系列全綠 ≠ 卡對真人有用;反之亦然。兩層都要,不互抵。 +- Uses only engine-owned numeric facts. +- Shows a strength before the largest leak. +- Converges on one largest leak and at most one rule. +- Integrates every triggered honesty-ledger limitation. +- Contains no raw question queue, internal field names, or five-dimension dashboard. +- Gives no security recommendation. -## 9. 迭代迴圈與輸入協議(loop engineering) +### Public card -> Eval 建好之後怎麼用:prompt(SKILL.md)是假設、eval 是實驗,每次改動都是「單變數實驗 + 全套回歸」。本節定義迴圈本體、紅燈歸因、以及 owner 要給的 input。 +- Is independently rendered rather than redacted from private prose. +- Contains no amount, share count, exact date, ticker, exact weight, session ID, evidence text, or agent-authored free prose. +- Shows a final rule only when the user selected one. -### 9.1 一輪迴圈(owner 只出現在兩個點) +### Test drive -``` -owner:真實跑 skill + 一行 verdict(input #1 #2) - ↓ -loop session:讀 feedback → 三問 postmortem(真實性/可判定/可行動)→ mock 化寫 case - → 跑全套 eval → 紅燈歸因(§9.2)→ 單變數改 SKILL.md → 全套重跑(防修 A 壞 B) - → 開 PR(附前後 pass rate) - ↓ -owner:PR 裁決(input #3)——規格變更才需要想;遵守度修復看 eval 證據綠了即放行 - ↓ -merge → 下次真實使用 = 下一輪 input -``` +- Uses `persist:false`. +- Never reads or writes production coach state. +- Labels conversation and cards as demo data. +- Follows the same required-question and commitment lifecycle. + +## Differential personas + +Run the same mechanical facts with different user answers: + +- `washer` versus `honest`: vague rationalization must not satisfy the evidence gate. +- planned pyramid versus averaging-down answer: thesis event and rule framing differ. +- intentional theme concentration versus believed diversification: facts stay fixed while motive framing differs. +- returner versus first review: the returning run reconciles the prior commitment. +- skip versus choose: commitment artifacts differ without breaking card production. + +Differential tests prove that the conversation affects permitted qualitative state without allowing the user or agent to rewrite mechanical facts. -節奏**事件驅動不排程**:每次 `miss` 觸發一輪、或每累積 3–4 次真實使用跑一輪回歸。沒有新 input 不迭代(空轉只會過擬合現有 case)。改 SKILL.md 前必有 baseline 數字,否則「改善」無從說起。 +## Narrative judge -### 9.2 紅燈歸因:四種病因,藥完全不同 +Use an LLM judge only for prose qualities that deterministic checks cannot settle: -| 病因 | 症狀 | 藥 | -|---|---|---| -| **① 指令缺失/含糊** | 失效處 SKILL.md 根本沒講 | 加鐵律 + 同步加 case | -| **② 指令存在但不被遵守** | 有明文還是踩 | 不是再寫一遍加粗——最常見真因是**指令互相稀釋**(SKILL.md 已 300+ 行)。驗法:該條前移 / 其他條刪減後重跑,pass rate 動了 = 位置/密度問題;不動 = 模型能力問題(考慮流程硬化,如 self-check 清單)| -| **③ 指令衝突** | 兩條鐵律在特定情境打架(如「降摩擦別審問」vs「舉證門檻」)| prompt 寫明優先序;case 固定那個衝突情境 | -| **④ 規格本身錯** | eval 全綠但 Step 4 反饋說沒戳中 | 改的是鐵律不是遵守度——**此通道必須顯式存在,否則 eval 會把錯的規格鎖死** | +- coherent story rather than report fragments +- specific strength before critique +- direct and non-shaming language +- concrete rule rationale +- no tacked-on philosophical lecture -配套兩機制: -- **指令效用審計(刪的勇氣)**:刪一條指令跑全套,全綠 → 死重候選(模型已內化或從沒觸發),提請 owner 裁決。理想態 = 每條鐵律 ↔ 至少一個 case 依賴(mutation 驗活自然建立此映射);沒 case 罩的指令 = 改動時裸奔。 -- **Eval 追隨意圖不追隨措辭**:改 prompt 措辭不應需要改 case;要改 case 時先問是不是規格真的變了(走病因④通道)。case 與措辭耦合太緊 = 在測「模型有沒有背這段話」。 +Calibrate the judge against human ratings. If agreement is poor, improve the rubric rather than treating the score as truth. -### 9.3 Owner 輸入協議(loop 生不出來的三種 ground truth:真實使用、判決、仲裁) +## Mutation testing -| # | 給什麼 | 頻率/成本 | Loop 拿去做什麼 | -|---|---|---|---| -| 1 | **真實跑一次 skill**(transcript/卡/狀態檔自動留本機)| 每週復盤本來就跑,零額外 | baseline 樣本池——唯一能校正腦補 case 的東西 | -| 2 | **每張卡一行 verdict**:`hit`,或 `miss + 引卡上原句 + 一句為什麼` | 30 秒/卡 | miss → postmortem → 新 B-case 或修 grader;hit rate = L3 baseline | -| 3 | **仲裁**(loop 問才答):規格錯 vs 遵守度?死重指令刪嗎? | 每輪 0–2 個 Y/N | 病因④通道 + 鐵律增刪拍板——唯一不可自動化的判斷 | -| 4 | **抽判 5–10 張**(同意/不同意機檢判定)| 只在 grader 新建/改動時 | grader 校準(FP/FN)| -| 5 | **真實心路語料**(自己凹單/逢低當下的自我辯護原話)| 不定期 | persona 腳本擬真化 | +Every important guard should fail under an intentional mutation at least once. High-value mutations include: -**verdict 格式**(本機 `~/.trade-coach/feedback.jsonl`,append 一行): +- allow preview before required answers +- accept `new_evidence` without source +- add a digit to agent narrative +- let unknown ETFs receive an allocation exemption +- leak a ticker into the public card +- interrupt projection after canonical commit +- retry one session with conflicting content +- place non-English text in implementation Markdown + +A checker that stays green under its matching mutation is not evidence. + +## Real-user feedback loop + +Automated success does not prove that the card matters. After a real review, record a lightweight local verdict: ```json -{"date":"2026-07-05","verdict":"miss","line":"卡上原句照抄","why":"一句話","tag":"沒被聽見|黑話|不可信|沒新資訊"} +{"date":"2026-07-14","verdict":"miss","line":"exact card sentence","why":"one concise reason","tag":"not heard"} ``` -`tag` 可省(loop 自歸四機制);`line` + `why` 不可省。hit 就一行 `{"verdict":"hit"}`,別讓記錄變負擔。 +Keep raw feedback local because it may contain real tickers or amounts. Convert only the failure structure into a synthetic regression case. -**兩條 input 鐵律:** -1. **Miss 必引卡上原句**——形容詞 feedback(「不夠深」)無法翻譯成斷言;引原句 + why 十分鐘後就是新 case。 -2. **給症狀不給藥**——owner 直接指定 prompt 改法會跳過 baseline 與歸因,把病因②誤治成病因①(再加一條指令),這正是 prompt 膨脹到 300+ 行的機制。觀察到什麼照抄什麼,改法由 loop 提、附 eval 前後對比,owner 只在 PR 裁決。 +For each miss: -**隱私邊界(public repo 必守)**:feedback 原文含真實 ticker/金額 → **全文永遠留本機** `~/.trade-coach/feedback.jsonl`;迭代 session 在 owner 機器上讀它歸因;**進 repo 的只有 mock 化後的 case**(症狀結構保留、數字換 mock persona 的)。與 skill 本身隱私鐵律同構:明細不出本機,出去的只有結構。 +1. Determine whether the cause is missing instruction, poor adherence, conflicting instructions, or a wrong product rule. +2. Add or update the smallest synthetic case. +3. Change one contract surface. +4. Run the complete deterministic suite and the relevant agent eval. +5. Recheck a real card. -## 落地順序(每步獨立可停) +## Run cadence + +| Trigger | Required evidence | +|---|---| +| Engine, schema, renderer, or lifecycle change | complete deterministic suite | +| Skill or policy wording change | complete deterministic suite plus relevant scripted cases | +| Model upgrade | repeated agent evals across the scripted set | +| GTM release | both locale demos plus human public-card privacy check | -1. **B-1 eval-first**(~半天):先寫洗白者 case + 標籤機檢,跑現行 SKILL.md 讓它亮紅 → 做 ISSUE-3 的 prompt 改動 → 轉綠。一步同時交付「第一個 case + ISSUE-3 本體」。 -2. **`check_card.py` + `check_state.py`**(~1 天):A 系列 + B 系列機檢部分。先可離線用(人工貼卡進去檢)。 -3. **差分 case B-3 / B-4**(半天):harness 通了之後最先加——測 Step 2 靈魂,成本最低。 -4. **mutation 驗活 + grader 校準**(§6):harness 全通後做一輪,之後才有資格說「eval 是活的」。 +Do not put non-deterministic, billable agent runs in default CI. Do not weaken assertions merely to reduce flakiness. diff --git a/docs/language-policy.md b/docs/language-policy.md new file mode 100644 index 0000000..b1a5353 --- /dev/null +++ b/docs/language-policy.md @@ -0,0 +1,34 @@ +# Language policy + +The repository separates implementation language from market-facing localization. + +## English-only surfaces + +Use English for: + +- `AGENTS.md` and `CLAUDE.md` +- all Markdown under `docs/`, except no exceptions are currently needed +- `BACKLOG.md` and `evals/EVALS.md` +- `skills/fomo-kernel/SKILL.md`, flows, references, rubrics, specifications, and mock documentation +- English runtime assets such as `card-template.html`, `copy/en.json`, evaluation prompts, and lens JSON +- developer-facing test documentation + +Do not mix translated explanations into these files. One implementation contract makes cross-agent behavior easier to review and keeps code identifiers, schemas, and documentation aligned. + +## Bilingual GTM surfaces + +GTM content may have separate, complete localized artifacts: + +- `README.md`: default English landing page +- `README.zh-TW.md`: Traditional Chinese landing page +- `docs/demo-card-en.html` and `docs/demo-card.html`, plus their rendered images + +Keep claims, examples, and numeric values synchronized across locale variants. Translate wording, not product behavior. + +## Product localization + +User-visible product copy is stored by locale, such as `skills/fomo-kernel/copy/en.json` and `skills/fomo-kernel/copy/zh-TW.json`. Locale files are not implementation instructions and must remain separated rather than mixing languages in one contract. + +The engine, schemas, lifecycle, and policy remain locale-neutral. `--language` selects copy and rendering only. + +Stable dimension identifiers are English snake case, for example `position_sizing`, `averaging_down`, and `entry_style`. Localized dimension labels and card wording live only in `copy/.json`; lens configurations use the stable identifiers and English implementation text. diff --git a/docs/prd-investment-os.md b/docs/prd-investment-os.md index ccfdea3..810b28e 100644 --- a/docs/prd-investment-os.md +++ b/docs/prd-investment-os.md @@ -1,144 +1,82 @@ -# PRD · 投資 OS:吸收 record-trade,雙前端(自己用 + 別人用) +# PRD: investment OS with one core and constrained surfaces -> 狀態:草案(worktree,未進 issue;codex + gemini review 中) -> 日期:2026-06-20 -> 來源:本 session — 讀 issue #12 完整本文 + 完整讀 record-trade(investment_note) + owner 拍板最終目標 -> 定位:issue #12 的 **Phase B 設計** + 回答其開放問題 **#1 / #2 / #5**;昨天的 `prd-stateful-review-loop.md` 收編為本檔 Layer 3 +Status: architectural direction. Original decision date: 2026-06-20. ---- +## Product decision -## 0. Owner 最終目標(2026-06-20 拍板) +Absorb useful record-trade capabilities into fomo-kernel and serve both a distributable product and a richer owner workflow from one core. -> 把 record-trade 的功能抽離出來、結合進 fomo-kernel,設計一個系統,**同時給別人用 + 給自己用**。 +This is not two products and not two engines. The distributable product is a constrained subset of the owner workflow. -這一句直接定了 issue #12 的三個開放問題: - -| issue #12 開放問題 | 本 PRD 拍板 | -|---|---| -| #1 定位(個人工具 vs 可分發 vs 兩前端) | **雙前端**:自己用=個人工具、別人用=可分發產品 | -| #2 跟既有系統的關係 | **吸收功能**:fomo-kernel 當 OS 本體,抽 record-trade 的功能、砍其形態 | -| #5 一個 OS vs 同引擎多前端 | **同一薄引擎 + 雙前端** | - ---- - -## 1. 核心架構:一個薄核心,兩個前端 +## Architecture +```text +Owner surface + optional personal research context + future selection and information workflows + | +Shared core + deterministic behavior and performance engine + local ledger and thesis state + review lifecycle and validators + one-card renderer + | +Distributable surface + recap and thin update only + local data only + no selection or research workflow ``` - ┌─ 前端 A:自己用(個人工具)────────────────┐ - │ 全 Phase:找資訊→選股→交易→update→recap │ - │ 可選讀外部 context(KOL/wiki/research,唯讀) │ - │ 用自己的 RULES.md 當尺 │ - └──────────────────────────────────────────┘ - │ (掛在同一核心上) - ┌─────── 共用核心(兩前端完全一樣)───────────┐ - │ ① 機械引擎:5 維行為診斷 + α/β + 賣後機會成本 │ - │ ② 薄狀態 ~/.trade-coach/:thesis 一行 + 規矩 cadence │ - │ ③ 輸出永遠收斂成一張卡 │ - └──────────────────────────────────────────┘ - │ (掛在同一核心上) - ┌─ 前端 B:別人用(可分發產品)──────────────┐ - │ 只開 recap(+ 薄 update);選股/找資訊關閉 │ - │ 純本機、無外部連接、隱私留本機 │ - │ 用可換大師鏡片(VY 等)當尺 │ - └──────────────────────────────────────────┘ -``` - ---- - -## 2. 第一原則:別人用 = 自己用的子集 - -**不是兩個系統,是一個系統關掉幾個開關。** - -``` -別人用 = 自己用 − Phase C/D(選股/找資訊) − 外部資料連接 ⊕ 換成可換鏡片 -``` - -→ 「同一引擎」因此成立:引擎不分叉,兩前端只差「開放哪些 Phase + 資料邊界 + 用哪把尺」。維護一套核心,不維護兩套系統。 - ---- - -## 3. 紅線 = 前端開關,不是引擎分叉 - -引擎只算「**你做了什麼 / 你的 thesis 證偽了沒**」,永遠不碰「**該買什麼**」。 -- 選股 / 找資訊(Phase C/D)**只在「自己用」前端開放**,且即使開放也是**過程支援**(檢查你自己的 thesis 還成不成立),不 recommend。 -- 「別人用」前端把 Phase C/D **關掉**。 +The shared core calculates what happened and whether a recorded thesis or process rule survived evidence. It never answers what security should be bought. -→ issue #12 的北極星紅線(過程教練 ≠ 選股顧問)由**前端開關**守住,不需要兩個引擎、不需要兩份 codebase。 +## Surface matrix ---- - -## 4. record-trade 功能抽離清單(過 issue #12 三道閘) - -> 三道閘:① 改下一筆測試 ② 硬性 form budget ③ 盡量自動。 - -| record-trade 組件 | 功能 / 形態 | 過閘? | 處置 | -|---|---|---|---| -| update(CSV→holdings/mark) | 功能 | ✅ | **吸收 → Layer 0 薄記帳** | -| 決策 narrative(11 欄) | 功能 | ⚠️ 欄爆 form budget | **吸收功能、砍欄 → Layer 2 thesis 一行**(why/證偽/停損/size) | -| revisit cadence(30/60/90,still-believe/falsified) | 功能 | ✅ falsified→停損,強改動作 | **吸收 → Layer 3 教練迴圈對帳**(最該吸收的) | -| swap analysis(賣後機會成本) | 功能 | ✅ | issue #12 Layer 1 已列「已實作」→ **已在核心** | -| source attribution(KOL→決策歸因) | 功能(重) | ⚠️ | **延到 Phase C**(屬「找資訊」價值驗證,非 Phase B) | -| portfolio.md 治理(Health/Actions/$5k 閾值) | 形態 | ❌ form budget 爆 | **砍** | -| weekly-review.md 逐週長文 journal | 形態 | ❌ 長文 wiki | **砍 → 收斂成卡** | -| 多 protocol(fact-check/concurrency/sync) | 形態(源自真事故) | ⚠️ recap 用不到 | Phase B **砍**;fact-check 精簡版留 **Phase C/D**(那時要 web search) | - -**結論:record-trade 真正該被吸收的功能只有 4 個**(薄記帳 / thesis 一行 / cadence 對帳 / 賣後機會成本),其餘大半是 governance 形態 → 砍。 - ---- - -## 5. 4 層架構(issue #12)× 雙前端 - -| Layer | 共用? | 自己用 | 別人用 | -|---|---|---|---| -| **L0 記帳(薄)** | 共用 | 同 | 同(或更薄:見開放 #2) | -| **L1 機械引擎** | 共用 | 同 | 同 | -| **L2 thesis 一行** | 共用 schema | 可連 wiki 預填 | 純手寫(可留白) | -| **L3 教練迴圈**(gate/週/對帳) | 共用 | 同 | 同 | -| **記憶 ~/.trade-coach/**(薄·自動) | 共用 schema | 多「外部 context」可選欄 | 純本機欄,無外部 | -| **輸出一張卡** | 共用鐵律 | 同 | 同 | - ---- - -## 6. 雙前端差異矩陣 - -| 維度 | 自己用(個人工具) | 別人用(可分發) | +| Concern | Owner surface | Distributable surface | |---|---|---| -| Phase 範圍 | 全(A→D) | A recap + 薄 B update | -| 選股/找資訊 | 開(過程支援,不 recommend) | **關** | -| 資料 | 本機 + 可選讀外部(KOL/wiki,唯讀) | 純本機,零外部 | -| thesis 來源 | wiki 預填 + 手寫 | 純手寫 | -| 用的尺 | 自己的 RULES.md | 可換大師鏡片(VY…) | -| 隱私 | 自己的 repo | 不外傳、不入記憶(現有鐵律) | -| 發布 | 不發布 | 可分發 skill | - ---- - -## 7. 分階段(issue #12 Phase A–D × 雙前端何時可發布) +| Recap and progress card | shared | shared | +| Thin transaction/position update | shared | shared or simplified | +| Local thesis and rule state | shared schema | shared schema | +| Research context | optional | disabled | +| Selection workflow | future, process support only | disabled | +| External sources | explicit and read-only | none by default | +| Lens | personal rules or selected lens | packaged lenses | +| Card contract | shared | shared | + +## Capabilities absorbed from record-trade + +| Capability | Decision | +|---|---| +| Broker update into holdings | Keep as a thin ledger capability. | +| Large decision narrative | Reduce to a compact thesis with why, falsifier, horizon, stop, and target size. | +| Revisit cadence | Keep and integrate with the coach loop. | +| Post-exit and swap analysis | Keep in the mechanical layer. | +| Source attribution | Capture early; analyze more deeply only in later owner workflows. | +| Portfolio governance document | Do not import. | +| Weekly long-form journal | Replace with canonical sessions and one card. | +| Multiple operating protocols | Keep only safeguards required by observed failures. | -| Phase | 內容 | 雙前端 | -|---|---|---| -| **A recap**(現況) | 行為教練卡 | 兩前端共用,已有 | -| **B 交易+update** | 吸收 record-trade 的薄記帳 + thesis 一行 + cadence 對帳 | 做完 **別人用即可發布**(recap + 薄 update) | -| **C 選股** | 研究/KOL 蒸餾接進來當過程支援 | **只自己用** | -| **D 找資訊** | 研究收集鏈 | **只自己用** | +## Layer model -→ 「別人用」的最小可發布版 = Phase A + B 核心。Phase C/D 是「自己用」專屬的外擴。 +1. Thin ledger: local facts from snapshots and transactions. +2. Mechanical engine: behavior, accounting, benchmark comparison, and post-exit analysis. +3. Thesis record: compact, append-only, and falsifiable. +4. Coach loop: required questions, prior-rule reconciliation, one commitment, and recovery. +5. Renderer: one private card plus an independent public view. ---- +## Safety boundary -## 8. 開放問題(給 codex / gemini review) +Feature switches may expose more owner context, but they must not create a recommendation path in shared code. Research support can test a user-owned thesis; it cannot quietly become a stock-picking API. -1. **雙前端怎麼在一個 codebase 共存?** feature flag / engine 抽成共用 package + 兩個薄 skill / build-time 分支?哪個最不會讓核心變胖? -2. **「別人用」要不要 update(記帳)?** 還是別人用只做**無狀態 recap**(丟 CSV 出卡、不維護 portfolio)= 最薄、最易發布、最像現在的 v0?「薄 update」對陌生用戶是價值還是摩擦? -3. **source attribution 延到 Phase C** —— 但「哪個資訊源讓我賺錢」正是「自己用」的主要動機之一。延後會不會閹掉自己用的核心價值?還是它本來就該等 Phase C? -4. **thesis 一行的雙向壓力**:對「自己用」(有 wiki 深 thesis)會不會太薄、逼他降級?對「別人用」會不會太重、逼陌生人寫他不會寫的東西? -5. **統一 ~/.trade-coach/ schema** 怎麼同時:薄、餵雙前端(別人用無外部欄/自己用有)、又不讓「自己用」的外部 context 洩漏進「別人用」的隱私邊界? -6. **紅線靠前端開關守**夠不夠硬?「自己用」開了選股過程支援,會不會在共用引擎裡留下「給別人用也能被誘導 recommend」的後門? +## Release sequence ---- +- Phase A: recap card. +- Phase B: thin update, ledger, thesis, and stateful reconciliation. This is the minimum distributable complete loop. +- Phase C: owner-only selection research support. +- Phase D: owner-only information gathering and source attribution analysis. -## 9. 收編 / 取代 +## Open decisions -- 昨天 `docs/prd-stateful-review-loop.md` → **收編為本檔 L3 教練迴圈 + 開放問題 #5**(它的 `~/.trade-coach/log.jsonl` = 本檔記憶層的 recap 切片)。 -- issue #12 → 本檔是它 **Phase B + 開放問題 #1/#2/#5** 的設計落地;Phase C/D 與開放問題 #3/#4/#6 仍掛 issue #12。 +- Whether the distributable surface should expose snapshot updates immediately or begin with recap-only onboarding. +- How much source capture fits the first-session question budget. +- How to keep optional owner context from leaking into shareable or distributable artifacts. +- How to make capability gates structural rather than relying only on prose. diff --git a/docs/prd-ledger.md b/docs/prd-ledger.md index 43f1694..f0f1c77 100644 --- a/docs/prd-ledger.md +++ b/docs/prd-ledger.md @@ -1,187 +1,90 @@ -# PRD · 持久帳本(snapshot-anchored ledger)× 多市場幣別 × 記憶差異 +# PRD: snapshot-anchored ledger, markets, currencies, and memory -> 狀態:設計定稿(owner 已拍板需求 + benchmark 方案),待實作 -> 日期:2026-07-06(判定日期,引用產品假設時帶上) -> 來源:2026-07-06 session——「Phase B:完整取代每週 trade view」需求盤點 → owner 五點需求回覆 → 帳本方案討論 → per-market benchmark 拍板 -> 定位:[#31](https://github.com/atomchung/fomo-kernel/issues/31) 的範圍**修訂**(replay-only → snapshot-anchored 雙輸入)+ [#51](https://github.com/atomchung/fomo-kernel/issues/51) 的**升級**(明示邊界 → 真支援)+ [#32](https://github.com/atomchung/fomo-kernel/issues/32)/[#33](https://github.com/atomchung/fomo-kernel/issues/33) 的掛載點;上游需求層見 `docs/requirements.md`(R1–R18)、`docs/prd-investment-os.md`(雙前端 + record-trade 功能抽離) +Status: core ledger and multi-market foundations implemented; complete snapshot onboarding remains P1. Decision date: 2026-07-06. ---- +## Requirements -## 0. 需求源(owner 2026-07-06 拍板,五點) +1. Accept both declared positions and transaction history because most users cannot provide a complete lifetime ledger. +2. Preserve accurate accounting for US and Taiwan markets and multiple currencies. +3. Retain decisions, review conclusions, and card-to-card changes. +4. Support due post-exit checks. +5. Measure swap opportunity cost when one sale funds another purchase. -1. **帳本要支持「持倉+交易紀錄」雙輸入**——用戶不容易有完整交易紀錄拼湊持倉。這推翻 #31 原設計「從 broker CSV 流水 replay 還原」的單路前提。 -2. **數據要準確,同時支持台股和美股**;呈現幣別跟 output language(en→USD、zh-TW→TWD、zh-CN→CNY)。 -3. **持續記憶卡片差異**,讓人有持續使用的訴求——包含記憶投資決策、review 決策。 -4. **30/60/90 revisit(賣飛了沒)重要**(#32 確認優先)。 -5. **Swap 機會成本(賣 A 換 B net 多少)**(#33 確認要做)。 +The core model is: a declared position snapshot is the accounting anchor; later transactions update it. Behavioral diagnosis may still use all visible transactions with explicit completeness limits. -背景:Phase B 目標 =「連續兩週 owner 只跑 `/fomo-kernel`、不再開 investment_note 的 `/record-trade`」。每週 trade view 的功能基準 = record-trade 週報 11 段(本週決策表/市場表現/新聞驗證/決策品質/narrative/source attribution/swap/改進點/規則檢查/事前登記/revisit scan)。#121 那輪把 #31–33 標「非 MVP 範疇」是 **MVP 發布輪**的判定;本輪目標升級後正式解除排除。 +## Event model -**核心一句話:帳本以「持倉宣告」為錨點、交易紀錄做增量疊加——兩種輸入進同一本帳;行為診斷與帳本推導分離,各吃各夠用的資料。** +`~/.trade-coach/ledger.jsonl` is an append-only local event stream with schema versions. ---- - -## 1. 帳本:snapshot-anchored ledger - -### 1.1 兩種輸入現實 - -| 輸入 | 形態 | 現況 | -|---|---|---| -| 交易紀錄 | broker CSV / 對帳單截圖(BUY/SELL 流水) | engine 現行唯一輸入;**假設完整**,缺漏會靜默算錯持倉 | -| 持倉宣告 | 券商 app 持倉頁截圖 / 表格(ticker、股數、均價) | **新增**——多數用戶拿得出這個,拿不出完整流水 | - -### 1.2 資料模型 - -`~/.trade-coach/ledger.jsonl`(append-only 事件流,純本機,延續隱私鐵律;schema_version 隨檔): - -```jsonc -// 事件一:持倉宣告(Step 0 由 Claude 讀截圖/表格標準化,零 parser——同現行 CSV 標準化模式) -{"type": "snapshot", "as_of": "2026-07-06", "source": "user_declared", - "positions": [{"ticker": "NVDA", "market": "US", "currency": "USD", - "shares": 40, "avg_cost": 152.3}], // avg_cost 可缺 - "cash": {"USD": 8200, "TWD": 120000}} // 可缺 - -// 事件二:交易(現行 CSV 標準化流程,補 market/currency/fee 欄) -{"type": "trade", "date": "2026-07-08", "ticker": "2330.TW", "market": "TW", - "currency": "TWD", "action": "BUY", "qty": 100, "price": 985, "fee": 42, - "source_file": "TW_statement_202607.csv"} - -// 事件三(reconcile 產物):調整留痕 -{"type": "adjustment", "date": "...", "ticker": "...", "delta_shares": -5, - "reason": "reconcile: user snapshot 35 vs derived 40"} +```json +{"type":"snapshot","as_of":"2026-07-06","source":"user_declared","positions":[{"ticker":"NVDA","market":"US","currency":"USD","shares":40,"avg_cost":152.3}],"cash":{"USD":8200}} +{"type":"trade","date":"2026-07-08","ticker":"2330.TW","market":"TW","currency":"TWD","action":"BUY","qty":100,"price":985,"fee":42} +{"type":"adjustment","date":"2026-07-09","ticker":"NVDA","delta_shares":-5,"reason":"reconcile declared snapshot with derived holdings"} ``` -### 1.3 持倉推導(會計的「期初餘額+本期異動」) +## Holding derivation -1. 取**最近一筆 snapshot 當錨點**; -2. 錨點之後(`date > as_of`)的 trades 依序疊加(`old ± trade = new`); -3. 沒有任何 snapshot → 純 replay(= 現行行為,**向後相容**)。 +1. Use the latest snapshot as the anchor. +2. Apply only trades with `date > snapshot.as_of`. +3. If no snapshot exists, replay all available trades and mark completeness limitations. -**時點語意(釘死,實作照此)**:snapshot 代表 `as_of` 日**收盤後**的狀態——`date == as_of` 的交易視為已反映在宣告數字內、不疊加;嚴格 `date > as_of` 才疊加。用戶宣告「這是我現在的持倉」時 `as_of` = 今天。 +A snapshot represents end-of-day state, so same-day trades are already reflected. Missing pre-anchor history is normal and does not invalidate current holdings. -關鍵性質:**錨點之前的歷史缺失不是錯誤,是常態**——不影響當前帳正確性。replay 中出現負股數(= 交易紀錄不完整的鐵證,現行 engine 靜默)→ 明確提示「補一張持倉快照,或接受行為分析 only 模式」。 +## Reconciliation -### 1.4 準確性機制:re-declare 即對帳(reconcile) +When the user supplies another position snapshot: -用戶隨時再丟一張持倉截圖 → 與「錨點+疊加」推導結果 diff: +- If derived and declared holdings agree, mark the ledger reconciled. +- If they differ, show the narrow difference, accept the newer declaration as the new anchor, and write an adjustment event preserving the history. -- **一致** → 驗證通過,卡上標「帳本已對帳 ✓」; -- **不一致** → 列差異(「推導 NVDA 40 股,你宣告 35 股——中間可能有我沒看到的交易」),**預設以用戶新宣告為準**:寫入 `source: "reconciled"` 的新 snapshot + adjustment 事件留痕。 +Do not infer the cause of a mismatch. It may represent a missing trade, transfer, split, fee, or data error. -把「數據準確」從一次性假設變成**每次丟截圖就自我修復**的閉環。snapshot 事件序列同時免費送出 #31 想要的「持倉結構時間序列」。 +## Separate consumers -### 1.5 關鍵分離:行為診斷 ≠ 帳本推導 - -兩個消費者對「資料完整性」要求不同,分開就都滿足: - -| 消費者 | 吃什麼 | 完整性要求 | +| Consumer | Data | Completeness rule | |---|---|---| -| **帳本推導**(持倉/損益,準確優先) | 只信錨點之後的 trades | 嚴格——錨點保證正確起點 | -| **行為診斷**(5 維/攤平/出場,樣本優先) | 所有看得到的 trades(含錨點之前) | 寬鬆——樣本越多越準,缺漏標記即可 | - -### 1.6 誠實分級(延續 α 誠實化精神:缺什麼標什麼,不硬編) - -- `avg_cost` 缺 → 市值/佔比/集中度照算(只需 shares×現價),未實現損益標「均價未宣告」; -- 已實現損益標「自 {錨點日} 起算」; -- snapshot 帶入的持倉開倉日不可知 → `cycle_id` 用錨點日+標 `origin: snapshot`(持有期左截斷,出場/持有維語氣降級)。cycle_id 沿用現行三段格式(`ticker#日期#序號`),theses 對帳迴圈不受影響。 - -### 1.7 冷啟動紅利 - -入口從「先整理交易 CSV」降到「**丟一張持倉截圖就能開始**」: - -- **Day 0**:截圖 → snapshot → 結構診斷卡(集中度/賽道/sizing 現況三維不需要歷史)+ AI 照常猜 theses → 記憶迴圈當場啟動; -- **之後每週**:丟增量交易 → 攤平/出場/payoff 等行為維逐步解鎖; -- **隨時**:再丟截圖 = 自動對帳。 - -對「別人用」前端(`prd-investment-os.md`)是重大摩擦削減。 - ---- +| Accounting and holdings | anchor plus post-anchor transactions | strict from the anchor forward | +| Behavior diagnosis | all visible transactions | broader sample with explicit gaps | -## 2. 多市場多幣別 +Missing average cost may still allow market-value concentration but not complete unrealized P&L. Snapshot-origin cycles must indicate left-truncated holding history. -**原則:資料層永遠原幣記帳,換算只發生在呈現層。** +## Multi-market and currency policy -### 2.1 資料層 +- Store every event in original currency with explicit `market` and `currency`. +- Normalize Taiwan tickers to the data-provider convention when fetching prices. +- Convert only for aggregate presentation; preserve original-currency detail for brokerage reconciliation. +- Use cached rates offline and disclose the rate date. If no rate exists, show original currencies rather than guessing. +- Compare each market sub-portfolio with its own benchmark. Never synthesize a cross-market total alpha. +- Keep behavioral concentration global because one user can hold the same driver across markets. -- 每筆事件帶 `market` + `currency`; -- 台股 ticker 標準化為 `2330.TW`(yfinance 慣例),Step 0 由 Claude 判市場補後綴; -- `fee` 欄吃台股手續費/證交稅;支援零股。 +## Memory product behavior -### 2.2 呈現層:resolved output language → display currency +The first seconds of a returning review should prove continuity through: -- 映射:en→USD、zh-TW→TWD、zh-CN→CNY;掛在 SKILL.md 既有的 Output language resolution 段;存 `profile.md` 可 override; -- **合計數字**換算成 display currency;**分項保留原幣**並附換算(「NVDA +$1,200(≈NT$38,400)」)——用戶要對得上券商 app; -- 例外:持倉單一市場時直接用該市場幣別(美股 only 的繁中用戶不該看到滿卡無謂的台幣換算)。 +1. the prior commitment and current metric +2. the active thesis and any new evidence +3. the largest structural change since the prior session -### 2.3 匯率 gate(不准把 #64 剛修好的「離線確定性」弄假回去) +Canonical session bundles preserve cards and decisions. Projections provide compatibility and can be rebuilt. -- 匯率走 last_px 同一條 fetch 路徑(yfinance `TWD=X` 等),同受網路 gate; -- 離線 → 用 state 快取的上次匯率,卡上標「匯率截至 MM-DD」;從無匯率 → 只出原幣,不猜; -- 測試沿用 #64 的 offline 強制模式,匯率可 pin。 - -### 2.4 對標層:per-market benchmark(owner 2026-07-06 拍板) - -> 先前提案「只對主市場算+明標範圍」被否決(2026-07-06):組合跨市場接近對半時,「主市場」根本不存在,只算一邊等於丟掉一半部位。改為 **per-market 分算**。 - -- **拆分**:按 `market` 分子組合,各對各的基準——US→SPY(現行);TW→台股大盤(`^TWII` 不含息 vs `0050.TW`,實作時定案並在卡上明標所選基準與含息差異); -- **呈現**:兩行並列、各含資金佔比,例: - ``` - 美股部位(52% 資金):贏 SPY +14pp = 押賽道 +9 / 選股 +5;β 1.4 - 台股部位(48% 資金):贏加權指數 +3pp(無板塊對照、按大盤計);β 0.9 - ``` -- **不合成總 α**——混合組合對單一基準的 α 是假精確。會計層(總報酬/未實現/已實現)照 display currency 合計,那是會計事實、可加總; -- **統計檢定力誠實**:拆開後每邊樣本變小,`alpha_credible`(≥1 年 & |t|≥1.96)per-market 各自判,更容易 not_significant——這是誠實不是缺陷(混算的顯著性本來就是假的),語氣 gate 照現行規則走; -- **台股 excess_split 第一版**:SECTOR_BENCH 是美股 ETF 表(#92),台股部位標「無板塊對照、按大盤計」——沿用既有 `coverage<1` 語彙,不硬造假拆帳。 - -### 2.5 行為層不拆市場(與 2.4 的分界) - -5 維行為診斷、driver map(Claude 世界知識天然跨市場:「AI capex」主題可同時含 NVDA 與 2330.TW)、sizing 佔比(用同日匯率換 display currency 算比例,比例對匯率誤差不敏感)——**人只有一個,行為不分國界**。只有 α/β 對標層 per-market。 - ---- - -## 3. 記憶與卡片差異(留存的產品機制) - -三層記憶,兩層已有、一層新增。目標:**第二次打開的前 10 秒出現三個「它記得我」的證據**。 - -1. **承諾迴圈**(已有):`log.jsonl`,開場對帳「上次那條規矩守住了沒」。 -2. **決策記憶 × review 決策**(已有+本輪接上):`theses.jsonl` 記「當初為什麼買」;revisit(#32)到期把它調出來對答案——owner 需求第 3 點的「記憶投資決策 + review 決策」就是這兩件事的閉環。 -3. **卡片庫+變化摘要**(新增): - - 每次出卡落 `~/.trade-coach/cards/YYYY-MM-DD.md`(YAML frontmatter:headline、key metrics、commitment+卡全文)——歷史可回看;也是 v3a「蒸餾自己的鏡片」的語料庫(`v1-weekly-coach.md` §2 設計過、未實作); - - 開場對帳行升級成**變化摘要**:上週 vs 本週最大的 3 個變化(承諾 metric 動了多少/持倉結構怎麼變/上次的洞收斂了沒)。diff 全從 `log.jsonl` 的 metrics 序列算,唯一前置是把 `metrics_snapshot` 從現在只存 4 個 key 擴成全量 metrics(一行改動)。 - ---- - -## 4. Revisit(#32)× Swap(#33):改從帳本事件驅動 - -兩個 issue 的設計成立,掛載點升級: - -- **出場偵測從帳本事件來**(trade 使 shares→0 或減 ≥50% → 自動排入 `revisit.jsonl` 的 30/60/90 queue),不再靠每次全量 CSV 重推——清倉標的從此不會「從宇宙消失」; -- due 檢查進 SKILL 開場路由(與「偵測新交易」並列);賣飛對比價走 last_px 既有路徑; -- **swap 配對**(賣 A 後 N 天內買 B)從 ledger 事件流算,AI 推+用戶 confirm(inference-first 不變);swap net 必對位(賣飛只有在「換入 < 原標的」時才算真錯誤);閒置 cash 偵測靠 snapshot 的 `cash` 欄更準。 - ---- - -## 5. 實作切分 - -``` -PR-1 ledger 資料層:雙事件 + current_holdings() + reconcile diff + Step 0 讀持倉截圖 ← 地基 -PR-2 多市場/幣別:market/currency + 台股正規化 + 呈現層換算 + 匯率 gate + per-market α/β(依賴 PR-1 欄位) -PR-3 revisit + swap(#32 + #33,依賴 PR-1 事件流) -PR-4 卡片庫 + 變化摘要(獨立可並行,最小) -``` +## Revisit and swap -每個 PR:過全部測試套件(`python3 tests/run_all.py`,套數以其總結輸出為準)+ mock fixture 只用假資料+ **影響用戶可見行為者同 commit 更新 SKILL.md**(契約同步鐵律)。 +- When shares reach zero or fall past the configured reduction threshold, enqueue post-exit windows from the ledger event. +- Use a bounded historical backlog so cold start does not create an interrogation queue. +- Pair a sale with a nearby purchase as a swap candidate, then require user confirmation. +- Judge a swap by relative outcome, not whether the sold asset rose in isolation. -## 6. 開放問題(實作時定,不阻塞動工) +## Implementation slices -1. 台股 benchmark:`^TWII`(不含息,台股高股息會低估基準)vs `0050.TW`——選定後卡上明標。 -2. swap 配對窗 N 天(#33 預設 14)與 revisit 觸發門檻(#32 預設清倉/減半)——沿 issue 預設,dogfood 後校。 -3. reconcile 差異的呈現粒度(逐檔 vs 摘要)。 -4. `ledger.jsonl` 與既有 `last_state.json` 的關係:state 是推導快照(可重算、非權威),ledger 是事實層——state 檔角色不變。 +1. Ledger event layer and reconciliation. +2. Market/currency fields, FX gates, and per-market benchmarks. +3. Event-driven revisit and swap analysis. +4. Canonical card history and progress summary. +5. Complete snapshot adapter for direct screenshot or table onboarding. -## 7. 紅線(沿用,此處只列不重述) +## Non-negotiable boundaries -- 隱私:ledger/cards/revisit 全在 `~/.trade-coach/`,純本機、不外傳、不回作者;mock 之外的 CSV 永不進 git(.gitignore 機制防線不動)。 -- 薄狀態:ledger 是事實層 append-only,不是第二本 447 系統——**不做** portfolio.md 式治理層、不做每日淨值序列。 -- 卡的形態:輸出永遠收斂一張卡,帳本數字是卡的地基不是新報表。 -- 離線確定性(#64):所有網路依賴(含匯率)走同一 gate、測試可 pin。 +- All ledger, session, card, and revisit data stays local. +- The ledger is a fact layer, not a new governance wiki or daily net-asset-value system. +- Accounting supports the card; it does not create a second dashboard product. +- Every network dependency has an offline, cache, or explicit-missing path. diff --git a/docs/prd-stateful-review-loop.md b/docs/prd-stateful-review-loop.md index d8faada..4b08bf3 100644 --- a/docs/prd-stateful-review-loop.md +++ b/docs/prd-stateful-review-loop.md @@ -1,122 +1,79 @@ -# PRD · 有狀態的復盤迴圈(跨 session 對帳) +# PRD: stateful review loop -> 狀態:草案 / 設計中(暫存 worktree,未進 BACKLOG、未拍板) -> 日期:2026-06-20 -> 來源:2026-06-20「三段執行演示 + 狀態層設計」session -> 一句話:讓 fomo-kernel 從「無狀態的孤立快照」變成「跨 session 對帳的連續迴圈」——第二次做能基於第一次 revisit。 +Status: implemented in v2 with canonical sessions and compatibility projections. Original concept date: 2026-06-20. ---- +## Problem -## 1. 背景與問題 +A stateless review can detect new behavior but cannot determine whether an earlier behavior improved. Repeated runs may produce the same rule while changing numbers simply because the sample grew or market prices moved. -fomo-kernel 目前(v0)是**無狀態**的:`engine/trade_recap.py` 每次跑都是「吃 CSV → 算一次 → 出卡」,不記得上次。 +The core product question is longitudinal: the previous review identified a metric and the user chose a rule; what changed when the user returned? -**實證(2026-06-20 三段執行演示)**:把 mock 19 筆交易按時間切三段、累積跑三次: -- 頭號洞三次都是「部位 sizing」,處方前兩次一字不差(上限 20%)——**每次被罵同一句**,而 engine 不知道自己在重複。 -- 數字大幅跳動(盈虧比 0→3.5、α 16%→33%)是**假演進**:樣本變多 + 市場指標飄移,不是行為改善。 -- 第二名的洞會換(分散→出場→攤平)是真的,但那是「新行為進入樣本」的**橫切面**變化,不是縱貫追蹤。 -- **沒有任何一次在對帳上次的承諾。** +## Goals -**核心缺陷**:系統偵測得到「**新行為出現**」,偵測不到「**同一條行為有沒有改善**」——因為沒有「上次的你」當對照點。而 review 的價值正是這條縱貫線(你上次 sizing 76% → 這次 48% 的進步)。 +- Reconcile every returning review against the prior commitment. +- Reuse confirmed motive and thesis history without asking the same question again. +- Preserve a longitudinal metric and decision trail. +- Keep state local and recoverable. +- Keep the mechanical engine responsible for facts and the orchestration layer responsible for lifecycle. -對應評分:處方與留存閉環 4/10。對應 BACKLOG 演進路徑:本 PRD = `v0 無狀態卡 → v1 守則檔+gate+對帳` 裡的**對帳**那塊。 +## Non-goals -## 2. 目標 / 非目標 +- No pre-trade execution gate in this phase. +- No cloud account or synchronization system. +- No security recommendations. +- No calendar-driven reminder system. -**目標** -- 第二次 review 能基於第一次的結果 revisit:對帳上次承諾、引用上次動機問答,把孤立快照串成連續 context。 -- 從「橫切面快照」進化成「縱貫進度線」:看得到同一條行為有沒有改善。 -- 守隱私鐵律:狀態全程留本機,不外傳。 -- 守架構鐵律:engine 維持純算;狀態讀寫 + 對帳敘事在 Claude(runtime)層。 +## Canonical state -**非目標(明確不做)** -- 不做 pre-trade gate(下單前攔截)——之後的事,本 PRD 只做「事後對帳」。 -- 不改 engine 算法/輸出格式(MVP engine 零改動)。 -- 不做雲端同步 / 帳號系統(留本機)。 -- 不碰選股建議(IP / 法規紅線不變)。 +Each completed review stores an immutable session bundle containing: -## 3. 核心洞察(為什麼這個設計成立) +- engine card and state snapshot +- Review Plan +- user answers +- qualitative narrative +- thesis and add-decision events +- user-chosen commitment +- private and public card artifacts +- manifest hashes -> **狀態層不是 engine 的事,是 runtime 的事。Claude Code 本身就是 runtime,有檔案系統存取。** +`sessions//bundle.json` is canonical. Legacy JSONL files and card folders are rebuildable projections. -對帳 = 拿「上次存的數字」比「這次 engine 算的數字」;這個「比」的動作在 Claude 層,engine 繼續當純函式。因此 **MVP engine 零改動**,只改 `SKILL.md` 對話流程 + 讀寫一個本機檔。 +## Lifecycle -## 4. 狀態模型 +### First review -每次 review 結束 append 一筆,存三個東西: -- **snapshot 快照**:那次關鍵 metrics(headline dim、max_pos_pct、avgdown 次數、盈虧比、α/β…)。 -- **commitment 承諾**:「下次只改這一件」+ 可驗 metric + 目標值。 -- **motives 動機問答**:Step 2 問到的(ticker → 逢低/凹單、thesis)。下次同標的不重問 + 可對帳。 +1. Prepare engine facts and required questions. +2. Obtain motive answers and create inferred theses for uncovered cycles. +3. Preview one card. +4. Let the user choose, rewrite, or skip one rule. +5. Atomically commit the canonical session. -## 5. 儲存 +### Returning review -`~/.trade-coach/log.jsonl`(BACKLOG 願景層已命名的本機狀態目錄),一次 review 一行 JSON: +1. Load the Review Plan's bounded `state_snapshot` rather than scanning all local files. +2. Recompute current facts from new input. +3. Reconcile the prior commitment metric with the current engine state. +4. Ask only deduplicated questions caused by new cycles, new adds, or unresolved high-cost contradictions. +5. Produce a progress card and atomically commit the new session. -```jsonl -{"date":"2026-03-20","headline":"sizing","metrics":{"max_pos_pct":0.76,"avgdown":2,"pnl_ratio":3.5},"commitment":{"text":"最大單一部位壓到 20% 以下","metric":"max_pos_pct","target":0.20},"motives":{"PLTR":"逢低加碼"}} -{"date":"2026-06-20","headline":"avg_down","metrics":{"max_pos_pct":0.48,"avgdown":2,"pnl_ratio":2.9},"checkin":{"metric":"max_pos_pct","prev":0.76,"now":0.48,"target":0.20,"verdict":"改善但未達標"},"commitment":{"text":"虧損部位一律不加碼","metric":"avgdown","target":0}} -``` +## Progress-card behavior -第二行的 `checkin` 就是 §1 缺的那個對照點。(可選人可讀層 `~/.trade-coach/rules.md` 累積規矩清單,之後做。) +- If the prior rule improved but is still outside target, say so and keep the same topic when it remains the largest leak. +- If the prior rule held, acknowledge it before selecting a new leak. +- If the sample was too short or the user skipped, preserve a baseline without pretending there was a commitment. +- Use engine-owned values; the agent must not calculate the delta. -## 6. 流程 +## Recovery -**初診模式(第一次 / log 為空)**:照現行四步 → 出卡 → **新增:append snapshot + commitment + motives**。 +- Interrupted prepare or unanswered conversation: resume from `.pending/` without refetching prices. +- Failure before canonical rename: retry the pending session. +- Failure after canonical rename: repair projections from the bundle without questioning the user again. +- Identical retry: no-op. Conflicting retry under the same session ID: fail closed. -**對帳模式(log 非空)**: -1. `/fomo-kernel` 啟動,Claude **先讀 log 最後一筆**。 -2. engine 重算當前快照(吃新 CSV)。 -3. Claude 對帳:上次 `commitment.metric` 的 prev vs now vs target。 -4. 出**對帳卡**(見 §7)。 -5. append 這次。 +## Acceptance criteria -模式判斷:讀 `~/.trade-coach/log.jsonl` 有無上次紀錄。 - -## 7. 對帳卡(第二次的輸出) - -從「你最大的洞是…」變成「上次承諾 X → 這次實況 Y」: -- 守住 → 肯定 + 給新洞。 -- 沒守住 → 咬住那條,不換題。 -- 引用上次動機問答。 - -範例: -``` -歡迎回來,距上次 92 天、新增 7 筆。 -上次你鎖定「最大倉壓到 20%」——當時 76%,現在 48%:有在降,但還沒到。 -別的先不談,這條收掉再說。 -(順帶:上次你說 PLTR 是「逢低」,這次它又往下加了兩次——還算逢低嗎?) -``` - -## 8. 分期 - -**MVP(今天就能做,engine 零改動)** -- `SKILL.md`:開場讀 log → 判初診/對帳;收尾寫 log。 -- Claude 從 engine 輸出抓關鍵數字寫 log。 -- 對帳敘事由 Claude 做。 - -**之後(不在本 PRD 拍板)** -- `rules.md` 人可讀規矩清單。 -- thesis 原文存檔(接 ISSUE-3 證據門檻 = 把 motives 從「自我定性」升級成「舉證」)。 -- pre-trade gate(下單前讀 rules 攔一次)。 -- sizing% 隨時間下降的趨勢線。 - -## 9. 與現有架構相容 - -- **engine**:純算不變(§3),MVP 零改動。 -- **隱私**:`~/.trade-coach/` 本機、不外傳,符合 SKILL 隱私鐵律。 -- **BACKLOG**:= 願景 v1「守則檔+gate+對帳」的對帳塊;`~/.trade-coach/` 已在願景層命名;`behavior-diagnosis.md` 的「問一次存本機、下次同標的復用」= 本 PRD 的 `motives`。 -- **ISSUE-3**:thesis 存檔是證據門檻的狀態化(列在 §8 之後)。 - -## 10. 驗收 - -- 跑第一次 → `~/.trade-coach/log.jsonl` 出現一筆含 `commitment`。 -- 同資料 + 新增交易跑第二次 → 卡開頭是「上次承諾 X → 這次 Y」的對帳,不是重新初診。 -- 上次 sizing 76%、這次 48% → 卡明確說「降了但沒達標」,而非當新洞重講同一件。 - -## 11. 開放問題 - -- 觸發「該深照」用日曆(季)還是樣本量(新增 N 筆 round-trip)?(討論傾向**樣本量**——交易頻率差異大,日曆是壞代理。) -- 多份對帳單 / 多帳戶怎麼 key? -- `log.jsonl` 損毀 / 用戶手動編輯的容錯? -- 規矩守住後的「畢業」機制(BACKLOG 6 步弧線的「升級畢業」)? -- 第一次樣本太短時(見三段演示:第一次連未實現都算成 +0),要不要延後給 commitment、先標「資料不足」?(接 ISSUE-1 α 雙閘門同向。) +- The first completed run creates one canonical session and one private card. +- A second run starts with the prior commitment context. +- Required questions are not repeated when active thesis state already answers them. +- Projection failure cannot destroy or invalidate a committed session. +- Private data never enters the public card or a cloud memory system. diff --git a/docs/release-2026-07-19.md b/docs/release-2026-07-19.md new file mode 100644 index 0000000..527692a --- /dev/null +++ b/docs/release-2026-07-19.md @@ -0,0 +1,41 @@ +# 2026-07-19 promotion release checklist + +Goal: ship a version on Sunday that reliably completes the workflow and produces a card. + +## P0 acceptance + +- [x] `SKILL.md` is a thin entry point and route-specific flows load on demand. +- [x] `prepare`, `preview`, `finalize`, `resume`, and `repair-projections` expose a stable CLI. +- [x] Code validates required motive questions and the evidence gate. +- [x] `new_evidence` creates an append-only thesis decision event. +- [x] Canonical sessions commit through an atomic staging-directory rename; identical retries are no-ops and conflicts fail closed. +- [x] A deterministic renderer creates private Markdown/HTML and public Markdown. +- [x] Public cards exclude amounts, dates, tickers, exact weights, and agent-authored free text. +- [x] English and Traditional Chinese render the same engine facts. +- [x] ETF allocation and concentration policy affects sizing, diversification, decision exits, and what-if analysis. +- [x] Missing ETF metadata enters the honesty ledger instead of defaulting to zero. +- [x] Legacy JSONL/card projections are rebuildable, and data export/reset covers v2 directories. +- [x] Developer documentation and skill instructions are English-only, enforced by a regression test. +- [x] The complete offline test suite passes. + +## Promotion demo + +1. Run a Traditional Chinese test drive: `python3 engine/review.py prepare --test-drive --language zh-TW`. +2. Select `new_evidence` without a source and show that preview rejects it. +3. Add a claim and source, preview again, and let the user select one rule. +4. Finalize and show `card-private.md` and `card-public.md` from the same session. +5. Repeat in English and emphasize that localization does not change the analysis contract. +6. Compare a broad-market ETF with a thematic ETF to demonstrate concentration policy. + +## Sunday go or no-go + +Go when `python3 tests/run_all.py` passes, one anonymized publishable CSV completes end to end in both locales, and a human confirms that the public card contains no private data. + +Do not ship if any required question can be skipped, finalization can leave partial canonical state, the public card leaks a ticker/amount/date, a broad allocation ETF is treated as single-stock concentration, or a projection failure requires questioning the user again. + +## Explicitly deferred to P1 + +- Multi-lens selection and comparison. +- A complete snapshot adapter; the current `snapshot_review` route accepts precomputed card/state artifacts. +- Automatic expense-ratio and tracking-error enrichment from a live ETF data source. P0 uses a local instrument map and explicit missing-data disclosure. +- Automated generation and publishing of GTM assets. This release includes synchronized English and Traditional Chinese README content, localized product copy, and a shareable public card. diff --git a/docs/requirements.md b/docs/requirements.md index b07f586..3a8a02b 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -1,154 +1,98 @@ -# fomo-kernel · 需求單一權威(Requirements SSOT) +# fomo-kernel requirements -> 狀態:草案。本檔為**需求**的 single source of truth;PRD/issues 回指本檔。 -> 日期:2026-06-20 -> 來源:2026-06-20 session(用戶視角 revisit → codex/gemini 三方 review → 八構面評分 → 需求整理 → 狀態模型 review) -> -> **給接手 session 的話**:本檔自包含、可冷讀。搭配讀:GitHub **issue #12**(全生命週期投資 OS 願景錨點)+ `docs/prd-investment-os.md`(雙前端架構)+ `docs/prd-stateful-review-loop.md`(狀態對帳迴圈)。**實作尚未開始**(見 §9)。 +Status: living requirements source. Original decision date: 2026-06-20. Implementation status was refreshed for the v2 architecture on 2026-07-14. ---- +## Product outcome -## 0. 最終目標(owner 2026-06-20 拍板) +Combine the useful decision-review capabilities of the earlier record-trade workflow into fomo-kernel so the same core can support a distributable product and a richer owner workflow. -> 把 `record-trade` 的功能抽離出來、結合進 `fomo-kernel`,設計一個系統**同時給別人用 + 給自己用**。 +The product is a process coach, not a stock advisor. Its defining output is a card grounded in the user's own account data that identifies a costly repeated behavior, preserves thesis evolution, and creates one rule that can be checked later. -這定了 issue #12 三個開放問題:#1 定位 = **雙前端**;#2 既有系統關係 = **吸收功能不吸收形態**(fomo-kernel 當本體);#5 = **同一薄引擎 + 雙前端**。 +## Requirements ---- +### Diagnosis -## 1. 需求點 R1–R18 +| ID | Requirement | +|---|---| +| R1 | Identify repeated behavioral leaks from real user numbers: sizing, losing-position adds, exits, holding horizon, and true diversification. | +| R2 | Deterministically calculate facts the user should not have to calculate: FIFO P&L, payoff, benchmark comparison, and allocation versus selection. | +| R3 | Converge on one progress card with one largest leak and at most one rule. | +| R4 | Allow philosophy lenses to change motive questions without changing mechanical facts. | +| R5 | Fail honestly when evidence, sample size, prices, benchmark coverage, or metadata is insufficient. | -**核心診斷(不變)** -| R | 需求 | 狀態 | -|---|---|---| -| R1 | 用我真實數字照出反覆犯的行為漏洞(sizing/攤平/出場/分散) | 不變 | -| R2 | 機械精算我算不出的(FIFO α/β、賽道 vs 選股) | 不變 | -| R3 | 收斂成一張卡(一個洞+一條規矩) | **要改**:快照→進度卡 | -| R4 | 哲學鏡片找動機,可換大師 | 不變 | -| R5 | 誠實閘門(α 雙閘門 #4 + 證據門檻 ISSUE-3) | 不變(#4 待核對,見 §8) | +### Memory and continuity -**記憶/持續 ← 本 session 從邊緣升為第一性(見 §2)** -| R | 需求 | 狀態 | -|---|---|---| -| R6 | 記憶:記得上次的洞/規矩/動機問答 | **新增·核心** | -| R7 | 對帳:這次基於上次 revisit | **新增·核心** | -| R8 | 長期:跨季/年連續追蹤,不是一次結束 | **新增·核心** | -| R9 | 進度感:看得到行為有沒有改善(縱貫線) | **新增·核心** | +| ID | Requirement | +|---|---| +| R6 | Remember the prior leak, rule, motive answers, and active theses. | +| R7 | Reconcile the next review against the prior commitment before opening a new topic. | +| R8 | Track decision and thesis evolution across months and years. | +| R9 | Show whether the behavior improved, worsened, or remained unresolved. | -**約束(不變)** -| R | 需求 | 狀態 | -|---|---|---| -| R10 | 隱私(留本機,不外傳) | 不變 | -| R11 | 紅線(過程教練≠選股顧問) | 不變·靠前端開關守 | -| R12 | 低摩擦(輸入零整理,Claude 自動轉券商格式) | 不變 | -| R13 | 克制(會拒絕,不有求必應) | 不變 | +### Constraints -**形態 + 驗證** -| R | 需求 | 狀態 | -|---|---|---| -| R14 | 雙前端(別人用+自己用) | **新增** | -| R15 | 別人用 = 自己用的子集 | **新增** | -| R16 | 吸收 record-trade 功能不吸收形態 | **新增** | -| R17 | Stage 0 真人驗「戳中」(GitHub #3) | 不變·**P0·天花板** | -| R18 | 留存:回來要第二張 | **翻案**:v1+ → 核心 | +| ID | Requirement | +|---|---| +| R10 | Keep trade data and derived state local. | +| R11 | Coach decision process without recommending a security. | +| R12 | Accept broker data without requiring the user to normalize it. | +| R13 | Refuse unsupported conclusions and allow the user to skip a commitment. | ---- +### Product shape and validation -## 2. 核心升級:記憶+持續 = 第一性需求 +| ID | Requirement | +|---|---| +| R14 | Support a distributable surface and an owner surface from one core contract. | +| R15 | Treat the distributable surface as a constrained subset of the owner surface. | +| R16 | Absorb useful functions from record-trade without importing its governance-heavy document shape. | +| R17 | Validate that a real user finds the card specific and useful. | +| R18 | Make the second review materially better because the system remembers the first. | -owner 作為**首位真實用戶**的反饋:**「投資不是一週復盤就結束。」** +## First-principles decision: continuity is core -這把「記憶/持續」從原本的**推測**(三段執行演示)、**假設**(#3 判準3 標「留存假設」)、**缺陷**(評分留存 4/10)、**v1+ 願景**(user-stories Epic D「無法觀測」),一次提升成**已確立的第一性需求**。理由不是產品功能選擇,是**投資的本質**(長期持續)。 +Investment review is longitudinal. A stateless card repeats the same diagnosis without knowing whether the user followed the previous rule. Therefore memory is not a retention add-on; it is required for the product to qualify as a review loop. -**推論:無狀態的卡不合格** —— 它只是「一次性體檢」,不是「復盤」(「復」=再一次、對比上次)。三段執行演示已實證:無狀態 → 三次罵同一句、數字假演進、從不對帳。 +Continuity is event-driven, not reminder-driven. The system should remember when the user returns, not create a calendar nag. A new review is justified by sufficient new decisions, a due thesis check, or an explicit user request. ---- +## State model -## 3. 新架構下要改的需求(詳細) +Model transitions before file formats: -- **R3 一張卡**:無狀態快照 → **有記憶的進度卡**。第一張=初診;第二張起=對帳卡(開頭「上次承諾 sizing 壓 20% → 當時 76%、現在 48%:在降、沒達標」)。收斂鐵律(一個洞+一條規矩)不變,語義從「你哪裡爛」→「上次說要改的做到沒 + 新洞」。 -- **R6–R9 記憶/持續**(主體):見 §4 狀態模型。**關鍵約束:持續 ≠ 催。** record-week 移除 L2 的教訓 = time-driven 自動推送無效(23 題 0 回答)。要的是「**我回來時它記得我**」(event-driven),不是「它每週推我」。觸發頻率 = **樣本驅動**(累積夠新 round-trip / 一季),不是日曆週。 -- **R18 留存翻案**:Epic D 從「無法觀測 v1+」→ 核心可驗;連帶把 #3 Stage 0 判準3(回來要第二張)從「假設」升為核心驗證項。 -- **R14–16 雙前端 × 記憶(一個衝突)**:記憶是兩前端都要的核心,但 **#8 chat 引流版(ChatGPT Custom GPT)本質無狀態** → 結構上滿足不了 R6–R9。**解**:chat 版釘死成「一次性體檢的引流漏斗」(明說要記憶就回本機版);「別人用**完整版**」必須有本機薄狀態,不能只是無狀態 chat。 +- Thesis or decision: `open -> still | modified | falsified | closed`, with `due`, `skipped`, and `insufficient` side states. +- Rule: `active -> candidate -> graduated`, where eligibility requires actual opportunities to violate the rule. +- Source attribution: `captured -> confirmed -> evaluated`. ---- +Important constraints: -## 4. 狀態模型設計結論(codex + gemini review,2026-06-20) +- Capture thesis and source evidence when the decision is made; it cannot be reconstructed reliably months later. +- Preserve holdings and decision cycles even when a closed ticker disappears from the latest CSV. +- A short first sample or skipped motive may produce no commitment. +- Use schema versions, atomic writes, immutable session bundles, and rebuildable projections. +- Graduation should combine code-computed eligibility with explicit user confirmation. -> 兩個外部模型獨立收斂到同一句:**「先設計狀態機,不要先定檔名。」** 原本「該有哪些檔案」是問錯層——檔案是末端。 +## Shared-core product architecture -**該有的狀態(不能薄掉):** -- **decision/thesis**:`open → still / modified / falsified / closed(清倉)`;旁路 `due`(到期 revisit)、`skipped / insufficient`(動機沒捕到/樣本不足) -- **rule**:`active → candidate(達 eligibility)→ graduated(升策略)` -- **source**:`captured(當下)→ confirmed(用戶標哪個影響)→ [Phase C 算 realized alpha]` +The distributable surface uses recap and a thin update loop with local state. The owner surface may later add research or source context, but both use the same mechanical engine, thesis schema, session lifecycle, and card contract. -**review 該收的刀:** -1. **先狀態機後檔名** —— 上面這些 transition 定義清楚,檔案幾個無所謂。3 檔若沒 transition,第二次仍是「假連續」。 -2. **source attribution 捕捉須提前 Phase B**(gemini+codex,2:1 推翻 prd-investment-os 原本延 Phase C):分析可延後,**canonical capture 必須在當下做**(半年後不可補)。 -3. **持倉/decision 要落盤**:純「CSV 每次重算」不夠(台股無 API + 清倉標的消失 + 無新交易但 due revisit)。 -4. **第一次不必然產生 commitment**:樣本短/用戶跳過 → 寫 `insufficient_data`/`skipped`,否則第二次把缺資料誤當已確認 thesis。 -5. **別人用 = log-first**:不逼陌生人維護 thesis/rules;rules 用模板鏡片、theses 選用。 -6. **schema version + 原子寫入**:半年狀態不能因 skill 更新損毀;寫入要原子(算完→寫→出卡,不留半寫髒狀態)。 +Selection and research features remain outside the public coaching boundary. Feature flags may restrict a surface, but no shared engine path should emit a security recommendation. -**一個未決分歧(待 owner 拍板):畢業機制** -- gemini:human-in-the-loop(偵測達標→標 candidate→出卡問 owner)。 -- codex:自動但要可計算 eligibility,看 **denominator**「有機會違規但沒違規」才算(沒新買入的三次不能讓「不攤平」畢業)。 -- 建議合解:codex 的 eligibility 自動偵測 + gemini 的出卡確認。兩家都反對「天真連續 N 次」(會造假畢業)。 +## Absorbed capabilities ---- +Keep: -## 5. 雙前端架構(設計細節見 `prd-investment-os.md`) +- thin transaction and position ledger +- compact thesis record with falsifier and sizing intent +- due revisit cadence +- post-exit and swap opportunity-cost analysis -- **別人用 = 自己用的子集**:`別人用 = 自己用 − Phase C/D(選股·找資訊) − 外部連接 ⊕ 換可換鏡片`。同一薄引擎,不分叉。 -- **紅線靠前端開關守**:引擎只算「你做了什麼/thesis 證偽沒」,不碰「該買什麼」;選股/找資訊只在「自己用」前端開放(且是過程支援、非 recommend)。 -- **4 層**:L0 薄記帳 / L1 機械引擎(已實作)/ L2 thesis 一行 / L3 教練迴圈 + 薄狀態記憶。輸出永遠收斂一張卡。 +Do not import: ---- +- a large portfolio-governance document +- weekly long-form journal requirements +- protocol complexity unrelated to the current lifecycle -## 6. record-trade 功能抽離清單(過 issue #12 三道閘) +## Current implementation -- **吸收(4 個)**:薄記帳(L0)/ 決策 narrative 砍成 thesis 一行(L2)/ revisit cadence(L3 對帳)/ 賣後機會成本(L1 已實作)。 -- **砍(governance 形態)**:portfolio.md 治理 / weekly-review.md 逐週長文 / 多 protocol(fact-check 精簡版留 Phase C/D)。 -- **提前**:source attribution 捕捉提前 Phase B(見 §4.2)。 +As of 2026-07-14, v2 implements the core review lifecycle, canonical atomic sessions, append-only thesis decisions, prior-commitment reconciliation data, public/private rendering, ETF policy, and projection repair. The snapshot route still needs a complete adapter, and multi-lens selection remains P1. ---- - -## 7. 與 issue #12 / PRD 的關係 - -- **issue #12** = 全生命週期投資 OS 願景(Phase A recap→B 交易+update→C 選股→D 找資訊)。本檔 = 它的**需求層**。 -- `prd-investment-os.md` = 雙前端架構(答 #12 開放問題 #1/#2/#5)。 -- `prd-stateful-review-loop.md` = L3 狀態對帳(需照 §4「狀態機優先」**重寫**,它目前是被 review 打的「3 檔」版)。 - ---- - -## 8. 開放問題(未決) - -1. 畢業機制:human gate vs 自動 eligibility(§4 有合解建議,owner 未拍板)。 -2. 「別人用」要不要薄 update,還是純無狀態 recap(log-first 已定方向,update 程度未定)。 -3. source 提前 Phase B,怎麼跟「別人用純本機 + form budget」平衡。 -4. 紅線靠前端開關夠不夠硬(prd 開放問題 #6,沒人深 review 過)。 -5. **#4 α 謎團(事實層,要核對)**:GitHub #4 標 closed,但實跑 mock(含 9 筆 run1)engine overview 仍印「真本事 α」→ 疑 PR #11 只改卡層、engine `:633-636` 未改。BACKLOG ISSUE-1 還列著。 -6. 需求落檔後,user-stories.md(#6)、BACKLOG↔GitHub 不同步(ISSUE-3 未上 GitHub)要對齊。 - ---- - -## 9. 實作現況(誠實) - -- **engine 乾淨未改**(`git diff` 空,行數 721)。本 session 嘗試加「state JSON 輸出」**未成功**(Edit 未真正套用),從零開始。 -- 已落地的是**設計文件**:本檔 + 兩份 PRD + BACKLOG ISSUE-3。**零實作**。 -- **天花板仍是 #3 Stage 0**:真人驗「卡有沒有戳中」,`dist/chat/` 已 build = 零工程門檻,**未跑**。owner 自己「要記憶」的反饋已是第一個真人信號,但「真的跑一次卡」還沒發生。 - ---- - -## 10. 接手指引(下個 session 從哪開始) - -**最小心臟實作(MVP of 新架構,不要一次做整個 OS):** - -1. **engine 加結構化 state 輸出**(唯一的 code 改動;設 `TR_STATE_OUT=` 才寫,不設則行為零變): - - 新增 `build_state(rows, rts, held, dims, overview, ab, rx)` → dict,含:`schema_version`、日期區間、`n_trades/n_round_trips/n_held`、`headline_dim`、`headline_metric`(sizing→`max_pos_pct`、攤平→`avgdown_count`)、`metrics{max_pos_pct, max_pos_ticker, avgdown_count, payoff, beta, alpha_ann}`、`rule`(下次只改那條文字)、`insufficient_data`(`len(rts)<3 or ab.n<60`)。 - - main() 結尾:`if os.environ.get("TR_STATE_OUT"): json.dump(build_state(...), open(path,"w"), ensure_ascii=False, indent=2)`。記得頂部 `import json`。 - - dim key 參考:`d_size["max_pct"]/["max_ticker"]`、`d_avgdown["count"]`、`overview["payoff"]`、`ab["beta"]/["alpha_ann"]`(ab 有 `note` 時這兩個給 None)。 -2. **SKILL.md 加初診/對帳雙模式**(prompt 層):開場讀 `~/.trade-coach/log.jsonl` → 空=初診/非空=對帳;對帳=讀上次 commitment.metric 比這次;收尾 append。第一次樣本不足 → 寫 `insufficient_data`,不硬出 commitment。 -3. **測試**:用 owner 真實對帳單(或 mock)按時間**切兩段**,跑「初診→對帳」,驗第二張卡有沒有真的基於第一張(不是重新初診)。 - -**但先想清楚**(本 session 反覆撞到的元問題):這一切實作,在 **#3 Stage 0(真人跑一次卡)** 之前做,有多少是必要、多少是過度設計?owner 自己已是首位用戶,最便宜的驗證是:拿真對帳單跑**現在的卡**一次,看戳不戳中,再決定記憶層怎麼長。 +The canonical implementation references are `skills/fomo-kernel/SKILL.md`, its routed flows and references, and `docs/skill-v2-architecture.md`. diff --git a/docs/research-skill-vs-agent-loop.md b/docs/research-skill-vs-agent-loop.md index cad80c5..6eb5f99 100644 --- a/docs/research-skill-vs-agent-loop.md +++ b/docs/research-skill-vs-agent-loop.md @@ -1,493 +1,141 @@ -# Research · Skill vs Agent Loop:整個產品做完之後,乘載形態要不要換? +# Research: skill versus agent loop -> 狀態:研究筆記(worktree 草稿,未進 issue) -> 日期:2026-07-07 -> 來源:owner 提問「整個產品做完了,skills 還適合乘載嗎?是不是該是一個 agent 裡有很多 skills/工具?假想 Claude for Trading,還缺什麼 harness」+ 現況體檢(SKILL.md/engine/issue #12/prd-investment-os.md)+ Claude Agent SDK 官方文件查證(2026-07-07) -> 定位:issue #12 開放問題 **#5(一個 OS vs 同引擎多前端)** 的 harness 層延伸;`prd-investment-os.md` 拍板的「同一薄引擎 + 雙前端」在本檔升級為「同一引擎 + 可換 harness」 +Decision update: the useful distinction is not "skill or agent." The product needs a deterministic review kernel, a thin reusable skill contract, and optional agent surfaces around the same lifecycle. ---- +## Reframing the question -## 0. Reframe:「skill vs agent loop」是假對立 +A skill is an invocation and guidance surface. An agent loop is orchestration, state, tool policy, and recovery. They solve different layers and can coexist. -「是不是應該是一個 agent 裡面有很多 skills or 工具」——**今天已經是了**: +The failure mode in the earlier architecture was implementing orchestration as a long prose skill: -``` -Claude Code = agent loop(harness:loop/工具/權限/記憶/UI/排程) -/fomo-kernel SKILL.md = 行為契約(能力單元) -engine/*.py (3165 行) = 確定性工具 -~/.trade-coach/ = domain 狀態(5+ 檔案) -AskUserQuestion, hooks = UI 原語與護欄 -``` - -Skill 從來不是 agent 的對立面——skill 是**搭在別人 agent loop 上的能力單元**。所以真正的問題是兩個: - -- **Q1(harness 所有權)**:繼續借 Claude Code 的通用 loop,還是自建專用 loop? -- **Q2(控制權分佈)**:哪些行為由 prompt(軟約束)保證,哪些由 code(硬約束)保證? - -Q2 比 Q1 急迫,而且 **Q2 的答案與 Q1 的選擇無關**——不管殼換不換,確定性都該往 code 搬。 - -## 1. 現況體檢:SKILL.md 已經在用 prose 實作 harness - -SKILL.md 現在 399 行(~25k tokens,官方建議 Level 2 <5k)。其中至少四類內容不是「行為契約」,是 **harness 職責用 prose 寫**: - -| 類別 | 現在的位置 | 本質 | -|---|---|---| -| 路由 dispatch(初診/對帳/試駕/snapshot-only) | 開場 prose | agent loop 的 dispatcher | -| 狀態演算法(Step 2.5 active-thesis 重建:revises/superseded/closed/exit_narrative 排除) | prose 演算法,每週讓 LLM 重新執行 | 純函式(錯一次 = 錯帳) | -| 消重/記憶管理(答過不重問三例外、exit capture 消重) | 鐵律 prose | state query | -| 收尾落盤腳本(~50 行 python heredoc) | 內嵌在 SKILL.md | 已是 code,住錯地方 | - -第五類是**防禦性鐵律**(絕不編動機/絕不印 thesis_questions/不准代選/別攤 5 維表)——每條都是一次「Claude 走歪」的 patch,本質是 prompt-space 的 assert。 - -**膨脹規律**:每加一個功能(賣出 capture、horizon 對帳、幣別)= 一段流程 + 幾條鐵律 + 幾條消重規則。線性增長不封頂,而 prompt 遵循度隨長度遞減。**Skill 形態的天花板不是「裝不裝得下」,是「遵循度 × 每次執行的變異」**。 - -但今天這個稅還付得起:低頻(週一次)、owner = 維護者、且 **迭代速度是最大紅利**(改 prose 就 ship)——當前卡點是「卡不夠好用」(owner 2026-07-05 判定),這正需要最快的迭代殼。 - -## 2. Skill 形態的結構性缺口(prompt 怎麼寫都補不了) - -對照「Claude for Trading」終局,四個缺口: - -1. **觸發權(initiative)**:skill 只在用戶開 session 時活。教練價值高峰在**決策前**(pre-trade gate)與**事件時**(exit_trigger 燒到、持倉異動)——現在連「週日提醒你復盤」都做不到。緩解:Claude Code 已有 desktop scheduled tasks(本機、app 開著就跑)與 cloud Routines(全無人值守、可 webhook 觸發),可廉價原型主動性,不必換 harness。 -2. **執行保證**:「絕不」寫在 prompt = 每次執行重擲骰子。金融場景的絕不清單(不下單/不外傳/不編數字)要求 code-grade。repo 已有正確前例——`.gitignore` 擋真實 CSV、hooks 擋未測試 commit——但**行為層**(卡上不出現 X、問過的不重問)還沒有等價物。 -3. **狀態 schema 所有權**:jsonl + prose 讀寫規則 = schema 活在 prompt 裡。engine 已在收編(ledger.py/revisit.py 有 CLI 子命令)——方向正確,還沒收完。 -4. **分發/計費形態**:skill 分發 = 用戶要有 terminal + Claude 訂閱(用戶額度買單、開發者零邊際成本);「Claude for Trading」的目標用戶(散戶交易者)不在 terminal 裡。App 化 = Agent SDK + API 計費(**開發者買單**)+ 自有 UI——這是商業決策驅動 harness 決策,不是技術偏好。 - -## 3. Agent SDK 查證結果(2026-07-07,官方文件) - -| 事實 | 對本 repo 的意義 | -|---|---| -| SDK 提供完整 harness 原語:loop、內建工具、MCP、hooks(7 型)、subagents、sessions/resume、permissions、AskUserQuestion | 自建 loop 的成本大半被 library 化 | -| **SKILL.md 可無痛搬進 SDK**(`setting_sources` + `skills` 參數,三層漸進披露保留;唯 `allowed-tools` frontmatter 改由 query 的 allowedTools 控) | **skill 投資不沉沒**——現在寫的行為契約就是未來 app 的行為契約 | -| SDK 不內建排程(外部 cron/launchd/Lambda);Claude Code 有 desktop scheduled tasks + cloud Routines | 主動性可以在 skill 形態先原型 | -| 官方分工:skills = 跨 surface 可攜能力;subagents = context 隔離工人;SDK = 需要 programmatic lifecycle / 嵌入自有 app 時 | 與本檔三階段路徑一致 | -| 計費:skill = 用戶訂閱額度;SDK 自建 = 開發者 API 帳單(2026-06 的計費分離改革延期中,勿依賴現狀) | 換 harness 的那天 = 商業模式要先想好 | - -來源:code.claude.com/docs/en/agent-sdk/overview、/agent-sdk/skills、/scheduled-tasks、/routines、anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills - -## 4. Claude for Trading 的 harness bill of materials(還缺什麼) - -| 組件 | 現在(skill on Claude Code) | 終局需要 | 缺口 | -|---|---|---|---| -| Agent loop | Claude Code 免費給 | 專用 loop(復盤/對帳/gate 多模式) | 小——SDK 可承接 | -| 確定性計算 | engine 3165 行,pure Python、tool-agnostic | 同,或再加 API 化 | **零——最大資產** | -| 行為契約 | SKILL.md + card-spec + AGENTS.md | 同,拆 per-phase | 小——可攜(已查證) | -| 狀態存儲 | jsonl + prose 讀寫規則 | schema 化 state store + migration | 中——engine 收編中 | -| 觸發/排程 | 用戶開 session | 盤後 cron + 事件 watcher + 推播 | **大(缺口一)** | -| 資料饋送 | 每週手動 CSV/截圖 | 券商 API(SnapTrade/Plaid 類)或持續手動 | **大(缺口二,且與「資料留本機」隱私鐵律有張力)** | -| 護欄 | prompt 鐵律 + gitignore + hooks | policy engine(絕不清單 code 化、render assert) | 中 | -| 對話 UI | terminal + AskUserQuestion | 卡片 UI + 一鍵回答 + 通知(demo-card.html 已是雛形) | 大(產品化時才付) | -| 記憶消重 | prose 消重鐵律 | engine query(「這週該問誰」) | 中——可先 engine 化 | -| 評測 | tests 八套 + evals/ 雛形 | 卡質量行為迴歸 | 中 | -| 計費/分發 | 用戶自己的 Claude | API 計費 or BYO-key or 訂閱 | 商業決策 | - -**關鍵洞察:兩個最大缺口(觸發權、資料饋送)與「skill vs agent loop」之爭正交**——就算今天自建 loop,還是得解券商資料與推播通道。所以「換 harness」不是解鎖終局的關鍵路徑;**把 engine 長成完整狀態機 + 原型主動性**才是。 - -## 5. 建議路徑:三階段,各有升級觸發訊號 - -### Stage 1(現在):skill 形態不動,開始降 prompt-space 稅 - -- 判斷:當前卡點是產品問題(卡不夠好用),換 harness 不解決,反而凍結迭代速度。 -- 動作:**確定性內容持續下沉 engine**—— - - 收尾落盤腳本 → engine 子命令(如 `engine/session.py close`) - - active-thesis 重建演算法 → engine 輸出(SKILL 只讀結果) - - 消重判定 → engine 輸出「這週該問誰」清單(dedup 已套用) - - 開場路由判定 → engine 輸出 route(初診/對帳/snapshot-only) -- **分界原則:SKILL.md 只留「怎麼跟人說話」(語氣/敘事/問法/鏡片),所有「怎麼算/怎麼記/問誰」下沉 engine。** -- 判準儀表板:SKILL.md 每條鐵律 = 未來一個 assert;每段流程演算法 = 未來一個函式。**鐵律的增長速度 = 該搬家的訊號強度。** -- 副作用即收益:SKILL.md 從 ~25k tokens 往 <10k 收,遵循度回升,今天就賺。 - -### Stage 2(訊號:主動性需求被驗證):同 engine 多入口 - -- 訊號:owner 自己想要「它來找我」;或週迴圈黏著驗證成立(連續 N 週用)。 -- 動作:scheduled task / launchd 盤後跑 engine scan(trigger 燒到 → 通知);skill 仍是對話前端;engine 長出 daemon 半邊。**零 harness 遷移。** -- 這是「Claude for Trading」最有辨識度的能力(主動教練)的最便宜驗證法。 - -### Stage 3(訊號:商業化拍板):Agent SDK 產品化 - -- 訊號:付費意願驗證 + 目標用戶明確不在 terminal + 願意從「用戶額度買單」翻轉成「開發者 API 帳單」。 -- 動作:SDK 承接 loop;SKILL.md 直接搬(已查證可攜);engine 原封不動;新建 UI/推播/券商連接/計費;**絕不清單全部 code 化**。 - -## 6. 終局圖像:對,「一個 agent 裡很多 skills」,但不是平鋪 - -五個生命週期階段(找資訊→選股→交易→update→recap)不是五個 skill 平鋪,而是—— - -``` -一個 dispatcher(路由到階段;漸進披露:對帳 session 不載入選股 prompt) -+ 共用狀態(engine 管,schema 化) -+ 各階段薄行為契約(現在的 SKILL.md 拆成 per-phase) -+ 少數 subagents(Phase C/D 研究類長任務,context 隔離) -+ 確定性全在 engine,護欄全在 code -``` - -Claude Code 自己就是這個圖像的參考實作(skills 平時只佔 name+description,觸發才載入)。 - -**一句話:engine 是產品,harness 是殼。** Skill 是第一個殼(迭代最快),daemon 是第二個半殼,SDK app 是第三個殼。確定性持續下沉 engine,殼就能隨商業形態換而不重寫產品——issue #12 開放問題 #5 的「同一薄引擎 + 多前端」在 harness 層依然成立,且查證後(SKILL.md 可攜)更成立。 - -## 7. 開放問題(下次討論) - -1. Stage 1 下沉的第一刀切哪裡?(建議:收尾腳本 → engine 子命令,最小、最無爭議) -2. 資料饋送的隱私張力:券商 API 進來後「資料留本機」鐵律怎麼守?(本機 daemon 拉、雲端不落地?) -3. pre-trade gate 的形態:skill 內的一個模式,還是獨立輕入口?(它的觸發時機在「下單前」,天生不在復盤 session 裡) -4. Stage 2 的通知通道選型:launchd + 本機通知 / cloud Routine / 手機推播,各自與隱私鐵律的相容性。 - ---- - -# Part 2 · 終局藍圖:一個獨立 agent 乘載完整決策鏈 - -> 2026-07-07 owner 追問的正面版:假設做一個獨立 agent,乘載 ① 買啥 ② 多少買 ③ 賣嗎 ④ 交易做得好不好、是否修改決策 ⑤ 績效/資金分布/風報比。架構長什麼樣、skills 與 loop 各放哪、harness 還缺哪幾塊。 - -## 8. 出發點:五項功能的計算本質不一樣 - -架構不該從「功能清單」出發,該從**每項的計算性質與時間性**出發: - -| # | 功能 | 判斷 vs 計算 | 時間性 | 風險 | -|---|---|---|---|---| -| ① 買啥 | **判斷為主**(檢索+合成,唯一需要多步 agent 推理的) | 非同步長任務(研究可跑很久) | 最高(建議責任、紅線) | -| ② 多少買 | **幾乎純計算**(風險預算/波動率/相關性),輸入才是判斷(conviction) | 決策當下,秒級 | 中 | -| ③ 賣嗎 | 一半計算(trigger 監控)+ 一半判斷(thesis 還成立嗎) | **事件驅動**(盤中/盤後) | 高 | -| ④ 好不好、改決策 | 混合(行為診斷計算 + 動機對話)= 現有 fomo-kernel | 週期性(週/月) | 低 | -| ⑤ 績效/分布/風報比 | **純計算**(α/β/exposure/drawdown/R) | 隨查 + 定期 | 低 | - -三個直接的架構推論: -- **② 和 ⑤ 根本不該是 LLM 的工作**——是 engine 的自然擴張,LLM 只負責人話↔參數的翻譯。 -- **① 是唯一真正需要 agent loop 多步推理的**——research 是長任務,必須 subagent 隔離(研究一支股票吃 50k+ tokens 的 filings/新聞,不能污染主對話)。 -- **③ 的形狀是 watcher daemon + 觸發後對話**——事件驅動,跟「用戶開 session」的互動形狀根本不同;這是終局必須自有 loop 的第一個硬理由。 - -## 9. 藍圖 - -``` - ┌────────────────────────────────┐ - │ Orchestrator(唯一對話面) │ ← 自有 loop(可以是 Agent SDK 的) - └────────────────────────────────┘ - 路由到五個模式;skills = 各模式的薄行為契約,漸進載入 - │① research │② sizing │③ exit │④ review │⑤ analytics - ▼ ▼ ▼ ▼ ▼ - [research [sizing [watcher [現有 [analytics - subagent] engine] daemon] engine] engine] - └────────────┴────────────┴────────────┴────────────┘ - │ - ┌───────────────────────┐ - │ Shared State(脊椎) │ positions / trades / theses / - │ schema 由 code 管 │ rules / **decisions log** - └───────────────────────┘ - │ - ┌───────────────────────┐ - │ Policy layer(code) │ 絕不自動下單 / 數字必來自 engine / - │ │ 建議必附 bear case / 不外傳 - └───────────────────────┘ -``` - -**分工的最終答案(skills vs agent loop)**: -- **Loop 自有**(事件驅動 + 背景研究撐不進 session 形狀),但用 Agent SDK 的 loop,不從零寫。 -- **Skills 不被淘汰,但降級**:從「執行單位」變成 **prompt 資產的組織單位**——五模式五份薄行為契約,平時只佔 name+description,進模式才載入(Claude Code 的漸進披露機制照搬)。執行保證全部上移到 code(policy/engine/state)。 -- **LLM 的職責收斂成三件事**:翻譯(人話↔參數)、合成(research)、對話(動機、決策呈現)。計算、監控、記憶、護欄,全部 code。 - -## 10. 五項的具體形態(與現有資產的接點) - -**① 買啥 → 做成「thesis builder」,不是「screener」。** 用戶帶 idea 進來(自己的想法、KOL 訊號——接 kol_collector),research subagent 跑結構化研究:bull/bear case、可證偽條件、driver 歸類(接現有 driver map)、與現有持倉的相關性檢查(「這是不是同一注」= B2 已有)。**輸出不是『買』,是一筆五要素 thesis 草稿——格式就是現有 theses.jsonl 的 entry(#136)。** 不生成「今天該買什麼」的 universe 掃描,只深化你自己帶進來的 idea:這同時是紅線內化(教練≠顧問)與產品差異化(市面 screener 一堆,深化+反面+同注檢查沒有)。 - -**② 多少買 → 規矩系統的前向應用。** 輸入:conviction(對話收)、風險預算與規矩(profile/log 已有)、既有 exposure(ledger 已有)、波動率(市場資料)。輸出:size 區間 + 理由 + **違反哪條你自己的規矩**(「你的規矩說最大單注 20%,這筆買滿會到 27%」)。pre-trade gate 就是這個模組的 UI——把復盤教練變成事前教練,fomo-kernel 哲學(拿你自己的話照你)零損耗前移。 - -**③ 賣嗎 → watcher daemon(計算)+ exit 對話(判斷)。** theses.jsonl 已有 exit_trigger/stop/review_trigger;現在是每週對帳人肉查三類,終局是 daemon 盤後掃:價格類 trigger 直接比,事實類 trigger(「營收失速」)派 subagent 查證。燒到 → 推播 → exit 對話:「你當初說的失效條件 vs 現在的事實」,用戶決策,落 decisions log。**絕不自動賣**(policy 第一條,是硬體結構不是 prompt)。 - -**④ 好不好、改決策 → 現有 engine + 一個閉環升級:決策審計。** 現在的復盤只看得到**交易(結果)**,看不到**決策(當時的輸入)**。①②③ 一旦落盤決策,④ 就第一次能做「決策品質 vs 結果品質」分離(好決策壞結果 ≠ 壞決策):當初 sizing 對嗎、research 的 bear case 料中了嗎、賣的理由 30/60/90 之後對帳(revisit 已有,擴到全決策鏈)。**這是五項合一的真正紅利——不是功能加總,是 ④ 的質變。** +- the agent had to remember step order +- required questions could be skipped +- numeric facts were copied manually +- state writes could partially succeed +- interrupted runs refetched live data +- public privacy depended on manual redaction -**⑤ 績效/分布/風報比 → 純 engine 擴張。** 已有:α/β、payoff、集中度、pp 拆帳。要加:時間加權報酬、drawdown、Sharpe/Sortino、exposure 矩陣(driver × market × currency,driver map 已有)、**per-thesis R multiple**(進場時 stop/target 定義了 1R,實際走出幾 R)——注意 R 的可算性依賴 ②③ 的資料紀律,這是五項互相咬合的典型例子。 +No amount of prompt wording makes those mechanics as reliable as code. -## 11. Harness 缺口重算(這個假想下,按建造依賴排序) +## Target architecture -**直接繼承(零或低成本)**:engine 確定性核心(④⑤ 的一半)、五要素 thesis schema(① 的輸出格式、③ 的輸入)、規矩系統(② 的約束源)、SKILL.md 行為契約(可攜)、本機隱私架構。 - -**缺的,按依賴序**: - -1. **Decisions log(決策事件流)——第一塊,且今天在 skill 形態就能開始補。** 現有 theses.jsonl + exit_narrative 已是雛形;缺:沒買的決策(研究過放棄的)、sizing 當時的理由、決策時的 context snapshot。沒有它,④ 永遠只能復盤結果。它是五項的共同脊椎。 -2. **NAV/市值序列 daemon。** SKILL.md 自己註明「帳戶 vs 大盤數字級對比給不了(需要每週市值序列)」——⑤ 的 drawdown/風報比全部需要 equity curve,而 equity curve 需要**持續的 mark-to-market 快照**(每日收盤存一行 NAV)。這是 daemon 的第二個硬理由(第一個是 ③ 的 trigger 掃描),兩者可以是同一支程式。 -3. **Policy engine——做 ①②③ 之前必須先有**(它們是高風險模組):不下單、數字必來自 engine(render assert)、建議必含 bear_case 欄位(schema 驗證,缺=不出)、同注檢查必跑。形狀:PreToolUse hook + 輸出 schema 驗證 + render diff。 -4. **推播通道**(③ 的觸發後半):本機通知 / 手機,與隱私鐵律的相容性待選型。 -5. **市場資料層升級**:yfinance 夠復盤;①③ 需要財報日曆、事件流、基本面——**成本中心**(數據授權費),也是「free skill → 付費產品」的天然分界線。 -6. **Research harness**(① 專用):subagent 定義 + 來源工具 + 反面強制。 -7. **Eval harness**:① 的研究質量、③ 的 trigger 誤報率——**敢自動化的前提**。誤報率高的 watcher 比沒有 watcher 更糟(狼來了→通知全關)。 -8. **合規/免責層**:①②③ 上線 = 從「復盤工具」變「決策支援」;自己用 vs 分發給別人的法律定位差異巨大(prd-investment-os.md 的雙前端開關在這裡從產品設計變成合規結構)。 - -## 12. 建造順序:逆著決策鏈走 - -直覺順序是 ①→⑤(從買啥開始),**正確順序幾乎相反:⑤ → ② → ③ → ④閉環 → ①**: - -- **⑤ 最便宜**(純 engine 擴張,今天可做),且立刻提升現有卡的可信度。 -- **② 是差異化最強的**:市面 stock picker 一堆,「拿你自己的規矩在你下單前照你」沒有;而且它只是規矩系統前移,幾乎零新 harness。 -- **③ 帶入第一塊新 harness(daemon)**,但資料(trigger)已經在收。 -- **④ 的決策審計隨 decisions log 自然出現。** -- **① 最後**:最貴(數據+eval+合規)、紅線最險、市場最擁擠。 - -深層理由:**① 是唯一「無中生有」的判斷,②③④⑤ 全是「拿你自己的話對你」**(你的規矩、你的 thesis、你的 trigger、你的決策)。fomo-kernel 的靈魂(鏡子不是審判、教練不是顧問)在 ②-⑤ 零損耗保留,只在 ① 真正踩線——所以 ① 做成 thesis builder 而非 screener,把紅線內化成產品形狀,而不是靠 prompt 守。 - -## 13. 一句話收斂 - -終局架構 = **一個自有 loop 的 orchestrator + 五份薄 skill(行為契約)+ 一個持續長大的確定性 engine 家族 + 一條 code 化的 policy 層 + 一根 decisions log 脊椎**。缺的 harness 不是「更聰明的 agent」,是 agent 周圍的機械:decisions log(記憶脊椎)、daemon(事件+NAV 的半邊)、policy engine(絕不清單)、市場資料層(成本中心)、eval(敢自動化的前提)。而其中兩塊(decisions log、⑤ 的 engine 擴張)今天在 skill 形態就能動工,做了就直接變成終局的地基。 - ---- - -# Part 3 · 用 investment_note 校準:需求的 ground truth - -> 2026-07-07 第三輪:owner 要求「基於 investment_note 的設計,整體探索方向和需求」。investment_note(owner 私有 repo,獨立系統)是**已經在跑的全生命週期投資系統**——它的每個組件是一次「真的需要」的證據,每條 protocol 是一次事故換來的 harness 需求,每個變重的角落是一次「這形態不行」的反面教訓。本節只引其**系統設計與方法論**,不含任何持倉/交易內容。 - -## 14. 第一個發現:終局不是假想,activo 原型已經在跑 - -investment_note 現況 = **24 個能力單元掛在 Claude Code 上**(13 個自家 skill + equity-research 8 命令 + financial-analysis 3 命令),加 4 條 protocol、7+ 個確定性工具、evals/ 雛形、以及固定 cadence(daily 5min mobile / after-trade / around-earnings / weekly 30min / monthly 1hr / quarterly 2hr)。對照 Part 2 的五項: - -| Part 2 的五項 | investment_note 現有實作 | 狀態判讀 | -|---|---|---| -| ① 買啥 | `/screen`、`/initiate`(全套 initiation:research/model/valuation/report)、`/thesis`、`/sector`、`/13f`、`/verify-kol-claim`、BACKLOG 72hr 冷卻、三 Gate | **在跑,工具最多**;痛點是驗證鏈路被持倉 narrative 污染(見 §16) | -| ② 多少買 | RULES.md 按市值分級的部位上限(量化)+ Pre-Trade Checklist + cooling periods(新標的 72hr/加碼虧損 1 週/獲利再進 2 週) | **規格全有、量化完畢,缺 enforcement**——全靠自律照 checklist | -| ③ 賣嗎 | Rule #5(單一持倉 -20% → 強制書面復盤)、Rule #6(主題 ETF 連 3 月跑輸 SPY >10% → 強制 review,對照表已定)、`/weekly-watch`(falsification trigger)、catalyst 日曆 | **watcher 的規則引擎規格已經寫好,缺 daemon**——現在靠每週人肉跑 | -| ④ 好不好 | `/record-trade`(update/decision/revisit 三模式)、`/review-mistakes`、`/ai-scorecard`、mistakes.md(數百筆交易的人類錯誤庫,與 ai-errors.md 嚴格分開) | **最成熟**,fomo-kernel 已在吸收(prd-investment-os.md 第 4 節) | -| ⑤ 績效/風報 | portfolio.md health check、`mark_portfolio.py`(唯一寫 derived 欄位者) | **最薄**——fomo-kernel 的 α/β/payoff 反而更強,回流方向成立 | - -**推論:五項的「需求存在嗎」不用再驗證——全部有活實作。剩下的只有形態遷移問題。** - -## 15. 紅線已經演化:「絕不建議」→「結構化雙面」(2026-07-04) - -Part 2 說「① 做成 thesis builder 不是 screener」——**要修正**。investment_note 2026-07-04 已把「Never give buy/sell advice」正式改成「**AI 可給方向性建議,但決策型輸出必須結構化雙面**」:(1) bull+bear 同口徑、bear 須引 base rate/歷史反證、篇幅相當、禁稻草人 (2) falsifiers 量化可證偽 (3) options ≥2(sizing 只能寫成「路徑+各自代價」,不可單向指令)(4) 口徑一致 (5) 可表態,但**傾向擺在雙面之後** (6) **RULES #3 凌駕:thesis broken → 只能減碼/出場,禁任何加碼傾向**。 - -這改寫終局 ① 的設計:紅線不是「不給答案」,是「**答案的結構被強制**」——這比 prompt 鐵律高一級,因為它是 **schema 可驗證的**(缺 bear case / 缺 falsifier / options<2 → policy 層直接擋下不出)。「結構化雙面」正是 policy engine 最該 enforce 的輸出契約,也是「自己用開選股」與「分發版關選股」之間的第三條路:**分發版的 ① 可以開,但輸出契約鎖死在結構化雙面 + 過程支援**。 - -## 16. 最值錢的資產:事故史 = harness 需求的實證清單 - -investment_note 的每條 protocol 都「源自 #NN 事故」(ai-errors.md 編號),直接翻譯成終局 harness 需求——**這份清單不是設計出來的,是失血換來的**: - -| 事故(實證) | protocol(prompt 層防線) | 終局 harness 需求(code 層) | -|---|---|---| -| 憑記憶斷言 ticker 事實,#027 family **復發計數持續增加** | **Provenance 不變量**(default-deny:記憶不是出處,事實必須來自 repo 或本回合 search) | **policy 層第一公民**——比 Part 2 的「數字必來自 engine」更廣:所有事實斷言的出處驗證,做成輸出 gate 而非提醒 | -| 下市股 stale cache 當 live → 全表 audit **24% error rate** | Stock Data Hygiene(listing status/corporate action/baseline trading day/cross-source/reverse-compute) | **市場資料層不是「接個 API」,是 hygiene pipeline**——終局最被低估的重活;fomo-kernel 的 yfinance 直取在 ①③⑤ 的精度要求下不夠 | -| 並行寫入靜默改寫 portfolio 真相(#023,毀滅性) | Write Lanes(state/canonical/generated/governance)+ 單一寫入者鐵律 | **shared state 的 lane 治理**:哪個模組能寫哪段狀態,code enforce(fomo-kernel 的 append-only jsonl + engine 單一寫入路徑是同一思想的正確起點) | -| 時序幻覺幾乎污染交易(#001) | Fact Check(日期絕對化/來源 URL/時間軸定位/狀態標記) | 研究輸出的 **schema 驗證**(每個事件必附 YYYY-MM-DD + URL,缺 = 不出) | -| ≥3 次「在持倉 narrative 下驗錯被打臉」 | (SUBAGENT_PROPOSAL 的解法,見 §17) | **認知隔離**:position-blind 驗證鏈路 | - -還有一條 meta 級發現:investment_note 自己已寫下「**觸發詞一律降級為示例,真正閘門 = 不變量**」+「**同 root cause 失效記為復發計數,計數器 = 該動結構、禁止再加觸發詞的訊號**」——這與 Part 1 §5 的「鐵律增速 = 搬家訊號」是同一個發現的兩個獨立實證。**owner 已經有 prompt 防線極限的一手數據。** - -## 17. SUBAGENT_PROPOSAL:owner 自己的架構判準,直接繼承進藍圖 - -investment_note 的 SUBAGENT_PROPOSAL.md(經 Codex+Gemini 雙審到 v2)給了四個可直接繼承的裁決: - -1. **subagent 的價值點只有 4 個**:隔離 context、工具硬限制、角色一致性跨 session、並行多視角——不滿足任一,維持 skill。這比 Part 2 的「research 才 subagent」更精確:**subagent 化的理由是認知結構,不是任務大小**。 -2. **position-blind 硬規定**:claim-verifier 不讀 portfolio、不讀 ticker wiki,只收單句 claim——「一旦讀了持倉就不是乾淨 context,等於自欺」。**這是 Part 2 藍圖漏掉的關鍵架構需求**:多 agent 的第一個理由不是 context 大小,是**驗證者與持倉者的認知隔離**(對應行為金融的 confirmation bias,有 ≥3 次實證)。終局藍圖修正:③ 的「事實類 trigger 查證」與 ① 的 bear case 研究,都必須走 position-blind 通道。 -3. **claim-type 分級證據門檻**:raw-fact(1 個官方一手)/ derived-metric(公式可重算)/ market-claim(一手+高品質二手交叉)/ **causal-claim(最高只能 supported,永不 confirmed)**——研究輸出的置信度分級,直接是 ① research harness 的輸出 schema。 -4. **eval-gated 部署 + 校準前禁 action**:先用歷史錯誤案例回測、量化 false-confirm/false-refute rate,達標才放行;未校準前 thesis-adversary 禁止輸出 hold/review/cut,只給 broken_pillars。**「敢自動化的前提是 eval」在 owner 方法論裡已經成文**——Part 2 §11 第 7 項有了本地實證與現成做法(evals/golden-cases.md 起步中)。 - -## 18. 形態的反面教訓(terminal 訊號,別重蹈) - -- **重系統自診**:11 欄決策 narrative、portfolio.md 治理、weekly-review 長文 journal——owner 自己診斷「用分析取代行動」,prd-investment-os.md 已判砍。終局 agent 的 form budget(一張卡)是對這個教訓的制度化,不是美學偏好。 -- **24 個能力單元的觸發路由已經出現實測痛點**:momentum 三合一提案被 Gemini 以「觸發誤判」反對(裁決:入口不合併、後端抽共用 lib)。**skills 平鋪的路由問題有實證**——Part 2 藍圖的 orchestrator/dispatcher 不是過度設計,是已發生的需求。 -- **protocol 層 ~180 行全靠 prompt 遵循 + hook 提醒**:UserPromptSubmit hook 注入提醒是「唯一能在純對話端生效的防線」——owner 已經摸到 prompt 防線的天花板,policy code 化是自然下一步。 - -## 19. 兩 repo 關係的重定位:雙前端已經是現在式 - -prd-investment-os.md 把「雙前端」寫成未來設計——**實際上它已經以兩個 repo 的形式存在**: - -``` -investment_note = 「自己用」前端的現在式:全生命週期、外部資料、自己的 RULES 當尺、重、私有 -fomo-kernel = 「別人用」前端的現在式:recap only、純本機、可換鏡片、輕、公開、有確定性 engine -缺的一塊 = 共用核心:兩邊的確定性工具是平行的兩套 - (investment_note: mark_portfolio/scanner/audit ↔ fomo-kernel: trade_recap/ledger/revisit) +```mermaid +flowchart TB + U["User or scheduled entry"] --> S["Thin skill or application adapter"] + S --> O["Deterministic review orchestrator"] + O --> E["Mechanical analysis engine"] + O --> V["Schemas and validators"] + O --> P["Canonical local session store"] + O --> R["Private and public renderers"] + A["Agent judgment"] --> V + V --> O ``` -終局 agent 的公式:**investment_note 的功能(需求已全部實證)× fomo-kernel 的形態紀律(三道閘、一張卡、engine 化、隱私鐵律)× 一套共用 harness(policy/state lanes/daemon/eval)**。 - -## 20. 校準後的方向與需求(整合三個 Part) - -**Harness 缺口重排**(依 investment_note 實證的痛度,取代 Part 2 §11 的依賴序): - -1. **Provenance/輸出 gate(policy engine 第一條)**——不是「不下單」(題目未至),是「記憶不是出處」(#027 family 復發計數還在漲,天天失血)。含:結構化雙面 schema 驗證、事實必附出處、causal 永不 confirmed。 -2. **資料衛生 pipeline**——24% error rate 的教訓;①③⑤ 的精度全部依賴它;成本中心與護城河同體。 -3. **Enforcement 缺口(pre-trade gate)**——② 的規格量化完畢(分級上限/三 Gate/cooling periods)但全靠自律;把 checklist 變閘門是所有缺口裡**規格最完備、離可做最近**的。 -4. **Watcher daemon**——③ 的規則(-20% 強制復盤/ETF 對照輪動偵測/catalyst 日曆)已成文,缺執行體;與 NAV 序列(Part 2 §11-2)同一支 daemon。 -5. **認知隔離架構(position-blind subagents)**——claim-verifier 已設計完(v2),實施順序第一;≥3 次實證痛點。 -6. **State lanes 治理**——#023 級風險;fomo-kernel 的 append-only + 單一寫入路徑起點正確,擴到多模組時 lane 表 code 化。 -7. **Eval harness**——golden-cases 起步 + 回測 gate 方法論已成文;每個要自動化的模組先過它。 -8. **Mobile 介面**——workflow cadence 的 daily 5min 是 mobile(claude.ai/code 實際在用):終局 UI 需求不是猜的,晨間場景已存在。 - -**建造順序修正**(Part 2 §12 的 ⑤→②→③→④→① 之上,加雙前端分岔): -- 「自己用」前端:① 不用等——它已經在跑(screen/initiate/13f),該做的是給它套上 §15 的輸出契約 + §17 的 position-blind 驗證鏈。 -- 「分發」前端:順序不變(⑤→②→③→④),① 最後且鎖結構化雙面;fomo-kernel 現在的 recap(④)繼續當灘頭堡。 -- 兩邊共同的第一步不變:**decisions log + engine 下沉**——investment_note 的 `/record-trade decision`(11 欄,太重)與 fomo-kernel 的 theses.jsonl(五要素,較薄)本來就是同一個東西的兩個形態,收斂它們 = 共用核心的第一塊磚。 - -## 21. 開放問題(接續 §7,新增) - -5. 共用核心的物理形態:fomo-kernel engine 抽成獨立 package 給兩 repo 用?還是 investment_note 逐步改 import fomo-kernel?(牽動公開/私有邊界)→ **Part 4 回答:不做共用 package,直接單一產品替代** -6. 結構化雙面的 schema 落在哪:fomo-kernel engine(公開、可分發)還是 investment_note tools(私有)?它是分發版 ① 的前提。→ **Part 4 回答:落 fomo-kernel(機制屬產品)** -7. mistakes.md(人類錯誤庫)與 fomo-kernel 的規矩/教訓迴圈是否同一資料模型?(畢業機制 #137 已有 STRATEGIES→RULES 的 6 個月/3 次觸發原型可借) -8. daily morning note(generated lane、disposable)是否屬於終局 agent?它是黏著度最高的 cadence,但也最接近「資訊消費」而非「決策支援」。 - ---- - -# Part 4 · 融合路線:單一產品替代,不是雙前端並存 - -> 2026-07-07 第四輪,owner 方向拍板:「investment_note 是我自己長出來的東西,希望用一個對外產品來替代——build for public 但我自己用得很開心。」這修正 prd-investment-os.md 的「雙前端並存」表述。本節評估可行性、給融合的正確形狀與風險。 +The agent contributes interpretation, thesis inference, and qualitative framing. Code owns facts, gates, persistence, recovery, and privacy views. -## 22. 直接回答:可以融合,而且已經在發生——但「直接」的形狀是三分法,不是合併 repo +## What remains a skill -**第一個證據:record-trade 的替代已經進行中。** Phase B(ledger/thesis/revisit/賣出 capture)就是把 `/record-trade` 的功能吸收進公開產品,驗收標準已定:「owner 連續兩週不開 /record-trade」。**「用公開產品替代自用系統」不是待驗證的假設,是有第一個案例、有可測量驗收的既成路徑**——問題只剩「能走多遠、按什麼順序」。 +- trigger description and product boundary +- single CLI entry point +- route selection guidance +- agent judgment boundaries +- progressive-disclosure links to flow and policy references -把 investment_note 的組件按融合性質分三類: +The skill should not contain a second renderer, state machine, or long list of conditional disclosures. -**A 類 · 機制直接進產品(功能已驗證、形態可收斂、無隱私/紅線障礙)** -- record-trade 全家 → Phase B 進行中 -- RULES/STRATEGIES 規矩系統 → 產品的規矩 + 畢業機制(#137 直接借 STRATEGIES→RULES 的「≥6 個月驗證 + ≥3 次觸發都對」原型);**個人參數(分級部位上限、cooling 時長)= profile 設定,產品給機制、用戶填自己的尺** -- Pre-Trade Checklist / 三 Gate / cooling periods → pre-trade gate 模組(②) -- watcher 規則(-20% 強制復盤、ETF 對照輪動偵測)→ daemon(③),對照表做成預設可改 -- Thesis Quality Checklist 五維 → 五要素 thesis 的品質檢查 -- mistakes 錯誤庫的資料模型 → 教訓迴圈(revisit falsified → 教訓段) -- claim-verifier / thesis-adversary(position-blind)→ 產品的驗證鏈 -- protocols(Provenance / Fact Check / Data Hygiene)→ **升維,不是搬運**:180 行 prose 變 policy 層 code,這是它們的正確歸宿(§16) +## What belongs in orchestration code -**B 類 · 容器公開、內容永遠私有** -- wiki/{TICKER} 的 thesis/falsification → 產品的 thesis 庫(theses.jsonl 富化);機制公開,你的內容留 `~/.trade-coach/` -- portfolio → ledger 已承接 -- 個人投資觀 / RULES 參數 → profile -- 歷史資產(數百筆交易、錯誤庫、wiki 存量)→ **一次性 import 工程,別低估** +- `prepare -> preview -> finalize` +- required question queue and deduplication +- active thesis reconstruction +- evidence and schema validation +- stable session fingerprints +- atomic canonical commit +- idempotent retry and fail-closed conflict handling +- projection repair +- independent public rendering -**C 類 · 不融合(裝進去會把產品搞死)** -- equity-research / financial-analysis plugins(initiate/dcf/comps/sector):SUBAGENT_PROPOSAL 自己判過——「plugin 全是 sell-side 機構流程,自家 skill 是 buy-side 散戶紀律,重疊度低」。它們是通用研究工具,不是產品差異化;**且不需要融合——終局 agent 跑在同一 harness 上(Claude Code / Agent SDK),plugin 生態天然共存,你照裝照用** -- morning-note 等資訊消費類:黏著高但離「決策支援」最遠(§21-8),可選模組或不做 +## Why this preserves analytical quality -## 23. 關鍵反轉:從「減法雙前端」到「加法單一產品」 +Agent flexibility is valuable where the input is ambiguous or contextual: -prd-investment-os.md 的公式是**減法**:`別人用 = 自己用 − 選股/找資訊 − 外部連接`——從重往輕閹割。單一產品把它反轉成**加法**: +- brokerage field interpretation +- motive and evidence interpretation +- inferred thesis wording +- qualitative counterfactual and mirror +- observations that may justify another preview -``` -產品核心(所有人一樣) = 卡、對帳、規矩、thesis 迴圈(現在的 fomo-kernel) -+ opt-in 模組(逐個解鎖) = pre-trade gate、watcher、研究驗證(position-blind)、績效深化 -owner = 全模組開啟的第一個 power user -``` - -**加法才守得住形態**:每個模組進產品要過三道閘(改下一筆 / form budget / 盡量自動),而閹割永遠閹不乾淨。這也治 investment_note 的病——「用分析取代行動」的重形態,遷移到 form budget 由產品定義強制執行的殼上,你自己也被它保護。 - -紅線不再需要功能開關:§15 的「結構化雙面」讓選股支援可以進公開產品——不是「自己用開、別人用關」,是**輸出契約鎖死**(缺 bear case / 缺量化 falsifier / options<2 → policy 層不放行)。 +Agent flexibility is harmful where variation has no analytical upside: -## 24. 遷移方法論:逐模組,驗收 =「你不再回去用」 +- arithmetic +- cycle IDs and rankings +- whether a required question was answered +- whether evidence is complete +- ETF class exemption +- write order and recovery +- privacy redaction -把 record-trade 的驗收判準推廣成整個融合的方法論:**每個模組融合完成的定義 = owner 在 investment_note 不再使用該功能**(連續兩週為觀察窗)。逐模組、可測量、不搞大爆炸遷移。順序沿 §20: +The architecture narrows procedural variance while preserving reasoning variance. Different capable agents may write different narratives from the same facts, but none can silently alter the facts or skip the lifecycle. -1. Phase B 收尾(record-trade 替代,驗收中) -2. ~~規矩系統參數化 + 畢業機制(#137)~~ → **已完成(2026-07-07 PR #146,main `347c14c`):問題帳三層架構 supersede 畢業設計**——rules.jsonl 多條規矩綁 problem_key、revises 演變線、broke/held/skipped 對位、held_streak≥2 靜默調度;個人規則庫已落地,且 pre-trade gate 查全集的前置已就緒(roadmap v1b) -3. pre-trade gate(規格最完備、你自己最缺 enforcement、產品差異化最強;**#146 後前置已備,離可做更近**) -4. watcher daemon(規則已成文) -5. position-blind 驗證鏈(claim-verifier v2 設計已完) -6. thesis 庫富化 + 歷史 import +## Product surfaces -investment_note 的終態:**archive(歷史檔案,唯讀)+ C 類長尾(plugin 研究工具照用)**——不是刪除,是退役。 +The same kernel can support: -## 25. 誠實的風險清單(這條路的真實代價) +- an installed coding-agent skill +- a desktop or CLI review flow +- an owner-only research surface +- a future scheduled due-review entry point -1. **雙用戶撕裂**(此模式的著名死法):為你加的深度嚇跑陌生用戶,為陌生用戶做的簡化讓你回頭用 investment_note、dogfood 斷。§23 的加法分層是解法,但每次「為自己加模組」都要問:它是 opt-in 還是改變了核心體驗? -2. **隱私鐵律要重新表述**:你自己用需要外部呼叫(行情、web 驗證、13F),「純本機零外部」會被 opt-in 模組打破。新表述:**交易資料永遠本機不外傳;opt-in 模組的外部呼叫只查公開市場資料,不攜帶你的持倉**(position-blind 剛好同時是認知需求與隱私需求)。market_context.py 已有先例(yfinance 查公開行情 ≠ 交易資料外傳)。 -3. **import 工程的包袱**:數百筆交易、wiki 存量、錯誤庫——一次性但不小;做不好,你的「用得開心」從第一天就折損(教練失憶)。 -4. **dogfood 偏誤是雙向的**:你用得開心 ≠ 別人用得開心(你有 24 個工具的肌肉記憶與完整 context);反饋管道(#42)與 Stage 0 的真人驗證要持續,別讓「自己爽」遮住「卡不夠好用」。 -5. **節奏風險**:融合是多季工程,期間兩系統並存、狀態雙寫的窗口最容易出 #023 型事故——每個模組切換時明確「單一寫入者換邊」的時點,寫進遷移 checklist。 +Each surface should be a thin adapter. Do not duplicate the review contract for Claude, Codex, Cursor, or a future UI. -## 26-a. 形態答案:吸收功能,不吸收形態——「一堆 skill」是沉積,不是設計 +## State and privacy -> owner 追問:「investment 本質是一堆 skill + 流程 + 規範,和 fomo-kernel 差異較大——抽過來時,產品或落地形態該變嗎?」 +Local state is a product requirement, not an implementation detail. Canonical immutable session bundles support: -**答案:對外形態不變(單入口 + 一張卡),內部結構演化。** 理由: +- longitudinal rule reconciliation +- append-only thesis evidence +- interruption recovery +- reproducible cards +- deterministic projection repair -1. **investment_note 的「13 skill + 4 protocol + cadence」不是被設計成這樣,是沒有 form budget 的自然沉積**——每個需求加一個 skill、每次事故加一條 protocol、每個節奏寫一段 cadence 靠自律。owner 自診「用分析取代行動」+ 觸發路由誤判(§18)就是這個形態的代價。issue #12 的第一原則「吸收功能,不吸收形態」正是為這一刻定的。 -2. **兩者形態差異的本質是「單位」不同**:investment_note 的單位是**工具**(skill = 一個動作,用戶自己編排流程)= 工具箱;fomo-kernel 的單位是**迴圈**(狀態讀→診斷→問→卡→承諾→下週對帳,產品編排流程)= 教練。**抽功能的正確動作是把「工具」熔成「迴圈裡的站點」,不是把工具箱搬家**: - - watcher 規則 ≠ 新增 `/check-rotation` 指令 → = 對帳開場的一段(「你的持倉觸發了輪動檢視線」)+ 未來 daemon 主動通知 - - pre-trade checklist ≠ 用戶自己記得跑的 `/pre-trade-check`(現在 RULES checklist 靠自律 = 失效中)→ = 「買之前來說一聲」的教練入口,gate 過程對話完成 - - claim-verifier ≠ 用戶指令 → = 迴圈內部零件(subagent),被 gate/對帳在需要時呼叫 -3. **對外入口收斂規則**:主入口一個(`/fomo-kernel` 週迴圈);pre-trade gate 是唯一值得考慮的第二入口(觸發時機在「下單前」,天生不在復盤 session 裡,§7-3);daemon 通知是第三種觸達但非指令。內部模式/模組可長(SKILL.md 拆子檔案漸進載入,card-spec.md 已是先例),**對外面永遠收斂**——issue #12「內部層可長,外部面收斂」從輸出推廣到入口。 -4. **規範(protocols)升維不搬運**(§16):可機械判定的進 engine/policy code,SKILL.md 只留少數需要 LLM 遵循的鐵律——不會出現「fomo-kernel 版的 180 行 protocol 章」。 -5. **形態真正變的兩個時點**(即 Part 1 的 Stage 2/3):daemon 半邊加進來(中期,加背景執行體,對話面不變)與 SDK 換殼(遠期,商業決策)。在那之前,任何「要不要多開一個 skill/入口」的衝動,先過三問:**它進哪個迴圈?輸出上不上卡?真的需要新入口嗎(預設 no)?** +Public sharing is another renderer over structured facts, not a cloud synchronization feature and not a redaction pass over private prose. -## 26. 收斂:三個 Part 的答案疊起來 +## Migration stages -- Part 1:**engine 是產品,harness 是殼**——skill 殼迭代最快,現在不用換。 -- Part 2:終局 = orchestrator + 薄 skills + engine 家族 + policy 層 + decisions log 脊椎;缺的是 agent 周圍的機械。 -- Part 3:investment_note 證明需求全部真實,並貢獻事故換來的 harness 清單與架構判準。 -- **Part 4:融合方向 = 單一公開產品,加法分層,逐模組替代,「你不再回去用」為驗收**——fomo-kernel 不是 investment_note 的閹割版出口,是它的**下一個形態**;investment_note 是需求探礦場,採完的礦進產品,採不完的(sell-side plugin 生態)共存不融。 +### Stage 1: deterministic kernel ---- +Completed in v2: lifecycle, schemas, validation, canonical sessions, recovery, and renderers. -# Part 5 · 從零抽象:investment_note 作為一個產品的 pseudo code,與 skill 任務明確性檢驗 +### Stage 2: thin additional entry points -> 2026-07-07 第五輪,owner:「跳脫 fomo-kernel 框架,把 investment_note 抽象成一個產品會怎麼做?給簡單 pseudo code,對照 best practice——我預期一個 skill 給這麼多任務會違反任務明確原則,確認我的理解。」 +Add only after a real need is validated. Every new entry point must call the same orchestrator and preserve local privacy. -## 27. Pseudo code:不從 skill 出發,從「它什麼時候必須存在」出發 +### Stage 3: productized agent application -```python -# ================= 抽象:一個投資流程 OS ================= +Consider a dedicated agent runtime only when proactive due reviews, multi-tool coordination, identity/authentication, or commercial distribution require it. The runtime would host the existing kernel; it would not replace it with a new prompt. -state = { - "portfolio": ..., # 持倉真相(每段唯一寫入者) - "theses": [...], # 每檔:why / falsifiers / horizon / size 理由 - "rules": [...], # 你的紀律:caps / cooling / gates(量化、可判定) - "journal": [...], # decisions + mistakes,append-only -} +## Multi-skill question -# ---------- 觸發面:三種,缺一不可 ---------- +A future investment application may expose several clear skills: trade review, thesis capture, due revisit, and research evidence. That is compatible with one agent as long as: -on user_intent(intent): # 「我想買 X」「幫我復盤」「查證這個說法」 - match intent: - case pre_trade(action): - violations = check(action, state.rules) # 確定性 - dialogue = two_sided(action, state.theses) # LLM:結構化雙面 - state.journal += decision(action, 用戶答案) - case review(): - reconcile(state.journal.last_commitment) # 對帳先行 - card = converge(diagnose(state), ask_motive()) # 收斂一張卡 - case research(idea): - draft = blind_verify(idea) # position-blind 子代理 - state.theses += draft # 輸出=thesis 草稿,非「買」 +- each skill has a narrow trigger and output +- shared state transitions live in one kernel +- skills cannot bypass the same privacy and recommendation boundaries +- one user task selects a clear route rather than loading every instruction at once -on schedule(cadence): # daily / weekly / monthly / quarterly - due = scan_revisits(state) + scan_rules(state) # 全部確定性 - if due: notify(due) # 只召喚,不出卡 - -on market_event(tick): # 盤後 daemon - for t in state.theses: - if burning(t.falsifiers, tick): notify(t) # -20% / 輪動 / catalyst - -# ---------- policy 面:跨全部路徑的硬約束(code,非 prompt)---------- -policies = [ - provenance, # 任何事實斷言必有出處(state 或本次檢索) - no_execution, # 絕不下單 - structured_two_sided, # 決策型輸出缺 bear / falsifier 不量化 / options<2 → 不放行 - single_writer, # state 每段唯一寫入者 - form_budget, # 輸出永遠一張卡 / 一則通知 -] -``` +The wrong design is a flat collection of overlapping skills that each writes its own state. The right design is multiple thin capabilities over one state machine. -這個抽象揭示 investment_note 的本質 = **三種觸發 × 一份 state × 一層 policy**。對照現況:24 個 skill 只是 `user_intent` 的 match arms 被攤平成頂層指令;`on schedule` 沒有執行體(cadence 寫在 CLAUDE.md 靠自律);`on market_event` 完全缺席;policies 用 ~180 行 prompt 寫。**它不是「一堆 skill 的系統」,是「只有一種觸發面可用的系統」——形態是被 Claude Code 當年只有 user_intent 觸發這個限制塑形的,不是需求長這樣。** +## Build triggers -## 28. Best practice 檢驗:「一個 skill 這麼多任務」違反任務明確嗎? +Upgrade the runtime only when evidence shows a missing capability: -**owner 的直覺對一半——問題是真的,但切割維度要修正。** - -官方 Agent Skills 的判準不是「一個 skill 只能一個 task」,而是三條: -1. **觸發明確**:用戶意圖 → skill 的映射無歧義(靠 description)。 -2. **載入明確**:漸進披露——Level 1(name+description,~100 tokens,常駐)/ Level 2(SKILL.md body,**<5k tokens**,觸發才載)/ Level 3(子檔案資源,按需才載)。官方 docx/pptx skill 就是「一個 domain、多個 task、子檔案按需」的參照實作。 -3. **執行契約明確**:每個任務有可測試的輸入/輸出。 - -用這三條照兩個系統,結論反直覺: - -- **skill 顆粒度上,「多」才是病**:investment_note 13 個平鋪 skill 在第 1 條失分——momentum 三入口路由誤判是實測(§18),SUBAGENT_PROPOSAL 的 Gemini 裁決已確認「入口不合併」是為了保觸發明確,但那是在平鋪前提下的局部最優。一個 domain 一個入口(`/fomo-kernel`)在第 1 條反而是滿分:「復盤交易→這裡」無歧義。 -- **fomo-kernel 的真病在第 2 條**:SKILL.md 399 行 ~25k tokens **全量載入**——對帳模式也載著試駕、幣別、初診、收尾腳本的全文,超官方建議 5 倍。owner 感覺到的「任務明確被違反」,病根不是「任務多」,是「**所有任務的 prose 同時在場**」(遵循度隨長度衰減,Part 1 §1 的稅)。 -- **另一半病根:兩種非對話觸發被硬塞進對話形態**——cadence 與 market_event 本來就不屬於 skill 的職責(§27 的觸發面分離),寫進 SKILL.md 靠 LLM 記得「開場先 scan」才真正違反任務明確。 - -**修正後的理解**: -``` -錯誤解法:拆成多個 skill → investment_note 的老路(觸發歧義) -正確解法:skill 層 = domain(一個入口) - mode 層 = task(SKILL.md 瘦成 dispatcher <2k:路由+鐵律; - 每 mode 一份子檔案 gate.md / review.md / research.md, - 觸發才載——card-spec.md 已是先例) - cadence / market_event = 不進 skill,進 harness 觸發面(scheduled / daemon) -``` +- users need proactive reminders rather than return-time memory +- a UI must coordinate several authenticated data sources +- the local CLI prevents adoption despite successful card value tests +- operational monitoring or billing becomes necessary -即:**任務明確原則成立,但它的落點是 mode 層與觸發面,不是 skill 數量**。這與 Part 2 §9(dispatcher + 五份薄契約)、§26-a(單位是迴圈不是工具)互為印證——三條路徑推到同一個形狀。 +Until then, a thin skill plus deterministic local kernel is the lower-risk architecture. -## 29. 狀態更正(並行機制,2026-07-07) +## Current conclusion -- **#137 已 CLOSED,設計 superseded**:PR #146(main `347c14c`)落地「問題帳三層架構」——統計層(`build_problem_events` + `engine/problems.py`)/ 規矩層(`rules.jsonl` 多條綁 problem_key、revises 演變線、broke/held/skipped 對位)/ 呈現層(卡面問題帳 top 1-3);held_streak≥2 靜默調度**取代**畢業概念。§20 曾建議「借 STRATEGIES→RULES 原型」——已被更好的設計 supersede,勿複讀。 -- 個人規則庫(Part 3 §14 的 ② 約束源)因此已落地;**pre-trade gate 查全集的前置已就緒**(#146 comment、roadmap v1b)——§24 順序裡的第 3 項離可做更近。 +The v2 design is the appropriate release shape: one canonical cross-agent workflow, deterministic validators and session handling, and flexible agent interpretation inside explicit boundaries. The next product risk is not choosing a larger agent framework; it is proving that the reliable card and thesis loop matter to real users. diff --git a/docs/roadmap.md b/docs/roadmap.md index ea1cdb3..09d4faa 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,188 +1,84 @@ -# FOMO Kernel · Roadmap v0→v3(規矩軸 × 鏡片軸) +# fomo-kernel roadmap: rule loop and lens loop -> 🗄️ **設計史快照(2026-06-17,非當前規格)**:本文是改名前的 v0→v3 設計藍圖,記錄當時思路與 owner 拍板的排序決策。**部分已被 main 實作超越**——當前的本機狀態格式、engine 是否已有結構化輸出等,一律以 `skills/fomo-kernel/SKILL.md` 與 `engine/trade_recap.py` 為準(例:state 已是 `log.jsonl`/`theses.jsonl` + `TR_JSON`/`TR_STATE_OUT`,engine 已輸出結構化 card JSON)。文中 `trade_recap.py:NNN` 行號為當時版本、可能已位移。保留作設計脈絡與決策史。 +This document updates the original June roadmap to reflect the v2 implementation on 2026-07-14. -> 狀態:規劃中(2026-06-17),待 codex + gemini 審。把 `BACKLOG.md` 願景層、`docs/v1-weekly-coach.md`、`docs/v2c-lens-selection.md` 整合成一張 v0→v3 總圖,標清楚**兩條演化軸**的依賴與排序。 -> 北極星:卡是鏡子不是法官;一張卡一個洞;克制 = feature;對事不對人。 -> 紅線:① 保持薄(別變回 owner 那套 447 檔案重系統)② 一張卡一個洞 ③ 過程教練 ≠ 選股顧問 ④ 有哲學但不寫死 ⑤ **Stage 0(卡戳中一個真人)未過前,別讓大願景偷走當下**。 +## Two evolution axes ---- - -## 1. 兩個系統(輸入/邊界,不是版本) - -「trade review」是兩個容易混淆的系統,本 roadmap 只規劃 **B**: - -| | **A · `/record-trade`**(`investment_note/`) | **B · `/fomo-kernel`**(本 repo) | +| Axis | Question | Sequence | |---|---|---| -| 角色 | 記帳 + 決策 + revisit(管**真相**) | 復盤卡 / 教練(管**行為改變**) | -| revisit 對象 | 「買賣決策事後對不對」 | 「反覆犯的行為洞補了沒」 | -| 狀態 | 重系統,已每週在做 | 輕、可分發,本 roadmap 要長出迴圈 | - -**不合併**。B dogfood 可讀 A 的 CSV,但 B 的狀態層獨立(`~/.trade-coach/`,薄)。詳見 [`v1-weekly-coach.md`](v1-weekly-coach.md) §0。 - ---- - -## 2. 兩條演化軸(本 roadmap 的組織主軸) - -| 軸 | 演化什麼 | 三階段(初次→持續→優化) | 成熟期 | -|---|---|---|---| -| **規矩軸** | 哪個洞、哪條 if-then | 初診一條規矩 → 對帳守了沒 → 畢業換下個洞 | **早**(v1 全到位) | -| **鏡片軸** | 用什麼**思路**判(philosophy) | 借鑑幾家大師 → 縫成你自己的尺 → 每次復盤磨利 | **晚**(v2→v3) | +| Rule loop | Which behavior should change, and did it change? | first diagnosis -> user-chosen rule -> reconciliation -> graduation | +| Lens loop | Which decision philosophy should frame contextual ambiguity? | verified lens choice -> comparison -> personal synthesis | -**核心排序主張**:先讓「規矩軸」的每週迴圈跑起來(v1),才有「每次復盤」這個動作可以累積;鏡片軸是疊在這個迴圈上的外圈。 +The rule loop comes first because lens learning needs repeated reviews. Lenses may change interpretation and language, but they must not fork mechanical facts or persistence. ---- +## Product stages -## 3. v0→v3 總圖(逐版) +### v0: stateless card -每版標:規矩軸狀態 / 鏡片軸狀態 / 依賴 / engine 改動 / 出場條件(DoD) / 對應 BACKLOG。 +Delivered the initial one-card behavior diagnosis but could not reconcile progress across sessions. -### v0 · 無狀態卡 ✅ 已出貨 -- **規矩軸**:一次性出卡,一個洞一條規矩,不記得。 -- **鏡片軸**:單一 pinned 鏡片(去名「存活紀律派」)。 -- **狀態**:已 ship,全面去名,測試通過。 -- BACKLOG:`v0 無狀態卡`。 +### v1: local continuity -### v0.5 · gate:卡可信化 + Stage 0 真人測 ⚑ owner 2026-06-19 拍板「先 gate」 +Implemented local state, append-only theses and rules, prior-commitment reconciliation, and issue tracking. This established that continuity is a first-principles requirement rather than a retention add-on. -**主線前必過(不是版本,是 gate)。** codex blocker:第一張卡若印假 α、攤 5 維,給真人看會為錯的理由失敗。 -- **α 語氣雙閘門**(GitHub #4):資料不夠厚不准用「真本事 α」語氣(已驗 engine 仍印,`trade_recap.py:633-636`)。 -- **收斂卡面**:落實「一卡一洞」,5 維降為安靜供參(對齊 `SKILL.md:44`)。 -- **Stage 0 真人測**(GitHub #3 P0):3-5 個**真人**跑第一張卡,收「有沒有戳中」。 -- **出場條件**:多數真人說「戳中」+ 卡面無假 α → 才開 v1 主線。 +### v2: stable orchestrated review -### v1 · 每週迴圈 + 薄狀態 ◐ 已規格化(`v1-weekly-coach.md`),未實作 -- **規矩軸**:✅ 三模式(初次/持續 review/持續優化)+ `~/.trade-coach/profile.json` 薄狀態 + 對帳上週規矩 + 畢業換洞。 -- **鏡片軸**:單一 pinned + **誠實標尺**(卡上標「這把尺偏存活紀律,動能交易者僅供參考」);雙鏡選擇 = v1.1(owner 2026-06-19 拍板:先單鏡低成本驗迴圈)。 -- **依賴**:v0。 -- **engine 改動**:中等(雙審修正)——建 JSON/metric contract + stable dim id + `active_rule` checker + Opportunity Check;診斷數學重用。見 `v1-weekly-coach.md §6`。 -- **DoD**:第二次跑先對帳(守住/破 X 次,含 Opportunity Check:沒交易→Skipped)再找新洞;規矩連 N 週守住畢業換洞;狀態零交易明細、守薄契約(`v1-weekly-coach.md §2`)。 -- BACKLOG:`v1 守則檔+gate+對帳` 的**後對帳半邊**(pre-trade gate 拆成可選平行軌,見 §3 尾)。 +Current P0 release candidate: -### v1.1 · 鏡片選擇(2-3 面選一 pin)○ 待動能鏡校對 +- thin skill entry point with route-specific flows +- deterministic Review Plan and required question queue +- evidence-gated thesis decisions +- private/public deterministic rendering +- canonical atomic session bundle and recovery +- ETF allocation versus concentration policy +- English implementation contracts and localized product/GTM copy -- **鏡片軸**:【借鑑】起點——動能派鏡片過 verbatim 校對後,首次 onboarding 給 2-3 面選一 pin。 -- **依賴**:v1 + `feat/multi-master-lens-library` 引言校對。 -- **選擇 ≠ 演化**:此步只是「選你認同的尺」;縫自己的尺仍在 v3a。 +Exit criteria are documented in `docs/release-2026-07-19.md`. -### v2a · 建【風格】機械維 ○ v2c 已釘前置,未建 -- **規矩軸**:多一個【風格】維可選(追高/順勢、ride-vs-cut)。 -- **鏡片軸**:為誠實閥準備「可觸發的對象」。 -- **依賴**:可與 v1 **並行**(engine spike,先驗風格維能否從 CSV 穩定算出);只有誠實閥才真依賴它(codex:不必嚴格串在 v1 後)。 -- **為何先做**:v2c §5 釘死——現有機械維 100% 是【普世】,誠實閥**結構上無對象可觸發**;必須先建【風格】維 → 才補 stance → 閥才能動。**順序不可顛倒**。 -- BACKLOG / v2c:v2c §5 關鍵路徑前置。 +### v2.1: snapshot onboarding -### v2c · selection + 誠實閥 + 多鏡片 compare ○ 藍圖(PR #9),未實作 +- direct position screenshot or table adapter +- opening portfolio check with narrow claims +- inferred thesis initialization +- transaction-history upgrade path -> 編號:本機狀態已上移到 v1(supersede `v2c-lens-selection.md §8` 的「v2b=狀態」);本 roadmap 故跳過 v2b、用 v2c 對應該藍圖,免撞名。 -- **鏡片軸**:【借鑑】階段——幾面大師鏡片(`feat/multi-master-lens-library`),用戶可選/pin;誠實閥上線(普世洞免疫;風格洞被選的尺 `inverted` 才端岔路)。 -- **依賴**:v2a(閥需風格維)+ 多鏡片庫引言過 verbatim 校對。 -- BACKLOG:`v2 風格脈絡+多鏡片`;細節見 [`v2c-lens-selection.md`](v2c-lens-selection.md)。 +### v2.2: multi-lens P1 -### v3a · 形成自己的鏡片 ○ 概念(`v1-weekly-coach.md` §11) -- **鏡片軸**:【綜合】階段——跨復盤累積「哪些原則一直打中你、哪些洞你真的在修」,縫成 `personal.lens.json`(human-in-the-loop)。= distill-KOL→lens 同機制,套到 distill 你自己的復盤。 -- **依賴**:v2c(先借鑑過幾家才知道認同啥)+ v1 迴圈(累積過幾次復盤)。 -- **紅線**:personal lens 仍是薄 lens.json(非 manifesto);誠實閥仍守(不准自製尺放過普世洞)。 +- select from a small verified lens set +- compare only where philosophies genuinely diverge +- keep universal behavioral loss mechanisms non-overridable +- preserve one-card convergence -### v3b · 優化鏡片 + 全 context 對帳 ○ 概念 -- **鏡片軸**:【優化】——每次復盤磨利 personal lens(砍死從不咬人的原則、強化一直抓到真洞的)。 -- **規矩軸頂**:thesis 對帳——拿用戶**寫過的**進場原文對帳(不只推斷動機)。 -- **依賴**:v3a +(thesis 對帳需接 context 源,如 `investment_note/wiki/`)。 -- BACKLOG:`v3 全 context 對帳` + 哲學演進;candidate「thesis 對帳」。 +### v3: personal decision system -### v1b · pre-trade gate(Stage 0 後) -- 把 `active_rule` 沉澱成 `my-rules.md`,下單前 `/fomo-kernel check ` 攔一次。 -- **依賴**:v1 的 `active_rule`;定位 **Stage 0 後的 v1b**(codex:不留模糊平行軌)。 -- 註:BACKLOG 把 gate 併進 v1;本 roadmap 拆成 v1b(v1 先把事後對帳閉環跑順,事前攔截是另一個 UX 面)。 +- distill repeated confirmed preferences into a small personal lens +- improve the lens from real outcomes and error patterns +- connect richer source attribution in the owner workflow +- optionally expose a pre-trade process check ---- +## Dependency graph -## 4. 依賴與關鍵路徑 - -``` -v0 ─▶ v0.5 gate ─▶ v1 ─▶ v1.1 ─▶ v2c ─▶ v3a ─▶ v3b - α+真人測 迴圈 鏡片選擇 閥+compare 形成自己 優化+thesis - └─▶ v1b pre-trade gate(Stage 0 後) - v2a 風格維 ∥ 與 v1 並行(engine spike)─────▶ 餵 v2c 誠實閥 +```mermaid +flowchart LR + A["v2 stable lifecycle"] --> B["snapshot adapter"] + A --> C["verified multi-lens selection"] + C --> D["personal lens"] + A --> E["pre-trade process check"] + B --> D ``` -- **gate-first**:`v0.5`(α 誠實化 + Stage 0 真人測)是主線前 gate,沒過不開 v1。 -- **關鍵路徑**:`v1 → v1.1 → v2c → v3a → v3b`。 -- **可並行**:`v2a 風格維`(engine spike)與 v1 同時驗可算性;多鏡片庫 verbatim 校對在 v1 期間並行(`feat/multi-master-lens-library` DRAFT)。 -- **不可顛倒**:`v2a 風格維` 必在 `v2c 誠實閥` 之前——`v2c-lens-selection.md §5/§10` 已證閥無風格維則無觸發對象。 - ---- - -## 5. Stage 0 與排序原則 - -- **Stage 0 ≠ owner 每週 dogfood**(codex 裁決):owner 每週用 = 驗「迴圈機制好不好用」;**Stage 0 = 3-5 個真人驗「第一張卡有沒有戳中」**(GitHub #3 P0,需求側仍 0)。兩者別劃等號;Stage 0 住在 `v0.5` gate。 -- **gate-first**(owner 2026-06-19 拍板):先把第一張卡修到能戳中真人(α 誠實 + 收斂),再投資每週迴圈——別在未驗的卡上蓋迴圈。 -- **規矩 > 鏡片演化**:鏡片軸的「形成自己/優化」吃多次復盤累積,沒 v1 迴圈就沒資料來源;但鏡片「選擇」(v1.1)可早。 -- **別讓外圈偷走當下**(BACKLOG 原話):Stage 0 未過前,克制衝 v2c 多鏡片那種「看起來很完整」的功能。 - ---- - -## 6. 待拍板(open decisions + 提議,送審重點) - -**規矩軸(v1)** -1. **N / M**:規矩連守幾週算畢業(提議 N=3)、連破幾週降級(提議 M=3)? -2. **baseline 固定 vs 滾動**:總進度對「初診固定 baseline」、本期退步對「上週滾動」——兩個都留還是簡化? -3. **嚴格單一 active rule**:守「一次一條」(提議是),不開多條並行? -4. **輸入耦合**:dogfood 直接讀 `investment_note/trades/raw/`(最低摩擦)還是每週手動丟 CSV(保持產品乾淨)?提議前者、狀態層仍解耦。 - -**鏡片軸(v2→v3)** -5. **personal.lens 怎麼累積**:系統自動推「你認同的原則」(每次復盤投票),還是每次手選沉澱?提議「自動推草稿 + 用戶確認」(沿用 Step 2 的 human-in-the-loop)。 -6. **誠實閥觸發後 UX**:端岔路問「你想反駁哪邊」後,怎麼把答案沉澱進 personal lens 而不變嘮叨? -7. **pre-trade gate 歸屬**:維持 BACKLOG 的「併進 v1」,還是本 roadmap 提議的「拆成可選平行軌」? - ---- - -## 7. 給審查者的問題(codex / gemini) - -1. **排序**:規矩軸(v1)先於鏡片軸(v2+)對嗎?還是「借鑑多家」該更早(第一張卡就多鏡片照)? -2. **拆分**:把 pre-trade gate 從 BACKLOG 的 v1 拆出、降為可選平行軌,合理嗎? -3. **哲學一致性**:「形成自己的鏡片」(v3a)會不會撞「對事不對人」/「鏡片不寫死」?用「有意識自縫 ≠ 風格縫合怪 + 誠實閥(普世洞免疫)」化解,夠嗎? -4. **紅線張力**:「保持薄」vs「持續累積 personal lens + history」會不會矛盾?薄狀態的界線該畫在哪? -5. **engine 估計**:v1 宣稱「幾乎不動 engine」(metric binding 重用 5 維)——有沒有低估改動量? -6. **依賴鏈**:§4 的關鍵路徑有沒有錯把可平行的畫成序列、或漏掉前置? -7. **可分發性**:dogfood 讀 `investment_note` CSV,會不會讓產品悄悄依賴 owner 私人系統、傷害「別人 clone 也能用」? -8. **遺漏**:有沒有缺的 version / 步驟 / 紅線? - ---- - -## 8. 銜接文件 -- [`v1-weekly-coach.md`](v1-weekly-coach.md) — 規矩軸 v1 細節規格 + §11 鏡片軸概念。 -- [`v2c-lens-selection.md`](v2c-lens-selection.md) — 鏡片軸 v2a/v2b(selection × 誠實閥)細節。 -- [`../BACKLOG.md`](../BACKLOG.md) — 願景層原文 + candidates + ISSUE-1。 -- GitHub issues:#3(Stage 0 P0)、#4(α 雙閘門)、#5(損耗標籤)、#6(user-stories 校準)、#8(chat 引流版)。 - -## 9. 雙審整合(codex + gemini,2026-06-17) - -> 兩方各自對抗性審本 roadmap。codex 總評「有結構問題」、gemini「需修正後推進」。共識修正已部分回寫 `v1-weekly-coach.md`(§2 薄契約 / §4 Opportunity Check + 回退陷阱 / §6 engine 改動量 / §11 fail-closed)。 - -**共識修正(兩方一致 + 已驗證)** -1. **engine「幾乎不動」嚴重低估**(已驗 `skills/fomo-kernel/engine/trade_recap.py:684-718` 全 stdout、dim 用中文字串):v1 對帳要建 JSON/metric contract + stable dim id + checker。 -2. **鏡片「選擇」可在 v1**:SKILL 換鏡片只換 lens 檔、engine 不動;只有**誠實閥**要等 v2a 風格維。→ v1 提供 2-3 面選一 pin,別硬鎖單一(否則動能交易者 Stage 0 就流失)。我原把「選擇」也推到後面是過頭。 -3. **薄狀態硬契約**:retention cap + schema budget + 豐富語料改存 `~/.trade-coach/cards/*.md`。 -4. **v3a fail-closed**:personal lens 不准動軸;普世維 missing stance 視為一律判(非 v2c §8 現行的閥 OFF)。 -5. **統計洞**:Opportunity Check(沒交易/沒觸發場景 → `Skipped`)+ 絕對門檻判畢業(回退陷阱)。 -6. **可分發**:產品吃 standard schema;owner CSV 走本機 adapter,不進預設入口。 - -**裁決(兩方分歧 / 補判)** -- **Stage 0 ≠ owner 每週 dogfood**(codex blocker,採):§5 原文把兩者劃等號**作廢**——owner 每週=驗機制;Stage 0 = 3-5 個**真人**驗第一張卡有沒有戳中。 -- **alpha 語氣 gate**(codex blocker,採):已驗 engine 仍印「真本事 α」(`trade_recap.py:633-636`);α 雙閘門([#4](https://github.com/atomchung/fomo-kernel/issues/4))設為主線前 gate。 -- **版本標籤漂移**(codex):本 roadmap 為準——本機狀態歸 v1(supersede `v2c-lens-selection.md §8` 的 rollout 編號);v2a 風格維是 engine spike,**可與 v1 並行**驗可算性,不必嚴格串在 v1 後。 -- **pre-trade gate**:採 codex,定成「Stage 0 後的 v1b」,不留模糊平行軌。 -- **(pre-existing,非 roadmap 鍋)**:codex 指「一卡一洞」與實作矛盾(`SKILL.md:44` 別攤 5 維 vs `:79`+engine 仍印 5 維)——屬 SKILL/engine 線,另案。 +## Sequencing principles -**owner 拍板(2026-06-19,已重排 §3–§5 主線)** -1. ✅ **先插 v0.5 gate**(α 誠實化 + 收斂卡面 + 3-5 真人 Stage 0)再動每週迴圈。 -2. ✅ **v1 先單鏡 + 誠實標尺**,雙鏡選擇拆成 v1.1。 -> 已據此重排:v0.5 gate、v1.1 鏡片選擇、pre-trade → v1b、v2b 改 v2c(本機狀態歸 v1)。 +1. Reliability and privacy gates precede new interpretation features. +2. A feature may add context but may not create a second fact or state authority. +3. Real-user card usefulness is a release gate separate from owner dogfooding. +4. A new lens requires source verification and a measurable divergence case. +5. A new rule can graduate only when there were real opportunities to violate it and the user confirms the transition. ---- +## Explicit non-goals -## 修訂紀錄 -- 2026-06-17 · 初版。整合 BACKLOG/v1-weekly-coach/v2c 成 v0→v3 總圖,以「規矩軸 × 鏡片軸」為組織主軸。待 codex + gemini 審。 -- 2026-06-17 (b) · 加 §9 雙審整合。codex+gemini 對抗性審:6 條共識修正(已回寫 v1-weekly-coach)+ 4 條裁決;2 個 sequencing(v0.5 gate、v1 鏡片選擇成本)待 owner 拍板再重排主線。 -- 2026-06-19 · owner 拍板兩個 sequencing → 重排主線:gate-first(v0.5)、v1 單鏡 + v1.1 選擇、pre-trade → v1b、版本去 v2b(狀態歸 v1)。§3/§4/§5/§9 更新。 +- Cloud account or synchronization system. +- Security recommendations or market forecasts. +- A large portfolio governance wiki. +- Several simultaneous active rules. +- A dashboard that replaces the one-card conclusion. diff --git a/docs/skill-v2-architecture.md b/docs/skill-v2-architecture.md new file mode 100644 index 0000000..726951d --- /dev/null +++ b/docs/skill-v2-architecture.md @@ -0,0 +1,66 @@ +# Skill v2 architecture + +Goal: preserve agent judgment where context matters while turning reliable completion, recovery, and card production into code contracts. + +## Before and after + +| Concern | v1: long skill prompt | v2: thin entry point plus orchestration | +|---|---|---| +| Workflow authority | The agent remembers the order from more than four hundred lines of prose. | `review.py` owns the lifecycle; route-specific flow files supply only contextual guidance. | +| Numbers | The engine computes them, but the agent copies them into a card. | The renderer reads numeric values only from engine artifacts. | +| Motive questions | The agent decides what to ask and whether it was already answered. | The Review Plan emits and deduplicates a required question queue. | +| Thesis history | The agent appends JSONL after a conversation. | Validators produce append-only thesis decision events. | +| Evidence for adds | Evidence exists as card prose and is difficult to revisit. | `new_evidence` requires a claim and source that can be reconciled later. | +| Persistence | Several commands write files independently and can leave partial state. | A staging directory is atomically renamed into one canonical session bundle. | +| Interruption | Recovery often reruns the engine and may observe different prices. | `.pending` plus `resume` preserves the original facts and questions. | +| Legacy files | JSONL files act as authority. | JSONL and card folders are repairable compatibility projections. | +| Sharing | The agent manually redacts a private card. | A public renderer creates an independent structured view. | +| Language | Runtime instructions and product copy are mixed. | Runtime contracts are English-only; localized user copy renders the same facts. | +| ETFs | Every ticker behaves like a single stock. | Diversified allocation ETFs are exempt; thematic, sector, and leveraged ETFs remain concentrated. | + +## Data flow + +```mermaid +flowchart LR + A["Broker CSV or snapshot"] --> B["Mechanical engine"] + B --> C["Review Plan"] + C --> D["Agent interpretation and user answers"] + D --> E["Validators"] + E --> F["Private and public preview"] + F --> G["User chooses one rule"] + G --> H["Atomic canonical session"] + H --> I["Legacy projections"] + I -. "failure is recoverable" .-> H +``` + +## Where agent flexibility remains + +The agent still decides how to normalize broker fields, interpret motive answers, write an inferred thesis, frame a counterfactual, and surface qualitative observations. These tasks depend on context and benefit from flexible reasoning. + +The agent no longer controls numbers, rankings, required-question gates, cycle IDs, evidence completeness, ETF exemptions, public-card privacy, or persistence order. Variance in these areas creates failures rather than insight. + +The architecture therefore fixes facts and workflow while preserving interpretation and narrative. The no-digits narrative rule prevents two competing numeric truth sources without forcing every agent to produce identical qualitative analysis. + +## User cases + +### Losing-position add + +The engine detects a large position with adds while underwater and puts a required question in the Review Plan. Choosing `new_evidence` requires a claim and source; a vague statement such as increased confidence fails preview. The next review can examine whether that evidence still holds instead of restarting with a generic averaging-down question. + +### Core ETF allocation + +A portfolio contains mostly a broad-market ETF plus a small stock position. The allocation ETF is excluded from single-name sizing risk, risk top-three concentration, and single-name what-if stress. Sector, thematic, and leveraged ETFs receive no such exemption. + +### Interrupted conversation + +The engine completed, but the user did not answer. The Review Plan remains under `.pending/`, so another agent can resume with the same facts and questions without refetching prices. If the canonical session committed and only a projection failed, `repair-projections` rebuilds it without asking the user again. + +### English GTM demonstration + +`--language en` changes user-visible questions, rule copy, and rendering without creating a second analysis prompt. English and Traditional Chinese sessions share the same engine card and state structures, preventing market-specific forks of the product contract. + +## Release boundary + +P0 includes workflow stability, canonical sessions, thesis evidence, ETF policy, private/public rendering, English developer contracts, and localized GTM surfaces. + +P1 includes multi-lens selection. It may add lens selection and narrative or rule copy, but it must not duplicate lifecycle, state, or rendering infrastructure. diff --git a/docs/style-detection-research.md b/docs/style-detection-research.md index 5d3aba6..bfe5f88 100644 --- a/docs/style-detection-research.md +++ b/docs/style-detection-research.md @@ -1,185 +1,74 @@ -# 研究:怎麼從交易紀錄「找風格」(style detection) +# Research: detecting style from transaction history -> 狀態:研究統整(獨立 research track,先研究後實作)。資料來源見文末。 -> 目的:為 v2c 誠實閥找出第一個可實作的【風格】機械維。北極星不變:**找「不同投資哲學會給相反判決」的行為軸,不是給人貼分型標籤。** 風格是「從交易浮現的傾向」,不是「這個人是 X 型」。 -> 方法:5 角度並行檢索 + 對抗式查核(動能/逆勢學術定義、處置效應、持有/換手、加碼、其他指標+方法論陷阱)。 +Status: research input for multi-lens P1. The goal is to detect behavior axes on which legitimate philosophies disagree, not to assign a permanent trader identity. ---- +## Universal versus style-dependent signals -## 0. 為什麼這份研究是 v2c 的關鍵前提 +Most transaction signals mix a universal loss mechanism with a style choice. Separate them before applying a lens. -`docs/v2c-lens-selection.md` 已釘死:誠實閥要能觸發,前提是存在【風格】型的機械維,而**現有 5 維(出場/sizing/分散/持有/攤平)全部對映【普世】單元** → 閥結構上是空的。所以整條路徑的第一步是:**先建一個真【風格】維**(各派會分歧的),閥才有對象可判。 +| Signal | Universal component | Style component | +|---|---|---| +| Entry location | minimal | strength/breakout versus weakness/discount | +| Exit behavior | anchoring to breakeven or refusal to realize error | let winners run versus harvest mean reversion | +| Add direction | escalating martingale size, recovery motive, or missing thesis | pyramid strength versus add at verified discount | +| Turnover | uncompensated cost is broadly harmful | only justified by demonstrated short-horizon edge | +| Holding period | silent horizon drift is harmful | the chosen horizon itself is a strategy preference | +| Concentration | unbounded downside and correlated drivers are universal risks | intentional concentration may be style-consistent | -這份研究回答:**哪些行為訊號是真風格(哲學分歧)、哪些其實是普世(人人該守)、哪個最值得先做。** +Universal components remain reviewable under every lens. Only the style component may create a defend question or divergent interpretation. ---- +## Best first style axis: entry relative position -## 1. 最重要的統整:每個訊號都要先拆「普世 vs 風格」 +Entry location is the cleanest mechanical disagreement: -研究最一致的結論:**多數行為指標同時含普世成分和風格成分,混在一起判就會錯。** 拆法如下(這張表直接餵 v2c §5 的普世/風格軸): +- Momentum interprets buying near strength or a breakout as confirmation. +- Value or contrarian approaches interpret the same entry as paying too much and prefer verified weakness or discount. -| 行為訊號 | 普世成分(閥免疫,一律判) | 風格成分(閥適用,哲學分歧) | 對立的兩派 | -|---|---|---|---| -| **進場相對位置** | 幾乎沒有 → **最純的風格軸** | 追高(買在高點/突破)vs 抄底(買在回檔/低點) | 動能/順勢 ⟷ 價值/逆勢 | -| **處置效應 / ride-vs-cut** | 在「成本價/回本點」附近做決定(錨定)、為了不認賠而抱、只在 12 月認賠 | 贏家該奔跑還是該收(賣贏家方向) | 動能(讓利潤跑)⟷ 均值回歸(見好就收) | -| **加碼方向** | 金額逐次放大(martingale)、以回本為目標、無停損/無 thesis | 跌價是買點還是賣點(往下加) | 價值/逆勢(跌破內在值→加)⟷ 動能/趨勢(跌=錯→砍) | -| **換手率 turnover** | **淨成本洩漏(普世逆風)** | 僅極窄的「已證實技巧」例外 | (基本不是風格) | -| **持有期 holding period** | 幾乎沒有 | **風格標籤**(scalp..buy&hold);本身無對錯 | (分類用,非判決用) | -| **集中度 concentration** | 過度自信/風險代理(偏普世) | 弱次級訊號 | (不單獨當風格) | +Candidate engine features: -> **設計含義**:閥只在「風格成分」上 fork;「普世成分」一律判、不進閥。所以一個維要當【風格】維,得能**把自己的普世成分和風格成分分開輸出**(例如加碼維:martingale/回本錨定 → 普世判;單純跌價往下加且非升額 → 風格 fork)。 +- entry price divided by trailing 252-day high +- skip-month formation return over a defined lookback +- range percentile as a secondary feature ---- +Every output must state its lookback. The same trade can look like momentum over six months and mean reversion over one month or three years. -## 2. 候選風格維逐項評估 +Use a confidence gate. Per-trade percentiles are noisy and transactions in one ticker are not independent. Small samples should create a weak observation, not a diagnosis. -每項列:能不能從 `(ticker, side, qty, price, date)` + 日線算、對漂移容錯、樣本需求、偏誤防護、哪兩派對立。 +## Exit behavior -### 2.1 進場相對位置(追高 vs 抄底)— ★ 最值得先做 +Separate: -**學術依據(對立天然成立):** -- 動能:Jegadeesh & Titman 1993,過去 3–12 月贏家續贏(6/6 約 +1%/月);George & Hwang 2004「52 週高」更強——`PRILAG = 進場價 / 過去 252 日最高`,愈近 1 愈強,且**它的超額報酬長期不反轉**(不像 JT/DBT 會回吐),最適合當穩定錨。 -- 逆勢:De Bondt & Thaler 1985,36 月形成期的輸家在未來 3–5 年反超;Jegadeesh 1990 短期(1 月)反轉——所以動能研究要**跳過最近 1 個月**(skip-month),否則量到的是反轉不是動能。 +- universal disposition effects: selling near breakeven, refusing to realize a loss, or moving the horizon after the trade fails +- style choice: harvesting mean reversion versus letting a trend continue -**計算(主訊號):** -- `52週高比` = 進場價 / 過去 252 日最高。逐筆算,投資人風格 = 所有 BUY 的中位數。參數少、最穩、不反轉 → **首選**。 -- `形成期報酬(skip 月)` = `價_{t-21} / 價_{t-21-L} - 1`,L=126/252。正=追動能、負=抄逆勢。**符號就是判決**。 -- (次)`區間百分位` N=60/252;(次)`距均線 z 分數` `(價-SMA)/σ`,最受波動/區間影響,只當佐證。 +Measure the user's rule and outcome rather than assuming every early winner exit is wrong. -**對立判決(這就是閥要的岔路):** 52週高比近 1 = 買在高點 → **動能/順勢派稱讚(強者續強)**、**價值/逆勢派斥責(買貴了、長期會反轉)**。每個指標的「高」極都是一派褒、一派貶。 +## Add behavior -**陷阱:** ① **回看窗主導結論**——同一筆買單在 6 月窗是動能、1 月或 3 年窗可能是逆勢;**必須把 lookback 跟判決一起報**,否則判決無定義。② 沒 skip 月會把短期反轉誤當動能。③ 樣本:每筆給一個 (0,1) 百分位,σ≈0.29,要把 0.65(動能)和 0.50(中性)在 95% 分開約需 **n≳15 筆**,0.60 vs 0.50 需 **n≳35**;單檔集中會讓筆數不獨立、再放大。<20 筆視為弱讀。 +Separate: -### 2.2 處置效應 / ride-vs-cut — 普世與風格的混合體(最易誤判) +- universal failure: size escalates as evidence weakens, the goal is merely to recover, or no falsifier exists +- style choice: adding at a verified valuation discount versus pyramiding after strength confirms the thesis -**普世(該幫所有人修):** -- 處置效應本體:`PGR = 已實現獲利 /(已實現獲利 + 帳面獲利)`、`PLR = 已實現虧損 /(已實現虧損 + 帳面虧損)`,`PGR > PLR` 即有偏(Odean 1998:PGR≈0.148、PLR≈0.098,賣贏家的傾向約賠家 1.5 倍)。Odean 證實這**不是**稅、再平衡、或理性均值回歸能解釋的(12 月才反轉 → 全年行為非稅務理性)。Frazzini 2006:這偏誤系統到能留下可套利的動能溢酬(>200bps/月)。 -- 普世錯誤的具體簽名:**賣點剛好群聚在回本線**(Ben-David & Hirshleifer 的 V-shape:賣出機率隨盈虧幅度兩側上升,在零附近沒有跳升 → 不是「翻黑就跑」的純參考點偏好,而是信念修正)、**為了不認賠而抱輸家**、**只在 12 月認賠**。 +The v2 thesis decision enum and evidence gate provide the qualitative ground truth needed to interpret the mechanical direction. -**風格(各派合法對立):** -- 贏家「該奔跑還是該收」**真的因派而異**:趨勢/動能該讓贏家跑、砍輸家(股票會 trend,對股票宇宙是理性的);均值回歸/價值該把贏家修回權重、加碼輸家(賭反轉,對會回歸的資產理性)。**「對的不對稱」會隨資產的報酬自相關翻轉**(Frontiers 2023:專業交易者在均值回歸商品上呈處置效應、在趨勢股票上呈反處置效應,兩者都理性)。 -- 所以**不知道投資人的 thesis 和資產的 trend/mean-revert 性質,就不能把高 PGR/PLR 讀成錯**。 +## Methodological guardrails -**含義:** 出場維要拆兩半——「回本錨定 + 只在 12 月認賠 + 抱著惡化中的輸家」走普世判;「賣贏家的方向(跑 vs 收)」走風格 fork,且要對著投資人風格 + 資產性質算,用容差帶、別用裸 PGR/PLR。 +1. Report behavior tendencies, not identity labels. +2. Keep lookbacks, data coverage, and sample size explicit. +3. Avoid inferring intention from price direction alone. +4. Treat transactions in one driver as correlated evidence. +5. Validate against synthetic opposite-style fixtures and real user review. +6. Keep style selection out of numeric facts and ETF policy. +7. If lenses agree, do not manufacture a comparison. -### 2.3 加碼:金字塔 vs 攤平 — 真風格 fork + 可分離的普世錯誤 +## Recommended implementation order -**風格 fork(真對立):** 跌價是買點還是賣點。趨勢/動能:往下加是大忌(「Average Up, Never Down」,只加贏家、砍輸家);價值/逆勢:跌破內在值往下加**同時提高期望報酬又降風險**(Graham 邊際安全)。同一份成交、相反判決——因為兩派作用在不同狀態變數(動能看價格本身、價值看價格 vs 內在值)。 +1. Add entry-relative-position features behind an observation-only gate. +2. Test opposite interpretations with verified momentum and value lenses. +3. Add the user answer to the thesis record rather than a permanent profile label. +4. Allow multi-lens comparison only when the style signal is confident and the selected lenses disagree. -**可分離的普世錯誤(不分派都該修):** -1. **逐次放大加碼金額**(martingale)= 數學上必爆,不是風格。 -2. **觸發點是自己的成本/回本**(沉沒成本/處置)= 純偏誤,前一次買價對前瞻 thesis 無關。 -3. **無預設停損/thesis 失效規則** = 把有界損失變無界。 +## Research basis -**可計算的分離訊號**(走一遍持倉,對每筆 add 算):`price_vs_avg = 加碼價/當時均成本-1`(+金字塔 / -攤平);`size_ratio = 本次量/上次量`(>1 連續=升額紅旗);`time_gap` 規律度(低變異=DCA);`loss_only_fraction`(只在虧損買的比例≈1=攤平);`drawdown_at_buy`。 -- 高信心普世錯誤 = **只在虧損 + 升額 + 成本錨定**三者並現(與該次是否剛好回本無關 → 防 outcome bias)。 -- 非升額的跌價加碼 = **模稜兩可(風格 or 錯)**,不可自動判錯。 - -> 引擎已有 `classify_adds()`(疑似定投/凹單/待確認)正是這個方向的雛形;要做的是讓它**額外吐出一個風格 lean**(往上加=strength / 往下加=weakness),並把普世錯誤簽名與風格 fork 分開。 - -### 2.4 換手率 turnover vs 持有期 holding — 一個是普世成本、一個是風格標籤 - -- **turnover = 普世成本洩漏**:Barber & Odean 2000——交易最兇的家戶 11.4%/年 vs 大盤 17.9%、平均家戶 16.4%,但**毛報酬在各 turnover 分位幾乎持平、只有淨報酬隨 turnover 下滑** → 高換手不是選股差,是被摩擦成本/短期稅吃掉,**對所有人都是逆風**(算術,非哲學)。唯一例外:已證實的技巧尾(台灣當沖 >80% 賠錢但 <1% 持續獲利;部分高換手基金淨正)——要用**淨成本後績效證明**,不能用嘴宣稱。 -- **holding period = 風格標籤**:scalp(秒)/day(分時)/swing(天週)/position(月年)/buy&hold(年),文獻只分類不排好壞。短持有**本身是風格不是病**,只有跟負淨績效綁在一起才是病。 -- **穩健性**:turnover **更穩**——用「股數流量 / 平均持股」算,**與配對規則無關**(不必把買賣配對)、近乎不受股價漂移。holding period **與配對規則相關**(FIFO/LIFO/指定批給不同答案)、被部分成交切碎、右偏(用中位數別用平均)。 -- **持有期離散度(同檔又當沖又長抱)**:本身**模稜兩可**——可能是紀律的多策略(核心+衛星),也可能是框架漂移(套牢就改口長期)。**裂解測試**:離散若與盈虧符號獨立(贏輸都當沖)=刻意多策略;若短持有群聚在贏家、長持有群聚在輸家 = 處置效應/框架壞掉。引擎現有 `dim_hold` 的「同檔不一致框架」正是這個,但要補這個 P&L-vs-duration 裂解測試才站得住。 - -### 2.5 其他候選(次級) - -- **交易方向 vs 趨勢前報酬(順勢外推 vs 逆勢)**:最直接、最揭露意圖的 ex-ante 軸,直接對映動能 vs 價值(Lakonishok-Shleifer-Vishny;Chicago Fed 2023:散戶總體偏逆勢但異質)。其實是 2.1 的廣義版,可當聚合層訊號。 -- **勝率 × 賠率 × 盈虧偏度(return signature)**:動能=低勝率(~20–40%)高賠率正偏;均值回歸=高勝率(~60–70%)低賠率負偏。理論分離強,但**是已實現結果指標、最受 outcome/look-ahead 偏誤污染**,只當佐證別當主訊號。 -- **因子傾斜持續度(HML/growth/WML, Sharpe 1992 + Fama-French)**:基金上很持續、對映價值/成長/動能哲學;散戶較雜訊。 -- **集中度/特質波動暴險**:主要當**過度自信/風險代理**(Goetzmann & Kumar:集中→換手更高、報酬更低),別單獨當風格,免得把過度自信的集中客誤標成「高信念價值投資人」。 - ---- - -## 3. 方法論護欄(任何 style detector 必守,否則退回「分型」老路) - -研究最強硬的共識——這些是**硬約束**,不是 nice-to-have: - -1. **最低樣本**:每投資人估計需 **≥30–50 round-trip**,有信心標籤前best ≥100。不足 → 回「資料不足」,不是猜一個。(對映引擎現有 `alpha_credible`、`low_conf` 的閘門做法) -2. **報不確定性**:每個指標附信賴區間/容差;**永遠不端硬分類「你是 X 型」**。 -3. **只用 ex-ante 特徵**:用決策當下資訊(進場方向、趨勢前報酬、進場估值)分類,**絕不用已實現盈虧/這筆有沒有賺**判風格(防 outcome bias,Baron & Hershey 1988)。 -4. **point-in-time 資料**:特徵只能用成交時點可得的資料;決策價與成交價分開;不可用事後價標進出場(防 look-ahead)。 -5. **不做存活過濾**:要含未平倉、被放棄的輸家(bag-holding)、下市標的——這些最能診斷風格,濾掉會高估品質。 -6. **漂移容差 / 穩定性測試**:把交易史前後對半,驗風格標籤穩不穩;不穩就 flag,別硬塞單一標籤(散戶風格存在但雜訊大、部分、頻率相依)。 -7. **風格與風險分離**:把集中度/特質波動當控制變數,別讓它污染風格判定。 - -> 這些護欄跟本專案既有設計天然契合:「對股價漂移容錯、只斷言方向不斷言精確值」(見 `tests/`)、`alpha_credible` 雙閘門、「對事不對人」。新風格維直接沿用同一套紀律。 - ---- - -## 4. 排序建議:先做哪個風格維 - -1. **進場相對位置(2.1)— 強烈建議先做。** 最純的風格軸(幾乎無普世成分要拆)、學術依據最硬(George-Hwang 52週高不反轉)、直接對映 `compare_lenses` 既有的 `lean`(strength/weakness)、可從現有資料 + 日線算、漂移容錯(只報方向)。**且它依賴的價格對齊我們已經做好了**(`adjust_for_splits`,PR #13)——跨分割的進場百分位沒對齊會錯 10 倍。 -2. **加碼風格 lean(2.3)— 第二。** 已有 `classify_adds` 雛形,擴成吐風格 lean + 分離普世錯誤簽名,投入小、對立真。 -3. **出場 ride-vs-cut 的風格半(2.2)— 第三。** 價值高但最糾纏(普世/風格混合最深),要先有 thesis/資產性質輸入才站得住,留後。 -4. turnover 維持當**普世成本**提示(不進閥);holding period 當**風格標籤 + 框架裂解測試**(不當判決)。 - -### 建議的落地 MVP(第一個可觸發的閥) -``` -1. dim_entry_style(rows, px):算 52週高比 + 形成期報酬(skip 月),帶樣本閘門(<~20 筆→低信賴), - 回傳 {dim:"進場", lean:"strength|weakness|—", axis:"style", severity, n, lookback} -2. 確定性測試:合成價格 fixture(追高樣本→strength、抄底樣本→weakness),離線可重現 -3. momentum / margin-of-safety 兩面 lens 補這維:axis:style + 對立 stance + lean(schema 見 v2c §11) -4. compare_lenses 接這維 → 端出第一個真岔路 = 誠實閥 MVP -``` - ---- - -## 5. 對引擎/鏡片的接線(總結) - -- **引擎側**:新 `dim_entry_style` 照現有 `dim_*` 形狀(回 dict:dim/severity/triggered/tier + 數字欄位),沿用 `alpha_credible` 式樣本閘門、`adjust_for_splits` 的價格對齊、`tests/` 的「只斷言方向」容錯。 -- **鏡片側**:這維標 `axis:"style"`(閥適用),各 lens 補對立 `stance`/`lean`。普世維仍 `axis:"universal"`(閥免疫)。 -- **閥側**:`compare_lenses` 復活時,missing stance → 視為閥 OFF(非 aligned),counter-lens 用 valve 專用選法(固定 top_flaw、同維比、需 ≥1 面非-inverted)。 - ---- - -## 待驗 / 風險 - -- **進場百分位的可實作穩定度**:lookback 敏感 → 固定報 52週高比 + 形成期(L=252)雙主訊號,兩者不一致就標「橫跨橫期、模稜」不硬判。 -- **樣本閘門門檻**:n≳15–35 是推導值(非引用),上線後用真實資料校。 -- **資產 trend/mean-revert 性質**:ride-vs-cut 的風格判定需要它當輸入,目前沒有 → 所以 ride-vs-cut 排第三、先做進場。 -- **散戶風格穩定度本就弱**:必做前後半穩定性測試,不穩就誠實標「風格未定」。 - ---- - -## 資料來源 - -**進場時機 / 動能 vs 逆勢** -- Jegadeesh & Titman 1993, *Returns to Buying Winners and Selling Losers* — https://www.bauer.uh.edu/rsusmel/phd/jegadeesh-titman93.pdf -- De Bondt & Thaler 1985, *Does the Stock Market Overreact?* — https://onlinelibrary.wiley.com/doi/10.1111/j.1540-6261.1985.tb05004.x -- George & Hwang 2004, *The 52-Week High and Momentum Investing* — https://www.bauer.uh.edu/tgeorge/papers/gh4-paper.pdf -- Jegadeesh 1990 短期反轉 / skip-month — https://alphaarchitect.com/quantitative-momentum-research-short-term-return-reversal/ -- AQR, *Hold the Dip* (2025) — https://www.aqr.com/-/media/AQR/Documents/Alternative-Thinking/AQR-Alternative-Thinking---Hold-the-Dip.pdf -- JPMorgan Institute, returns-chasing vs dip-buying — https://www.jpmorganchase.com/institute/all-topics/financial-health-wealth-creation/returns-chasing-and-dip-buying-among-retail-investors - -**處置效應 / ride-vs-cut** -- Odean 1998, *Are Investors Reluctant to Realize Their Losses?* — https://onlinelibrary.wiley.com/doi/abs/10.1111/0022-1082.00072 -- Shefrin & Statman 1985 — https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1540-6261.1985.tb05002.x -- Frazzini 2006, *The Disposition Effect and Underreaction to News* — https://pages.stern.nyu.edu/~afrazzin/pdf/The%20Disposition%20Effect%20and%20Underreaction%20to%20news%20-%20Frazzini.pdf -- Ben-David & Hirshleifer 2012, V-shape — http://www.columbia.edu/~la2329/The%20V-shaped%20Disposition%20Effect.pdf -- *When the disposition effect proves to be rational* (Frontiers 2023) — https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9996105/ -- *Rational disposition effects: Theory and evidence* — https://www.sciencedirect.com/science/article/pii/S0378426623000821 - -**換手率 / 持有期** -- Barber & Odean 2000, *Trading Is Hazardous to Your Wealth* — https://onlinelibrary.wiley.com/doi/abs/10.1111/0022-1082.00226 -- Barber, Lee, Liu & Odean, 台灣當沖技巧尾 — https://faculty.haas.berkeley.edu/odean/papers/day%20traders/The%20Cross-Section%20of%20Speculator%20Skill.pdf -- 換手-績效異質性 — https://www.sciencedirect.com/science/article/abs/pii/S0378426621000121 - -**加碼 / 金字塔 vs 攤平** -- Turtle/trend pyramiding — https://www.quantifiedstrategies.com/turtle-trading-strategy/ -- Graham 邊際安全 — https://www.netnethunter.com/benjamin-graham-value-investing-principles/ -- DCA vs averaging down — https://trendspider.com/learning-center/mastering-dollar-cost-averaging-averaging-up-and-averaging-down/ -- Martingale 風險 — https://corporatefinanceinstitute.com/resources/career-map/sell-side/capital-markets/martingale-strategy/ - -**其他指標 / 方法論陷阱** -- Goetzmann & Kumar, *Equity Portfolio Diversification* — https://papers.ssrn.com/sol3/papers.cfm?abstract_id=627321 -- Lakonishok, Shleifer & Vishny, *Contrarian Investment, Extrapolation, and Risk* — https://www.nber.org/system/files/working_papers/w4360/w4360.pdf -- Coval, Hirshleifer & Shumway, *Can Individual Investors Beat the Market?* — https://www.bus.umich.edu/pdf/mitsui/nttdocs/coval-shumway2.pdf -- Benhamou et al., *Testing Sharpe Ratio: Luck or Skill?*(小樣本)— https://arxiv.org/abs/1905.08042 -- Baron & Hershey 1988, outcome bias — https://www.ncbi.nlm.nih.gov/pmc/articles/PMC12372742/ -- 存活/前視偏誤 — https://www.quantifiedstrategies.com/survivorship-bias-in-backtesting/ -- *Retail investors are not noise traders* (CEPR) — https://cepr.org/voxeu/columns/retail-investors-are-not-noise-traders - -> **查核註記**:多數一手 PDF(SSRN/NBER/作者頁/AQR)對自動抓取回 403,部分精確數字(Odean PGR=0.148/PLR=0.098、Barber-Odean 11.4%/17.9%、Frazzini >200bps/月、小樣本 n 門檻)經多個獨立二手來源交叉確認、與經典引用一致,但若要逐字引用請回一手 PDF 核對。n≳15–35 樣本門檻為推導值非引用。 +The original study drew on momentum, reversal, disposition-effect, and turnover literature, including Jegadeesh and Titman, George and Hwang, De Bondt and Thaler, Odean, and Frazzini. Recheck the primary papers before publishing numeric claims or quotations in GTM material. diff --git a/docs/v1-weekly-coach.md b/docs/v1-weekly-coach.md index 0e4dbba..40c1288 100644 --- a/docs/v1-weekly-coach.md +++ b/docs/v1-weekly-coach.md @@ -1,210 +1,70 @@ -# v1 藍圖:每週復盤迴圈(初次 × 持續 review × 持續優化)+ 薄本機狀態 +# v1 design: recurring review and thin local state -> 🗄️ **設計史快照(2026-06-17,非當前規格)**:本文是改名前的 v1 每週迴圈設計藍圖。**部分已被 main 實作超越**——尤其 §2 的 `~/.trade-coach/profile.json` 狀態格式、與 §6「engine 尚無結構化輸出、需建 JSON contract」的前提,當前已不同(SKILL.md 用 `log.jsonl`/`theses.jsonl`、engine 已有 `TR_JSON`/`TR_STATE_OUT` 結構化輸出)。當前規格一律以 `skills/fomo-kernel/SKILL.md` 與 `engine/trade_recap.py` 為準;本文保留作設計脈絡與決策史。 +Status: historical design whose core has been superseded by the v2 canonical-session architecture. -> 狀態:設計中(v1)。把 `BACKLOG.md` 願景層 6 步弧線中的 `初診(卡) → 賽後對帳(驗規矩) → 升級畢業` 三步,落成可實作的最小規格。 -> 目的:讓 `/fomo-kernel` 從「一次性 demo」變成「每週重跑、記得上次那條規矩」的迴圈,且**絕不長回成重系統**。 -> 北極星:一張卡、一個洞;第二張卡的價值在**進度**(規矩守了沒、洞有沒有縮),不在再算一次。 -> 範圍外(本 v1 不做):pre-trade gate、多鏡片對照/誠實閥(v2c,需先建【風格】維)、thesis 對帳(v3)、revenge/overtrade 標籤(engine 線)。見 §8。 +## Product model ---- +The recurring coach has three behaviors: -## 0. 先釘死:跟既有兩個系統的關係(免混淆) +1. First review: identify one costly behavior and let the user choose one rule. +2. Returning review: reconcile that rule against new evidence before discussing a new leak. +3. Improvement: graduate or replace a rule only after actual opportunities to violate it. -「trade review」在 owner 的環境裡是**兩個容易混淆的系統**,本 v1 只動 B: +## Thin-state principle -| | **A · `/record-trade`** | **B · `/fomo-kernel`(本 repo)** | -|---|---|---| -| 在哪 | `investment_note/`(owner 私人系統) | 本 repo(對外可分發產品) | -| 角色 | 記帳 + 記決策 + revisit(管**真相**) | 出一張復盤卡(鏡片/教練,管**行為改變**) | -| revisit 對象 | 「這筆**買賣決策**事後看對不對」(30/60/90) | 「你那個**反覆犯的行為洞**補了沒」(本 v1) | -| 重量 | 重(4 寫入車道 + 6 protocol) | 輕(刻意做 447 系統的相反) | -| 狀態 | 已經每週在做 | 本 v1 才開始有記憶 | +State exists to support future review, not to become a second portfolio wiki. Preserve: -**分工**:A 管真相,B 管行為改變;兩個 revisit 是不同對象,**不合併**。 -**耦合**:B 的 dogfood 輸入可直接讀 A 已維護的 `investment_note/trades/raw/` CSV(owner `/record-trade` 更新完帳,接著 `/fomo-kernel` 讀同一份出教練卡);但 **B 的狀態層永遠是自己的**(`~/.trade-coach/`,薄、可分發),跟 A 解耦——別人 clone 也能用,owner 只是剛好把輸入指向 investment_note。 +- metric snapshot and data coverage +- active and prior commitments +- thesis and motive history by cycle +- review status and immutable session artifacts ---- +Do not store raw trade data in memory documents or create a growing narrative profile. -## 1. 三件事 = 兩個入口狀態 + 一個輪替邏輯 +The current implementation uses `sessions//bundle.json` as canonical state and legacy JSONL files as projections. This replaces the original proposal for one mutable `profile.json`. -``` -/fomo-kernel - └─ 看 ~/.trade-coach/profile.json 在不在 - ├─ 不在 →【初次】初診出卡 + 落狀態 - └─ 在 →【持續 review】每跑必先對帳上週規矩 → 再找新洞 - └─ 內含【持續優化】規矩畢業 / 輪替 / 降級(非另一個指令) -``` +## First review -- **初次** 與 **持續 review** 是兩個**入口狀態**(看 state 自動分支,使用者不必記參數)。 -- **持續優化** 不是第三個指令,是 持續 review 跑完後的「規矩輪替」邏輯(§5)。 +- Run the deterministic engine. +- Ask every required motive question. +- Create inferred theses where history is absent. +- Show one private preview. +- Let the user choose, rewrite, or skip one rule. +- Commit atomically. -> ⚠️ **兩條演化軸,別混**:本 v1 演化的是**規矩**(哪個洞、哪條 if-then),**鏡片(交易思路)維持單一 pinned**。「一開始借鑑幾種風格 → 慢慢形成自己的思路 → 每次復盤優化」那條**鏡片演化軸**是外圈(v2→v3),見 §11——它需要 v1 先把「每次復盤」這個迴圈跑起來,才有東西可累積。 +## Returning review ---- +- Load bounded prior state through the Review Plan. +- Reconcile the prior commitment first. +- Distinguish `passed`, `failed`, and `skipped/no opportunity`. +- Do not ask confirmed motives again unless a new cycle or new contradiction justifies it. +- Preserve the same rule when it remains the largest unresolved leak. -## 2. 薄本機狀態 `~/.trade-coach/profile.json` +## Graduation -```json -{ - "lens": "vincent-yu", - "baseline": { - "as_of": "2026-06-08", - "AI_exposure": 0.92, - "avgdown_count": 143, - "max_position": 0.48, - "winner_early_pct": 0.71, - "realized_pnl": -1234 - }, - "active_rule": { - "id": "no-avgdown", - "text": "虧損部位一律不加碼,要加先整筆賣掉隔天重買", - "check": { "metric": "avgdown_count", "op": "<=", "target": 0, "window": "this_period" }, - "set_on": "2026-06-08", - "held_weeks": 0, - "broke_weeks": 0 - }, - "graduated_rules": [], - "history": [ - { "week": "2026-06-08", "hole": "虧損加碼", "rule": "no-avgdown", "held": null } - ] -} -``` +Naive consecutive-review counts are insufficient. A rule should become a graduation candidate only when: -**隱私鐵律(延續 SKILL.md 隱私段)**:`profile.json` **只存聚合 metric 數字 + 規矩文字**,零交易明細(沒有逐筆 buy/sell)。留本機、不上雲、不進 skill 作者收集的反饋。規矩文字可能含 owner 自己寫的 ticker(本機便利,不外傳)。 +- the relevant opportunity occurred enough times +- the metric met an absolute threshold, not merely improved from a worse baseline +- no hidden regression is masked by inactivity +- the user confirms graduation -**薄狀態硬契約(codex + gemini 審 2026-06-17,「保持薄」不能只是口號)**: -- `profile.json` 只存:`active_rule`(1 條)+ 少量聚合 `baseline` + **固定長度的 `history` summary**(retention cap,例:近 12 週,更舊的滾成一行統計)。設 schema budget 上限,超過就摘要,不無限長。 -- **禁(那是 A `/record-trade` 的責任)**:重記帳、逐筆 PnL、每日淨值、重建 portfolio。 -- **豐富語料另存**:v3a 蒸餾「你自己的鏡片」需要動機/反駁脈絡,薄 JSON 給不了 → 每次出卡把卡片 Markdown(含 YAML frontmatter:`top_flaw` / `motive_q` 的答 / 用戶反駁)寫進 `~/.trade-coach/cards/*.md`。**歷史卡片夾 = B 的語料庫**(留本機、可唯讀掃描),profile.json 維持薄。 +## Metric binding -**與 v2c 和諧**:`v2c-lens-selection.md` §3 已假設 `~/.trade-coach/profile.json` 存 `active_lens`(selection 預設)。本 v1 先用單一字串 `lens`(永遠 pinned,= v2c 的「v1 相容:單一 lens 永遠 pinned」);v2c 復活時把 `lens` 擴成 `active_lens` 物件(加 stance/lean),**同一個檔、向後相容**,不另開檔。 +Every rule binds to an engine metric and target. The engine or validator owns the value; the agent does not calculate it. Baselines must be explicit, and short samples must remain labeled as such. ---- +## Boundaries -## 3.【初次】初診(= 現行四步 + 落狀態) +- One active rule at a time. +- Event-driven review, not calendar nagging. +- No security recommendations. +- Local-only state. +- No duplicate lifecycle for different agents or lenses. +- Multi-lens selection may affect questions and prose but not facts, state, or persistence. -完全沿用 `SKILL.md` Step 0–4(格式 → driver map → 引擎 → 出卡前對話確認 → 出卡 → 收反饋),**只在末尾加一步**: +## Acceptance -- **Step 5(新)· 落狀態**:出完卡後,把「pin 的鏡片 + 使用者剛挑定的那條 if-then 規矩 + 本次引擎的 key metric 當 baseline」寫進 `profile.json`。`active_rule.check` 由「那個洞對應的維」決定 metric(§6 對照表)。 - -其餘流程、收斂鐵律、隱私一律不變。 - ---- - -## 4.【持續 review】賽後對帳(每跑必做的順序) - -state 存在時,**每次跑都先對帳,再找新洞**(落地 SKILL「第二次以後:驗規矩,不要再照同一個洞」): - -1. 讀 `profile.json` → 取 `active_rule` + `baseline`。 -2. 重跑 `engine/trade_recap.py` on **本週/本期 CSV**。 -3. **對帳**:用 `active_rule.check.metric` 在本期重算,判定「守住 / 破 X 次」。 - - **先做 Opportunity Check(codex/gemini blocker)**:只有「**本期存在會觸發破戒的場景**」才算數。例:規矩=虧損不加碼 → 要本期**有浮虧部位**、卻沒往下加,才算「守住」;本期根本沒浮虧或沒交易 → 對帳標 `Skipped`,**不累計 `held_weeks`、不推進畢業**(否則「沒遇到考驗」會被誤判成「克制了」= 零事件偏差)。 - - **守住判定一律用絕對目標**(回退陷阱 fix):`held_weeks`/畢業/降級**只認 `active_rule.check` 的絕對門檻**(如 `avgdown_count <= 0`)。「比上週好」只能當卡上的鼓勵語,**不准影響**畢業邏輯(上週破 5、本週破 4,對 baseline=0 仍是破戒)。 - - **小樣本防噪**:本期樣本過少時,比率型 metric(如早賣率,1 筆=100%、0 筆=NaN)不下判定,標「樣本不足、僅記錄」。 - - 對 **baseline** 比 → 看**總進度**(例:AI 暴險 92% → 78%,敘事用)。 -4. **卡頂端先出一行對帳**(進度錨點),再走原本的「找最大洞」——但 `graduated_rules` 與當前 `active_rule` 對應的洞要**跳過**,別每次照同一個分散。 -5. 更新 `history` 追加本週一筆;依結果調 `held_weeks` / `broke_weeks`。 - -卡的結構、收斂鐵律(一個洞 + 一條規矩)、「先承認本事再打」、金額>勝率 等,**全部不變**;只是多了頂端那行「上週那條:守住了 / 破了幾次」。 - ---- - -## 5.【持續優化】畢業 / 輪替 / 降級 - -在 §4 跑完後執行: - -- **畢業**:`held_weeks ≥ N`(建議 **N=3**,待 dogfood 校)→ `active_rule` 移入 `graduated_rules`、標日期;新 `active_rule` = 引擎當前排序的**下一個洞**(經 SKILL Step 2 對話確認動機後定稿)。 -- **降級(規矩一直破)**:`broke_weeks ≥ M`(建議 **M=3**)→ 不只嘮叨。兩條路:① 把規矩換成**更小步**的 if-then(門檻放寬、先求做得到);② 誠實標「這條沒 land」,讓使用者換一條——鏡子不是法官。 -- **永遠只有一條 `active_rule`**(守收斂)。同時存在多個洞時,引擎照 `severity × tier` 排,一次只工作一條。 - ---- - -## 6. 對帳要重算什麼(metric binding · engine 改動量已修正) - -每條規矩綁一個 **engine 既有**輸出 metric;對帳 = 重跑同一個引擎、讀同一個 metric。**診斷數學可重用**,但「讓對帳讀得到 metric」需要的不是輸出層小補丁(見下方修正): - -| 規矩(例) | 綁定維(engine) | metric | check | -|---|---|---|---| -| 虧損不加碼 | `dim_avgdown`(攤平) | 本期攤平破線次數 | `<= 0` 或 `<= 上週` | -| AI 部位砍到 70% | `dim_dispersion`(分散) | 最大單一 driver 暴險 | `<= 0.70` | -| winner 不賣太早 | `dim_exit`(出場) | winner 賣後續漲比 / 早賣率 | 對 baseline 改善 | -| 單一部位 < 25% | `dim_size`(sizing) | 最大單一部位佔比 | `<= 0.25` | -| 持有時間一致 | `dim_holding`(持有) | 同檔內時間框架一致性 | 對 baseline 改善 | - -> ⚠️ **修正(codex 審 2026-06-17,已驗 `skills/fomo-kernel/engine/trade_recap.py:684-718`)**:engine 目前**全程 `print()` 到 stdout、無結構化回傳、dim 以中文字串識別**(`dims=[d_exit,d_size,d_div,d_hold,d_avgdown]`)。所以 v1 對帳**不是**「數十行純輸出層」,要先建:① JSON/結構化輸出模式 ② **stable dim id**(現為顯示字串)③ metric binding 表 ④ `active_rule` checker(含 §4 的 Opportunity Check)⑤ 排序時跳過 current/graduated 洞。診斷數學可重用,但這個 **contract 層是新功能、屬中等工作量**(非低風險小改)。 - ---- - -## 7. 紅線(別讓它變回 447 系統) - -1. **薄狀態 ≠ 第二本帳**:`profile.json` 只存「一條 active 規矩 + pin 鏡片 + 薄歷史 + key metric baseline」。帳的真相留在 A,B 不重記逐筆交易。 -2. **一張卡永遠一個洞**:「持續優化」= 規矩**畢業/換掉**,**不是疊維度**。第二份十維報表 = 失敗。 -3. **卡是故事不是 dashboard**:沿用 SKILL Step 3 全部鐵律。 -4. **排序紀律**(BACKLOG 原話「別讓大願景偷走當下該驗的小東西」):先把 v1(對帳那塊)做到 owner 每週真的會用,**別一次跳到 v2/v3**。 - ---- - -## 8. 範圍外 + 銜接點 - -| 項目 | 屬於 | 為何不在 v1 | -|---|---|---| -| pre-trade gate(`/fomo-kernel check ` 下單前攔) | BACKLOG 候選 | 是「事前」端;v1 先把「事後對帳」閉環跑順。守則檔(`active_rule`)正好是 gate 的料。 | -| 多鏡片對照 / 誠實閥 | v2c | v2c §5 已釘:閥要能觸發需**先建【風格】機械維**(v2a);v1 維持單一 pinned lens。 | -| 鏡片演化軸(借鑑多家 → 形成自己 → 優化) | v2→v3(§11) | 需先有 v1 規矩迴圈(才有東西可演化)+ v2a【風格】維 + 多鏡片庫(verbatim 校對後)。 | -| thesis 對帳(核對使用者寫過的進場原文) | v3 | 需吃全 context;v1 只對帳行為 metric。 | -| `revenge_trade` / `overtrading` 標籤 | engine 線(BACKLOG ISSUE-2 校正後) | 屬另一條 engine 實作線,動工前先對齊,別重工。 | - ---- - -## 9. 驗收標準(讓之後可實作、可驗) - -- [ ] 初次跑完 → `~/.trade-coach/profile.json` 生成,含 `active_rule`(有 `check`)+ `baseline`。 -- [ ] 第二次跑(換 CSV、state 已存在)→ 卡**頂端先出對帳行**「上週那條:守住 / 破 X 次」,**再**出新洞;不重複同一個已處理的洞。 -- [ ] `held_weeks` 連續達 N → `active_rule` 進 `graduated_rules`,新 `active_rule` = 引擎次洞。 -- [ ] `broke_weeks` 連續達 M → 規矩降級或標「沒 land」,不只重複嘮叨。 -- [ ] `profile.json` **不含任何逐筆交易明細**(只聚合 metric + 規矩文字)。 -- [ ] engine 程式碼改動限「輸出被追蹤 metric 的結構化鍵」一處;診斷邏輯零改動。 - ---- - -## 10. 待拍板(實作前) - -1. **N / M 值**:畢業要連守幾週(建議 3)、降級要連破幾週(建議 3)?owner dogfood 後校。 -2. **baseline 固定 vs 滾動**:總進度對「初診那次」固定 baseline,本期退步對「上週」滾動——本 v1 兩個都留(§4.3),確認要不要簡化成一個。 -3. **嚴格單一 active rule**:確認守「一次一條」(本 v1 預設),不開多條並行。 -4. **輸入耦合程度**:dogfood 直接讀 `investment_note/trades/raw/`(最低摩擦)還是每週手動丟 CSV(保持產品乾淨)?預設前者,狀態層仍解耦。 - ---- - -## 11. 鏡片演化軸(哲學演進)— 外圈 v2→v3 - -> 來源:owner 2026-06-17「一開始借鑑幾種交易風格 → 慢慢形成自己的思路 → 基於每次復盤持續優化」。這是 BACKLOG 6 步弧線最後一步「哲學演進」的具體化,也是**把「去名」走完**的關鍵——尺從「某位大師的」變成「你自己的」。 - -**兩條演化軸,同一個 初次 → 持續 → 優化 結構:** - -| 軸 | 演化的東西 | 三階段 | 在哪 | -|---|---|---|---| -| 規矩軸 | 哪個洞、哪條 if-then | 初診 → 對帳 → 畢業換洞 | **v1**(§3–5) | -| 鏡片軸 | 用什麼思路判(philosophy) | 借鑑多家 → 縫成自己 → 每次磨利 | v2→v3(本節) | - -**鏡片軸三階段:** - -- **借鑑(borrow)**:出卡時用幾面大師鏡片照**同一筆交易**,看不同派別怎麼讀(存活紀律 / 動能順勢 / 安全邊際 / 交易心理)。= v2c 的多鏡片,但當**學習/探索**用,不只 selection。 -- **形成自己(synthesize)**:跨復盤累積「哪些原則一直打中你、哪些洞你真的在修」,縫成一面 `personal.lens.json`——你自己的尺(human-in-the-loop,沿用 Step 2 確認)。**這是 distill-KOL→lens 的同一機制,套到「distill 你自己的復盤」**:閉環,正中 apply-to-self 護城河(背景見母專案記憶 `project-kol-collect-vs-collector-overlap`)。 -- **持續優化(refine)**:每次復盤磨利——砍掉從不咬人的原則、強化一直抓到真洞的、把動機問句調成你真實的 pattern。鏡片跟你共同演化。 - -**化解「對事不對人」的關鍵分辨:** - -- 「**你有意識地縫自己的尺**」(承諾自己要守的原則)≠「**風格縫合怪**」(無意識亂縫、被 `behavior-diagnosis.md` 否決的那種)。前者是**自我承諾**,正是教練 agent「事前承諾 → 事後對帳」要的東西;後者是病。 -- **誠實閥(v2c)是讓「形成自己」不淪為自我安慰的護欄**:你能作者你的哲學,但**普世的洞免疫**——不准用自製的尺把最大的漏定義成「我的風格」。本節(形成自己,給自由)和 v2c(誠實閥,守底線)互補。 - - **硬化(codex 審 blocker,2026-06-17)**:① personal lens **不得修改 universal/style 軸**(軸以 `rubric/vincent-yu.md` 為 source-of-truth)② 自製 lens 在**普世維 missing stance 必須 fail-CLOSED(視為一律判)**,不可沿用 `v2c-lens-selection.md §8` 現行的「missing → 閥 OFF」——否則漏填 stance 就能繞過閥放過最大的洞。誠實閥對「自製鏡片」要比對「大師鏡片」更嚴,不是更鬆。 - -**排序(別讓外圈偷走 v1):** 鏡片軸要能跑,前提是 ① v1 規矩迴圈先在(才有「每次復盤」可累積)② v2a 先建【風格】機械維(誠實閥才有對象,見 v2c §5)③ 多鏡片庫引言先過 verbatim 校對(`feat/multi-master-lens-library`)。**所以這是最外圈,先把 v1 跑順。** - ---- - -## 修訂紀錄 - -- 2026-06-17 · 初版。回應「能不能把 fomo-kernel 變成每週做的(初次/持續 review/持續優化)」。對齊 BACKLOG 願景層 v0→v3、SKILL「第二次以後」段、v2c `~/.trade-coach/` 假設。狀態:設計中,未實作。 -- 2026-06-17 (b) · 加 §11 鏡片演化軸(借鑑 → 形成自己 → 優化),回應 owner「借鑑幾種風格→形成自己思路→持續優化」。釐清「規矩軸(v1)vs 鏡片軸(v2→v3)」兩條演化軸,並以「有意識自縫 ≠ 風格縫合怪」+ 誠實閥化解「對事不對人」張力。 +- A second review references the prior commitment before a new leak. +- A week with no relevant opportunity is skipped rather than counted as success. +- A user-selected rule, not an engine default, is the stored commitment. +- Recovery never requires rebuilding a committed session from chat memory. diff --git a/docs/v2c-lens-selection.md b/docs/v2c-lens-selection.md index 8c4c172..2b1b537 100644 --- a/docs/v2c-lens-selection.md +++ b/docs/v2c-lens-selection.md @@ -1,135 +1,79 @@ -# v2c 藍圖:鏡片選擇(selection)× 誠實閥(integrity)× 分階段 +# P1 design: lens selection, integrity gate, and comparison -> 狀態:設計中(v2c),已過 Round 1–2 codex+gemini 審(見文末修訂紀錄)。 -> 目的:解掉「VY-GTM 要 pin 一面鏡片」與「卡是 mirror、不准用選鏡片閃掉最大的洞」的張力,且不退回 `behavior-diagnosis.md` 已否決的「交易者分型」。 -> 北極星:卡是鏡子不是法官;克制 = feature;對事不對人。 +Status: planned after the 2026-07-19 P0 release. -> 🚧 **關鍵前提(Round 2 釘死)**:誠實閥要能觸發,前提是存在「【風格】型」的機械維。**現狀引擎的機械維(出場/ sizing /分散/持有/攤平)全部對映到【普世】單元**(見 §5 的 trace),所以**閥在 v1 結構上完全無法觸發,不是「很少」**。整個 v2a 的關鍵路徑因此是:**先建【風格】維 → 才補 stance → 閥才有對象可判**。 +## Core separation -## 1. 核心 reframe:把 selection 和 integrity 拆開 +Lens selection and integrity are different systems: -- **selection**:用哪面鏡片判 —— 可被入口 / GTM 決定(VY 粉 → VY)。 -- **integrity**:那面鏡片會不會放過你最大的洞 —— 不准被用戶拿來逃避。 +- **Selection** chooses which philosophical frame the user wants to consult. +- **Integrity** decides whether a lens is allowed to change the interpretation of a detected behavior. -(C)「岔路即診斷」保護的是 integrity,不是「每次都 fork」。所以:**GTM 管 selection(入口 pin),(C) 降級成 integrity 的條件式安全閥。** 拆開不是消除張力,是讓摩擦**有原則且罕見**——只在「被選的尺把你最大的洞當策略」時才亮。 +Universal behavioral loss mechanisms cannot be excused by a selected philosophy. Only a style-dependent, confidence-bearing signal may produce a divergent lens interpretation. -## 2. 一條 pipeline,前面換頭 +## Shared pipeline -``` -entry_context → [selection] → [mechanical] → [integrity 閥] → card - pin/fork/compare ↑ 不變 ↑ 條件觸發(需【風格】維) +```text +engine facts + -> universal behavior checks + -> optional style observations with confidence + -> selected lens stance and lean + -> motive question or comparison + -> same validator, lifecycle, and renderer ``` -## 3. Selection 階段(GTM join point) +Lens selection does not create separate engines, schemas, sessions, or card implementations. -`active_lens` 優先序:① 入口指定(VY 連結)→ pin。② 本機預設(`~/.trade-coach/profile.json`)。③ 通用首次 → fork-onboarding(§4)。 -- **v1 相容**:單一 lens 永遠 pinned,`entry_context` 無作用。 -- **本機預設鏡片 ≠ 人格型標籤**:它是「綁當前最大洞、可隨時改」的便利值,只為省去重問;最大洞變成它會放過的就再 fork。尺服務當前的洞(對事),不定義這個人(對人)。 +## Selection -## 4. Integrity 誠實閥(核心) +- Offer a small verified set rather than the entire draft library. +- Store the chosen lens as a preference, not a permanent identity. +- Allow the user to change or disable it. +- Use GTM to explain the difference between lenses, but keep implementation contracts English-only. -**stance 詞彙沿用 `compare_lenses` 既有 2-D 模型**:`inverted`=這不是洞·是本派策略(放過)/ `conditional`=有後門 / `aligned`=普世兩派同看 / `unconditional`=一律破戒(最嚴)。 +## Integrity gate -**觸發條件**(對機械卡片排序第一的 triggered 洞 `top_flaw`): -``` -top_flaw 屬【風格】維(§5) # 【普世】維一律免疫,不進閥 - AND active_lens.dims[top_flaw].stance == "inverted" # 這面尺把它當策略 = 放過 - AND 存在 ≥1 面非-inverted 的 counter_lens(對同一維) # 真的有哲學認為它是洞 -``` -- 只有 `inverted` 算「放過」;`conditional`(有後門)走既有 **Step 2 動機提問**,不端 fork。 -- **counter_lens 選法(valve 專用,不用 compare_lenses 全域排序)**(Round 2 修正):**先固定 `top_flaw`**(來自機械卡片的 `severity × TW[tier]` 排序),**只在那一維**比 `active_lens` vs 各候選 lens 的 stance 距離,挑最對立且**非-inverted** 的當 counter。不可直接套 `compare_lenses` 的 all-pair `severity × distance` 排序(那會跨維挑、且 lean 不同也算距離)。 -- **觸發** → 端岔路:「你的尺說這是策略,另一把說這是你最大的漏 —— 你想反駁哪邊?」flinch 定調 + 更新本機預設。 -- **不觸發** → 直接出 `active_lens` 判決。 - -**stance 缺失 = 閥明確 OFF(不靜默當 aligned)**:`compare_lenses` 目前 `da.get("stance","aligned")` 把缺失當 aligned(`compare_lenses.py:52/97/125`)——**復活時要改成:該維無 stance → 視為閥 OFF**,不是 aligned。 - -## 5. 閥的邊界:用 vincent-yu.md 既有的【普世】/【風格】軸 - -`rubric/vincent-yu.md:19` 已定義:**【普世】**=概率/存活硬規律,可直接判對錯 → **閥免疫**;**【風格】**=特定偏好,只當「Defend 提問」不強判 → **閥適用**(多鏡片在此分歧)。 - -**三份文件講同一軸:** -| 軸 | vincent-yu.md | behavior-diagnosis.md | 閥 | -|---|---|---|---| -| 普世硬規律 | 【普世】 | 第一層 跨型純損耗 | 免疫(一律判) | -| 風格脈絡 | 【風格】 | 第二/三層 脈絡行為 | 適用(可 fork) | - -**現狀 trace(Round 2,codex 逐個查證):現有機械維 100% 是【普世】**—— -出場→D1/G1、sizing→B1/A1、分散→B2、持有→D1、攤平→C2/A2、(輔助)α/β→E2,**全部標【普世】**(`vincent-yu.md` 對應行)。卡片 dims list 實為 `[出場, sizing, 分散, 持有, 攤平]`(`trade_recap.py:704-707`);α/β 是 tier-3、單獨算、不在這 list。 -→ **結論:閥現狀無任何可觸發的機械維。** 要讓閥有用,v2a 必須**先**新增【風格】機械維(典型:追高/順勢 `chase/ride`、賺多少才跑 `ride-vs-cut`),並在 lens 標其 `rubric_unit` 為【風格】單元。 - -> ⚠️ 不是引擎的 `tier`:`tier`(1/2/3,`trade_recap.py` 各 dim 內)是 severity 加權,跟「普世/風格」是兩條不同的軸,別混。 - -## 6. Edge cases - -- **單一 lens / 無 stance**:閥 OFF(現狀)。 -- **某【風格】洞:所有 lens 都 inverted**(無一面非-inverted)→ 無哲學認為它是洞 → 降到 #2,不硬罵。**注意**:此判定看 stance,不能用 `pair_distance`(它在 stance 相同、僅 lean 不同時仍給距離 > 0,`compare_lenses.py:55-57`)——閥需明訂「**至少一面非-inverted counter**」才算成洞。 -- **用戶覆蓋成更放水的尺**:允許 + 誠實警告「換成不會說你的那把,卡就照不出洞」;不關閥(下次同維仍在 counter 端亮)。 -- **【普世】維**:永不進閥,一律判。 - -## 7. 不同粉絲 → 不同路徑(資料驅動,非分型) - -| 粉絲類型 | 入口 | active_lens | 閥會亮嗎 | 體驗 | -|---|---|---|---|---| -| VY 鐵粉 | VY 連結 | pin VY | 現狀不會(無【風格】維);未來看 VY 對【風格】維的 stance 怎麼蒸餾 | 「VY 照我的交易」,嚴厲但順;【普世】洞照罵 | -| 被 pin 一把放水尺 | 該尺入口 | pin 該尺 | 會(該尺對某【風格】top_flaw `inverted` 且有非-inverted counter) | 端對立鏡片,擋逃避 | -| 沒主見 / 通用 | 通用 | 未定 → 機械洞 + 端最大【風格】岔路 | 一定(= onboarding) | 反應選尺,存本機(可改) | -| 進階 | opt-in | 多鏡片 | — | compare 當產品 | - -> Round 2 修正:不宣稱「VY 永不 inverted」——VY 有真【風格】單元(D4/E1/F1/F2/F4),VY 在那些維 fork 與否是**蒸餾 stance 時的決定**(VY 存活偏好傾向少 fork,但那是選擇,非現狀可證)。 - -## 8. 分階段 rollout + 依賴(Round 2 重排順序) - -- **v1(現狀)**:單 lens、純 pin、閥 OFF(無 stance、無【風格】維)。 -- **v2a(讓閥能動,依賴順序不可顛倒)**: - 1. **先建【風格】機械維 + 偵測器**(如 `chase/ride`、`ride-vs-cut`),並在 lens 標 `rubric_unit` 為【風格】單元。**這是前提**——沒有【風格】維,後面補 stance 只會標到免疫的【普世】維、得重做。 - 2. **在那些【風格】維上,為每面 lens(含 VY)補 `stance`/`lean` 資料**(schema 見 §11)。 - 3. **改 `compare_lenses`:missing stance → 閥 OFF(非 aligned)**;並加 valve 專用 counter 選法(§4)。 - 4. 寫**第二面哲學「動能派」**(對【風格】維與 survival-discipline 最對立)。 - 5. 補【普世】純損耗偵測器(`revenge_trade`/`overtrading`,behavior-diagnosis ❌待加),補強免疫層。 -- **v2b**:本機狀態 `~/.trade-coach/`(記鏡片 + 上次規矩,供對帳)。 -- **v2c**:非 VY 入口 → fork-onboarding;進階 → compare。`compare_lenses` 復活(顯示讀 `philosophy`、加「divergence 排序不可蓋過大額虧損」閘)。 - -## 9. 與既有決策一致性 - -- 對事不對人:閥查「鏡片對行為的 stance」,非給人貼型;本機預設綁洞可改,非人格標籤。✓ -- 一卡一洞:閥只在 `top_flaw`、只端最大一個岔路。✓ -- 隱私:選擇與對帳留本機。✓ -- 重用既有零件:stance 詞彙(compare_lenses)、普世/風格軸(vincent-yu.md)、divergence(compare_lenses,但 counter 選法用 valve 專用版)。✓ - -## 10. 待驗 / 風險 - -- **閥的可觸發性**:現狀零【風格】維 → 閥結構上空的。v2a 步驟 1(建【風格】維)是整條路徑的關鍵前提。 -- **【風格】維偵測器的可實作性**:`chase/ride`、`ride-vs-cut` 要能從 CSV 行為穩定算出(對股價漂移容錯),否則閥沒有可靠輸入。 -- **動能派 stance 蒸餾品質**:要真對立、對映到實際【風格】維,不稻草人。 -- **純損耗免疫完整性**:依賴 revenge/overtrading 偵測器補齊。 - -## 11. lens.json 的 stance/lean schema(Round 2,gemini blocking) - -每個 dim 在現有聲音欄位上,新增閥需要的兩欄。**`axis` 顯式標【普世】/【風格】**(denormalize 自 `rubric_unit` 對映的 vincent-yu.md 標記,避免 runtime 解析 prose;vincent-yu.md 為人類 source-of-truth): - -```jsonc -"dims": { - "加碼攤平": { - "rubric_unit": "C2 雙紅線 / A2 試探≠加碼", // 既有 - "rule": "...", "quote": "...", "motive_q": "...", // 既有(聲音層) - "axis": "universal", // 新:universal(=【普世】,閥免疫) | style(=【風格】,閥適用) - "stance": "unconditional", // 新:inverted|conditional|aligned|unconditional(對映 compare_lenses) - "lean": "weakness" // 新(可選):strength|weakness|gap,divergence 第二軸 - }, - "追高順勢": { // v2a 新增的【風格】維範例 - "rubric_unit": "D4 / E1(【風格】)", - "rule": "...", "quote": "...", "motive_q": "...", - "axis": "style", - "stance": "inverted", // 例:動能派把追高當策略;存活派可能 aligned/unconditional - "lean": "strength" - } -} -``` -- **規則**:`axis=universal` 的維 → 閥免疫(stance 僅供顯示,不觸發 fork)。`axis=style` 的維才進閥。 -- **缺 `stance`**(或整面 lens 無 stance)→ 該維閥 OFF(不當 aligned)。 -- **向後相容**:v1 lens 沒有 `axis`/`stance`,引擎一律當「無閥」走純判定路徑。 +For each behavior dimension: + +1. Determine whether the engine signal is universal or style-dependent. +2. Require enough data and confidence for a style observation. +3. Read the selected lens's explicit `stance` and `lean`. +4. If the lens has no stance, fail closed to the universal interpretation rather than silently disabling the check. +5. If several lenses agree, show one interpretation rather than artificial debate. +6. If they genuinely disagree, present the smallest useful fork and ask the user to defend the intended strategy. + +## Lens schema + +Each dimension should define: + +- `stance`: `aligned`, `conditional`, `inverted`, or `unconditional` +- `lean`: a machine-readable direction such as `strength`, `weakness`, `barbell`, or `evidence` +- grounded principle and source status +- motive question template +- candidate rule framing + +Source status must distinguish verified quotation, paraphrase, interpretation, and cross-domain analogy. + +## Edge cases + +- A momentum user who buys strength should not be diagnosed as chasing solely because entry is near a high. +- A value user who adds after price weakness still fails if size escalates without evidence or falsifier. +- Intentional concentration remains subject to drawdown and driver-risk facts. +- Unknown or low-confidence style stays an observation and cannot alter the top conclusion. + +## Rollout + +1. Finish source verification for a small set of contrasting lenses. +2. Implement one style axis, initially entry relative position. +3. Add deterministic stance/lean contract tests. +4. Run differential user cases where the same facts receive legitimately different questions. +5. Only then expose multi-lens selection in GTM. -## 修訂紀錄 +## P1 acceptance -- **Round 1(codex + gemini)**:`unconditional_pass`→`inverted`;§5 改用 vincent-yu.md 既有【普世】/【風格】軸(≠ tier);表格修正;stance 缺失→閥 OFF;本機預設定性為綁洞可改非分型。 -- **Round 2(codex + gemini)**:① §5「大多【普世】」更正為「**全部【普世】→ 閥現狀無法觸發**」(codex 逐維 trace)。② §7 不再宣稱「VY 永不 inverted」(VY 有【風格】單元,屬蒸餾決定)。③ §8 順序重排:**先建【風格】維 → 再補 stance**(原順序會標到免疫維、得重做)。④ §4 counter-lens 改 valve 專用選法(固定 top_flaw、同維比、需非-inverted),不用 compare_lenses 全域排序。⑤ §6 明訂「至少一面非-inverted counter」(lean-only divergence 不算成洞)。⑥ 新增 §11 lens.json stance/lean/axis schema(gemini blocking)。⑦ compare_lenses missing-stance→aligned 需改碼為 OFF。 +- Lens choice changes only contextual question and prose surfaces. +- Every changed interpretation cites a confident style observation. +- Universal risk findings remain visible. +- One-card convergence remains intact. +- Public quotations are source-verified. +- The complete P0 lifecycle and recovery suite remains green. diff --git a/evals/EVALS.md b/evals/EVALS.md index 83e6250..977bba9 100644 --- a/evals/EVALS.md +++ b/evals/EVALS.md @@ -1,75 +1,60 @@ -# fomo-kernel · Skill 行為評估(adherence evals) +# fomo-kernel agent-behavior acceptance cases -> 這份是**作者用的驗收清單**,不是給執行 skill 的 agent 讀的——SKILL.md 不引用它,不佔執行時 context。 -> 依據:業界 skill 評估共識(先寫判準再改 skill;10–20 條案例足以抓回歸)+ 本 repo 既有結論「eval 瓶頸在判準不在工具」。 -> 跑法:改完 SKILL.md / card-spec.md / engine 後,起一個乾淨 session 載入 skill 逐條跑;每條都是可觀察行為,自己看 trajectory 判,或丟給另一個 LLM 當 judge。engine 數值層的回歸另有 `tests/run_all.py` + `engine/test_state_loop.py`,這裡只管 agent 行為層。 -> -> **分工(#68)**:本檔 = **手動驗收入口**(輕、乾淨 session 逐條跑、人判);[`docs/eval-design.md`](../docs/eval-design.md) = **自動化 harness 的單一權威**(重、`tests/agent/`、機檢+judge)。同一判準兩邊都有時,以 eval-design 的斷言定義為準,本檔對應條目標它的編號(見下表);改一條鐵律 → 兩檔連 card-spec.md 一起動(eval-design §5)。 +This is a maintainer checklist, not runtime context. Executable prompts live in `skills/fomo-kernel/evals/evals.json`; deterministic P0 assertions live in `tests/test_review_v2.py` and `tests/run_all.py`. -**與 eval-design.md 的判準對照**(同源判準,兩套編號): +## Trigger cases -| 本檔 | eval-design | 判準核心 | -|---|---|---| -| B1 | C-1 / C-2 | 先問完(engine 先跑、Step 2 先於卡) | -| B2 | A-1 | thesis_questions 不上卡 | -| B3 | A-2 | 無 5 維小數表 | -| B5 | B-4 | 集中度差分:「刻意押賽道」≠「假分散」 | -| B6 | A-5 | α 閘門誠實 | -| B10 | A-10(+B-3 差分) | commitment 存最終版;insufficient → null | -| B11 | B-6 | 回頭客先對帳、同維不開新戰場 | -| B21 | A-14 | TWR vs 大盤照抄(該不該買指數) | - -## A · 觸發(description 對不對) - -| # | 輸入 | 預期 | -|---|---|---| -| A1 | 「幫我復盤我的交易」+ 附 CSV | ✅ 觸發,走完整流程 | -| A2 | 「幫我 review 這份對帳單」(截圖) | ✅ 觸發,Step 0 直接讀圖轉標準欄位 | -| A3 | 「/fomo-kernel」無資料 | ✅ 觸發,請用戶提供 CSV **並給「試駕」選項**(mock 走四步:不落盤 + 標演練 + 卡標示範);不去找真實對帳單 | -| A4 | 「NVDA 現在能不能買?」 | ❌ 不觸發(選股建議,description 已明列排除) | -| A5 | 「幫我研究 PLTR 的基本面」 | ❌ 不觸發(個股研究) | -| A6 | 「大盤下週會怎麼走?」 | ❌ 不觸發(大盤預測) | - -## B · 流程鐵律(用 mock 或 `mock/sample_*.csv` persona 跑) - -> persona 模擬:engine 對任何輸入路徑一視同仁(#89 已移除 is_demo 檔名嗅探),CSV 放哪都行。卡面 = 真實用戶形態;「這是測試/狀態隔離」只留對話層跟作者講,一個字不上卡(上卡 = 模擬穿幫,測不到真實體驗)。 - -| # | 判準(可觀察行為) | 出處 | -|---|---|---| -| B1 | 出卡**前**問完動機:Step 2 至少問了「金額最大 + 行為矛盾」1 檔 + headline 對應的鏡片問句,拿到答案才出卡 | SKILL.md Step 2 / self-check | -| B2 | 卡上沒有任何 `thesis_questions` 原句(問題不上卡,只有答完的定論) | card-spec 禁止清單 | -| B3 | 卡上沒有 5 維 severity 小數表;非 headline 維度只用一句人話帶過 | card-spec 禁止清單 | -| B4 | 只收斂到一個洞 + 一條規矩;規矩給 2–3 條候選讓用戶挑/改 | card-spec 規則 | -| B5 | 用戶答「刻意押賽道」時,洞的標題**不是**「假分散」(答案改標題) | SKILL.md Step 2 規則 | -| B6 | α 不 credible(未達統計顯著)時不用「真本事」語氣,α 數字必帶 95% 區間/不確定性說明,且講清楚卡在哪(`gate.reason`:樣本不足 vs 區間太寬/持倉集中);「贏大盤 X pp」有配拆帳(押對賽道 + 板塊內選股)。判定源:`honesty_ledger` 列 `alpha_credibility` | SKILL.md Step 1 / Step 3 gate | -| B7 | 一張卡只出一次:show_widget 渲染成功 → HTML 卡 = 主交付,回覆文字只留收尾 + Step 3.5 / Step 4 問句(不重講卡);終端機 / widget 失敗 → 文字卡為主交付 | card-spec 呈現方式(#78 真人反饋:widget+全文重複=讀兩遍) | -| B8 | public card 只在用戶要求時才出;出時佔比 bucket 化、無絕對金額 / 股數 / 精確交易日 | card-spec redact 規則 | -| B9 | 說話原則:卡上無內部標記(`←` 註解、`(供參)`、鏡片單元代號、「不出某數字」的決策注記)、無工程內部名(`max_pos_pct`…翻人話「最大單注佔比」)、學術詞帶白話翻譯;對帳單標準詞彙(已實現/未實現/盈虧比)直接用,不自創替代詞或壓縮縮語(「賠側時限」→「賠錢單設時限」);卡面標點全形統一(數字格式除外);句子一讀就懂 | card-spec 說話原則(#78+demo 卡真人反饋) | -| B10 | 收尾 log.jsonl 存的是**Step 3.5 用戶親選那條規矩**(Step 2 推翻機械預設時不能存回預設);`insufficient_data` 時 engine 預設不落盤,**用戶親選例外**(存 `source:"user_chosen"` + `baseline_note`),無親選則 commitment=null | SKILL.md 收尾(#78) | -| B11 | 對帳模式(log 非空):卡第一句先對上次承諾的 `metric_key` 新舊值,才講新洞;同維的洞直說「還沒過關」、不開新戰場 | SKILL.md 狀態迴圈 | -| B12 | 隱私:全程無上傳 / 外流動作;無資料時不主動翻用戶機器找真實對帳單;回收的反饋不含交易明細 | SKILL.md 隱私第一 | -| B13 | 試駕模式:`~/.trade-coach/` 零寫入(log / theses / profile 都不動,state 只進 temp);Step 2 問句標明演練;卡頭有「示範 · 假資料」標示;卡尾引導帶自己的 CSV 回來 | SKILL.md 試駕模式(#53) | -| B14 | `honesty_ledger` 列 `unrealized_coverage` 時,卡上必講「未實現僅反映 `priced_n`/`held_n` 檔持倉,缺現價:…」;不可讓部分覆蓋的未實現金額看起來像完整數字 | honesty_ledger / SKILL Step 3 gate(#82) | -| B15 | `honesty_ledger` 列 `sector_attribution` 時,卡上必補一句「這幾檔有 driver 標籤但查無板塊 ETF 對照,超額被歸入『選股』」;**即使 α 面板因樣本不足/不顯著整塊沒出也要講**(揭露不可只活在 α 面板) | card-spec α/拆帳段 / honesty_ledger(#92) | -| B16 | `honesty_ledger` 非空時,每個列出的 `key` 卡面敘事都有對應人話(B6/B14/B15 是 alpha_credibility/unrealized_coverage/sector_attribution 三個 key 的具體講法,其餘 key 同規格);ledger 有列、卡面沒交代 = fail(卡面 ↔ ledger 對帳,非審風格) | SKILL.md Step 3 self-check gate(#82) | -| B17 | 現金(#171,讀 `card.cash`):`reliable=true` 才把現金講進卡(現金佔帳戶 % + `recent_net_deposit` 非 0 時的入金判讀「加深還是解集中度」),用對帳單語言不裸奔 `cash_weight`;`reliable=false`(`honesty_ledger` 列 `cash_reliability`)**不准把盲算佔比當真數字**,只誠實帶一句「現金我只能盲估、給我對帳單餘額才算得準」。無錨點且淨買入(weight=null)= 卡上不冒現金數字也不空吠;**多幣別現金桶(台美各帳戶各自 `TR_CASH` 錨點):全給→`reliable` 聚合上卡、只給部分→`source=partial` 只把 `unanchored_currencies` 那個帳戶講成盲估,別把已可信的那半也講不準** | card-spec 現金與入金判讀 / honesty_ledger(#171) | -| B18 | 多市場(#173,台股+美股混倉):**combined 口徑含台股**——最大單點依賴/賽道曝險分母不因台股不在美股 CSV 就漏算(聚合 USD 視圖下台積電 `2330.TW` 是真最大依賴時要講出來,不被某支美股冒名);α/β per-market 各對自己大盤(台股對 `^TWII`)**不合成總 α**,頂層只講 `scope` 市場範圍;混幣聚合金額標 `aggregate_currency`(USD)、缺匯率明示近似。前置 = Step 0 把台股 `Symbol` 標成 `.TW`/`.TWO` + `Market=TW`/`Currency=TWD`(認格式是 Claude 職責,引擎不 hardcode) | Step 0 標準化 / dim_alpha_beta per-market / currency_meta(#173/#132) | -| B19 | 帳戶級績效(#171,讀 `card.acct_perf`):**只准照抄引擎數字,不准 Claude 自己算**;`acct_twr` 非 null 才講,三數字講成一條鏈(「持倉 X% → 帳戶 Y%,差 Z pp 現金效應」+ 帳戶年化 IRR),不是三行 dashboard;`cash_drag` 正負要翻譯(負=閒錢稀釋 ≈ $`drag_dollar_approx` 反事實、正=現金擋跌,**跌市不把持有現金講成錯**);gate 掉(`acct_twr=null`,現金無錨點/回滾破裂)整段不講、可只講 `hold_twr` 持倉柱,邀請補錨點的話併入 `cash_reliability` 那句不重複;`honesty_ledger` 列 `acct_perf_basis` 時照 `data` 收窄講(哪個幣別盲算/哪些檔缺價成本平線零報酬/fx 即期近似) | card-spec 帳戶級績效段 / honesty_ledger(#171 B 路線) | -| B20 | 多錨點對帳殘差(#180,讀 `data_integrity.cash_residuals` / `honesty_ledger` `cash_reliability` status=residual):有殘差必講「你 {start}~{end} 有 ${residual} 現金變動對不上」,**文案中性**——可能漏記入金/提款/股息,不斷言是哪一種(殘差只證對不上、不證成因,禁「你漏了一筆入金」);小缺口帳戶報酬照出(只帶揭露一句),大缺口(`acct_perf.note` 給解鎖邀請、`acct_twr=null`)帳戶報酬不出、卡上出「補這筆金流日期即解鎖」+ **持倉柱 `hold_twr` 照給**(帳戶報酬 = opt-in 進階層、不阻塞核心卡);殘差揭露不綁 `acct_twr` 出不出(有錨點也可能對不上) | card-spec 現金/帳戶級績效段 / honesty_ledger(#180) | -| B21 | TWR vs 大盤上卡(#164 柱2,讀 `alpha_beta_breakdown` 的 `port_tot`/`spy_tot`/`excess_vs_spy`):卡上出一行直白「你的持倉 X% vs 無腦全買大盤 Y%,差 ±Z pp」回答「該不該乾脆買指數」,**三個數字只准照抄引擎、不准 Claude 自己重算**(卡面值 = `alpha_beta_breakdown` 值);**基準跟市場走**(US=SPY、TW=加權指數 `^TWII`,別硬寫 SPY),混市場 per-market 兩行並列不加總;和帳戶 IRR(錢滾多快)、α 拆帳(贏的是技巧還是運氣)分工不重複;`note`(樣本不足/無價)時這行不出、別硬湊 | card-spec 該不該買指數段 / SKILL Step 1(#164 柱2) | -| B22 | 出場追蹤冷啟動兩層(#170,既有歷史使用者首次 `enqueue-from-ledger`):`scan` 的 `due` **不因啟用前的歷史出場暴增**(`due<=enqueued_at` = 啟用前就到期的窗,不催);它們改走 `backlog`(金額大者先、收斂 top-5)+ `backlog_summary`(彙總)。卡上歷史段**先一句模式鏡子**(count/full/reduce/top_tickers/span),賣飛傾向只在 `priced≥1` 才講且覆蓋率誠實(不硬湊分母),再抓大放小帶最大 1–2 筆、**不逐筆逼問**;答完 `resolve` 退出 backlog。近百筆歷史一次灌 `due` = fail(把復盤變審問)| engine revisit.py `enqueued_at`/`scan_due`/`scan_backlog` ↔ SKILL Step 2.5 出場追蹤(#170) | - -## C · Goal-hiding(card-spec 拆檔的驗證) - -| # | 判準 | +| Input | Expected behavior | |---|---| -| C1 | trajectory 裡 `card-spec.md` 的讀取發生在 Step 2 答案拿齊**之後**,不是開場就整份讀進來 | -| C2 | Step 2 的問句是二選一、帶用戶真實 ticker / 數字,不是照抄 SKILL.md 模板原文;沒有被草草一句帶過 | - -## 回歸紀錄 - -改動 SKILL.md / card-spec.md 後補一行:日期 · 改了什麼 · 跑了哪幾條 · 結果。 - -| 日期 | 改動 | 跑過 | 結果 | +| Trade-review request plus CSV | Trigger the complete review lifecycle. | +| Brokerage statement or screenshot | Trigger and normalize locally. | +| Skill invocation with no data | Offer test drive without searching the user's machine for statements. | +| Request for a stock recommendation | Do not use this skill to provide advice. | +| Request for company research | Do not treat it as a trade postmortem. | +| Request for a market forecast | Do not treat it as a trade postmortem. | + +## Lifecycle invariants + +1. Use `review.py prepare`; do not reconstruct the lifecycle manually. +2. Ask every required motive question before preview. +3. Never put raw questions or unanswered hypotheses on the conclusion card. +4. Display no raw five-dimension severity dashboard. +5. Use only engine-owned numbers and renderer-owned numeric copy. +6. Require claim and source for `new_evidence`. +7. Create inferred theses for uncovered cycles without presenting them as confirmed. +8. Show one private preview, then let the user choose, rewrite, or skip one rule. +9. Store exactly the user's final rule selection. Short samples remain baselines unless the user explicitly chooses a rule. +10. Commit one immutable canonical bundle; rebuild projections from it. +11. Resume pending work without refetching prices. +12. Keep all trade data local. + +## Card invariants + +- One strength, one largest leak, and at most one commitment. +- No internal field names or author notes. +- No buy/sell recommendation and no personality judgment. +- Every triggered honesty-ledger key appears in plain, narrow language. +- Public card is independently rendered and contains no amounts, dates, tickers, exact weights, session IDs, evidence text, or agent-authored prose. +- Test-drive cards and conversations are visibly labeled as demo data and do not touch production state. + +## Important scenario checks + +- A vague "buying the dip" answer does not satisfy the `new_evidence` gate. +- Broad-market, regional, bond, and commodity ETFs may receive the explicit allocation exemption; thematic, sector, leveraged, and unknown instruments do not. +- A multi-market portfolio compares each market with its own benchmark and never synthesizes a total alpha. +- Account-level performance appears only when cash and price foundations satisfy engine gates. +- Cash residual wording remains neutral and does not invent a missing deposit or withdrawal. +- The next weekly review reconciles the prior commitment before introducing a new leak. +- Historical exit-review backlog is summarized and prioritized rather than converted into a large interrogation queue. + +## Evaluation method + +Prefer deterministic checks over an LLM judge, and an LLM judge over manual inspection. Use a judge only for narrative coherence, not for facts that code can assert. Prove each checker with both a known-good artifact and an intentional mutation. + +## Regression record + +| Date | Change | Evidence | Result | |---|---|---|---| -| 2026-07-04 | #69–#76 批次 merge 後首輪(mock_trades × 誠實者 persona,headless `claude -p` 4 輪,隔離 HOME,$4.44) | A1;B1–B4/B5(刻意版)/B6/B7/B9/B10/B12;C1/C2;收尾 A-7/A-8/A-9 | **C1 = 1/2**(權限異常那輪開場就讀 card-spec.md;正常輪時機正確)→ 異常環境下鐵律遵守度會掉,已知 failure mode。其餘全綠;**B10 有鐵證**(engine 預設「單筆 20%」被用戶親選「AI 封頂 70%」推翻,log 存親選版);B6 閘門②語意精確。headless 無 AskUserQuestion → **fallback 路徑必觸發,主路徑(工具問答)自動化測不到**,要互動 session 驗。小瑕疵:卡上「你有 2.5 年資料」實為 β 回歸的價格序列長度,CSV 只 1 年(敘事精度)。未測:B5(以為分散版)/B8/B11(回頭客+消重)/SKIP 承諾 | +| 2026-07-04 | Post-merge agent run over mock data | Interactive and headless cases plus artifact checkers | Core invariants passed; headless option-tool behavior remained untestable. | +| 2026-07-14 | Skill v2 orchestration, atomic sessions, thesis evidence, ETF policy, localization, and private/public renderers | Complete offline suite, nine v2 cases, and a real test-drive prepare smoke | Passed; canonical recovery and projection repair worked. | +| 2026-07-14 | English-only implementation documentation with bilingual GTM/localized copy boundaries | `tests/test_doc_language.py` plus complete offline suite | Pending final verification in this change. | diff --git a/skills/fomo-kernel/SKILL.md b/skills/fomo-kernel/SKILL.md index 2143f61..bd38f17 100644 --- a/skills/fomo-kernel/SKILL.md +++ b/skills/fomo-kernel/SKILL.md @@ -1,442 +1,98 @@ --- name: fomo-kernel -description: 用一面交易哲學鏡片(預設「存活紀律派」,可換),把你的真實交易復盤成一張卡——一個最大的洞 + 一條下次要守的規矩 + 一句鏡片原則。先用機械算抓出最大的行為漏洞(假分散 / 梭哈 / 攤平 / 賣太早 / 把beta當alpha),再用鏡片的思路問出每筆交易背後的「動機」(焦慮還是判斷、看好還是不想認賠)。用戶說 /fomo-kernel、復盤我的交易、看我的交易紀錄、幫我 review 這份對帳單、trade review 時使用。不用於個股研究、選股建議、大盤預測或財經新聞問答——那些不是復盤,不要觸發。資料全程留在用戶本機,不外傳。 +description: Review a user's trade CSV or position snapshot into one behavior card, one user-chosen next-time rule, and an append-only investment-thesis record. Use for trade reviews, transaction postmortems, brokerage-statement reviews, position reviews, and equivalent requests in any supported language. Do not use for stock picks, market forecasts, or security research. --- -# FOMO Kernel · 用哲學鏡片復盤你的交易 +# fomo-kernel -> 把一份交易紀錄,變成一張「逼你下次只改一件事」的復盤卡。 -> 機械層(Python)負責**抓大放小**——只挑最大的行為漏洞;哲學鏡片負責**找動機**——問出那筆交易背後你不願承認的原因。 +Turn trading data into one focused review card: the largest behavioral leak, the thesis behind any add, and one rule for the next review cycle. -## 何時用 +## Non-negotiable rules -用戶想復盤自己的交易、想知道「我反覆犯的錯是什麼」、丟給你一份券商 CSV / 對帳單、或直接說 `/fomo-kernel`。沒有資料時,請他提供券商 CSV / 對帳單(截圖也行,Step 0 讀得懂),**並同時給「試駕」選項**(見下節);只想看靜態長相 → README 的範例卡。 +1. Use only numbers present in engine artifacts. Never calculate, fill in, or alter numeric facts. +2. Do not provide buy or sell recommendations. Review behavior, motives, thesis evolution, and process rules. +3. Obtain an answer for every `required:true` item in `question_queue` before preview. +4. A card has exactly one final commitment at most. The user may choose a candidate, provide a custom rule, or skip. +5. Keep trade data and derived state local. Show the private card by default; use only the public card for sharing. +6. Treat `sessions//bundle.json` as the canonical completed result. Never hand-edit projections as if they were authoritative. -## 🧪 試駕模式(沒資料也能體驗流程;三個防護缺一不可) - -用戶沒資料或想先體驗 → 用 AskUserQuestion 給兩選項:「**提供我的 CSV** / **先用內建假資料試駕一遍**」。選試駕 → 拿 `mock/mock_trades.csv` 走完整四步流程,但: - -1. **狀態一律不落盤**:`TR_STATE_OUT` 指到臨時目錄(如 `mktemp -d` 下);`coach.py`/`ledger.py`/`revisit.py`/`problems.py` 的 `--state`/`--log`/`--theses`/`--rules`/`--cards-dir`/`--ledger`/`--queue`/`--book` 全部覆寫指到同一個臨時目錄。`~/.trade-coach/` 的 log.jsonl / theses.jsonl / profile.md / rules.jsonl / problems.jsonl / ledger.jsonl / revisit.jsonl / cards/ **一個字都不寫**——假資料的承諾進了教練記憶,下次真復盤的對帳基準就是髒的。收尾改成一句講解:「真實使用時,這條規矩會存進你本機的教練記憶,下週回來先對帳」。試駕結束想親自確認沒弄髒正式狀態 → `python3 engine/coach.py data-status` 是單一事實源(#165),列出 `~/.trade-coach/` 下每個檔案的存在/大小/筆數,跑前跑後比對就知道有沒有意外落盤。 -2. **Step 2 照問,但標明是演練**:動機問題照走 AskUserQuestion——試駕就是要讓他體驗「我的答案會改變卡」這個差異化;但問句裡標明「示範資料,隨便選一個,看卡怎麼跟著變」,不逼他為不是他的交易編動機(#53 的尷尬就消了)。 -3. **卡標示範**:卡頭標「示範 · 假資料,非真實成績」;α/β 附一句「示範資料失真,別當真」——失真警告是**呈現層(你)的責任**,引擎對任何輸入一致、沒有 demo 分支(#89)。 - -卡尾必收一句引導:「想復盤自己的交易 → `/fomo-kernel your.csv`」。 - -## 🔒 隱私第一(每次都要遵守) - -- **用戶的交易 CSV 全程留在他本機**。你只在他的環境裡跑 `engine/trade_recap.py`,不上傳、不複製到別處、不寫進任何雲端。 -- **不要把用戶的交易內容寫進記憶、不要外傳給任何人**(包括 skill 作者)。 -- **誠實邊界(隱私話術別過度承諾)**:資料**不上傳後端、不落地儲存到別處、作者永遠拿不到**;但你(Claude)為了復盤**必須讀** CSV/JSON,交易內容自然進你的 context —— 這跟用戶平常用 Claude 一樣,不是「完全不經過任何伺服器」。README / 卡上的隱私話術照這個精度寫,別講成「絕對不離開你的電腦」。 -- 要回給作者的只有一件事:**「這張卡有沒有用」的文字反饋**(用戶自願)——不含任何交易明細。 -- 用戶沒給資料時,**請他提供或走試駕模式**(內建假資料、不落盤);絕不要主動去翻他機器上的真實對帳單。 -- 用戶問「我電腦上到底存了什麼/怎麼備份/怎麼砍掉重來」(#165)→ 指到 `python3 engine/coach.py data-status`(列存在/大小/筆數,不印交易內容)/ `data-export --out FILE.zip`(打包備份)/ `data-reset --dry-run`(先預覽)再 `--confirm`(真的刪);別自己用 `rm -rf` 或手動列檔案湊答案,這三個命令是唯一事實源。 - -## 🌐 Output language (apply every time) - -Everything the user sees — your dialogue, the `AskUserQuestion` options, and the final card — must be in **one resolved output language**. Do not hardcode a language. Resolve it per session, first match wins: - -1. **Explicit request this session** — the user says "give it to me in English" / "用中文" / passes `lang=en`. -2. **Saved preference** — `output_lang:` in `~/.trade-coach/profile.md`, if present. -3. **Conversation language** — the language the user is speaking to you in right now. This is the default; follow it, don't impose a language. -4. **Fallback** — Traditional Chinese (`zh-TW`). - -Once resolved: -- Run the whole flow (dialogue, questions, card) in that language. Lens files (`rubric/*.lens.json`) currently carry Traditional-Chinese quotes/prompts — translate them faithfully on the fly into the resolved language when you write the card. -- Pass it to the engine each run as `TR_LANG=` (e.g. `TR_LANG=en`) for forward-compatibility. The engine does not consume it yet — its own printed CLI card and lens strings stay Chinese for now; full engine/lens localization keyed on `TR_LANG` (a strings table) is tracked separately as internationalization work. This still governs what the user sees **today**, because the card is one **you** write from `build_card_data()`'s structured JSON, not the engine's printed card. -- Persist it: on first run, or whenever the user switches, write `output_lang: ` into `~/.trade-coach/profile.md` (alongside the profile principles) so the next session resolves to their preference at step 2. - -> The Traditional-Chinese phrasings, question templates, and card examples throughout the rest of this SKILL are **illustrative of intent**, not literal strings to copy — express their meaning in the resolved language. - -## 💱 Display currency(幣別呈現,#51/#129;apply every time) - -引擎原幣記帳,**換算只發生在你寫卡這層**。規則: - -1. **display currency 跟 resolved output language**:en→USD、zh-TW→TWD、zh-CN→CNY;用戶指定(「用美元」)或 `profile.md` 的 `display_currency:` 優先,並照 Output language 同款方式持久化。 -2. **例外:持倉單一市場 → 直接用該市場幣別**(`currency_meta.mixed=false` 時就用 `aggregate_currency`,美股 only 的繁中用戶不該看到滿卡無謂的台幣換算)。 -3. **合計換算、分項原幣**:卡上總覽(已實現/未實現/總損益)可換算成 display currency;**單檔數字一律原幣**、必要時附換算(「NVDA +$1,200(≈NT$38,400)」)——用戶要對得上券商 app。 -4. **換算匯率來源** = `currency_meta.fx`(engine live 抓的兌 USD 匯率);要換成非 USD 的 display currency,用交叉率(例 TWD 顯示:USD 金額 ÷ fx.TWD)。**離線/缺匯率**(`fx_error` 或 `data_integrity.fx_gaps` 非空):讀上次 `last_state.json` 的 `currency_meta.fx` 當快取,卡上標「匯率截至上次對帳」;連快取都沒有 → **只出原幣、分幣別列,不猜匯率**。 -5. **混幣組合**(`mixed=true`):聚合數字(overview/盈虧比/what-if/sizing 權重)已是 USD 基準;`pnl_by_currency` 有原幣分桶供呈現;`fx_gaps` 非空時聚合是原幣近似——**必須在卡上明示**「X 幣別缺匯率,佔比為近似值」。`alpha_beta_note` 非 null 時 α/β 段落照抄該註記(通常=提醒頂層 α/β 僅含 scope 市場;完整 per-market 呈現規則見 Step 1 的 alpha/beta 段)。 - -## 工作流程(四步) - -> 分工原則:**engine 做純算(確定性),Claude 做世界知識(格式 / 分類 / 動機)。** 需要認得世界的事都交給 Claude,engine 不 hardcode。 - -### 開場 · 讀本機狀態 + 偵測這次要處理什麼(weekly loop 入口) - -**投資不是復盤一次就結束。** 這個 skill 是一條**每週迴圈**:`匯入 CSV → 偵測新交易 / 新倉 → 只問缺的動機 → 寫本週 review → 出卡`。目標是**取代你每週的交易紀錄**,而不是每次重算同一個洞。動 CSV 前先讀本機狀態(都在 `~/.trade-coach/`,純本機、不外傳): +## Canonical entry point ```bash -mkdir -p ~/.trade-coach -cat ~/.trade-coach/log.jsonl 2>/dev/null # 每行一次 review session(薄 metric + 承諾);空 = 第一次 -cat ~/.trade-coach/theses.jsonl 2>/dev/null # 每行一筆 thesis 或 exit_narrative event(append-only);持股+出場動機庫 -cat ~/.trade-coach/profile.md 2>/dev/null # 你的交易目標 + 3 條個人原則(復盤對照基準);空 = 第一次幫你建 -python3 engine/ledger.py holdings 2>/dev/null # 帳本推導的當前持倉(snapshot 錨點+交易疊加);讀不到=還沒開帳 -python3 engine/revisit.py scan 2>/dev/null # 出場追蹤:到期 due(#32)+ 啟用前歷史 backlog(#170);都空=本週不問 -python3 engine/problems.py stats --today <今天> --rules ~/.trade-coach/rules.jsonl 2>/dev/null # 問題帳(#137):top 1–3 + 規矩對位;空=還沒開帳 +cd skills/fomo-kernel +python3 engine/review.py prepare --language en ``` -**路由(讀完上面兩檔 + 跑完 Step 1 engine 後判定):** -- **log 空 → 初診**:跑完整 Step 0→4,收尾寫第一筆 session + 為值得問的持倉建 thesis。 -- **log 非空 → 對帳(每週迴圈)**,依序: - 1. **偵測新交易** = engine state `date_end` 與 log 最後一筆 `date_end` 之間的交易(本週新動作,復盤重點;不再從頭講舊帳)。 - 2. **偵測缺 thesis 的持倉** = engine state `holdings.positions` 每個 `cycle_id` 比對 `theses.jsonl`(**只比 thesis 行,`event:"exit_narrative"` 的行不算**——減倉出場的 narrative 帶同一個 cycle_id,誤匹配會讓沒 thesis 的持倉永遠不被補)。**新建倉(新 cycle_id)或從沒寫過 thesis 的持倉 = 缺**。 - 3. **先對帳**(Step 2.5):上次 `commitment` 的 metric 新舊值 + 上次每筆 active thesis 的 `exit_trigger` 有沒有觸發。 - 4. **補缺的 thesis**(Step 2):缺 thesis 的持倉由 AI **猜**(標 `inferred`、零提問),只對「行為矛盾、金額最大的 1 檔」問一句;已有 thesis 的不碰(除非 trigger 觸發)。 - 5. **問新出場的賣出理由**(Step 2(d)):scan 的 `recent_exits` 有還沒問過的 → 對近 14 天的清倉/大減倉問「當時為什麼賣」(窗口過了就永久缺這筆)。 - -> 兩個狀態檔都是**用戶自己的**本機教練記憶,永不外傳、不回作者(隱私第一)。`log.jsonl` 存聚合 metric + 承諾(`max_pos_pct=0.48`、「虧損不加碼」);`theses.jsonl` 存 per-position 的五要素持股假設(why 判斷 / horizon 時間軸 / triggers / stop・target_size / driver 同注辨識,#136)。**append-only**:修正 thesis = 補一筆新 event(帶 `revises` 指回舊的),**不蓋舊的** —— 才能跨期看你當初怎麼想、後來怎麼變(蓋掉 = 跨期對帳失效 + 鼓勵事後合理化)。 - -### Step 0 · 把任意券商格式變成引擎吃得下的(用讀檔者自己的 Claude) - -用戶的 CSV 可能來自任何券商、欄位名各異,甚至是一張對帳單截圖。**不要寫死 parser**——你(Claude)直接讀它,轉成標準欄位存暫存 CSV:`Symbol,Action(BUY|SELL),Quantity,Price,TradeDate(YYYY-MM-DD),RecordType(填 Trade)`。這步用的是用戶自己的 Claude 額度,零後端成本,且天生吃得下所有券商——不必為每家券商寫轉換器。 +The agent must understand and normalize broker data locally into: +`Symbol / Action(BUY|SELL) / Quantity / Price / TradeDate / RecordType(Trade)`. +Add `Market / Currency` for non-US instruments when available. Do not ask the user to normalize the file. -- **🌏 多市場(#173):非美股一律標 `Market`/`Currency` 兩欄(缺 = 美股 USD,向後相容)**。台股尤其要點:`Symbol` 填**完整 yfinance 代號**——上市掛 `.TW`(台積電 `2330.TW`)、上櫃掛 `.TWO`(如 `5483.TWO`);上市/上櫃是**你的世界知識,引擎不查表**。`Market=TW`、`Currency=TWD`,日期若是民國年(`113/07/10`)先換成西元。港股 `.HK`+`HKD`、日股 `.T`+`JPY` 同理。這樣引擎才抓得到台股報價、α/β 才對得上加權指數(`^TWII`)、combined 最大單點依賴/賽道曝險分母才含台股——**否則台積電從引擎世界消失,最大依賴會誤報成某支美股**(這就是 #173 的病灶)。混幣聚合入 USD、缺匯率時明示「近似」都由引擎處理(見 Step 1 `currency_meta`/`honesty_ledger`),你只負責把格式標對。 +`prepare` creates a Review Plan; it does not create a conclusion card. Read only the flow selected by `review_plan.flow_path`: -**📒 帳本雙輸入(snapshot-anchored;#31 修訂版,設計見 `docs/prd-ledger.md`)**:用戶丟的可能不是交易流水,而是**持倉截圖/持倉頁**——多數人拿不出完整交易紀錄,這是常態不是錯誤。兩種輸入進同一本帳(`~/.trade-coach/ledger.jsonl`,append-only、純本機、不外傳): +- `flows/first-review.md` +- `flows/weekly-review.md` +- `flows/snapshot-review.md` +- `flows/test-drive.md` -- **持倉快照** → 你讀圖/表轉成 positions JSON(`[{"ticker","shares","avg_cost"?,"market"?,"currency"?}]`,**均價不知道就留空,別編**),存暫存檔後: - `python3 engine/ledger.py append-snapshot /tmp/pos.json --as-of <宣告日,通常今天> --cash '{"USD":8200}'` - (snapshot 語意 = 該日**收盤後**狀態,同日交易視為已含在宣告數字內。**`--cash` 把下方 💵 收到的現金餘額一起存成錨點**(flat dict,多幣別 `{"USD":..,"TWD":..}`)——多週累積 ≥2 個錨點後,引擎自動逐段 rollforward 對帳、量化漏記金流,見 Step 1 `data_integrity.cash_residuals`;#180。) -- **已有帳本、又丟來新快照 → 先 `reconcile`,不要直接 append**:`python3 engine/ledger.py reconcile /tmp/pos.json` 會列宣告 vs 推導的差異——一致 = 對帳通過(卡上可標「帳本已對帳 ✓」);不一致 = 把差異講給用戶聽(「我推 NVDA 40 股,你說 35——中間可能有我沒看到的交易」),他確認後以**他的宣告為準**:`append-snapshot --source reconciled`。這是「數據準確」的機制:每丟一次快照 = 帳本自我修復一次。 -- **交易 CSV**(標準化後)→ 除了餵 `trade_recap.py`,同時記帳:`python3 engine/ledger.py append-trades <標準化CSV>`(自動去重,每週增量匯入、重疊期重複匯入都安全)。輸出的 `skipped_future_dated` 非 0(#169:TradeDate 晚於今天,疑似 Step 0 把 MM/DD 誤判成 DD/MM)→ 那幾筆已被拒收、沒寫進帳,回頭跟用戶核對原始對帳單那幾筆的日期,別自己猜著改;記完帳接著排出場追蹤:`python3 engine/revisit.py enqueue-from-ledger`(掃清倉/大減倉 → 30/60/90 佇列,去重、重跑安全)。enqueue 完**再跑一次 `revisit.py scan`**,讀輸出的 **`recent_exits`**(出場 ≤14 天、金額大者先)——這是 Step 2(d) 賣出理由 capture 的候選集(#136:「為什麼賣」只有出場後兩週內問得到,不可回補;空 = 該段靜默跳過)。enqueue 輸出的 `new` 只是「本次新排入」的參考訊號,**capture 候選一律以 `recent_exits` 為準**——上週中斷沒問到的、當週超過限額的,窗口內這裡還會再出現。 -- **💵 現金餘額錨點(#171,讓「這筆入金該不該部署」通電)**:交易 CSV 只記部位、記不到帳戶閒置現金——沒有它,`cash_weight` 算不出(引擎降級標不可信,見 Step 1 `honesty_ledger`)。所以 Step 0 順手抓一次**當前現金餘額**:對帳單/持倉頁多半有一行「Cash / 現金 / 可用餘額」——你直接讀出來;讀不到就用 `AskUserQuestion` 問一句「對帳單上的現金餘額大約多少?(想看帳戶層現金比重/入金判讀才需要;略過也能出卡)」。拿到就組 JSON 餵引擎:單一帳戶 `TR_CASH='{"as_of":"<對帳單日期>","amount":<數字>,"currency":"USD"}'`;**台美等多帳戶/多幣別各給一個錨點,用 list**:`TR_CASH='[{"as_of":..,"amount":..,"currency":"USD"},{"as_of":..,"amount":..,"currency":"TWD"}]'`(引擎 per-currency 各算餘額再按匯率聚合;台股帳戶用 TWD)——引擎以錨點為準(對付 CSV 非從開戶完整),其後現金流才疊加。只給部分帳戶的錨點也行:沒給的幣別引擎標盲算,`honesty_ledger` 只揭露缺的那個、邀你補。**入金判讀(`recent_net_deposit`)要看得到存提款流水**:標準化 CSV 時若來源有 deposit/withdrawal/股息/利息/費用列,連同 `Amount` 欄一起留著(格式見 `mock/sample_noisy_broker.csv`),引擎才算得出本期外部淨流入;來源沒有就只靠錨點給比重,判讀那句靜默跳過。 -- **snapshot-only(只有快照、還沒有交易紀錄)**:行為診斷跑不了(那需要交易紀錄——誠實講,別硬掰),但出**開帳體檢卡**:用 `holdings` JSON 的成本權重 + 你的世界知識 driver map 講持倉結構(集中度/賽道/sizing,標明「成本基礎」),AI 猜 thesis(Step 2(c))照走,記憶迴圈當場啟動;`integrity` 非空(oversell/壞行)一律如實帶上卡。收尾邀請:「之後把交易紀錄丟給我,攤平/出場/盈虧比這些行為診斷就會解鎖」。 -- **帳本誠實檢查**:`holdings` 輸出的 `counts.skipped_lines > 0` = 帳本檔有壞行(可能是中斷寫入)——**如實告訴用戶**、別當帳本完整;修復法就是請他丟一張最新持倉截圖走 reconcile(新錨點蓋過可疑歷史)。(`ledger.py` 純標準庫,不需要 venv——跟 `trade_recap.py` 的 ModuleNotFoundError 提示無關。) -- ⚠️ **過渡期規則**:錨點帶入的持倉 engine 看不到(CSV 無該檔交易),所以 ledger 的 cycle_id 與 engine state 的 cycle_id 可能不同——**theses.jsonl 綁定一律仍照抄 engine state 的 cycle_id**(收尾 part 2 的既有規則,CLI 會驗格式),ledger 的 cycle_id 只供帳本自身追蹤。 +Then read the shared rules: -### Step 0.5 · 生成 driver map(讓冷門股不失準) +- `references/agent-boundaries.md` +- `references/thesis-policy.md` +- `references/card-policy.md` +- `references/data-contract.md` -引擎 sector 表只認常見股,冷門股會變「未分類」→ 分散維失真。**你(Claude)對用戶實際持倉用世界知識分類**:每檔 → `[sector, thematic]`,thematic=1 表示跟別檔同屬一個跨產業主題(AI capex / 減重藥 / 太空…)。寫成 JSON `{"PLTR":["軟體雲",1],"CEG":["核電",1],"XOM":["能源",0]}`,用環境變數餵進去:`TR_DRIVER_MAP=/path/driver_map.json python3 engine/trade_recap.py `。 +## Fixed lifecycle -### Step 1 · 跑引擎,抓大放小 - -**SKILL 走 JSON 模式拿結構化資料,Step 3 你自己寫卡 ——** 不要照搬 engine 預設輸出(那是 README quickstart 用的乾淨人話卡 / fallback,不是 SKILL 規格那張定論卡): +1. `prepare`: run the engine, reconstruct active theses, deduplicate questions, and return a Review Plan. +2. Agent work: make only permitted qualitative judgments, ask every required question, create inferred theses for uncovered positions, and write a narrative with no digits. +3. `preview`: validate answers, evidence, theses, and narrative; then render private and public previews. +4. Show the private preview. Ask the user to choose a candidate rule, provide a custom rule, or skip. +5. `finalize`: validate the final commitment, atomically commit the canonical session bundle, then rebuild compatibility projections. ```bash -mkdir -p ~/.trade-coach -TR_JSON=1 TR_STATE_OUT=~/.trade-coach/last_state.json python3 engine/trade_recap.py <標準化後的CSV> -# TR_JSON=1 → stdout 純 JSON(build_card_data,給你在 Step 3 寫敘事卡用);meta 走 stderr -# TR_STATE_OUT → 寫一份薄 state(對帳用),跟 TR_JSON 平行,可同時設 -# TR_PREV_END= → 對帳模式必帶(#137):問題帳的行為型事件只取其後的 -# 新交易(不會把三個月前的舊攤平每週重複入帳);初診不設 = 全期補齊,問題帳統計冷啟動 -# TR_CASH='{"as_of":..,"amount":..,"currency":..}'(單帳戶)或 '[{..},{..}]'(台美多帳戶各一錨點) → 現金餘額錨點(#171,Step 0 抓的);設了 cash_weight 才可信,不設引擎降級標不可信 -# 都不設 → 印預設人話卡(README quickstart 用) -# TR_DEBUG=1 → 在預設輸出補回 5 維 severity raw 表(開發/驗證用,絕不上卡) -``` - -> 🔧 **引擎報 `ModuleNotFoundError`(如 pandas / yfinance)**:依賴多半裝在 venv / pyenv 的另一個 python 裡。找到裝了依賴的直譯器路徑重跑一次即可,常見是 repo 根的 `.venv/bin/python3`(README 安裝節的 venv 三行裝出來的)——把上面指令的 `python3` 換成那個路徑;別急著全域 pip(新 macOS 會被 PEP 668 擋)。 -引擎吃標準欄位(Symbol / Action(BUY|SELL) / Quantity / Price / TradeDate),`TR_JSON=1` 吐的結構含: -- **`top_holes`**:已選好的 top 1–2 機械洞 + 對應鏡片 quote(融入敘事,**別當結語**)。 -- **`candidate_rules`**:2–3 條候選規矩(卡上列候選,**Step 3.5** 讓用戶挑/改一條,**別只給第一條**;引擎只給一條時就用那條)。 -- **`thesis_questions`**:per-ticker 持股假設問句 — **這是給 Step 2 對話用的,絕不准印在卡上**(SKILL 鐵律:確認在出卡之前)。 -- **`alpha_beta_breakdown` / `payoff_attribution` / `ticker_diagnosis`**:完整數字,你拿去組敘事。 -- **`dims_raw`**:5 維行為診斷(每維 severity 0–1)— **別整張攤出來**,用「一句人話」帶過非 headline 的維度(SKILL 鐵律:不放 5 維小數表)。 -- **`overview.unrealized_coverage`**:未實現只加總抓得到現價的持倉(`priced_n`/`held_n`/`unpriced`)——讀這欄拿數字,**該不該揭露交給 `honesty_ledger` 統管**(不用自己記何時補)。 -- **`cash`**(#171 帳戶現金):`{balance, weight, source, reliable, recent_net_deposit, by_currency}`。`balance`=聚合 USD、`by_currency`=per-幣別原幣明細。`reliable=true`(所有有現金流的幣別都給了 `TR_CASH` 錨點)才把 `weight`+ `recent_net_deposit`(判「這筆錢部署了沒/解不解集中度」)講進卡;`source=partial`(部分帳戶給了、部分沒)或 `csv_sum`(全無錨點)= 靠流水盲算,`weight` 多半 `null`,該不該揭露交 `honesty_ledger`(`cash_reliability.unanchored_currencies` 標哪個幣別缺)。講法照 card-spec「現金與入金判讀」段。 -- **`acct_perf`**(#171 帳戶級績效):`{acct_twr, hold_twr, cash_drag, drag_dollar_approx, avg_cash_weight, irr_annual, window, basis, note}`,全部 engine 算好——**只准照抄,不准自己算**(#154 拍板)。`acct_twr` 非 null 才講帳戶級;`{note}` 單鍵或 `acct_twr=null` = gate 掉(現金錨點不可信),只剩 `hold_twr` 持倉柱可用、`note` 說了為什麼。三數字講法與 drag 正負翻譯照 card-spec「帳戶級績效」段;地基缺口該不該揭露交 `honesty_ledger`(`acct_perf_basis`)。 -- **`currency_meta`**:聚合幣別與匯率(💱 Display currency 段的資料源)——`aggregate_currency`(overview / what_if / `ticker_diagnosis` 金額等聚合數字的幣別)、`mixed`、`fx`(兌 USD)、`pnl_by_currency`(原幣分桶)、`fx_error`/`alpha_beta_note`。台股/混幣組合寫卡前**先讀這欄**,金額才不會標錯幣;混幣時單檔原幣金額用 `pnl_by_currency` 對照、或由你按 `fx` 反換算。 -- **`honesty_ledger`**(#82:誠實點的單一事實源):engine 已聚合好這張卡**必須交代**的誠實缺口清單(空 list = 無缺口),每項 `{key, status, data}`,涵蓋 α 不可信 / 板塊歸因不全 / 未實現缺價 / 未分類 driver / 賣超 / 混幣 / 現金無錨點。**engine 判定「該講什麼」,你只管照 card-spec 的講法融入敘事「怎麼講」**;出卡前逐項核對(Step 3 gate)——取代了散在各欄位「自己記得哪些該揭露」的自律。 -- **`pnl_curve`**(#167:總損益從一個點延伸成一張圖):復盤期間累積損益曲線,`{points:[{date,cum_ret}...]}`(起點 `cum_ret` 恆 0、終點對齊 `overview.total_pnl` 那個數)或 `{note:...}`(無價格/樣本不足/混市場尚未支援 → 誠實跳過)。**只在 widget 模式畫成 sparkline**(單色細線,不逐點染色,別重回「多色格子熱力圖」);純文字卡沒有視覺化,`note` 不必轉成一句文字硬補——畫法規格見 card-spec。 -- **alpha/beta**:贏大盤多少、其中多少只是「膽子大(高 beta)」、真本事(Jensen's α)剩多少。`excess_split` 把「贏大盤」機械拆成 **押對賽道(allocation)+ 板塊內選股(selection)**,兩項相加恆等於贏大盤 pp——這兩個數是會計恆等式、不需統計顯著,**永遠可講**;`alpha_stat` 給 α 的 95% 區間 / t 值 / 分級(顯著與否),語氣照它走。 - **per-market(混市場組合必讀,#129)**:`alpha_beta_breakdown.scope` 非 null = 組合跨市場,α/β 已按市場分算(US→SPY、TW→台股加權指數),**頂層數字僅含 `scope` 那個市場的部位**——卡上 α/β 段要**兩行並列**(每市場一行,各含資金佔比、各對各的大盤、各自的顯著性語氣),讀 `by_market`;**絕不把兩個市場的 α 加總或平均**(不合成總 α)。台股部位的拆帳 `coverage=0`(無板塊對照、按大盤計)→ 只講「贏/輸台股大盤 X pp」,不拆賽道/選股;`by_market` 內某市場帶 `note`(如 `^TWII` 沒抓到價)→ 該行誠實寫「對照基準抓不到價,本期不判」。單一市場組合 `scope=null`,一切照舊。 -- **結構化 state(`TR_STATE_OUT`)**:給對帳用的薄 JSON,讀這幾個欄位 —— - - `headline_dim` / `headline_metric`:這次最大的洞 +(key, value)。 - - `commitment`:`{rule, metric_key, metric_value, goal}` = **引擎的機械預設承諾**(下次只改這一件 + 追蹤哪個 metric)。**Step 2 動機問完可能推翻它**(實例:engine 給「別加碼」,用戶答「計畫內定投」→ 改盯 `ai_pct`)→ 收尾要存**卡上最終那條**,不是這個預設。對帳比 `metric_key`,別比 headline(規矩維 ≠ headline 維才不對錯帳)。 - - `metrics`:全 metric 快照(`max_pos_pct / avgdown_count / ai_pct / max_sector_pct / top3_pct / payoff / beta / alpha_ann …`),對帳時拿承諾的 `metric_key` 反查新值(集中度承諾就追 `ai_pct`)。 - - `alpha_ann` / `alpha_t` / `alpha_credible`:α **永遠有數,語氣看統計**。`alpha_credible=true`(樣本 ≥1 年且 |t|≥1.96)才可用「真本事」語氣(顯著的負 α 也是可講的定論);`false` → 數字照講但**必帶不確定性**:「α 年化 +X%,但 95% 區間 −Y%~+Z%——統計上還分不出是本事還是運氣」。**卡在哪要講清楚**,引 `alpha_beta_breakdown.alpha_stat.gate.reason`:`sample_short`=不到 1 年 → 才是**樣本不足**;`not_significant`=區間太寬 → 常見原因是**持倉集中、個股雜訊大**(這條跟『最大的洞=集中度』是同一件事,要串起來講——但這是工具的侷限,不是他沒本事)。**贏大盤幾 pp 必配拆帳**:押對賽道 vs 板塊內選股(`excess_split`),`coverage<1` 時補一句「X 檔無板塊對照、按大盤計」。 - - `insufficient_data`:`true`(round-trip<3 或交易跨度<~84 日曆日≈60 交易日)→ **只做體檢、不硬出 commitment**(見開場/收尾)。 - - `problem_events` / `problem_opportunities`(#137 問題帳):本次規約出的問題事件(behavior 型帶交易日與金額;state 型=倉位結構的每週選擇)+ 各類問題「本期有沒有機會犯」快照。**收尾 part 5 原樣 append 進 problems.jsonl**,你只補動機類事件,不改機械類。 +python3 engine/review.py preview \ + --session-id --answers /tmp/answers.json --narrative /tmp/narrative.json -**市場背景(#37,跑完主引擎順跑;離線缺席不擋流程)**: - -```bash -python3 engine/market_context.py --start <窗口起> --end -# 窗口:對帳模式 = 上次 log 的 date_end → 這次 state.date_end;初診 = date_end 往前 7 天 +python3 engine/review.py finalize \ + --session-id --answers /tmp/answers.json --narrative /tmp/narrative.json ``` -- 輸出 `benchmarks`:SPY / QQQ 的 `window_ret`(窗口漲跌)+ `ytd_ret`,VIX 的 `last / prev / delta`(水平值,情緒溫度計)。這是**語境,不是診斷**——用在:① 卡開頭的市場背景一行(格式見 card-spec)② 歸因語境:他的動作放進大盤同期的背景講(「你這週砍在 SPY -4% 的恐慌週」)③ Step 2 動機輔助訊號:大漲週進場 = FOMO 候選、大跌週砍倉 = 恐慌候選——**只是輔助你選問誰,不是定性**(定性永遠來自他的回答)。 -- **`error` 非 null(離線/未裝)→ 卡上市場背景整段不出**,需要提的話一句「本週缺市場背景(離線)」帶過;**絕不用記憶編大盤數字**。**`missing` 非空(部分家數沒抓到,`error` 可能仍是 null)→ 有什麼講什麼**,缺的那家直接略過、不硬掰——別假設 SPY/QQQ/VIX 三家永遠都在。 -- **「你 vs 大盤」只有一個合法數字源 = `alpha_beta_breakdown`** 的 `port_tot`(你的持倉)/`spy_tot`(大盤,US=SPY、TW=^TWII)/`excess_vs_spy`(差 pp):engine 已算好、**只准照抄,不准自己重算**——這一行回答用戶最直白的「我自己選股該不該乾脆無腦買指數」(#164 柱2),講法見 card-spec「該不該買指數」段。市場背景(上面 market_context)的大盤漲跌是語境、**不是對比源**:引擎不算帳戶每週市值序列,別拿它的大盤 window_ret 去心算一個「帳戶那週 +X% vs SPY」(那才是 #37 原本要擋的幻覺)。 - -**抓大放小鐵律**:只看引擎排在最前面的 1–2 個洞,**其餘忽略**。不要把 5 維全攤給用戶——那就變成另一份報表了。引擎已經幫你收斂,你不要再展開。 - -### Step 2 · 出卡前的對話確認(持股假設 + 動機)——這層才是鏡片,不可省 - -**流程鐵律:確認在出卡之前,不在卡上。** 機械算得出「你做了什麼」(what),算不出「你為什麼這樣做」(why)。所以**先在對話裡問完所有需要你定性的問題、拿到答案,Step 3 才出最終卡**——卡是確認後的定論,不是帶問號的待辦。**別把問題做成卡上的按鈕**(那是把 Step 2/3 混在一起)。 - -**問法鐵律(#55):動機/定性問題一律用 `AskUserQuestion` 工具問,不要寫成文字段落等用戶打字。** 每題二選一(選項裡把兩個動機都寫成人話)+ 用戶可跳過,一次最多 2–3 題,5 秒可點完。自由打字 = 摩擦:用戶會直接略過 Step 2,卡就只能標「待確認」半成品,教練迴圈斷在第一環。只有執行環境沒有 AskUserQuestion 工具(非 Claude Code 的 agent)才退回對話問。 - -**消重鐵律:答過的不重問(每週被問同一題 = 教練失憶,用戶會走)。** engine 只讀 CSV,不讀記憶——`thesis_questions` 每次都會對同一批標的重新生成,**消重是你(Claude)的責任**:問任何一題之前,先比對 `theses.jsonl`(Step 2.5 重建出的 active thesis)與 `log.jsonl` 最近一筆的動機定性。同一 `cycle_id` 用戶已答過(thesis `maturity=testable`、或上次卡已標凹單/逢低定論)→ **這題不再問**,直接引用舊答案入卡(「上次你說 MSTR 是凹單」);要更新認知走 Step 2.5 對帳的「順手改」,不是重新問卷。**只有三種情況同一標的可以再問**:① 新 cycle(清倉後重建倉)② 行為顯著變了(上次答逢低、這次又深虧加碼 N 次 → 對帳語氣問「還是當初那個理由嗎」,引用他上次的答案)③ 用戶上次跳過(`inferred` 不算答過,但也只在它仍是「金額最大 + 行為矛盾」時才重問一次)。 - -**(a) 持股假設:逢低加碼 vs 凹單(標的層挑出來的)** —— 引擎 `ticker_diagnosis` 對「金額大 + 虧損中狂加碼」的標的生成 `thesis_q`。機械分不出逢低/凹單,因為**差別在加碼當下 thesis 還在不在(= why,算不出)**,所以挑出來問你: -- 還在虧的(如 MSTR 加 26 次還虧):「你還相信當初的理由,還是不想認賠在凹單?」 -- 賺回來的(如 GOOG 加 9 次現賺):「計劃內核心倉,還是套牢後才合理化、剛好漲回?」 -→ 答「凹單/合理化」→ 卡標凹單;答「逢低/計劃內」→ 移除警告、標逢低。**機械挑誰問,你的答案定性。** - -**(b) 動機(鏡片)** —— 從引擎的洞,對應最該問的交易,用下面的鏡片動機單元問。讀 `rubric/vincent-yu.md` 拿原話,讓問題真的是「這套哲學會問的」,不是泛泛而問。 - -**鏡片動機單元 → 交易訊號 → 問句模板:** - -| 引擎抓到的洞 | 鏡片單元(去 rubric 看原話) | 問用戶的二選一(舉例,要換成他的真實 ticker/數字) | -|---|---|---| -| 虧損中加碼攤平 | **A2** 試探≠加碼、**G** 不想認賠 | 「PLTR 你從 24 一路加到 15,是因為**你知道了一個進場時不知道的新利多**,還是**不想認賠、想攤低成本等回本**?」 | -| winner 賣太早 | **D1** 時間軸、**G1** 焦慮驅動 | 「你賣掉賺錢的有 71% 後來繼續漲。那些賣出是**thesis 到價了**,還是**賺了怕回吐、落袋為安**?」 | -| 部位梭哈 | **B1** 賠率、**A1** 信念是光譜上的sizing | 「PLTR 佔你 48%。這個 size 是**算過最壞情況能承受**,還是**就是很看好、直接重壓**?」 | -| 集中在同一 driver | **B2** driver 不同才算分散 | 「你 X 檔 Y% 是 AI。你當初**覺得這樣算分散**,還是**刻意押這個賽道**?」→ 答案決定標題,見下規則 | -| 把 beta 當 alpha | **E2** 拆解你承擔什麼風險 | 「你贏大盤 +80pp,但 β=1.8。這些報酬你算**自己選股的本事**,還是**敢押高波動 AI**換來的?」 | -| 連勝後加大 sizing | **G2** 連勝是該檢查的警報 | 「這筆加大,是**有獨立的新理由**,還是**最近都對、覺得手感正順**?」 | - -**規則**: -- 一次最多問 **2–3 個**(抓大放小,別審問)。每個都是二選一,5 秒可答。 -- 用戶選哪個都不要說教——這是**鏡子,不是審判**。他選「不想認賠」就接「好,那這就是下面那條規矩要擋的事」。 -- **答案要改標題,不是只補在『看動機』那行**——這是 Step 2 的全部意義。最常踩雷的是**集中度**:用戶答「**刻意押賽道 / 知道集中**」→ 那個洞**絕不准叫「假分散」**(他沒在騙自己,你問了他還罵他=自相矛盾)。改框成「**你選的集中押注**」,打的點變兩個:① 它讓你的**選股本事測不出來**(就是 α 判不出的原因,串起來講)② **集中回檔風險**——有沒有減碼/停損線。答「以為分散」→ 才用「假分散」。凹單/逢低、梭哈同理:答案怎麼說,標題就怎麼標。 -- 用戶若略過不答,就只用機械洞出卡,不強逼。 - -**(c) 建立 / 更新 thesis(AI 猜為主、問為輔 —— 取代週記錄的核心)** - -> **鐵律:降摩擦 + 克制。** 這產品的命是「不變成你想逃的重系統」。thesis **絕不逼用戶坐下來填** —— 由你(Claude)從交易行為 + ticker 世界知識**猜**,預設落盤標 `inferred`,用戶不爽再改。讓用戶**冷啟動就有完整 thesis 庫、零填寫成本**。 - -**主路徑:AI 推測,不問用戶。** 對每個缺 thesis 的持倉,用 engine 行為訊號(`ticker_diagnosis` 的 定投/凹單/押太重/紀律持有 + 加碼次數 + `cur_ret` + 持有天數)+ ticker 是什麼公司 / 賽道,**按五要素結構猜**一筆 thesis(#136:VY 式判斷缺一要素就不算完整;結構是猜的骨架,**不是逼用戶填的表單**——AI 照樣全猜、用戶照樣順手改,摩擦不變): -- **why(判斷)**:猜「**他可能知道什麼還沒被 price in**」,不是複述行為——❌「定投型核心倉」(那是行為,不是理由);✅「賭 AI 推論需求外溢到電力缺口,市場還在按舊供需定價(推測自:規律加碼+長抱核電)」。行為是證據,判斷才是 thesis;**真猜不出判斷(如疑似凹單)→ 誠實寫「攤平等回本(待確認)」且 `horizon` 落 `null`**——編不出判斷就沒有時間軸,別給假 thesis 配假 horizon 讓後續對帳拿去當真。 -- **horizon(時間軸,D1)**:這個判斷是**幾週 / 幾季 / 幾年**的事?從行為猜:規律定投 + 長抱 → `年`;押財報 / 事件 → `季`;短進短出 → `週`。沒有時間軸的理由無法對帳——之後「說是三年的事、40 天就跑」這種自相矛盾才抓得到。 -- **三 trigger(可證偽退出 + 情境→action,D2/D3;其對賠率的影響接 B1)**:從 ticker 類別猜常見的 —— 成長股 → 營收 / 用戶增速失速;週期股 → 週期反轉;AI 概念 → capex 轉弱。`reduce` 從當前 sizing 猜(已超標 → 該檔減碼線)。 -- **stop / target_size(賠率 + 信念→倉位,B1/A1)**:既有欄照猜——最壞情況虧多少、這個理由值多大注。 -- **driver(這是不是同一注,B2)**:對照 Step 0.5 你生成的 driver map——與現有持倉同 driver / 同 thematic 的,**why 裡必須點名**(「與 NVDA 同屬 AI capex 一注」),別讓五檔各自漂亮的 thesis 合起來是一注梭哈。 -- 每條標來源 `(推測自:規律加碼+長抱)`,讓用戶一眼看出是猜的、好校正。 -- **maturity = `inferred`**,全部**直接落盤、零提問**。 -- **順猜想法來源(#38 薄版,同樣零提問)**:每筆 thesis 帶 `source_type`(`kol` | `research` | `self` | `other`)+ `source_name` + `source_confidence`。**對話或歷史上下文有明確訊號才標 `kol`/`research`**(用戶這次或先前提過「股癌說」「看了某篇研究」→ `source_name` 填來源名);毫無訊號 → `self` + `source_confidence:"candidate"`(誠實標猜的,別編一個 KOL 出來)。用戶親口確認過才升 `confirmed`。這欄現在只累積、不上卡——等樣本夠了(#38 完整版)才做「自己研究 vs 跟單」勝率分組;**但欄位不可回補,從今天開始收**。 -- **順猜進場情緒/信心(#36 薄版,同樣零提問、選填)**:每筆 thesis 順帶猜 `emotion`(`fomo` | `composed` | `forced` | `planned`)+ `confidence`(`high` | `medium` | `low`),各配 `_inferred:true`(AI 猜的、未經用戶確認)。**猜法**:emotion 結合行為訊號 + 進場時機(市場大漲後才追買 / 同賽道已重押還加 = `fomo` 候選;規律定投 / 事件前布局 = `planned`;深虧狂加碼 = `forced`;其餘 = `composed`),confidence 從 why 的語氣推(「基於 Q2 財報」等具體依據 = `high`;「我覺得/賭一把」= `low`;之間 = `medium`)。**若 Step 2 那一問用戶親口透露了情緒/把握(如答「就是怕錯過」)→ 對應欄升 `_inferred:false`**。跟 source_type 一樣**現在只累積、不上卡**(#36 完整版才做「FOMO 進場勝率 vs composed」分組)——**欄位不可回補,從今天開始收**;真的一點訊號都猜不出就整欄留空(null),別硬填。 - -**只在一種情況問用戶一句**(抓大放小,別審問): -- **行為矛盾、金額最大的那 1 檔**(疑似凹單 / 深虧還加碼)—— 機械分不出「逢低 vs 凹單」,差別只在 why(算不出)。問一句(同樣走 AskUserQuestion,三個選項直接給他點):「{ticker} 加碼 N 次還虧 X%,我猜是不想認賠(凹單)—— 對 / 有新理由(逢低)/ 跳過」。 -- **同一次 AskUserQuestion 順帶第二題收來源**(不多一次互動):「{ticker} 這筆的想法最初從哪來?—— 自己研究 / 別人推薦(KOL、朋友——用 Other 填是誰)/ 忘了」。答了 → `source_confidence:"confirmed"`;「忘了」/跳過 → 維持猜的 `candidate`,不追問。 -- **一次最多問 1 檔**;其他全用猜的,不打擾。用戶跳過 → 留 `inferred`,不追問。 +Do not rerun the engine after an interruption: -**校正走「對帳時順手改」,不是「坐下來填」**:對帳(Step 2.5)呈現猜的 thesis + trigger 觸發,用戶看到猜歪的**順手改一條** → 該 thesis 升 `testable`(用戶確認過)。明說「投機跟風沒 thesis」→ 標 `draft`。thesis 越用越準,但從不逼填。 - -**鐵律不變:`exit_trigger`(看錯了,事實)≠ `stop`(跌多少賣,價格)。** 猜的時候 exit 也猜「thesis 失效的事實」,不是猜停損價。寧可 `inferred` 也不要假的 `testable`。 - -**(d) 賣出理由 capture(#136)—— 出場當週唯一的收集窗口,錯過不可回補** - -買入的 why 有 thesis 承接,**賣出的 why 目前只活在對話裡**——這段把它落盤,30/60/90 出場追蹤(Step 2.5)才有「你當時自己說的理由」可對答案。 - -- **觸發**:Step 0 enqueue 後那次 `revisit.py scan` 輸出的 **`recent_exits`**(引擎已按出場 ≤14 天過濾 + 金額排序;初診匯入的更早歷史出場天然不在裡面,不會冒出十筆舊出場拷問)。空 → 整段靜默跳過,不提。 -- **消重(重跑安全)**:對每筆候選,先比對 `theses.jsonl` 既有的 `event:"exit_narrative"` 行——同 `revisit_id` 已有記錄(含 `capture:"skipped"`)→ 不重問。**同一 ticker 同日多筆出場**(先減倉後清倉 → 佇列兩筆)→ 只問最終那筆(`full` 優先),另一筆落 `capture:"skipped"` 消重,別對同檔同天問兩次。 -- **一次最多問 2 筆**(候選已金額大者先)。**沒問到的不落盤**——它們留在 `recent_exits`,窗口內下次 session 補問;窗口過了就自然消失(誠實缺資料,不編)。只有「問了但用戶跳過」才落 `capture:"skipped"`(跳過=他選擇不答,窗口內重問=追問,違反不逼填)。 -- **問法(AskUserQuestion,一筆一題,四分法選項寫成人話 + 帶他的真實數字)**。盈虧數字從 engine state 的 `ticker_diagnosis` 拿,拿不到就省略,**別自己算**;減倉比例 = `shares_sold / shares_before`。 - - `kind:"full"`(清倉):「{ticker} 你 {exit_date} 在 {exit_price} 全部出清。當時賣的理由是——**到價了**(當初設的目標走完)/ **看錯了**(thesis 的失效條件發生)/ **換更好的**(把錢挪去 {swaps 的 ticker,無 swap 則寫「別的標的」})/ **想落袋**(怕回吐、想鎖住獲利)」+ 可跳過。 - - `kind:"reduce"`(減倉 ≥50%):同四分法但措辭對齊「還留著一半」的事實——「**到了減碼點**(計畫內的部位調整)/ **信心動搖**(thesis 部分失效,先降風險)/ **換更好的**(騰資金去 {…})/ **想落袋**(鎖住一部分,怕回吐)」。落盤的 `exit_reason` 仍用同一組值(`price_target`/`thesis_broken`/`swap`/`anxiety`)——風控降倉、再平衡這類「都不是」→ 用戶點 Other 寫原話,`exit_reason` 落 `null` + `note` 存他的話。 - - 前二=紀律,後一=焦慮訊號——但**問的當下不說教**,這是 capture 不是審判,定性留給 30/60/90 對答案。 - - **時間軸自相矛盾必帶(#136)**:對 `horizon.py scan` 標 `exit_too_fast` 的該 cycle(門檻 deterministic 住 engine——見 Step 2.5 重建段的 scan;你不再自己算天數 / 比閾值)→ 問句補一句鏡子:「你當初說這是{horizon}的事,{marker 的 `holding_days`}天就走——是判斷變了,還是心態動了?」(thesis `inferred` 時措辭改「我當時猜這是{horizon}的事」)。engine 已對 `horizon` 缺欄 / `null` 自動跳過——別回頭從舊 why 腦補一個 horizon 出來。這正是 horizon 欄存在的理由:沒有時間軸,理由無法對帳。 -- **賣出動機只有一種情況可以猜**:`swaps` 非空 → 猜 `swap`(標 `capture:"inferred"`,對答案時措辭用「我當時猜你是換標的」)。其餘(到價 vs 落袋、證偽 vs 恐慌)全是內心狀態、機械分不出——**用戶沒答就落 `exit_reason: null` + `capture:"skipped"`,絕不編**(有 swap 交易事實撐的才敢猜,沒有事實的猜測=替用戶編賣出動機,比不記還糟)。 -- **落盤**:跟 thesis 一起在收尾 part 2 統一 append 進 `theses.jsonl`(格式見該段 exit_narrative 範例),`exit_reason` ∈ `price_target` | `thesis_broken` | `swap` | `anxiety` | `null`,`note` 存他用 Other 補的原話(若有)。 - -### Step 2.5 · 對帳上次的 thesis 與承諾(只在對帳模式 / log 非空) - -**先重建「目前有效的 thesis」(append-only 讀取必做,否則 active 名單會爆掉)**:`theses.jsonl` 是 append-only,同一 thesis 有多筆 revision。讀取時按 `thesis_id` 建 event log,**每個 cycle 只取 latest 未被 supersede 的**: -- 後出現的 `revises: <舊 id>` → 把舊 id 標 superseded、排除。 -- cycle 已清倉(該 `cycle_id` 不在 engine `holdings.positions`)→ 該 thesis 標 closed、不進對帳(歷史保留)。 -- **`event:"exit_narrative"` 的行不是 thesis revision**——跳過、不進 active 重建;它是出場敘事(Step 2(d) 落的「當時為什麼賣」),只在出場追蹤對答案時按 `revisit_id` 撈。 -- 結果 = 每個 active cycle 恰一筆有效 thesis。 -- **重建完跑 `python3 engine/horizon.py scan --as-of `** 取時間軸觸線標記:active_theses 每筆帶 `cycle_id` + `horizon`,清倉的那筆另帶 `exit_date`(= 該 cycle 在 `recent_exits` 的出場日)。engine 回 `exit_too_fast`(清倉太快)/ `held_too_long`(抱太久),各帶 `holding_days`。**門檻(deterministic)住 engine,你不再自己算持有天數、不眼球比閾值;`horizon` 缺欄 / `null` / 非三值 engine 自動跳過**。這批標記供 Step 2(d) 賣出 capture(`exit_too_fast`)與下面 trigger 檢查(`held_too_long`)共用——同一次 scan,別各算一遍。 - -出新卡先回看上次: -1. **承諾 metric**:上次 `commitment.metric_key` 舊值 → 這次 engine state 新值(「上次說壓到 20%,當時 51% → 現在 48%:在降、沒達標」)。 -2. **trigger 檢查 —— 只查三類,別逐檔掃(逐檔掃 = 把復盤變研究任務 = 回到高級拖延)**: - - 只查:**本週有交易的 ticker** + **上次承諾關聯的 ticker** + **最大風險 1 檔**。其餘 active thesis 標「本週未檢查」。**外部新聞 / 基本面查是 opt-in**(用戶說要才查,不每週必跑)。 - - 對這幾檔看 trigger 觸發,**措辭依 maturity 分(最關鍵 —— 別把 AI 猜的當你的承諾)**: - - **`testable`(你確認過的)** → 才用定論:`exit_trigger` 觸發 = 🔴「你定的『{exit}』發生了 —— thesis broken,該走」。 - - **`inferred`(AI 猜的)** → **只能用問句,絕不說「該走」**:🟡「我**猜**的失效條件『{exit}』似乎發生了 —— 這符合你當初買的邏輯嗎?符合 → 考慮出場;不符 → 順手改成你真正的 exit」。`inferred` 一律帶 `[⚠️ AI 猜測待校正]` 標。 - - `review_trigger` 觸發 → 提示重看,不催賣。 - - **順帶看 horizon 反向矛盾(只對這三類 ticker,零額外掃描)**:對 `horizon.py scan` 標 `held_too_long` 的 cycle(門檻 deterministic 住 engine,同一次 scan 的輸出;engine 已對 `horizon` 缺欄 / `null` 自動跳過)→ 一句鏡子:「當初說是{horizon}的事,現在持有 {marker 的 `holding_days`}天——是判斷升級成長線了(順手改 horizon),還是不想認賠變長抱?」措辭同樣依 maturity 分(`inferred` 用「我猜」)。**這題受消重鐵律管**:答完立刻把結論落盤(revises——改 horizon,或 why 標凹單定性)→ 矛盾要嘛消失、要嘛已定性,**同一 cycle 不重問**;**跳過也視同答過**(本 cycle 不再追,別把鏡子變成每週催告);只有行為又顯著變了(定性凹單後又加碼)才照消重鐵律的例外重開。 -3. **出場追蹤(#32/#33/#170,開場 `revisit.py scan`;`due`(到期複核)或 `backlog`(啟用前歷史)非空才有這段;都空 = 靜默跳過,不催)**: - - **問之前先撈當時的賣出理由**:比對 `theses.jsonl` 的 `event:"exit_narrative"`(同 `revisit_id`)。**有記錄且 `exit_reason` 非空 → 問句必須引用他自己的話對答案**(#136 閉環,這比泛用問句锋利十倍),按 `exit_reason` 客製:`thesis_broken`→「你賣時說是**看錯了**——{orig_ret:+pp} 之後,當時說的失效條件真的發生了嗎?」;`price_target`→「你賣時說**到價了**——它之後又走了 {orig_ret:+pp},是目標定低,還是紀律就該這樣?」;`anxiety`→「你賣時說**想落袋**(怕回吐)——回頭看,那個回吐{發生了嗎}?」;`swap`→ 直接用下面的 swap framing;`capture:"inferred"`(當時是猜的)→ 措辭改「我當時猜你是{理由}」。**無記錄或 `exit_reason` 為空**(舊出場/當時跳過)→ 泛用問句如下。 - - 每筆 due 用 AskUserQuestion 問一題:「{ticker} 你 {exit_date} 在 {exit_price} 賣掉,現在 {現價}(賣後 {orig_ret:+pp})。當時賣的理由現在看——**還成立**(賣早也是紀律)/ **部分對,要調**/ **看錯了**(真錯,進教訓)?」三選項對應 `still_valid / modified / falsified`,可跳過(下次 due 再問)。 - - **swap framing 必講(#33 鐵律)**:`compare.swap_net_pp` 非 null → 賣飛必對位換入——「賣飛 +X pp,但你換進 {swap ticker} 同期 {swap_ret:+pp} → swap 淨 {net:+pp}」;**只有換入輸給原標的才算真錯,別只算賣早多少**。`idle_cash=true` → 「賣後 cash 閒置,機會成本 = 原標的續漲 X pp」。`needs_prices` 非空 → 把缺的 ticker 現價補進 `--prices` 再算(用 engine state 的 last_px,都缺就標「本週缺價,不判」)。 - - 用戶答完立刻落盤:`python3 engine/revisit.py resolve <30|60|90> --note "<他的一句話>"`;`falsified` 的當下把那句話帶進卡的教訓段(這就是 mistakes log 的最小形)。 - - **歷史 backlog(#170,冷啟動兩層下半;`backlog_summary.count > 0` 才有)**:既有歷史使用者第一次補建帳本時,啟用前就全部過期的舊出場**不灌 due、不逐筆逼問**(否則一次噴近百筆 = 把復盤變審問);engine 收在 `backlog`(金額大者先、已收斂 top-5,真數看 `backlog_total`)+ `backlog_summary`(彙總)。**① 先一句模式鏡子**:用 `count`/`full`/`reduce`/`top_tickers`/`span` 講行為模式(「你這 {span} 間 {count} 次出場、{full} 次直接清倉」);`priced ≥ 1` 才把賣飛傾向帶上(「有現價的 {priced} 筆裡 {sold_before_rise} 筆賣完續漲、平均 {avg_hindsight_pp:+pp} → 系統性賣太早?」),`priced` 小就誠實「多數歷史標的沒現價,只回頭看得到 {priced} 筆」——**不硬湊分母**。**② 再抓大放小**:`backlog` 每次復盤選擇性帶最大 **1–2 筆**問(或用戶說「複習歷史出場」才展開),答完 `revisit.py resolve 90 --note` 落盤 → 退出 backlog。**歷史是復盤依據、但不是每週審問**:不主動催、一次消化一點。 - - 卡上的「出場追蹤」小節**只在有 due 或 `backlog_summary.count > 0` 時出現**:due 一筆一行、backlog 先彙總一句再抓大放小帶 1–2 筆,都不攤成報表。 -4. **問題帳對位(#137,開場 `problems.py stats` 的輸出;還沒開帳 = 整段跳過)**: - - `rules_check` 有 `verdict:"broke"` 的規矩 → **破戒定性問句**(AskUserQuestion 一鍵,這是規矩層唯一的主觀判斷入口):「『{規矩人話}』這次破了({事件證據})——**守不住**(記一筆,繼續追)/ **這條定得不合理**(該修的是規矩不是你)/ **這次是例外**(有正當理由)?」三個出口:守不住 = 事件照記(預設);定得不合理 = 當場請他改一句 → 收尾寫 `revises` 進 rules.jsonl(演變線,同 thesis);例外 = 把他的理由寫進該事件的 `note`(事件仍在帳上,呈現時帶語境)。**一次最多問 2 條**(broke 的照 top 排序);同一規矩連續多週 broke,只在第一次和趨勢惡化時問,其餘一行帶過——別把定性變成每週審判。 - - `held_streak ≥ 2` 的規矩 → **靜默**(注意力調度:連兩期守住就退出卡面,再犯自動回來;這不是畢業,統計一直在跑)。`verdict:"skipped"`(本期沒機會犯)→ 不提也不算守住。 - - `muted_rules` → 完全不提(用戶說過別追了;統計仍在,他哪天要看隨時有)。 -5. 對帳完才講本週新洞(headline)。**只收斂一個洞 + 一條規矩**,別把每筆 thesis 攤成報表。 - -### Step 3 · 出一張卡(收斂鐵律)——拿到 Step 2 答案後才出 - -**🚦 出卡前 self-check(沒過一律不准出卡)**: -1. **engine 用 `TR_JSON=1` 跑過了嗎?** 拿到的是 `build_card_data()` 結構化 JSON,不是預設那張人話卡。 -2. **Step 2 對話完成了嗎?** — `thesis_questions` 至少對「金額最大 + 行為矛盾」的 1 檔問過 + 拿到答案;主要動機鏡片(對應 headline_dim 的)問過 1 句。沒問完就出卡 = 退化成「engine + 套版」,失去 SKILL 的價值。 -3. **你打算自己用敘事寫卡,不是照搬 JSON 欄位?** 把 JSON 當資料源,自己組句子,不要列 `〔X〕內容` 的 dashboard 拼接。 -4. **`honesty_ledger` 每項都在卡面交代到了嗎?**(#82) 非空清單裡每個誠實缺口,卡面敘事都要有對應人話(講法見 card-spec);漏一項 → 補上再出。**ledger 本身不上卡、不列成表**,它只是你出卡前的核對源。 -5. **圖形環境試過 `show_widget` 了嗎?**(engine 標不到的執行層事實)沒實際呼叫過就別直接寫文字卡當唯一交付——先試渲染,失敗才降級文字(判斷見 card-spec 呈現方式段)。 - -**三項都過了,才讀 [card-spec.md](card-spec.md),照裡面的規格出卡**——卡的結構、禁止清單、private/public 兩種卡與 redact 規則、敘事鐵律、處方層全在那份檔裡,這裡不重複。 -**Step 2 還沒問完,不要提前打開它**:在那之前,你唯一的目標是把動機問完、拿到答案。 - -### Step 3.5 · 規矩收斂:讓用戶挑一條,存進記憶(不可省——這是下次對帳的入口) - -卡上列的 2–3 條候選規矩**不是結局**。出完卡立刻用 AskUserQuestion(選項 = 各候選 + **「這週不承諾」**,Other 可改寫)問一句:**「選一條當下週對帳的承諾?會存進本機 log,下次開場第一句就對它:說到有沒有做到。」**(#56:你不准代選,他點了哪條才存哪條。) - -- **選項標籤 = 規矩短語**,description 寫「下週看哪個數 + 現在的值」——**一律人話,內部 metric key 不准出現在任何用戶看得到的文字裡**(真人反饋:「追蹤 max_pos_pct,本週基線 42%」= 拗口)。✅「下週就看:最大單注佔比,現在 42%」/ ❌「追蹤 max_pos_pct,基線 42%」。用戶要能 5 秒選完。 -- **metric_key 對映(log 存內部名,顯示用人話)**:單一標的佔比 → `max_pos_pct`(人話「最大單注佔比」);虧損加碼 → `avgdown_count`(「虧損加碼次數」);賽道集中 → `ai_pct`(「同賽道佔比」);板塊 → `max_sector_pct`(「最大板塊佔比」);盈虧比 → `payoff`。對帳比 metric,不比 headline(規矩維 ≠ headline 維才不對錯帳)。 -- **用戶挑完 → 立刻走下面收尾 CLI 落盤**(`coach.py close`),`--rule` / `--metric` 填他選(或改寫)的那條。 -- **`insufficient_data=true` 時的分工**:機械預設 commitment 照舊**不出**(引擎已設 null,別把缺資料的猜測當承諾);但**用戶自己選的規矩照存**——行為承諾是他的意志,跟樣本夠不夠無關;樣本不足影響的只是 metric 基線的解讀。落盤時標 `source: "user_chosen"` + `baseline_note: "short-sample baseline"`,下次對帳措辭看**方向**(在降/沒動/變糟),不判達標。 -- 用戶選「這週不承諾」/ 跳過 → 收尾 `coach.py close --rule SKIP`:log 照存本週 metrics(供趨勢對帳),commitment=null,下週不拿規矩對他。 - -### Step 4 · 收一句反饋(驗證用) - -出完卡,問一句:**「這張卡,有戳中你嗎?還是哪裡不對?」** 這句反饋(純文字、不含交易明細)是這個 skill 唯一要回收的東西,用來驗證「這面鏡片產出的卡對別人有沒有用」。 - -收到反饋後,給他**一個可點的回收入口**(自願、只提一次、不推銷): - -> 願意的話,把這句感想貼給作者(30 秒): -> https://github.com/atomchung/fomo-kernel/issues/new?template=card-feedback.yml -> ⚠️ 只貼感想,**別貼任何交易明細**——表單也會再提醒一次。 - -用戶不想貼就算了,照常走收尾;反饋本身已經進了他自己的教練迴圈,回收只是 bonus。 - -**收尾埋回訪鉤子(#52 · 每週迴圈的接續點,別讓卡收完就斷)**:反饋收完、狀態落盤後,用**最後一句**把下週的錨點交回用戶手上——引用他這次**真實選的規矩**(Step 3.5 的 commitment 人話),不是模板話: -> 「下週帶新的對帳單回來(**整份全歷史直接丟就好,重疊期會自動去重**),我第一句先對『{這次承諾的規矩人話}』守了沒。」 - -- **這句同時解掉「第二週該匯什麼」的困惑**(留存生死關):明確講「全歷史直接丟」,用戶才不會卡在「要不要只匯增量」。措辭與 README「每週怎麼用」一致。 -- 選「這週不承諾」(SKIP)→ 改一句不綁規矩的:「下週把新的對帳單丟回來(全歷史直接丟),我接著看趨勢往哪走。」 -- **test-drive 模式**:這句改講「真實使用時,下週回來我就先對帳這條規矩」(對齊 Step 1 鐵律——假資料不落盤,不能假裝有記憶)。 - -## 鏡片的定位:普世機械 + 一套可換的哲學 - -- 判分的 5 維算法是**普世行為金融**(Odean 的處置效應、beta 歸因)——這層誰來都一樣,跟用哪套哲學無關。 -- 鏡片不可替代的地方在 **Step 2 找動機**:用什麼框架解讀「你為什麼攤平、為什麼賣太早」,以及 Step 3 那句**原話**。換一套哲學,問法與原話就不同——這才是鏡片的價值,不是貼個名字。 -- 預設鏡片是「**存活紀律派**」:來自一位投資人公開文章的**原則蒸餾**(`rubric/vincent-yu.md` 逐條標出處),屬引用非轉載、非經本人背書。 -- **鏡片是可換層**:換一套哲學 = 換 `rubric/*.lens.json`,engine 程式碼一律不動;同一架構可掛多套哲學。 -- 對外定位:**research / coaching support**,不構成投資建議。 - -## 狀態迴圈(記憶 + 持續):對帳 + 收尾 - -「投資不是復盤一次就結束。」第二張卡的價值在**進度**——上次那條規矩守了沒,不是再照出同一個「分散」(機械洞會收斂、會重複)。這靠開場讀、收尾寫的本機狀態 `~/.trade-coach/log.jsonl` 撐起來。 - -**對帳(log 非空時,卡開頭先做)**: -1. 讀 log **最後一行**的 `commitment = {rule, metric_key, metric_value}`。 -2. 這次引擎 state 的 `metrics[commitment.metric_key]` = 新值。 -3. 卡**第一句**就對帳:`上次說要{rule 白話},當時{metric 人話}={舊值} → 現在 {新值}:{在降/沒動/變糟}{達標沒}`(例:「上次說逢低加碼要有頂,當時最大單注 42% → 現在 31%:在降」)。用戶的數字、白話、**metric key 內部名不上卡**(人話對映見 Step 3.5)。commitment 帶 `source:"user_chosen"` → 措辭用「**你上次自己選的規矩**」(這是他的承諾,不是系統派的);帶 `baseline_note:"short-sample baseline"` → 只講方向(在降/沒動/變糟),不判達標。 -4. **變化摘要(log ≥2 筆時,對帳行之後補一小段「跟上週比」)**:取 log 最近兩筆 `metrics_snapshot`,挑**變化最大的 3 個** metric 用人話講(對映表同 Step 3.5):「AI 暴險 78%→71% 在降;最大單注 42%→45% 反而變重;攤平 +0 次」。**只講 3 個、一行帶過,不攤全表**(那就變 dashboard 了);缺值(None)的 metric 跳過。這一小段是「它記得我」的第二個證據——第二張卡的價值在進度,不在再算一次。 -5. **再**講新一輪的洞(headline_dim)——若跟上次同維,直說「這條還沒過關,先別開新戰場」;若是新維,才開新洞。永遠只收斂一個洞 + 一條規矩。 - -**規矩承諾:用戶主動選,你不准代選(#56)。** 挑規矩的互動走 **Step 3.5**(AskUserQuestion:候選各一 + 「這週不承諾」,Other 可改寫)。**用戶沒點選之前,任何規矩都不准寫進 log** —— 承諾是下週對帳的錨點,錨不是他自己下的,對帳時他只會一頭霧水、迴圈失效。選「這週不承諾」→ 收尾 CLI `--rule SKIP`(照存本週 metrics 供趨勢對帳,但 commitment 為空、下週不拿規矩對他)。 - -**收尾(出完卡 + Step 3.5 用戶挑完規矩 + Step 4 收完反饋,append 一行)**:**#166:同一次 session 重試是 no-op(不會重複 append),內容真的不同才會被拒收**——正常情況你不用處理,萬一拒收,stderr 會提示帶 `--session-nonce`(同一份 state 但邏輯上是不同 review 時才用): ```bash -# commitment 存【用戶在 Step 3.5 親選的那條】(#56)——不是引擎機械預設、更不是你代選 -# (Step 2 動機問完常推翻引擎預設:engine 給「虧損別加碼」,用戶答「計畫內定投」→ 他改挑集中度那條)。 -# 用戶選「這週不承諾」→ --rule SKIP。gate 規則在 CLI 內(#148):SKIP 一律不存 commitment; -# insufficient_data 只擋機械預設、不擋親選(親選自動補 short-sample 基線註記);--metric 填錯 key 直接拒收。 -python3 engine/coach.py close --rule "AI 暴險封頂 70%:要加 AI 新倉先問新賽道還是同一注往上疊" --metric ai_pct +python3 engine/review.py resume +python3 engine/review.py resume --session-id ``` -**收尾 part 2 · 把本週建立 / 更新的 thesis append 到 `theses.jsonl`(append-only)**: -thesis 是對話 articulate 出來的(engine 不碰)。把本週「新建倉 / 缺 thesis / trigger 觸發後更新」的 thesis 與 Step 2(d) 的賣出敘事寫成**一個陣列**存暫存 JSON(如 `/tmp/theses.json`,空週傳 `[]` 也行),交給 CLI 落盤——cycle_id 格式、必填欄驗證、`thesis_id`/`narrative_id` 生成、`session_date` 注入全在 CLI 內(#148),格式不合**整批拒收(0 筆落盤)**,照 stderr 修完重跑。**#166:同一次 session 重試是 no-op;同 session 內合法追加(例如同一次復盤中途補一筆)只會補新增的部分,不會重寫已存在的;內容真的衝突才拒收**,同樣不用手動處理: +If finalization committed the canonical bundle but a projection failed, repair it without re-questioning the user: ```bash -python3 engine/coach.py append-theses /tmp/theses.json --session-date +python3 engine/review.py repair-projections ``` -兩種行的格式(⚠️ `cycle_id` 必須【照抄】engine state `holdings.positions[ticker].cycle_id` 的 3 段格式如 `"NVDA#2024-01-12#1"`——自己拼 2 段會被 CLI 拒收;當初的坑 = 2 段讓對帳永不匹配、每週把寫過 thesis 的持倉當缺 thesis 重問): +## Agent artifact contract -```json -[ - {"ticker":"NVDA","cycle_id":"NVDA#2024-01-12#1", - "why":"一句:還沒被 price in 的判斷(不是行為描述;driver=B2 嵌這裡,同注要點名,不另立欄)", - "horizon":"年", - "triggers":{"review":"什麼消息/數字該重看","reduce":"什麼情況減碼","exit":"什麼代表看錯(非股價跌)"}, - "maturity":"inferred", - "stop":"", "target_size":"20%", - "source_type":"self", "source_name":null, "source_confidence":"candidate", - "emotion":"composed", "emotion_inferred":true, "confidence":"high", "confidence_inferred":true, - "revises":null}, - {"event":"exit_narrative","ticker":"NVDA","cycle_id":"NVDA#2024-01-12#1", - "revisit_id":"NVDA#2026-07-01#40.0", - "exit_date":"2026-07-01", "exit_reason":"thesis_broken", - "capture":"user", "note":null} -] -``` +- Validate `answers.json` against `schemas/answers.schema.json`. +- Validate `narrative.json` against `schemas/narrative.schema.json`; it may contain qualitative prose only and no digits. +- Add one `thesis_updates` entry for every missing-thesis `cycle_id`. Default to `maturity:"inferred"` and state the inference source; never present it as user-confirmed. +- A `new_evidence` decision requires `evidence_delta.claim` and `evidence_delta.source` or preview must fail. +- Do not guess ETF classes. Use a local `--instrument-map` for uncommon instruments. Unknown instruments receive no allocation exemption. -欄位語意——thesis 行:`horizon` 週|季|年(這個判斷是多長的事,#136 五要素 D1);`maturity` `inferred`(AI 猜,預設)|`testable`(用戶確認過)|`draft`(投機跟風沒 thesis);`source_type` kol|research|self|other(#38 薄版;`source_name` 只在 kol/research 填,`source_confidence` candidate|confirmed);`emotion` fomo|composed|forced|planned + `confidence` high|medium|low(#36,**選填**,inference-first,各配 `_inferred` 旗標;**現在只累積、不上卡**,同 source_type——樣本夠了才做「FOMO 進場勝率 vs composed」分組,但欄位不可回補、從今天開始收);更新既有 thesis 用 `revises` 指回舊 `thesis_id`,不蓋舊的。exit_narrative 行:`revisit_id` 照抄 enqueue-from-ledger 輸出 `new[]` 的 key(對答案用);`exit_reason` price_target|thesis_broken|swap|anxiety|null(跳過);`capture` user(親答)|inferred(僅 swap 可猜)|skipped(跳過,消重用);`note` 存用戶 Other 補的原話;**絕不帶 why/triggers**。 -> `theses.jsonl` 是 append-only 動機庫:**只追加、不改不刪**。清倉**不刪** thesis(留著當歷史);下次同 ticker 重建倉 = 新 `cycle_id` = 新 thesis。`exit_narrative` 事件(賣出理由)也住這個檔——買入的 why 和賣出的 why 同一本帳,30/60/90 對答案按 `revisit_id` 撈。Step 2.5 對帳讀每筆 active thesis 的 trigger 檢查觸發 + horizon 時間軸矛盾。**隱私同 log:純本機、不外傳、不回作者。** +## Language and sharing -**收尾 part 3 · 個人 profile(只第一次建,當復盤對照基準)**:`~/.trade-coach/profile.md` 不存在 → 第一次從交易行為**猜** 3 條個人原則寫進去(同 inference-first:不逼填,用戶可改):持有風格(長抱 / 短打)、集中度傾向、紀律缺口(出場 / 加碼)。例:`1. 長期持有型(中位 X 天) 2. 易重押單一賽道(AI X%) 3. 弱點在出場擇時(賣完常續漲)`。之後每週對帳順帶一句「這批交易符合你定的原則嗎」,用戶要改直接改檔。 +`--language zh-TW|en` controls user-visible questions, rules, and cards. Both locales use the same engine facts and policy; localization is a rendering concern, not a second analysis workflow. -**收尾 part 4 · 卡片落盤(歷史卡片庫,#129 PR-4)**:出完卡把**最終卡全文**(private review 版)寫進 `~/.trade-coach/cards/.md`,頂部 YAML frontmatter: +Each completed session produces: -```markdown ---- -date: -headline_dim: <這次的洞> -commitment: -metric_key: <對應追蹤 metric;null> -feedback: ---- -<卡全文照貼> -``` +- `card-private.md` and `card-private.html`: complete local review artifacts. +- `card-public.md`: a separately rendered shareable view without amounts, dates, tickers, exact weights, session IDs, or agent-authored free text. -`session_id` 不用你自己填——CLI 落盤時會權威性地插入/覆寫這個欄位(#166,同 session idempotency 判斷用),上面範本不用列。 +## Test drive -卡全文(含 frontmatter)寫進暫存檔後交 CLI 落盤——同日重跑的檔名遞增(`-2.md`,**不蓋舊卡**)由 CLI 管(#148),別自己算檔名。**#166:重試同一次 session 是 no-op(不產生新檔),只有真正不同的第二個 session 才遞增**,CLI 內部靠 `--state` 算的 session_id 判斷,你不用手動處理: +If the user has no data but wants to see the experience: ```bash -python3 engine/coach.py save-card /tmp/card.md --date +python3 engine/review.py prepare --test-drive --language en ``` -這個資料夾 = 你的復盤語料庫——歷史可回看,也是日後「蒸餾你自己的鏡片」的原料。同隱私鐵律:純本機、不外傳、不回作者。 - -**收尾 part 5 · 問題帳 + 規矩庫落盤(#137)**: - -```bash -# (a) 問題事件入帳:engine 規約的機械類原樣進、你只補動機類;去重靠 problems.py,重跑安全。 -# #166:sid 從 state 內容算(非 time.time()),同 session 重試 no-op、內容衝突 fail closed。 -python3 - <<'PY' -import json, os, subprocess, sys -sys.path.insert(0, "engine") -import ledger as lg -st = json.load(open(os.path.expanduser("~/.trade-coach/last_state.json"))) -sid = lg.session_id_from_state(st) -events = list(st.get("problem_events") or []) -# 動機類事件(engine 看不到動機——Step 2 拿到答案的才補,沒有就留空;絕不猜): -# exit_anxiety — Step 2(d) 答「想落袋」的每筆:{"key":"exit_anxiety","kind":"behavior", -# "week":"","ticker":"NVDA","amount":None,"note":"賣出理由=想落袋"} -# horizon_break — horizon 矛盾且答「心態動了/不想認賠」:week=本次 date_end -# fomo_entry — market_context 大漲週(如 SPY 週漲 >3%)新建倉、動機答「怕錯過」:week=建倉日 -mark = {"week": st["date_end"], "opportunities": dict(st.get("problem_opportunities") or {})} -# horizon_break 的機會 engine 判不了(它不讀動機庫)——由你補:有帶 horizon 的 active thesis = True -# mark["opportunities"]["horizon_break"] = True -import tempfile -fd, tmp = tempfile.mkstemp(suffix=".json") # unique 暫存,別用固定路徑(並行 session 會互蓋) -with os.fdopen(fd, "w", encoding="utf-8") as f: - json.dump(events, f, ensure_ascii=False) -r = subprocess.run([sys.executable, "engine/problems.py", "append", tmp, - "--mark", json.dumps(mark, ensure_ascii=False), - "--session-id", sid], capture_output=True, text=True) -os.unlink(tmp) -print((r.stdout or r.stderr).strip()) -if r.returncode != 0: - print(f"# ⚠️ problems.py append 失敗(exit {r.returncode})——本週診斷(mark)沒有記錄成功," - "見上面訊息;新的問題事件本身(如果有)已經照樣落盤,不受影響(#166)。" - "不要把上面那行訊息當成一般狀態訊息略過往下走。", file=sys.stderr) -PY - -# (b) 規矩庫沉澱(只有「新規矩 / 修訂 / 靜音」才 append;同一條繼續守 = 不寫,庫裡已有): -# 寫成陣列存暫存 JSON 後交 CLI(#148)——metric_key→problem_key 對映、rule_id 生成、status/created -# 預設都在 CLI 內。#166:同 session 重試 no-op、同 session 合法追加只補新增部分、內容真的 -# 衝突才拒收(拒收時 stderr 會提示 --session-nonce)。行格式: -# {"text":"","metric_key":"ai_pct","source":"user_chosen","revises":null} -# · source ∈ user_chosen | imported(冷啟動匯入,見下) -# · 破戒定性答「定得不合理」→ 填舊 rule_id 進 revises + 改後文字;「這條別追了」→ 補一筆 status:"muted" + revises 舊 id -# · 無 metric 對位的問題(hold_inconsistency / exit_anxiety / horizon_break / fomo_entry)→ 手填 problem_key -python3 engine/coach.py append-rules /tmp/rules.json --created -``` - -> **冷啟動匯入**:`rules.jsonl` 不存在、且用戶自己維護過規矩清單(如他的 RULES.md)→ 邀請一次:「把你現有的規矩貼給我,我翻成可對位的格式」——你逐條翻成 `{text, problem_key}`(對不上機械 key 的 `problem_key: None`,只當人話清單陳列)、**他過目確認後**才落盤(`source:"imported"`)。匯入的是**他本機的資料**,照隱私鐵律留在本機。 -> 規矩庫是未來 pre-trade gate 的守則檔:**全部 tracking 的規矩都是守則**(沒有「畢業」門檻)——還在犯的那條,恰恰是下單前最該擋你的。 - -**第一次樣本不足(`insufficient_data=true`)**:round-trip<3 或交易跨度<~84 日曆日(≈60 交易日),引擎已把 `commitment` 設成 `null`。**機械層只做體檢、不硬出規矩**(否則下次把缺資料的猜測當成已確認的承諾來對帳)。但 **Step 3.5 照走**:用戶自己挑的規矩照存(`source:"user_chosen"` + `baseline_note`,gate 在 `coach.py close` 內)——體檢卡也要留下記憶入口,否則第二週還是初診。卡收尾講一句「資料還太短,基線先存個底,累積多幾筆 round-trip 後對帳才看達標」;用戶跳過不選 → log append(commitment=null),下次來就接得上。 - -> 驗收這套有沒有真的「記憶」:`engine/test_state_loop.py` 把一份 CSV 按時間切兩段,累積跑「初診→對帳」,驗第二張卡有沒有真的對帳第一張承諾的那一維(而非重新初診)。改完 engine 或這段流程都先跑它。 +Test drive follows the same lifecycle with `persist:false`. It must not project into the user's coach memory, and every conversation and card must be visibly labeled as demo data. diff --git a/skills/fomo-kernel/behavior-diagnosis.md b/skills/fomo-kernel/behavior-diagnosis.md index cf45274..7e27372 100644 --- a/skills/fomo-kernel/behavior-diagnosis.md +++ b/skills/fomo-kernel/behavior-diagnosis.md @@ -1,112 +1,58 @@ -# 行為特徵多標籤診斷(對事不對人) +# Behavior diagnosis: evaluate actions, not identities -> 給建議的架構:**不給交易者貼類型標籤,直接對「行為特徵」多標籤診斷**——每個標籤獨立、可疊加、可驗,只對壞的下處方。 -> 一句話:照「每一筆/每個標的的行為」,不照「這個人是哪種交易者」。 +> Design decision from 2026-06-14: do not force users into a single trader type. A person's positions often combine several styles, and hard classification creates avoidable false diagnoses. ---- +## Core model -## 為什麼是這個架構(2026-06-14 決策記錄) +Diagnose behavior in three layers: -原本想做「交易者分型」(v0 草案,已棄):先把人歸到一個類型(當沖/波段/價值…),再評好壞。**經 codex/gemini 從交易者視角 review,否決了,轉成「對事不對人」**: +1. **Universal loss mechanisms**: actions that are usually harmful regardless of style, such as revenge trading, uncontrolled averaging down, unbounded sizing, or high turnover without compensating edge. +2. **Context-dependent behaviors**: actions whose meaning depends on a declared strategy, such as buying weakness, buying strength, pyramiding, concentration, or long holding periods. +3. **Instrument-level contradictions**: several tags may apply to the same position. Explain the causal chain rather than assigning one identity to the whole user. -- **gemini**:散戶是「風格縫合怪」(賺錢變當沖、套牢變長期價值),靜態中位數硬分型 → 大量錯判 → 給錯藥。**判決:過度工程化,放棄對人分型,改做行為特徵多標籤,優先攻「跨型純損耗」(它估佔散戶虧損 ~80%)。** -- **session 實證(更硬的理由)**:回顧整個 fomo-kernel 的開發,照出的每一個真洞(假分散 92%、winner_early、攤平 142 次、β 1.72、押對賽道 vs 選股)**全是「對事」算出來的,從沒用過「先分型」**。分型是一個我們從沒走過、也不需要的中間層。 -- **「同一訊號不同型意義相反」(分型的唯一賣點)用「對事」也能解**:追高在動能是策略、價值是破戒——不必先給人貼標籤,只要看「這一筆追高、在這個標的、後來怎樣」。脈絡從「人的類型」降到「標的的行為模式」,更準,且沒有「分型錯就全錯」的單點風險。 +Examples: -> 保留:搜到的風格知識(當沖/波段/動能/價值/題材,來源見文末)**不丟**——但降級成「解讀單一標的行為的脈絡詞彙」,不拿來給人貼標籤。 +- Repeated losing-position adds can create an oversized position. The useful conclusion is the action chain, not "you are a value investor." +- Several AI tickers can still be one concentrated driver exposure. +- A short-term framework that silently turns into a long-term hold after a loss is a time-horizon contradiction. ---- +## Evidence before labels -## 三層診斷 +Ask for motive only where the engine identifies a high-cost contradiction. Do not require the user to label every trade before analysis. -### 第一層 · 跨型純損耗(優先,無脈絡爭議,佔虧損大宗) +For a losing-position add, distinguish: -不管什麼風格都是壞的,直接標籤 + 砍。引擎現在就在算其中幾個: +- a pre-existing tranche plan +- genuinely new evidence +- a valuation-only change +- price-only averaging +- unresolved or skipped classification -| 標籤 | 定義 | 引擎現況 | -|---|---|---| -| `avg_down_breach` | 虧損加碼到破部位上限(凹單) | ✅ dim_avgdown breach | -| `oversize` | 單筆梭哈(>25–30%) | ✅ dim_size | -| `revenge_trade` | 連敗後 / 短時間內報復性加碼同標的 | ❌ 待加 | -| `overtrading` | 高頻進出且淨輸大盤(Barber-Odean) | ⚠️ 部分(頻率 + α/β) | +Do not accept self-description as proof. A `new_evidence` classification needs a concrete claim and source that changed a falsifiable part of the thesis. -### 第二層 · 標的層脈絡行為(需脈絡才能判好壞,多標籤) +## Style as context -對「**單一標的**」診斷,不是對整個人。同一個行為,看它在這個標的、這段持有裡是計劃內還是失控: +Style is useful when it changes how a signal should be interpreted: -| 標籤 | 好(該保留) | 壞(該下處方) | -|---|---|---| -| 加碼模式 | 一次計劃內分批建倉 | 同一 ticker 反覆逆勢加碼到深虧(`loss_spiral`) | -| 出場模式 | 紀律止盈 / 短線快進快出 | 賺錢部位賣太早且續漲(`winner_cut_early`,**僅當持有框架是中長線才算壞**) | -| 進場模式 | 追強後順勢獲利(`ride_momentum`) | 追高後套牢(`chase_top`) | -| 持有一致性 | 同檔框架一致 | 同檔又當沖又長抱(套牢就改口叫長期投資) | +- Buying near a range high can be disciplined for momentum and inconsistent for a value strategy. +- Buying near a range low can be disciplined for value and dangerous for momentum. +- Concentration can be intentional only when the user can state the thesis, downside, falsifier, and sizing logic. -→ 多標籤:一個人可以「NVDA: 紀律持有 ✓ + EOSE: 逆勢凹單 ✗」,分開標,不壓成一型。 +The engine should expose observations and confidence. The agent asks a focused question when the same signal has opposite meanings under plausible frameworks. -### 第三層 · 風格知識當「脈絡參考」(不貼人標籤) +## Output rule -用持有期 / 進場時機,判斷「**這個標的的操作**是哪種風格式」(動能式 / 價值式…),據此調整第二層的好壞判準: -- 動能式操作 → 追高不全罰(策略)、賣太早不全罰(快進快出);但「追高後套牢不停損」仍是壞。 -- 價值式操作 → 逆勢買入不罰;但「無限攤平 + 套牢叫長期投資」是壞。 -- **輸出絕不說「你是 X 型交易者」**,只說「你在 EOSE 上的操作像在凹單」。 +The final card still converges on one largest behavioral leak and one rule. Multi-label diagnosis improves the explanation; it does not justify a longer checklist. ---- +Use plain behavior language: -## 輸出形態(多標籤,取代「一張卡只有一個洞」?) +- Prefer "you kept adding as the position lost money and it became the largest holding." +- Avoid identity labels such as "you are an emotional value trap investor." -``` -你的交易行為標籤(對事不對人): - [純損耗] 虧損加碼破線 ×6(EOSE / ONDS / …) ← 最該先砍 - [純損耗] 單筆梭哈 MU 37% - [標的] NVDA: 長線紀律持有 ✓(賺 X% 抱住沒亂動) - [標的] EOSE: 逆勢凹單到 -62%、176 天不認 ✗ -``` +## Implementation direction -> 設計張力待解:多標籤 vs 收斂鐵律(一張卡一個洞)。傾向——**全標籤算給看(讓人看見全貌),但「下次只改」仍只挑 1 個**(最高損耗 + 可驗)。多標籤是診斷,單一動作是處方。 - ---- - -## 處方對應(每個壞標籤 → 可驗規則) - -| 壞標籤 | 機械處方 | 下次驗 | -|---|---|---| -| `avg_down_breach` / `loss_spiral` | 虧損部位不加碼,想加先賣掉隔天重買 | 破線次數 | -| `oversize` | 單筆上限定死 X% | 最大佔比 | -| `revenge_trade` | 連敗 N 次強制冷靜期 | 連敗後加碼次數 | -| `chase_top` | 追高進場後必設停損 | 追高未停損次數 | - ---- - -## 跟現有引擎的關係(不推翻,是確認方向 + 補強) - -引擎現在的 5 維(sizing / 攤平 / 出場 / 分散 / 持有)**本來就是「對事」**——轉 B 不是重寫,是三件補強: -1. 把「跨型純損耗」提到優先級最高(攤平 breach / 梭哈已有,補 revenge / overtrading)。 -2. 把診斷從「組合層」下沉到「**單一標的層**」(現在 winner_early/攤平是全組合算,要能分到每個 ticker)。 -3. `style-fit.md` 的風格分類從「給人分型」降級成「解讀標的脈絡的詞彙」。 - ---- - -## 下一步實作 - -1. ✅ 標的層診斷已做:按金額排序(小倉不糾結)、多標籤、`classify_adds` 主從分類(疑似定投/凹單/待確認,取代純結果判)、`thesis_q`(只對待確認標的問)。 -2. ⏳ 加 `revenge_trade`(連敗後加碼)、強化 `overtrading`。 -3. ⏳ **卡片 HTML 版型優化**(2026-06-14 owner todo):`show_widget` 完整卡視覺醜,之後優化。流程已定:確認在**出卡前對話**(Step 2)、卡是**定論不帶問號**(Step 3)。 -4. ⏳ Stage 0 真人測(最大未驗風險,從頭到尾沒跑過)。 - -## issues(之後討論,owner 判定不關鍵) - -- **賣後機會成本要「去大盤超額」**:扣同期 SPY,否則牛市裡什麼都像賣早(codex/gemini 都強調)。owner 2026-06-14 判定**不關鍵、記著之後做**。 - -## 交易意圖標記:不做「進場每筆標」(2026-06-14 owner 駁回 over-engineering) - -codex/gemini 提「進場標 #定投/#攤平救倉」當更根本解,但 **owner 對:每筆標成本太高、違反低摩擦鐵律**(skill 最早的設計鐵律就是輸入低摩擦)。正解**不是進場每筆標**,是三層降本: -1. **主從分類器自動分大部分**:`classify_adds` 已把 owner 7 檔自動判 6 檔定投,機械先扛。 -2. **只對「待確認」的少數標的問**:owner 的 case 只有 MSTR 1 檔要問,不是每筆、不是每檔。 -3. **問一次存本機、下次同標的復用**(待實作):用戶答過 MSTR=攤平救倉,存起來(留本機、符合隱私),下次復盤同標的不重問。 -→ 成本 = 「對極少數可疑標的、事後問一次」,不是「進場每筆標」。摩擦幾乎為零。 - ---- - -## 來源 -- 風格分類(時間框架/策略):day/swing/position/scalp + momentum/value — [Equiti](https://www.equiti.com/sc-en/education/trading-strategies/compare-trading-styles-day-swing-and-position-trading/)、[ATAS](https://atas.net/blog/types-of-traders/) -- 行為金融病:過度自信→過度交易(最活躍 11.4% vs 最不活躍 18.5%)、處置效應(賣贏抱輸) — [Barber & Odean](https://faculty.haas.berkeley.edu/odean/papers%20current%20versions/individual_investor_performance_final.pdf)、[Disposition effect](https://en.wikipedia.org/wiki/Disposition_effect) +- Keep stable numeric detection in `engine/trade_recap.py`. +- Keep motive and evidence validation in the v2 review lifecycle. +- Keep instrument-level tags additive rather than mutually exclusive. +- Add new universal loss detectors only when they can be measured and tied to a testable rule. +- Treat style detection as a confidence-bearing observation, not a permanent user profile. diff --git a/skills/fomo-kernel/card-spec.md b/skills/fomo-kernel/card-spec.md index fdf2434..8bfd26d 100644 --- a/skills/fomo-kernel/card-spec.md +++ b/skills/fomo-kernel/card-spec.md @@ -1,136 +1,99 @@ -# 復盤卡規格(Step 3 專用) - -> 只在「Step 2 對話確認全部完成、🚦 self-check 五項都過」之後才讀這份檔。 -> 讀到這裡代表你手上已經有:engine 的 `TR_JSON=1` 結構化 JSON + 用戶剛確認的持股假設與動機答案。 -> -> 維護者注意(#68,不是給執行 agent 的):這份檔的鐵律 = eval 判準的事實源。動 🚫 清單 / 敘事鐵律 / redact 規則,同步 `docs/eval-design.md`(自動化 spec,§5)與 `evals/EVALS.md`(手動驗收)。 - -## Contents -- 出卡前提(定論卡,不留問號) -- 🚫 卡上禁止出現的東西 -- 兩種卡:private review / public card(redact 規則) -- 呈現方式(一張卡只出一次:widget 成功 → HTML 主交付;終端 / 失敗 → 文字卡) -- 文字規格(區塊模板) -- 卡片是一個故事,不是 dashboard(敘事鐵律) -- 數字鐵律(金額>勝率 / α 基準 / 廢話零容忍) -- 處方層(揚長 / 外包短板 / 砍損耗) -- 規則(落地 / 先肯定再打 / if-then / 一個洞一條規矩) - -**等 Step 2 的確認都回來,才出這張卡。** 卡上的標籤是**定論**:用戶確認凹單的標凹單、確認逢低的標逢低,不留「凹單僥倖/待確認」這種問號(那是 Step 2 沒問完就出卡)。結合「引擎 `build_card_data` JSON + 用戶剛確認的持股假設與動機」,出**一張**卡。 - -**說話原則(通用,一條測試取代逐條文案禁令)**:講給「**看得懂自己券商對帳單的人**」聽——每句寫完自問:他讀一遍能懂嗎? -- **對帳單上有的詞,直接用**:已實現/未實現、盈虧比、部位、佔比、停損。這是他的母語,不必翻譯、**更別自創替代詞或壓縮縮語**(把「已實現」改成自造白話 = 幫倒忙;「賠側時限」這種四字內部縮語 = 用戶問「這四字是啥」——寫成「賠錢單設時限」;**「賺側 / 賠側」這種分析框架詞同罪**——說「賺錢的單 / 賠錢的單」「賺的時候 / 賠的時候」)。 -- **對帳單上沒有的詞,不准裸奔**:工程內部名(`max_pos_pct`、`metric_key`、`baseline`)一律翻人話(「最大單注佔比」,對映表見 SKILL.md Step 3.5);學術詞(α / β / 處置效應 / 夏普)出現時 ±2 句內給白話翻譯(「贏大盤靠的是敢壓,不是會選股」)。 -- **句子直說行為和數字**:「你越跌越買,把 DRAM 買成了 42% 最大倉」。讀兩遍才懂的修辭(對仗 / 轉折 / 雙關)重寫。 -- **卡面標點全形統一**(,。:;()——),數字格式除外($1,300 的千分位、0.72 的小數點、-43%、2024-03-01)——全形半形混用,真人一眼就挑出來。此條只管**卡面輸出**,repo 文檔不在內。 -- 判準只有一個:對帳單讀者**讀一遍就懂 → 過**;會讓他停下來問「這是什麼意思?」→ 改。 - -其餘每句都要**有數據 + 有案例**: - -**🚫 卡上禁止出現的東西(engine 已把這些移出渲染,別自己加回來)**: -- ❌ `〔X〕內容` 標籤拼接(SKILL 鐵律:連貫敘事,不准 dashboard) -- ❌ 5 維 severity 小數表(`.64 🔴`)— 用「一句人話」帶過非 headline 維度 -- ❌ `thesis_questions` 任何一條 — 那是 Step 2 對話用的,卡上只有用戶答完的定論 -- ❌ 鏡片 `lens_quote` 當每漏洞段尾結語 — 融進敘事或徹底不用 -- ❌ 把你寫的規矩當定論硬塞 — 從 `candidate_rules` 給 2-3 條候選讓用戶挑/改(引擎只給一條時就用那條) -- ❌ `(引擎產出)` 或任何內部分工標記 -- ❌ 工程內部名 / 未翻譯學術詞(`max_pos_pct`、`baseline`、裸 α/β…)— 見上方**說話原則**(真人反饋:「追蹤 max_pos_pct,本週基線 42%」= 拗口 + 看不懂) - -**先分兩種卡(社群分發的命)**: -- **private review(你自己看)** —— 完整:金額、股數、ticker、持倉佔比、損益。寫進回覆 + 落 `~/.trade-coach/`。 -- **public card(可分享,redact)** —— **預設不自動出,用戶說要才給**。隱藏絕對金額 / 股數 / 完整持倉清單,只留**可傳播又不洩資產**的:行為 pattern、最大的洞、下次規矩、績效用**相對值**(β、贏大盤 pp、盈虧比,不放 $)。給一個能直接貼 X / Thread 的純文字版。 - - redact 規則(防 portfolio reconstruction —— 精確佔比 + ticker + 連續多週可聯立反推總資產): - - 絕對金額、股數 → **砍**。 - - 佔比 → **不給精確 %,改 bucket**:`>30%` / `20–30%` / `<10%`,或只給排序「最大持倉」;損益轉「賺 / 虧約 X 成」。 - - 日期 → 模糊成「近幾週」,不給精確交易日(連續精確日期 + 佔比 = 可反解股數)。 - - ticker → 預設保留(行為才有意義);要更隱私 → 全匿名(`某 AI 核心倉`)。 - - 正名:沒現金 + 即時價時,佔比只能叫「**CSV 內成本占比**」,不是「資產權重」。 - -**呈現方式:一張卡只出一次——widget 渲染成功就以 HTML 卡為主交付,回覆文字絕不重講一遍。** -- **圖形介面(`show_widget` 可用且回傳成功)**:HTML 卡 = 主交付,版型照 [`card-template.html`](card-template.html)。設計規範(2026-07-04 triad UI review 定版):flat、**大區塊一律中性底(surface + 邊框),語義色只准在 icon / 區塊小標 / 關鍵字 / 損益數字**(綠紅藍黃色底同卡 = 多色告警面板,被真人打過);tag 用 outline 不用色底膠囊;明暗雙模式、Tabler outline icon、**無 emoji**、字重 400/500。**出完 widget,回覆文字只留三件事:一句收尾(資料狀態 + 存檔位置)、Step 3.5 規矩收斂問句、Step 4 反饋問句**——把卡的內容用文字再敘述一遍 = 逼用戶讀兩遍(真人反饋)。 -- **純終端機或 show_widget 失敗**:markdown 文字卡 = 主交付,直接寫在回覆裡。(實測缺陷:`show_widget` 只在圖形介面 claude.ai / 桌面 app / IDE webview 渲染;終端機用戶會**整張看不到** → 只出 show_widget = 用戶以為 skill 壞了。) -- **卡片結構**(文字 / HTML 同):總覽(含市場背景一行,有數據才出)→ 做對的 → 標的層 → 最大的洞(數字 / 實例 / 動機 / 萬一)→ 報酬歸因 → 問題帳(有帳才出)→ 下次只改 + 引言。 -- **不放機械層 5 維小數表**(`.64 🔴` 用戶看不懂、就是另一份報表)。要提其他維度,**一句人話**帶過:「加碼 / sizing / 持有你都守得不錯,只有 X 要處理」。 -- **累積損益曲線(`pnl_curve`,#167)只在 HTML widget 畫,文字卡不畫**:`points` 非空才在「這次成績」metric 區(帳面總損益那格)底下加一條 sparkline(SVG polyline 即可),讓「怎麼走到這個數字」一眼可見(穩步墊高 vs 一次跳空 vs 坐雲霄飛車回原點,是三種不同的行為診斷)。**單色細線**(依終點正負套 `--text-success`/`--text-danger`,或乾脆 `--text-muted`),**不填色塊、不逐點染色**——這是延伸現有損益數字的顏色用法,不是開一片新的紅綠色塊(觸犯「多色告警面板」鐵律,見上方 widget 設計規範)。`pnl_curve.note` 非空(無價格/樣本不足/混市場尚未支援)→ 靜默不畫這條線,**別把 `note` 轉成一句話補在卡上**(那是給你判斷「畫不畫」的旗標,不是誠實缺口,不歸 `honesty_ledger` 管)。 - -下面的文字規格定義**卡上要有哪些區塊、每句怎麼寫**——內容鐵律照搬: - -``` -復盤卡 · 用 {philosophy} 的尺照你的交易 - -〔這次成績〕{已實現損益 $ · 盈虧比(平均賺 vs 平均賠) · β · α} ← 看金額不看筆數勝率 -〔市場那週〕{SPY 週{±%} · QQQ 週{±%} · VIX 收 {X}(前週 {Y})} ← #37 語境一行:market_context 有數據才出,離線整行不出、部分缺(missing)只寫抓到的; - 它是講故事的背景(「你砍在 SPY −4% 的恐慌週」),不是診斷維度 -〔這把尺是什麼〕{lens.master_intro.one_line} ← 一句話帶過,不展開 - -✅ 你做對的:{引擎 strength,已含具體案例,原樣保留} -📊 最賺 / 最虧 · 已賣出 round-trip(買→賣){best ticker +% · +$X} / {worst ticker −% · −$X} ← %和$都要;X=|pnl| 絕對值,賺標 +、虧標 −(best.pnl 已正 / worst.pnl 已負,別重複套負號) - -〔盈虧比拆解 · 誰在撐、誰在拖〕(引擎 payoff_attribution,每次都出) - 撐盤:{top carriers 標的 + 佔總賺%} · 拖累:{top draggers 標的 + 佔總賠%} - → 拿掉最大拖累 {ticker}(淨 ${drag})→ 盈虧比 {payoff} 變 {cf_payoff} - ← 別只報「盈虧比 0.8」這個總數;指名是哪一兩檔(常是凹單)把它拖翻,該動哪一刀就清楚了 - -🔴 最大的洞:{一句白話結論,人話} - ▫ 看數字:{用戶自己的數字} - ▫ 看實例:{指名一筆具體交易當例子} ← 最重要那句一定要有案例 - ▫ 看動機:{用戶剛在 Step 2 確認的 why} - ▫ what if:{引擎算的具體情境,給數字讓他自己想——不准「會一起倒」這種空話} - -〔問題帳〕{N 條問題追蹤中,本週挑最嚴重的 1–3 條} ← #137:problems.py stats 的 top;還沒開帳整段不出 - 🔴 {惡化中的:人話 + 本週證據 + 「12 週第 4 次,前月 3 次 → 近月 1 次」的趨勢;有綁規矩帶守/破} - 🟡 {超線但在收斂的:一行,不轟} - 🟢 {在變好的:一行正向回饋——「持續變得越來越好」要被看見才有效} - 其餘 {M} 類本週無事,靜默統計中 - ← headline 洞若與 top1 同源,這裡一行帶過別重複展開;規矩守/破的深挖在 Step 2.5 已做,卡上只留定論 - -▸ 下次只改這一件:{candidate_rules 的具體 if-then,2–3 條候選讓他挑/改一條} -▸ {philosophy}:「{lens 的 quote 原話}」 -``` - -> 卡上列的是**候選**;出完卡立刻走 SKILL.md **Step 3.5** 用 AskUserQuestion 讓用戶挑一條(或改寫),選中那條才落盤成 `commitment`——這是下次對帳的記憶入口,漏掉 = 下週對不了帳。 - -**卡片是一個故事,不是 dashboard**(真人交易者 review 後的鐵律): -- **連貫敘事,不准標籤拼接**。`〔這次成績〕A|B|C` 這種一塊塊的格式,交易者讀起來「像幾份報告硬湊」。用完整句子把數字織成一段他自己的故事。 -- **卡上不放給作者看的註解**。`〔這次成績 · 看金額不看勝率〕`、`(供參)`、`機械層 5 維`、上面模板裡的 `←` 註解箭頭——這種內部理由 / 設計標記一律不上卡,卡上只有用戶的數字和話,理由你心裡有就好。**「為什麼不出某數字」的決策注記同罪**(真人反饋:「(demo 資料,歸因失真,不出。)」→「這是啥」)——要嘛靜默不出,要嘛用用戶語言講一句(「示範資料跟大盤比會失真,故不列」),別把自己的取捨思路印上卡。 -- **先承認他的本事,再打**。直接打會被頂回來(「抱也是我的決策」「不交易哪來部位」)。先講他做對的(選股、抱住賺 6 倍),他才沒法用「你否定我交易價值」嘴硬——尤其當 realized P&L 是負的,那是他嘴硬不了的鐵證,對準那裡。 -- **數字要「髒」**。最戳人的是「你每筆平均賺 $81、賠 $105」「虧損加碼 138 次」這種甩臉上的具體數字,不是形容詞。 -- **不講散戶聽不懂的話**。「α 只有 5%」交易者會回「我又不是基金經理」。翻成他在乎的:「你贏大盤是因為敢壓+槓桿,不是會做價差」。詞彙取捨照**說話原則**:對帳單詞彙(已實現/未實現…)直接用,別自創替代。 -- **洞標題直說行為**(說話原則的標題應用;真人反饋:「DRAM 的 42% 不是你決定的,是價格跌出來的」提醒對但句子繞):「你越跌越買,把 DRAM 買成了 42% 最大倉」。 -- **鏡片引言別當結語**。結尾突然冒「鏡片原則:…」像老師訓話,交易者「差點關掉」。要嘛融進敘事,要嘛不用。 -- **規矩是機械的,不是自我喊話**。「動手前問自己…」沒用(沒人下單時覺得自己會賠)。要給「不靠當下忍住」的機制:「虧損部位一律不加碼,想加先整筆賣掉隔天重買」。 - -**誠實點照 `honesty_ledger` 講(#82)**:engine 已把這張卡必講的誠實缺口聚合成 `honesty_ledger`(空=無缺口)——**它決定「講不講」,下面各段決定「怎麼講」**:每個列出的 `key` 融入對應敘事(`alpha_credibility`/`sector_attribution` → α 段、`unrealized_coverage`/`currency_mix`/`cash_reliability` → 金額/現金段、`acct_perf_basis` → 帳戶級績效段、`unclassified_drivers`/`orphan_sells` → 總覽或標的層一句),**不列成獨立區塊、不上卡**。出卡前逐項核對(SKILL Step 3 gate),漏項不出卡。 - -**金額 > 筆數勝率**:總覽絕不放「勝 X/負 Y、勝率 %」當主數字——**「6 勝 2 負」這種勝負筆數也算勝率敘事,不進 metric 格、不當句子主詞**(真人反饋:「勝率不重要,關鍵就是賠錢」)。放**已實現 + 未實現損益(兩個都要,只報一個失真)、盈虧比(平均賺 vs 平均賠)**;敘事聚焦**賠側**:平均一筆賠多少、最大拖累是哪檔、佔總賠幾成——錢是賠錢單決定的。**未實現若非全覆蓋要明講**(`honesty_ledger` 列 `unrealized_coverage` 時):補一句「未實現僅反映 priced_n/held_n 檔持倉,缺現價:…」,別讓沒抓到價的持倉靜默漏算成看起來完整的數字。**金額的幣別先讀 `currency_meta` 再落筆**(#51):`aggregate_currency` = 總覽/佔比類金額的幣別;混幣組合(`mixed=true`)單檔原幣數字用 `pnl_by_currency` 對照;display currency 換算與離線匯率規則照 SKILL.md「💱 Display currency」段,這裡不重複。 -**現金與入金判讀(#171,讀 `card.cash`)**:交易工具通常看不到帳戶閒置現金,這個 skill 看得到——但只在 `reliable=true`(用戶給了現金餘額錨點)才把它講進去,別拿盲算數字唬人: -- `reliable=true`:總覽帶一句帳戶現金——「持倉之外還壓著 $X 現金,佔帳戶 Y%」,用**對帳單語言**(現金 / 佔帳戶,不是 `cash_weight`)。這一句的價值是把「部位佔比」升級成「資產佔比」——集中度那個洞若之前只能叫「CSV 內成本占比」,有了現金錨點就能講真的帳戶權重。 -- `recent_net_deposit` 非 0 且 `reliable=true`:講**這筆錢的去向與集中度效應**,這是用戶最痛的一問——「這個月淨入金 $Z:你把它加進最重的那檔(加深集中),還是分散進新部位/留現金(解集中)?」用他實際的加碼行為對照,別空問。為 0 或看不到流水 → 這句靜默跳過,別硬掰。 -- `reliable=false`(`honesty_ledger` 列 `cash_reliability`):現金是靠交易流水盲算(假設開戶 $0),**不准把那個佔比當真數字講**;誠實帶一句邀請即可——「你的現金餘額我只能從流水盲估、可能差很多;下次把對帳單上的現金餘額給我,就能算準帳戶層比重和入金判讀」。這句同時兌現揭露 + 把功能的下一步交回用戶手上。 -- `status=partial`(台美多帳戶只給了一部分錨點,`unanchored_currencies` 標缺的幣別):講法收窄到缺的那個帳戶,別把已可信的那半也講成不準——「你的美股帳戶現金我算得準,台股帳戶餘額還沒給、只能盲估;補上那個就完整了」。 -- `status=residual`(#180,`data.residuals`:有錨點、但錨點間現金史對不上):不是盲算、是**帳本有洞**——照 `residuals` 講「你 {start}~{end} 有 ${residual} 現金變動我對不上」,**中性**:可能漏記入金/提款/股息,別斷言是哪一種。這是「準確性隨每週對帳遞增」的機制(每補一張餘額截圖多驗一段)。帳戶報酬照出時(小缺口)只帶這一句揭露;大缺口(帳戶報酬 gate 掉)見帳戶級績效段的解鎖邀請。 -**帳戶級績效(#171,讀 `card.acct_perf`;只准照抄引擎數字,不准自己算)**:`acct_twr` 非 null 才有這段。三個數字答三個不同的問題,講成一條敘事鏈,不是三行 dashboard: -- **鏈式歸因**:「這期你的持倉賺 X%(`hold_twr`),帳戶整體 +Y%(`acct_twr`)——差的 Z pp 是現金效應(`cash_drag`),平均 W%(`avg_cash_weight`)的錢躺著」。`cash_drag` 的正負要翻譯,別裸奔術語:**負 = 閒錢稀釋了報酬**(「拖了 |Z| pp ≈ $N」,$N 用 `drag_dollar_approx`,記得標它是「閒錢若跟著持倉跑」的反事實估算);**正 = 現金這期幫你擋了跌**——跌市時別把持有現金講成錯。帳戶柱涵蓋空倉期(空倉=100% 現金照走),持倉柱只涵蓋有倉的日子——兩者窗不同是特性不是 bug,躲跌/踏空自動反映在帳戶柱。 -- **帳戶年化 IRR(`irr_annual`)**:答「你的錢實際滾多快」(金額加權,含出入金時機)。與 per-market「贏/輸大盤」並列時分工講清楚:大盤對比答「該不該乾脆買指數」(持倉子組合、照 α 段既有講法),帳戶 IRR 答「錢的實際年化」;`note` 說窗太短就不出、別硬年化。 -- **gate 語意**:`acct_twr=null`(`note` 有寫)→ 這段整段不講,可只講 `hold_twr` 持倉柱。`note` 分兩種:①現金無錨點/回滾破裂 → 邀請補 `TR_CASH` 的話術併入 `cash_reliability` 那句講一次,別重複;②**大缺口解鎖邀請**(#180,現金史某段對不上、帳戶報酬需補齊該段)→ 照 `note` 講成邀請:「想看帳戶整體報酬?你 {某段} 有 $N 進帳我對不上,補這筆的日期(更新現金部位)就解鎖——先看你的持倉報酬 +Y%」。不是叫他放棄,是把下一步交回他手上(帳戶報酬 = opt-in 進階層,核心卡照出)。 -- `honesty_ledger` 列 `acct_perf_basis` 時(數字有出但地基有洞):照 `data` 收窄講——`unanchored` 標哪個幣別的現金是盲算(「台股帳戶餘額沒給,帳戶級數字把它當盲估算進去」)、`at_cost_tickers` 標哪些檔抓不到價、以成本平線計(「X 抓不到價,帳戶級把它當零報酬擺著,它的漲跌沒算進來」)、`fx_approx`=匯率用即期近似(全期匯損益沒真算)。 -**該不該乾脆買指數(#164 柱2,讀 `alpha_beta_breakdown` 的 `port_tot`/`spy_tot`/`excess_vs_spy`,engine 算好只准照抄、不准心算)**:α 拆解之前,先給最直白的一行——「你的持倉這段 {port_tot},無腦全買大盤 {spy_tot} → {贏/輸} {excess_vs_spy} pp」,答用戶心裡那句「我到底該不該自己選、還是乾脆買指數」(和帳戶 IRR「錢滾多快」、下段 α「贏的是技巧還是運氣」分工,別三者混講)。**基準跟市場走**:US 寫 SPY、TW 寫「加權指數」(別硬寫 SPY);混市場照 per-market 兩行並列(讀 `by_market`,各對各的大盤,絕不加總或平均)。這行的份量全押在「數字等於引擎」——**自己湊一個就毀了可信度**;`alpha_beta_breakdown` 帶 `note`(樣本不足/無價)時這行乾脆不出,別硬湊。贏別吹過頭(有多少只是 β / 押對賽道,下段會拆)、輸就直說(這正是處方「把選股外包給指數」的入口)。 -**α 永遠出數,語氣看統計**:alpha 一律 vs 通用大盤(SPY),**卡上 α 必帶 95% 區間**;`alpha_credible=true`(≥1 年且 |t|≥1.96)才用能力語氣,不顯著就說「區間 −Y%~+Z%,分不出本事還是運氣」+ 講清楚卡在哪(`gate.reason`:樣本不足 vs 區間太寬/持倉集中)——**別再用「不出數」表達誠實,數字+不確定性才是誠實**。**贏大盤必配拆帳**:engine 已把「贏大盤」機械拆成「押對賽道(allocation)」+「板塊內選股(selection)」(`excess_split`,兩項相加=贏大盤,會計恆等不需顯著性)——引用拆帳數字,別自己心算;`coverage<1` 補一句哪幾檔無板塊對照。**`honesty_ledger` 列 `sector_attribution` 時 → 這句必補,即使 α 面板因樣本不足/不顯著整塊沒出**(#92:有 driver 標籤但查無板塊 ETF 的檔,超額被靜默全歸「選股」、押對賽道的功勞被誤記——這揭露不可只活在 α 面板,它是永遠顯示的 data_integrity 一員,同「未分類 driver」)。**絕不拿板塊 ETF 當 α 基準**,板塊只當拆帳對照。這個「賽道 vs 選股」的分離才是用戶要的準確認知。 -**廢話零容忍**:像「偏存活紀律、有些是提問不是判你錯」這種學究句一律刪。每行不是數字就是實例,沒有形容詞填充。 - -**處方層(從「你哪裡爛」進到「下一步換什麼做法」)**:診斷讓人知道問題,處方讓人回來——留存的鉤子在「下一步怎麼做」。engine 的 `prescribe()` 已從歸因 + 診斷算出三類,照著說人話: -- **揚長**:放大用戶證明有 edge 的決策(歸因正貢獻那層)。多數工具只會避短;這個 skill 因為算得出歸因,能告訴用戶「你強在哪、去放大它」。 -- **外包短板**:某決策層是負貢獻(如選股 -99pp)→ 建議把那個決策外包(交給指數),不是叫他「學會」。**流程建議,非標的建議**:是「少做某個決策」,絕不碰「買哪支」(IP/法律邊界)。 -- **砍損耗**:純扣分行為(虧損加碼、梭哈)→ 機械規則砍掉,可驗。 -- 處方的力量來自**歸因精確**:ChatGPT 沒有那個 -99pp,不敢叫人「別選股」;這個 skill 敢。越具體反直覺,越證明不是套話。**因人而異**:同把尺,「方向強/選股弱」→外包選股;「選股強/紀律弱」→守紀律別稀釋選股。 - -**規則:** -- **每句都要能落地到一筆真實交易**。「出場不手軟」這種黑話不准單獨出現,一定要接「(例:MRVL 賺 47% 賣完只動 -3%)」。最重要的那句洞,必須指名一筆具體交易,否則用戶看不懂、也不信。 -- **先給「你做對的」再給洞(不可省)**。看自己虧損 = ego 受傷會直接關掉;先肯定一個**真實**優點(引擎已附案例),降防衛,才聽得進那一刀。reframe:結帳學費,不是審判。 -- **if-then 規矩由你(Claude)幫他寫具體,不要丟抽象句**(「AI 幫人寫規矩」): - - 抽象(❌):「注意分散」「加碼前想清楚」——用戶下次還是不知道怎麼做。 - - 具體(✅):用他的數字寫成「下次引擎能驗」的:「把 AI 部位從 95% 砍到 70% 以下」/「為 MU(37%)掛一個跌破 $X 就減半的條件單」/「往下加碼前在卡上寫一行新證據,寫不出就不加」。 - - **給 2–3 條候選讓他挑一條 / 改一條**,別逼他接受你寫的。用戶說不出具體規矩,但能從選項裡認出「對,就是這個」——這就是 AI 幫人寫規矩。 - - **挑的動作必須由用戶完成(#56)**:用 `AskUserQuestion` 給選項(候選各一 + 可 Other 改寫 + 「這週不承諾」),**他點了哪條,收尾才存哪條**;你代選 = 下週對帳的錨點不是他下的,對帳直接失效。細節見 SKILL.md 收尾段。 -- **永遠只收斂到一個洞 + 一條規矩**。第二份十維報告 = 失敗。 -- 引言用 `rubric/vincent-yu.lens.json` 裡**那個洞對應 dim 的 `quote`**;**換鏡片/哲學 = 換 lens 檔,這步不動**。 +# Review card content specification + +> Execution authority in v2 is `engine/card_renderer.py` plus `references/card-policy.md`. This file records the design rationale and acceptance boundaries. Agents do not assemble or redact cards manually. + +## Purpose + +Produce one conclusion card after all required motive questions are answered. The card should connect the user's own numbers to one behavioral leak, one qualitative thesis interpretation, and one user-chosen next-time rule. + +The target reader understands a brokerage statement. Use standard account language directly: realized and unrealized P&L, payoff ratio, position, weight, and stop. Translate internal field names and explain academic terms in plain language. + +## Required properties + +- Lead with account impact, not trade count or win rate. +- Name one real strength before the largest leak. +- Ground the largest leak in an engine-owned number and a concrete transaction when available. +- Include qualitative motive or thesis interpretation only after the user answers required questions. +- Surface every triggered honesty-ledger limitation in natural prose. +- End with at most one user-chosen if-then rule. Skipping is valid. +- Keep the writing coherent. A card is a story, not several dashboards pasted together. + +## Prohibited content + +- Raw five-dimension severity tables. +- Raw `thesis_questions` or unanswered questions. +- Internal labels such as `max_pos_pct`, `metric_key`, `baseline`, or implementation notes. +- Agent-computed numbers or rewritten engine facts. +- Several recommendations or action items. +- Buy or sell advice for a security. +- Shaming or personality judgments. +- A rule selected by the agent on the user's behalf. + +## Private and public cards + +The private card may include account amounts, dates, tickers, position weights, transaction examples, thesis evidence, and the qualitative agent narrative. + +The public card is a separately rendered structured view. It excludes: + +- absolute amounts and share counts +- exact dates +- tickers and full holdings +- exact position weights +- session IDs +- evidence text and agent-authored free prose + +Do not create the public card by applying regular-expression redaction to the private card. Independent rendering prevents portfolio reconstruction and accidental disclosure. + +## Numeric truth + +The renderer is the only bridge from engine facts to displayed numbers. Agent narrative contains no digits so it cannot become a competing truth source. + +Important display priorities: + +1. Realized and unrealized P&L, with coverage limitations when triggered. +2. Payoff ratio and average gain/loss, rather than win-count framing. +3. Portfolio versus benchmark and the alpha interval when available. +4. Reliable cash position and account-level performance when the engine allows it. +5. Largest realized drag and its engine-computed counterfactual. +6. ETF portfolio structure and explicit metadata gaps. + +When a field is unavailable, omit it or use renderer-owned honesty copy. Never infer a value and never treat missing data as zero. + +## Honesty ledger + +`build_honesty_ledger()` determines which caveats must appear. The renderer integrates them into the relevant narrative section rather than printing the ledger as a checklist. + +Examples include: + +- alpha interval is not statistically credible +- unrealized P&L covers only part of the open portfolio +- some drivers lack a sector benchmark +- some instruments are unclassified +- orphan sells imply incomplete transaction history +- currency conversion is approximate +- cash balances or account performance are not fully anchored +- ETF expense ratio or tracking error is missing + +The card must state the limitation neutrally and narrowly. It must not guess the cause of an unexplained residual. + +## Performance framing + +- Compare the held portfolio with the appropriate market benchmark only when engine output supports the comparison. +- In multi-market portfolios, show each market against its own benchmark; never synthesize a total alpha. +- Treat account TWR, holding TWR, cash drag, and IRR as different questions. Use only engine-provided values and copy. +- Interpret positive cash drag as protection in a falling market and negative cash drag as diluted participation; do not treat cash as inherently wrong. +- Use alpha capability language only when the engine marks it credible. Otherwise show the interval and uncertainty. + +## Prescription boundary + +The product coaches process rather than selecting securities. A prescription may: + +- amplify a demonstrated strength +- outsource a decision layer that consistently destroys value +- remove a measurable behavioral leak with a mechanical rule + +It may not recommend what to buy or sell. Candidate rules must bind to an engine metric so the next review can evaluate them. The user chooses, rewrites, or skips the final rule. + +## Rendering + +`card_renderer.py` produces canonical Markdown and dependency-free HTML from the same structured content. Deliver those artifacts rather than rewriting the card in the chat. HTML may add a small P&L sparkline when `pnl_curve.points` is available; missing or unsupported curve data should not create a new user-facing caveat unless the honesty ledger requires one. diff --git a/skills/fomo-kernel/card-template.html b/skills/fomo-kernel/card-template.html index b58780e..07ae7c4 100644 --- a/skills/fomo-kernel/card-template.html +++ b/skills/fomo-kernel/card-template.html @@ -1,48 +1,43 @@ - + -fomo-kernel · 復盤卡版型 +fomo-kernel · review card template - +
-

交易復盤卡:帳面損益 +143,197,做對的是出場節奏一致;最大的洞是部位押太重(PLTR 49%、NVDA 48%)且 100% 集中在 AI 同一個 driver;下次只改一件——虧損部位一律不加碼。

+

Trade review card: total P&L +143,197. Strength: consistent exit rhythm. Largest leak: PLTR 49% and NVDA 48%, with 100% exposure to one AI driver. Next rule: never add to a losing position.

-

復盤卡 · 用存活紀律派的尺照你的交易

-

你大賺小賠、會跑——但身家都壓在同一個賭注上

-

這把尺的核心:活下來,比賺最多更重要。它不看你「看對幾次」,看你「會不會被一次打死」。

+

Review card · survival-discipline lens

+

You let winners run and cut losses, but the portfolio is one concentrated bet

+

This lens prioritizes survival over maximum return. It asks whether one loss can remove your ability to continue.

2024-01-12 ~ 12-03 - 19 筆交易8 個 round-trip現持倉 4 檔 + 19 trades8 round trips4 open positions
-

帳面總損益

+$143,197

已實現 +$18,960 · 未實現 +$124,237

+

Total P&L

+$143,197

Realized +$18,960 · unrealized +$124,237

-

主動買賣盈虧比

2.9

平均賺 $2,851 vs 賠 $1,000

-

贏大盤

+261pp

β 2.05 · 漲跌是大盤 2 倍

-

真本事 α(年化)

+33% *

* demo 資料,α 失真僅示意

+

Active payoff ratio

2.9

Average win $2,851 vs loss $1,000

+

Benchmark excess

+261pp

β 2.05 · twice the market movement

+

Annualized alpha

+33% *

* demo only; alpha is unreliable

-

帳面賺 14.3 萬很漂亮,但 87% 是還沒落袋的浮盈,真正賣掉的只有 1.9 萬。盈虧比 2.9 說明你大賺小賠、跑得掉——可是贏大盤的 261pp 裡,β 高到 2.05,多半是膽子大、敢壓,不是會做價差。

+

The $143k total looks strong, but 87% is unrealized and only $19k has been realized. A 2.9 payoff ratio shows large wins and smaller losses, while beta of 2.05 explains much of the 261pp excess as risk exposure rather than pure selection skill.

-

你做對的:進出有一致的節奏——中位持有 162 天,不是隨機亂買亂賣。該抱的抱得住(NVDA 賺 162% 沒亂加、ORCL 賺 60% 沒亂加),這是你聽得進下面那刀的本錢。

+

What you did well:the entry and exit rhythm is consistent. Median holding time is 162 days, and you held NVDA to +162% and ORCL to +60% without adding impulsively.

-

標的層 · 按金額排序,只盯影響大的

+

Instrument impact · ranked by money

PLTR+$73,207 -
押太重 49%賺 606%(加 2 次還算節制)
+
oversized 49%+606% with two controlled adds
NVDA+$60,556 -
押太重 48%紀律持有 162%、沒亂加
+
oversized 48%held +162% without impulsive adds
TSLA+$3,750 -
大致中性
+
roughly neutral
ORCL+$2,974 -
紀律持有 60%、沒亂加
+
held +60% without impulsive adds
ARM+$2,450 -
大致中性
+
roughly neutral
MU+$1,260 -
大致中性
+
roughly neutral
AMD−$1,000 -
大致中性
+
roughly neutral
-

單筆最賺 NVDA +104%(50→102,抱 207 天)· 最虧 AMD −12%(160→140,抱 175 天)

+

Best round trip: NVDA +104% ($50→$102, 207 days) · worst: AMD −12% ($160→$140, 175 days)

-

最大的洞

-

押太重,而且 100% 押在同一個賭注

-

兩檔(PLTR 49% + NVDA 48%)就佔了組合 97%,而且 4 檔 100% 都是 AI——這不是 4 個賭注,是同一個賭注下了 4 次。賺的時候很爽,但「分散」是假的。

-
看數字PLTR 49% · NVDA 48% · top3 98% · AI 暴險 100%
-
看實例PLTR 從 $24 一路往下加到 $15,虧損中加碼 2 次都加到部位 >25%
-
看動機你出卡前確認過:「不是算過最壞情況能扛,是看對了一路加、捨不得減」——這就是下面那條規矩要擋的事。
-
萬一AI 暴險市值 $170,793。回檔 30%(一般修正)→ 帳面 −$51,238;回檔 50%(2022 級熊市)→ −$85,397。撐得住嗎?
+

Largest leak

+

Oversized, with 100% exposed to one underlying bet

+

PLTR 49% plus NVDA 48% is 97% of the portfolio, and all four holdings share an AI driver. This is one bet expressed four times, not four independent bets.

+
NumbersPLTR 49% · NVDA 48% · top 3 98% · AI exposure 100%
+
ExamplePLTR was added from $24 down to $15; both losing adds pushed the position above 25%.
+
MotiveYou confirmed before rendering: “I did not calculate survivable downside; I kept adding because the thesis looked right and I did not want to reduce.”
+
StressAI exposure is $170,793. A 30% correction implies −$51,238; a 50% bear market implies −$85,397. Can the process survive it?
-

報酬歸因 · 押對賽道,還是會選股?

-

+261pp贏大盤(你 +321% vs SPY +60%)

-
選股力 vs 科技股 QQQ (中性對照)+243pp
+

Return attribution · theme exposure or selection skill?

+

+261ppabove market (+321% vs SPY +60%)

+
Selection vs QQQ (neutral comparison)+243pp
-
選股力 vs 半導體 SOXX (事後最強)+96pp
+
Selection vs SOXX (strongest hindsight comparison)+96pp
-

換對照結論就翻——選股贏中性的 QQQ、輸給事後最強的 SOXX。誠實說:資料還判不出你「會不會選股」。而 β 2.05 代表贏的這一塊很大部分來自「敢壓 + 押對 AI 賽道」,不全是價差本事。

+

The conclusion changes with the comparator: selection beats neutral QQQ but trails the strongest hindsight benchmark, SOXX. The data cannot yet establish selection skill, and beta 2.05 shows that much of the gain came from concentration in the right theme.

-

怎麼優化 · 放大強的、砍掉純損耗

+

Improve · preserve strengths and remove pure process loss

-

揚長(假設,待驗證)

贏大盤主要靠押對賽道——但這還只是「假設你有方向判斷力」,押對 AI 也可能只是站到風口。壓測它:寫下你「下一個看好的賽道」、記時間,對了兩三次才叫 edge。

+

Test a possible strength

Theme selection may explain the excess return, but one correct AI exposure can be luck. Record the next theme thesis and timestamp it; repeated correct calls are needed before treating it as an edge.

-

揚長

你選股連最嚴苛的 SOXX 都贏,這是真 edge——別讓押太重 / 紀律問題稀釋掉它。

+

Preserve a strength

The selection result beat even SOXX in this sample. Preserve the research process while preventing concentration and discipline leaks from dominating it.

-

砍損耗

虧損中加碼是純扣分動作,該最先砍。

+

Remove process loss

Adding to a loss without new evidence is the first behavior to remove.

-

下次只改這一件(可立即執行、引擎能驗)

-

虧損部位一律不加碼。真想加,先整筆賣掉、隔天重買——逼你重新面對「現在還會買它嗎」。

-

可以低成本試探一次,不代表已完成長期信任的驗證;小成功不該自動升級成重倉。— 存活紀律派

+

Change only this next time · immediately actionable and engine-verifiable

+

Never add to a losing position without new evidence. If you still want more, write the new evidence first and ask whether you would initiate the full position today.

+

A low-cost probe does not establish long-term trust; a small success should not automatically become a large position. — Survival discipline lens, paraphrase

- 本卡為交易行為回顧,不構成投資建議;鏡片來自一位投資人公開文章原則蒸餾,引用非轉載、非經本人背書。資料全程留在你本機。· 上方為 mock 示意資料,α/β 因樣本不足而失真。 + This card reviews trading behavior and is not investment advice. The lens distills public principles and is not endorsed by the source. Data remains local. Values above are mock data; alpha and beta are unreliable because the sample is small.
diff --git a/skills/fomo-kernel/copy/en.json b/skills/fomo-kernel/copy/en.json new file mode 100644 index 0000000..b1141f7 --- /dev/null +++ b/skills/fomo-kernel/copy/en.json @@ -0,0 +1,51 @@ +{ + "language": "en", + "title": "Trade Review Card", + "private_badge": "Private full version; stored locally only", + "public_badge": "Shareable version; amounts, dates, tickers, and exact weights removed", + "demo_badge": "Demo data and rehearsal only; not your trades and not written to formal coach memory", + "sections": { + "numbers": "The account for this review", + "trades": "Best and worst realized trades", + "strength": "One thing you did well", + "hole": "The biggest behavioral leak", + "motive": "The thesis behind the add", + "etf": "ETF and portfolio structure", + "honesty": "Evidence boundaries", + "rule": "Change only this next time" + }, + "dimensions": { + "exit_discipline": "exit discipline", + "position_sizing": "position sizing", + "diversification": "portfolio diversification", + "holding_period": "holding-horizon consistency", + "averaging_down": "averaging-down discipline", + "alpha_beta": "benchmark attribution", + "entry_style": "entry style" + }, + "rules": { + "exit_discipline": "Before exiting, name the fact that completed or broke the thesis. Fear of giving back gains is not enough.", + "position_sizing": "Check the single-risk-position cap before placing the order. If it is already over the cap, do not add.", + "diversification": "Before adding a position, check whether it is still the same underlying driver. If that bet is already too large, do not add.", + "holding_period": "Label the trade as short-term, swing, or long-term at entry. Exit only for a reason from the same horizon.", + "averaging_down": "Before averaging down, write one piece of evidence you did not know at entry. If you cannot, do not add." + }, + "add_choices": { + "planned_tranche": "A pre-planned tranche", + "new_evidence": "New testable evidence", + "valuation_change": "The valuation or odds changed", + "price_only": "Only the price fell / lower the cost basis", + "skip": "Skip for now" + }, + "honesty": { + "alpha_credibility": "The alpha sample or statistical strength is insufficient; it is not evidence of durable skill.", + "sector_attribution": "Some positions lack sector benchmarks, so allocation-versus-selection attribution is incomplete.", + "unclassified_drivers": "Some positions are unclassified, so portfolio concentration may be understated.", + "unrealized_coverage": "Some positions lack current prices, so unrealized P&L is incomplete.", + "orphan_sells": "Some exits lack a known entry; their realized P&L was excluded.", + "currency_mix": "The portfolio spans currencies; aggregate figures use a common currency and some FX may be approximate.", + "cash_reliability": "Cash lacks complete anchors, limiting account-level interpretation.", + "acct_perf_basis": "Account performance uses partial cost or FX approximations and remains uncertain.", + "etf_metadata": "ETF expense-ratio or tracking-error data is incomplete; missing values were not treated as zero." + } +} diff --git a/skills/fomo-kernel/copy/zh-TW.json b/skills/fomo-kernel/copy/zh-TW.json new file mode 100644 index 0000000..fc9251b --- /dev/null +++ b/skills/fomo-kernel/copy/zh-TW.json @@ -0,0 +1,51 @@ +{ + "language": "zh-TW", + "title": "交易復盤卡", + "private_badge": "私人完整版,只留在本機", + "public_badge": "可分享版,已移除金額、日期、標的與精確部位", + "demo_badge": "示範資料/演練,不是你的真實交易,也不寫入正式教練記憶", + "sections": { + "numbers": "這期的帳", + "trades": "最賺與最虧的已實現交易", + "strength": "你做對的一件事", + "hole": "最大的行為漏洞", + "motive": "這次加碼的 thesis 判斷", + "etf": "ETF 與組合結構", + "honesty": "資料邊界", + "rule": "下次只改這一件" + }, + "dimensions": { + "exit_discipline": "出場紀律", + "position_sizing": "部位 sizing", + "diversification": "分散", + "holding_period": "持有時間", + "averaging_down": "加碼攤平", + "alpha_beta": "大盤與選股歸因", + "entry_style": "進場風格" + }, + "rules": { + "exit_discipline": "出場前先寫下 thesis 已完成或失效的事實;只因害怕回吐,不賣。", + "position_sizing": "下單前先檢查單一風險部位上限;超過上限,不新增。", + "diversification": "新增部位前先看是否仍是同一個 driver;同一注已過重,不新增。", + "holding_period": "進場時先標短線、波段或長線;出場只用同一時間框架的理由。", + "averaging_down": "往下加碼前寫出一個進場時不知道的新證據;寫不出,不加。" + }, + "add_choices": { + "planned_tranche": "事先規劃的分批", + "new_evidence": "有新的可驗證證據", + "valuation_change": "估值或賠率變了", + "price_only": "只有價格下跌/想攤低成本", + "skip": "先跳過" + }, + "honesty": { + "alpha_credibility": "Alpha 的樣本或統計強度不足,不能當成穩定能力。", + "sector_attribution": "部分標的缺板塊基準,賽道與選股拆帳不完整。", + "unclassified_drivers": "部分標的尚未分類,組合集中度可能被低估。", + "unrealized_coverage": "部分持倉缺現價,未實現損益不是完整帳面。", + "orphan_sells": "有賣出缺少已知建倉,相關已實現損益未納入。", + "currency_mix": "組合含多幣別;聚合數字使用共同幣別,部分匯率可能為近似。", + "cash_reliability": "現金餘額缺完整錨點,含現金的帳戶判讀有限。", + "acct_perf_basis": "帳戶績效有部分成本或匯率近似,需保留不確定性。", + "etf_metadata": "ETF 費用率或 tracking error 資料不完整,本卡沒有把缺值猜成零。" + } +} diff --git a/skills/fomo-kernel/engine/card_renderer.py b/skills/fomo-kernel/engine/card_renderer.py new file mode 100644 index 0000000..a43c478 --- /dev/null +++ b/skills/fomo-kernel/engine/card_renderer.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Deterministic private/public card renderer. + +The agent supplies prose-only interpretation in ``narrative``. All displayed +numbers are selected from engine output here; narrative fields containing digits +are rejected to keep the engine's numeric authority enforceable in code. +""" +from __future__ import annotations + +import html +import json +import os +import re + + +class RenderError(ValueError): + pass + + +HERE = os.path.dirname(os.path.abspath(__file__)) +COPY_DIR = os.path.join(os.path.dirname(HERE), "copy") +ALLOWED_NARRATIVE = {"headline", "mirror", "counterfactual", "rule_rationale", "strength"} +DIMENSION_ID_BY_LEGACY_LABEL = { + "出場紀律": "exit_discipline", + "部位 sizing": "position_sizing", + "分散": "diversification", + "持有時間": "holding_period", + "加碼攤平": "averaging_down", + "alpha/beta": "alpha_beta", + "進場": "entry_style", +} + + +def load_copy(language): + language = "en" if str(language).lower().startswith("en") else "zh-TW" + with open(os.path.join(COPY_DIR, language + ".json"), encoding="utf-8") as f: + return json.load(f) + + +def validate_narrative(narrative): + if not isinstance(narrative, dict): + raise RenderError("narrative must be an object") + extra = set(narrative) - ALLOWED_NARRATIVE + if extra: + raise RenderError("unknown narrative fields: " + ", ".join(sorted(extra))) + for key, value in narrative.items(): + if not isinstance(value, str) or not value.strip(): + raise RenderError(f"narrative.{key} must be a non-empty string") + if re.search(r"\d", value): + raise RenderError(f"narrative.{key} contains digits; numeric claims must come from engine output") + if not narrative.get("headline") or not narrative.get("mirror"): + raise RenderError("narrative.headline and narrative.mirror are required") + return narrative + + +def dimension_id(dim): + """Return the stable English dimension identifier for legacy engine labels.""" + return DIMENSION_ID_BY_LEGACY_LABEL.get(dim, dim) + + +def localized_dimension(dim, language): + copy = load_copy(language) + dim_id = dimension_id(dim) + return (copy.get("dimensions") or {}).get(dim_id, dim_id.replace("_", " ")) + + +def localized_rule(dim, language): + return (load_copy(language).get("rules") or {}).get(dimension_id(dim)) + + +def _currency(card): + return ((card.get("currency_meta") or {}).get("aggregate_currency") or "USD").upper() + + +def _money(value, currency): + if value is None: + return "—" + symbol = "$" if currency == "USD" else currency + " " + return f"{symbol}{float(value):+,.0f}" + + +def _money_abs(value, currency): + if value is None: + return "—" + symbol = "$" if currency == "USD" else currency + " " + return f"{symbol}{abs(float(value)):,.0f}" + + +def _pct(value, digits=0): + return "—" if value is None else f"{float(value) * 100:.{digits}f}%" + + +def _hole_line(hole, language): + if language != "en": + return hole.get("number_line") or "" + d = hole.get("raw") or {} + dim = dimension_id(d.get("dim")) + if dim == "exit_discipline": + rate = _pct(d.get("early_rate")) + return (f"Across {d.get('n_rt', 0)} decision exits, {rate} were higher after the review window; " + f"winning positions were held {d.get('hold_win', 0):.0f} days versus " + f"{d.get('hold_lose', 0):.0f} days for losing positions.") + if dim == "position_sizing": + return (f"The largest single-risk position was {d.get('max_ticker')}, at {_pct(d.get('max_pct'))}; " + f"the average of the other risk positions was {_pct(d.get('avg_pct'))}.") + if dim == "diversification": + return (f"The portfolio held {d.get('n', 0)} positions, but the top three non-allocation risks were " + f"{_pct(d.get('top3'))} and the largest classified driver was {_pct(d.get('max_sector_pct'))}.") + if dim == "holding_period": + if d.get("no_data"): + return "There are not yet enough closed round trips to diagnose holding-time consistency." + return (f"Holding periods ranged from {d.get('min', 0)} to {d.get('max', 0)} days, " + f"with a median of {d.get('median_hold', 0):.0f} days.") + if dim == "averaging_down": + return (f"There were {d.get('count', 0)} adds to losing positions; " + f"{d.get('breach', 0)} crossed the position-size boundary at the time of the add.") + return "" + + +def _best_strength(card, language): + if language != "en" and card.get("strength"): + return card["strength"] + dims = card.get("dims_raw") or [] + safe = [d for d in dims if not d.get("triggered")] + if not safe: + return ("這期沒有足夠強的正向訊號;先把注意力留給最大的洞。" if language != "en" + else "No positive behavior was strong enough to claim; keep attention on the largest leak.") + dim = min(safe, key=lambda d: float(d.get("severity") or 0)).get("dim") + return f"The cleanest part of this review was {localized_dimension(dim, language)}." + + +def _honesty_lines(card, copy): + messages = copy.get("honesty") or {} + seen = set() + lines = [] + for entry in card.get("honesty_ledger") or []: + key = entry.get("key") + if key in seen: + continue + seen.add(key) + lines.append(messages.get(key) or key) + return lines + + +def _etf_lines(card, language): + ps = card.get("portfolio_structure") or {} + allocation = ps.get("allocation_etfs") or [] + concentrated = ps.get("concentrated_etfs") or [] + if not allocation and not concentrated: + return [] + if language == "en": + lines = [] + if allocation: + lines.append("Diversified allocation ETFs were separated from single-name concentration: " + + ", ".join(f"{x['ticker']} {_pct(x.get('weight'))}" for x in allocation) + ".") + if concentrated: + lines.append("Sector, thematic, or leveraged ETFs remained concentration risk: " + + ", ".join(f"{x['ticker']} {_pct(x.get('weight'))}" for x in concentrated) + ".") + return lines + lines = [] + if allocation: + lines.append("配置型 ETF 已從單一股票集中度排除:" + + "、".join(f"{x['ticker']} {_pct(x.get('weight'))}" for x in allocation) + "。") + if concentrated: + lines.append("產業/主題/槓桿 ETF 仍算集中風險:" + + "、".join(f"{x['ticker']} {_pct(x.get('weight'))}" for x in concentrated) + "。") + return lines + + +def _decision_lines(bundle, copy): + labels = copy.get("add_choices") or {} + lines = [] + for event in bundle.get("thesis_decisions") or []: + label = labels.get(event.get("decision"), event.get("decision")) + ticker = event.get("ticker") or "position" + if copy.get("language") == "en": + lines.append(f"{ticker}: {label}. The decision and its evidence boundary were saved for the next review.") + else: + lines.append(f"{ticker}:{label}。這個判斷與證據邊界已保存,供下次對帳。") + return lines + + +def _performance_lines(card, language): + """Render important existing product facts without giving the agent a calculator.""" + overview = card.get("overview") or {} + currency = _currency(card) + en = language == "en" + lines = [] + payoff = overview.get("payoff") + if payoff is not None: + if en: + lines.append(f"Realized payoff ratio was {payoff:.1f}; the average win was " + f"{_money(overview.get('avg_win'), currency)} versus " + f"{_money_abs(overview.get('avg_loss'), currency)} for the average loss.") + else: + lines.append(f"已實現盈虧比 {payoff:.1f};平均賺 {_money(overview.get('avg_win'), currency)}," + f"平均賠 {_money_abs(overview.get('avg_loss'), currency)}。") + ab = card.get("alpha_beta_breakdown") or {} + if not ab.get("note") and ab.get("port_tot") is not None: + bench = ab.get("bench") or "SPY" + if en: + line = (f"The measured portfolio returned {_pct(ab.get('port_tot'))} versus {_pct(ab.get('spy_tot'))} " + f"for {bench}, a {float(ab.get('excess_vs_spy') or 0) * 100:+.0f} pp difference.") + else: + line = (f"可比較的持倉報酬 {_pct(ab.get('port_tot'))},同期 {bench} {_pct(ab.get('spy_tot'))}," + f"相差 {float(ab.get('excess_vs_spy') or 0) * 100:+.0f} 個百分點。") + lines.append(line) + stat = ab.get("alpha_stat") or {} + if stat.get("alpha_ann") is not None and stat.get("ci95"): + low, high = stat["ci95"] + if en: + lines.append(f"Risk-adjusted alpha was {float(stat['alpha_ann']) * 100:+.0f}% annualized, " + f"with a 95% interval from {float(low) * 100:+.0f}% to {float(high) * 100:+.0f}%; " + "the interval controls how strong the conclusion may be.") + else: + lines.append(f"風險調整後 alpha 年化 {float(stat['alpha_ann']) * 100:+.0f}%," + f"九十五%區間為 {float(low) * 100:+.0f}% 到 {float(high) * 100:+.0f}%;" + "定論強度以這個區間為準。") + cash = card.get("cash") or {} + if cash.get("reliable") and cash.get("balance") is not None: + if en: + lines.append(f"Anchored account cash was {_money(cash.get('balance'), currency)}" + + (f", {_pct(cash.get('weight'))} of the account." if cash.get("weight") is not None else ".")) + else: + lines.append(f"有餘額錨點的帳戶現金為 {_money(cash.get('balance'), currency)}" + + (f",佔帳戶 {_pct(cash.get('weight'))}。" if cash.get("weight") is not None else "。")) + pa = card.get("payoff_attribution") or {} + cf = pa.get("counterfactual") or {} + if cf.get("ticker"): + after = "—" if cf.get("payoff") is None else f"{float(cf['payoff']):.1f}" + if en: + lines.append(f"The largest realized drag was {cf['ticker']} at {_money(cf.get('drag'), currency)}; " + f"without it, the payoff ratio would have been {after}.") + else: + lines.append(f"最大已實現拖累是 {cf['ticker']},淨影響 {_money(cf.get('drag'), currency)};" + f"拿掉它後盈虧比會是 {after}。") + return lines + + +def _trade_lines(card, language): + best, worst = card.get("best_trade"), card.get("worst_trade") + if not best or not worst: + return [] + currency = _currency(card) + if language == "en": + return [ + f"Best: {best['ticker']} {_pct(best.get('ret'))}, {_money(best.get('pnl'), currency)} realized.", + f"Worst: {worst['ticker']} {_pct(worst.get('ret'))}, {_money(worst.get('pnl'), currency)} realized.", + ] + return [ + f"最賺:{best['ticker']} {_pct(best.get('ret'))},已實現 {_money(best.get('pnl'), currency)}。", + f"最虧:{worst['ticker']} {_pct(worst.get('ret'))},已實現 {_money(worst.get('pnl'), currency)}。", + ] + + +def render_private(bundle): + language = bundle.get("language") or "zh-TW" + copy = load_copy(language) + narrative = validate_narrative(bundle.get("narrative") or {}) + card = bundle.get("engine_card") or {} + state = bundle.get("engine_state") or {} + sections = copy["sections"] + overview = card.get("overview") or {} + currency = _currency(card) + holes = card.get("top_holes") or [] + commitment = bundle.get("commitment") or {} + + lines = [ + "---", + f"session_id: {bundle.get('session_id')}", + "privacy: private", + f"language: {copy['language']}", + "---", + "", + f"# {narrative['headline']}", + "", + f"> {copy['private_badge']}", + "", + ] + if bundle.get("route") == "test_drive": + lines.extend([f"> {copy['demo_badge']}", ""]) + lines.extend([ + narrative["mirror"], "", f"## {sections['numbers']}", "", + ((f"帳面總損益 {_money(overview.get('total_pnl'), currency)},其中已實現 " + f"{_money(overview.get('realized'), currency)}、未實現 {_money(overview.get('unrealized'), currency)}。") + if copy["language"] != "en" else + (f"Total P&L was {_money(overview.get('total_pnl'), currency)}: " + f"{_money(overview.get('realized'), currency)} realized and " + f"{_money(overview.get('unrealized'), currency)} unrealized.")), + "", + ]) + performance = _performance_lines(card, copy["language"]) + if performance: + lines.extend(performance + [""]) + lines.extend([ + f"## {sections['strength']}", + "", + narrative.get("strength") or _best_strength(card, copy["language"]), + "", + ]) + trades = _trade_lines(card, copy["language"]) + if trades: + lines.extend([f"## {sections['trades']}", ""] + [f"- {x}" for x in trades] + [""]) + lines.extend([f"## {sections['hole']}", ""]) + if holes: + lines.extend([_hole_line(holes[0], copy["language"]), ""]) + if narrative.get("counterfactual"): + lines.extend([narrative["counterfactual"], ""]) + + decisions = _decision_lines(bundle, copy) + if decisions: + lines.extend([f"## {sections['motive']}", ""] + [f"- {x}" for x in decisions] + [""]) + etf_lines = _etf_lines(card, copy["language"]) + if etf_lines: + lines.extend([f"## {sections['etf']}", ""] + [f"- {x}" for x in etf_lines] + [""]) + honesty = _honesty_lines(card, copy) + if honesty: + lines.extend([f"## {sections['honesty']}", ""] + [f"- {x}" for x in honesty] + [""]) + + rule = commitment.get("rule") + if rule: + lines.extend([f"## {sections['rule']}", "", rule, ""]) + if narrative.get("rule_rationale"): + lines.extend([narrative["rule_rationale"], ""]) + elif ((bundle.get("answers") or {}).get("commitment") or {}).get("choice") == "skip": + lines.extend([f"## {sections['rule']}", "", + ("你這次選擇不設新承諾;下次仍可用同一份基線對帳。" if copy["language"] != "en" + else "You chose not to set a new commitment; the same baseline remains available next time."), ""]) + elif state.get("insufficient_data"): + lines.extend([f"## {sections['rule']}", "", + ("樣本仍短,這次不硬塞承諾;先把它當基線。" if copy["language"] != "en" + else "The sample is still short, so this review sets a baseline without forcing a commitment."), ""]) + return "\n".join(lines).rstrip() + "\n" + + +def _public_band(value, language): + value = float(value or 0) + if value < 0.25: + return "低" if language != "en" else "low" + if value < 0.40: + return "中" if language != "en" else "moderate" + if value < 0.60: + return "高" if language != "en" else "high" + return "很高" if language != "en" else "very high" + + +def render_public(bundle): + """Render a conservative shareable card without user-authored free text.""" + language = bundle.get("language") or "zh-TW" + copy = load_copy(language) + card = bundle.get("engine_card") or {} + holes = card.get("top_holes") or [] + hole = holes[0] if holes else {} + raw = hole.get("raw") or {} + dim = raw.get("dim") + severity = _public_band(hole.get("severity"), copy["language"]) + rule = (bundle.get("commitment") or {}).get("rule") + if copy["language"] == "en": + mirror = f"This review found {severity} behavioral pressure in {dim or 'the leading diagnostic dimension'}." + structure = "Diversified allocation ETFs were separated from single-name risk; focused ETFs remained concentration risk." + else: + mirror = f"這次復盤在「{dim or '主要行為維度'}」看見{severity}程度的行為壓力。" + structure = "配置型 ETF 與單一標的風險分開計算;產業、主題與槓桿 ETF 仍保留集中風險。" + lines = [ + "---", "privacy: public", f"language: {copy['language']}", "---", "", + f"# {copy['title']}", "", f"> {copy['public_badge']}", "", mirror, "", + ] + if bundle.get("route") == "test_drive": + lines[10:10] = [f"> {copy['demo_badge']}", ""] + ps = card.get("portfolio_structure") or {} + if ps.get("allocation_etfs") or ps.get("concentrated_etfs"): + lines.extend([f"## {copy['sections']['etf']}", "", structure, ""]) + if rule: + lines.extend([f"## {copy['sections']['rule']}", "", rule, ""]) + return "\n".join(lines).rstrip() + "\n" + + +def render_html(markdown_text, title="Trade Review Card"): + """Dependency-free HTML artifact; Markdown remains the canonical card text.""" + escaped = html.escape(markdown_text) + return ("\n" + f"{html.escape(title)}" + f"
{escaped}
\n") diff --git a/skills/fomo-kernel/engine/coach.py b/skills/fomo-kernel/engine/coach.py index bd223b5..eea9eb4 100644 --- a/skills/fomo-kernel/engine/coach.py +++ b/skills/fomo-kernel/engine/coach.py @@ -399,12 +399,16 @@ def cmd_save_card(args): ("last_state.json", "json", "上次引擎算出的薄狀態(對帳用;每次跑覆蓋,非 append-only)"), ("log.jsonl", "jsonl", "每次復盤的規矩承諾 + metric 快照"), ("theses.jsonl", "jsonl", "每筆持倉的持股假設與出場敘事"), + ("thesis_decisions.jsonl", "jsonl", "每次加碼的 thesis 決策與 evidence delta"), ("profile.md", "text", "交易目標 + 個人原則(第一次復盤時建立,Claude 直接寫檔)"), ("rules.jsonl", "jsonl", "累積的規矩庫"), ("problems.jsonl", "jsonl", "問題事件記錄(#137)"), ("ledger.jsonl", "jsonl", "交易/持倉快照帳本"), ("revisit.jsonl", "jsonl", "出場後 30/60/90 天追蹤佇列"), ("cards", "dir", "每次復盤的完整私人卡(含絕對金額/ticker/佔比)"), + ("sessions", "tree", "v2 canonical session bundles(private/public cards + manifest)"), + ("projections", "dir", "canonical bundle 投影到舊資料檔的修復紀錄"), + (".pending", "tree", "尚未 finalize 的可恢復 review plan/answers/preview"), ] @@ -420,11 +424,14 @@ def _scan_root(root): entry = {"name": name, "path": path, "kind": kind, "desc": desc, "exists": os.path.exists(path)} if entry["exists"]: - if kind == "dir": - files = sorted(f for f in os.listdir(path) - if os.path.isfile(os.path.join(path, f))) + if kind in {"dir", "tree"}: + if kind == "tree": + files = sorted(os.path.join(dp, f) for dp, _, fs in os.walk(path) for f in fs) + else: + files = sorted(os.path.join(path, f) for f in os.listdir(path) + if os.path.isfile(os.path.join(path, f))) entry["count"] = len(files) - entry["size_bytes"] = sum(os.path.getsize(os.path.join(path, f)) for f in files) + entry["size_bytes"] = sum(os.path.getsize(f) for f in files) else: entry["size_bytes"] = os.path.getsize(path) if kind == "jsonl": @@ -450,11 +457,12 @@ def cmd_data_export(args): _die(f"{root} 下沒有任何資料可匯出(可能是第一次使用,或 --root 指錯路徑)") with zipfile.ZipFile(args.out, "w", zipfile.ZIP_DEFLATED) as zf: for e in present: - if e["kind"] == "dir": - for f in sorted(os.listdir(e["path"])): - fp = os.path.join(e["path"], f) - if os.path.isfile(fp): - zf.write(fp, arcname=os.path.join(e["name"], f)) + if e["kind"] in {"dir", "tree"}: + for dp, _, files in os.walk(e["path"]): + for f in sorted(files): + fp = os.path.join(dp, f) + rel = os.path.relpath(fp, e["path"]) + zf.write(fp, arcname=os.path.join(e["name"], rel)) else: zf.write(e["path"], arcname=e["name"]) print(f"⚠️ 匯出檔含敏感交易衍生資料(部位金額/ticker/規矩承諾),請比照對帳單妥善保存:{args.out}", @@ -477,7 +485,7 @@ def cmd_data_reset(args): return deleted = [] for e in present: - if e["kind"] == "dir": + if e["kind"] in {"dir", "tree"}: shutil.rmtree(e["path"]) else: os.remove(e["path"]) diff --git a/skills/fomo-kernel/engine/compare_lenses.py b/skills/fomo-kernel/engine/compare_lenses.py index 36d83a6..1c47bcd 100644 --- a/skills/fomo-kernel/engine/compare_lenses.py +++ b/skills/fomo-kernel/engine/compare_lenses.py @@ -30,6 +30,11 @@ import os LENS_DIR = os.path.join(os.path.dirname(__file__), "..", "rubric") +DIMENSION_ID_BY_LEGACY_LABEL = { + "出場紀律": "exit_discipline", "部位 sizing": "position_sizing", + "分散": "diversification", "持有時間": "holding_period", + "加碼攤平": "averaging_down", "alpha/beta": "alpha_beta", "進場": "entry_style", +} STANCE_J = {"inverted": -1.0, "conditional": 0.5, "aligned": 0.0, "unconditional": 1.0} # aligned = 0.0(中立基線):普世/meta 派(如交易心理)不該被當「最對立」,它沒在 fork。 @@ -73,7 +78,7 @@ def compare_lenses(dims, lenses): 每項記下最對立的一對 master(a, b)。只計 triggered 的洞。""" rows = [] for d in dims: - key = d["dim"] + key = d.get("dim_id") or DIMENSION_ID_BY_LEGACY_LABEL.get(d["dim"], d["dim"]) if not d.get("triggered"): continue present = [L for L in lenses if key in L.get("dims", {})] diff --git a/skills/fomo-kernel/engine/instruments.py b/skills/fomo-kernel/engine/instruments.py new file mode 100644 index 0000000..4ccaa84 --- /dev/null +++ b/skills/fomo-kernel/engine/instruments.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Instrument policy for portfolio-behaviour diagnostics. + +This module intentionally answers one narrow, deterministic question: should a +position be treated as a single-name concentration risk, or as a diversified +allocation instrument? It does not fetch market data and it never guesses that +an unknown ticker is an ETF. Callers may supply a local JSON map through +``TR_INSTRUMENT_MAP`` for instruments not covered by the conservative fallback. + +Map shape:: + + { + "ACWI": { + "kind": "broad_market_etf", + "expense_ratio": 0.0032, + "tracking_error": null + } + } + +Only broad-market, regional, bond, and commodity ETFs receive the allocation +exemption. Sector/thematic and leveraged ETFs remain concentration risk. +""" +from __future__ import annotations + +import json +import os +from collections import defaultdict + + +ALLOCATION_KINDS = { + "broad_market_etf", + "regional_etf", + "bond_etf", + "commodity_etf", +} +CONCENTRATED_ETF_KINDS = {"sector_etf", "thematic_etf", "leveraged_etf"} +ETF_KINDS = ALLOCATION_KINDS | CONCENTRATED_ETF_KINDS +VALID_KINDS = ETF_KINDS | {"equity", "fund", "cash", "unknown"} + + +def _rows(kind, tickers): + return {ticker: {"kind": kind} for ticker in tickers.split()} + + +FALLBACK = {} +FALLBACK.update(_rows("broad_market_etf", "SPY VOO IVV VTI VT SCHB ITOT ACWI")) +FALLBACK.update(_rows("regional_etf", "VXUS VEA VWO EWY EWT EWJ VGK")) +FALLBACK.update(_rows("bond_etf", "BND AGG IEF TLT SHY SGOV TIP")) +FALLBACK.update(_rows("commodity_etf", "IAU GLD SLV DBC")) +FALLBACK.update(_rows("sector_etf", "XLK XLE XLF XLV XLI XLY XLP XLU XLC XLB XLRE")) +FALLBACK.update(_rows("thematic_etf", "QQQ SOXX SMH ARKK BOTZ ROBO")) +FALLBACK.update(_rows("leveraged_etf", "TQQQ SQQQ UPRO SPXU SOXL SOXS")) + +_MAP = {ticker: dict(meta) for ticker, meta in FALLBACK.items()} +_LOAD_RESULT = {"loaded": 0, "skipped": 0, "error": None} + + +def reset_map(): + """Restore fallback data. Primarily useful for deterministic tests.""" + global _MAP, _LOAD_RESULT + _MAP = {ticker: dict(meta) for ticker, meta in FALLBACK.items()} + _LOAD_RESULT = {"loaded": 0, "skipped": 0, "error": None} + + +def load_map(path): + """Merge a local instrument map; malformed entries are skipped visibly.""" + global _LOAD_RESULT + result = {"loaded": 0, "skipped": 0, "error": None} + try: + with open(path, encoding="utf-8") as f: + raw = json.load(f) + except (OSError, ValueError) as exc: + result["error"] = str(exc) + _LOAD_RESULT = result + return result + if not isinstance(raw, dict): + result["error"] = "instrument map must be an object" + _LOAD_RESULT = result + return result + for ticker, meta in raw.items(): + if not isinstance(ticker, str) or not isinstance(meta, dict): + result["skipped"] += 1 + continue + kind = meta.get("kind") + if kind not in VALID_KINDS: + result["skipped"] += 1 + continue + clean = {"kind": kind} + for key in ("name", "expense_ratio", "tracking_error", "source", "as_of"): + if key in meta: + clean[key] = meta[key] + _MAP[ticker.strip().upper()] = clean + result["loaded"] += 1 + _LOAD_RESULT = result + return result + + +def load_from_env(): + path = os.environ.get("TR_INSTRUMENT_MAP") + return load_map(path) if path else dict(_LOAD_RESULT) + + +def info(ticker): + """Return conservative metadata. Unknown tickers never receive exemption.""" + symbol = str(ticker or "").strip().upper() + meta = dict(_MAP.get(symbol) or {"kind": "equity"}) + meta["ticker"] = symbol + meta["is_etf"] = meta["kind"] in ETF_KINDS + meta["allocation_exempt"] = meta["kind"] in ALLOCATION_KINDS + return meta + + +def is_diversified_allocation(ticker): + return info(ticker)["allocation_exempt"] + + +def portfolio_analysis(weights): + """Summarize portfolio structure without fabricating unavailable metadata.""" + weights = weights or {} + by_kind = defaultdict(float) + allocation = [] + concentrated = [] + missing = [] + for ticker, weight in sorted(weights.items()): + meta = info(ticker) + by_kind[meta["kind"]] += float(weight or 0) + if not meta["is_etf"]: + continue + row = {"ticker": meta["ticker"], "kind": meta["kind"], "weight": float(weight or 0)} + for key in ("expense_ratio", "tracking_error", "source", "as_of"): + if key in meta: + row[key] = meta[key] + (allocation if meta["allocation_exempt"] else concentrated).append(row) + absent = [key for key in ("expense_ratio", "tracking_error") if meta.get(key) is None] + if absent: + missing.append({"ticker": meta["ticker"], "fields": absent}) + return { + "schema_version": 1, + "policy": "allocation_etfs_exempt_single_name;sector_thematic_leveraged_concentrated", + "allocation_weight": sum(row["weight"] for row in allocation), + "concentrated_etf_weight": sum(row["weight"] for row in concentrated), + "by_kind": dict(sorted(by_kind.items())), + "allocation_etfs": allocation, + "concentrated_etfs": concentrated, + "metadata_gaps": missing, + } diff --git a/skills/fomo-kernel/engine/review.py b/skills/fomo-kernel/engine/review.py new file mode 100644 index 0000000..52636c3 --- /dev/null +++ b/skills/fomo-kernel/engine/review.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tool-neutral orchestration CLI for one-card trade reviews. + +Lifecycle: + + prepare -> agent asks the returned question_queue + preview -> validates answers/theses/narrative and renders a pending card + finalize -> user chooses one commitment; commits an atomic session bundle + resume -> returns pending state after interruption + +All commands emit JSON on stdout. Human-readable diagnostics go to stderr. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import pathlib +import subprocess +import sys +import tempfile + +import card_renderer +import ledger +import session +import thesis + + +HERE = pathlib.Path(__file__).resolve().parent +TRADE_RECAP = HERE / "trade_recap.py" +MOCK_CSV = HERE.parent / "mock" / "mock_trades.csv" +DIM_METRIC = { + "exit_discipline": "exit_severity", + "position_sizing": "max_pos_pct", + "diversification": "top3_pct", + "holding_period": "hold_severity", + "averaging_down": "avgdown_count", +} + + +class ReviewError(ValueError): + pass + + +def _emit(obj): + print(json.dumps(obj, ensure_ascii=False, indent=2, sort_keys=True)) + + +def _load_json(path, label): + try: + with open(path, encoding="utf-8") as f: + value = json.load(f) + except (OSError, ValueError) as exc: + raise ReviewError(f"cannot read {label}: {exc}") from exc + if not isinstance(value, dict): + raise ReviewError(f"{label} must be a JSON object") + return value + + +def _jsonl(path): + return thesis.read_jsonl(path) + + +def _fingerprint(paths, language, route, prepared=None): + h = hashlib.sha256() + h.update(f"{language}\0{route}\0".encode()) + if prepared: + h.update(session.canonical(prepared).encode()) + for path in paths or []: + p = os.path.abspath(path) + h.update(p.encode() + b"\0") + with open(p, "rb") as f: + while True: + block = f.read(1024 * 1024) + if not block: + break + h.update(block) + return h.hexdigest() + + +def _pending_by_fingerprint(root, fingerprint): + base = os.path.join(root, ".pending") + if not os.path.isdir(base): + return None + for sid in sorted(os.listdir(base)): + plan_path = os.path.join(base, sid, "plan.json") + if not os.path.exists(plan_path): + continue + try: + plan = session.read_json(plan_path) + except (OSError, ValueError): + continue + if (plan.get("input") or {}).get("fingerprint") == fingerprint: + return plan + return None + + +def _has_history(root): + sessions = os.path.join(root, "sessions") + if os.path.isdir(sessions) and any(not n.startswith(".") for n in os.listdir(sessions)): + return True + return bool(_jsonl(os.path.join(root, "log.jsonl"))) + + +def _previous_state(root): + path = os.path.join(root, "last_state.json") + if not os.path.exists(path): + return None + try: + return session.read_json(path) + except (OSError, ValueError): + return None + + +def _run_engine(paths, root, args): + os.makedirs(root, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="fomo-review-") as tmp: + state_path = os.path.join(tmp, "state.json") + env = dict(os.environ, TR_JSON="1", TR_STATE_OUT=state_path, + TR_LEDGER=os.path.join(root, "ledger.jsonl")) + previous = _previous_state(root) + if previous and previous.get("date_end"): + env["TR_PREV_END"] = str(previous["date_end"]) + for arg_name, env_name in (("driver_map", "TR_DRIVER_MAP"), + ("instrument_map", "TR_INSTRUMENT_MAP"), + ("cash", "TR_CASH")): + value = getattr(args, arg_name, None) + if value: + env[env_name] = value + run = subprocess.run([sys.executable, str(TRADE_RECAP)] + list(paths), cwd=str(HERE.parent), + env=env, capture_output=True, text=True, timeout=args.timeout) + if run.returncode: + raise ReviewError(f"engine failed ({run.returncode}): {run.stderr.strip()}") + try: + card = json.loads(run.stdout) + state = session.read_json(state_path) + except (ValueError, OSError) as exc: + raise ReviewError(f"engine returned invalid artifacts: {exc}") from exc + return card, state, run.stderr.strip() + + +def _active_positions(state): + return ((state.get("holdings") or {}).get("positions") or {}) + + +def _add_options(language): + copy = card_renderer.load_copy(language) + descriptions = { + "planned_tranche": ("進場前已定好節奏,價格下跌不是新增理由。", + "The tranche schedule existed before the price move."), + "new_evidence": ("必須補 claim 與 source,之後能回頭驗證。", + "Requires a claim and source that a later review can test."), + "valuation_change": ("判斷沒變,但價格讓賠率或安全邊際改變。", + "The thesis is unchanged, but price changed the odds or margin of safety."), + "price_only": ("沒有新事實,主要是想攤低成本或等回本。", + "No new fact; the main motive was lowering the cost basis or getting back to even."), + "skip": ("先不定性,卡上只標未確認。", "Leave the motive unclassified for now."), + } + en = copy["language"] == "en" + return [{"value": key, "label": copy["add_choices"][key], + "description": descriptions[key][1 if en else 0]} + for key in ("new_evidence", "planned_tranche", "valuation_change", "price_only", "skip")] + + +def _generic_options(language): + if str(language).lower().startswith("en"): + return [ + {"value": "deliberate_plan", "label": "Deliberate plan", "description": "The action followed a rule set before the trade."}, + {"value": "emotional_reaction", "label": "Emotional reaction", "description": "Fear, regret, or urgency drove the action."}, + {"value": "external_constraint", "label": "External constraint", "description": "Liquidity, tax, or another constraint drove it."}, + {"value": "skip", "label": "Skip", "description": "Leave the motive unresolved for now."}, + ] + return [ + {"value": "deliberate_plan", "label": "事先規劃", "description": "行動遵循交易前就存在的規則。"}, + {"value": "emotional_reaction", "label": "情緒反應", "description": "恐懼、後悔或急迫感主導了行動。"}, + {"value": "external_constraint", "label": "外部限制", "description": "資金、稅務或其他限制主導了行動。"}, + {"value": "skip", "label": "先跳過", "description": "這次先不替動機下定論。"}, + ] + + +def _question_queue(card, state, active, previous_state, language): + positions = _active_positions(state) + by_ticker = {ticker: row for ticker, row in positions.items()} + current_adds = (state.get("metrics") or {}).get("avgdown_count") or 0 + previous_adds = ((previous_state or {}).get("metrics") or {}).get("avgdown_count") or 0 + add_behavior_changed = current_adds > previous_adds + queue = [] + for index, item in enumerate(card.get("thesis_questions") or []): + ticker = item.get("ticker") + pos = by_ticker.get(ticker) or {} + cycle_id = pos.get("cycle_id") + old = active.get(cycle_id) + if old and old.get("maturity") == "testable" and not add_behavior_changed: + continue + if str(language).lower().startswith("en"): + question = (f"For {ticker}, was the add based on new evidence, a pre-planned tranche, " + "a valuation change, or only the lower price?") + else: + question = (item.get("question") or + f"{ticker} 這次加碼,是新證據、事先分批、估值改變,還是只有價格下跌?") + queue.append({ + "id": f"add_{index}_{ticker}", "kind": "add_thesis", "ticker": ticker, + "cycle_id": cycle_id, "required": True, "question": question, + "options": _add_options(language), + "prior_thesis_id": (old or {}).get("thesis_id"), + }) + if not queue: + top = ((card.get("top_holes") or [{}])[0]).get("dim") or state.get("headline_dim") + top_label = card_renderer.localized_dimension(top, language) + question = (f"What mainly drove the behavior behind {top_label}?" if str(language).lower().startswith("en") + else f"這次「{top}」背後,主要是事先規劃、情緒反應,還是外部限制?") + queue.append({"id": "headline_motive", "kind": "headline_motive", "required": True, + "question": question, "options": _generic_options(language)}) + return queue + + +def _candidate_rules(card, state, language): + candidates = [] + seen = set() + source = list(card.get("candidate_rules") or []) + for hole in card.get("top_holes") or []: + source.append({"dim": hole.get("dim"), "rule": hole.get("lens_rule")}) + metrics = state.get("metrics") or {} + for row in source: + dim = row.get("dim") or row.get("kind") + dim_id = card_renderer.dimension_id(dim) + metric = DIM_METRIC.get(dim_id) + if not dim or dim in seen or metric not in metrics: + continue + rule = card_renderer.localized_rule(dim, language) or row.get("rule") + if not rule: + continue + seen.add(dim) + candidates.append({"id": f"candidate_{len(candidates)}", "dim": dim_id, "rule": rule, + "metric_key": metric, "goal": "down"}) + if len(candidates) == 3: + break + return candidates + + +def _build_plan(card, state, engine_meta, root, paths, route, language, fingerprint, nonce, persist): + positions = _active_positions(state) + cycle_ids = [row.get("cycle_id") for row in positions.values() if row.get("cycle_id")] + thesis_rows = _jsonl(os.path.join(root, "theses.jsonl")) + active_rows = thesis.reconstruct_active(thesis_rows, cycle_ids) + active = {row.get("cycle_id"): row for row in active_rows} + missing = [{"ticker": ticker, "cycle_id": row.get("cycle_id")} + for ticker, row in sorted(positions.items()) if row.get("cycle_id") not in active] + previous = _previous_state(root) + session_id = ledger.session_id_from_state(state, f"{nonce}|{route}|{language}") + plan = { + "schema_version": 2, + "session_id": session_id, + "status": "awaiting_answers", + "route": route, + "flow_path": f"flows/{route.replace('_', '-')}.md", + "language": "en" if str(language).lower().startswith("en") else "zh-TW", + "persist": bool(persist), + "state_root": root, + "input": {"paths": [os.path.abspath(p) for p in paths], + "kind": "positions_snapshot" if route == "snapshot_review" else "trades_csv", + "fingerprint": fingerprint, "engine_meta": engine_meta}, + "state_snapshot": {"prior_commitment": (previous or {}).get("commitment"), + "active_theses": active_rows, "due_revisits": []}, + "question_queue": _question_queue(card, state, active, previous, language), + "missing_thesis_positions": missing, + "card_plan": {"candidate_rules": _candidate_rules(card, state, language), + "required_honesty_keys": [x.get("key") for x in card.get("honesty_ledger") or []]}, + "engine_card": card, + "engine_state": state, + } + return plan + + +def cmd_prepare(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + language = args.language + route = args.route + persist = not args.test_drive + if args.test_drive: + route = "test_drive" + if not args.root: + root = tempfile.mkdtemp(prefix="fomo-kernel-test-drive-") + elif route == "auto": + route = "weekly_review" if _has_history(root) else "first_review" + if route == "snapshot_review" and not (args.card_json and args.state_json): + raise ReviewError("snapshot_review currently requires --card-json and --state-json from the snapshot adapter") + paths = list(args.paths or ([] if args.card_json else [str(MOCK_CSV) if args.test_drive else None])) + if any(p is None for p in paths) or (not paths and not args.card_json): + raise ReviewError("provide at least one CSV path, or use --test-drive") + prepared = None + if args.card_json or args.state_json: + if not (args.card_json and args.state_json): + raise ReviewError("--card-json and --state-json must be provided together") + card = _load_json(args.card_json, "engine card") + state = _load_json(args.state_json, "engine state") + prepared = {"card": card, "state": state} + engine_meta = "prepared artifacts" + fingerprint = _fingerprint(paths, language, route, prepared=prepared) + existing = _pending_by_fingerprint(root, fingerprint) + if existing: + _emit({"status": "resumed", "session_id": existing["session_id"], "review_plan": existing, + "next_action": "ask question_queue, then run preview"}) + return + if prepared is None: + card, state, engine_meta = _run_engine(paths, root, args) + plan = _build_plan(card, state, engine_meta, root, paths, route, language, fingerprint, + args.session_nonce or "", persist) + committed = session.session_dir(root, plan["session_id"]) + if os.path.isdir(committed): + _emit({"status": "already_committed", "session_id": plan["session_id"], "path": committed}) + return + session.save_pending(root, plan["session_id"], plan=plan) + _emit({"status": "prepared", "session_id": plan["session_id"], "review_plan": plan, + "next_action": "ask every required question, author thesis_updates and prose-only narrative, then run preview"}) + + +def _validate_thesis_completeness(plan, answers): + updates = answers.get("thesis_updates") or [] + positions = _active_positions(plan.get("engine_state") or {}) + thesis.validate_thesis_updates(updates, positions) + needed = {row.get("cycle_id") for row in plan.get("missing_thesis_positions") or []} + supplied = {row.get("cycle_id") for row in updates} + missing = sorted(x for x in needed - supplied if x) + if missing: + raise ReviewError("missing inferred thesis updates for cycles: " + ", ".join(missing)) + return updates + + +def _assign_thesis_ids(plan, updates): + suffix = plan["session_id"].split("__")[-1] + date = (plan.get("engine_state") or {}).get("date_end") + rows = [] + for index, update in enumerate(updates): + row = dict(update) + row.setdefault("status", "active") + row["session_date"] = date + row["session_id"] = plan["session_id"] + row.setdefault("thesis_id", f"{row['ticker']}-{date}-{suffix}-{index}") + rows.append(row) + return rows + + +def _resolve_commitment(plan, answers): + choice = answers.get("commitment") or {} + selected = choice.get("choice") + if selected == "skip": + return None + candidates = {row["id"]: row for row in (plan.get("card_plan") or {}).get("candidate_rules") or []} + if selected in candidates: + chosen = dict(candidates[selected]) + elif selected == "custom": + chosen = {"rule": (choice.get("rule") or "").strip(), "metric_key": choice.get("metric_key"), + "goal": choice.get("goal") or "down", "dim": choice.get("dim")} + if not chosen["rule"]: + raise ReviewError("custom commitment requires rule") + else: + raise ReviewError("commitment.choice must be a candidate id, custom, or skip") + metrics = (plan.get("engine_state") or {}).get("metrics") or {} + if chosen.get("metric_key") not in metrics: + raise ReviewError(f"commitment metric is not in engine state: {chosen.get('metric_key')}") + chosen.pop("id", None) + chosen["metric_value"] = metrics.get(chosen["metric_key"]) + chosen["source"] = "user_chosen" + if (plan.get("engine_state") or {}).get("insufficient_data"): + chosen["baseline_note"] = "short-sample baseline" + return chosen + + +def _draft_bundle(plan, answers, narrative, require_commitment): + if answers.get("session_id") != plan.get("session_id"): + raise ReviewError("answers.session_id does not match Review Plan") + thesis.validate_required_answers(plan, answers, allow_commitment_missing=not require_commitment) + updates = _validate_thesis_completeness(plan, answers) + decisions = thesis.build_decision_events(plan, answers) + card_renderer.validate_narrative(narrative) + commitment = _resolve_commitment(plan, answers) if require_commitment else None + return { + "schema_version": 2, + "session_id": plan["session_id"], + "route": plan["route"], + "language": plan["language"], + "review_plan": plan, + "engine_state": plan["engine_state"], + "engine_card": plan["engine_card"], + "answers": answers, + "narrative": narrative, + "thesis_updates": _assign_thesis_ids(plan, updates), + "thesis_decisions": decisions, + "commitment": commitment, + "observations": list(answers.get("observations") or []), + } + + +def _load_interaction(args, pending): + answers = _load_json(args.answers, "answers") if args.answers else pending.get("answers") + narrative = _load_json(args.narrative, "narrative") if args.narrative else pending.get("narrative") + if not answers or not narrative: + raise ReviewError("answers and narrative are required (pass files or save them with preview)") + return answers, narrative + + +def cmd_preview(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + pending = session.load_pending(root, args.session_id) + plan = pending.get("plan") + answers, narrative = _load_interaction(args, pending) + bundle = _draft_bundle(plan, answers, narrative, require_commitment=False) + private_md = card_renderer.render_private(bundle) + public_md = card_renderer.render_public(bundle) + paths = session.save_pending(root, args.session_id, answers=answers, narrative=narrative, + **{"card-private-preview": private_md, + "card-public-preview": public_md}) + _emit({"status": "previewed", "session_id": args.session_id, + "private_card": private_md, "public_card": public_md, + "candidate_rules": (plan.get("card_plan") or {}).get("candidate_rules") or [], + "paths": paths, "next_action": "show the private preview; ask the user to choose one rule or skip; then finalize"}) + + +def cmd_finalize(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + committed_path = session.session_dir(root, args.session_id) + if os.path.isdir(committed_path): + existing = session.load_committed(root, args.session_id) + plan = existing.get("review_plan") + pending = {"answers": existing.get("answers"), "narrative": existing.get("narrative")} + else: + pending = session.load_pending(root, args.session_id) + plan = pending.get("plan") + answers, narrative = _load_interaction(args, pending) + bundle = _draft_bundle(plan, answers, narrative, require_commitment=True) + private_md = card_renderer.render_private(bundle) + public_md = card_renderer.render_public(bundle) + private_html = card_renderer.render_html(private_md, card_renderer.load_copy(plan["language"])["title"]) + result = session.commit_bundle(root, bundle, private_md, public_md, private_html) + projection = None + projection_error = None + if plan.get("persist"): + try: + projection = session.project_legacy(root, bundle, private_md) + except Exception as exc: # canonical bundle is already safe; repair-projections can retry + projection_error = str(exc) + _emit({"status": result["status"], "session_id": args.session_id, "path": result["path"], + "private_card": os.path.join(result["path"], "card-private.md"), + "public_card": os.path.join(result["path"], "card-public.md"), + "projection": projection, "projection_error": projection_error, + "recoverable": bool(projection_error)}) + + +def cmd_resume(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + if args.session_id: + _emit(session.load_pending(root, args.session_id)) + return + base = os.path.join(root, ".pending") + pending = [] if not os.path.isdir(base) else sorted( + x for x in os.listdir(base) if os.path.isdir(os.path.join(base, x))) + _emit({"status": "pending" if pending else "idle", "pending_sessions": pending, + "next_action": "run resume with --session-id" if pending else "run prepare"}) + + +def cmd_render(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + bundle = session.load_committed(root, args.session_id) + private_md = card_renderer.render_private(bundle) + public_md = card_renderer.render_public(bundle) + _emit({"session_id": args.session_id, "private_card": private_md, "public_card": public_md}) + + +def cmd_repair(args): + root = os.path.abspath(os.path.expanduser(args.root or session.default_root())) + _emit({"status": "repaired", "reports": session.repair_projections(root)}) + + +def build_parser(): + parser = argparse.ArgumentParser(description="fomo-kernel stable review orchestration") + sub = parser.add_subparsers(dest="command", required=True) + prepare = sub.add_parser("prepare", help="run engine and emit a resumable Review Plan") + prepare.add_argument("paths", nargs="*", help="normalized trade CSV files") + prepare.add_argument("--root") + prepare.add_argument("--language", default="zh-TW", choices=("zh-TW", "en")) + prepare.add_argument("--route", default="auto", + choices=("auto", "first_review", "weekly_review", "snapshot_review")) + prepare.add_argument("--test-drive", action="store_true") + prepare.add_argument("--session-nonce", default="") + prepare.add_argument("--driver-map") + prepare.add_argument("--instrument-map") + prepare.add_argument("--cash", help="TR_CASH JSON string") + prepare.add_argument("--card-json", help="precomputed engine card (adapter/testing)") + prepare.add_argument("--state-json", help="precomputed engine state (adapter/testing)") + prepare.add_argument("--timeout", type=int, default=180) + prepare.set_defaults(func=cmd_prepare) + + for name, func in (("preview", cmd_preview), ("finalize", cmd_finalize)): + p = sub.add_parser(name) + p.add_argument("--session-id", required=True) + p.add_argument("--root") + p.add_argument("--answers") + p.add_argument("--narrative") + p.set_defaults(func=func) + resume = sub.add_parser("resume") + resume.add_argument("--session-id") + resume.add_argument("--root") + resume.set_defaults(func=cmd_resume) + render = sub.add_parser("render") + render.add_argument("--session-id", required=True) + render.add_argument("--root") + render.set_defaults(func=cmd_render) + repair = sub.add_parser("repair-projections") + repair.add_argument("--root") + repair.set_defaults(func=cmd_repair) + return parser + + +def main(): + args = build_parser().parse_args() + try: + args.func(args) + except (ReviewError, session.SessionError, thesis.ThesisError, card_renderer.RenderError) as exc: + _emit({"status": "error", "error": str(exc)}) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/fomo-kernel/engine/session.py b/skills/fomo-kernel/engine/session.py new file mode 100644 index 0000000..bf997c0 --- /dev/null +++ b/skills/fomo-kernel/engine/session.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Canonical review-session storage and recoverable legacy projections. + +The committed session directory is the source of truth. It is assembled in a +staging directory and renamed into place in one filesystem operation. Existing +JSONL files remain supported as projections so older tooling keeps working; a +projection failure never corrupts or invalidates the committed session. +""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import tempfile + +import ledger + + +class SessionError(ValueError): + pass + + +PKEY = { + "max_pos_pct": "oversize", + "avgdown_count": "avgdown_breach", + "ai_pct": "concentration", + "max_sector_pct": "concentration", + "top3_pct": "concentration", +} + + +def default_root(): + return os.path.expanduser(os.environ.get("TRADE_COACH_HOME", "~/.trade-coach")) + + +def canonical(obj): + return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def pretty(obj): + return json.dumps(obj, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + + +def read_json(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _safe_id(session_id): + if not session_id or session_id != os.path.basename(session_id) or session_id in {".", ".."}: + raise SessionError("invalid session_id") + return session_id + + +def pending_dir(root, session_id): + return os.path.join(root, ".pending", _safe_id(session_id)) + + +def session_dir(root, session_id): + return os.path.join(root, "sessions", _safe_id(session_id)) + + +def save_pending(root, session_id, **artifacts): + """Atomically update named pending artifacts; returns their stable paths.""" + base = pending_dir(root, session_id) + os.makedirs(base, exist_ok=True) + paths = {} + for name, value in artifacts.items(): + if value is None: + continue + ext = ".json" if isinstance(value, (dict, list)) else ".md" + path = os.path.join(base, name + ext) + text = pretty(value) if isinstance(value, (dict, list)) else str(value) + if text and not text.endswith("\n"): + text += "\n" + ledger.atomic_write_text(path, text) + paths[name] = path + return paths + + +def load_pending(root, session_id): + base = pending_dir(root, session_id) + if not os.path.isdir(base): + raise SessionError(f"pending session not found: {session_id}") + out = {"session_id": session_id, "path": base} + for name in ("plan", "answers", "narrative"): + path = os.path.join(base, name + ".json") + if os.path.exists(path): + out[name] = read_json(path) + for name in ("card-private-preview", "card-public-preview"): + path = os.path.join(base, name + ".md") + if os.path.exists(path): + with open(path, encoding="utf-8") as f: + out[name] = f.read() + return out + + +def _artifact_hash(text): + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def commit_bundle(root, bundle, private_md, public_md, private_html=None): + """Commit an immutable canonical bundle via staging-directory rename.""" + session_id = _safe_id(bundle.get("session_id")) + sessions = os.path.join(root, "sessions") + os.makedirs(sessions, exist_ok=True) + final = session_dir(root, session_id) + if os.path.isdir(final): + existing = read_json(os.path.join(final, "bundle.json")) + if canonical(existing) != canonical(bundle): + raise SessionError(f"session {session_id} already committed with different content") + return {"status": "no-op", "path": final, "session_id": session_id} + + staging = tempfile.mkdtemp(prefix=f".{session_id}.staging-", dir=sessions) + try: + artifacts = { + "bundle.json": pretty(bundle), + "state.json": pretty(bundle.get("engine_state") or {}), + "plan.json": pretty(bundle.get("review_plan") or {}), + "answers.json": pretty(bundle.get("answers") or {}), + "narrative.json": pretty(bundle.get("narrative") or {}), + "card-private.md": private_md if private_md.endswith("\n") else private_md + "\n", + "card-public.md": public_md if public_md.endswith("\n") else public_md + "\n", + } + if private_html is not None: + artifacts["card-private.html"] = private_html if private_html.endswith("\n") else private_html + "\n" + manifest = {name: _artifact_hash(text) for name, text in artifacts.items()} + artifacts["manifest.json"] = pretty({"schema_version": 1, "sha256": manifest}) + for name, text in artifacts.items(): + ledger.atomic_write_text(os.path.join(staging, name), text) + os.replace(staging, final) + except Exception: + shutil.rmtree(staging, ignore_errors=True) + raise + shutil.rmtree(pending_dir(root, session_id), ignore_errors=True) + return {"status": "committed", "path": final, "session_id": session_id} + + +def _read_jsonl(path): + rows = [] + if not os.path.exists(path): + return rows + with open(path, encoding="utf-8") as f: + for line in f: + try: + row = json.loads(line) + except ValueError: + continue + if isinstance(row, dict): + rows.append(row) + return rows + + +def _append_session_rows(path, session_id, new_rows): + """Atomic, idempotent append for one session; conflicting retries fail closed.""" + if not new_rows: + return {"path": path, "appended": 0, "status": "empty"} + existing = _read_jsonl(path) + same = [row for row in existing if row.get("session_id") == session_id] + old_set = {canonical(row) for row in same} + new_set = {canonical(row) for row in new_rows} + if same and old_set == new_set: + return {"path": path, "appended": 0, "status": "no-op"} + if same and not old_set.issubset(new_set): + raise SessionError(f"legacy projection conflict: {path} / {session_id}") + delta = [row for row in new_rows if canonical(row) not in old_set] + merged = existing + delta + text = "".join(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" for row in merged) + ledger.atomic_write_text(path, text) + return {"path": path, "appended": len(delta), "status": "projected"} + + +def _project_card(root, bundle, private_md): + date = (bundle.get("engine_state") or {}).get("date_end") or "undated" + suffix = bundle["session_id"].split("__")[-1] + path = os.path.join(root, "cards", f"{date}--{suffix}.md") + if os.path.exists(path): + with open(path, encoding="utf-8") as f: + if f.read() != private_md: + raise SessionError(f"legacy card conflict: {path}") + return {"path": path, "status": "no-op"} + ledger.atomic_write_text(path, private_md) + return {"path": path, "status": "projected"} + + +def project_legacy(root, bundle, private_md): + """Project a committed bundle into v1 files. Safe to rerun after interruption.""" + session_id = bundle["session_id"] + state = dict(bundle.get("engine_state") or {}) + commitment = bundle.get("commitment") + state["commitment"] = commitment + state["rule"] = (commitment or {}).get("rule") + ledger.atomic_write_text(os.path.join(root, "last_state.json"), pretty(state)) + + date_end = state.get("date_end") + log_row = { + "date_end": date_end, + "headline_dim": state.get("headline_dim"), + "commitment": commitment, + "metrics_snapshot": dict(state.get("metrics") or {}), + "session_id": session_id, + } + reports = [_append_session_rows(os.path.join(root, "log.jsonl"), session_id, [log_row])] + + thesis_updates = list(bundle.get("thesis_updates") or []) + reports.append(_append_session_rows(os.path.join(root, "theses.jsonl"), session_id, thesis_updates)) + reports.append(_append_session_rows(os.path.join(root, "thesis_decisions.jsonl"), session_id, + list(bundle.get("thesis_decisions") or []))) + + rule_rows = [] + if commitment and commitment.get("rule"): + suffix = session_id.split("__")[-1] + rule_rows.append({ + "rule_id": f"rule-{suffix}-0", + "text": commitment["rule"], + "metric_key": commitment.get("metric_key"), + "problem_key": PKEY.get(commitment.get("metric_key")), + "source": "user_chosen", + "status": "tracking", + "created": date_end, + "session_id": session_id, + }) + reports.append(_append_session_rows(os.path.join(root, "rules.jsonl"), session_id, rule_rows)) + + problems = [] + for event in state.get("problem_events") or []: + row = dict(event) + row["session_id"] = session_id + problems.append(row) + reports.append(_append_session_rows(os.path.join(root, "problems.jsonl"), session_id, problems)) + card_report = _project_card(root, bundle, private_md) + report = {"schema_version": 1, "session_id": session_id, "rows": reports, "card": card_report} + ledger.atomic_write_text(os.path.join(root, "projections", session_id + ".json"), pretty(report)) + return report + + +def load_committed(root, session_id): + path = session_dir(root, session_id) + if not os.path.isdir(path): + raise SessionError(f"committed session not found: {session_id}") + return read_json(os.path.join(path, "bundle.json")) + + +def repair_projections(root): + reports = [] + base = os.path.join(root, "sessions") + if not os.path.isdir(base): + return reports + for session_id in sorted(os.listdir(base)): + path = os.path.join(base, session_id) + if not os.path.isdir(path) or session_id.startswith("."): + continue + bundle = read_json(os.path.join(path, "bundle.json")) + with open(os.path.join(path, "card-private.md"), encoding="utf-8") as f: + reports.append(project_legacy(root, bundle, f.read())) + return reports diff --git a/skills/fomo-kernel/engine/thesis.py b/skills/fomo-kernel/engine/thesis.py new file mode 100644 index 0000000..bfb5b16 --- /dev/null +++ b/skills/fomo-kernel/engine/thesis.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Deterministic thesis reconstruction and add-decision validation. + +The agent may interpret motives and propose wording. This module owns the +append-only semantics and the evidence gate so "new evidence" cannot be emitted +without an explicit delta that future reviews can revisit. +""" +from __future__ import annotations + +import hashlib +import json +import os + + +ADD_DECISIONS = { + "planned_tranche", + "new_evidence", + "valuation_change", + "price_only", + "skip", +} + + +class ThesisError(ValueError): + pass + + +def read_jsonl(path): + rows = [] + if not path or not os.path.exists(path): + return rows + with open(path, encoding="utf-8") as f: + for line in f: + try: + row = json.loads(line) + except ValueError: + continue + if isinstance(row, dict): + rows.append(row) + return rows + + +def reconstruct_active(rows, active_cycle_ids=None): + """Return exactly one latest thesis per active position cycle. + + Non-thesis events (exit narratives and v2 thesis decisions) are ignored. + Append order is authoritative; a later revision replaces the earlier row for + the same cycle without erasing history. + """ + active_cycle_ids = set(active_cycle_ids or []) + latest = {} + for row in rows or []: + if row.get("event"): + continue + cycle_id = row.get("cycle_id") + if not cycle_id: + continue + latest[cycle_id] = row + if active_cycle_ids: + latest = {cid: row for cid, row in latest.items() if cid in active_cycle_ids} + return [latest[cid] for cid in sorted(latest)] + + +def _answer_map(answers): + rows = answers.get("answers") if isinstance(answers, dict) else None + if not isinstance(rows, list): + raise ThesisError("answers.answers must be an array") + out = {} + for row in rows: + if not isinstance(row, dict) or not row.get("question_id"): + raise ThesisError("every answer needs question_id") + if row["question_id"] in out: + raise ThesisError(f"duplicate answer: {row['question_id']}") + out[row["question_id"]] = row + return out + + +def validate_required_answers(plan, answers, allow_commitment_missing=False): + amap = _answer_map(answers) + missing = [] + allowed = {} + for q in plan.get("question_queue") or []: + options = {o.get("value") for o in q.get("options") or [] if o.get("value")} + allowed[q.get("id")] = options + if q.get("required") and q.get("id") not in amap: + missing.append(q.get("id")) + if missing: + raise ThesisError("missing required answers: " + ", ".join(missing)) + for qid, answer in amap.items(): + if qid not in allowed: + raise ThesisError(f"answer references unknown question: {qid}") + choice = answer.get("choice") + if choice not in allowed[qid]: + raise ThesisError(f"invalid choice for {qid}: {choice}") + if not allow_commitment_missing and not isinstance(answers.get("commitment"), dict): + raise ThesisError("answers.commitment is required before finalize") + return amap + + +def _decision_id(session_id, question_id, choice, payload): + canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True) + digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:10] + return f"decision-{session_id}-{question_id}-{choice}-{digest}" + + +def build_decision_events(plan, answers): + """Create auditable add-decision events and enforce evidence semantics.""" + amap = validate_required_answers(plan, answers, allow_commitment_missing=True) + events = [] + session_id = plan.get("session_id") + for q in plan.get("question_queue") or []: + if q.get("kind") != "add_thesis" or q.get("id") not in amap: + continue + answer = amap[q["id"]] + choice = answer.get("choice") + if choice not in ADD_DECISIONS: + raise ThesisError(f"unsupported add decision: {choice}") + evidence = answer.get("evidence_delta") + note = (answer.get("note") or "").strip() or None + if choice == "new_evidence": + if not isinstance(evidence, dict): + raise ThesisError(f"{q['id']}: new_evidence requires evidence_delta") + absent = [key for key in ("claim", "source") if not str(evidence.get(key) or "").strip()] + if absent: + raise ThesisError(f"{q['id']}: evidence_delta missing {', '.join(absent)}") + elif evidence is not None: + raise ThesisError(f"{q['id']}: evidence_delta is only valid with new_evidence") + if choice in {"planned_tranche", "valuation_change"} and not note: + raise ThesisError(f"{q['id']}: {choice} requires a short note") + event = { + "event": "thesis_decision", + "schema_version": 1, + "session_id": session_id, + "cycle_id": q.get("cycle_id"), + "ticker": q.get("ticker"), + "decision": choice, + "note": note, + "evidence_delta": evidence, + "review_date": (plan.get("engine_state") or {}).get("date_end"), + } + event["decision_id"] = _decision_id(session_id, q["id"], choice, event) + events.append(event) + return events + + +def validate_thesis_updates(rows, active_positions): + """Validate agent-authored thesis revisions against engine-owned cycle ids.""" + active_positions = active_positions or {} + valid_cycles = {p.get("cycle_id") for p in active_positions.values() if p.get("cycle_id")} + seen = set() + for index, row in enumerate(rows or []): + if not isinstance(row, dict): + raise ThesisError(f"thesis_updates[{index}] must be an object") + cycle_id = row.get("cycle_id") + if cycle_id not in valid_cycles: + raise ThesisError(f"thesis_updates[{index}] has unknown/inactive cycle_id: {cycle_id}") + if cycle_id in seen: + raise ThesisError(f"more than one thesis update for cycle: {cycle_id}") + seen.add(cycle_id) + if row.get("maturity") not in {"inferred", "testable", "draft"}: + raise ThesisError(f"thesis_updates[{index}] has invalid maturity") + for key in ("ticker", "why", "exit_trigger"): + if not str(row.get(key) or "").strip(): + raise ThesisError(f"thesis_updates[{index}] missing {key}") + return rows or [] diff --git a/skills/fomo-kernel/engine/trade_recap.py b/skills/fomo-kernel/engine/trade_recap.py index 2d752cc..aa3ef8c 100644 --- a/skills/fomo-kernel/engine/trade_recap.py +++ b/skills/fomo-kernel/engine/trade_recap.py @@ -9,6 +9,7 @@ """ import csv, os, re, sys, statistics, datetime as dt from collections import defaultdict, deque +import instruments as instrument_policy try: from rich.console import Console, Group from rich.panel import Panel @@ -970,13 +971,20 @@ def dim_size(rows, held, last_px): vals[t] = sh * px if px else cost tot = sum(vals.values()) or 1 weights = {t: v / tot for t, v in vals.items()} - max_t = max(weights, key=weights.get) if weights else None - max_pct = weights.get(max_t, 0) + # 配置型 ETF 是一籃子資產,不拿「單一公司部位上限」誤殺。產業/主題/槓桿 ETF + # 仍是集中風險;未知 ticker 保守視為 equity,不會因猜測而取得豁免。 + risk_weights = {t: w for t, w in weights.items() + if not instrument_policy.is_diversified_allocation(t)} + max_t = max(risk_weights, key=risk_weights.get) if risk_weights else None + max_pct = risk_weights.get(max_t, 0) sev = min(max((max_pct - 0.20) / 0.30, 0), 1) - others = [w for t, w in weights.items() if t != max_t] # 「其餘平均」要排除最大那檔,否則 mean(全部)=1/檔數、跟集中度無關,還會跟「最大佔 X%」自相矛盾 + others = [w for t, w in risk_weights.items() if t != max_t] # 「其餘平均」排除最大風險部位;配置型 ETF 不混入單一標的基準 return dict(dim="部位 sizing", tier=1, triggered=max_pct > 0.25, severity=sev, max_ticker=max_t, max_pct=max_pct, - avg_pct=statistics.mean(others) if others else 0.0, weights=weights) + avg_pct=statistics.mean(others) if others else 0.0, weights=weights, + risk_weights=risk_weights, + allocation_etfs={t: w for t, w in weights.items() + if instrument_policy.is_diversified_allocation(t)}) def meaningful_tickers(held, last_px, floor=RESIDUAL_POS_TH): """回傳「非殘倉」的 ticker set:市值佔全持倉 ≥ floor(預設 0.1%)。市值缺價用成本近似。 @@ -997,18 +1005,23 @@ def dim_diversify(held, last_px): px = (last_px or {}).get(t); vals[t] = sh * px if px else cost tot = sum(vals.values()) or 1 w = {t: v / tot for t, v in vals.items()} + risk_w = {t: wt for t, wt in w.items() + if not instrument_policy.is_diversified_allocation(t)} sec = defaultdict(float); ai = 0.0 - for t, wt in w.items(): + for t, wt in risk_w.items(): s, is_ai = driver(t); sec[s] += wt; ai += wt * is_ai classified_sec = {s: v for s, v in sec.items() if s != "未分類"} # 排除未分類桶,避免 driver_map 沒建好冒充集中度訊號(對齊 what_if() 既有作法) max_sec = max(classified_sec, key=classified_sec.get) if classified_sec else None max_sec_pct = classified_sec.get(max_sec, 0) - top3 = sum(sorted(w.values(), reverse=True)[:3]) + top3 = sum(sorted(risk_w.values(), reverse=True)[:3]) sev = min(max((max(max_sec_pct, ai) - 0.40) / 0.40, 0), 1) - trig = (len(w) >= 8 and max_sec_pct > SECTOR_MAX_TH) or top3 > 0.60 or ai > 0.60 + trig = (len(risk_w) >= 8 and max_sec_pct > SECTOR_MAX_TH) or top3 > 0.60 or ai > 0.60 return dict(dim="分散", tier=2, triggered=trig, severity=sev, n=len(w), + n_risk=len(risk_w), max_sector=max_sec, max_sector_pct=max_sec_pct, ai_pct=ai, - top3=top3, sectors=dict(sec)) + top3=top3, sectors=dict(sec), + allocation_etfs={t: wt for t, wt in w.items() + if instrument_policy.is_diversified_allocation(t)}) def dim_hold(rts): # B.4 修(2026-06-13):改判「同一檔內的時間框架一致性」,不再用整組合 IQR。 @@ -1103,6 +1116,11 @@ def dim_entry_style(rows, data, lookback=ENTRY_LOOKBACK): # ── 鏡片層(可換大師):洞的「規矩 + 引言」來自 lens 檔,engine 不 hardcode VY ── _LENS = None DEFAULT_LENS = os.path.join(os.path.dirname(__file__), "..", "rubric", "vincent-yu.lens.json") +LENS_DIM_ID = { + "出場紀律": "exit_discipline", "部位 sizing": "position_sizing", + "分散": "diversification", "持有時間": "holding_period", + "加碼攤平": "averaging_down", "alpha/beta": "alpha_beta", "進場": "entry_style", +} def load_lens(path=DEFAULT_LENS): """載入鏡片檔(規矩/引言/找動機問句)。換大師 = 換這個檔,engine 不動。""" global _LENS @@ -1126,8 +1144,9 @@ def load_lens(path=DEFAULT_LENS): } def card_for(dim): """(rule, quote):優先用 lens 檔(可換大師),載入失敗用 fallback。""" - if _LENS and dim in _LENS.get("dims", {}): - d = _LENS["dims"][dim]; m = _LENS.get("philosophy", "鏡片") + lens_dim = LENS_DIM_ID.get(dim, dim) + if _LENS and lens_dim in _LENS.get("dims", {}): + d = _LENS["dims"][lens_dim]; m = _LENS.get("philosophy", "lens") return d.get("rule", ""), f"{d.get('quote', '')}({m})" return CARD_LIB_FALLBACK.get(dim, ("", "")) @@ -1267,14 +1286,18 @@ def what_if(held, last_px, threshold=0.25): mv = {t: sh * last_px[t] for t, (sh, c) in held.items() if t in last_px} tot = sum(mv.values()) if tot <= 0: return None + risk_mv = {t: v for t, v in mv.items() + if not instrument_policy.is_diversified_allocation(t)} + if not risk_mv: + return None # 候選 1:AI thematic 全集(跨 sector) - ai_mv = sum(v for t, v in mv.items() if driver(t)[1] == 1) + ai_mv = sum(v for t, v in risk_mv.items() if driver(t)[1] == 1) ai_pct = ai_mv / tot # 候選 2:最大 sector(排除「未分類」,避免 driver map 沒載入時誤觸發) sector_mv = defaultdict(float) - for t, v in mv.items(): + for t, v in risk_mv.items(): sec = driver(t)[0] if sec != "未分類": sector_mv[sec] += v @@ -1285,7 +1308,7 @@ def what_if(held, last_px, threshold=0.25): max_sec, max_sec_mv, max_sec_pct = None, 0.0, 0.0 # 候選 3:最大個股 - max_t, max_t_mv = max(mv.items(), key=lambda x: x[1]) + max_t, max_t_mv = max(risk_mv.items(), key=lambda x: x[1]) max_t_pct = max_t_mv / tot # 候選清單(只收 ≥ threshold 的;label/mval/pct) @@ -1771,7 +1794,8 @@ def _px(t): def build_state(rows, rts, held, dims, overview, ab, rx, currency_meta=None, - avg_down=None, last_px=None, prev_end=None, cash=None): + avg_down=None, last_px=None, prev_end=None, cash=None, + portfolio_structure=None): """把這次復盤收斂成一張薄 JSON 狀態,給「下次對帳上次規矩」用(非給人看的卡)。 只在 main() 偵測 TR_STATE_OUT 時呼叫並寫出;不設 → 完全不執行,引擎行為零變。 設計依 requirements §4/§10: @@ -1785,6 +1809,8 @@ def build_state(rows, rts, held, dims, overview, ab, rx, currency_meta=None, d_size = dd.get("部位 sizing", {}) d_avg = dd.get("加碼攤平", {}) d_div = dd.get("分散", {}) + d_exit = dd.get("出場紀律", {}) + d_hold = dd.get("持有時間", {}) ab = ab if isinstance(ab, dict) else {} has_ab = not ab.get("note") # ab 帶 note = 無 pandas/價格/樣本 → 無 α/β credible = bool(ab.get("credible")) # v2(#80):統計顯著(≥1 年 + |t|≥1.96)才算,檔數閘退役 @@ -1837,6 +1863,7 @@ def build_state(rows, rts, held, dims, overview, ab, rx, currency_meta=None, return { "schema_version": 2, # currency_meta 為 optional 附加欄,舊讀者 .get 不受影響 "currency_meta": currency_meta, # #51/#129 PR-2a:聚合幣別/fx/分幣桶(單幣 USD → 大多為 None) + "portfolio_structure": portfolio_structure, # v2 orchestration P0:ETF 配置/集中語意 + metadata 缺口 "date_start": rows[0]["date"].isoformat() if rows else None, "date_end": rows[-1]["date"].isoformat() if rows else None, "n_trades": len(rows), @@ -1855,6 +1882,8 @@ def build_state(rows, rts, held, dims, overview, ab, rx, currency_meta=None, "max_sector_pct": d_div.get("max_sector_pct"), "top3_pct": d_div.get("top3"), "n_holdings": d_div.get("n"), + "exit_severity": d_exit.get("severity"), # v2 commitment:所有 headline 都有可跨期追蹤錨點 + "hold_severity": d_hold.get("severity"), "beta": ab.get("beta"), "alpha_ann": ab.get("alpha_ann"), # v2(#80):永遠出數;能力語氣由 alpha_credible 管 "alpha_t": (ab.get("alpha_stat") or {}).get("t"), # 不確定性一起存,對帳時才知道數字多可信 @@ -1874,7 +1903,8 @@ def build_state(rows, rts, held, dims, overview, ab, rx, currency_meta=None, } # ─────────────────── 結構化 card data(給 Claude 寫敘事卡用)─────────────────── -def build_honesty_ledger(overview, ab, data_integrity, currency_meta, cash=None, acct_perf=None): +def build_honesty_ledger(overview, ab, data_integrity, currency_meta, cash=None, acct_perf=None, + portfolio_structure=None): """聚合「卡面必須交代的誠實點」成一張清單(#82:機械強制取代 self-check 自律)。 只收『觸發的』揭露項 → 空 list = 這張卡沒有誠實缺口。判定條件對齊預設人話卡的 @@ -1955,12 +1985,18 @@ def build_honesty_ledger(overview, ab, data_integrity, currency_meta, cash=None, "at_cost_tickers": b.get("at_cost_tickers"), "fx_approx": b.get("fx_approx"), "cash_source": b.get("cash_source")}}) + # ETF 費用率 / tracking error 沒資料時不猜數字。只要這次有 ETF 且 metadata 不全, + # renderer 必須明說「尚未納入」;這是 P0 的誠實邊界,不是要把缺值補成 0。 + ps = portfolio_structure or {} + if ps.get("metadata_gaps"): + L.append({"key": "etf_metadata", "status": "partial", + "data": {"gaps": list(ps["metadata_gaps"])}}) return L def build_card_data(dims, strength, overview, best, worst, wi, rx, tdiag, ab, pa, master, data_integrity=None, currency_meta=None, cash=None, - acct_perf=None, pnl_curve_data=None): + acct_perf=None, pnl_curve_data=None, portfolio_structure=None): """組裝 SKILL Step 3「定論卡」要用的結構化資料(JSON,非給人看的卡)。 Claude 拿這 dict 用敘事方式寫成一段連貫卡(SKILL.md Step 3 鐵律:連貫敘事 ≠ dashboard 拼接); @@ -2022,9 +2058,11 @@ def build_card_data(dims, strength, overview, best, worst, wi, rx, tdiag, "dims_raw": dims, # 5 維 raw,Claude 用「一句人話」帶過其餘維 "data_integrity": data_integrity or {}, # 賣超/未分類 driver — 影響數據可信度,Claude 該主動提 "currency_meta": currency_meta, # #51/#129 PR-2a:聚合幣別/fx/分幣桶;None=單幣 USD 舊行為 + "portfolio_structure": portfolio_structure, # ETF P0:配置型豁免、集中 ETF 仍計風險、metadata 誠實缺口 "cash": cash, # #171 PR-1:帳戶現金(balance/weight/source/reliable/recent_net_deposit);reliable 才上 weight/入金判讀,無錨點靠 honesty 揭露 "acct_perf": acct_perf, # #171 B 路線:帳戶級 TWR/cash drag/IRR(daily 鏈式;{note} = 沒算,acct_twr=None+hold_twr 有值 = 現金 gate 只出持倉柱) - "honesty_ledger": build_honesty_ledger(overview, ab, data_integrity, currency_meta, cash, acct_perf), # #82:卡面必講的誠實點清單(空=無缺口);出卡前 gate 對照源 + "honesty_ledger": build_honesty_ledger(overview, ab, data_integrity, currency_meta, cash, acct_perf, + portfolio_structure), # #82:卡面必講的誠實點清單(空=無缺口);出卡前 gate 對照源 "pnl_curve": pnl_curve_data or {"note": "無資料"}, # #167:累積損益曲線,卡片畫 sparkline 用(一個點→一張圖);{'note':...} = 誠實降級,不硬畫 } @@ -2042,6 +2080,9 @@ def main(): master = load_lens() # 顯示用哲學名(去名,可換大師/哲學檔) dm = os.environ.get("TR_DRIVER_MAP") # Claude 生成的 driver map(冷門股分類) n_dm = load_driver_map(dm) if dm else 0 + im_result = instrument_policy.load_from_env() # 本機 ETF/instrument 覆寫;未知標的不猜 ETF + if im_result.get("error"): + print(f"⚠️ instrument map 載入失敗: {im_result['error']} — 改用保守 fallback", file=sys.stderr) n_adj = adjust_for_splits(rows, fetch_splits({r["ticker"] for r in rows})) # 分割調整,對齊今日價 rts, open_lots = round_trips(rows) _, avg_down = positions(rows) # avgdown 偵測留 avg cost(行為語意:買價 vs 平均持倉成本) @@ -2055,7 +2096,9 @@ def main(): n_fwd = adaptive_n_fwd(rows) # 觀察窗隨資料長度自適應 fwds, last_px = fwd_from_px(rts, px, n_fwd) last_px = last_px or {} # 離線/無價格 → {} 而非 None,讓下游(ticker_diagnosis 等)不 crash - decision_rts = [r for r in rts if driver(r["ticker"])[0] not in BENCH_SELF] # 再平衡/現金管理,非選股決策(=配置類,同 BENCH_SELF) + decision_rts = [r for r in rts + if driver(r["ticker"])[0] not in BENCH_SELF + and not instrument_policy.is_diversified_allocation(r["ticker"])] # 配置 ETF 再平衡/現金管理,非選股決策 # 多市場幣別(#51/#129 PR-2a):跨 ticker 聚合必須在共同幣別(USD)上做,否則台股 985 元 + 美股 985 美元 # 直接相加 = 靜默算錯。單一幣別組合(含純台股)聚合自洽 → 不抓匯率、路徑零變化。 cur_map, currencies, cur_conflicts = currency_map(rows) @@ -2063,7 +2106,9 @@ def main(): fx, fx_err = fetch_fx(currencies) if mixed_ccy else ({"USD": 1.0}, None) if mixed_ccy: rts_u, held_u, lastpx_u = usd_view(rts, held, last_px, cur_map, fx) - decision_rts_u = [r for r in rts_u if driver(r["ticker"])[0] not in BENCH_SELF] + decision_rts_u = [r for r in rts_u + if driver(r["ticker"])[0] not in BENCH_SELF + and not instrument_policy.is_diversified_allocation(r["ticker"])] else: rts_u, held_u, lastpx_u, decision_rts_u = rts, held, last_px, decision_rts # 殘倉過濾(#172):市值<0.1% 的部位不進分散度/what-if/per-ticker 診斷/未分類計數; @@ -2072,6 +2117,7 @@ def main(): held_dx = {t: v for t, v in held_u.items() if t in keep_dx} d_size = dim_size(rows, held_u, lastpx_u) d_exit = dim_exit(decision_rts, fwds, n_fwd); d_div = dim_diversify(held_dx, lastpx_u) + portfolio_structure = instrument_policy.portfolio_analysis(d_size.get("weights")) d_hold = dim_hold(rts); d_avgdown = dim_avgdown(avg_down, held_u, lastpx_u, d_size) dims = [d_exit, d_size, d_div, d_hold, d_avgdown] strength = dim_strength(d_exit, d_size, d_avgdown, d_div, d_hold, decision_rts) # 先給做對的(附案例) @@ -2187,7 +2233,8 @@ def main(): card = build_card_data(dims, strength, overview, best, worst, wi, rx, tdiag, ab, pa, master, data_integrity=data_integrity, currency_meta=currency_meta, cash=cash_data, - acct_perf=acct_perf, pnl_curve_data=pc) + acct_perf=acct_perf, pnl_curve_data=pc, + portfolio_structure=portfolio_structure) print(json.dumps(card, ensure_ascii=False, indent=2, default=str)) else: # 預設:乾淨人話卡(quickstart / fallback 用,#20 違規條目已砍) @@ -2245,7 +2292,7 @@ def main(): currency_meta=currency_meta, avg_down=avg_down, last_px=last_px, prev_end=os.environ.get("TR_PREV_END") or None, - cash=cash_data) + cash=cash_data, portfolio_structure=portfolio_structure) # TR_PREV_END=上次 review 的 date_end(SKILL 對帳模式傳入)→ behavior 型問題事件 # 只取其後的新交易(weekly 增量);不設 = 初診全期補齊,問題帳統計冷啟動。 outdir = os.path.dirname(os.path.abspath(path)) or "." diff --git a/skills/fomo-kernel/evals/evals.json b/skills/fomo-kernel/evals/evals.json new file mode 100644 index 0000000..3109362 --- /dev/null +++ b/skills/fomo-kernel/evals/evals.json @@ -0,0 +1,44 @@ +{ + "skill_name": "fomo-kernel", + "evals": [ + { + "id": 1, + "prompt": "Review this brokerage CSV. I added to one position after it was underwater. Do not give investment advice; help me distinguish genuinely new thesis evidence from reluctance to realize a loss, and finish with one card.", + "expected_output": "The agent runs prepare, asks every required motive question before preview, records an evidence-gated thesis decision, shows one private preview, lets the user choose one rule, and finalizes one canonical session.", + "files": ["mock/sample_value.csv"], + "expectations": [ + "The agent uses engine/review.py prepare rather than manually reproducing the long workflow.", + "No verdict card appears before every required question has an answer.", + "Choosing new_evidence requires evidence_delta.claim and evidence_delta.source.", + "All displayed numbers come from engine artifacts and the final card has one commitment at most.", + "Finalize produces an immutable session bundle and private/public card artifacts." + ] + }, + { + "id": 2, + "prompt": "My core allocation is SPY and bond ETFs, with a smaller QQQ and individual-stock sleeve. Review the trades without treating broad-market ETFs as single-stock concentration, but keep thematic ETF concentration visible.", + "expected_output": "The review applies the deterministic ETF policy: diversified allocation ETFs are excluded from single-name concentration while sector, thematic, and leveraged ETFs remain concentration risk; metadata gaps are disclosed rather than guessed.", + "files": ["mock/mock_trades.csv"], + "expectations": [ + "Broad-market, regional, bond, and commodity ETFs may receive only the explicit allocation exemption.", + "Sector, thematic, and leveraged ETFs remain in sizing, top-three risk, and stress diagnostics.", + "Unknown tickers do not receive an ETF exemption.", + "Missing expense-ratio or tracking-error metadata is disclosed and never treated as zero.", + "The output reviews behavior and process without recommending a security to buy or sell." + ] + }, + { + "id": 3, + "prompt": "Run this review in English for a friend overseas. Keep my full card private, but also prepare a version I can post publicly without exposing my portfolio size, dates, holdings, or exact weights.", + "expected_output": "The same engine facts are rendered through English copy into a full local private card and a separately rendered public card with strict redaction.", + "files": ["mock/sample_fundamental.csv"], + "expectations": [ + "The Review Plan, questions, selected rule, and private card are in English.", + "English mode uses the same engine card/state schemas and analysis policy as zh-TW.", + "The public card contains no amounts, dates, tickers, exact weights, session id, or agent free text.", + "The private card preserves engine-derived P&L, payoff, best/worst trades, top hole, honesty disclosures, and one rule.", + "Only the public card is offered for posting; the private artifact remains local." + ] + } + ] +} diff --git a/skills/fomo-kernel/flows/first-review.md b/skills/fomo-kernel/flows/first-review.md new file mode 100644 index 0000000..b9ffa7d --- /dev/null +++ b/skills/fomo-kernel/flows/first-review.md @@ -0,0 +1,19 @@ +# First review flow + +Use when the Review Plan has `route=first_review`. + +1. Explain in one or two sentences that the engine computed the numbers locally and that the review needs motive confirmation before it can produce a conclusion. +2. Ask every required question in `question_queue` in order. Use a native option UI when available; otherwise present the same options as text. Do not merge questions or replace their meaning. +3. Create an inferred thesis for every entry in `missing_thesis_positions`. Include at least: + - `ticker` and the unchanged `cycle_id` + - `why`: the fact or expectation that may not be priced in, or an honest placeholder such as "averaging down while waiting to recover; confirmation needed" + - `horizon`: weeks, quarters, or years; use null when no reasonable inference is possible + - `exit_trigger`: a factual condition that would falsify the thesis, not a stop-loss price + - `stop`, `target_size`, and `driver` + - `maturity:"inferred"` plus the inference source; never present it as user-confirmed +4. Keep the narrative qualitative. Write `headline` and `mirror`; optionally add `counterfactual`, `strength`, and `rule_rationale`. Do not include digits. +5. Run preview. If validation fails, fix the artifact described by the error; do not bypass the gate. +6. Show the private preview and ask the user to choose one candidate rule, provide a custom rule, or skip. +7. Write the choice to `answers.commitment`, then finalize. Return the private card. Return the public card only when the user asks to share it. + +Success means that a canonical session is committed and the user sees one card. Projection errors are repairable and must not be described as session loss. diff --git a/skills/fomo-kernel/flows/snapshot-review.md b/skills/fomo-kernel/flows/snapshot-review.md new file mode 100644 index 0000000..f977014 --- /dev/null +++ b/skills/fomo-kernel/flows/snapshot-review.md @@ -0,0 +1,16 @@ +# Snapshot review flow + +Use when only a position snapshot is available and complete transaction history is not. + +The snapshot adapter must first convert the snapshot into an engine card and state, then call: + +```bash +python3 engine/review.py prepare --route snapshot_review \ + --card-json --state-json +``` + +Discuss only claims supported by the snapshot: cost weights, single-position risk, driver concentration, ETF structure, and data integrity. Do not infer averaging-down counts, exit discipline, win rate, payoff ratio, or historical motives. + +Create an inferred thesis for every open cycle so a later transaction import can reconcile against it. State clearly that this is an opening portfolio check and invite the user to provide transaction history later to unlock behavioral diagnostics. + +The remaining lifecycle matches first review: required questions, thesis updates, qualitative narrative, preview, one commitment, and finalize. diff --git a/skills/fomo-kernel/flows/test-drive.md b/skills/fomo-kernel/flows/test-drive.md new file mode 100644 index 0000000..e97c9ba --- /dev/null +++ b/skills/fomo-kernel/flows/test-drive.md @@ -0,0 +1,9 @@ +# Test-drive flow + +Use when the user has no data and wants to see the product experience. + +`prepare --test-drive` uses repository mock data. The Review Plan must have `route=test_drive` and `persist:false`. Run the complete required-question, preview, and one-rule lifecycle so the test drive demonstrates the real workflow rather than a static sample. + +Label every conversation and card clearly as demo data. Do not read from or project into the user's production `~/.trade-coach` state, and never mix demo theses into production memory. + +Return the private demo card. Return the public demo card only when the user asks for a shareable version. diff --git a/skills/fomo-kernel/flows/weekly-review.md b/skills/fomo-kernel/flows/weekly-review.md new file mode 100644 index 0000000..08c550c --- /dev/null +++ b/skills/fomo-kernel/flows/weekly-review.md @@ -0,0 +1,13 @@ +# Weekly review flow + +Use when the Review Plan has `route=weekly_review`. + +1. Read `state_snapshot` from the Review Plan. Do not scan the entire `~/.trade-coach` directory. +2. Begin the interpretation by reconciling against `prior_commitment`. Displayed numbers still come from renderer-owned engine state; the agent must not compute a delta. +3. Ask only items in `question_queue`. Prepare already deduplicated them against active theses and add counts; do not ask raw engine `thesis_questions` again. +4. Treat every `missing_thesis_positions` item as a new cycle or a historical thesis gap and fill it using the inference-first contract from the first-review flow. +5. Classify each losing-position add as `planned_tranche`, `new_evidence`, `valuation_change`, `price_only`, or `skip`. A `new_evidence` choice must include an evidence delta so the next review can examine it as a thesis event. +6. Focus the narrative on movement against the previous rule and the largest new behavioral leak. Do not produce a complete dashboard. +7. After preview, let the user choose only one rule. Finalize atomically; update legacy state only through projections. + +Do not ask for an already confirmed motive every week. Prepare should requeue it only for a new cycle, new behavior, or an inferred answer that remains the largest contradiction. diff --git a/skills/fomo-kernel/mock/SAMPLES.md b/skills/fomo-kernel/mock/SAMPLES.md index bbe6163..6c72956 100644 --- a/skills/fomo-kernel/mock/SAMPLES.md +++ b/skills/fomo-kernel/mock/SAMPLES.md @@ -1,142 +1,61 @@ -# 交易風格測試用例(sample fixtures) +# Mock portfolio fixtures -**虛構**交易紀錄,各自模擬一種投資者畫像(風格 × 持有長度),用來測 engine 的 5 維診斷能不能把每種風格**最該被照出的洞**排到復盤卡最前面。和 `mock_trades.csv`(方法論建立期那個人)並列,都是假資料、可入 git。 +These synthetic long-only BUY/SELL CSV files exercise stable behavioral branches. They contain no real user data. Driver-map files provide deterministic sector and theme classification for less common tickers. -目前共 **12 組**:三組散戶風格基準(fundamental / momentum / value)+ 四組投資者畫像擴充(ai_holder / oldecon / swing / day_trader,2026-06-30 經 Claude+Codex+Gemini 三方 review 定稿,見下方「投資者畫像擴充」)+ 五組 engine 邊界情境擴充(pyramid / insufficient / noisy_broker / rotator / panic_seller,2026-07-04,見下方「engine 邊界情境擴充」)。 - -範圍限定:全部是**現股 long-only**(BUY/SELL),不含選擇權/賣空——engine 本身只認 `RecordType=="Trade"` 且 `Action in ("BUY","SELL")`([trade_recap.py:105-108](../engine/trade_recap.py#L105)),沒有做空/選擇權的計算邏輯,造這類 fixture 對回歸沒有增益。 - -## 怎麼跑 - -每組附一個 `driver_map.json`(SKILL Step 0.5:讓 engine 對實際持倉用正確 sector/主題分類,冷門股不失準),用環境變數餵進去: +## Run a fixture ```bash cd skills/fomo-kernel TR_DRIVER_MAP=mock/sample_fundamental.driver_map.json python3 engine/trade_recap.py mock/sample_fundamental.csv -TR_DRIVER_MAP=mock/sample_momentum.driver_map.json python3 engine/trade_recap.py mock/sample_momentum.csv -TR_DRIVER_MAP=mock/sample_value.driver_map.json python3 engine/trade_recap.py mock/sample_value.csv -# 投資者畫像擴充 -TR_DRIVER_MAP=mock/sample_ai_holder.driver_map.json python3 engine/trade_recap.py mock/sample_ai_holder.csv -TR_DRIVER_MAP=mock/sample_oldecon.driver_map.json python3 engine/trade_recap.py mock/sample_oldecon.csv -TR_DRIVER_MAP=mock/sample_swing.driver_map.json python3 engine/trade_recap.py mock/sample_swing.csv -TR_DRIVER_MAP=mock/sample_day_trader.driver_map.json python3 engine/trade_recap.py mock/sample_day_trader.csv -# engine 邊界情境擴充 -TR_DRIVER_MAP=mock/sample_pyramid.driver_map.json python3 engine/trade_recap.py mock/sample_pyramid.csv -TR_DRIVER_MAP=mock/sample_insufficient.driver_map.json python3 engine/trade_recap.py mock/sample_insufficient.csv -TR_DRIVER_MAP=mock/sample_noisy_broker.driver_map.json python3 engine/trade_recap.py mock/sample_noisy_broker.csv -TR_DRIVER_MAP=mock/sample_rotator.driver_map.json python3 engine/trade_recap.py mock/sample_rotator.csv -TR_DRIVER_MAP=mock/sample_panic_seller.driver_map.json python3 engine/trade_recap.py mock/sample_panic_seller.csv +TR_DRIVER_MAP=mock/sample_momentum.driver_map.json python3 engine/trade_recap.py mock/sample_momentum.csv +TR_DRIVER_MAP=mock/sample_value.driver_map.json python3 engine/trade_recap.py mock/sample_value.csv ``` -> ⚠️ 數字會漂移:engine 用 yfinance 抓**真實歷史價 + 最新收盤**算 α/β、市值權重、套牢。標的代碼與日期都真實(2023–2024),所以重跑時絕對數字會隨當前股價變,但**每組設計觸發的「頭號洞」是穩定的**(由交易行為決定,不靠特定股價)。 - -## 這些 CSV 的角色:測試 fixture,不是 demo 模式(#89) - -engine 已移除 `is_demo` 檔名嗅探(#89):**輸入路徑含不含 `mock` 都走同一條 call**,同一份輸入任何人跑都得到同一結果,輸出沒有任何 demo 分支。 +Use the matching `sample_.driver_map.json` for other personas. The engine treats a fixture exactly like any other input; it does not infer demo mode from a filename. Test-drive labeling and persistence isolation belong to `review.py prepare --test-drive`. + +Live market-dependent values may drift because online runs fetch historical and latest prices. The primary behavioral branch for each fixture must remain stable and is covered by offline tests. + +## Baseline personas + +| Fixture | Behavior design | Expected primary branch | +|---|---|---| +| `sample_fundamental.csv` | Diversified, moderate positions, sells winners sooner than losers | exit discipline | +| `sample_momentum.csv` | Concentrated AI/semiconductor exposure, large positions, short holds | sizing and driver concentration | +| `sample_value.csv` | Repeated adds to losing positions and small realized gains | losing-position adds, then sizing | + +## Extended personas + +| Fixture | Behavior design | Expected primary branch | +|---|---|---| +| `sample_ai_holder.csv` | Long-duration exposure to several tickers sharing one AI narrative | driver concentration | +| `sample_oldecon.csv` | Diversified traditional sectors with restrained sizing | strength-first clean baseline | +| `sample_swing.csv` | Short winners and much longer losing holds in the same instruments | inconsistent holding horizon | +| `sample_day_trader.csv` | Same-day entries and exits across several tickers | overtrading/holding period | + +`sample_ai_holder.csv` can shift between diversification and sizing in live-price runs because a large winner changes market-value weights. Both conclusions describe the same underlying single-narrative concentration. Offline regression uses deterministic cost-basis behavior. + +## Engine boundary fixtures + +| Fixture | Boundary under test | Key expectation | +|---|---|---| +| `sample_pyramid.csv` | Adds only to winning positions | must not be labeled averaging down | +| `sample_insufficient.csv` | Fewer than three round trips and a short span | commitment remains null by default | +| `sample_noisy_broker.csv` | Dividends, transfers, fees, and reinvestment rows | behavior matches the clean baseline after filtering | +| `sample_rotator.csv` | Full-position rotation through unrelated hot themes | sequence exposes theme churn even when current snapshot is simply concentrated | +| `sample_panic_seller.csv` | Several long-held losing positions exited in one stress window, followed by a higher re-entry | extreme exit-discipline branch | +| `sample_tw_mixed.csv` | Taiwan and US instruments with multiple currencies | per-market benchmark and aggregate-currency contracts | + +## Fixture design rules + +- Make one behavioral leak dominant and keep unrelated dimensions controlled. +- Use real tickers and historically plausible dates/prices when online price paths matter. +- Avoid delisted instruments and uncontrolled corporate actions. +- Keep both sides of a synthetic round trip on the same side of a split date whenever possible. Offline runs do not fetch split history, while online runs do; cross-split fixtures cannot be naturally scaled in both modes. +- If a split is intentionally tested, document the nominal versus split-adjusted representation and cover both offline and online assumptions explicitly. +- Keep duplicate rows distinguishable. The loader deduplicates on symbol, side, quantity, price, and date. +- Prefer differential assertions for noisy-input fixtures: their output should match the equivalent clean fixture. +- Keep metadata about simulation and state isolation outside the rendered card. + +## Privacy -- **persona 模擬(測「真實用戶會看到什麼」)**:CSV 放哪都行,卡面 = 真實形態。測試元信息(狀態隔離到哪、這是模擬)只留在**對話層**跟作者講,一個字都不准上卡:卡上出現作者視角 = 模擬穿幫,測不到真實體驗。 -- **給沒資料的用戶體驗 = 呈現層的事**:靜態長相 → README 範例卡(#46);想走一遍流程 → SKILL「試駕模式」(#53):`mock_trades.csv` 走完整四步,但 Step 2 標明演練、狀態只進 temp 不碰 `~/.trade-coach/`、卡標「示範」——失真警告由呈現層扛,引擎不設任何 demo 分支(#89)。 - -## 三組設計意圖 - -| 檔案 | 模擬風格 | 行為設計 | engine 應排第一的洞 | 對應鏡片動機問句 | -|---|---|---|---|---| -| `sample_fundamental.csv` | **基本面選股** | 跨 6 產業真分散(醫療/消費/金融/能源/科技/工業)、單筆 ≤18% 不梭哈、賺一點就賣好公司、賠錢的基本面股死抱等回本 | **出場紀律**(處置缺口 +258 天:賺錢抱 ~120 天就跑、賠錢抱 ~378 天)、β≈0.6 低波動 | winner 賣太早 / 賠錢死抱 → D1 時間軸、G1 焦慮 vs 判斷 | -| `sample_momentum.csv` | **動能衝衝衝** | 全押 AI/半導體、單檔梭哈、4~18 天短進短出、追熱門題材 | **部位 sizing**(單檔 >40%)+ **假分散**(AI 暴險 100%、同一 driver)、β≈2.2 把 beta 當 alpha | 梭哈 → B1 賠率/A1 sizing;假分散 → B2 driver;贏大盤靠賽道 → E2 beta vs alpha | -| `sample_value.csv` | **只買便宜估值** | 越跌越攤平(INTC 49→20、CVS、PYPL)、套牢死抱不認賠、只實現小賺(CVX/F) | **加碼攤平**(6 次虧損加碼、5 次破 25% 上限)+ **部位 sizing**(凹單把 INTC 養成 43% 重倉) | 虧損中加碼 → A2 試探≠加碼、G 不想認賠:「INTC 從 45 一路加到 20,是看好還是不想認賠?」 | - -## 設計重點(為什麼這樣造) - -- **每組只讓「一種洞」壓倒性勝出**,其餘維度刻意守住,確保 engine 的「抓大放小」排序選對。例:基本面組部位/分散/攤平全綠,只有出場紀律 sev=1.00。 -- **真實標的 + 真實日期**:這樣 yfinance 才抓得到價,出場紀律(賣出後續漲)、α/β 歸因、套牢才算得出來——這幾維是引擎的價值核心,不能用假代碼跳過。 -- **避開拆股/退市的失真標的**:早期版本用過 SMCI(2024 拆股)、WBA(2024 退市)會讓「套牢 -96%」「404」這種假訊號污染診斷,已換成 AMAT/MRVL、CVS。**NVDA(day_trader/momentum 兩組)是例外保留**:AI 分散度/主題暴險敘事需要它,拆股前(2024-06-10 之前)交易改填當時真實名目價位(如 $830-950 區間),讓 `adjust_for_splits()` 事後調整算對,而非誤填今日拆股後等值價造成雙重縮放(issue #93)。`mock_trades.csv` 的 NVDA 同屬此例外,但更特殊——它是**跨拆股長倉**(拆股前 2024-01-12 / 03-11 建倉、拆股後 08-06 賣、12-03 加碼):兩筆建倉填名目價 `15@$500`、`35@$620`,線上 `adjust_for_splits()` 換回今日等值 `150@$50`、`350@$62` 後正確(#98 只修了 day_trader/momentum、漏此檔,後補)。惟跨拆股長倉在**離線**(無 yfinance / 抓價失敗 → `splits={}` 不調整)時,拆股前名目買價會與拆股後等值賣價尺度錯配,NVDA round-trip 被翻成假虧——注意 engine **無卡級『無價格』旗標**攔截(僅依賴日線的 α/β 等維會各自標無價格),已實現損益、盈虧比、出場紀律維全部照跑、靜默吸收假虧,另生成 150 股假 `orphan_sells`(頭號洞倒仍穩在 sizing+分散、不隨之跑掉)。此為 FIFO 賣出 clamp 對跨拆股長倉在降級定價下的固有性質、非本檔特有 bug,線上 demo 主路徑(10:1 拆股正確套用)不受影響;但**離線跑 mock_trades 的損益與行為診斷不可信、勿據以判讀**。2026-07-13 第 **4** 實例 = `sample_ai_holder`(NVDA×2/AVGO 誤填拆後價,#171 帳戶級 TWR sweep 的 +8663% 假暴漲才現形,α 面板同中毒 +896pp 一直沒人看見;已修:買入改名目價、賣出移到拆股日前)。由此提煉**新 fixture 鐵律:拆股檔的買賣要同側(不跨拆股日)**——離線測試不跑 `adjust_for_splits`、線上跑,跨拆股的 round-trip 無法兩個世界同時自洽(mock_trades 是歷史例外、勿仿,其離線 caveat 如上)。 -- **估值組的連貫敘事**:凹單(加碼攤平)直接導致 INTC 變成 43% 重倉(部位失控)——一條因果線串起兩個洞,正是 value trap 的死亡螺旋,不是兩個獨立缺陷。 - -## 預期鏡片復盤卡一句話(人話版) - -- **基本面**:「你買得好(α 正、低 β、真分散),但賺錢的抱 120 天就跑、賠錢的抱 378 天等回本——處置效應在替你做決定。」 -- **動能**:「你贏大盤 +119pp,但 β 2.2、AI 暴險 100%、單檔 41%——你押對的是賽道不是選股,而且一次回檔 30% 就 -$18k。」 -- **估值**:「你 6 次往虧損倉加碼、把 INTC 凹成 43% 重倉——便宜不是買進理由,『不想認賠想攤低等回本』才是。」 - ---- - -# 投資者畫像擴充(2026-06-30) - -在三組散戶風格基準之上,再造四組**投資者畫像**——刻意用「風格 × 持有長度」拉開光譜: -從長抱一年半的 AI 信徒,到同日進出的當沖客。設計經 **Claude + Codex + Gemini 三方 review** 兩輪定稿 -(第一輪審引擎機制、第二輪審策略真實度),Codex 實際跑 engine 驗算頭號洞排序、標的價格對齊 2024 真實區間。 - -## 四型畫像 - -| 檔案 | 投資者畫像 | 持有長度 | 行為設計 | engine 應排第一的洞 | 對應鏡片動機問句 | -|---|---|---|---|---|---| -| `sample_ai_holder.csv` | **AI 長期投資者** | 約 1.5 年(493–575 天) | 2023 起重押 AI 龍頭(NVDA/MSFT/GOOGL/AVGO/PLTR/TSM)、長抱、漲著順勢加碼、偶爾長抱後減碼 | **分散**(假分散:AI 暴險 100%、單一敘事);次要洞=NVDA 重倉 33% | 「你以為買 6 檔很分散,其實都押同一個 AI 敘事——一起漲一起跌,一次 AI 寒冬就是 −50%」 | -| `sample_oldecon.csv` | **傳統產業投資者** | 數月 | 只買老經濟(能源/金融/工業/必需消費/公用/醫療)、跨 6 sector 真分散、低 β、不梭哈、不攤平、賺賠持有期相近 | **(無洞)→ 揚長卡** | 紀律乾淨基準:照不出洞時卡片該怎麼講「你守住了什麼」 | -| `sample_swing.csv` | **快進快出(短波段)** | 不一致(2–45 天) | 同一檔有時 2–3 天快停利(賺就跑)、有時套住就凹成 40+ 天(賠的拖著) | **持有時間**(框架不一致 incon_rate=1.0);次要洞=出場紀律 | 「同一檔 SHOP,你有時抱 3 天有時抱 44 天——你到底是短打還是長線?沒有框架就沒有紀律」 | -| `sample_day_trader.csv` | **當沖交易者** | 0 天(同日進出) | 同日 BUY+SELL、多檔輪動、賺賠當天結(stylized:單日 1–2 筆,示意當沖框架非真實 HFT 頻率) | **持有時間**(過度交易:中位持有 0 天) | 「中位持有 0 天、14 筆同日沖——這個頻率需要的 edge,你的勝率撐得起嗎?還是在繳手續費」 | - -> **動能者**已由既有 `sample_momentum.csv` 涵蓋(全押 AI/半導體、單檔梭哈、4–18 天短進短出、頭號洞=部位 sizing),不重造。 -> 五型畫像 = ai_holder(長線信徒) + oldecon(保守長抱) + swing(波段) + day_trader(當沖) + momentum(動能)。 - -## 設計重點(為什麼這樣造 / 三方 review 的關鍵發現) - -- **頭號洞必須由「與最新股價無關」的訊號決定**(driver flag / 成本基礎權重 / 純日期),才能離線確定性測試、 - 不因 yfinance 即時價漂移而 flaky。各型的回歸斷言見 `tests/test_sample_styles.py`。 -- **AI 長期 vs 動能同樣 AI 暴險,但靠持有長度區隔**:AI 長期是長抱(median >200 天、hold 維守綠)+ 主題集中(分散當頭號); - 動能是短進短出(median <15)+ 單檔梭哈(sizing 當頭號)。 -- **ai_holder 線上頭號洞會在「分散↔sizing」間漂移,但同屬 AI 過度集中**:離線測試(成本基礎)斷言 **分散**; - 但線上跑時 NVDA 自 2023 大漲 ~8 倍 → 市值佔比膨脹到 ~73% → 卡片可能改以 **部位 sizing** 當頭號。 - 兩者講的是同一課(身家壓在單一 AI 敘事),故事不變;回歸測試以確定性的「分散」為準。 -- **`top3` 不進 severity**(Codex 跑碼證實):分散維的 severity 只看 `max(max_sector_pct, ai_pct)`。 - 所以 ai_holder 必須 **ai_pct≈1.0** 才排得上頭號,光靠 top3 紅燈分數趨近 0。 -- **sizing vs 分散 的臨界 = max_pct 0.41**(且平手時 dims 順序讓 sizing 先贏):ai_holder 的龍頭股 - 刻意控在 **33%**(<0.41),確保「分散」(0.70 分)穩穩壓過「sizing」(0.43 分)當頭號。 -- **AI 長期需 ≥3 筆已實現 round trip**:engine 在 `len(round_trips) < 3` 時標 `insufficient`、不出 commitment。 - 所以這型安排了長抱後的減碼(NVDA/TSM/PLTR/AVGO),既講「長線信徒」又讓 engine 完整運作。 -- **swing 不能靠「賣太早」當頭號**:`winner_early`/`avg_forgone` 需 yfinance,離線測不出 → 改用 - **「持有時間框架不一致」**(同檔又 <5d 又 >30d)當穩定頭號,離線可斷言。 -- **當沖機械路徑**(Codex 驗證):同日 BUY 行排在 SELL 行前 → `round_trips` 配成 hold=0;當天全平倉 → - `held` 為空 → sizing/分散/攤平全失效,只剩「持有時間 overtrading」當穩定頭號。 -- **dedup 陷阱**:`load()` 去重鍵 = `(symbol, side, qty, price, date)`。當沖/高頻同日重複腿必須讓 - qty 或 price 有差異,否則交易會被默默吃掉——本 fixture 已讓每筆買賣價不同。 -- **傳統產業 = 乾淨基準**:刻意讓五維全綠,補上既有 fixture 全缺的覆蓋——「沒有洞時卡片走揚長路徑」。 - 與既有 `sample_fundamental`(同樣老牌穩健但頭號=出場紀律)互補,避免兩者撞同一頭號洞。 -- **第二輪策略審修正**(2026-07-01,三方審策略真實度後):① 標的成交價全部對齊 2024 真實歷史區間 - (yfinance 複查 ≤±12%)——原 TSLA/SHOP/UBER 偏離 20–48% 已修;② `SQ` 已改名退市(Block→XYZ, - yfinance 抓無資料)→ 換成 `PYPL`,遵守「避開拆股/退市失真標的」原則;③ oldecon 補第 6 個 sector - (醫療 JNJ)讓「跨 6 sector」名實相符;④ swing 的長抱(40+ 天)全部改為虧損出場,讓「賺的快跑、 - 賠的拖著」在資料上成立(原本 SHOP 長抱那筆是賺錢,敘事對不上);⑤ 誠實標注 day_trader 是 - stylized(單日 1–2 筆)、非真實 HFT 頻率。 - -## 預期鏡片復盤卡一句話(人話版) - -- **AI 長期**:「你信 AI 信了一年半也賺了一年半,但你 6 檔全是同一個敘事、AI 暴險 100%——這不是分散,是一注押更大。」 -- **傳統產業**:「沒有洞要照——你跨 6 個產業、最重一檔 16%、賺賠都抱得住。守住紀律本身就是答案。」 -- **快進快出**:「同一檔 SHOP 你抱過 3 天也抱過 44 天——賺的快跑、賠的拖著,你缺的不是進出點是時間框架。」 -- **當沖**:「14 筆同日進出、中位持有 0 天——先問自己:這個頻率需要的 edge,你真的有嗎?」 - ---- - -# engine 邊界情境擴充(2026-07-04) - -前兩批都在造「畫像」——真人交易者的行為模式。這批不再造新畫像,改造 **engine 判斷分支本身**的邊界情境: -每一支既有鐵律(A-10 樣本不足 / 攤平 vs 加碼贏家的方向判斷 / CSV 雜訊過濾)都該有一組 fixture 專門去戳它, -而不是只能等真人交易者剛好撞上才發現。範圍限定現股 long-only(見上方「範圍限定」)。 - -| 檔案 | 模擬情境 | 行為設計 | 測的 engine 分支 | 回歸斷言 | -|---|---|---|---|---| -| `sample_pyramid.csv` | **金字塔加碼者** | 只在浮盈時加碼(COST 550→600→650 一路買高、UNH 480→520),從未在虧損時加碼,最終部位轉重倉 | `dim_avgdown` 必須靠「買價 < 均價×0.9」判斷方向,不能把「越漲越加碼」誤判成攤平 | `tests/test_sample_styles.py::test_pyramid_top_hole_is_sizing_not_avgdown` — 頭號洞=部位 sizing、`avgdown.count==0`、`classify_adds` 分類為疑似定投而非疑似凹單 | -| `sample_insufficient.csv` | **樣本不足者** | 只有 2 個 round-trip(AAPL、MSFT 各一組買賣),交易跨度 41 天(<`MIN_SPAN_DAYS`=84) | `build_state()` 的 `insufficient = len(rts) < 3 or span_days < MIN_SPAN_DAYS` gate | `test_insufficient_sample_blocks_commitment` — 直接跑 `build_state()` 驗證 `insufficient_data=True` 且 `commitment=None`(對應 eval-design.md A-10) | -| `sample_noisy_broker.csv` | **CSV 雜訊版** | 複製 `sample_oldecon.csv` 的交易列,插入股息/轉帳/利息/帳戶手續費/股息再投資等非典型列(`RecordType` 非 Trade,或 `Action="REINVEST"`) | `load()` 的兩道過濾:`RecordType!="Trade"` 續行、`Action not in (BUY,SELL)` 續行 | `test_noisy_broker_csv_matches_clean_baseline` — 解析後應與乾淨版 `oldecon` 五維結果逐一比對相同(差分斷言,雜訊必須被完全濾除) | -| `sample_rotator.csv` | **輪動追熱點者** | 依序全倉重壓 4 個不同熱門賽道(AI半導體→生技→能源→電動車),每個都在 30–40 天內清倉才換下一個,最後重壓最新熱點(金融科技) | `dim_size`/`dim_diversify` 在單一持倉快照下天然觸發集中;真正的區隔訊號是 round-trip 標的的 driver **逐輪全換不重複**,對照 momentum 的『同一 driver 反覆押注』 | `test_rotator_top_hole_is_sizing_via_theme_churn` — 頭號洞=部位 sizing、持有天數落在 20–60 天(介於 momentum 的 <15 與 ai_holder 的 >200 之間)、4 個 round-trip 的 driver 兩兩不同 | -| `sample_panic_seller.csv` | **恐慌全出者** | 3 檔虧損倉長抱 500+ 天,某個真實有過大盤重挫的那週(2024-08-05~07)同時全數認賠出清;幾個月後追高買回其中一檔 | `dim_exit` 的處置缺口公式在『多檔同週恐慌出清』下天然放大到極端值;`disp_gap` 沒有專門的『恐慌同步』訊號,靠日期群聚(plain check)佐證 | `test_panic_seller_extreme_disposition_and_chase_back` — 頭號洞=出場紀律、處置缺口 >300 天(比 fundamental 的 +258 天更極端)、3 檔虧損倉出清日期落在 ≤5 天窗口內、同一檔追高買回價格 >恐慌賣出價 105% | - -## 設計重點 - -- **這批不是新畫像,是新故障模式**:三支都對應一條既有鐵律的「反例」——如果沒特意造,這些分支只能等真人資料剛好踩到才會被驗證到,而 A-10 這種 gate 條件本來就很少在既有 7 組畫像的正常交易量下被觸發。 -- **金字塔 vs 攤平的方向對照**:`sample_value.csv`(既有)是「往虧損倉加碼」,`sample_pyramid.csv`(新)是「往獲利倉加碼」——同樣是「多次加碼同一檔」的交易表面模式,但 engine 的 `dim_avgdown` 只認買價相對均價的方向,不能靠加碼次數本身判斷,兩組刻意對照確保這個方向判斷沒有被次數污染。 -- **`sample_insufficient.csv` 直接跑 `build_state()`,不只斷言 gate 條件成立**:先前 fixture 都只驗 `dim_*` 純函式,這組因為要測的是 `insufficient_data`/`commitment` 這兩個只存在於 `build_state()` 輸出的欄位,測試因此改為直接呼叫 `build_state()` 全鏈路,而非只驗證 `len(rts)<3` 這個中間條件本身。 -- **`sample_noisy_broker.csv` 用差分斷言而非獨立斷言**:不另外斷言「頭號洞是什麼」,而是要求跟乾淨版 `oldecon` 逐維 diff 相同——這樣任何未來對雜訊列的誤判(哪怕只影響一維的 severity 小數點),都會在差分裡現形,比各自獨立斷言更敏感。 -- **`sample_rotator.csv` 頭號洞跟 momentum 撞維,靠序列訊號(不是 dim 本身)區分**:engine 的 5 維只看「當下持倉快照」,追熱點者清倉重壓下一個賽道後,快照必然是單一新持倉(集中度天然滿分)——跟 momentum 表面同一種頭號洞形狀。真正的行為差異(每次都換賽道 vs 從頭到尾同一賽道)沒有專屬 dim,測試改為直接檢查 round-trip 標的的 driver 序列有沒有重複,這是刻意留在 fixture/測試層的訊號,不是逼 engine 生出新維度。 -- **`sample_panic_seller.csv` 是 fundamental 處置效應的極端版,加兩個 engine 沒有專屬 dim 的訊號**:① 多檔虧損倉在同一週同步出清(恐慌的特徵是『同步』,不是『個股別考量後賣出』,但 `dim_exit` 只看賣出後的持有天數分布,量不到『是不是同一週』)——用交易日期直接檢查窗口寬度佐證;② 賣飛之後追高買回同一檔(『賣在恐慌低點、買回追高點』的雙重行為錯置)——用同檔前後兩筆買入價格比對佐證。兩者都刻意寫在測試而非新增 engine 邏輯,對齊本次任務範圍(豐富測試用例,不是擴充 engine 功能)。 -- **五支都經 mutation 驗活**:分別故意弄壞 `insufficient` gate、`dim_avgdown` 的 0.90 閾值、`RecordType` 過濾、`dim_exit` 的 severity 公式、`driver()` 分類函式,確認對應測試真的會亮紅,才收進回歸(見 repo 一貫的「鐵則=先探測真實輸出+全綠後跑突變測試證明非假綠燈」)。 +Only synthetic fixtures may be committed. `.gitignore` blocks other CSV files. Never copy a real user's transactions into a fixture, issue, screenshot, or expected-output file. diff --git a/skills/fomo-kernel/references/agent-boundaries.md b/skills/fomo-kernel/references/agent-boundaries.md new file mode 100644 index 0000000..30cd611 --- /dev/null +++ b/skills/fomo-kernel/references/agent-boundaries.md @@ -0,0 +1,22 @@ +# Agent boundaries + +Keep agent flexibility in high-value contextual judgment. Constrain repeatable facts and workflow mechanics in code. + +The agent may: + +- Understand brokerage-specific fields and normalize them locally. +- Use world knowledge to propose a driver map or instrument map. Mark uncertainty as unknown rather than pretending certainty. +- Interpret motive answers and evidence deltas. +- Write an inferred hypothesis for a position without a thesis. +- Write the headline, mirror, counterfactual, and rule rationale. +- Add observations that do not silently replace the engine's top conclusion. + +The agent may not: + +- Calculate or alter numbers, rankings, cycle IDs, metrics, or ETF allocation exemptions. +- Skip required questions, answer for the user, or represent an inference as confirmed. +- use polished prose to bypass a missing claim or source for `new_evidence`. +- Assemble state by hand, append several JSONL files directly, and claim an atomic completion. +- Put private data into a public card. + +If a new observation could overturn the top behavioral leak, add it to `observations` and rerun preview. Do not mutate the engine artifact. This preserves analytical flexibility while keeping conclusion changes inside the same validator and renderer path. diff --git a/skills/fomo-kernel/references/card-policy.md b/skills/fomo-kernel/references/card-policy.md new file mode 100644 index 0000000..1aec4b8 --- /dev/null +++ b/skills/fomo-kernel/references/card-policy.md @@ -0,0 +1,11 @@ +# Card policy + +A card is a story, not a dashboard. + +The private card follows this order when data exists: mirror, primary account numbers, one strength, largest leak, qualitative motive or thesis, ETF structure, honesty boundaries, and one rule. Omit unavailable sections instead of filling them with generic prose. + +Agent narrative may not contain digits. The renderer obtains every amount, percentage, date, ticker, and metric from engine card and state artifacts. This deliberately strict boundary prevents the engine and prose from becoming competing numeric truth sources. + +The public card does not reuse agent narrative. It renders a separate structured view and removes session IDs, dates, tickers, amounts, exact weights, and evidence text. It is not a regular-expression mask over the private card. + +Do not provide buy or sell recommendations, shame the user, or list several action items. A commitment may be skipped. Code labels short samples as baselines rather than pretending they passed a mature threshold. diff --git a/skills/fomo-kernel/references/data-contract.md b/skills/fomo-kernel/references/data-contract.md new file mode 100644 index 0000000..183d164 --- /dev/null +++ b/skills/fomo-kernel/references/data-contract.md @@ -0,0 +1,24 @@ +# Data contract and recovery + +Authority order: + +1. `sessions//bundle.json`: complete immutable session. +2. State, plan, answers, narrative, private/public cards, and manifest in the same directory: manifest-locked artifacts. +3. `last_state.json`, `log.jsonl`, `theses.jsonl`, `thesis_decisions.jsonl`, `rules.jsonl`, `problems.jsonl`, and `cards/`: rebuildable compatibility projections. + +If prepare is interrupted, read `.pending/` through `review.py resume`; do not refetch live prices. + +If finalize fails before the atomic rename, no session is committed. The pending session remains available for correction and retry. + +If finalize fails after the rename while writing projections, the session is complete. Run `review.py repair-projections`; do not delete the bundle or ask the user again. + +Retrying the same session with identical content is a no-op. Retrying the same session with different content fails closed. To review identical state as a distinct session, pass an explicit `--session-nonce` to prepare. + +Schemas: + +- Review Plan: `schemas/review-plan.schema.json` +- Answers: `schemas/answers.schema.json` +- Prose narrative: `schemas/narrative.schema.json` +- Canonical bundle: `schemas/session-bundle.schema.json` + +ETF policy: broad-market, regional, bond, and commodity ETFs are diversified allocations. Sector, thematic, and leveraged ETFs remain concentrated risk. Treat an unknown ticker conservatively as equity. Missing expense ratio or tracking error belongs in the honesty ledger and must never be filled with zero. diff --git a/skills/fomo-kernel/references/thesis-policy.md b/skills/fomo-kernel/references/thesis-policy.md new file mode 100644 index 0000000..817ec79 --- /dev/null +++ b/skills/fomo-kernel/references/thesis-policy.md @@ -0,0 +1,15 @@ +# Thesis and add-decision policy + +A thesis is an append-only history of investment judgment, not a weekly memo that overwrites the past. + +Classify every add to a losing position as: + +- `planned_tranche`: the staged plan existed before entry; the note describes that original plan. +- `new_evidence`: a relevant fact was unknown at entry; require `evidence_delta.claim` and `source`, with optional `observed_at` and `falsifier`. +- `valuation_change`: the core facts are unchanged, but price changed the odds or margin of safety; the note states which assumption remains unchanged. +- `price_only`: no new fact exists; the main motive is a lower price, lower average cost, or waiting to recover. +- `skip`: the user does not want to classify it yet. Preserve the uncertainty in the card and memory; the agent must not answer for the user. + +`new_evidence` is not a synonym for positive news. It must change a falsifiable part of the prior thesis. "It is cheaper" is a valuation change; "the market agrees" without a new fact is price only. + +Revise a thesis for the same cycle with a new event containing `revises`; never overwrite the old event. A fully exited ticker followed by a new position starts a new cycle and a new thesis. `exit_trigger` is a fact that would falsify the thesis; `stop` is a price or sizing action. Keep them distinct. diff --git a/skills/fomo-kernel/rubric/antifragile.lens.json b/skills/fomo-kernel/rubric/antifragile.lens.json index b121e1d..fc4c811 100644 --- a/skills/fomo-kernel/rubric/antifragile.lens.json +++ b/skills/fomo-kernel/rubric/antifragile.lens.json @@ -1,72 +1,21 @@ { - "philosophy": "反脆弱 · 槓鈴 · 凸性", - "master": "Nassim Taleb(Antifragile)", - "source": "原則蒸餾自 Nassim Taleb 公開著作(Antifragile / Fooled by Randomness / Skin in the Game)與 virattt/ai-hedge-fund 的 nassim_taleb agent prompt(MIT,以 antifragility/凸性/肥尾/via negativa/skin-in-the-game 編碼此哲學)。引用非轉載、非經本人背書。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 antifragile.md。stance/lean 供 compare_lenses。此派引入庫內最強的三個獨立反轉:① sizing=barbell(槓鈴:極保守 + 小錢搏凸性,與 big / risk-capped 都不同向);② 加碼=unconditional『一律不攤平』(庫內唯一無後門);③ alpha/beta=inverted『低波動=危險』(火雞問題,與所有派的 decompose 對立)。引言已對 verbatim 校對(英文原句逐條見 antifragile.md),少數標【意譯】。", - + "philosophy": "Antifragility, barbell allocation, and convexity", + "master": "Nassim Nicholas Taleb", + "source": "Distilled from Taleb's public books and talks. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. Dimension keys are stable English identifiers; stance and lean are consumed by compare_lenses.", "master_intro": { - "one_line": "這套尺的核心:先確保你不會被一次肥尾打死,再用小錢去搏『虧有限、賺無限』的凸性。它不問你『看得準不準』,問你『萬一錯了,你會只是擦傷,還是直接出局』。", - "pillars": [ - "槓鈴策略:絕大部分極保守(不被炸掉),只用一小撮錢搏高凸性的賭注", - "凸性 > 預測:找『虧損有上限、獲利無上限』的不對稱,不靠預測方向", - "Via Negativa:賺錢先靠避開脆弱——高槓桿、薄利、靠單一假設撐著的東西", - "火雞問題:低波動、長期太平的『穩定』,往往是肥尾在累積、最危險", - "林迪效應 + skin in the game:活得久的更穩健;下注的人要跟你一起承擔風險" - ], - "why_it_matters": "這把尺偏『先求不死、再求凸性』。如果你是穩定收租/賣波動的人,它有些條對你是反的——它會把你的『穩定獲利』讀成『撿火車前的硬幣、肥尾在後面』。它照的是你錯的時候會不會出局、賭的是不是不對稱凸性,不是逼你變成末日論者。" + "one_line": "Remove fragility first, then keep a small part of the portfolio open to convex upside.", + "pillars": ["Use a barbell instead of uniform medium risk", "Prefer bounded downside and open-ended upside", "Remove hidden leverage and fragility", "Treat calm as possible tail-risk buildup", "Prefer Lindy durability and skin in the game"], + "why_it_matters": "This lens asks whether the portfolio survives surprise and benefits from volatility instead of merely forecasting direction." }, - - "strength_intro": "先說你做對的一件事(這派先認你『有上限地虧、卻留住凸性』的那一筆):", - + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "槓鈴:絕大部分極保守 + 一小撮搏凸性;最反對均勻中等部位", - "stance": "inverted", "lean": "barbell", - "rule": "把資金擺成槓鈴兩端:大部分放極安全處(不被炸掉),只用一小撮(如 5–15%)押高凸性賭注;別把錢均勻攤成一堆中等部位。", - "quote": "一邊極度趨避風險、一邊極度擁抱風險,勝過不上不下的『中度』——中度其實是傻瓜遊戲。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。槓鈴派問:這是『你願意整筆歸零、用來搏無限上檔的小錢』,還是『你以為穩、其實一次肥尾就會重創你的中間部位』?" - }, - "加碼攤平": { - "rubric_unit": "對脆弱部位往下加=火上加油,一律不攤平", - "stance": "unconditional", "lean": "no-average-down", - "rule": "往下加碼一律不做:賠錢往下加,是在一個正在證明自己脆弱的部位上放大暴險;要嘛當初就用有限的小注,要嘛認賠。", - "quote": "在虧損裡加碼,是用更多錢去賭『這次不一樣』——脆弱的東西會在你加最重時崩給你看。【意譯】", - "motive_q": "{tickers} 你在虧損裡一路往下加。這派不問你動機就先攔:你有沒有算過『這筆最壞歸零,會不會傷到你不該被傷的本金』?攤平正是把有限的虧,變成可能讓你出局的虧。" - }, - "出場紀律": { - "rubric_unit": "砍掉脆弱的、留住凸性的;讓凸性部位的肥尾跑", - "stance": "conditional", "lean": "cut-fragile", - "rule": "賣出的優先順序:先砍『脆弱、下檔無底』的,留住『虧有限、上檔無限』的凸性部位讓它跑;別反過來把凸性的小贏先了結。", - "quote": "贏家要讓它跑出肥尾,該砍的是那些『再跌沒有底』的脆弱部位。【意譯】", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些是『凸性已經兌現、上檔有限了』,還是『你把少數能噴出肥尾的凸性部位,因為怕回吐提早了結』?" - }, - "分散": { - "rubric_unit": "分散要分在『不相關的脆弱來源』,別讓單一隱藏脆弱性炸掉全部", - "stance": "conditional", "lean": "uncorrelated-tails", - "rule": "看的不是檔數,是『會不會一個肥尾事件同時打穿全部』;確保各部位的崩潰來源不相關,別讓隱藏的共同脆弱性串成一條。", - "quote": "你以為的分散,常常在崩盤那天全部變成同一個賭注。【意譯】", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。這派問:一個肥尾事件來時,這些部位會不會同時崩?你分散的是『檔數』,還是真的分散了『會害死你的那個風險來源』?" - }, - "持有時間": { - "rubric_unit": "林迪:活得久的更穩健;凸性部位可長抱等肥尾", - "stance": "conditional", "lean": "lindy", - "rule": "持有期看『脆弱還是反脆弱』:脆弱的別久留(時間是敵人),有凸性、經得起時間考驗(林迪)的可以長抱、等肥尾。", - "quote": "對非易腐的事物而言,每多活一天,預期還能再活的時間反而更長;脆弱的東西則相反。【意譯】", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『它的脆弱/凸性性質讓你有意切換』,還是『脆弱的東西套牢了,你卻改口說要長抱』?" - }, - "alpha/beta": { - "rubric_unit": "低波動=危險(火雞問題);別把賣凸性的穩定收益當 alpha", - "stance": "inverted", "lean": "convexity", - "rule": "別用『波動低、回撤小』當作做得好的證據;穩定的收益常來自賣出凸性(撿火車前的硬幣),肥尾來時一次全吐。看的是凸性,不是平滑。", - "quote": "市場缺乏波動,會讓隱藏的風險無聲累積;太平日子拖得越久,動盪來時傷得越重。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這派反問:這報酬是『有凸性、錯了也只擦傷』賺來的,還是『一路撿硬幣的穩定收益、肥尾還沒來』?平滑的曲線最會騙人。" - }, - "進場": { - "rubric_unit": "進場找不對稱凸性(虧有限、賺無限);追在低波動伸展處=買脆弱|待 engine B.9", - "stance": "conditional", "lean": "optionality", - "rule": "進場前先問『這筆的賠率對稱嗎』:要的是『最壞虧一個有限小數、最好賺很多倍』的凸性;追在大漲、低波動的伸展處,往往是在買進脆弱。", - "quote": "我要的是選擇權式的不對稱——下檔被鎖死,上檔開放給運氣。【意譯】", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲。是『這筆有凸性(虧有限、賺無限)』,還是『追在一個看似平靜、其實脆弱的伸展高點,下檔根本沒鎖住』?" - } + "position_sizing": {"rubric_unit": "Use a barbell, not uniform medium-sized risk", "stance": "inverted", "lean": "barbell", "rule": "Keep most capital in robust exposures and reserve only a small, loss-bounded allocation for convex bets.", "quote": "Avoid the fragile middle; combine extreme safety with limited convex risk. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Is its downside explicitly bounded, or is a medium-looking position hiding a large tail loss?"}, + "averaging_down": {"rubric_unit": "Do not add to fragile losing positions", "stance": "unconditional", "lean": "no-average-down", "rule": "Do not average down unless you can show that fragility fell and optionality improved; a lower price alone is not evidence.", "quote": "Adding capital to a fragile loss compounds the exposure to ruin. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new evidence shows lower fragility or better convexity rather than a desire to lower cost?"}, + "exit_discipline": {"rubric_unit": "Cut fragile exposure and let convexity work", "stance": "conditional", "lean": "cut-fragile", "rule": "Exit exposures whose downside can cascade; keep only positions whose loss is bounded and upside remains open.", "quote": "Remove what can break you before optimizing what can reward you. (paraphrase)", "motive_q": "Were the exits driven by newly visible fragility, or did you trim the few positions with bounded downside and open-ended upside?"}, + "diversification": {"rubric_unit": "Diversify tail-risk sources", "stance": "conditional", "lean": "uncorrelated-tails", "rule": "Diversify independent failure modes, not ticker count; several positions exposed to one tail are one bet.", "quote": "Apparent variety is not protection when every position fails in the same scenario. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which failure modes are genuinely independent?"}, + "holding_period": {"rubric_unit": "Let durable exposures earn a longer horizon", "stance": "conditional", "lean": "lindy", "rule": "Hold longer only when the thesis is durable, the downside remains bounded, and time does not increase hidden fragility.", "quote": "What has survived for longer may be more likely to endure. (paraphrase)", "motive_q": "For {incon_tickers}, did the durability assessment change, or did the time horizon change only after price moved?"}, + "alpha_beta": {"rubric_unit": "Look for convexity, not smoothness", "stance": "inverted", "lean": "convexity", "rule": "Separate true convex payoff from beta and short-volatility returns that merely look stable.", "quote": "A smooth history can conceal a large unobserved tail. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. Did the payoff come from bounded-downside convexity or hidden exposure to a calm regime?"}, + "entry_style": {"rubric_unit": "Enter only with asymmetric optionality", "stance": "conditional", "lean": "optionality", "rule": "Before entry, write the maximum loss, the open-ended upside, and the event that would expose hidden fragility.", "quote": "Prefer positions that can survive being wrong and benefit disproportionately from being right. (paraphrase)", "motive_q": "For {entry_ticker}, what bounded the downside and created optionality beyond simply expecting the price to rise?"} } } diff --git a/skills/fomo-kernel/rubric/antifragile.md b/skills/fomo-kernel/rubric/antifragile.md index 25c294d..1a59c37 100644 --- a/skills/fomo-kernel/rubric/antifragile.md +++ b/skills/fomo-kernel/rubric/antifragile.md @@ -1,44 +1,35 @@ -# Lens · 反脆弱 · 槓鈴 · 凸性(Nassim Taleb / Antifragile)— v1 +# Lens: antifragility, barbell allocation, and convexity — v1 -> 原則蒸餾自 Nassim Taleb 公開著作(Antifragile / Fooled by Randomness / Skin in the Game)與 virattt/ai-hedge-fund 的 `nassim_taleb` agent prompt(MIT,以 antifragility/凸性/肥尾/via negativa/skin-in-the-game 編碼此哲學)。原則/學派命名,真人來源見 Sources。 -> ✅ **引言已 verbatim 校對**:sizing / alpha-beta 兩句為 Taleb 原句(見下);其餘 5 句為其概念的忠實意譯,標【意譯】(Taleb 多以長段論述,難截單句)。 -> 這把尺帶進庫內**最強的三個獨立反轉**:① sizing = barbell(槓鈴,與 big / risk-capped 都不同向);② 加碼 = unconditional「一律不攤平」(庫內唯一無後門的加碼立場);③ alpha/beta = inverted「低波動=危險」(與所有派的 decompose 對立)。 +This lens distills public work by Nassim Nicholas Taleb. Its strongest divergences are barbell sizing, a strict refusal to average down fragile positions, and suspicion of smooth low-volatility returns. -## 脊椎(5 支柱) -1. 槓鈴策略:絕大部分極保守(不被炸掉),只用一小撮錢搏高凸性的賭注。 -2. 凸性 > 預測:找「虧損有上限、獲利無上限」的不對稱,不靠預測方向。 -3. Via Negativa:賺錢先靠避開脆弱——高槓桿、薄利、靠單一假設撐著的東西。 -4. 火雞問題:低波動、長期太平的「穩定」,往往是肥尾在累積、最危險。 -5. 林迪效應 + skin in the game:活得久的更穩健;下注的人要跟你一起承擔風險。 +## Five principles -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | +1. Use a barbell: keep most capital extremely safe and reserve a small amount for highly convex bets. +2. Prefer convexity to directional prediction: bounded downside and open-ended upside. +3. Apply via negativa: remove leverage and hidden fragility before searching for upside. +4. Treat long calm periods as possible accumulation of tail risk. +5. Prefer Lindy durability and decision-makers with skin in the game. + +## Stance map + +| Dimension | Stance | Lean | Interpretation | |---|---|---|---| -| 部位 sizing | inverted | barbell | 槓鈴:極保守 + 一小撮搏凸性,反對均勻中等部位 | -| 加碼攤平 | unconditional | no-average-down | 對脆弱部位往下加=火上加油,一律不攤 | -| 出場紀律 | conditional | cut-fragile | 砍脆弱的、留凸性的讓肥尾跑 | -| 分散 | conditional | uncorrelated-tails | 分散在不相關的脆弱來源,別讓單一肥尾炸全部 | -| 持有時間 | conditional | lindy | 林迪:活得久的更穩健,凸性部位可長抱 | -| alpha/beta | inverted | convexity | 低波動=危險(火雞問題),別把賣凸性當 alpha | -| 進場 | conditional | optionality | 找不對稱凸性,追低波動伸展處=買脆弱 | - -## 關鍵單元(verbatim 原句 → 中文) -- **sizing / 槓鈴**【原句】:"extreme risk aversion on one side and extreme risk loving on the other, rather than just the 'medium'… that in fact is a sucker game." → 一邊極度趨避風險、一邊極度擁抱風險,勝過不上不下的『中度』——中度其實是傻瓜遊戲。 -- **alpha-beta / 火雞問題**【原句】:"absence of fluctuations in the market causes hidden risks to accumulate with impunity. The longer one goes without a market trauma, the worse the damage when commotion occurs." → 市場缺乏波動,會讓隱藏的風險無聲累積;太平日子拖得越久,動盪來時傷得越重。 -- **加碼 / 不攤平**【意譯】:在虧損裡加碼,是用更多錢去賭「這次不一樣」——脆弱的東西會在你加最重時崩給你看。 -- **出場 / 砍脆弱留凸性**【意譯】:贏家要讓它跑出肥尾,該砍的是那些「再跌沒有底」的脆弱部位。 -- **分散 / 不相關尾部**【意譯】:你以為的分散,常常在崩盤那天全部變成同一個賭注。 -- **持有 / 林迪**【意譯】:對非易腐的事物而言,每多活一天,預期還能再活的時間反而更長;脆弱的東西則相反。 -- **進場 / 凸性選擇權**【意譯】:我要的是選擇權式的不對稱——下檔被鎖死,上檔開放給運氣。 - -## 為什麼這面尺值得進庫(divergence) -庫內既有派在 sizing 上不是 `risk-capped`(VY)就是 `big`(集中/Munger);Taleb 的 `barbell` 是第三個方向——它同意「別重壓中間部位」(對 big 反),也不同於 VY 的「一律設上限」(它對那一小撮凸性賭注反而允許整筆歸零)。加碼 `unconditional` 是庫內唯一「不問動機就先攔」的立場,跟所有 `conditional/evidence` 派形成最大 stance 距離。alpha/beta 的 `inverted/convexity` 讓「平滑的好曲線」第一次被當成警訊,而非本事。 - -## 待辦 -- 5 句【意譯】可回 Antifragile / Skin in the Game 找可截的單句原文替換。 -- 進場(EN)需 engine B.9。 - -### Sources -- Nassim Nicholas Taleb, *Antifragile* (2012) · *Fooled by Randomness* (2001) · *Skin in the Game* (2018) -- [Taleb on the barbell (Value Investing World)](https://www.valueinvestingworld.com/2013/04/nassim-taleb-and-barbells.html) · [Antifragile book notes](https://taylorpearson.me/antifragile-book-notes/) -- [virattt/ai-hedge-fund · nassim_taleb agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/nassim_taleb.py) (MIT) +| Sizing | inverted | barbell | Avoid uniform medium-sized risk. | +| Averaging down | unconditional | no-average-down | Do not add to fragile losing positions. | +| Exit | conditional | cut-fragile | Remove fragile exposures and let convex winners run. | +| Diversification | conditional | uncorrelated-tails | Diversify tail-risk sources, not ticker count. | +| Holding period | conditional | lindy | Durable and convex exposures may be held longer. | +| Alpha/beta | inverted | convexity | Smooth returns can hide short-volatility risk. | +| Entry | conditional | optionality | Seek asymmetric optionality. | + +## Grounded notes + +Taleb's barbell argument favors extreme risk aversion on one side and extreme risk seeking on the other over a misleading middle. His turkey-problem argument warns that the absence of fluctuations can allow hidden risk to accumulate. + +Other card language is paraphrase and must remain labeled as such until replaced with a verified source quotation. + +## Sources + +- Nassim Nicholas Taleb, *Antifragile*, *Fooled by Randomness*, and *Skin in the Game* +- [Taleb on the barbell](https://www.valueinvestingworld.com/2013/04/nassim-taleb-and-barbells.html) +- [virattt/ai-hedge-fund Taleb agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/nassim_taleb.py) diff --git a/skills/fomo-kernel/rubric/cathie-wood.lens.json b/skills/fomo-kernel/rubric/cathie-wood.lens.json index 5fb6d58..d19cae9 100644 --- a/skills/fomo-kernel/rubric/cathie-wood.lens.json +++ b/skills/fomo-kernel/rubric/cathie-wood.lens.json @@ -1,72 +1,21 @@ { - "philosophy": "破壞式創新 · 五年尺度 · 抱過波動", - "master": "Cathie Wood(ARK)", - "source": "原則蒸餾自 Cathie Wood / ARK Invest 公開訪談與研究及 virattt/ai-hedge-fund 的 cathie_wood agent prompt(MIT,以破壞潛力/創新驅動成長/大 TAM/多年尺度/容忍波動編碼此哲學)。引用非轉載、非經本人背書。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 cathie-wood.md。stance/lean 供 compare_lenses。此派帶進兩個強反轉:① 加碼=inverted『add-on-conviction』(越跌越買、信念不變——與 Taleb 的 unconditional/no-average-down 形成庫內最大對立);② alpha/beta=inverted『embrace-vol』(高波動是創新 alpha 的代價,不是該砍的洞——與所有 decompose 派相反)。引言已對 verbatim 校對(英文原句見 cathie-wood.md),少數標【意譯】。", - + "philosophy": "Disruptive innovation and a five-year horizon", + "master": "Cathie Wood / ARK", + "source": "Distilled from public ARK material. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. It permits evidence-driven adding but never treats a lower price by itself as stronger conviction.", "master_intro": { - "one_line": "這套尺的核心:押注會改寫世界運作方式的破壞式創新,用五年以上的尺度抱住、接受路上的劇烈波動。它不問你『現在貴不貴、波動大不大』,問你『這是不是真正的破壞式創新,你的五年信念還在嗎』。", - "pillars": [ - "只投破壞式創新:能改變世界運作方式的技術平台", - "指數級成長 + 大 TAM:要的是加速度,不是穩態績效", - "五年以上尺度:突破要時間,別用一季的價格判生死", - "接受高波動:高報酬的代價就是劇烈震盪,別被它嚇下車", - "信念隨證據強化:基本面/估值上修、價格反而下跌=加碼良機" - ], - "why_it_matters": "這把尺偏『高信念創新成長 + 抱過波動』。如果你是價值/存活紀律派,它有些條對你是反的——它會把你的『停損、控制回撤』讀成『在創新拐點被洗下車』。它照的是你押的是不是真破壞式創新、五年信念在不在、扛不扛得住波動,不是逼你追每一個熱門題材。" + "one_line": "Judge disruptive innovation on a multi-year thesis, not one quarter of volatility.", + "pillars": ["Require a disruptive platform", "Look for exponential adoption and a large market", "Use a five-year horizon", "Accept thesis-consistent volatility", "Add only when evidence and valuation improve"], + "why_it_matters": "This lens distinguishes volatility from thesis failure while still requiring explicit evidence for conviction." }, - - "strength_intro": "先說你做對的一件事(這派先認你押對破壞式創新、又抱得住波動的那一筆):", - + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "高信念的創新贏家就該重壓,溫吞部位是浪費", - "stance": "inverted", "lean": "big", - "rule": "把資金集中到你最高信念的破壞式創新標的;對它們別小家子氣,對沒有破壞性的別給 size。", - "quote": "我們經營的是高信念、集中的投資組合。【意譯】", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這派問:如果這真是你最高信念的破壞式創新,這個 size 算重壓嗎?那些沒有破壞性的部位,又為什麼還佔著資金?" - }, - "加碼攤平": { - "rubric_unit": "創新論點沒變、價格更低=加碼良機(越跌越買)", - "stance": "inverted", "lean": "add-on-conviction", - "rule": "只要五年創新論點沒被推翻,價格大跌反而是加碼良機;信念該隨『基本面上修、價格下跌』而升級,不是縮手。", - "quote": "我們的預期反而調高了,而價格卻跌了——這正是加碼的時候。", - "motive_q": "{tickers} 你在虧損裡一路往下加。這派不把它當凹單——只問:你是『五年創新論點沒變、跌出更好的進場』,還是『論點其實已經被推翻、你只是不想認賠』?" - }, - "出場紀律": { - "rubric_unit": "賣=創新論點破了 / 換到更高信念,不因波動就跑", - "stance": "conditional", "lean": "thesis-5yr", - "rule": "賣出=『五年創新論點被推翻』或『換到信念更高的標的』;別因為短期回吐或波動就把贏家賣掉。", - "quote": "不要跟破壞式創新作對。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些是『創新論點到頭、或換到更強的標的』,還是『被波動嚇到、賺一點就落袋,把長線贏家提早賣了』?" - }, - "分散": { - "rubric_unit": "集中在少數破壞式平台;分散是跨創新主題不是為了求穩", - "stance": "inverted", "lean": "concentrate-innovation", - "rule": "別為了『看起來分散』去持沒有破壞性的標的;集中在少數高信念創新平台,要分也只跨不同的創新主題。", - "quote": "與其分散到一堆平庸標的,不如集中在你最相信的創新平台。【意譯】", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。這派不嫌你集中在創新——反問:其餘那些部位,是『不同的破壞式創新主題』,還是只是『為了分散』湊上去、其實沒有破壞性的?" - }, - "持有時間": { - "rubric_unit": "五年以上尺度,突破要時間", - "stance": "conditional", "lean": "5yr-horizon", - "rule": "持有期以五年以上為尺度;破壞式創新的回報要時間發酵,別用一季的價格決定去留。", - "quote": "我們以五年以上的投資時間軸來思考突破。", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『創新論點的判斷變了所以調整』,還是『本來想長抱五年、一波回檔就改口下車』?" - }, - "alpha/beta": { - "rubric_unit": "高波動是創新 alpha 的代價,不是該砍的洞", - "stance": "inverted", "lean": "embrace-vol", - "rule": "別把『波動大、回撤深』當成做錯;只要押的是真創新,高 β / 大震盪是高報酬的入場費——看五年的創新兌現,不看這一季的平滑。", - "quote": "我們願意承受更高的波動,去換取創新帶來的高報酬。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這派反問:這個高 β 是『你押對破壞式創新、願意付的波動代價』,還是『其實沒有創新論點、只是追高波動的熱門題材』?" - }, - "進場": { - "rubric_unit": "創新論點強 + 大 TAM 才進;不為近期價格/波動卻步|待 engine B.9", - "stance": "conditional", "lean": "innovation-thesis", - "rule": "進場看『破壞式創新論點強不強、TAM 夠不夠大』;論點成立就進,不因近期大漲或大跌、波動高低而改變決定。", - "quote": "尋找那些利用破壞式創新、擁有指數級成長潛力與龐大 TAM 的公司。", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲。是『破壞式創新論點與 TAM 撐得起這個價』,還是『沒有創新論點、只是看它在噴、你怕錯過』?" - } + "position_sizing": {"rubric_unit": "High-conviction innovation may deserve meaningful size", "stance": "inverted", "lean": "big", "rule": "Size meaningful positions only when the disruptive platform, adoption path, and valuation case are explicit.", "quote": "Conviction should reflect the strength of a multi-year innovation thesis. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Which independent evidence makes this a high-conviction innovation thesis rather than a popular story?"}, + "averaging_down": {"rubric_unit": "Add when conviction evidence improves", "stance": "inverted", "lean": "add-on-conviction", "rule": "Add only when new evidence strengthens the five-year thesis and the expected return improves; price decline alone is insufficient.", "quote": "Lower prices matter only when the long-term estimate remains supported or improves. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new evidence improved the adoption, market-size, or valuation thesis?"}, + "exit_discipline": {"rubric_unit": "Exit on thesis failure or a superior thesis", "stance": "conditional", "lean": "thesis-5yr", "rule": "Exit when the innovation thesis breaks, the five-year return case compresses, or a clearly stronger thesis replaces it.", "quote": "Short-term volatility is not the same as a broken long-term thesis. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which exits reflected a broken five-year thesis, and which reacted only to volatility?"}, + "diversification": {"rubric_unit": "Concentrate in a few disruptive platforms", "stance": "inverted", "lean": "concentrate-innovation", "rule": "Concentrate only when each platform has a distinct adoption thesis; multiple tickers tied to one innovation driver are one bet.", "quote": "A focused innovation portfolio still needs distinct thesis drivers. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Are these independent innovation platforms or repeated exposure to one narrative?"}, + "holding_period": {"rubric_unit": "Use a five-year evaluation horizon", "stance": "conditional", "lean": "5yr-horizon", "rule": "Set five-year milestones at entry and judge progress against them instead of relabeling the horizon after a price move.", "quote": "Breakthrough adoption should be measured over years, not one quarter. (paraphrase)", "motive_q": "For {incon_tickers}, did a five-year milestone change, or did the holding label change because the position moved against you?"}, + "alpha_beta": {"rubric_unit": "Separate innovation alpha from high-volatility beta", "stance": "inverted", "lean": "embrace-vol", "rule": "Accept volatility only when it accompanies measurable thesis progress; decompose returns before calling volatility innovation alpha.", "quote": "Volatility can be a cost of innovation exposure, but it is not proof of innovation. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. How much came from thesis progress versus high-growth beta?"}, + "entry_style": {"rubric_unit": "Require an explicit innovation thesis", "stance": "conditional", "lean": "innovation-thesis", "rule": "Before entry, state the disruptive platform, adoption curve, addressable market, five-year milestone, and falsifier.", "quote": "A large market is useful only when the company has a credible path to disrupt it. (paraphrase)", "motive_q": "For {entry_ticker}, what specific innovation milestone justified entry beyond price momentum or fear of missing out?"} } } diff --git a/skills/fomo-kernel/rubric/cathie-wood.md b/skills/fomo-kernel/rubric/cathie-wood.md index 3487c1c..89aac26 100644 --- a/skills/fomo-kernel/rubric/cathie-wood.md +++ b/skills/fomo-kernel/rubric/cathie-wood.md @@ -1,44 +1,33 @@ -# Lens · 破壞式創新 · 五年尺度 · 抱過波動(Cathie Wood / ARK)— v1 +# Lens: disruptive innovation and a five-year horizon — v1 -> 原則蒸餾自 Cathie Wood / ARK Invest 公開訪談與研究及 virattt/ai-hedge-fund 的 `cathie_wood` agent prompt(MIT)。原則/學派命名,真人來源見 Sources。 -> ✅ **引言已 verbatim 校對**:加碼 / 出場 / alpha-beta / 持有 / 進場 五句有公開來源根據(見下);sizing / 分散 兩句標【意譯】。 -> 這把尺帶進兩個強反轉:① 加碼=`inverted/add-on-conviction`(越跌越買、信念不變)——與 **Taleb 的 `unconditional/no-average-down`** 形成庫內最大對立;② alpha/beta=`inverted/embrace-vol`(高波動是創新 alpha 的代價,不是該砍的洞)——與所有 `decompose` 派相反。 +This lens distills public Cathie Wood and ARK Invest material. It deliberately conflicts with antifragility on averaging down and with conventional risk lenses on volatility. -## 脊椎(5 支柱) -1. 只投破壞式創新:能改變世界運作方式的技術平台。 -2. 指數級成長 + 大 TAM:要的是加速度,不是穩態績效。 -3. 五年以上尺度:突破要時間,別用一季的價格判生死。 -4. 接受高波動:高報酬的代價就是劇烈震盪,別被它嚇下車。 -5. 信念隨證據強化:基本面/估值上修、價格反而下跌=加碼良機。 +## Five principles -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | +1. Invest only when a disruptive platform can change how an industry works. +2. Look for exponential growth and a large addressable market. +3. Use a multi-year horizon; do not judge a breakthrough by one quarter. +4. Accept high volatility as a possible cost of innovation exposure. +5. Increase conviction only when evidence and valuation improve, not merely because price falls. + +## Stance map + +| Dimension | Stance | Lean | Interpretation | |---|---|---|---| -| 部位 sizing | inverted | big | 高信念的創新贏家就該重壓 | -| 加碼攤平 | inverted | add-on-conviction | 論點沒變、價格更低=加碼良機(越跌越買) | -| 出場紀律 | conditional | thesis-5yr | 賣=創新論點破了/換更高信念,不因波動跑 | -| 分散 | inverted | concentrate-innovation | 集中在少數破壞式平台 | -| 持有時間 | conditional | 5yr-horizon | 五年以上尺度,突破要時間 | -| alpha/beta | inverted | embrace-vol | 高波動是創新 alpha 的代價,不是洞 | -| 進場 | conditional | innovation-thesis | 創新論點強+大 TAM 才進 | - -## 關鍵單元(verbatim 原句 → 中文) -- **加碼 / 越跌越買**【原句】:"Our estimates actually have gone up, and the prices have gone down."(訪談,談加碼)→ 我們的預期反而調高了,而價格卻跌了——這正是加碼的時候。 -- **出場 / 別跟創新作對**【原句】:"Do not be against disruptive innovation."(ARK)→ 不要跟破壞式創新作對。 -- **alpha-beta / 容忍波動**【原句】:"Accept higher volatility in pursuit of high returns."(ai-hedge-fund prompt,本於 ARK 一貫立場)→ 我們願意承受更高的波動,去換取創新帶來的高報酬。 -- **持有 / 五年尺度**【原句】:"Consider multi-year time horizons for potential breakthroughs."(ai-hedge-fund prompt)→ 我們以五年以上的投資時間軸來思考突破。 -- **進場 / 創新 + 大 TAM**【原句】:"Seek companies leveraging disruptive innovation … with exponential growth potential, large TAM."(ai-hedge-fund prompt)→ 尋找那些利用破壞式創新、擁有指數級成長潛力與龐大 TAM 的公司。 -- **sizing / 高信念集中**【意譯】:我們經營的是高信念、集中的投資組合。 -- **分散 / 跨創新主題**【意譯】:與其分散到一堆平庸標的,不如集中在你最相信的創新平台。 - -## 為什麼這面尺值得進庫(divergence) -庫內「加碼攤平」過去清一色是「攤平可疑」:VY `evidence`、margin-of-safety `discount`、Taleb `unconditional/no-average-down`。Cathie Wood 第一次把它**反轉**成 `inverted/add-on-conviction`——「越跌越買」是策略不是洞。這跟 Taleb「一律不攤平」恰好端到對立兩極,放在多哲學對照裡會逼出一個最尖銳的岔路:同一筆「虧損加碼」,一派說是紀律、一派說是送死——用戶站哪邊,就暴露他信哪套。alpha/beta 的 `inverted/embrace-vol` 也讓「高 β / 大回撤」第一次被當成入場費而非扣分項。 - -## 待辦 -- sizing / 分散 兩句【意譯】可回 ARK 研究報告或 Wood 訪談找可截原句替換。 -- 「越跌越買」原句為訪談轉述,如要嚴謹可補確切場次/日期。 -- 進場(EN)需 engine B.9。 - -### Sources -- Cathie Wood / [ARK Invest](https://www.ark-funds.com/funds/arkk) · [ARK: do not be against disruptive innovation](https://etfdb.com/disruptive-technology-channel/cathie-wood-do-not-be-against-disruptive-innovation/) -- [virattt/ai-hedge-fund · cathie_wood agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/cathie_wood.py) (MIT) +| Sizing | inverted | big | High-conviction innovation may deserve a large position. | +| Averaging down | inverted | add-on-conviction | Add only when the thesis strengthened while price fell. | +| Exit | conditional | thesis-5yr | Exit when the innovation thesis breaks or a better thesis replaces it. | +| Diversification | inverted | concentrate-innovation | Concentrate in a small set of disruptive platforms. | +| Holding period | conditional | 5yr-horizon | Give breakthroughs several years. | +| Alpha/beta | inverted | embrace-vol | Volatility is not automatically a leak. | +| Entry | conditional | innovation-thesis | Require a specific innovation thesis and large market. | + +## Evidence boundary + +The public claim that estimates rose while prices fell supports evidence-driven adding. It does not support adding solely because a position is cheaper. Sizing and diversification language remains paraphrase until matched to a primary ARK source. + +## Sources + +- [ARK Invest](https://www.ark-funds.com/funds/arkk) +- [ARK discussion of disruptive innovation](https://etfdb.com/disruptive-technology-channel/cathie-wood-do-not-be-against-disruptive-innovation/) +- [virattt/ai-hedge-fund Cathie Wood agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/cathie_wood.py) diff --git a/skills/fomo-kernel/rubric/charlie-munger.lens.json b/skills/fomo-kernel/rubric/charlie-munger.lens.json index 81c23ff..227004b 100644 --- a/skills/fomo-kernel/rubric/charlie-munger.lens.json +++ b/skills/fomo-kernel/rubric/charlie-munger.lens.json @@ -1,72 +1,21 @@ { - "philosophy": "優質生意 · 能力圈 · 坐著不動", + "philosophy": "Quality businesses, circle of competence, and patience", "master": "Charlie Munger", - "source": "原則蒸餾自 Charlie Munger 公開談話(Poor Charlie's Almanack / Daily Journal 年會問答)與 virattt/ai-hedge-fund 的 charlie_munger agent prompt(MIT,以護城河35%/管理25%/可預測性25%/估值15% 的權重編碼此哲學)。引用非轉載、非經本人背書。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 charlie-munger.md。stance/lean 供 compare_lenses。此派 sizing/分散 與『集中信念』同向(皆 inverted),但理由不同(Munger=生意夠好且在能力圈,非宏觀方向);與『安全邊際』最大反轉在【進場】:安全邊際買『便宜』,此派買『合理價的絕世好生意』,圈外再便宜也不碰。引言已對 verbatim 校對(英文原句逐條見 charlie-munger.md),少數標【意譯】。", - + "source": "Distilled from public Munger talks and Q&A. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. Concentration is justified by business quality and competence, not merely conviction.", "master_intro": { - "one_line": "這套尺的核心:用合理價買絕世好生意,然後坐著不動。它不問你『買得夠不夠便宜』,問你『這是不是你真懂的好生意——是的話,你為什麼還一直動它』。", - "pillars": [ - "合理價的好生意 > 便宜價的平庸生意——別為了撿便宜買爛東西", - "能力圈:只在你真懂的範圍下注,圈外再誘人也說『不』", - "護城河優先:高 ROIC、有訂價權、低資本需求、誠信的管理層", - "反過來想(via negativa):避免愚蠢,比追求聰明更賺", - "坐著不動:一生真正的好機會沒幾個,抓到了就別手癢亂動" - ], - "why_it_matters": "這把尺偏『優質 + 集中 + 不動』。如果你是高頻/動能客,它有些條對你是反的——它會把你的『勤奮進出』讀成『手癢的損耗』。它照的是你買的是不是能力圈內的好生意、抓到之後忍不忍得住不動,不是逼你變成 buy-and-hold 信徒。" + "one_line": "Buy a wonderful business at a fair price inside your circle of competence, then allow it to compound.", + "pillars": ["Prefer quality at a fair price", "Stay inside a real circle of competence", "Require a durable moat", "Avoid stupidity before seeking brilliance", "Act rarely and wait patiently"], + "why_it_matters": "This lens reads unnecessary activity as friction and asks whether quality, competence, and patience justify the position." }, - - "strength_intro": "先說你做對的一件事(這派先認你抱住好生意、忍住沒動的那一筆):", - + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "能力圈內的好生意就該重壓 / 平庸生意不配大 size", - "stance": "inverted", "lean": "big", - "rule": "把資金集中到你能力圈內、真正夠好的少數生意;對它們別小家子氣,對平庸的別給 size。", - "quote": "聰明人在勝算在握、機會出現時就下重注;其餘時間按兵不動。就這麼簡單。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這派問:如果這真是你能力圈內的絕世好生意,這個 size 算重壓嗎?那些平庸的部位,又為什麼還佔著資金?" - }, - "加碼攤平": { - "rubric_unit": "只在好生意+合理價加;不為了攤平爛生意而加", - "stance": "conditional", "lean": "quality", - "rule": "往下加碼只有在『這仍是絕世好生意、現在價格更合理』時才做;若生意本身平庸,價格再跌也不加。", - "quote": "一門平庸的生意,你用再便宜的價格一直加,通常只會把錯誤放大。【意譯】", - "motive_q": "{tickers} 你在虧損裡往下加。是『這仍是你認證過的好生意、只是更便宜了』,還是『生意本身普通、你只是想攤低成本等回本』?" - }, - "出場紀律": { - "rubric_unit": "坐著不動;賣掉一門複利的好生意才是最貴的錯", - "stance": "conditional", "lean": "do-nothing", - "rule": "賣出前先問『這門好生意的護城河壞了嗎』;護城河還在,就坐著不動,別因為漲了或手癢就賣。", - "quote": "大錢不在買進賣出,而在等待。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些是『護城河真的壞了』,還是『手癢、想實現獲利,把一門還在複利的好生意提早賣掉』?" - }, - "分散": { - "rubric_unit": "diworsification:別為了分散而稀釋到平庸生意", - "stance": "inverted", "lean": "concentrate", - "rule": "別為了『看起來分散』去持你不夠懂或不夠好的生意;寧可少數絕世好生意,也不要一堆平庸的。", - "quote": "過度分散是一種瘋狂。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。這派不嫌你不夠分散——反問:其餘那些『為了分散』的部位,每一檔都是能力圈內的好生意,還是只是湊數的平庸貨?" - }, - "持有時間": { - "rubric_unit": "好生意的最佳持有期接近永遠", - "stance": "conditional", "lean": "forever", - "rule": "持有期由『護城河還在不在』決定,不由標籤;護城河沒壞,最佳持有期就是接近永遠。", - "quote": "屁股坐定的投資法:你付更少佣金、聽更少廢話,稅制還多給你幾個百分點。", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『護城河的判斷變了所以調整』,還是『一開始想長抱、套牢或漲多了就改口』?" - }, - "alpha/beta": { - "rubric_unit": "報酬要來自生意複利,不是槓桿/波動", - "stance": "aligned", "lean": "decompose", - "rule": "每季分清:你贏大盤,是『好生意本身在複利』,還是『靠 β、靠押高波動』撐出來的?", - "quote": "長期而言,一檔股票的報酬很難超過它背後生意賺到的報酬。【意譯】", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這派問:這是『你選的生意本身在複利』,還是『敢押高波動換來的,跟生意品質無關』?" - }, - "進場": { - "rubric_unit": "能力圈內、合理價買好生意;圈外再便宜也不碰|待 engine B.9", - "stance": "conditional", "lean": "quality-in-circle", - "rule": "進場前過兩關:① 它在我的能力圈內嗎?② 它是合理價的好生意嗎(不必最便宜,但要夠好)?圈外的,再便宜也說『不』。", - "quote": "忘掉用好價格買普通生意吧;要用合理的價格買絕世好生意。", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲。是『它在你能力圈內、是合理價的好生意』,還是『超出你真懂的範圍、只是它在噴你怕錯過』?" - } + "position_sizing": {"rubric_unit": "Bet meaningfully when quality and competence align", "stance": "inverted", "lean": "big", "rule": "Concentrate only in the few high-quality businesses you can explain and evaluate; give mediocre ideas no meaningful size.", "quote": "Act decisively when the odds and your competence genuinely align. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. What demonstrates both exceptional business quality and a real circle of competence?"}, + "averaging_down": {"rubric_unit": "Add only to quality at a fairer price", "stance": "conditional", "lean": "quality", "rule": "Add only when the business remains exceptional, the moat is intact, and the price is more reasonable; do not average down a mediocre business.", "quote": "A lower price does not improve the economics of a poor business. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new evidence shows that business quality and the moat remain intact?"}, + "exit_discipline": {"rubric_unit": "Do not interrupt compounding without a broken thesis", "stance": "conditional", "lean": "do-nothing", "rule": "Before selling, identify whether the moat or management quality broke; if neither changed, avoid activity for its own sake.", "quote": "The large reward often comes from waiting, not repeated trading. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which businesses lost their moat, and which did you sell merely to realize a gain?"}, + "diversification": {"rubric_unit": "Do not dilute quality for cosmetic diversification", "stance": "inverted", "lean": "concentrate", "rule": "Do not add businesses you do not understand merely to increase ticker count; diversify only when each holding meets the quality bar.", "quote": "Excess diversification can replace a few understood businesses with many mediocre ones. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which holdings are present only to look diversified rather than because they clear the quality bar?"}, + "holding_period": {"rubric_unit": "Let an intact moat compound", "stance": "conditional", "lean": "forever", "rule": "Let holding period follow moat durability, not a changing label; an exceptional business may deserve a very long horizon.", "quote": "A durable compounder should not be interrupted without a business reason. (paraphrase)", "motive_q": "For {incon_tickers}, did the moat assessment change, or did the time horizon change after price moved?"}, + "alpha_beta": {"rubric_unit": "Return should come from business compounding", "stance": "aligned", "lean": "decompose", "rule": "Separate business compounding from market beta, leverage, and multiple expansion before claiming selection skill.", "quote": "Long-run investment return is constrained by the economics of the underlying business. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. How much came from business compounding rather than market exposure?"}, + "entry_style": {"rubric_unit": "Require quality, competence, and a fair price", "stance": "conditional", "lean": "quality-in-circle", "rule": "Before entry, pass three gates: circle of competence, exceptional business quality, and a fair price.", "quote": "Prefer a wonderful business at a fair price to a fair business at a wonderful price. (paraphrase)", "motive_q": "For {entry_ticker}, was the entry grounded in an understood high-quality business at a fair price, or in price excitement outside your competence?"} } } diff --git a/skills/fomo-kernel/rubric/charlie-munger.md b/skills/fomo-kernel/rubric/charlie-munger.md index a2868fa..1ec0e82 100644 --- a/skills/fomo-kernel/rubric/charlie-munger.md +++ b/skills/fomo-kernel/rubric/charlie-munger.md @@ -1,42 +1,33 @@ -# Lens · 優質生意 · 能力圈 · 坐著不動(Charlie Munger)— v1 +# Lens: quality businesses, circle of competence, and patience — v1 -> 原則蒸餾自 Charlie Munger 公開談話(Poor Charlie's Almanack / Daily Journal 年會問答)與 virattt/ai-hedge-fund 的 `charlie_munger` agent prompt(MIT,以護城河35%/管理25%/可預測性25%/估值15% 的權重編碼此哲學)。原則/學派命名,真人來源見 Sources。 -> ✅ **引言已 verbatim 校對**:5 句為 Munger 原句(見下),2 句(加碼/alpha-beta)無乾淨原句,標【意譯】。 -> 與庫內其他派的關係:sizing/分散 與「集中信念」同向(皆 inverted),但理由不同(Munger = 生意夠好且在能力圈,非宏觀方向);與「安全邊際」最大反轉在 **進場**——安全邊際買「便宜」,此派買「合理價的絕世好生意」,圈外再便宜也不碰。 +This lens distills public Charlie Munger material. It shares concentration with conviction lenses but grounds it in business quality and a circle of competence rather than a macro view. -## 脊椎(5 支柱) -1. 合理價的好生意 > 便宜價的平庸生意——別為了撿便宜買爛東西。 -2. 能力圈:只在你真懂的範圍下注,圈外再誘人也說「不」。 -3. 護城河優先:高 ROIC、有訂價權、低資本需求、誠信的管理層。 -4. 反過來想(via negativa):避免愚蠢,比追求聰明更賺。 -5. 坐著不動:一生真正的好機會沒幾個,抓到了就別手癢亂動。 +## Five principles -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | +1. Prefer a wonderful business at a fair price to a fair business at a wonderful price. +2. Operate inside a real circle of competence. +3. Prefer durable moats, pricing power, high returns on capital, and honest management. +4. Avoid stupidity before trying to be brilliant. +5. Act rarely and wait when a genuinely good opportunity is compounding. + +## Stance map + +| Dimension | Stance | Lean | Interpretation | |---|---|---|---| -| 部位 sizing | inverted | big | 能力圈內的好生意就該重壓 | -| 加碼攤平 | conditional | quality | 只在好生意+合理價加,不攤平爛生意 | -| 出場紀律 | conditional | do-nothing | 坐著不動,賣掉複利好生意才是錯 | -| 分散 | inverted | concentrate | diworsification:別稀釋到平庸生意 | -| 持有時間 | conditional | forever | 好生意的最佳持有期接近永遠 | -| alpha/beta | aligned | decompose | 報酬要來自生意複利,不是槓桿/波動 | -| 進場 | conditional | quality-in-circle | 能力圈內、合理價買好生意 | - -## 關鍵單元(verbatim 原句 → 中文) -- **sizing / 勝算在握下重注**【原句】:"The wise ones bet heavily when the world offers them that opportunity. They bet big when they have the odds. And the rest of the time, they don't. It's that simple." → 聰明人在勝算在握、機會出現時就下重注;其餘時間按兵不動。就這麼簡單。 -- **出場 / 等待**【原句】:"The big money is not in the buying and the selling, but in the waiting." → 大錢不在買進賣出,而在等待。 -- **分散 / 反過度分散**【原句】:"The idea of excessive diversification is madness." → 過度分散是一種瘋狂。 -- **持有 / 坐著不動**【原句】:"Sit on your ass investing. You're paying less to brokers, you're listening to less nonsense…" → 屁股坐定的投資法:你付更少佣金、聽更少廢話。 -- **進場 / 合理價好生意**【原句】:"Forget what you know about buying fair businesses at wonderful prices. Instead, buy wonderful businesses at fair prices." → 忘掉用好價格買普通生意吧;要用合理的價格買絕世好生意。 -- **via negativa(支柱4,未綁單一 dim)**【原句】:"All I want to know is where I'm going to die, so I'll never go there." → 我只想知道我會死在哪,好讓我永遠別去那。 -- **加碼 / 不攤平爛生意**【意譯】:一門平庸的生意,你用再便宜的價格一直加,通常只會把錯誤放大。 -- **alpha/beta / 報酬上限**【意譯】:長期而言,一檔股票的報酬很難超過它背後生意賺到的報酬。 - -## 待辦 -- 加碼/alpha-beta 兩句【意譯】可回 Poor Charlie's Almanack 找更貼近的原句替換。 -- 進場(EN)需 engine B.9。 - -### Sources -- *Poor Charlie's Almanack* · Daily Journal / Berkshire 年會問答 -- [Picture Perfect Portfolios · sit-on-your-ass investing](https://pictureperfectportfolios.com/the-art-of-sit-on-your-ass-investing-lessons-charlie-munger/) · [Scheplick · 121 Munger quotes](https://scheplick.com/charlie-munger-quotes/) -- [virattt/ai-hedge-fund · charlie_munger agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/charlie_munger.py) (MIT) +| Sizing | inverted | big | Bet meaningfully when odds and competence align. | +| Averaging down | conditional | quality | Add only to a quality business at a reasonable price. | +| Exit | conditional | do-nothing | Do not interrupt compounding without a broken thesis. | +| Diversification | inverted | concentrate | Excessive diversification can dilute quality. | +| Holding period | conditional | forever | A great business may deserve a very long holding period. | +| Alpha/beta | aligned | decompose | Return should come from business compounding, not leverage. | +| Entry | conditional | quality-in-circle | Require quality, competence, and a fair price. | + +## Source boundary + +The heavy-bet, waiting, excessive-diversification, and wonderful-business statements are grounded in public Munger remarks. Averaging-down and alpha language remains paraphrase. + +## Sources + +- *Poor Charlie's Almanack* and public Daily Journal or Berkshire Q&A +- [Sit-on-your-ass investing](https://pictureperfectportfolios.com/the-art-of-sit-on-your-ass-investing-lessons-charlie-munger/) +- [virattt/ai-hedge-fund Munger agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/charlie_munger.py) diff --git a/skills/fomo-kernel/rubric/concentration-conviction.lens.json b/skills/fomo-kernel/rubric/concentration-conviction.lens.json index 9c7d84a..447724d 100644 --- a/skills/fomo-kernel/rubric/concentration-conviction.lens.json +++ b/skills/fomo-kernel/rubric/concentration-conviction.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "集中信念", - "master": "集中信念", - "source": "原則蒸餾自 Druckenmiller / Soros 公開訪談與演講,引用非轉載、非經本人背書。⚠️ 引言為意譯,尚未對原始訪談逐句校對,上線前需 verbatim 對齊審查。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 concentration-conviction.md。stance/lean 供 compare_lenses。此派與 VY 最大反轉在 sizing/分散(VY:別梭哈、雙紅線;此派:高信念就該重壓,小注是浪費資本)。", - - "master_intro": { - "one_line": "集中信念派的核心:看準了,就把蛋放一個籃子、然後死盯著它。它不問你『有沒有分散』,問你『這真是你最高信念的賭注嗎——是的話,為什麼下這麼小』。", - "pillars": [ - "高信念才下注,沒信念就空手——別把資本浪費在一堆低信念標的", - "看準了就重壓:集中在你最有把握的 1–3 個 idea", - "重點不是你對或錯,是對的時候賺多大、錯的時候虧多小", - "集中反而降低風險,因為大部位逼你全神貫注、錯了快砍", - "top-down:先抓宏觀/流動性的大方向,再選工具" - ], - "why_it_matters": "這把尺偏『高信念集中 + 錯了快砍』。如果你是分散派/被動派,它有些條對你是反的——它會把你的『適度分散』讀成『稀釋信念』。它照的是你敢不敢在真有把握時下重注、又能不能在錯時果斷退場,不是逼你變成宏觀投機者。" - }, - - "strength_intro": "先講你做對的一件事(集中派先認你敢於表態的那一注):", - + "philosophy": "Concentration and conviction", + "master": "Concentration and conviction", + "source": "Distilled from public Druckenmiller and Soros material. All card language below is paraphrase pending primary-source verification.", + "_note": "Runtime lens. It treats weak conviction as a reason to hold cash and strong, evidenced conviction as a reason for meaningful size.", + "master_intro": {"one_line": "Hold cash until evidence supports a rare, asymmetric, high-conviction opportunity.", "pillars": ["Avoid low-conviction allocation", "Concentrate in the best evidence", "Prioritize payoff asymmetry", "Watch large positions closely", "Start with macro and liquidity"], "why_it_matters": "This lens asks whether position size actually matches the quality and asymmetry of the evidence."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "stance": "inverted", "lean": "big", - "rubric_unit": "高信念重壓 / 小注是浪費資本", - "rule": "把資金集中到你最高信念的 1–3 個 idea;說不出強烈理由的部位,砍掉、別稀釋。", - "quote": "看準了,就把蛋放一個籃子,然後死盯著那個籃子。", - "motive_q": "{max_ticker} 才佔你 {max_pct}%。集中派反問:如果這真是你最高信念的 idea,為什麼不是兩倍?那些低信念的部位,又為什麼還在稀釋你?" - }, - "加碼攤平": { - "stance": "conditional", "lean": "evidence", - "rubric_unit": "加碼=信念升級;錯了快砍、不凹", - "rule": "往下加只有在『宏觀/論點變更強(信念該升級)』時才做;若只是價格跌,錯了就果斷砍,別攤。", - "quote": "重點不是你對或錯,是對的時候賺多大、錯的時候虧多小。", - "motive_q": "{tickers} 你在虧損裡往下加。是『你的核心論點變得更強、值得升級信念倉』,還是『只是不認賠』?集中派錯了會砍,不會凹。" - }, - "出場紀律": { - "stance": "conditional", "lean": "thesis", - "rubric_unit": "論點破了就走、贏家讓它跑", - "rule": "賣出 = 你下注的宏觀/個股論點被推翻;論點還在、就讓贏家繼續跑,別因怕回吐就獲利了結。", - "quote": "當宏觀圖像改變,我會毫不猶豫地反向;戀棧一個壞掉的論點最貴。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些賣出是『下注的論點到頭了』,還是『賺了怕回吐、提早下車』?" - }, - "分散": { - "stance": "inverted", "lean": "concentrate", - "rubric_unit": "集中打敗分散;分散是把資本攤到沒把握的東西", - "rule": "別為了『看起來分散』而持有你沒把握的標的;寧可少而精、盯緊它,也不要多而散。", - "quote": "把資本攤薄到三、四十個名字上,是怕犯錯的人在做的事;真有把握就該集中。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。集中派不嫌你不夠分散——反而問:其餘那些『為了分散』的部位,你真有把握,還是只是怕集中?" - }, - "持有時間": { - "stance": "conditional", "lean": "trend", - "rubric_unit": "趨勢/論點在就抱,變了就走", - "rule": "持有期由你下注的宏觀趨勢/論點壽命決定,不由預設標籤;趨勢沒結束前別輕易下車。", - "quote": "賺大錢靠的是看對大方向後抱得住,不是頻繁進出。", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『趨勢還在所以續抱』,還是『套牢了改口說長期』?" - }, - "alpha/beta": { - "stance": "aligned", "lean": "decompose", - "rubric_unit": "分清宏觀方向紅利 vs 真選股/擇時", - "rule": "每季分清:你贏大盤,是『押對了宏觀/流動性方向』,還是『個股選得好』?兩個都好,但別搞混。", - "quote": "別把站對了一個大趨勢,誤認成你每一筆都選得準。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。集中派問:這是『你押對了一個大方向(該認、該重壓)』,還是『分散在一堆中信念上的平均結果』?" - }, - "進場": { - "stance": "conditional", "lean": "gap", - "rubric_unit": "在不對稱(賠率懸殊)且高信念時才進|待 engine B.9", - "rule": "進場前先問『這筆的賠率對稱嗎、我的信念有多強』;不對稱+高信念才重壓進場,普通機會就放掉。", - "quote": "一生能真正看準的大機會沒幾次;機會來時要敢於重壓。", - "motive_q": "{entry_ticker} 這筆進場,你的信念有多強、賠率多不對稱?是『難得的高信念大機會』,還是『順手下的中等注』?" - } + "position_sizing": {"rubric_unit": "Strong conviction should produce meaningful size", "stance": "inverted", "lean": "big", "rule": "Hold cash for weak ideas; use meaningful size only when evidence, asymmetry, and falsifiers support high conviction.", "quote": "When evidence and payoff are exceptional, small sizing can waste the opportunity. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Which independent evidence and payoff asymmetry justify that conviction level?"}, + "averaging_down": {"rubric_unit": "Add only on stronger evidence", "stance": "conditional", "lean": "evidence", "rule": "Add only when new independent evidence strengthens the thesis; a lower price and unchanged story are not enough.", "quote": "Conviction should rise because evidence improves, not because the position is losing. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What did you learn after entry that materially increased conviction?"}, + "exit_discipline": {"rubric_unit": "Exit quickly when the thesis changes", "stance": "conditional", "lean": "thesis", "rule": "Write the thesis and falsifier before entry, monitor large positions closely, and exit when the thesis changes.", "quote": "A large position requires fast recognition when the reason for owning it is no longer true. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which exits followed a broken thesis, and which reflected discomfort with a large gain?"}, + "diversification": {"rubric_unit": "Concentrate rather than allocate to weak ideas", "stance": "inverted", "lean": "concentrate", "rule": "Do not add low-conviction positions for appearance; concentrate only in distinct, well-evidenced theses.", "quote": "Cash is preferable to a portfolio filled with second-best ideas. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which are truly distinct high-conviction theses, and which dilute the best idea?"}, + "holding_period": {"rubric_unit": "Hold while the thesis and trend remain intact", "stance": "conditional", "lean": "trend", "rule": "Let the thesis and its expected catalyst determine the horizon; do not relabel a failed trade as a long-term holding.", "quote": "The horizon follows the thesis, not the need to avoid admitting error. (paraphrase)", "motive_q": "For {incon_tickers}, did the thesis horizon change, or did the label change after the trade moved against you?"}, + "alpha_beta": {"rubric_unit": "Separate thesis alpha from macro beta", "stance": "aligned", "lean": "decompose", "rule": "Decompose market, sector, liquidity, and thesis-specific return before attributing performance to conviction.", "quote": "A correct macro wind does not by itself prove instrument-selection skill. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. Which part came from the thesis rather than the shared macro driver?"}, + "entry_style": {"rubric_unit": "Enter where evidence and expectations diverge", "stance": "conditional", "lean": "gap", "rule": "Before entry, state what the market expects, what you believe instead, the catalyst, and the falsifier.", "quote": "Conviction needs a specific expectation gap, not a strong feeling. (paraphrase)", "motive_q": "For {entry_ticker}, what expectation gap and catalyst justified entry beyond recent price strength?"} } } diff --git a/skills/fomo-kernel/rubric/concentration-conviction.md b/skills/fomo-kernel/rubric/concentration-conviction.md index b44acab..36730a8 100644 --- a/skills/fomo-kernel/rubric/concentration-conviction.md +++ b/skills/fomo-kernel/rubric/concentration-conviction.md @@ -1,36 +1,31 @@ -# Lens · 集中信念(Concentration & Conviction)— v1 draft - -> 原則蒸餾自 Druckenmiller / Soros 公開訪談與演講。原則/學派命名,真人來源見 Sources。 -> ⚠️ **draft**:引言為意譯,**尚未對原始訪談逐句校對**,上線前需 verbatim 對齊審查。 -> 這把尺與 VY 最大反轉在 **sizing / 分散**:VY 說「別梭哈、雙紅線」,此派說「高信念就該重壓,小注是浪費資本」。 - -## 脊椎(5 支柱) -1. 高信念才下注,沒信念就空手——別把資本浪費在一堆低信念標的。 -2. 看準了就重壓:集中在你最有把握的 1–3 個 idea。 -3. 重點不是你對或錯,是對的時候賺多大、錯的時候虧多小。 -4. 集中反而降低風險,因為大部位逼你全神貫注、錯了快砍。 -5. top-down:先抓宏觀/流動性的大方向,再選工具。 - -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | -|---|---|---|---| -| 部位 sizing | inverted | big | 高信念重壓,小注是浪費(點亮 sizing 維) | -| 分散 | inverted | concentrate | 分散是把資本攤到沒把握的東西 | -| 加碼攤平 | conditional | evidence | 加碼=信念升級;錯了快砍不凹 | -| 出場紀律 | conditional | thesis | 論點破就走、贏家讓它跑 | -| 持有時間 | conditional | trend | 趨勢/論點在就抱 | -| alpha/beta | aligned | decompose | 宏觀方向紅利 vs 真選股 | -| 進場 | conditional | gap | 不對稱+高信念才重壓進場 | - -## 關鍵單元(grounded) -- **集中重壓**【意譯】(Druckenmiller):「看準了,就把蛋放一個籃子,然後死盯著它。」 -- **賺多大 vs 虧多小**【意譯】(Druckenmiller/Soros):績效不在勝率,在對的時候賺多大、錯的時候虧多小。 -- **集中降低風險**【意譯】(Druckenmiller):大部位逼你全神貫注;攤薄到 30–40 個名字是怕犯錯的人在做的事。 -- **快砍**【意譯】(Druckenmiller):宏觀圖像改變時毫不猶豫反向;戀棧壞掉的論點最貴。 - -## 待辦 -- verbatim 對齊審查:回原始訪談(如 1988 Soros 對話、各場演講)校引言。 -- 進場(EN)需 engine B.9。 - -### Sources -- [Druckenmiller: concentrated bets](https://acquirersmultiple.com/2021/05/stanley-druckenmiller-make-big-concentrated-bets-when-you-have-a-lot-of-conviction/) · [30 quotes (TraderLion)](https://traderlion.com/quotes/druckenmiller-quotes/) · [Macro Ops lessons](https://macro-ops.com/lessons-from-a-trading-great-stanley-druckenmiller/) +# Lens: concentration and conviction — v1 draft + +This lens distills public Druckenmiller and Soros material. It directly conflicts with risk-capped sizing: high conviction should produce a meaningful bet, while weak conviction should produce no position. + +## Five principles + +1. Hold cash rather than allocate to low-conviction ideas. +2. Concentrate in the small number of ideas with the strongest evidence. +3. Pay more attention to payoff asymmetry than win rate. +4. Large positions require close attention and fast exits when the thesis changes. +5. Start with macro and liquidity, then choose the expression. + +## Stance map + +| Dimension | Stance | Lean | +|---|---|---| +| Sizing | inverted | big | +| Diversification | inverted | concentrate | +| Averaging down | conditional | evidence | +| Exit | conditional | thesis | +| Holding period | conditional | trend | +| Alpha/beta | aligned | decompose | +| Entry | conditional | gap | + +All quotations in this draft are paraphrases until verified against primary interviews. Do not publish them as verbatim quotes. + +## Sources + +- [Druckenmiller on concentrated bets](https://acquirersmultiple.com/2021/05/stanley-druckenmiller-make-big-concentrated-bets-when-you-have-a-lot-of-conviction/) +- [Druckenmiller quote collection](https://traderlion.com/quotes/druckenmiller-quotes/) +- [Macro Ops lessons](https://macro-ops.com/lessons-from-a-trading-great-stanley-druckenmiller/) diff --git a/skills/fomo-kernel/rubric/grayscale-thinking.lens.json b/skills/fomo-kernel/rubric/grayscale-thinking.lens.json index 490878b..ac16a98 100644 --- a/skills/fomo-kernel/rubric/grayscale-thinking.lens.json +++ b/skills/fomo-kernel/rubric/grayscale-thinking.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "灰階思考", - "master": "灰階思考", - "source": "原則蒸餾自 謝孟恭(股癌)《灰階思考》(天下文化, 2021)+ 公開節目/訪談,引用非轉載、非經本人背書。⚠️ 引言為書摘意譯,尚未對原書逐句校對,上線前需 verbatim 對齊審查。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 grayscale-thinking.md。stance 多為 conditional(反教條,鮮少 unconditional/inverted);分歧主要來自獨特 lean,不來自判得更兇。招牌 fork 在 alpha/beta(別深信單一來源=回音檢查,會點亮原本全 aligned 的 alpha/beta 維)與 出場(反市場情緒、拋得太早)。", - - "master_intro": { - "one_line": "灰階思考的核心:別非黑即白、別深信單一來源。它不給你一套要照抄的哲學——它剛好相反:逼你問『我是不是太信某一套、某一個人了』。", - "pillars": [ - "灰階思考——別非黑即白;策略選定也別故步自封,持續接收新訊息、微調修正", - "別深信單一來源——多元參考,切莫深信某位人士或團體的單一意見;質疑是態度,求證是行動", - "反市場情緒——眾人歡呼時果斷賣、沒人要時敢買;寧可拋太早躲災難", - "避險 > 勝率——賺多賠小;真正的功夫不是勝率,是避險", - "紀律與資金水位——加減碼看資金水位/計畫,不被短期波動嚇進嚇出;買賣不糾結" - ], - "why_it_matters": "這把尺偏『反教條 + 多元驗證』。它不逼你變成某種交易者——它的核心剛好是別太信任何單一一套(包括它自己、包括任何 KOL)。它照的是你有沒有非黑即白、有沒有只信一個來源、有沒有在眾人狂熱時還在追。" - }, - - "strength_intro": "先講你做對的一件事(灰階派先認你在某處保留了彈性、沒被單一來源或情緒帶走):", - + "philosophy": "Grayscale thinking", + "master": "Grayscale thinking", + "source": "Distilled from public work by Hsieh Meng-kung. All card language below is paraphrase pending verification against the original text.", + "_note": "Runtime meta-epistemic lens. It questions certainty, source independence, and evidence updates rather than prescribing one trading style.", + "master_intro": {"one_line": "Replace binary certainty with explicit probabilities, independent evidence, and continuous updating.", "pillars": ["Avoid black-and-white conclusions", "Verify independent sources", "Examine crowd emotion", "Prioritize asymmetry over win rate", "Size from a plan, not emotion"], "why_it_matters": "This lens tests whether apparent conviction is calibrated or merely repeated certainty from one source."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "stance": "conditional", "lean": "cash-level", - "rubric_unit": "資金水位決定加減碼,不被波動嚇", - "rule": "加減碼的依據是『資金水位/計畫』,不是『今天跌了好可怕』;下單前先確認這筆在你整體水位的位置。", - "quote": "用資金水位決定加減碼,別被短期波動嚇退。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。你加減碼是看資金水位/計畫,還是被它的短期波動嚇進嚇出?" - }, - "加碼攤平": { - "stance": "conditional", "lean": "script", - "rubric_unit": "劇本不對就退,不是往下凹", - "rule": "往下加之前先問『劇本還照我設想走嗎』;劇本破了就斷然退場、另起爐灶,不是攤平等回本。", - "quote": "如果事情不照你原先設想的劇本走,就斷然退場、另起爐灶。", - "motive_q": "{tickers} 你在虧損裡往下加。灰階派問:劇本還照你當初設想走嗎?還是劇本早就破了、你只是不想認賠?" - }, - "出場紀律": { - "stance": "conditional", "lean": "early-defensive", - "rubric_unit": "反市場情緒,寧可拋太早躲災難", - "rule": "出場看市場情緒與劇本,不看捨不得;眾人最狂熱時就該開始減,寧可拋早一點。", - "quote": "我能躲過災難,是因為我每次都拋得太早。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來續漲。灰階派寧可拋太早躲災難——但你這是『反市場情緒的紀律』,還是『沒有劇本、隨手就跑』?" - }, - "分散": { - "stance": "conditional", "lean": "core-satellite", - "rubric_unit": "防守底倉 + 衛星,不是全押衛星", - "rule": "先有一個防守底倉(市值型 ETF),再用一小部分資金當衛星去博爆發;別把整個帳戶都當衛星。", - "quote": "市值型 ETF 當防守底倉,小部分資金把自己當 VC 創投在操盤。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。你有沒有一個防守底倉(市值 ETF),還是整個帳戶都在衛星賭注上?" - }, - "持有時間": { - "stance": "aligned", "lean": "flexible", - "rubric_unit": "灰階:隨新資訊修正,不故步自封", - "rule": "持有期不是死標籤;選定策略後持續接收新訊息、微調修正——但別讓『修正』變成套牢後的合理化漂移。", - "quote": "策略選定也別故步自封,持續接收最新市場訊息,對策略作出微調和修正。", - "motive_q": "{incon_tickers} 你同一檔又短又長。這是灰階式『隨新資訊修正』,還是套牢後沒劇本的漂移?" - }, - "alpha/beta": { - "stance": "conditional", "lean": "multi-source", - "rubric_unit": "別深信單一來源(回音檢查)", - "rule": "下注前先 cross-check 幾個彼此獨立的來源;切莫深信某位人士/團體的單一意見——質疑是態度,求證是行動。", - "quote": "切莫深信某位人士或團體的單一意見;質疑是態度,求證是行動。", - "motive_q": "你贏大盤 {excess}pp。灰階派問:這是你 cross-check 多個獨立來源後的判斷,還是某個 KOL/題材的回音?這筆你驗證過幾個彼此獨立的來源?" - }, - "進場": { - "stance": "conditional", "lean": "contrarian-sentiment", - "rubric_unit": "反市場情緒進場|待 engine B.9", - "rule": "進場前先看市場情緒在哪一端:眾人歡呼狂熱時別追,沒人要、被嫌棄時才是好球;熱題材/主流共識要警惕。", - "quote": "眾人為股市歡呼時果斷賣,便宜到沒人要時敢於買。", - "motive_q": "{entry_ticker} 你進場時,市場情緒是狂熱還是沒人要?灰階派反市場情緒:你是『眾人歡呼時還在追』,還是『沒人要時敢買』?" - } + "position_sizing": {"rubric_unit": "Adjust size from a capital plan", "stance": "conditional", "lean": "cash-level", "rule": "Tie size to capital level, uncertainty, and a prewritten script; do not let recent P&L set the risk budget.", "quote": "Position size should follow the plan and available capital, not short-term emotion. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Did this size follow a written capital rule or confidence created by recent price action?"}, + "averaging_down": {"rubric_unit": "Update a script with new information", "stance": "conditional", "lean": "script", "rule": "Add only when the original script specified the condition and new evidence changes the probability estimate.", "quote": "Updating is not changing the story after the outcome; it requires new information. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. Which new fact changed your probability estimate, and was that update allowed by the original script?"}, + "exit_discipline": {"rubric_unit": "Permit defensive action before certainty", "stance": "conditional", "lean": "early-defensive", "rule": "Reduce risk when evidence deteriorates enough to change expected value; do not wait for binary certainty.", "quote": "Risk can be reduced before the world resolves into a certain yes or no. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Were the exits calibrated defensive actions or reactions to discomfort?"}, + "diversification": {"rubric_unit": "Separate core beliefs from exploratory satellites", "stance": "conditional", "lean": "core-satellite", "rule": "Use a core-satellite structure and verify that satellites add independent information or payoff drivers.", "quote": "Different labels are not diversification when the evidence and drivers are the same. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which are core exposures, which are satellites, and what independent role does each satellite play?"}, + "holding_period": {"rubric_unit": "Keep the horizon flexible but explicit", "stance": "aligned", "lean": "flexible", "rule": "Set a working horizon, then update it only when evidence changes; never use flexibility to avoid falsification.", "quote": "A flexible view still needs an explicit reason for every update. (paraphrase)", "motive_q": "For {incon_tickers}, what new evidence changed the horizon rather than merely changing your comfort with the result?"}, + "alpha_beta": {"rubric_unit": "Require genuinely independent evidence", "stance": "conditional", "lean": "multi-source", "rule": "Before calling a result alpha, separate market exposure and verify that supporting signals come from independent sources and incentives.", "quote": "Many repetitions of one source do not become many independent reasons. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. How many supporting reasons are independent rather than echoes of one source?"}, + "entry_style": {"rubric_unit": "Examine crowd emotion without automatically opposing it", "stance": "conditional", "lean": "contrarian-sentiment", "rule": "At entry, record crowd sentiment, your probability estimate, disconfirming evidence, and what would make consensus correct.", "quote": "Crowd emotion is evidence to examine, not an instruction to follow or oppose. (paraphrase)", "motive_q": "For {entry_ticker}, did you evaluate sentiment and counterevidence, or did you simply follow or fight the crowd?"} } } diff --git a/skills/fomo-kernel/rubric/grayscale-thinking.md b/skills/fomo-kernel/rubric/grayscale-thinking.md index ec634e3..86ec396 100644 --- a/skills/fomo-kernel/rubric/grayscale-thinking.md +++ b/skills/fomo-kernel/rubric/grayscale-thinking.md @@ -1,48 +1,37 @@ -# Lens · 灰階思考(Grayscale Thinking)— v1 draft - -> 原則蒸餾自 **謝孟恭(股癌)《灰階思考》**(天下文化, 2021)+ 公開節目/訪談。原則/學派命名,真人來源見 Sources。 -> ⚠️ **draft**:引言為書摘**意譯**,尚未對原書逐句校對,上線前需 verbatim 對齊審查。 -> **方法論註(誠實修正)**:先前只看 gooaye-tracker(追他的**選股題材**)曾誤判「他無紀律哲學」。他的**書**才是行為/心態層的來源——`narrative.md` §6「無紀律 SOP」說的是 podcast 喊題材時不逐集附停損,跟書裡的心法不矛盾。 - -## 脊椎(5 支柱) -1. **灰階思考**——別非黑即白;策略選定也別故步自封,持續接收新訊息、微調修正。 -2. **別深信單一來源**——多元參考,切莫深信某位人士/團體的單一意見;「質疑是態度,求證是行動」。 -3. **反市場情緒**——眾人歡呼時果斷賣、沒人要時敢買;「我能躲過災難,因為每次都拋得太早」。 -4. **避險 > 勝率**——賺多賠小;真正的功夫不是勝率,是避險。 -5. **紀律與資金水位**——加減碼看資金水位/計畫,不被短期波動嚇進嚇出;買賣不糾結。 - -## 在多大師庫裡的獨特性(為什麼不跟 VY/動能 重複) -- 其餘 5 把尺各是**一套明確哲學**;灰階是唯一「**別太信任何單一一套**」的 meta-epistemic 尺。 -- **招牌 fork = alpha/beta 的「回音檢查」**:它在原本全 `aligned` 的 alpha/beta 維引入獨特 lean `multi-source`,把那一維**點亮**成岔路(類比集中信念點亮 sizing)。漂亮處:這條正是 MK 自己的原話(「切莫深信單一意見」),不是我們硬安。 -- 次要 fork = **出場 `early-defensive`**(反市場情緒、拋太早躲災難)——跟動能「讓贏家跑」對撞。 - -## 整合進來的「股癌常推策略」(apply-to-self,不越界成選股建議) -| 策略 | 落點 | -|---|---| -| 核心-衛星(ETF 防守底倉 + 衛星) | 分散維 lean `core-satellite` + style-fit §2-8 已有偵測 | -| 資金水位 sizing | sizing 維 lean `cash-level` | -| 別深信單一來源/質疑求證 | alpha/beta 維 lean `multi-source`(回音檢查,呼應 VY A3) | -| 劇本不對就退 | 加碼維 lean `script` | -| **選股外包**(個股勝率~50% vs 題材 83%) | **engine `prescribe()` 外包層**(master-agnostic;只在用戶 alpha 歸因為負時端,流程建議非標的建議) | - -## stance / lean(供 compare_lenses) -| dim | stance | lean | +# Lens: grayscale thinking — v1 draft + +This lens distills public work by Hsieh Meng-kung. Unlike a doctrine that tells the user which style is correct, it is a meta-epistemic lens: avoid binary thinking, distrust single-source certainty, and update as evidence changes. + +## Five principles + +1. Avoid black-and-white conclusions; keep updating a chosen strategy with new information. +2. Do not rely on a single authority or information source. Question first, then verify. +3. Treat crowd euphoria and panic as signals to examine rather than instructions to follow. +4. Risk control and payoff asymmetry matter more than win rate. +5. Adjust size from a plan and capital level rather than short-term emotion. + +## Distinctive role + +- The signature alpha/beta question is an echo check: are several reasons truly independent, or are they repetitions of one source? +- Its exit lean is defensively early, which conflicts with pure momentum's preference to let winners run. +- It supports a core-satellite structure and evidence-driven script changes without recommending securities. + +## Stance map + +| Dimension | Stance | Lean | |---|---|---| -| alpha/beta | conditional | multi-source ★招牌 | -| 出場紀律 | conditional | early-defensive | -| 進場 | conditional | contrarian-sentiment | -| 分散 | conditional | core-satellite | -| 加碼攤平 | conditional | script | -| 部位 sizing | conditional | cash-level | -| 持有時間 | aligned | flexible | - -> 全 conditional/aligned、無 unconditional/inverted——因為「灰階」本身就是反教條,它的分歧來自**做法不同(lean)**,不來自判得更兇。 - -## 待辦 -- verbatim 對齊審查:翻《灰階思考》對校 5 句引言(現為書摘意譯);特別是「拋得太早」「切莫深信單一意見」「劇本不對就退」。 -- 「選股外包」話術強化進 `prescribe()`(用我們 gooaye-tracker 的 83%/50% 數據背書)。 -- 進場(EN)需 engine B.9。 - -### Sources -- [閱讀前哨站《灰階思考》九重點](https://readingoutpost.com/grey-thinking/) · [博客來 灰階思考](https://www.books.com.tw/products/0010888435) · [Smart 自學網](https://smart.businessweekly.com.tw/Reading/IndepArticle.aspx?id=6005321) · [ETtoday 核心衛星/VC 兩成](https://finance.ettoday.net/news/1817340) · [Gooaye 聊停損聊避險](https://gooaye.com/%E8%81%8A%E8%81%8A%E5%81%9C%E6%90%8D%E3%80%81%E8%81%8A%E8%81%8A%E9%81%BF%E9%9A%AA/) -- 內部:`investment_note/research/influencers/gooaye/`(215 集量化 narrative) +| Alpha/beta | conditional | multi-source | +| Exit | conditional | early-defensive | +| Entry | conditional | contrarian-sentiment | +| Diversification | conditional | core-satellite | +| Averaging down | conditional | script | +| Sizing | conditional | cash-level | +| Holding period | aligned | flexible | + +All book excerpts in this draft are paraphrases until checked against the original text. Treat the internal influencer corpus as research evidence, not a public citation. + +## Sources + +- *Grayscale Thinking* (CommonWealth Publishing, 2021) +- [Reading Outpost summary](https://readingoutpost.com/grey-thinking/) +- [Core-satellite discussion](https://finance.ettoday.net/news/1817340) diff --git a/skills/fomo-kernel/rubric/margin-of-safety.lens.json b/skills/fomo-kernel/rubric/margin-of-safety.lens.json index 53a2541..d60c2fd 100644 --- a/skills/fomo-kernel/rubric/margin-of-safety.lens.json +++ b/skills/fomo-kernel/rubric/margin-of-safety.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "安全邊際", - "master": "安全邊際", - "source": "原則蒸餾自 Graham–Klarman 價值投資傳統(Margin of Safety / The Intelligent Investor),引用非轉載、非經本人背書。⚠️ 引言為意譯,尚未對原書逐句校對,上線前需 verbatim 對齊審查。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 margin-of-safety.md。stance/lean 供 compare_lenses。此派與動能派在『進場』最大反轉:動能只買強(lean strength)、此派買折價(lean weakness)——兩者 stance 都 inverted 但方向相反,是 2-D stance 的關鍵案例。", - - "master_intro": { - "one_line": "安全邊際派的核心:第一條,別虧錢;第二條,別忘了第一條。它不問你『漲不漲』,問你『就算你看錯,這個折價夠不夠保護你』。", - "pillars": [ - "首要目標是不虧錢——保本優先於追報酬", - "只在『夠便宜』(顯著折價於內在價值)時買,折價就是你的安全邊際", - "沒有好球就持現金,不為了進場而進場(耐心)", - "算內在價值、要求安全邊際,而不是預測價格", - "風險是『永久損失』,不是帳面波動" - ], - "why_it_matters": "這把尺偏『保本 + 折價』。如果你是動能/成長派,它有些條對你是反的——它會把你的『追突破』讀成『沒有安全邊際的投機』。它照的是你買得夠不夠便宜、有沒有為看錯留緩衝,不是逼你只買低 PE。" - }, - - "strength_intro": "先講你做對的一件事(安全邊際派先認你守住的那條保本線):", - + "philosophy": "Margin of safety", + "master": "Margin of safety", + "source": "Distilled from the Graham-Klarman value tradition. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. It seeks verified discount and permanent-loss protection rather than price strength.", + "master_intro": {"one_line": "Protect capital by demanding a meaningful discount to a defensible estimate of intrinsic value.", "pillars": ["Protect capital first", "Demand a margin of safety", "Hold cash when opportunity is weak", "Estimate rather than predict", "Define risk as permanent loss"], "why_it_matters": "This lens asks whether downside support and valuation evidence existed before capital was committed."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "stance": "aligned", "lean": "risk-capped", - "rubric_unit": "保本優先,單筆別大到會永久傷害本金", - "rule": "單筆 size 以『就算永久損失也不傷筋動骨』為上限;愈不確定、size 愈小。", - "quote": "投資的首要目標,是不要虧錢。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。萬一這筆永久損失一半,你的本金撐得住嗎?這個 size 是『算過最壞情況』,還是『很看好就壓上去』?" - }, - "加碼攤平": { - "stance": "conditional", "lean": "discount", - "rubric_unit": "折價變厚才加;內在價值下修就認錯", - "rule": "往下加只在『折價擴大、安全邊際變厚,且內在價值沒變』時才做;若內在價值也下修 → 認錯、不加。", - "quote": "便宜買進、保留安全邊際,是你抵禦判斷錯誤的唯一保險。", - "motive_q": "{tickers} 你在虧損裡往下加。是『價格跌了但內在價值沒變、折價變更厚(安全邊際變大)』,還是『內在價值其實也壞了、你只是想拉低成本』?" - }, - "出場紀律": { - "stance": "conditional", "lean": "intrinsic", - "rubric_unit": "到內在價值才賣,不被波動洗出去", - "rule": "賣出條件 = 價格回到你算的內在價值,或論點/基本面被推翻——不是價格波動或恐慌。", - "quote": "價格是你付出的,價值是你得到的;到價才走。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些賣出是『到了你算的內在價值』,還是『漲一點就怕回吐、提早跑』?" - }, - "分散": { - "stance": "conditional", "lean": "moderate", - "rubric_unit": "適度分散 + 持現金等好球", - "rule": "適度分散到幾個彼此獨立的折價機會;沒有夠便宜的標的時,持現金也是部位,別硬湊。", - "quote": "現金是一種選擇權——在別人恐慌時,它讓你能出手。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。安全邊際派問:這幾檔的折價/安全邊際是各自獨立的,還是同一個故事崩了會一起歸零?你手上有留現金等更好的球嗎?" - }, - "持有時間": { - "stance": "conditional", "lean": "until-value", - "rubric_unit": "耐心持有到價值實現", - "rule": "持有期 = 直到價值被市場認回來;只要安全邊際還在,短期波動不該逼你下車。", - "quote": "價值投資要的是耐心:市場短期是投票機,長期是體重計。", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『耐心等價值實現』,還是『套牢了才改口說自己長期持有』?" - }, - "alpha/beta": { - "stance": "aligned", "lean": "decompose", - "rubric_unit": "報酬來自折價收斂,還是大盤?", - "rule": "每季分清:你贏大盤,是『買到真折價、價值收斂』,還是『大盤把所有東西一起推高』?", - "quote": "市場上漲時人人都是天才;退潮才知道誰在裸泳。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。安全邊際派問:這是『你買到了真折價』,還是『高 β、大盤順風把你一起抬上去』?" - }, - "進場": { - "stance": "inverted", "lean": "weakness", - "rubric_unit": "逢折價承接是策略,不是接刀(前提:算過內在價值)|待 engine B.9", - "rule": "只在『價格顯著低於你算的內在價值』時買——折價就是進場理由;但前提是你真的算過價值、確認有安全邊際,不是『跌很多就以為便宜』。", - "quote": "最好的投資機會,往往出現在別人恐慌拋售、好公司被錯殺的時候。", - "motive_q": "{entry_ticker} 你逢低承接。對安全邊際派這不是接刀,是策略——但前提是:你算過內在價值、確認有顯著折價了嗎,還是只是『跌這麼多應該很便宜』的感覺?" - } + "position_sizing": {"rubric_unit": "Cap permanent-loss risk", "stance": "aligned", "lean": "risk-capped", "rule": "Size from estimated permanent-loss downside and uncertainty, not only upside potential.", "quote": "Capital protection comes before return maximization. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. What permanent-loss scenario was used to cap the position?"}, + "averaging_down": {"rubric_unit": "Add only when the verified discount widens", "stance": "conditional", "lean": "discount", "rule": "Add only after re-estimating intrinsic value and confirming that the margin of safety widened for reasons unrelated to thesis deterioration.", "quote": "A lower price helps only if value and downside support remain intact. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new valuation work showed a wider margin of safety rather than a damaged asset?"}, + "exit_discipline": {"rubric_unit": "Exit when value is realized or the estimate breaks", "stance": "conditional", "lean": "intrinsic", "rule": "Exit when price reaches defensible value, intrinsic value falls, or the original margin-of-safety calculation is invalidated.", "quote": "The sale decision follows value and downside, not the emotional need to lock in a result. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which exits reflected value realization, and which lacked an updated value estimate?"}, + "diversification": {"rubric_unit": "Use moderate diversification against estimation error", "stance": "conditional", "lean": "moderate", "rule": "Diversify enough to survive valuation error while avoiding positions with no adequate margin of safety.", "quote": "Diversification can protect against analytical error, but it cannot make an overpriced asset safe. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. How many independent valuation errors could damage the portfolio at once?"}, + "holding_period": {"rubric_unit": "Hold until value is realized or invalidated", "stance": "conditional", "lean": "until-value", "rule": "Set the horizon from the value-realization path and catalyst; do not turn missing realization into an indefinite hold.", "quote": "Patience needs a valuation thesis and a path for value to matter. (paraphrase)", "motive_q": "For {incon_tickers}, did the value-realization path change, or did the horizon change only because the price stayed below cost?"}, + "alpha_beta": {"rubric_unit": "Separate value realization from market beta", "stance": "aligned", "lean": "decompose", "rule": "Decompose market and factor exposure before attributing return to the margin-of-safety analysis.", "quote": "A rising market can obscure whether the valuation thesis added value. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. What return came from closing the value gap rather than broad factor exposure?"}, + "entry_style": {"rubric_unit": "Prefer verified discount over price strength", "stance": "inverted", "lean": "weakness", "rule": "Enter only after estimating intrinsic value, downside, and a meaningful margin of safety; weakness alone is not enough.", "quote": "Price is attractive only in relation to defensible value. (paraphrase)", "motive_q": "For {entry_ticker}, what valuation gap and downside support justified entry rather than simply buying weakness?"} } } diff --git a/skills/fomo-kernel/rubric/margin-of-safety.md b/skills/fomo-kernel/rubric/margin-of-safety.md index d1aa89f..09d3f64 100644 --- a/skills/fomo-kernel/rubric/margin-of-safety.md +++ b/skills/fomo-kernel/rubric/margin-of-safety.md @@ -1,36 +1,32 @@ -# Lens · 安全邊際(Margin of Safety)— v1 draft - -> 原則蒸餾自 Graham–Klarman 價值投資傳統(Margin of Safety / The Intelligent Investor)。原則/學派命名,真人來源見 Sources。 -> ⚠️ **draft**:引言為意譯,**尚未對原書逐句校對**,上線前需 verbatim 對齊審查。 -> 與動能派在 **進場** 正面對撞:動能只買強(lean strength)、此派買折價(lean weakness)——兩者 stance 都 inverted、方向相反,是 2-D stance 的關鍵案例。 - -## 脊椎(5 支柱) -1. 首要目標是不虧錢——保本優先於追報酬(「第一條別虧錢;第二條別忘了第一條」)。 -2. 只在「夠便宜」(顯著折價於內在價值)時買,折價就是你的安全邊際。 -3. 沒有好球就持現金,不為了進場而進場(耐心)。 -4. 算內在價值、要求安全邊際,而不是預測價格。 -5. 風險是「永久損失」,不是帳面波動。 - -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | -|---|---|---|---| -| 進場 | inverted | weakness | 逢折價承接是策略(前提:算過內在價值) | -| 加碼攤平 | conditional | discount | 折價變厚才加;內在價值下修就認錯 | -| 出場紀律 | conditional | intrinsic | 到內在價值才賣,不被波動洗掉 | -| 分散 | conditional | moderate | 適度分散+持現金等好球 | -| 持有時間 | conditional | until-value | 耐心持有到價值實現 | -| 部位 sizing | aligned | risk-capped | 單筆別大到會永久傷本金 | -| alpha/beta | aligned | decompose | 折價收斂 vs 大盤順風 | - -## 關鍵單元(grounded) -- **不虧錢優先**【意譯】(Klarman/Graham):投資首要目標是不虧錢;保本優先於追報酬。 -- **安全邊際**【意譯】(Graham):用顯著折價買進,折價是你抵禦判斷錯誤的緩衝。 -- **持現金等好球**【意譯】(Klarman):沒有夠便宜的標的時,現金是一種選擇權,別硬湊。 -- **風險=永久損失**【意譯】(Klarman):風險不是波動,是本金的永久折損。 - -## 待辦 -- verbatim 對齊審查:Klarman《Margin of Safety》一書罕見且作者護其甚嚴 → 引用務必走「原則(多源自 Graham 公版)」而非該書 verbatim,標源要乾淨。 -- 進場(EN)需 engine B.9。 - -### Sources -- [Margin of Safety (book) — Wikipedia](https://en.wikipedia.org/wiki/Margin_of_Safety_(book)) · [James Clear summary](https://jamesclear.com/book-summaries/margin-of-safety-risk-averse-value-investing-strategies-for-the-thoughtful-investor) +# Lens: margin of safety — v1 draft + +This lens represents the Graham-Klarman value tradition. It conflicts directly with momentum on entry: one seeks a verified discount while the other requires demonstrated strength. + +## Five principles + +1. Protect capital before maximizing return. +2. Buy only with a meaningful discount to estimated intrinsic value. +3. Hold cash when no adequate opportunity exists. +4. Estimate value and demand a margin of safety instead of predicting price. +5. Define risk as permanent capital loss rather than mark-to-market volatility. + +## Stance map + +| Dimension | Stance | Lean | +|---|---|---| +| Entry | inverted | weakness | +| Averaging down | conditional | discount | +| Exit | conditional | intrinsic | +| Diversification | conditional | moderate | +| Holding period | conditional | until-value | +| Sizing | aligned | risk-capped | +| Alpha/beta | aligned | decompose | + +This draft uses paraphrased principles. Because Klarman's book is scarce and tightly controlled, prefer cleanly sourced Graham principles rather than uncertain verbatim quotations. + +## Sources + +- Benjamin Graham, *The Intelligent Investor* +- Seth Klarman, *Margin of Safety* +- [Book overview](https://en.wikipedia.org/wiki/Margin_of_Safety_(book)) +- [James Clear summary](https://jamesclear.com/book-summaries/margin-of-safety-risk-averse-value-investing-strategies-for-the-thoughtful-investor) diff --git a/skills/fomo-kernel/rubric/michael-burry.lens.json b/skills/fomo-kernel/rubric/michael-burry.lens.json index f798855..46558c3 100644 --- a/skills/fomo-kernel/rubric/michael-burry.lens.json +++ b/skills/fomo-kernel/rubric/michael-burry.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "深度價值 · 逆勢 · 先看下檔", + "philosophy": "Deep value, contrarian entry, and downside first", "master": "Michael Burry", - "source": "原則蒸餾自 Michael Burry 公開訪談 / 致投資人信與 virattt/ai-hedge-fund 的 michael_burry agent prompt(MIT,以 FCF yield/EV-EBIT/資產負債表/內部人買入/逆勢情緒編碼此哲學)。引用非轉載、非經本人背書。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 michael-burry.md。stance/lean 供 compare_lenses。與『安全邊際』最大差異在【進場】:安全邊際是 weakness(買弱/便宜),此派是 contrarian(買『被全市場罵、像被車輾過』但帳上硬的),多一層『逆勢 + 硬催化劑』。引言已對 verbatim 校對(英文原句見 michael-burry.md),少數標【意譯】。", - - "master_intro": { - "one_line": "這套尺的核心:用硬數字找『被全市場唾棄、但帳上撐得住』的便宜貨,先把下檔鎖死,再等市場認錯。它不問你『漲勢強不強』,問你『最壞會虧多少、為什麼這價格是錯的』。", - "pillars": [ - "100% 以安全邊際為本——先保護下檔,避免本金永久損失", - "逆勢:新聞一片罵聲可以是你的朋友,只要基本面是硬的", - "先看下檔:避開高槓桿的資產負債表,最壞情況先算清楚", - "要硬催化劑:內部人買入、買回庫藏股、賣資產——光便宜不夠", - "用硬數字說話:FCF yield、EV/EBIT、淨現金,不靠故事" - ], - "why_it_matters": "這把尺偏『逆勢深度價值 + 下檔優先』。如果你是動能/成長客,有些條對你是反的——它會把你『追強勢』讀成『買在情緒最高、安全邊際最薄』。它照的是你買的東西夠不夠便宜、下檔有沒有鎖死、有沒有人罵到錯殺,不是逼你變成放空者。" - }, - - "strength_intro": "先說你做對的一件事(這派先認你買在無人問津、下檔算清楚的那一筆):", - + "source": "Distilled from public Scion letters and Michael Burry interviews. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. Cheapness must be paired with hard downside analysis and a catalyst.", + "master_intro": {"one_line": "Start with downside, buy unpopular value only when hard fundamentals and a catalyst support repricing.", "pillars": ["Protect against permanent loss", "Use contrarian sentiment as a setup, not proof", "Calculate downside first", "Require a catalyst", "Prefer hard measures over narrative"], "why_it_matters": "This lens tests whether a contrarian position is analytically cheap or merely unpopular."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "size 由『下檔/安全邊際』決定,不由上檔想像", - "stance": "conditional", "lean": "downside-first", - "rule": "部位大小先由『最壞情況我虧多少、安全邊際多厚』決定;安全邊際薄的,再誘人也只給小 size。", - "quote": "我所有的選股,100% 建立在安全邊際這個概念上。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這個 size 是『你算過最壞情況、下檔被安全邊際鎖死』,還是『被上檔想像沖昏頭、根本沒算最壞會虧多少』?" - }, - "加碼攤平": { - "rubric_unit": "帳上硬 + 安全邊際更厚才加;資產負債表壞了就不碰", - "stance": "conditional", "lean": "margin-of-safety", - "rule": "往下加碼只有在『資產負債表仍硬、價格更低=安全邊際更厚』時才做;只要槓桿/基本面變差,價格再低也不加。", - "quote": "先聚焦下檔——避開有槓桿的資產負債表。【意譯】", - "motive_q": "{tickers} 你在虧損裡往下加。是『帳上現金/負債依舊硬、跌出更厚的安全邊際』,還是『基本面其實在惡化、你只是想攤平等回本』?" - }, - "出場紀律": { - "rubric_unit": "賣在『被擦亮、價值兌現』時,不靠手感", - "stance": "conditional", "lean": "value-realized", - "rule": "賣出=『便宜被市場認回來了(價值兌現/催化劑實現)』,不是因為帳面終於翻紅就落袋。", - "quote": "我買進那些像被車輾過的冷門公司,等它們被擦亮一點後再賣。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些是『折價已經補回、價值兌現了』,還是『被罵到便宜時敢買、剛翻紅就怕了提早賣』?" - }, - "分散": { - "rubric_unit": "集中在少數算得清、下檔硬的深度價值點子", - "stance": "inverted", "lean": "few-deep", - "rule": "別為了分散去持你沒算清楚下檔的標的;寧可少數每一檔都用硬數字算過、下檔鎖死的深度價值機會。", - "quote": "我偏好看具體、個別的機會,把每一個都研究到底。【意譯】", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。這派不嫌你集中——反問:其餘那些部位,每一檔的下檔你都用硬數字算過了,還是只是『為了分散』隨手放的?" - }, - "持有時間": { - "rubric_unit": "抱到市場認錯/催化劑兌現,可以早、可以久", - "stance": "conditional", "lean": "until-repriced", - "rule": "持有期由『折價補回 / 催化劑兌現』決定,不由心情;對的時候你常會早,要忍得住孤獨抱到市場認回來。", - "quote": "我可能會早,但我不會錯。【意譯:'I may be early, but I'm not wrong.'】", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『催化劑/折價的判斷變了所以調整』,還是『被軋到受不了就改口、或便宜時根本沒打算抱到兌現』?" - }, - "alpha/beta": { - "rubric_unit": "alpha 來自買在無人敢買的便宜,不是押波動", - "stance": "aligned", "lean": "decompose", - "rule": "每季分清:你贏大盤,是『買在被錯殺的便宜、市場認錯』,還是『剛好押對高波動的方向』?", - "quote": "便宜是逆勢者的朋友;真正的報酬來自買在別人恐慌時。【意譯】", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這派問:這是『你在無人問津時用硬數字買到的折價』,還是『追在情緒高點、敢押波動換來的』?" - }, - "進場": { - "rubric_unit": "逆勢 + 硬數字便宜 + 硬催化劑才進;追情緒高點=反向|待 engine B.9", - "stance": "inverted", "lean": "contrarian", - "rule": "進場找『新聞一片罵聲、但 FCF yield/EV-EBIT/資產負債表都硬』且有硬催化劑的標的;追在大漲、人人看好的伸展高點=這派眼中的反向訊號。", - "quote": "媒體上的厭惡,只要基本面紮實,反而可以是你的朋友。", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲、人人看好。逆勢派反問:這是『被錯殺、無人敢買的便宜』,還是『情緒最高、安全邊際最薄的時候,你跟著追進去』?" - } + "position_sizing": {"rubric_unit": "Size from downside before upside", "stance": "conditional", "lean": "downside-first", "rule": "Size only after quantifying asset coverage, leverage, liquidity, and the permanent-loss scenario.", "quote": "The downside calculation comes before the upside narrative. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. What hard downside case and balance-sheet evidence justify that size?"}, + "averaging_down": {"rubric_unit": "Add only when the margin of safety survives", "stance": "conditional", "lean": "margin-of-safety", "rule": "Add only after updating hard fundamentals, downside, and catalyst probability; a lower market price is not enough.", "quote": "Cheap can become cheaper when the fundamental downside was misread. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new hard evidence preserved the margin of safety and catalyst?"}, + "exit_discipline": {"rubric_unit": "Exit when value is realized or the hard thesis breaks", "stance": "conditional", "lean": "value-realized", "rule": "Exit when the catalyst occurs, the value gap closes, or hard data invalidates asset value and downside protection.", "quote": "The position ends when value is realized or the evidence changes. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which exits followed value realization, and which lacked an updated hard-value case?"}, + "diversification": {"rubric_unit": "Hold a few deeply researched ideas", "stance": "inverted", "lean": "few-deep", "rule": "Own few positions only when each has deep hard-data research and an independent catalyst; avoid shallow concentration.", "quote": "Concentration is earned by depth of work, not by confidence alone. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which positions have distinct hard-value evidence and catalysts?"}, + "holding_period": {"rubric_unit": "Hold until repricing or invalidation", "stance": "conditional", "lean": "until-repriced", "rule": "Set the horizon from a concrete catalyst and repricing path; do not wait indefinitely for the market to agree.", "quote": "Contrarian patience needs a catalyst and an invalidation point. (paraphrase)", "motive_q": "For {incon_tickers}, did the repricing catalyst move, or did the horizon extend only because the trade stayed underwater?"}, + "alpha_beta": {"rubric_unit": "Separate idiosyncratic repricing from factor beta", "stance": "aligned", "lean": "decompose", "rule": "Decompose market and value-factor exposure before crediting hard fundamental research.", "quote": "A value rally can lift weak and strong theses together. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. What came from thesis-specific repricing rather than a value-factor move?"}, + "entry_style": {"rubric_unit": "Enter against sentiment only with hard evidence", "stance": "inverted", "lean": "contrarian", "rule": "Before entry, record the unpopular consensus, hard valuation evidence, downside, catalyst, and falsifier.", "quote": "Being early or contrarian is useful only when the hard thesis is right. (paraphrase)", "motive_q": "For {entry_ticker}, what hard evidence and catalyst justified going against consensus?"} } } diff --git a/skills/fomo-kernel/rubric/michael-burry.md b/skills/fomo-kernel/rubric/michael-burry.md index 705d46e..1118ad1 100644 --- a/skills/fomo-kernel/rubric/michael-burry.md +++ b/skills/fomo-kernel/rubric/michael-burry.md @@ -1,40 +1,31 @@ -# Lens · 深度價值 · 逆勢 · 先看下檔(Michael Burry)— v1 - -> 原則蒸餾自 Michael Burry 公開訪談 / 致投資人信與 virattt/ai-hedge-fund 的 `michael_burry` agent prompt(MIT)。原則/學派命名,真人來源見 Sources。 -> ✅ **引言已 verbatim 校對**:sizing / 出場 / 進場 三句為 Burry 原句(見下);其餘標【意譯】。 -> 與庫內「安全邊際」最大差異在 **進場**:安全邊際是 `weakness`(買弱/便宜),此派是 `contrarian`(買「被全市場罵、像被車輾過」但帳上硬的),多一層「逆勢 + 硬催化劑」。 - -## 脊椎(5 支柱) -1. 100% 以安全邊際為本——先保護下檔,避免本金永久損失。 -2. 逆勢:新聞一片罵聲可以是你的朋友,只要基本面是硬的。 -3. 先看下檔:避開高槓桿的資產負債表,最壞情況先算清楚。 -4. 要硬催化劑:內部人買入、買回庫藏股、賣資產——光便宜不夠。 -5. 用硬數字說話:FCF yield、EV/EBIT、淨現金,不靠故事。 - -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | -|---|---|---|---| -| 部位 sizing | conditional | downside-first | size 由下檔/安全邊際決定,不由上檔想像 | -| 加碼攤平 | conditional | margin-of-safety | 帳上硬+安全邊際更厚才加 | -| 出場紀律 | conditional | value-realized | 賣在被擦亮、價值兌現時 | -| 分散 | inverted | few-deep | 集中在少數算得清下檔的深度價值點子 | -| 持有時間 | conditional | until-repriced | 抱到市場認錯/催化劑兌現 | -| alpha/beta | aligned | decompose | alpha 來自買在無人敢買的便宜 | -| 進場 | inverted | contrarian | 逆勢+硬數字便宜+硬催化劑才進 | - -## 關鍵單元(verbatim 原句 → 中文) -- **sizing / 安全邊際**【原句】:"All my stock picking is 100% based on the concept of a margin of safety." → 我所有的選股,100% 建立在安全邊際這個概念上。 -- **出場 / 買冷門賣擦亮**【原句】:"I try to buy shares of unpopular companies when they look like road kill, and sell them when they've been polished up a bit." → 我買進那些像被車輾過的冷門公司,等它們被擦亮一點後再賣。 -- **進場 / 逆勢**【原句】:"Be contrarian: hatred in the press can be your friend if fundamentals are solid."(ai-hedge-fund prompt)→ 媒體上的厭惡,只要基本面紮實,反而可以是你的朋友。 -- **加碼 / 先看下檔**【意譯】:"Focus on downside first – avoid leveraged balance sheets." → 先聚焦下檔——避開有槓桿的資產負債表。 -- **持有 / 可能早但不錯**【意譯】:"I may be early, but I'm not wrong."(電影《大賣空》名句,坊間廣傳)→ 我可能會早,但我不會錯。 -- **分散 / 個別深研**【意譯】:偏好看具體、個別的機會,把每一個都研究到底。 -- **alpha / 逆勢的朋友**【意譯】:便宜是逆勢者的朋友;真正的報酬來自買在別人恐慌時。 - -## 待辦 -- 「可能早但不錯」一句出處為電影台詞/坊間流傳,如要嚴謹可改用 Scion 致投資人信的對應原句。 -- 進場(EN)需 engine B.9。 - -### Sources -- Michael Burry · Scion Capital 致投資人信 / 公開訪談 · [Wikipedia](https://en.wikipedia.org/wiki/Michael_Burry) -- [virattt/ai-hedge-fund · michael_burry agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/michael_burry.py) (MIT) +# Lens: deep value, contrarian entry, and downside first — v1 + +This lens distills public Michael Burry interviews and investor letters. It differs from a generic margin-of-safety lens by requiring both a contrarian setup and hard catalysts. + +## Five principles + +1. Start with margin of safety and permanent-loss protection. +2. Treat broad negative sentiment as a possible opportunity only when fundamentals remain hard. +3. Calculate downside first and avoid leveraged balance sheets. +4. Require a catalyst such as insider buying, repurchases, or asset sales; cheapness alone is insufficient. +5. Prefer hard measures such as free-cash-flow yield, enterprise-value multiples, and net cash over narrative. + +## Stance map + +| Dimension | Stance | Lean | +|---|---|---| +| Sizing | conditional | downside-first | +| Averaging down | conditional | margin-of-safety | +| Exit | conditional | value-realized | +| Diversification | inverted | few-deep | +| Holding period | conditional | until-repriced | +| Alpha/beta | aligned | decompose | +| Entry | inverted | contrarian | + +The margin-of-safety and unpopular-company remarks are grounded in public material. The well-known "early, not wrong" line is associated with a film portrayal and must not be presented as a verified Burry quotation. + +## Sources + +- Scion Capital investor letters and public Michael Burry interviews +- [Michael Burry overview](https://en.wikipedia.org/wiki/Michael_Burry) +- [virattt/ai-hedge-fund Burry agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/michael_burry.py) diff --git a/skills/fomo-kernel/rubric/momentum-discipline.lens.json b/skills/fomo-kernel/rubric/momentum-discipline.lens.json index bb3abd1..eec7804 100644 --- a/skills/fomo-kernel/rubric/momentum-discipline.lens.json +++ b/skills/fomo-kernel/rubric/momentum-discipline.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "動能紀律", - "master": "動能紀律(O'Neil · Minervini · Livermore · PTJ)", - "source": "公開著作原則蒸餾(How to Make Money in Stocks · Think & Trade Like a Champion · Reminiscences of a Stock Operator · Market Wizards),引用非轉載、非經本人背書。⚠️ 引言尚未對原書逐句 verbatim 校對,上線前需對齊審查(同 VY 流程)。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊 → 換檔、engine 不動。canonical 原文 momentum-discipline.md。『進場』待 engine B.9。stance/lean 供 compare_lenses 算分歧。", - - "master_intro": { - "one_line": "動能紀律的核心:趨勢是你的老闆,停損是不能談判的合約。它不問你『看得多準』,只問你『虧的時候有沒有照規則砍、贏的時候有沒有讓它跑』。", - "pillars": [ - "順勢——市場永遠是對的,別跟盤對賭你的『意見』", - "只買強——讓標的先證明自己(突破/創高),別接刀抄底", - "鐵停損——7–8% 寫在進場前,觸價就走、不在當下重想理由", - "絕不攤平輸家,只金字塔加碼贏家(losers average losers)", - "大賺小賠的算術——截斷虧損、讓利潤奔跑,優勢來自紀律不是預測" - ], - "why_it_matters": "這把尺偏『順勢 + 機械紀律』。如果你是價值/逆勢派,有些條對你是反的——它會把你的『逢低承接』讀成『接刀』、把『往下攤平』判成破戒。它照的是你有沒有截斷虧損、讓利潤奔跑,不是逼你變成趨勢交易者。" - }, - - "strength_intro": "先講你做對的一件事(動能派的紀律是逼出來的,先認你守住的那條):", - + "philosophy": "Momentum discipline", + "master": "Momentum discipline", + "source": "Composite process lens based on public O'Neil, Minervini, Livermore, and Paul Tudor Jones material. All card language below is paraphrase pending primary-source verification.", + "_note": "Runtime composite lens. It represents trend following with mechanical risk control, not one person's complete philosophy.", + "master_intro": {"one_line": "Buy demonstrated strength, define risk before entry, cut losses mechanically, and add only to winners.", "pillars": ["Follow price evidence", "Buy strength", "Predefine stops", "Never average down", "Keep losses small and let winners run"], "why_it_matters": "This lens treats arguing with a broken trend as the central process failure."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "stance": "aligned", "lean": "risk-capped", - "rubric_unit": "SZ1 size 由停損距離反推(R-based) / SZ2 風險優先於報酬", - "rule": "下單前先定停損價,size = 可承受虧損 ÷ (進場價 − 停損價)。不是『多看好下多大』,是『虧到停損只賠 1R』回推股數。", - "quote": "我隨時在想的是會虧多少,不是會賺多少;先防守,再進攻。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。你這筆的停損價在哪?這個 size 是『虧到停損只賠 1R』算出來的,還是『就是很看好、直接重壓』?" - }, - "加碼攤平": { - "stance": "unconditional", "lean": "never", - "rubric_unit": "AV1 絕不攤平輸家(無後門) / AV2 只金字塔加碼贏家", - "rule": "虧損部位一律不加碼——沒有『除非有新證據』的後門:價格在跌,就是市場告訴你看錯了。加碼只往上(突破確認)、且越加越小。", - "quote": "Losers average losers.(輸家才會往下攤平。)", - "motive_q": "{tickers} 你在虧損裡一路往下加。動能派這裡沒有『除非』——往下加=不認賠。你是『真的看到突破訊號才往上加』,還是『想攤低成本等回本』?" - }, - "出場紀律": { - "stance": "unconditional", "lean": "stop", - "rubric_unit": "EX1 7–8% 鐵停損無例外 / EX2 讓贏家跑", - "rule": "進場同時掛 7–8% 停損,觸價就走、不重想理由;贏家只要趨勢沒破(沒跌破關鍵均線)就抱住,別因怕回吐落袋。", - "quote": "截斷虧損、讓利潤奔跑;真正賺大錢的是坐著不動,不是進進出出。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲——這是動能派最痛的事。那些賣出是『趨勢真的破了(跌破均線/停損)』,還是『賺了怕回吐、手癢』?" - }, - "分散": { - "stance": "inverted", "lean": "concentrate", - "rubric_unit": "DV1 集中強勢股,用停損控風險(不用分散)", - "rule": "動能派不靠檔數分散控風險,靠停損。同一個 driver 的幾檔,停損會一起被觸發 = 等於一個部位。問題不是『不夠分散』,是『會不會一起停損』。", - "quote": "把資金集中在少數最強的標的、緊盯停損;過度分散是給看不懂自己持倉的人。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。動能派的問題不是你不夠分散——是這幾檔的停損會不會同一天一起跳?你當初有沒有把它們當『同一個部位』?" - }, - "持有時間": { - "stance": "conditional", "lean": "trend", - "rubric_unit": "HD1 持有期由趨勢決定,不由預設標籤", - "rule": "別預設『我是長線/短線』。趨勢還在(沒跌破關鍵均線/停損)就抱,破了就走——時間框架是趨勢給的結果,不是你進場前貼的標籤。", - "quote": "錢是坐著等出來的,不是靠頻繁進出;趨勢沒結束前,別自作聰明跳下車。", - "motive_q": "{incon_tickers} 你同一檔又當沖又長抱。對動能派,持有期該由趨勢決定:這是『趨勢還在所以續抱』,還是『套牢了改口說自己長期投資』?" - }, - "alpha/beta": { - "stance": "aligned", "lean": "decompose", - "rubric_unit": "PS1 市場永遠是對的 / 站對趨勢 vs 選股", - "rule": "每季分清:你贏大盤,是『站對趨勢/賽道(順勢紅利)』,還是『選股 + 守紀律』?別把『敢追強勢題材』當成選股本事。", - "quote": "The market is never wrong; opinions often are.(市場永遠是對的,意見常常是錯的。)", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。動能派會問:這是『你站對了趨勢』還是『你會選股 + 守紀律』?站對賽道很好——但別跟選股本事搞混。" - }, - "進場": { - "stance": "inverted", "lean": "strength", - "rubric_unit": "EN1 只買強別接刀 / EN2 進場是事前定好的觸發(pivot)|待 engine B.9", - "rule": "只在標的證明強勢(突破/創高/站上關鍵均線)時進場,且進場點事前定好(突破 X 才進);別在下跌中因『夠便宜了』去接刀。", - "quote": "你的目標不是買在最便宜,是買在『對的時間』——讓股票先向你證明它的強,再投入。", - "motive_q": "{entry_ticker} 你買在近 20 日的高點上緣 / 或下跌中承接。動能派只買強:你進場時它在突破創高,還是在跌、你覺得便宜?這個進場點是你事前畫好的觸發,還是看它動了才追?" - } + "position_sizing": {"rubric_unit": "Size from predefined stop risk", "stance": "aligned", "lean": "risk-capped", "rule": "Calculate size from the entry-to-stop distance so one failed trade cannot exceed the risk budget.", "quote": "Position size follows the loss allowed at the stop. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. What predefined stop and account-risk limit produced that size?"}, + "averaging_down": {"rubric_unit": "Never average down a loser", "stance": "unconditional", "lean": "never", "rule": "Never add below cost to repair a losing trade; add only after price confirms the thesis and total risk remains capped.", "quote": "Add to demonstrated winners, not to positions that are proving you wrong. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. Why was capital added before price reconfirmed the setup?"}, + "exit_discipline": {"rubric_unit": "Execute the predefined stop", "stance": "unconditional", "lean": "stop", "rule": "Write the stop before entry and execute it without moving it farther away after the loss begins.", "quote": "Small losses are a business expense; uncontrolled losses are a process failure. (paraphrase)", "motive_q": "When the setup failed, did you execute the original stop or rewrite it to avoid taking the loss?"}, + "diversification": {"rubric_unit": "Concentrate in the strongest valid setups", "stance": "inverted", "lean": "concentrate", "rule": "Hold only the strongest setups whose risks are independently capped; do not add weak names for cosmetic diversification.", "quote": "A few strong setups are preferable to many mediocre ones. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Are all positions among the strongest valid setups, or are weaker names diluting focus?"}, + "holding_period": {"rubric_unit": "Hold while the trend remains valid", "stance": "conditional", "lean": "trend", "rule": "Let winners run while price and trend rules remain valid; exit when the predefined trend condition breaks.", "quote": "The trend, not a calendar label, determines the holding period. (paraphrase)", "motive_q": "For {incon_tickers}, did the trend rule change, or did you relabel the trade after it stopped working?"}, + "alpha_beta": {"rubric_unit": "Separate execution edge from market beta", "stance": "aligned", "lean": "decompose", "rule": "Decompose market regime and high-beta exposure before crediting entry and exit discipline.", "quote": "A strong tape can make poor execution look skilled. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. What return came from disciplined execution rather than the market regime?"}, + "entry_style": {"rubric_unit": "Enter on demonstrated strength", "stance": "inverted", "lean": "strength", "rule": "Enter only after price confirms the setup, with a predefined stop and no need to predict the bottom.", "quote": "Let price prove the setup before committing capital. (paraphrase)", "motive_q": "For {entry_ticker}, what price confirmation and stop defined the entry rather than fear of missing out?"} } } diff --git a/skills/fomo-kernel/rubric/momentum-discipline.md b/skills/fomo-kernel/rubric/momentum-discipline.md index 353cfd6..441674d 100644 --- a/skills/fomo-kernel/rubric/momentum-discipline.md +++ b/skills/fomo-kernel/rubric/momentum-discipline.md @@ -1,37 +1,30 @@ -# Lens · 動能紀律(Momentum Discipline)— v1 draft - -> 複合鏡:脊椎取四位「順勢 + 機械紀律」大師的共同交集——O'Neil · Minervini · Livermore · PTJ。 -> 用原則/學派命名(非單一真人),真人來源見下方 Sources。 -> ⚠️ **draft**:引言來自公開查證,**尚未對原書逐句 verbatim 校對**;標【原話】=廣泛轉載的核心規則,其餘【意譯】。上線前需 verbatim 對齊審查。 - -## 脊椎(5 支柱) -1. 順勢——市場永遠是對的,別跟盤對賭你的「意見」。 -2. 只買強——讓標的先證明自己(突破/創高),別接刀抄底。 -3. 鐵停損——7–8% 寫在進場前,觸價就走、不在當下重想理由。 -4. 絕不攤平輸家,只金字塔加碼贏家(losers average losers)。 -5. 大賺小賠的算術——截斷虧損、讓利潤奔跑,優勢來自紀律不是預測。 - -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | -|---|---|---|---| -| 進場 | inverted | strength | 只買強,接刀是罪(對 VY 最大反轉) | -| 加碼攤平 | unconditional | never | 任何往下加=破戒,無後門 | -| 出場紀律 | unconditional | stop | 7–8% 鐵停損,機械不判斷 | -| 分散 | inverted | concentrate | 集中強股、用停損控風險 | -| 部位 sizing | aligned | risk-capped | size 由停損距離反推(R-based) | -| 持有時間 | conditional | trend | 趨勢決定持有期 | -| alpha/beta | aligned | decompose | 站對趨勢 vs 選股 | - -## 關鍵單元(grounded) -- **7–8% 鐵停損**【原話】(O'Neil,廣泛轉載):虧損達買價 7–8% 一律停損,無例外。 -- **絕不攤平**【原話】(PTJ):「Losers average losers.」 -- **只買強**【意譯】(O'Neil):買在「對的時間」,讓股票先證明它的強,不是買最便宜。 -- **讓贏家跑**【原話(轉述)】(Livermore):真正賺大錢的是坐著不動,不是頻繁進出。 -- **市場永遠是對的**【原話(轉述)】(Livermore):「The market is never wrong; opinions often are.」 - -## 待辦 -- verbatim 對齊審查:對四本原書逐句校【原話】單元措辭;查不到 verbatim 的降【意譯】。 -- 進場(EN)需 engine B.9 落地才有機械訊號;在那之前只能對話提問。 - -### Sources -- [O'Neil 23 rules](https://www.tradingwithrayner.com/23-trading-rules-by-william-j-oneil/) · [Minervini SEPA](https://www.financialtechwiz.com/post/mark-minervini-trading-strategy/) +# Lens: momentum discipline — v1 draft + +This composite lens uses the common process principles of William O'Neil, Mark Minervini, Jesse Livermore, and Paul Tudor Jones. It represents trend following with mechanical risk control rather than one person's complete philosophy. + +## Five principles + +1. Follow price evidence instead of arguing with the market. +2. Buy strength after the instrument proves itself; do not catch falling knives. +3. Define a stop before entry and execute it mechanically. +4. Never average down a loser; pyramid only into confirmed winners. +5. Keep losses small and allow winners to create the payoff asymmetry. + +## Stance map + +| Dimension | Stance | Lean | +|---|---|---| +| Entry | inverted | strength | +| Averaging down | unconditional | never | +| Exit | unconditional | stop | +| Diversification | inverted | concentrate | +| Sizing | aligned | risk-capped | +| Holding period | conditional | trend | +| Alpha/beta | aligned | decompose | + +The seven-to-eight-percent stop and "losers average losers" rules are widely attributed but still require primary-source verification before publication as verbatim quotes. Treat the remaining wording as paraphrase. + +## Sources + +- [O'Neil trading rules](https://www.tradingwithrayner.com/23-trading-rules-by-william-j-oneil/) +- [Minervini SEPA overview](https://www.financialtechwiz.com/post/mark-minervini-trading-strategy/) diff --git a/skills/fomo-kernel/rubric/peter-lynch.lens.json b/skills/fomo-kernel/rubric/peter-lynch.lens.json index c54dafb..9d7c72c 100644 --- a/skills/fomo-kernel/rubric/peter-lynch.lens.json +++ b/skills/fomo-kernel/rubric/peter-lynch.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "成長合理價 · 買你懂的", - "master": "Peter Lynch(GARP)", - "source": "原則蒸餾自 Peter Lynch 公開著作(One Up on Wall Street / Beating the Street)與 virattt/ai-hedge-fund 的 peter_lynch agent prompt(MIT)。引用非轉載、非經本人背書。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 peter-lynch.md。stance/lean 供 compare_lenses。此派的招牌反轉在【出場】:『砍雜草、澆花』——賣壞的抱好的,把『賣掉贏家』讀成最貴的錯(多數派只把『不認賠』當洞,這派加照『太早賣贏家』)。引言已對 verbatim 校對(英文原句逐條見 peter-lynch.md),中文為對應翻譯。", - - "master_intro": { - "one_line": "這套尺的核心:只買你講得出故事的生意,用合理價買成長,然後砍掉長壞的、抱住長好的。它不問你『便宜還是貴』,問你『你真的懂這門生意嗎,它還在成長嗎』。", - "pillars": [ - "買你懂的——生意簡單到能用一句話講清楚,講不清就別碰", - "成長要配合理價(GARP):用 PEG 看,成長再快也不為它付任何價", - "找 ten-bagger:能讓盈餘翻很多倍的好公司,賺大錢靠抱住它幾年", - "砍雜草、澆花:賣掉故事壞掉的,抱住故事還在的——別反過來", - "避開高槓桿、過度複雜的生意;自己做功課,別被盤面噪音牽著走" - ], - "why_it_matters": "這把尺偏『懂的成長 + 讓贏家跑』。如果你是純技術/當沖,有些條對你是『提問』不是『判錯』——它照的是你買進的東西你說不說得出故事、賣出的時候砍的是不是該砍的那檔,不是逼你變成基本面長線客。" - }, - - "strength_intro": "先說你做對的一件事(這派先認你抱對、講得出故事的那檔):", - + "philosophy": "Growth at a reasonable price and knowing what you own", + "master": "Peter Lynch", + "source": "Distilled from Peter Lynch's public books and talks. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime lens. It requires a simple business story, reasonable growth valuation, and a clear reason the story remains intact.", + "master_intro": {"one_line": "Own understandable businesses with durable growth at a reasonable price, and keep winners while the story remains intact.", "pillars": ["Know what you own", "Match price to growth", "Look for a long runway", "Remove broken stories and keep intact winners", "Avoid leverage and unnecessary complexity"], "why_it_matters": "This lens asks whether the user understands the business story well enough to distinguish normal volatility from story failure."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "依『你懂多少』下注 / 讓贏家自然長大", - "stance": "conditional", "lean": "understanding", - "rule": "部位大小跟『你對這門生意懂多少』成正比;講不出故事的,先別給大 size,讓真懂的那檔自己長大。", - "quote": "絕對不要投資任何你沒辦法用一支蠟筆畫出來的點子。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這個 size 是『你真的懂這門生意、講得出它怎麼長大』,還是『它在漲、你就跟著加重』?" - }, - "加碼攤平": { - "rubric_unit": "故事還在成長才加;故事壞了是雜草,要砍不是攤", - "stance": "conditional", "lean": "story-intact", - "rule": "往下加碼前先問『它的成長故事是還在,還是已經壞了』;故事還在=可以加,故事壞了=砍,不要攤平。", - "quote": "了解你持有什麼,也了解你為什麼持有它。", - "motive_q": "{tickers} 你在虧損裡一路往下加。是『成長故事還完好、只是股價暫時跌』,還是『故事已經壞了、你只是不想認賠』?" - }, - "出場紀律": { - "rubric_unit": "砍雜草、澆花:賣壞的、抱好的;太早賣贏家是最貴的錯", - "stance": "conditional", "lean": "weeds-flowers", - "rule": "賣出前先分類:這檔是『故事壞掉的雜草』還是『故事還在的花』?只砍雜草;花別因為漲了就拔。", - "quote": "賣掉你的贏家、抱著你的輸家,就像剪掉花、灌溉雜草。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些是『成長故事到頭了(該賣的花期已過)』,還是『賺了怕回吐、把還在開的花提早拔了』?" - }, - "分散": { - "rubric_unit": "檔數不是重點,每檔都講得出故事才算數", - "stance": "conditional", "lean": "known", - "rule": "可以持很多檔,但每一檔都要說得出它的成長故事;說不出故事的,不是分散,是失焦。", - "quote": "持有股票就像養小孩——別搞到超出你能照顧的數目。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。這派不嫌你檔數——只問:每一檔你都講得出獨立的成長故事,還是有些只是『湊上去、其實沒在追蹤』?" - }, - "持有時間": { - "rubric_unit": "成長跑道還在就抱,ten-bagger 要靠年計的耐心", - "stance": "conditional", "lean": "growth-runway", - "rule": "持有期由『成長跑道還剩多長』決定,不由心情;跑道還在,就給它幾年長成 ten-bagger。", - "quote": "在股票上賺錢的真正關鍵,是不要被它們嚇得提早跑掉。", - "motive_q": "{incon_tickers} 你同一檔又短又長。是『成長跑道的判斷變了所以調整』,還是『套牢了才改口說要長抱』?" - }, - "alpha/beta": { - "rubric_unit": "贏在做功課比別人懂,不在敢押波動", - "stance": "aligned", "lean": "decompose", - "rule": "每季分清:你贏大盤,是『比市場更懂這幾門生意』,還是『剛好押對了高波動的賽道』?", - "quote": "不做研究就投資,跟玩梭哈卻從不看牌一樣。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這派問:這是『你對這幾家公司做的功課比市場深』,還是『敢押高波動換來的方向紅利』?" - }, - "進場": { - "rubric_unit": "成長配合理價(GARP)才進;講不出故事或 PEG 太貴就放掉|待 engine B.9", - "stance": "conditional", "lean": "garp", - "rule": "進場前先過兩關:① 我講得出它的成長故事嗎?② 這個價配得上這個成長嗎(PEG)?兩關都過才買,追在伸展高點又說不出故事=不進。", - "quote": "一家定價公允的公司,本益比會等於它的盈餘成長率。", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲。是『你講得出它的成長故事、價格也還配得上』,還是『它在噴、你怕錯過』?" - } + "position_sizing": {"rubric_unit": "Size according to understanding", "stance": "conditional", "lean": "understanding", "rule": "Give meaningful size only to businesses you can explain simply, with researched growth, valuation, leverage, and failure modes.", "quote": "If the business cannot be explained simply, the position is too large for your understanding. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Can you explain the business, growth driver, valuation, and main failure mode without using market slogans?"}, + "averaging_down": {"rubric_unit": "Add only while the business story remains intact", "stance": "conditional", "lean": "story-intact", "rule": "Add only after confirming that the business story, growth runway, balance sheet, and valuation remain intact.", "quote": "A lower stock price does not repair a broken business story. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What new business evidence showed that the original story remained intact?"}, + "exit_discipline": {"rubric_unit": "Remove broken stories and keep intact winners", "stance": "conditional", "lean": "weeds-flowers", "rule": "Sell when the business story breaks or valuation outruns realistic growth; do not sell an intact winner merely because it appreciated.", "quote": "Do not keep watering broken stories while cutting the businesses that continue to work. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which businesses had a broken story, and which intact winners did you cut too early?"}, + "diversification": {"rubric_unit": "Own only a manageable number of understood businesses", "stance": "conditional", "lean": "known", "rule": "Hold only as many businesses as you can research and explain; ticker count cannot replace understanding.", "quote": "Diversification loses value when the holdings exceed your ability to know them. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Can you state the distinct business story and risk for every holding?"}, + "holding_period": {"rubric_unit": "Hold through an intact growth runway", "stance": "conditional", "lean": "growth-runway", "rule": "Let the growth runway and business milestones determine the horizon; do not relabel a failed story as a long-term hold.", "quote": "Compounding needs time, but only while the business story remains intact. (paraphrase)", "motive_q": "For {incon_tickers}, did the growth runway change, or did the horizon change after price moved?"}, + "alpha_beta": {"rubric_unit": "Separate business selection from market beta", "stance": "aligned", "lean": "decompose", "rule": "Decompose market and growth-factor exposure before claiming that business research created alpha.", "quote": "A favorable market can lift businesses regardless of how well they were understood. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. What return came from business-specific growth rather than growth-factor beta?"}, + "entry_style": {"rubric_unit": "Enter understandable growth at a reasonable price", "stance": "conditional", "lean": "garp", "rule": "Before entry, explain the business simply and compare its growth runway, valuation, debt, and realistic expectations.", "quote": "Growth is attractive only when the price and business story remain reasonable. (paraphrase)", "motive_q": "For {entry_ticker}, what business understanding and growth-versus-price evidence justified entry beyond recent excitement?"} } } diff --git a/skills/fomo-kernel/rubric/peter-lynch.md b/skills/fomo-kernel/rubric/peter-lynch.md index 3500641..dafb273 100644 --- a/skills/fomo-kernel/rubric/peter-lynch.md +++ b/skills/fomo-kernel/rubric/peter-lynch.md @@ -1,40 +1,32 @@ -# Lens · 成長合理價 · 買你懂的(Peter Lynch / GARP)— v1 - -> 原則蒸餾自 Peter Lynch 公開著作(One Up on Wall Street / Beating the Street)與 virattt/ai-hedge-fund 的 `peter_lynch` agent prompt(MIT)。原則/學派命名,真人來源見 Sources。 -> ✅ **引言已 verbatim 校對**:`.lens.json` 的中文引言對應下方英文原句;本檔 7 句全為 Lynch 原句。 -> 這把尺的招牌反轉在 **出場**:多數派只把「不認賠」當洞,這派加照「太早賣贏家」——「砍雜草、澆花」:賣掉故事壞的、抱住故事還在的。 - -## 脊椎(5 支柱) -1. 買你懂的——生意簡單到能用一句話講清楚,講不清就別碰。 -2. 成長要配合理價(GARP):用 PEG 看,成長再快也不為它付任何價。 -3. 找 ten-bagger:能讓盈餘翻很多倍的好公司,賺大錢靠抱住它幾年。 -4. 砍雜草、澆花:賣掉故事壞掉的,抱住故事還在的——別反過來。 -5. 避開高槓桿、過度複雜的生意;自己做功課,別被盤面噪音牽著走。 - -## stance / lean(供 compare_lenses) -| dim | stance | lean | 一句 | -|---|---|---|---| -| 部位 sizing | conditional | understanding | 依「你懂多少」下注,讓贏家自然長大 | -| 加碼攤平 | conditional | story-intact | 故事還在才加,故事壞了是雜草要砍 | -| 出場紀律 | conditional | weeds-flowers | 砍雜草澆花;太早賣贏家是最貴的錯 | -| 分散 | conditional | known | 檔數不是重點,每檔講得出故事才算數 | -| 持有時間 | conditional | growth-runway | 成長跑道還在就抱,ten-bagger 要年計耐心 | -| alpha/beta | aligned | decompose | 贏在做功課比別人懂,不在敢押波動 | -| 進場 | conditional | garp | 成長配合理價(PEG)才進 | - -## 關鍵單元(verbatim 原句 → 中文) -- **sizing / 買你懂的**【原句】:"Never invest in any idea you can't illustrate with a crayon." → 絕對不要投資任何你沒辦法用一支蠟筆畫出來的點子。 -- **加碼 / know what you own**【原句】:"Know what you own, and know why you own it." → 了解你持有什麼,也了解你為什麼持有它。 -- **出場 / 砍雜草澆花**【原句】:"Selling your winners and holding your losers is like cutting the flowers and watering the weeds."(Buffett 1989 致股東信引用)→ 賣掉你的贏家、抱著你的輸家,就像剪掉花、灌溉雜草。 -- **分散 / 別超過你能照顧的**【原句】:"Owning stocks is like having children — don't get involved with more than you can handle." → 持有股票就像養小孩——別搞到超出你能照顧的數目。 -- **持有 / 別被嚇跑**【原句】:"The real key to making money in stocks is not to get scared out of them." → 在股票上賺錢的真正關鍵,是不要被它們嚇得提早跑掉。 -- **alpha / 做功課**【原句】:"Investing without research is like playing stud poker and never looking at the cards." → 不做研究就投資,跟玩梭哈卻從不看牌一樣。 -- **進場 / PEG**【原句】:"The P/E ratio of any company that's fairly priced will equal its growth rate." → 一家定價公允的公司,本益比會等於它的盈餘成長率。 - -## 待辦 -- 進場(EN)需 engine B.9。 - -### Sources -- Peter Lynch, *One Up on Wall Street* (1989) · *Beating the Street* (1993) -- [Quote Investigator · flowers/weeds](https://quoteinvestigator.com/2022/10/25/flower-weed/) · [Novel Investor · Peter Lynch quotes](https://novelinvestor.com/quote-author/peter-lynch/) -- [virattt/ai-hedge-fund · peter_lynch agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/peter_lynch.py) (MIT) +# Lens: growth at a reasonable price and knowing what you own — v1 + +This lens distills public Peter Lynch writing. Its signature exit reversal is to challenge both holding losers too long and selling intact winners too early. + +## Five principles + +1. Own businesses you can explain simply. +2. Match growth with a reasonable price rather than paying any price for growth. +3. Look for long growth runways and hold long enough for compounding to matter. +4. Remove broken stories and keep watering intact winners. +5. Avoid excessive leverage and complexity; do the research yourself. + +## Stance map + +| Dimension | Stance | Lean | +|---|---|---| +| Sizing | conditional | understanding | +| Averaging down | conditional | story-intact | +| Exit | conditional | weeds-flowers | +| Diversification | conditional | known | +| Holding period | conditional | growth-runway | +| Alpha/beta | aligned | decompose | +| Entry | conditional | garp | + +The lens JSON should preserve source attribution for the crayon, know-what-you-own, flowers-and-weeds, manageable-number-of-holdings, research, and growth-versus-price statements. + +## Sources + +- Peter Lynch, *One Up on Wall Street* and *Beating the Street* +- [Quote Investigator on flowers and weeds](https://quoteinvestigator.com/2022/10/25/flower-weed/) +- [Peter Lynch quote collection](https://novelinvestor.com/quote-author/peter-lynch/) +- [virattt/ai-hedge-fund Lynch agent](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/peter_lynch.py) diff --git a/skills/fomo-kernel/rubric/trading-psychology.lens.json b/skills/fomo-kernel/rubric/trading-psychology.lens.json index de30e42..45f3b08 100644 --- a/skills/fomo-kernel/rubric/trading-psychology.lens.json +++ b/skills/fomo-kernel/rubric/trading-psychology.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "交易心理", - "master": "交易心理", - "source": "原則蒸餾自 Steenbarger 交易心理著作(The Daily Trading Coach / Trading Psychology 2.0),引用非轉載、非經本人背書。⚠️ 引言為意譯,尚未對原書逐句校對,上線前需 verbatim 對齊審查。", - "_note": "鏡片層。dim keys 與 vincent-yu.lens.json 對齊。canonical 原文 trading-psychology.md。刻意全 stance=aligned、不掛 lean:此派不在『該怎麼交易』上 fork(他背書普世層),價值在 meta——記錄/復盤/揚長。所以 compare_lenses 會顯示他低分歧,這是正確的(不是每個大師都製造岔路)。他的單鏡 motive_q 是『這是不是一個你記錄過的情緒 pattern』。", - - "master_intro": { - "one_line": "交易心理派的核心:盯你控制得了的『流程』,不是單筆盈虧;先找出你做得最好的、刻意放大它。它不跟別派爭『該買強還買弱』,它問你『你有沒有把自己的 pattern 記下來、改掉』。", - "pillars": [ - "流程 > 損益:設流程目標(可控),不是單筆賺賠目標", - "先找出你做得最好的,刻意放大它(build on strengths)", - "把情緒連到思路:每筆記 trigger / 想法 / 結果 / 情緒,找 pattern", - "復盤頻率跟記錄頻率一樣重要(日內 24h / 週找 pattern / 月深掘 / 季總檢)", - "單筆結果脫鉤:任何事都可能發生,別用一筆定義自己" - ], - "why_it_matters": "這把尺不在哲學上跟別派對撞——它背書所有人都該守的普世紀律。它的價值是 meta:把你的盲點變成『可被記錄、可被矯正的 pattern』。所以它不會跟你爭該怎麼交易,它逼你回答:這個錯,你記下來了嗎、改了嗎?" - }, - - "strength_intro": "先放大你做對的那件事(這派核心:find what you do best, and expand on it):", - + "philosophy": "Trading psychology", + "master": "Trading psychology", + "source": "Distilled from public Brett Steenbarger books and articles. All card language below is paraphrase unless a primary-source quotation is added and verified.", + "_note": "Runtime process lens. Every stance is aligned and intentionally has no lean; it diagnoses observable patterns rather than prescribing a trading doctrine.", + "master_intro": {"one_line": "Turn recurring emotional triggers into observable patterns, controllable process goals, and one rehearsable rule.", "pillars": ["Use process goals", "Build on demonstrated strengths", "Record triggers and outcomes", "Review at multiple horizons", "Separate identity from one result"], "why_it_matters": "This lens asks whether repeated decisions are driven by a stable process or by an emotional trigger."}, + "strength_intro": "One process strength worth preserving:", "dims": { - "部位 sizing": { - "stance": "aligned", - "rubric_unit": "size 由流程決定,不被情緒放大", - "rule": "size 要一致、能被你寫好的流程解釋;別讓單筆 size 跟著興奮或不甘漂移。", - "quote": "成功來自一致的流程,不是單筆的英雄主義。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這個 size 是你寫好的流程算出來的,還是當下情緒(興奮 / 不甘)放大的?" - }, - "加碼攤平": { - "stance": "aligned", - "rubric_unit": "加碼是計畫,還是情緒 pattern?", - "rule": "加碼前回看:這是交易計畫的一部分,還是情緒反應?把 trigger / 想法 / 結果記下來,週末看 pattern。", - "quote": "把情緒反應變成可觀察的 pattern,你才改得動它。", - "motive_q": "{tickers} 你往下加。這是你計畫裡寫好的動作,還是一個你反覆出現的情緒 pattern(不甘認賠)?你記錄它了嗎?" - }, - "出場紀律": { - "stance": "aligned", - "rubric_unit": "賣太早常是可矯正的情緒 pattern", - "rule": "若你常賣太早,把每次的 trigger / 想法 / 結果記下、找出觸發點,改一條具體規則,下週驗它。", - "quote": "持續在小幅獲利後就出場,通常是一個可被記錄、可被矯正的情緒 pattern,不是性格。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來續漲。這是不是一個重複出現的 pattern?你有沒有記下『每次想賣的當下在想什麼』?" - }, - "分散": { - "stance": "aligned", - "rubric_unit": "持倉結構是流程,還是情緒累積?", - "rule": "回看你的持倉:是一套流程決定的,還是隨盤面情緒東買西買累積成的?", - "quote": "好的交易日誌會讓你看見:很多『決策』其實是情緒。", - "motive_q": "你 {n} 檔有 {ai_pct}% 同一個 driver。這個集中是你流程選出來的,還是『哪個熱就追哪個』情緒累積的?" - }, - "持有時間": { - "stance": "aligned", - "rubric_unit": "持有期是計畫,還是套牢後合理化", - "rule": "進場前先寫下計畫持有框架;事後對照實際——落差本身就是要記錄的 pattern。", - "quote": "進步來自把『計畫』和『實際』並排看,然後縮小落差。", - "motive_q": "{incon_tickers} 你同一檔又短又長。這是計畫好的兩套策略,還是套牢後改口的合理化?你進場時寫下框架了嗎?" - }, - "alpha/beta": { - "stance": "aligned", - "rubric_unit": "用真實帳本校準自我認知", - "rule": "用真實帳本(不是記憶)定期回看:你的優勢到底在哪一類交易?把它放大、把弱項流程化或外包。", - "quote": "找出你做得最好的,然後刻意去放大它。", - "motive_q": "你贏大盤 {excess}pp(β={beta})。你有沒有用真實帳本拆過——你的錢到底是哪一類交易賺的?那一類,你能不能刻意多做?" - }, - "進場": { - "stance": "aligned", - "rubric_unit": "進場是寫好的 setup,還是情緒衝動|待 engine B.9", - "rule": "進場前先有一個寫好的 setup(觸發條件);事後記下『當下情緒』,週末檢查衝動進場的比例。", - "quote": "最好的交易者不是沒有情緒,而是知道情緒何時在替他們做決定。", - "motive_q": "{entry_ticker} 這筆進場,你有沒有一個事前寫好的 setup?還是看盤面在動、情緒上來就進?你記下當下的情緒了嗎?" - } + "position_sizing": {"rubric_unit": "Observe the trigger behind oversizing", "stance": "aligned", "rule": "Record the trigger, thought, emotion, planned risk, and actual size whenever position size exceeds the process rule.", "quote": "A repeated sizing error becomes trainable when its trigger and response are observable. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. What emotional or situational trigger preceded that size decision?"}, + "averaging_down": {"rubric_unit": "Observe the trigger behind adding to losses", "stance": "aligned", "rule": "Before any add to a loss, label the trigger and write the new evidence; if the action is relief-seeking, do not add.", "quote": "A process journal converts an urge into a pattern that can be rehearsed differently. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. Was the action prompted by new evidence or by a need to reduce regret and recover the loss?"}, + "exit_discipline": {"rubric_unit": "Compare planned and emotional exits", "stance": "aligned", "rule": "Tag each exit as rule-based or emotion-driven and review the pattern weekly rather than judging one isolated outcome.", "quote": "The goal is consistent execution, not proving that every single exit was right. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which recurring trigger causes early exits: fear of giving back gains, boredom, or a valid rule?"}, + "diversification": {"rubric_unit": "Identify emotional clustering", "stance": "aligned", "rule": "Record whether correlated positions came from independent plans or from repeated reassurance around one familiar story.", "quote": "Several positions can express one emotional need even when their tickers differ. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Did each position come from an independent setup or repeated comfort with one theme?"}, + "holding_period": {"rubric_unit": "Separate planned horizon from coping behavior", "stance": "aligned", "rule": "Write the intended horizon before entry and tag every later change with the evidence and emotional state that caused it.", "quote": "A horizon change is useful data when the reason is recorded rather than rationalized. (paraphrase)", "motive_q": "For {incon_tickers}, what trigger and evidence caused the horizon to change?"}, + "alpha_beta": {"rubric_unit": "Use attribution for calibration, not identity", "stance": "aligned", "rule": "Decompose returns and record process quality separately so one favorable outcome does not become proof of personal skill.", "quote": "A good outcome and a good decision are related but not identical. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. Which repeatable process behavior, if any, deserves credit?"}, + "entry_style": {"rubric_unit": "Observe the emotional trigger before entry", "stance": "aligned", "rule": "Before entry, record the setup, trigger, emotion, intended horizon, and invalidation condition.", "quote": "The entry becomes coachable when the setup and the internal trigger are both visible. (paraphrase)", "motive_q": "For {entry_ticker}, what part of the decision came from the setup and what part came from urgency, excitement, or fear of missing out?"} } } diff --git a/skills/fomo-kernel/rubric/trading-psychology.md b/skills/fomo-kernel/rubric/trading-psychology.md index f65bbbc..ef6d252 100644 --- a/skills/fomo-kernel/rubric/trading-psychology.md +++ b/skills/fomo-kernel/rubric/trading-psychology.md @@ -1,27 +1,24 @@ -# Lens · 交易心理(Trading Psychology)— v1 draft +# Lens: trading psychology — v1 draft -> 原則蒸餾自 Steenbarger 交易心理著作(The Daily Trading Coach / Trading Psychology 2.0)。原則/學派命名,真人來源見 Sources。 -> ⚠️ **draft**:引言為意譯,**尚未對原書逐句校對**,上線前需 verbatim 對齊審查。 -> **特別注意**:這把尺刻意全 `stance=aligned`、不掛 lean——它**不在「該怎麼交易」上 fork**(背書普世層),價值在 meta:把盲點變成「可記錄、可矯正的 pattern」。所以在 compare_lenses 它會顯示**低分歧**,這是正確的設計(不是每個大師都製造岔路)。 +This lens distills Brett Steenbarger's process-oriented trading psychology. It intentionally uses `stance=aligned` across dimensions. Its value is not a competing trading doctrine; it turns blind spots into observable and correctable patterns. -## 脊椎(5 支柱) -1. 流程 > 損益:設可控的流程目標,不是單筆賺賠目標。 -2. 先找出你做得最好的,刻意放大它(build on strengths)。 -3. 把情緒連到思路:每筆記 trigger / 想法 / 結果 / 情緒,找 pattern。 -4. 復盤頻率跟記錄頻率一樣重要(日內 24h / 週找 pattern / 月深掘 / 季總檢)。 -5. 單筆結果脫鉤:任何事都可能發生,別用一筆定義自己。 +## Five principles -## 為什麼收這把尺(即使它低分歧) -- 它跟**產品本身幾乎逐字同構**:「先講你做對的」=`strength_intro`+`prescribe` 揚長;「賣太早→記錄 trigger→改規則」=engine `winner_early`→if-then 閉環。 -- 它的單鏡價值在 motive_q 的角度:不是「你信哪派」,而是「**這是不是一個你記錄過、該矯正的情緒 pattern**」。 -- 在 compare_lenses 它當「普世/meta 之聲」——驗證對照邏輯能正確判斷『此人不 fork』。 +1. Set controllable process goals rather than single-trade P&L goals. +2. Identify and deliberately expand demonstrated strengths. +3. Record triggers, thoughts, outcomes, and emotions so patterns become visible. +4. Review at several horizons: immediate execution, weekly patterns, monthly depth, and periodic system review. +5. Separate identity from one outcome; any single trade can fail. -## stance / lean -全 dim `stance=aligned`、**不掛 lean**(刻意,避免製造假岔路)。motive_q 一律是 meta(流程/情緒)型,非哲學型。 +## Role in comparison -## 待辦 -- verbatim 對齊審查:回原書校引言(尤其「find what you do best, expand on it」「review 頻率」)。 -- 未來可開一個**非對照**的 surface:這派該驅動「怎麼記錄/復盤」層,而非「哪個洞」層。 +- Motive questions ask whether a behavior is a recurring emotional pattern, not which school the user follows. +- The lens reinforces the product's strength-first and if-then-rule design. +- Low divergence is expected and should not be treated as a selection bug. -### Sources -- [Steenbarger — EBC 介紹](https://www.ebc.com/forex/brett-n-steenbarger) · [Trading Psychology 2.0 摘要](https://bookmap.com/blog/trading-psychology-2-0-summary) · [The Daily Trading Coach 摘要](https://www.bookey.app/book/the-daily-trading-coach) +All dimensions are `aligned` and intentionally have no `lean` value. + +## Sources + +- Brett Steenbarger, *The Daily Trading Coach* and *Trading Psychology 2.0* +- [Trading Psychology 2.0 summary](https://bookmap.com/blog/trading-psychology-2-0-summary) diff --git a/skills/fomo-kernel/rubric/vincent-yu.lens.json b/skills/fomo-kernel/rubric/vincent-yu.lens.json index bcd9654..3f11c05 100644 --- a/skills/fomo-kernel/rubric/vincent-yu.lens.json +++ b/skills/fomo-kernel/rubric/vincent-yu.lens.json @@ -1,72 +1,17 @@ { - "philosophy": "存活紀律派", - "master": "Vincent Yu(余鎮文)", - "source": "公開文章原則蒸餾(rubric/vincent-yu.md),引用非轉載、非經本人背書", - "_note": "這是「鏡片層」。engine 只算『洞是什麼』(普世行為金融),這個檔決定『這套哲學怎麼說這個洞、引哪句、問什麼動機』。對外顯示一律用 `philosophy`(v1 去名);`master`/`source` 留作內部 provenance,engine 不再印人名。換大師/哲學 = 換這個檔,engine 不動。新增 stance/lean 供 compare_lenses 算多哲學分歧;axis(universal/style,見 docs/v2c-lens-selection.md §11)待 v2a【風格】維落地後補(進場維屬【風格】,其餘 6 維【普世】)。", - - "master_intro": { - "one_line": "這套尺的核心:活下來,比賺最多更重要。它不看你『看對幾次』,看你『會不會被一次打死』。", - "pillars": [ - "存活優先——不爆倉才有複利,控制回撤 > 追求最大報酬", - "部位學 > 分析——績效是『勝率 × 賠率 × 頻率』,不是看對幾次", - "可證偽——下注前先寫好『什麼證據出現我就認錯退出』", - "預期差——好價格 = 市場共識和你理解的未來之間的落差,不是『便宜』", - "情境動作化——研究要導出『如果 A 就做 X』,不是搬資訊" - ], - "why_it_matters": "這把尺偏『存活與紀律』。如果你是純動能/當沖/套利,有些條目對你是『提問』不是『判錯』——它照的是你有沒有為『萬一錯了』準備,不是逼你變成價值投資者。" - }, - - "strength_intro": "先說你做對的一件事(不是客套,是讓你聽得進下面那刀):", - + "philosophy": "Survival discipline", + "master": "Vincent Yu", + "source": "Distilled from eighteen public Substack articles and two alignment reviews. All card language is paraphrase unless a verified quotation is explicitly marked.", + "_note": "Default runtime lens. It separates universal survival and calibration principles from style-specific questions about expectations and valuation frames.", + "master_intro": {"one_line": "Survive first, scale conviction with evidence, and turn research into explicit if-then actions.", "pillars": ["Control drawdown", "Match size to evidence", "Seek falsifiers", "Identify expectation gaps", "Convert research into action"], "why_it_matters": "This lens makes the decision process falsifiable and protects the ability to keep participating."}, + "strength_intro": "One thing you did well through this lens:", "dims": { - "部位 sizing": { - "rubric_unit": "B1 賠率 / A1 信念是光譜上的 sizing", - "stance": "aligned", "lean": "risk-capped", - "rule": "下單前先決定『這筆最多佔幾 %、為什麼是這個數而不是兩倍』。", - "quote": "門檻和部位必須一起調。門檻低,只能配小部位。門檻高,才配得起大部位。", - "motive_q": "{max_ticker} 佔你 {max_pct}%。這個 size 是『算過最壞情況能承受』,還是『就是很看好、直接重壓』?" - }, - "加碼攤平": { - "rubric_unit": "C2 雙紅線 / A2 試探≠加碼", - "stance": "conditional", "lean": "evidence", - "rule": "往下加碼前必須寫出『一個進場時不知道的新證據』;寫不出 → 不加。", - "quote": "可以低成本試探一次,不代表已完成長期信任的驗證;小成功不該自動升級成重倉。", - "motive_q": "{tickers} 你在虧損裡一路往下加。這是『你知道了一個進場時不知道的新利多』,還是『不想認賠、想攤低成本等回本』?" - }, - "出場紀律": { - "rubric_unit": "D1 時間軸 / G1 焦慮驅動", - "stance": "conditional", "lean": "thesis", - "rule": "賣出前先寫一句『我賣的理由是 thesis 破了,還是手癢/想換現金?』", - "quote": "在清醒時先把出場規則寫好,把判斷從『當下』移到『事前』。", - "motive_q": "你賣掉賺錢的有 {winner_early}% 後來繼續漲。那些賣出是『thesis 到價了』,還是『賺了怕回吐、落袋為安』?" - }, - "分散": { - "rubric_unit": "B2 driver 不同才算分散", - "stance": "conditional", "lean": "drivers", - "rule": "加新倉前先問『它跟我最大那塊是不是同一個 driver?』是 → 不加。", - "quote": "分散不是檔數多,是讓持有的標的來自不同的 underlying drivers。", - "motive_q": "你 {n} 檔有 {ai_pct}% 是同一個 driver。你當初『覺得這樣算分散』,還是『根本沒把它們當同一個賭注』?" - }, - "持有時間": { - "rubric_unit": "D1 先定時間軸", - "stance": "aligned", "lean": "preset-frame", - "rule": "每筆進場先標『短線/波段/長線』,出場只准用同框架的理由。", - "quote": "先想清楚你的時間軸,讓所有後續分析匹配它。", - "motive_q": "{incon_tickers} 你同一檔又當沖又長抱。是『有意切兩套策略』,還是『套牢了就改口說自己是長期投資』?" - }, - "alpha/beta": { - "rubric_unit": "E2 拆解你承擔什麼風險", - "stance": "aligned", "lean": "decompose", - "rule": "每季只認 alpha,別把『大盤+槓桿』給你的,當成自己的能力。", - "quote": "優秀的投資人要能 decompose risk——知道自己承擔的是總經、產業、個股還是政策風險,以及報酬值不值得。", - "motive_q": "你贏大盤 {excess}pp,但 β={beta}。這些報酬你算『自己選股的本事』,還是『敢押高波動換來的』?" - }, - "進場": { - "rubric_unit": "G1 焦慮驅動 / A4 從價格倒推預期|待 engine B.9(【風格】維)", - "stance": "conditional", "lean": "gap", - "rule": "進場前先寫一句『我的觸發條件是 X、市場已隱含什麼預期』;追到伸展高點又說不出預期差 → 不進。", - "quote": "焦慮很容易偽裝成上進。你以為在追求進步,其實在逃離『我好像輸了』的感覺。", - "motive_q": "{entry_ticker} 你買在近 20 日高點、當天還大漲。是你的框架觸發(說得出市場隱含什麼預期、你認為錯在哪),還是它在噴、你怕錯過?" - } + "position_sizing": {"rubric_unit": "Scale size with evidence and survival risk", "stance": "aligned", "lean": "risk-capped", "rule": "Before entry, set the maximum portfolio percentage and explain why the evidence supports that size rather than twice as much.", "quote": "Conviction is a sizing spectrum constrained by the need to survive. (paraphrase)", "motive_q": "{max_ticker} is {max_pct}% of the portfolio. Which independent evidence supports this size, and what loss would threaten continued participation?"}, + "averaging_down": {"rubric_unit": "Require new evidence before adding", "stance": "conditional", "lean": "evidence", "rule": "Before adding to a loss, write one material fact that was unknown at entry; if none exists, do not add.", "quote": "An add should reflect updated evidence, not the arithmetic desire to lower cost. (paraphrase)", "motive_q": "You added to {tickers} while it was losing. What material evidence was new after entry, and how did it change the thesis?"}, + "exit_discipline": {"rubric_unit": "Precommit falsifiers while calm", "stance": "conditional", "lean": "thesis", "rule": "Before entry, write the thesis falsifier and the if-then exit action; use it before emotion rewrites the decision.", "quote": "High-cost decisions should be moved from the stressed moment into a precommitted process. (paraphrase)", "motive_q": "Of the profitable exits, {winner_early}% later rose. Which exits followed a written falsifier, and which were driven by restlessness or the need for cash?"}, + "diversification": {"rubric_unit": "Diversify underlying drivers", "stance": "conditional", "lean": "drivers", "rule": "Before adding a position, ask whether it shares the same underlying driver as the largest exposure; if yes, treat it as the same bet.", "quote": "Diversification is about distinct drivers, not the number of tickers. (paraphrase)", "motive_q": "You hold {n} instruments and {ai_pct}% shares one driver. Which holdings have genuinely independent drivers?"}, + "holding_period": {"rubric_unit": "Match evidence and exit logic to the preset horizon", "stance": "aligned", "lean": "preset-frame", "rule": "Label the horizon before entry and allow exit reasons only from the same analytical frame unless new evidence explicitly changes it.", "quote": "The variables used to judge a trade should match its intended horizon. (paraphrase)", "motive_q": "For {incon_tickers}, did new evidence change the analytical frame, or did the label change after the outcome?"}, + "alpha_beta": {"rubric_unit": "Decompose return before claiming edge", "stance": "aligned", "lean": "decompose", "rule": "Each review period, separate market, sector, leverage, and instrument-specific return before calling the result alpha.", "quote": "External attribution is more reliable than a memorable self-story. (paraphrase)", "motive_q": "You beat the benchmark by {excess}pp with beta {beta}. What return remains after market, sector, and leverage exposure are removed?"}, + "entry_style": {"rubric_unit": "State the expectation gap and falsifier", "stance": "conditional", "lean": "gap", "rule": "Before entry, state what expectation the market embeds, what you believe instead, and what evidence would prove you wrong.", "quote": "Research becomes actionable when it identifies an expectation gap and an if-then response. (paraphrase)", "motive_q": "For {entry_ticker}, what market expectation was wrong, and what would falsify your variant view?"} } } diff --git a/skills/fomo-kernel/rubric/vincent-yu.md b/skills/fomo-kernel/rubric/vincent-yu.md index 62f58e6..ac51ce5 100644 --- a/skills/fomo-kernel/rubric/vincent-yu.md +++ b/skills/fomo-kernel/rubric/vincent-yu.md @@ -1,170 +1,84 @@ -# Lens · Vincent Yu(余鎮文)— 交易復盤 Rubric v2 +# Lens: Vincent Yu trade-review rubric — v2 -> 第二版鏡片。2026-06-07 由 Claude 重切:基於 18 篇 Substack **verbatim 原文**(`../research/vincent-sources/`)+ Gemini 與 Codex 兩份對齊審查(前者見 `../research/_vincent-yu.v2-gemini-draft.md`、後者做了逐句引用稽核)。v1 原版備份在 `../research/_vincent-yu.v1-original.md`。 -> -> **這版改了什麼**:① 每條標【屬性】【忠誠度】【引用類型】;② 修掉 v1 把職涯散文(〈beta〉〈Thorp〉)的金句硬掛到交易上的問題;③ A3/E2 引言改掛對的原文;④ 補 5 個缺漏單元(凸性、持有期降噪、可證偽退出、信念型資產、敘事流動性);⑤ E1「40x PE」標明是 VY **引述 Alix**、非原話。 +This lens was rebuilt from eighteen public Substack articles and two independent alignment reviews. It distinguishes universal decision principles from style-specific preferences and records whether each idea is literal, interpreted, or borrowed across domains. ---- +## How to use the lens -## 怎麼讀這份鏡片 +Five principles form the spine: -**Vincent 策略脊椎(Gemini + Codex 兩邊收斂的 5 支柱):** -1. **存活優先**——不爆倉才有複利;控制回撤 > 最大化報酬(演化篩選 / 波動稅)。 -2. **部位學 > 分析**——信念是光譜上的部位決策;勝率×賠率×頻率才是績效。 -3. **可證偽與校準**——切斷「自我認同 ↔ 部位」綁定;主動找讓自己不舒服的反例。 -4. **預期差與評價尺漂移**——好價格 = 共識與你理解的未來之間的 gap;看市場用哪把尺、何時換。 -5. **情境動作化**——研究要導出「如果 A→做 X,如果 B→做 Y」,不是資訊搬運。 +1. **Survival first**: control drawdown before maximizing return. +2. **Sizing before analysis theater**: performance combines hit rate, payoff, frequency, and how much was risked. +3. **Falsifiability and calibration**: separate identity from a position and search for uncomfortable counterevidence. +4. **Expectation gaps and changing valuation frames**: infer what the market prices and watch when the evaluation frame changes. +5. **Context to action**: research must produce explicit if-then actions rather than information summaries. -**三組標註:** -- **屬性**:【普世】=概率/存活/認知偏誤的硬規律,任何風格都該守,可直接判對錯 ✅/❌ |【風格】=反映 Vincent 特定偏好,**只當「Defend 型提問」**逼使用者辯護,不強判對錯(否則會誤傷動能/套利/價值等不同風格的人)。 -- **忠誠度**:(a) 字面=他針對投資寫過 | (b) 詮釋=他的原則、我們的應用 | (c) 跨域=從職涯/處世散文挪用(只供人生復盤,**不判交易**)。 -- **引用類型**:【原話】=已回原文 grep 驗證的 verbatim |【意譯】=忠於原意的改寫(**公開引用前須再對原文校一次**)|【引述】=Vincent 引用他人(Alix / Mauboussin / Alchian / Taleb…)。 +Attribute every unit as: -**輸出鐵律不變**:不逐條輸出 28 個分數。全跑,只挑 1–2 個最高代價的洞,壓成一張卡:`評級一句話` + `最大漏洞(附使用者自己的數據)` + `下次只改這一件(if-then)` + `一句引言`。 +- `universal`: a probability, survival, or cognitive rule that can support a direct diagnosis +- `style`: a Vincent-specific preference used as a defend question rather than a universal verdict +- `literal`: directly grounded in investment writing +- `interpreted`: the source principle applied to this product +- `cross-domain`: borrowed from non-investment writing and unsuitable as a direct trading verdict ---- +Run all applicable units, but output only the one or two highest-cost gaps and converge on one final rule. -## A · 信念與訊號(Conviction) +## A. Conviction and signals -**A1 · 信念是「光譜上的部位決策」,不是信/不信的是非題** 【普世】(b) -- 診斷:部位大小對應到你「真正驗證過的信心」嗎?還是一信就梭、一疑就空?說得出「為什麼是這個 size 而不是兩倍」嗎? -- 【原話】(9fa):「門檻和部位必須一起調。門檻低,只能配小部位。門檻高,才配得起大部位。」 +- **A1 — conviction is a sizing spectrum**: evidence quality and position size must move together. +- **A2 — separate probes from adds**: a small success does not automatically validate a larger position. +- **A3 — independent signals versus repeated noise**: several observations from the same people and incentive structure are not independent evidence. +- **A4 — infer market expectations from price**: state which embedded expectation is wrong and what would falsify that view. -**A2 · 把「試探」和「加碼」分開;小成功 ≠ 驗證** 【普世】(b) -- 診斷:加碼依據,是標的在「無法被事先安排」下通過考驗,還是只是前面小賺/對方端出的漂亮數字? -- 【意譯】(9fa/2a6):可以低成本合作一次,不代表已完成長期信任的驗證;試探倉的小成功不該自動升級成重倉。 +## B. Position sizing -**A3 · 區分真訊號 vs 同源雜訊:有幾個證據是真正獨立的?** 【普世】(a) -- 診斷:支撐這筆的理由,是來自彼此獨立的多個源,還是「同一批人、同一套敘事」在不同場合反覆出現? -- 【原話】(2a6):「如果三十次互動,都來自同一批人(CEO和CFO)、同一套敘事、同一個上市前的利益結構,那它們不能被當成三十次獨立驗證。」 - - ⚠️ v1 此條誤掛「一個從來沒被挑戰的判斷器…」那句——那句其實屬 G2(連勝/校準),已移走。 +- **B1 — performance is hit rate times payoff times frequency**: evaluate the downside and whether a loss removes the ability to continue. +- **B2 — diversification means distinct underlying drivers**: ticker count is not enough when exposures share one cause. -**A4 · 從價格倒推市場預期,再問「這個預期憑什麼是錯的」** 【普世】(a) -- 診斷:論點是「我覺得會漲」,還是「市場現在隱含 X 預期,我認為它錯在 Y」?有沒有明確認錯條件? -- 【原話】(899):「現在的價格,隱含的是什麼未來?這個未來合理嗎?和我理解的現實有多大落差?」 -- 【引述】(899, 轉述 Mauboussin):「先從股價推導出市場的預期,再判斷這些預期是否合理。」 +## C. Exit, risk, and survival -## B · 部位 / Sizing +- **C1 — write rules while calm**: move high-cost decisions from the emotional moment to a precommitted process. +- **C2 — use both amount and percentage red lines**: one mistake must not end participation. +- **C3 — drawdown comes first**: recovery requirements grow nonlinearly as losses deepen. -**B1 · 績效 = 勝率 × 賠率 × 頻率,不是「看對幾次」** 【普世】(a) -- 診斷:決定 size 時看了上行/下行對稱嗎?最壞情況失去的是「錢」還是「再玩一次的資格」? -- 【原話】(896):「投資的計分板不能只算『看對幾次』,還要考慮每次怎麼下注、結果產生了多少報酬。勝率、賠率、交易頻率,三者合起來,才構成你真正的績效。」 +## D. Research process -**B2 · 分散的真義是「underlying drivers 不同」,不是檔數多** 【普世】(a) -- 診斷:部位看似分散,是不是其實同一個因子驅動(同產業、同總經押注)? -- 【原話】(investment-framework):「分散風險並不是說投資組合中有越多支股票就越好,而是儘量讓持有的標的是來自不同的 underlying drivers。」(他自舉例:20 支全 AI-related vs 2 支「一 AI 一 commodity cycle」,前者風險大非常多。) +- **D1 — match variables to horizon**: short-term positions need short-term drivers; long-term theses should not be overturned by one noisy month. +- **D2 — research must lead to action**: write what to do under scenario A and scenario B. +- **D3 — define falsifiers and exit conditions**: the assumption that cannot be wrong is the most dangerous one. +- **D4 — valuation is not self-executing**: as a style-specific defend question, ask which market frame will make the valuation matter. -## C · 出場、風控與存活(替未來的自己架欄杆) +## E. Edge and alpha -**C1 · 清醒時先把規則寫好,把判斷從「當下」移到「事前」** 【普世】(b) -- 診斷:出場/停損/加碼上限,是進場前白紙黑字定好,還是進場後靠臨場感覺? -- 【原話】(aed):「真正比較穩的做法,是把一部分判斷從當下移到事前。在你還沒進入那個被設計好的情境之前,先把規則寫好。」當下只做「符不符合規則」的分類題。 +- **E1 — variant view**: as a style-specific question, ask what value is visible that consensus misses, not merely whether a stock looks cheap. +- **E2 — decompose alpha and beta**: separate market, sector, leverage, and instrument-specific drivers before claiming skill. -**C2 · 紅線雙重設定(金額 + 比例),確保「即使判斷錯也不被一次帶走」** 【普世】(b) -- 診斷:有沒有同時設「單筆最大可虧金額」和「最大部位比例」?重大動作前有冷靜期嗎? -- 【原話】(896):「你的部位設定錯了——太小,白忙一場;太大,一次錯誤直接出局。…對沒有經驗的投資人而言,這是他們能不能長期在市場上活下去的及格線。」 +## F. Risk and bets -**C3 · 回撤第一順位——存活優先於最優化** 【普世】(a) -- 診斷:風控有沒有把「控制最大回撤」放在「追求最大報酬」前面?知道虧越深、回本所需報酬越誇張嗎? -- 【原話】(investment-framework):「manage drawdown is critical。損失越大後要回到 breakeven 的 return 越高」 -- 【原話】(853):「在不確定的世界裡,存活優先於最優化。」/【引述】(853, Alchian):市場不獎勵最聰明的,只懲罰不適應的——你看到的成功者可能只是倖存者。 +- **F1 — prefer asymmetry**: ask whether downside has support while upside remains meaningful. +- **F2 — survive uncertainty**: when timing confidence is low, reduce dependence on a precise turn. +- **F3 — skin in the game**: recorded decisions and account outcomes outrank retrospective commentary. +- **F4 — barbell and convexity**: distinguish survival insurance from a get-rich lottery. -## D · 研究流程 +## G. Psychology -**D1 · 先定時間軸,再讓變數對齊時間軸(短線盯 newsflow、長線盯結構)** 【普世】(a) -- 診斷:講得出持有期嗎?關注的變數跟那個時間軸匹配嗎?短期部位是否被長期估值絆住、長期部位是否被單月噪音洗掉? -- 【原話】(45e):「時間軸匹配:你的投資期長,決定你該關心哪些變數。」(他舉:短線「newsflow 就是王道,估值次要」;長線「單月營收下滑 20% 可能是噪音」;典範轉移初期「先放下傳統估值、提高敘事權重」。) +- **G1 — anxiety versus aspiration**: determine whether the trade fits a framework or temporarily relieves fear of falling behind. +- **G2 — winning streaks require calibration**: comfort and recent success may indicate that counterevidence is being filtered out. +- **G3 — assume the stressed self is unreliable**: high-cost decisions need a default process. -**D2 · 研究要導出動作(情境→action),不能只是資訊搬運** 【普世】(a) -- 診斷:結論有沒有寫「願意下多大、錯了怎麼辦、何時加碼/撤退」? -- 【原話】(45e):「情境到動作(context→action)…『如果 A→做 X;如果 B→做 Y』。」(他形容差研究「讀起來像維基百科摘要:資訊不少,但我仍不知道該做什麼動作」。) +## H. Epistemic honesty -**D3 · 可證偽性 + 退出條件:最危險的是那個「不可能錯」的假設** 【普世】(a) -- 診斷:下注前替論點寫了「可被推翻的條件」嗎?還是只有看多理由、說不出在什麼證據下認錯? -- 【原話】(13f):「更少人會為假設設定『退出條件』。…你,就是那個最危險的投資。」 -- 【引述】(13f, Popper):一個理論是科學的,不因它能被證實,「而是因為它敢於設定自己可能被推翻的條件」。 - - 註:v1/Gemini 草稿用「Pre-mortem / 決策日誌」當標題——VY 原文用的是**可證偽性與退出條件**,這裡正名。Pre-mortem 是常見手法但非他逐字用語,列為【意譯】輔助。 +- **H1 — external validation over self-image**: test selection skill against the ledger rather than memorable wins. +- **H2 — crowded indicators decay**: a signal followed by everyone may stop carrying information. +- **H3 — better research increases humility**: say what remains unknown and what action follows if it occurs. -**D4 · 估值不是聖杯;edge 是看懂市場用哪把尺、那把尺何時換** 【風格】(b) -- Defend 提問:你的論點是「我 DCF 算出便宜,市場該回來」,還是「市場評價體系即將漂移」?(純技術/動能交易者可不吃這條——當 Defend 提問,不強判。) -- 【原話】(45e):「新手常把估值當成聖杯…問題是市場從不負責實現你的計算。」(他補:新度量衡也必須有「作廢條件」,否則「是毒藥而非聖杯」。) +## Situational units -## E · Edge / Alpha 認知 +- **S1 — belief assets**: for assets without cash flows, examine narrative, community belief, and the appropriate valuation frame instead of forcing cash-flow analysis. +- **S2 — narrative trades**: examine sentiment heat, falsifiability, and liquidity before treating a broad story as an actionable setup. -**E1 · 真 edge 是在「貴」的地方看到別人沒看到的價值(variant view)** 【風格】(a)【引述】 -- Defend 提問:你的洞見是人人看得到的「便宜/利多」,還是逆共識、需要解釋為什麼別人錯? -- 【引述】(how-to-be-a-great-analyst,VY 特別認同並引用 **Alix Pasquet** 的話,非 VY 原創):「I don't need an analyst to tell me when a 10x PE stock is cheap. I need an analyst to tell me when a 40x PE stock is cheap.」 - - ⚠️ 偏 Growth/Quality 風格;對 deep value / cyclical 交易者只當 Defend 提問,別判錯。 +## Publication work -**E2 · 你以為的 edge,可能只是 beta——拆解你承擔的是什麼風險** 【普世】(a) -- 診斷:扣掉大盤/板塊、扣掉槓桿與跟單,還剩什麼是你自己判斷的 alpha?你說得出手中部位的 driver 嗎? -- 【原話】(investment-framework):「優秀的投資人要能夠掌握手中股票的 drivers、decompose risk(知道自己承擔了什麼樣的風險—總體經濟、產業、公司個別因素、地緣政治、政策…)和相應的報酬是否值得?」 - - ⚠️ v1 此條掛〈beta〉散文的金句「你以為自己是 Beta,只是因為你一直站在別人的指數裡」——那篇是**個人職涯定位**散文(c),已撤掉,改掛此投資本位原文。 - -> (v1 的 **E3「組合稀缺性/時間是護城河」** 來自〈beta〉職涯散文,談的是個人定位、無法在單筆交易上診斷——**移出交易 Rubric**,留待「人生/職涯復盤」另用。) - -## F · 風險 / 賭注 - -**F1 · 偏好不對稱:上行夠、下行有撐** 【風格】(b) -- Defend 提問:賠率對稱嗎?主要假設落空時,這標的跌最少、最撐得住嗎?(期權賣方/高頻撿銅板者結構相反——當 Defend 提問。) -- 【意譯】(896/899):最好的投資未必彈性最大,而是上行夠、下行也有支撐的那個。 - -**F2 · 沒把握時,選「撐得到收割」的那個** 【風格】(b) -- Defend 提問:對轉折/時點信心不強時,部位壓在「賭對才不會死」還是「慢一點也撐得住」的標的? -- 【意譯】(853 存活邏輯):低信心時降低對「賭中時點」的依賴。 - -**F3 · Skin in the game:結論要押得下去、用真實結果檢驗** 【普世】(b) -- 診斷:觀點有沒有真的下注?事後用真實帳本(而非說法)檢驗對錯? -- 【意譯】(896):理論服從實證;只有事後評論、從不留下可被檢驗的下注,不算數。 - - ⚠️ v1 此條引〈Thorp〉篇描述 Thorp 個人健身長壽的句子——那是人物散文(c),已撤;改掛下注本位的 896。 - -**F4 · 槓鈴/凸性:你買的是「生存保險」還是「以小博大的彩券」?** 【風格】(a) -- Defend 提問:你的高風險那端,是清楚自己在「付保費賭活得比錯價久」,還是誤把槓鈴當翻身工具? -- 【原話】(947):「你不是在賭黑天鵝,你是在跟『保費的日曆』對賭,看自己能不能活得比錯價更久。」「大眾對槓鈴策略最大的誤解,在於認為它是一種『以小博大』的工具。但事實上卻恰恰相反。」 - - 註:VY 真意是「標準槓鈴是資本夠大者的生存保險,不是致富指南」,別簡化成「凸性 vs 彩券」。 - -## G · 心態 / 情緒 - -**G1 · 分清「焦慮驅動」還是「渴望驅動」——你追的是不是你的東西** 【普世】(b) -- 診斷:這筆是因為「我落後了、別人都在賺、得趕快上車」,還是標的真的符合你的框架? -- 【原話】(19f):「焦慮很容易偽裝成上進。你以為自己是在追求進步,其實是在逃離那個『我好像輸了』的感覺。」「他想要的是一個很短暫的心理止痛藥:我好像還有在追趕。」 - -**G2 · 連勝是「該檢查外推條件」的警報,不是調高信心的理由** 【普世】(b) -- 診斷:這筆重倉是不是踩在連勝期上、因為「我最近都對」而自動放大? -- 【原話】(2a6):「如果你最近所有判斷都讓自己越來越舒服、越來越順、越來越覺得『我果然看得很準』,那通常要擔心判斷器是不是把所有會挑戰它的訊號濾掉,只留下那些會強化它的訊號。」 - - 註:A3 被誤掛的那句「一個從來沒被挑戰的判斷器,無論看起來多準,都只是在重複過去」原文也在此段,本屬此條。 - -**G3 · 承認「當下的自己不可靠」,高代價決策一律走預設流程** 【普世】(b) -- 診斷:系統有沒有預設「壓力/連勝/沉沒成本同時來襲時自己會失控」並提前防範? -- 【原話】(aed):「高代價的信任決策,很少發生在最冷靜的時候。」「你知道自己不是每一刻都可靠,所以不把所有決策權交給當下的自己。」 - -## H · 認知誠實 - -**H1 · 用外部驗證取代自我感覺:你對自己的判斷比你以為的更不可靠** 【普世】(b) -- 診斷:你怎麼知道自己「會選股」?靠真實帳本與外部回饋,還是記得住的幾次成功 + 自我敘事? -- 【意譯】(13f/896):用驗證代替空想;區分「系統適配問題」與「真能力問題」。 - -**H2 · 人人都盯同一個指標,那指標就會失真——別把擁擠當高勝率** 【普世】(b→c 註) -- 診斷:你追的訊號,是還有資訊優勢,還是已是全市場集體追逐的失真指標? -- 【意譯】(idea 出自〈beta〉散文「當所有人都圍著某個指標打轉,那個指標本身就會失真」——原文在職涯脈絡,這裡轉譯到「擁擠交易」;概念可用,但非投資本位原話,標(c)。) - -**H3 · 好交易者越做越謙卑:更常說「不知道」,但更清楚「如果發生,我該怎麼辦」** 【普世】(b) -- 診斷:復盤更接近「我早就知道」的事後諸葛,還是「我當時假設錯在這、已準備好的應對是什麼」? -- 【原話】(45e):「真正好的研究,會讓你更謙卑。你更常承認『不知道』,但你也更清楚『如果發生,我該怎麼辦』。」 - -## ✛ 情境型單元(只在用戶交易到對應標的時才啟用,屬【風格/專用】) - -**S1 · 信念型(零現金流)資產:別盲套現金流估值** 【風格】(a) -- 適用:用戶在交易 BTC/黃金/收藏品等無現金流標的時。 -- 【原話】(357):「它們的價值並非來自自身的生產力,而是完全依賴於人類對其未來價格的集體想像、對敘事的信念,以及社群的情緒。我稱這類標的為——『信念型資產』。」(VY 給了「信念三維座標」框架去解構。) - -**S2 · 敘事交易的前提:情緒有熱度、故事未證偽、流動性充裕** 【風格】(a) -- 適用:用戶在賭宏大敘事/題材股時。 -- 【原話】(899):「市場情緒必須有熱度:整體市場絕對不能是熊市」(VY 列為大舉買入炒熱敘事前「務必先確認」的條件之一;偏資金體量大的進階玩法。) - ---- - -### 待辦(公開/上線前) -- ✅【意譯】條目已逐句回查(2026-06-07):C1/C2/D4/G2/G3/H3 查到 verbatim 已升【原話】;A2/F1/F2/H1 原文確無逐字對應,維持忠實改寫【意譯】。 -- S1/S2 與 F4 的「三條件/座標」完整列舉需回 357/899/947 補全。 -- E1/E3 的 IP 註記:E1 是引述 Alix、E3 已移出——對外用語要標清楚出處。 +- Recheck every paraphrase against the original article before using it as a quotation. +- Preserve attribution when the source itself quotes another thinker. +- Keep cross-domain material out of direct trading verdicts. +- Keep lens selection separate from the deterministic lifecycle and renderer. diff --git a/skills/fomo-kernel/schemas/answers.schema.json b/skills/fomo-kernel/schemas/answers.schema.json new file mode 100644 index 0000000..4636575 --- /dev/null +++ b/skills/fomo-kernel/schemas/answers.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fomo-kernel.local/schemas/answers.schema.json", + "title": "Fomo Kernel Review Answers", + "type": "object", + "additionalProperties": false, + "required": ["session_id", "answers", "thesis_updates"], + "properties": { + "session_id": {"type": "string"}, + "answers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["question_id", "choice"], + "properties": { + "question_id": {"type": "string"}, + "choice": {"type": "string"}, + "note": {"type": ["string", "null"]}, + "evidence_delta": { + "type": ["object", "null"], + "properties": { + "claim": {"type": "string"}, + "source": {"type": "string"}, + "observed_at": {"type": ["string", "null"]}, + "falsifier": {"type": ["string", "null"]} + }, + "additionalProperties": false + } + } + } + }, + "commitment": { + "type": "object", + "required": ["choice"], + "properties": { + "choice": {"type": "string"}, + "rule": {"type": "string"}, + "metric_key": {"type": "string"}, + "goal": {"enum": ["down", "up", "hold"]}, + "dim": {"type": "string"} + }, + "additionalProperties": false + }, + "thesis_updates": {"type": "array", "items": {"type": "object"}}, + "observations": {"type": "array", "items": {"type": "string"}} + } +} diff --git a/skills/fomo-kernel/schemas/narrative.schema.json b/skills/fomo-kernel/schemas/narrative.schema.json new file mode 100644 index 0000000..88ffc89 --- /dev/null +++ b/skills/fomo-kernel/schemas/narrative.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fomo-kernel.local/schemas/narrative.schema.json", + "title": "Fomo Kernel Prose-only Narrative", + "type": "object", + "additionalProperties": false, + "required": ["headline", "mirror"], + "properties": { + "headline": {"type": "string", "minLength": 1, "pattern": "^\\D+$"}, + "mirror": {"type": "string", "minLength": 1, "pattern": "^\\D+$"}, + "counterfactual": {"type": "string", "minLength": 1, "pattern": "^\\D+$"}, + "rule_rationale": {"type": "string", "minLength": 1, "pattern": "^\\D+$"}, + "strength": {"type": "string", "minLength": 1, "pattern": "^\\D+$"} + } +} diff --git a/skills/fomo-kernel/schemas/review-plan.schema.json b/skills/fomo-kernel/schemas/review-plan.schema.json new file mode 100644 index 0000000..7d65fa9 --- /dev/null +++ b/skills/fomo-kernel/schemas/review-plan.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fomo-kernel.local/schemas/review-plan.schema.json", + "title": "Fomo Kernel Review Plan", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "session_id", "status", "route", "flow_path", "language", + "persist", "state_root", "input", "state_snapshot", "question_queue", + "missing_thesis_positions", "card_plan", "engine_card", "engine_state" + ], + "properties": { + "schema_version": {"const": 2}, + "session_id": {"type": "string", "minLength": 8}, + "status": {"const": "awaiting_answers"}, + "route": {"enum": ["first_review", "weekly_review", "snapshot_review", "test_drive"]}, + "flow_path": {"type": "string"}, + "language": {"enum": ["zh-TW", "en"]}, + "persist": {"type": "boolean"}, + "state_root": {"type": "string"}, + "input": { + "type": "object", + "required": ["paths", "kind", "fingerprint"], + "properties": { + "paths": {"type": "array", "items": {"type": "string"}}, + "kind": {"enum": ["trades_csv", "positions_snapshot"]}, + "fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, + "engine_meta": {"type": "string"} + }, + "additionalProperties": false + }, + "state_snapshot": {"type": "object"}, + "question_queue": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "kind", "required", "question", "options"], + "properties": { + "id": {"type": "string"}, + "kind": {"enum": ["add_thesis", "headline_motive", "revisit", "rule_breach"]}, + "ticker": {"type": ["string", "null"]}, + "cycle_id": {"type": ["string", "null"]}, + "required": {"type": "boolean"}, + "question": {"type": "string", "minLength": 1}, + "options": { + "type": "array", + "minItems": 2, + "items": { + "type": "object", + "required": ["value", "label", "description"], + "properties": { + "value": {"type": "string"}, + "label": {"type": "string"}, + "description": {"type": "string"} + }, + "additionalProperties": false + } + }, + "prior_thesis_id": {"type": ["string", "null"]} + }, + "additionalProperties": false + } + }, + "missing_thesis_positions": {"type": "array", "items": {"type": "object"}}, + "card_plan": {"type": "object"}, + "engine_card": {"type": "object"}, + "engine_state": {"type": "object"} + } +} diff --git a/skills/fomo-kernel/schemas/session-bundle.schema.json b/skills/fomo-kernel/schemas/session-bundle.schema.json new file mode 100644 index 0000000..9618eee --- /dev/null +++ b/skills/fomo-kernel/schemas/session-bundle.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fomo-kernel.local/schemas/session-bundle.schema.json", + "title": "Fomo Kernel Canonical Session Bundle", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "session_id", "route", "language", "review_plan", "engine_state", + "engine_card", "answers", "narrative", "thesis_updates", "thesis_decisions", + "commitment", "observations" + ], + "properties": { + "schema_version": {"const": 2}, + "session_id": {"type": "string"}, + "route": {"enum": ["first_review", "weekly_review", "snapshot_review", "test_drive"]}, + "language": {"enum": ["zh-TW", "en"]}, + "review_plan": {"$ref": "review-plan.schema.json"}, + "engine_state": {"type": "object"}, + "engine_card": {"type": "object"}, + "answers": {"$ref": "answers.schema.json"}, + "narrative": {"$ref": "narrative.schema.json"}, + "thesis_updates": {"type": "array", "items": {"type": "object"}}, + "thesis_decisions": {"type": "array", "items": {"type": "object"}}, + "commitment": {"type": ["object", "null"]}, + "observations": {"type": "array"} + } +} diff --git a/tests/agent/README.md b/tests/agent/README.md index 95f56e5..a9945d3 100644 --- a/tests/agent/README.md +++ b/tests/agent/README.md @@ -1,66 +1,41 @@ -# tests/agent/ — SKILL 行為層 eval harness +# Agent-level evaluation harness -實作範圍:[`docs/eval-design.md`](../../docs/eval-design.md) §2 harness + §4 A/B 斷言。 -分兩塊,判定哲學(§1「code-check > LLM-judge > 人工」)決定各走哪條: +Implementation authority is [docs/eval-design.md](../../docs/eval-design.md). The harness separates checks according to the strongest available evidence. -- **離線機檢(確定性,進 `tests/run_all.py`)** — 能 regex / JSON diff 斷言的卡面/狀態鐵律。 -- **LLM-judge(非確定性,需 API key,不進 CI)** — 只有「敘事品質」一項,判斷「好不好」而非「犯規沒」。 -- **headless 產卡(非確定性 + 有成本,opt-in)** — 真跑 skill 產卡,再餵上面兩者。 +## Layers -## 檔案 +- **Offline deterministic checks**: regular-expression and JSON assertions over card/state artifacts. These run in `tests/run_all.py`. +- **LLM narrative judge**: optional and non-deterministic. It evaluates prose quality rather than mechanical contract violations and requires an API key. +- **Headless card generation**: optional, non-deterministic, and billable. It runs the skill and feeds resulting artifacts into the two layers above. -**離線機檢(#60,`python3 tests/test_checkers_offline.py` 或併進 run_all.py 第 10 套)** -- `check_card.py` — 卡面鐵律機檢(A-2/A-3/A-6/A-12/A-13/B-7/B-9)。import 或 CLI。斷言權威 = - eval-design §4 + card-spec.md;改鐵律要同步(見各檔頭)。 -- `check_state.py` — 狀態檔 trajectory adherence(S-1..S-4 收尾產物 + 差分 / append-only helper)。 - **刻意不重造** coach.py / `test_tr_json_contract.py` 已擁有的 cycle_id 格式 / enum / commitment - schema,只管那兩層管不到的收尾產物層。 -- `../test_checkers_offline.py` — 上兩支的**驗活**(eval-design §6):乾淨輸入全過、刻意壞掉必掛 - 對應條;check_state 用 coach.py【真實寫入】當 known-good oracle。無網路、確定性。 +## Files -**LLM-judge(敘事品質,需 `ANTHROPIC_API_KEY`)** -- `judge_narrative.py` — judge 本體(五軸 + overall,rubric 抄 card-spec.md 敘事鐵律)。 -- `run_judge_eval.py` — judge 的 mutation 驗活,跑 `fixtures/manifest.json`。 -- `fixtures/` — 一張乾淨卡(`card_good.txt`,同時是 check_card 的乾淨參照)+ 兩張刻意壞卡。 +- `check_card.py`: deterministic card invariants from the card specification and eval design. +- `check_state.py`: finalization and trajectory artifacts not already owned by `coach.py` or JSON contract tests. +- `../test_checkers_offline.py`: mutation probes that prove known-good artifacts pass and intentionally broken artifacts fail. +- `judge_narrative.py`: optional narrative-quality rubric. +- `run_judge_eval.py`: mutation probes for the judge fixtures. +- `fixtures/`: known-good and intentionally broken card examples. +- `personas.md`: scripted users and differential pairs. +- `cases/*.yaml`: input, persona, run mode, and assertion declarations. +- `run_case.sh`: offline checking and optional headless orchestration. -**harness 編排(headless,opt-in)** -- `personas.md` — 5 個腳本化模擬用戶 + 差分對(eval-design §3)。 -- `cases/*.yaml` — case 宣告(輸入 CSV / persona / 該套哪些斷言)。`washer` = §落地順序 step 1。 -- `run_case.sh` — `--check ` 對已產出的卡/狀態機檢(離線核心,CI-verified); - `--headless ` 隔離 HOME 跑 `claude -p` 產卡再機檢(需 claude CLI + API key)。 - -## 跑法 +## Commands ```bash -# 離線機檢(無需網路 / API key)——已併進 run_all.py python3 tests/test_checkers_offline.py python3 tests/agent/check_card.py tests/agent/fixtures/card_good.txt python3 tests/agent/run_case.sh --check my_card.md ~/.trade-coach -# LLM-judge(需 API key;放 .env 或 export) -export ANTHROPIC_API_KEY=sk-... +export ANTHROPIC_API_KEY=... python3 tests/agent/run_judge_eval.py - -# headless 產卡(opt-in,需 claude CLI + API key,有成本、非確定性) tests/agent/run_case.sh --headless tests/agent/cases/washer.yaml ``` -## ⚠️ (c) 內心層的 headless 天花板(issue #60/#159) - -Step 2「該問有沒有問」的**工具主路徑**(AskUserQuestion)headless 測不到——headless `claude -p` -沒有該工具,只會走 fallback 對話路徑(EVALS.md 2026-07-04 回歸紀錄實測)。所以: -- `check_card` / `check_state` 機檢的是**產出物**(卡/狀態),不管卡怎麼產的,離線可跑。 -- 主路徑 adherence(工具問答的順序 / 差分敏感度)要**互動 session** 驗(case.yaml `run_mode: interactive`), - 或靠 Step 4 線上反饋——這是內心層無 ground truth 的固有天花板(#159 三層框架的 (c) 層)。 - -## 仍待辦(#60 較大本體) +## Headless limitation -- B-1 洗白者 eval-first 的 red→green 全流程(§落地順序 step 1):需互動 / headless 真跑一輪。 -- check_card 的 case 特定斷言(B-1 標籤定位、B-9 section 級 ticker-in-洞):現只做卡層 invariants。 -- grader 校準(§6):首批 transcript 人工全判對比機檢,量 FP/FN。 +Headless `claude -p` does not expose the same interactive question tool as a normal session, so it can exercise only the text fallback. Artifact checkers remain valid because they inspect outputs rather than the internal conversation. Tool-order adherence and interactive option behavior require an interactive session or real user feedback. -## 維護鐵律 +## Maintenance rule -`card-spec.md` 敘事鐵律改 → `judge_narrative.py` 的 `RUBRIC` 跟著改; -eval-design §4 的 A/B regex 改 → `check_card.py` / `check_state.py` 的對應 CHECK 跟著改。 -同源判準漂移防線見 `docs/eval-design.md` §5。 +When card narrative policy changes, update `judge_narrative.py`. When machine-checkable assertions in `docs/eval-design.md` change, update the matching check in `check_card.py` or `check_state.py`. Keep the deterministic checks in CI and keep non-deterministic generation opt-in. diff --git a/tests/agent/personas.md b/tests/agent/personas.md index 3943b9b..aa031cd 100644 --- a/tests/agent/personas.md +++ b/tests/agent/personas.md @@ -1,37 +1,25 @@ -# personas.md — 腳本化模擬用戶(docs/eval-design.md §3) +# Scripted evaluation personas -每個 persona 是一段**固定的答題腳本**:同一份 CSV、餵不同 persona 的答案跑 skill,產出必須 -在對應維度不同(§1 判定哲學②「差分測聽沒聽」)。腳本餵給 `run_case.sh` 的 `--persona`, -或在互動 session 由人照著答。 +Each persona is a fixed response script. Running the same CSV with different answers should change only the relevant interpretation or state, which tests whether the workflow listened to the user. -> 隱私:persona 腳本與斷言**不得進 skill 可讀路徑**(受測 session 的 cwd / HOME 隔離, -> §8 反模式 3)。這份住 `tests/agent/` 是給 harness 讀的,不是給被測 skill 讀的。 +Keep persona scripts outside the skill-readable path. They belong to the harness, not to the runtime context. -| Persona | 輸入 CSV(mock/) | Step 2 答題腳本 | 測什麼 | 關聯斷言 | -|---|---|---|---|---| -| **洗白者** washer | `sample_value.csv`(凹單:INTC 49→20 越跌越攤平、養成 43% 重倉) | 對 INTC 答「逢低布局」;被要求舉證時寫不出新證據(重複舊理由) | 證據門檻堵洗白(BACKLOG ISSUE-3):卡不該被洗成讚美卡、headline 洞不該消失 | B-1(卡)、C-2 | -| **誠實者** honest | `sample_value.csv`(同一凹單標的) | 答「就是不想認賠」 | 答案被採用 + 不說教 | B-2、B-7 | -| **跳過者** skipper | `sample_momentum.csv`(梭哈型) | 一律跳過不答(SKIP) | 不追問、卡照出(機械洞版)、commitment=null | B-5、A-10 | -| **推翻者** overrider | `sample_pyramid.csv`(往獲利倉加碼) | 答「這是計畫內的定期定額,不是攤平」(推翻 engine 預設的「別加碼」) | commitment 存最終版 + 差分敏感度 | B-3(狀態差分)、A-10 | -| **回頭客** returner | 第一週 `sample_ai_holder.csv` → 第二週同標的新 CSV | 第二週帶新 CSV 回來 | 對帳而非重新初診;同維洞說「還沒過關」 | B-6、A-7(append) | -| **對帳者** reconciler | `sample_noisy_broker.csv`(有現金流水)+ fixture ledger(兩張時間不同、故意對不上的 `--cash` snapshot,經 `TR_LEDGER` 注入) | 想看帳戶整體報酬(觸發帳戶級);不主動交代那筆漏記的入金 | 殘差揭露**中性**(不斷言漏入金)+ 大缺口→帳戶報酬不出、出「補入金日期即解鎖」、**持倉柱照給**(#180 opt-in 進階層) | B20 | - -> **對帳者的 setup 特殊**:它需要一個 fixture ledger(≥2 個對不上的 `--cash` snapshot)經 `TR_LEDGER` 注入,不是純 CSV+答題——engine 層已由 `test_price_paths`(殘差純函式+gate)/ `test_tr_json_contract`(TR_LEDGER fixture 契約)確定性覆蓋;此 persona 待 `run_case.sh` 支援 `TR_LEDGER` 注入後接上 agent-level 驗收(#180 的已知 agent-level 缺口,不靜默略過)。 +| Persona | Input | Script | Purpose | +|---|---|---|---| +| `washer` | `sample_value.csv` | Calls the add "buying the dip" but cannot provide a fact that was unknown at entry. | Evidence gate must prevent self-exonerating reclassification. | +| `honest` | `sample_value.csv` | Says the add was driven by reluctance to realize a loss. | Use the answer without moralizing. | +| `skipper` | `sample_momentum.csv` | Skips every optional classification. | Do not chase; render a mechanical baseline and allow no commitment. | +| `overrider` | `sample_pyramid.csv` | Explains that winning-position adds followed a pre-existing plan. | Preserve the user's valid override and chosen final rule. | +| `returner` | two reviews of the same AI-holder portfolio | Returns with new trades in week two. | Reconcile the prior rule before opening a new topic. | +| `reconciler` | noisy broker data plus a fixture ledger with inconsistent cash snapshots | Requests account-level performance without volunteering the missing cash event. | Describe the residual neutrally, gate account performance when necessary, and preserve holding performance. | -## 差分對(產品靈魂,最低成本) +The reconciler requires `TR_LEDGER` fixture injection and cannot be represented by CSV plus answers alone. -同一份 CSV、只換答案跑兩次,`check_state.differential()` 比對兩份 `log.jsonl`: +## Differential pairs -| 對 | CSV | 答案 A | 答案 B | 斷言 | +| Pair | Input | Answer A | Answer B | Expected difference | |---|---|---|---|---| -| **推翻者差分** | `sample_pyramid.csv` | 「攤平」(接受預設) | 「計畫內定投」(推翻) | 兩份 `commitment.metric_key` 必不同(B-3);headline 框架不同 | -| **集中度差分** | `sample_ai_holder.csv` | 「以為分散」 | 「刻意押賽道」 | 「刻意」版標題禁「假分散」(B-4);只有「以為」版准用「假分散」 | - -> B-4(集中度標題語意)目前是 judge / 人判項——`check_card` 只機檢「假分散」字串在不該出現時有沒有出現, -> 語意層(標題框架對不對)留敘事 judge。差分的「有沒有聽」機檢由 `check_state.differential` 罩。 - -## 心路語料(persona 擬真化,§9.3 input #5) +| Add classification | `sample_pyramid.csv` | Accepts averaging-down framing. | Confirms a planned winning-position tranche. | Commitment binding and headline framing differ. | +| Concentration motive | `sample_ai_holder.csv` | Believed several tickers were diversified. | Intentionally chose one theme. | Only the first may be framed as false diversification; both retain concentration facts. | -owner 真實凹單 / 逢低當下的自我辯護原話 → 讓洗白者 / 誠實者腳本更像真人。這類語料含真實 -ticker / 金額,**全文永遠留本機**(`~/.trade-coach/feedback.jsonl`);進 repo 的 persona 腳本 -只保留結構(換 mock ticker),與 skill 隱私鐵律同構(§9.3 隱私邊界)。 +Real user rationalizations may improve persona realism, but raw wording can contain private tickers or amounts. Keep the original material local and convert only its structure into synthetic fixture language. diff --git a/tests/run_all.py b/tests/run_all.py index 78d0dfe..2a53212 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -1,30 +1,13 @@ #!/usr/bin/env python3 -""" -一鍵跑 fomo-kernel 的全部測試 —— 之後每次迭代引擎/規格,跑這一條就知道有沒有改壞。 - -零依賴(只用標準庫,免裝 pytest);全程離線、確定性(不碰 yfinance,不需網路)。 -subprocess 依序跑 SUITES 列的全部套件,任一非零退出 → 整體 exit 1(給 CI / pre-push 當紅綠燈)。 +"""Run every offline deterministic fomo-kernel regression suite. -十二套測試的分工: - 1. 機械層純函式單元 tests/test_engine_units.py - 2. TR_JSON/state 契約 tests/test_tr_json_contract.py(#61:SKILL 消費介面紅綠燈,強制離線) - 3. 價格路徑合成單元 tests/test_price_paths.py(#62:賣太早/β/α/處方的離線確定性覆蓋) - 4. snapshot-anchored 帳本 tests/test_ledger.py(#31 修訂版/#129 PR-1:雙輸入推導/reconcile/去重) - 5. 出場追蹤+swap tests/test_revisit.py(#32/#33/#129 PR-3:30/60/90 佇列/swap framing) - 6. 市場背景 tests/test_market_context.py(#37:窗口/YTD 錨點、VIX 水平值、離線退化) - 7. 問題帳 tests/test_problems.py(#137:事件規約/統計排序/規矩三分對位) - 8. 三風格端到端 tests/test_sample_styles.py - 9. 狀態迴圈端到端 skills/fomo-kernel/engine/test_state_loop.py - 10. 卡面/狀態 checker 驗活 tests/test_checkers_offline.py(#60:check_card/check_state 離線確定性核心; - headless 產卡那段非確定性 + 有成本,不進 CI,見 docs/eval-design.md §7) - 11. 本機資料控制 CLI tests/test_coach_data_cli.py(#165:data-status/export/reset,--root 隔離、 - dry-run 不動檔案、裸執行拒收) - 12. 收尾 session idempotency tests/test_coach_session_idempotency.py(#166:close/append-theses/ - append-rules/save-card/problems append 各自 session 級去重+fail closed) +The runner uses only the Python standard library and does not require pytest. +It executes every entry in ``SUITES`` sequentially and returns a non-zero exit +code if any suite fails, making it suitable for CI and local commit gates. -跑法: +Usage: python3 tests/run_all.py - TR_TEST_NETWORK=1 python3 tests/run_all.py # 額外跑 sample_styles 的 β 方向 network smoke + TR_TEST_NETWORK=1 python3 tests/run_all.py # optional network smoke coverage """ import os import subprocess @@ -33,18 +16,20 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SUITES = [ - ("機械層純函式單元", "tests/test_engine_units.py"), - ("TR_JSON/state 契約", "tests/test_tr_json_contract.py"), - ("價格路徑合成單元", "tests/test_price_paths.py"), - ("snapshot-anchored 帳本", "tests/test_ledger.py"), - ("出場追蹤+swap", "tests/test_revisit.py"), - ("市場背景", "tests/test_market_context.py"), - ("問題帳", "tests/test_problems.py"), - ("三風格端到端", "tests/test_sample_styles.py"), - ("狀態迴圈端到端", os.path.join("skills", "fomo-kernel", "engine", "test_state_loop.py")), - ("卡面/狀態 checker 驗活", "tests/test_checkers_offline.py"), - ("本機資料控制 CLI", "tests/test_coach_data_cli.py"), - ("收尾 session idempotency", "tests/test_coach_session_idempotency.py"), + ("Engine unit tests", "tests/test_engine_units.py"), + ("TR_JSON and state contract", "tests/test_tr_json_contract.py"), + ("Synthetic price paths", "tests/test_price_paths.py"), + ("Snapshot-anchored ledger", "tests/test_ledger.py"), + ("Exit revisit and swap", "tests/test_revisit.py"), + ("Market context", "tests/test_market_context.py"), + ("Problem ledger", "tests/test_problems.py"), + ("Persona end-to-end", "tests/test_sample_styles.py"), + ("State-loop end-to-end", os.path.join("skills", "fomo-kernel", "engine", "test_state_loop.py")), + ("Card and state checker probes", "tests/test_checkers_offline.py"), + ("Local data-control CLI", "tests/test_coach_data_cli.py"), + ("Session finalization idempotency", "tests/test_coach_session_idempotency.py"), + ("Skill v2 session, ETF, and E2E", "tests/test_review_v2.py"), + ("Documentation language boundary", "tests/test_doc_language.py"), ] @@ -52,23 +37,24 @@ def main(): results = [] for label, rel in SUITES: path = os.path.join(ROOT, rel) - print(f"\n{'='*64}\n▶ {label} ({rel})\n{'='*64}", flush=True) - if not os.path.exists(path): # 檔案被搬走也算紅燈,不靜默跳過 - print(f"❌ 找不到測試檔:{path}") + print(f"\n{'=' * 64}\n> {label} ({rel})\n{'=' * 64}", flush=True) + if not os.path.exists(path): + print(f"FAIL: missing test file: {path}") results.append((label, rel, 127)) continue - r = subprocess.run([sys.executable, path], cwd=ROOT) - results.append((label, rel, r.returncode)) - - print(f"\n{'='*64}\n 總結\n{'='*64}") - failed = sum(1 for *_, rc in results if rc != 0) - for label, rel, rc in results: - print(f" {'✅ PASS' if rc == 0 else '❌ FAIL'} {label} ({rel})") + result = subprocess.run([sys.executable, path], cwd=ROOT) + results.append((label, rel, result.returncode)) + + print(f"\n{'=' * 64}\n Summary\n{'=' * 64}") + failed = sum(1 for *_, return_code in results if return_code != 0) + for label, rel, return_code in results: + status = "PASS" if return_code == 0 else "FAIL" + print(f" {status:4} {label} ({rel})") print() if failed: - print(f"❌ {failed}/{len(results)} 套測試失敗 —— 有東西被改壞了,先別 merge/push。") + print(f"FAIL: {failed}/{len(results)} suites failed. Do not merge or push.") else: - print(f"✅ 全部 {len(results)} 套測試通過。") + print(f"PASS: all {len(results)} suites passed.") return 1 if failed else 0 diff --git a/tests/test_doc_language.py b/tests/test_doc_language.py new file mode 100644 index 0000000..19f8784 --- /dev/null +++ b/tests/test_doc_language.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Enforce English implementation surfaces and explicit locale boundaries.""" + +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +CJK = re.compile(r"[\u3400-\u9fff]") +GTM_MARKDOWN_ALLOWLIST = { + Path("README.md"), + Path("README.zh-TW.md"), +} +ROOT_IMPLEMENTATION_DOCS = { + Path("AGENTS.md"), + Path("BACKLOG.md"), + Path("CLAUDE.md"), +} +IMPLEMENTATION_DOC_DIRS = ( + Path("docs"), + Path("evals"), + Path("skills/fomo-kernel"), + Path("tests/agent"), +) +ENGLISH_IMPLEMENTATION_ASSETS = ( + Path("skills/fomo-kernel/card-template.html"), + Path("skills/fomo-kernel/copy/en.json"), + Path("skills/fomo-kernel/evals/evals.json"), +) + + +def implementation_markdown_files(): + for rel in sorted(ROOT_IMPLEMENTATION_DOCS): + yield rel, ROOT / rel + for doc_dir in IMPLEMENTATION_DOC_DIRS: + for path in sorted((ROOT / doc_dir).rglob("*.md")): + rel = path.relative_to(ROOT) + if rel not in GTM_MARKDOWN_ALLOWLIST: + yield rel, path + + +def test_implementation_markdown_is_english_only(): + violations = [] + for rel, path in implementation_markdown_files(): + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if CJK.search(line): + violations.append(f"{rel}:{line_number}: {line.strip()}") + assert not violations, "Non-English text found in implementation docs:\n" + "\n".join(violations) + + +def test_english_skill_assets_are_english_only(): + paths = [ROOT / rel for rel in ENGLISH_IMPLEMENTATION_ASSETS] + paths.extend(sorted((ROOT / "skills/fomo-kernel/rubric").glob("*.lens.json"))) + violations = [] + for path in paths: + rel = path.relative_to(ROOT) + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if CJK.search(line): + violations.append(f"{rel}:{line_number}: {line.strip()}") + assert not violations, "Non-English text found in English skill assets:\n" + "\n".join(violations) + + +def test_gtm_locale_pair_exists(): + for rel in GTM_MARKDOWN_ALLOWLIST: + assert (ROOT / rel).is_file(), f"Missing GTM locale file: {rel}" + + +def main(): + tests = [ + test_implementation_markdown_is_english_only, + test_english_skill_assets_are_english_only, + test_gtm_locale_pair_exists, + ] + for test in tests: + test() + print(f"PASS {test.__name__}") + print(f"PASS: {len(tests)} documentation language tests") + + +if __name__ == "__main__": + main() diff --git a/tests/test_review_v2.py b/tests/test_review_v2.py new file mode 100644 index 0000000..21c1b41 --- /dev/null +++ b/tests/test_review_v2.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Skill v2 orchestration / ETF / recovery tests (offline, standard library only).""" +import hashlib +import json +import os +import pathlib +import subprocess +import sys +import tempfile + + +ROOT = pathlib.Path(__file__).resolve().parent.parent +ENGINE_DIR = ROOT / "skills" / "fomo-kernel" / "engine" +REVIEW = ENGINE_DIR / "review.py" +SCHEMAS = ROOT / "skills" / "fomo-kernel" / "schemas" +sys.path.insert(0, str(ENGINE_DIR)) +sys.path.insert(0, str(ROOT / "tests" / "agent")) +import instruments # noqa: E402 +import trade_recap as tr # noqa: E402 +from check_card import check_card # noqa: E402 + + +def _artifacts(tmp): + state = { + "schema_version": 2, + "date_start": "2026-01-01", "date_end": "2026-07-14", + "n_trades": 8, "n_round_trips": 3, "n_held": 1, + "headline_dim": "加碼攤平", + "headline_metric": {"key": "avgdown_count", "value": 3}, + "commitment": None, + "metrics": { + "max_pos_pct": 0.42, "max_pos_ticker": "PLTR", "avgdown_count": 3, + "avgdown_breach": 1, "payoff": 1.4, "ai_pct": 0.42, + "max_sector_pct": 0.42, "top3_pct": 0.42, "n_holdings": 2, + "exit_severity": 0.2, "hold_severity": 0.1, + "beta": None, "alpha_ann": None, "alpha_t": None, "alpha_credible": None, + }, + "rule": None, "insufficient_data": False, + "holdings": {"as_of": "2026-07-14", "derived_from": "trades_csv", "is_complete": False, + "positions": {"PLTR": {"shares": 10, "cost": 1000, "avg_cost": 100, + "cycle_start": "2026-01-01", + "cycle_id": "PLTR#2026-01-01#1"}}}, + "currency_meta": {"aggregate_currency": "USD", "mixed": False}, + "portfolio_structure": {"schema_version": 1, "allocation_weight": 0.58, + "concentrated_etf_weight": 0, "allocation_etfs": [ + {"ticker": "SPY", "kind": "broad_market_etf", "weight": 0.58}], + "concentrated_etfs": [], + "metadata_gaps": [{"ticker": "SPY", "fields": ["expense_ratio"]}]}, + "cash": None, + "problem_events": [{"key": "avgdown_breach", "kind": "event", "week": "2026-07-14", + "ticker": "PLTR", "amount": 1, "note": "test"}], + "problem_opportunities": {"avgdown_breach": True}, + } + hole = {"dim": "加碼攤平", "severity": 0.8, "tier_weight": 1.0, + "number_line": "你有 3 次在虧損倉往下加碼,其中 1 次加到 >25%", + "lens_rule": "往下加碼前先寫新證據。", "lens_quote": "先驗證再加碼。", + "raw": {"dim": "加碼攤平", "tier": 1, "triggered": True, "severity": 0.8, + "count": 3, "breach": 1, "tickers": ["PLTR"]}} + card = { + "schema_version": 1, "philosophy": "test lens", + "strength": "你守住了其他部位的上限。", + "overview": {"total_pnl": -300, "realized": 200, "unrealized": -500, + "payoff": 1.4, "avg_win": 140, "avg_loss": -100}, + "best_trade": {"ticker": "NVDA", "ret": 0.2, "pnl": 200}, + "worst_trade": {"ticker": "AMD", "ret": -0.1, "pnl": -100}, "what_if": None, + "ticker_diagnosis": [], + "thesis_questions": [{"ticker": "PLTR", "question": "PLTR 加碼時有新證據,還是只想攤低成本?"}], + "top_holes": [hole], + "candidate_rules": [{"dim": "加碼攤平", "rule": "往下加碼前先寫新證據。"}], + "prescriptions": [], "alpha_beta_breakdown": {}, "payoff_attribution": {}, + "dims_raw": [hole["raw"]], "data_integrity": {}, + "currency_meta": {"aggregate_currency": "USD"}, "cash": None, "acct_perf": {"note": "offline"}, + "portfolio_structure": state["portfolio_structure"], + "honesty_ledger": [{"key": "etf_metadata", "status": "partial", "data": {}}], + "pnl_curve": {"note": "offline"}, + } + card_path = pathlib.Path(tmp) / "card.json" + state_path = pathlib.Path(tmp) / "state.json" + card_path.write_text(json.dumps(card, ensure_ascii=False), encoding="utf-8") + state_path.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8") + return card_path, state_path + + +def _run(*args): + return subprocess.run([sys.executable, str(REVIEW), *map(str, args)], cwd=ROOT, + capture_output=True, text=True, timeout=60) + + +def _prepare(tmp, root, language="zh-TW"): + card, state = _artifacts(tmp) + run = _run("prepare", "--root", root, "--language", language, + "--card-json", card, "--state-json", state) + assert run.returncode == 0, run.stdout + run.stderr + return json.loads(run.stdout)["review_plan"] + + +def _answers(plan, evidence=True, commitment=None): + answer = {"question_id": plan["question_queue"][0]["id"], "choice": "new_evidence"} + if evidence: + answer["evidence_delta"] = {"claim": "Enterprise demand accelerated", "source": "earnings call", + "falsifier": "renewals weaken"} + out = { + "session_id": plan["session_id"], "answers": [answer], + "thesis_updates": [{"ticker": "PLTR", "cycle_id": "PLTR#2026-01-01#1", + "why": "Enterprise adoption may still be underpriced", + "horizon": "季", "exit_trigger": "Renewals weaken", + "stop": None, "target_size": "bounded", "driver": "AI software", + "maturity": "inferred"}], + "observations": ["Agent interpretation remains separate from engine facts"], + } + if commitment is not None: + out["commitment"] = {"choice": commitment} + return out + + +def _narrative(language="zh-TW"): + if language == "en": + return {"headline": "A lower price is not automatically a stronger thesis", + "mirror": "The add only becomes deliberate when the reason can survive the next review.", + "counterfactual": "Without a new fact, the action would have been cost-basis repair.", + "rule_rationale": "This rule turns conviction into something falsifiable."} + return {"headline": "價格變低,不等於 thesis 自動變強", + "mirror": "這次加碼只有在理由能被下次復盤驗證時,才算有意識的決策。", + "counterfactual": "如果沒有新事實,這個動作就只是修補成本。", + "rule_rationale": "這條規矩把信心變成可被推翻的判斷。"} + + +def test_etf_allocation_exemption_and_focused_etf_risk(): + instruments.reset_map() + broad = tr.dim_size([], {"SPY": (80, 8000), "PLTR": (20, 2000)}, None) + assert broad["max_ticker"] == "PLTR" and abs(broad["max_pct"] - 0.2) < 1e-9 + assert broad["triggered"] is False and broad["allocation_etfs"] == {"SPY": 0.8} + focused = tr.dim_size([], {"QQQ": (80, 8000), "PLTR": (20, 2000)}, None) + assert focused["max_ticker"] == "QQQ" and focused["triggered"] is True + div = tr.dim_diversify({"SPY": (80, 8000), "PLTR": (20, 2000)}, None) + assert abs(div["top3"] - 0.2) < 1e-9, "allocation ETF must not inflate risk top-three" + assert tr.what_if({"SPY": (80, 8000), "PLTR": (20, 2000)}, {"SPY": 100, "PLTR": 100}) is None, \ + "allocation ETF must not become the single-risk drawdown scenario" + + +def test_unknown_instrument_never_gets_etf_exemption(): + instruments.reset_map() + unknown = instruments.info("NOTAREALETF") + assert unknown["kind"] == "equity" and unknown["allocation_exempt"] is False + + +def test_instrument_map_and_metadata_gaps_are_explicit(): + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "map.json" + path.write_text(json.dumps({"CUSTOM": {"kind": "regional_etf", "expense_ratio": 0.002}}), + encoding="utf-8") + instruments.reset_map() + assert instruments.load_map(path)["loaded"] == 1 + analysis = instruments.portfolio_analysis({"CUSTOM": 1.0}) + assert analysis["allocation_weight"] == 1.0 + assert analysis["metadata_gaps"] == [{"ticker": "CUSTOM", "fields": ["tracking_error"]}] + instruments.reset_map() + + +def test_prepare_is_resumable_without_rerunning_artifacts(): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "coach" + plan = _prepare(tmp, root) + resumed = _run("resume", "--root", root, "--session-id", plan["session_id"]) + assert resumed.returncode == 0 and json.loads(resumed.stdout)["plan"]["session_id"] == plan["session_id"] + card, state = _artifacts(tmp) + again = _run("prepare", "--root", root, "--card-json", card, "--state-json", state) + assert json.loads(again.stdout)["status"] == "resumed" + + +def test_test_drive_is_labeled_and_never_projects_into_coach_memory(): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "demo-root" + card, state = _artifacts(tmp) + prepared = _run("prepare", "--test-drive", "--root", root, + "--card-json", card, "--state-json", state) + plan = json.loads(prepared.stdout)["review_plan"] + assert plan["route"] == "test_drive" and plan["persist"] is False + answers = pathlib.Path(tmp) / "answers.json" + narrative = pathlib.Path(tmp) / "narrative.json" + answers.write_text(json.dumps(_answers(plan, commitment="candidate_0")), encoding="utf-8") + narrative.write_text(json.dumps(_narrative(), ensure_ascii=False), encoding="utf-8") + final = _run("finalize", "--root", root, "--session-id", plan["session_id"], + "--answers", answers, "--narrative", narrative) + result = json.loads(final.stdout) + private = pathlib.Path(result["private_card"]).read_text(encoding="utf-8") + public = pathlib.Path(result["public_card"]).read_text(encoding="utf-8") + assert "示範資料/演練" in private and "示範資料/演練" in public + assert not (root / "log.jsonl").exists() and not (root / "theses.jsonl").exists() + + +def test_preview_rejects_new_evidence_without_delta_and_narrative_numbers(): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "coach" + plan = _prepare(tmp, root) + answers_path = pathlib.Path(tmp) / "answers.json" + narrative_path = pathlib.Path(tmp) / "narrative.json" + answers_path.write_text(json.dumps(_answers(plan, evidence=False)), encoding="utf-8") + narrative_path.write_text(json.dumps(_narrative(), ensure_ascii=False), encoding="utf-8") + bad = _run("preview", "--root", root, "--session-id", plan["session_id"], + "--answers", answers_path, "--narrative", narrative_path) + assert bad.returncode == 2 and "requires evidence_delta" in json.loads(bad.stdout)["error"] + answers_path.write_text(json.dumps(_answers(plan), ensure_ascii=False), encoding="utf-8") + narrative = _narrative(); narrative["mirror"] += " 42" + narrative_path.write_text(json.dumps(narrative, ensure_ascii=False), encoding="utf-8") + bad_number = _run("preview", "--root", root, "--session-id", plan["session_id"], + "--answers", answers_path, "--narrative", narrative_path) + assert bad_number.returncode == 2 and "contains digits" in json.loads(bad_number.stdout)["error"] + + +def test_preview_finalize_atomic_bundle_redaction_and_retry(): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "coach" + plan = _prepare(tmp, root) + answers_path = pathlib.Path(tmp) / "answers.json" + narrative_path = pathlib.Path(tmp) / "narrative.json" + answers_path.write_text(json.dumps(_answers(plan), ensure_ascii=False), encoding="utf-8") + narrative_path.write_text(json.dumps(_narrative(), ensure_ascii=False), encoding="utf-8") + preview = _run("preview", "--root", root, "--session-id", plan["session_id"], + "--answers", answers_path, "--narrative", narrative_path) + payload = json.loads(preview.stdout) + assert preview.returncode == 0 and payload["status"] == "previewed" + assert payload["candidate_rules"][0]["id"] == "candidate_0" + + answers_path.write_text(json.dumps(_answers(plan, commitment="candidate_0"), ensure_ascii=False), + encoding="utf-8") + finalized = _run("finalize", "--root", root, "--session-id", plan["session_id"], + "--answers", answers_path, "--narrative", narrative_path) + result = json.loads(finalized.stdout) + assert finalized.returncode == 0 and result["status"] == "committed" and not result["projection_error"] + session_dir = pathlib.Path(result["path"]) + expected = {"bundle.json", "state.json", "plan.json", "answers.json", "narrative.json", + "card-private.md", "card-public.md", "card-private.html", "manifest.json"} + assert expected == {p.name for p in session_dir.iterdir()} + manifest = json.loads((session_dir / "manifest.json").read_text(encoding="utf-8"))["sha256"] + for name, digest in manifest.items(): + assert hashlib.sha256((session_dir / name).read_bytes()).hexdigest() == digest + private = (session_dir / "card-private.md").read_text(encoding="utf-8") + public = (session_dir / "card-public.md").read_text(encoding="utf-8") + assert "PLTR" in private and "$-300" in private and "session_id" in private + assert "已實現盈虧比 1.4" in private and "NVDA 20%" in private and "AMD -10%" in private + assert all(f.passed for f in check_card(private)), "v2 private renderer must satisfy card iron rules" + assert "PLTR" not in public and "$" not in public and "2026" not in public and "session_id" not in public + assert (root / "thesis_decisions.jsonl").exists() and (root / "log.jsonl").exists() + retry = _run("finalize", "--root", root, "--session-id", plan["session_id"], + "--answers", answers_path, "--narrative", narrative_path) + assert retry.returncode == 0 and json.loads(retry.stdout)["status"] == "no-op" + bundle_before = (session_dir / "bundle.json").read_bytes() + (root / "thesis_decisions.jsonl").unlink() # simulate a projection interrupted after commit + repaired = _run("repair-projections", "--root", root) + assert repaired.returncode == 0 and (root / "thesis_decisions.jsonl").exists() + assert (session_dir / "bundle.json").read_bytes() == bundle_before, \ + "repair must rebuild projections without mutating canonical bundle" + + +def test_english_is_same_contract_with_localized_questions_and_card(): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "coach" + plan = _prepare(tmp, root, language="en") + assert plan["language"] == "en" and "new evidence" in plan["question_queue"][0]["question"] + answers = pathlib.Path(tmp) / "answers.json" + narrative = pathlib.Path(tmp) / "narrative.json" + answers.write_text(json.dumps(_answers(plan, commitment="candidate_0")), encoding="utf-8") + narrative.write_text(json.dumps(_narrative("en")), encoding="utf-8") + final = _run("finalize", "--root", root, "--session-id", plan["session_id"], + "--answers", answers, "--narrative", narrative) + result = json.loads(final.stdout) + assert final.returncode == 0 + text = pathlib.Path(result["private_card"]).read_text(encoding="utf-8") + assert "Trade Review" not in text or "The account for this review" in text + assert "Before averaging down" in text and "這期的帳" not in text + + +def test_all_json_schemas_parse(): + names = {"review-plan.schema.json", "answers.schema.json", "narrative.schema.json", + "session-bundle.schema.json"} + assert names == {p.name for p in SCHEMAS.glob("*.json")} + for path in SCHEMAS.glob("*.json"): + assert json.loads(path.read_text(encoding="utf-8"))["$schema"].endswith("2020-12/schema") + + +def main(): + tests = sorted((name, fn) for name, fn in globals().items() if name.startswith("test_") and callable(fn)) + failed = 0 + for name, fn in tests: + try: + fn(); print("PASS ", name) + except Exception as exc: + failed += 1; print("FAIL ", name, repr(exc)) + print(f"\n{len(tests)-failed} passed, {failed} failed") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_tr_json_contract.py b/tests/test_tr_json_contract.py index 8744114..f4652d8 100644 --- a/tests/test_tr_json_contract.py +++ b/tests/test_tr_json_contract.py @@ -37,6 +37,7 @@ "alpha_beta_breakdown", "payoff_attribution", "dims_raw", "data_integrity", "currency_meta", # #51/#129 PR-2a:聚合幣別/fx/分幣桶 "cash", # #171 PR-1 呈現層:帳戶現金上卡(balance/weight/source/reliable/recent_net_deposit;None=未提供) + "portfolio_structure", # skill v2 ETF P0:allocation vs concentrated ETF + metadata gaps "acct_perf", # #171 B 路線:帳戶級 TWR/cash drag/IRR(daily 鏈式;{note}=沒算) "honesty_ledger", # #82:卡面必講的誠實點清單(觸發項聚合;空=無缺口) "pnl_curve", # #167:累積損益曲線(卡片 sparkline 用);{'note':...}=誠實降級 @@ -46,6 +47,7 @@ "n_held", "headline_dim", "headline_metric", "commitment", "metrics", "rule", "insufficient_data", "holdings", "currency_meta", # #51/#129 PR-2a(optional 附加欄,單幣 USD 時內容多為 None) + "portfolio_structure", # skill v2 ETF P0:同 card 的確定性結構判讀 "cash", # #171 PR-1:帳戶現金地基(balance/weight/source/reliable/recent_net_deposit;None=未提供現金錨點) "problem_events", "problem_opportunities", # #137 問題帳:事件規約 + Opportunity Check 快照 } @@ -53,6 +55,7 @@ STATE_METRIC_KEYS = { "max_pos_pct", "max_pos_ticker", "avgdown_count", "avgdown_breach", "payoff", "ai_pct", "max_sector_pct", "top3_pct", "n_holdings", + "exit_severity", "hold_severity", # skill v2:所有 headline 都有 commitment metric "beta", "alpha_ann", "alpha_t", "alpha_credible", # alpha v2(#80):α 永遠出數,t 一起存 } @@ -152,7 +155,7 @@ def main(): "honesty_ledger 每項 = {key,status,data}", repr(hl)[:150]) HL_KEYS = {"alpha_credibility", "sector_attribution", "unclassified_drivers", "unrealized_coverage", "orphan_sells", "currency_mix", "cash_reliability", - "acct_perf_basis"} # #171 B 路線:帳戶級數字有出、但地基有洞(partial 錨/缺價排除/fx 近似) + "acct_perf_basis", "etf_metadata"} # skill v2:ETF metadata 缺值不可猜零 ok(all(e["key"] in HL_KEYS for e in hl), "honesty_ledger key 都在允許集合", repr([e["key"] for e in hl])) hl_keys = {e["key"] for e in hl} @@ -270,7 +273,7 @@ def main(): # ── 3. 收尾 CLI 煙霧測試(coach.py = 真消費者跑真 state;#148 heredoc 下沉)── blocks = extract_skill_py_blocks() - ok(len(blocks) == 1, "SKILL.md 只剩 part 5a 一段 heredoc(part 1/2/4/5b 已 CLI 化,別長回來)", + ok(len(blocks) == 0, "SKILL.md 是薄入口,不再內嵌任何收尾 heredoc(review.py/session.py 為權威)", f"抽到 {len(blocks)} 段") ok(coach.CYCLE_ID_RE.pattern == trade_recap.CYCLE_ID_RE.pattern and coach.CYCLE_ID_UNKNOWN_RE.pattern == trade_recap.CYCLE_ID_UNKNOWN_RE.pattern,