# 📘 HANDOFF — ระบบ Fund Manager (v5 / z12)

> เอกสารส่งมอบงานสำหรับอนาคต (คนหรือ AI เข้ามาแก้ต่อก็อ่านไฟล์นี้ไฟล์เดียวจบ)
> อัปเดตล่าสุด: 2026-09-28 • อ่านออนไลน์: https://us.gurateto.com/docs
> ⚠️ ตัวเลขและกติกาทั้งหมดในไฟล์นี้ **ผ่านการ verify จากข้อมูลดิบแล้ว** — ถ้าแก้ระบบแล้วตัวเลขเปลี่ยน ต้องแก้ไฟล์นี้ + README + หน้าเว็บ ตามไปด้วย (บทเรียนที่ 59 ใน skill)

---

## 1. ระบบนี้คืออะไร (30 วินาที)

เว็บช่วยจัดพอร์ตกองทุนรวมไทย: ดึง NAV รายวัน ~900 กองจาก settrade → ให้คะแนนทุกกองด้วยสูตร **z12** → เลือก 6 กอง → บอกน้ำหนักเป็น % → ผู้ใช้ซื้อตาม **ปีละ 2 ครั้ง (ปลาย ก.พ. / ปลาย ส.ค.)** เท่านั้น ที่เหลือคือกดดูได้แต่พอร์ตไม่เปลี่ยน

| URL | เครื่องที่เสิร์ฟ |
|---|---|
| https://us.gurateto.com/ | us.gurateto.com (192.3.222.126, AWS) → Flask :6969 |
| https://fund.worstsoul.com/ | เครื่อง local → cloudflared → nginx :18080 → Flask :6969 |
| https://us.gurateto.com/docs | เอกสารไฟล์นี้ |

## 2. กติกาปัจจุบัน (z12 = score_v2.json v2.7) — ห้ามเดา ให้เปิดไฟล์ยืนยัน

| รายการ | ค่า | ไฟล์/ตำแหน่ง |
|---|---|---|
| สูตรคะแนน | `0×mom6 + 1.0×mom12 + 0.2×sharpe − 0.15×\|MDD12\|` (มอมเมนตัม 12 เดือนล้วน) | `config/score_v2.json → score` |
| จำนวนกอง / เพดานหมวด | 6 กอง / หมวดละ ≤ 2 | `rotation.positions, top_n_cat` |
| exit_rank | 72 (ถือกองเดิมถ้ายังติดอันดับ ≤72) | `rotation.exit_rank` |
| น้ำหนัก | `mom` = ตามความแรง **6 เดือน** (ต่างจากคะแนนที่ใช้ 12 เดือน — เป็น design) | `rotation.weight` |
| เงินสด | 0% (ลงเต็ม จำนวน) ไม่มี K-CASH ในพอร์ต | `rotation.cash_floor` |
| รอบ rebalance | ก.พ. + ส.ค. เท่านั้น | `rotation.rebalance_months` |
| Regime guard | SET50 (SCBSET50) เดือนเดียว ≤ −7% → **ระงับการอัปเดตพอร์ต (ไม่มีการขาย ไม่มีเงินสด)** แล้ว re-pick ใหม่ตอนฟื้น ≥ −3.5% | `regime.*` + `backtest_engine_v4.py` |
| ตัดกองที่ซื้อไม่ออก | RMF/SSF/ThaiESG/LTF (`forbid_tax_keywords`) + กองสถาบัน `-UI`/“ห้ามขาย” (`retail_only:1`) | `eligibility` |
| Universe | `config/universe_v5.json` (928 กอง) — fetch script auto-pick ตัวเลขสูงสุด | `fetch_fund_history_v4.py` |

## 3. ตัวเลขที่ **ถูกต้อง** (อ้างได้ในเอกสาร/UI)

พิสูจน์ซ้ำได้ด้วย: `scripts/verify_z12_numbers_v1.py`, `verify_z12_numbers_v2.py`, `daily_mdd_check_v1.py`, `verify_faq_monthly_vs_semi.py` (log: `results_v14_*.log`)

| ตัวเลข | ค่า | วิธีวัด |
|---|---|---|
| z12 CAGR gross | TRAIN 13.30% / OOS 29.35% / **FULL 15.39%** | engine v4, cost 0.1%, 2016-07..2026-07 |
| z12 net หลังค่าธรรมเนียมซื้อ 1.5% | **~14.16%/ปี (≈14.2%)** | buys/yr 0.718 (26 ครั้ง/10 ปี) |
| z12 MDD | **−17.53% รายเดือน** / −23.35% รายวัน (2024-07-16→2025-04-08) / วันเดียวแย่สุด −8.3% (2026-03-04) | รายวัน: `daily_mdd_check_v1.py` |
| SET50 ซื้อ-ถือ (2016-08..2026-07) | **+4.03%/ปี, MDD −33.3% รายเดือน / −39.9% รายวัน** | cumprod บน factor (ห้าม pct_change — ผิด!) |
| รายเดือน vs ครึ่งปี | ครึ่งปีชนะทุกช่วง: FULL 15.39% vs 6.76% (121 ครั้ง, net −12.46%) | `results_v14_faq_check.log` |

**ตัวเลขที่ผิด/ตายแล้ว — ห้ามเอามาอ้างเด็ดขาด** (สาเหตุ: บั๊ก `1+factor`, benchmark ใช้ `s/s[0]`, panel เดือนค้าง):
CAGR 13.97%, 12.69%, 10.27%, 12.45%/18.32% (ยุค v2.4), OOS 18.78%, SET50 −0.1%/−25.7% หรือ −0.56% — ทุกไฟล์ `results_v1, v3(legacy sweeps), v6, v7, v9, v10` เป็นหลักฐานการวิจัยที่ ** superseded** แล้ว เก็บไว้แค่ดูประวัติ

## 4. ไฟล์ที่ “ระบบจริง” ใช้ (ห้ามย้าย/ห้ามแก้โดยไม่ restart)

```
scripts/webapp_v5.py           ← เว็บ + HTML + JS (แก้ UI = แก้ไฟล์นี้แล้ว restart)
scripts/fund_score_v3.py       ← สูตรคะแนนหนึ่งเดียวของโลก (21 unit tests ในตัว)
scripts/fetch_fund_history_v4.py ← ดึง NAV (auto-pick universe_v<N>.json)
scripts/build_monthly_nav_v1.py  ← สร้าง panel รายเดือน (drop เดือนที่ยังไม่จบ!)
scripts/backtest_engine_v4.py  ← engine จริง (web ใช้กติกาเดียวกันผ่าน CFG_S)
config/score_v2.json           ← z12 (แก้ = restart service)
config/universe_v5.json        ← universe ปัจจุบัน
scripts/page_v6_body.html      ← ต้นฉบับ HTML ของหน้าเว็บ (อย่าแก้ใน .py ตรง ๆ)
scripts/splice_page_v6.py      ← แปะ body ลง webapp_v5.py (ตรวจ id ก่อนเสมอ)
scripts/test_webapp_v5_regime.py (17) + test_ui_node.js (12) + test_ui_live_node.js (14) + render_snapshot.js
scripts/verify_*_v1.py, v2.py, daily_mdd_check_v1.py, verify_faq_monthly_vs_semi.py ← หลักฐานตัวเลข
docs/HANDOFF.md                ← ไฟล์นี้ (เสิร์ฟที่ /docs)
```

## 5. วิธีแก้ระบบ (recipes)

**แก้ UI (ข้อความไทย):** ห้ามพิมพ์ไทยทับใน `webapp_v5.py` ตรง ๆ — ให้แก้ `scripts/page_v6_body.html` แล้วรัน:
```
python3 scripts/splice_page_v6.py --check && python3 scripts/splice_page_v6.py
```
(splice Assert ทุก id ที่ JS ใช้; จบบล็อก `<!doctype html>` ถึง `<script>` เท่านั้น JS ไม่ถูกแตะ)

**แก้คะแนน/กติกา:** `config/score_v2.json` เท่านั้น (webapp/engine อ่าน CFG_S ณ import) → **restart ทั้ง 2 เครื่อง** → รัน verify suite → อัปเดตตัวเลขใน page_v6_body.html + README + ไฟล์นี้

**ทดสอบก่อน deploy เสมอ:**
```
python3 scripts/fund_score_v3.py            # 21 unit
python3 scripts/test_webapp_v5_regime.py    # 17
node scripts/test_ui_live_node.js http://127.0.0.1:6969   # 14 (ต้อง 14/14)
python3 -c "s=open('scripts/webapp_v5.py',encoding='utf-8').read();a=s.index('<script>')+8;b=s.index('</script>');open('/tmp/s.js','w').write(s[a:b])" && node --check /tmp/s.js
```

**Deploy:** local = `sudo systemctl restart fund-webui.service` (port 6969, ห้ามใช้ unit `--user` เดิมที่ disable ไว้แล้ว) • remote = `scp` ไฟล์ + `ssh root@192.3.222.126 'systemctl restart fund-webui-v4'` (key `~/.ssh/gurateto_key`) • ยืนยัน md5 ตรงกัน + curl public URL ทั้งสอง

## 6. กับดักที่เคยเจ็บมาแล้ว (สรุปจาก 60+ lessons)

1. **panel เก็บ growth FACTOR ไม่ใช่ return** — `factor−1` ครั้งเดียวเท่านั้น; `s/s[0]` ห้ามใช้, ต้อง `cumprod` (บั๊กนี้ทำให้ SET50 อ่านผิด −0.1% ทั้งที่เป็น +4.03%)
2. **อย่าคํานวณ/เลือกบนเดือนที่ยังไม่จบ** — build ตัดเดือนปัจจุบันเสมอ ไม่งั้น OOS หลอก
3. **แก้ config แล้วต้อง restart** — webapp อ่าน CFG_S ครั้งเดียว ณ import
4. **unit ซ้ำชนพอร์ต** — local ใช้ system unit `fund-webui.service` ตัวเดียว (user unit ถูก disable+ปิดถาวรแล้ว); remote ใช้ `fund-webui-v4` (ชื่อค้างยุค v3 — ExecStart คือความจริง, ไม่ใช่ชื่อ unit)
5. **deploy แล้วยังเห็นข้อความเก่า** — เช็ค md5 ไฟล์ + ExecStart ของ unit ก่อน, แล้วค่อยโทษ cache; verify จาก **public URL** ไม่ใช่เครื่องตัวเอง
6. **JS `\n` ใน Python string** — ต้องเขียน `\\n` ไม่งั้นทั้งเพจตายเงียบ (HTTP 200 แต่ไม่ render) — ตรวจด้วย `node --check` บน JS ที่ *เสิร์ฟจริง*
7. **เลขทศนิยม/วันไทย** ให้ปัด+แปลงใน JS แล้วเสมอ (`num()`, `thaiMonth()`)
8. **MDD รายเดือน ต่ำกว่าจริง 4–6pp** — อ้างความเสี่ยงต้องบอกทั้งสอง basis
9. **exit_rank keep-loop ต้อง cap ด้วย positions** ไม่งั้นพอร์ตเกิน 6 กอง
10. **subprocess ใช้ `sys.executable`** ไม่ใช่ `python3` เพียว ๆ (remote pandas มีแต่ใน venv)
11. **rsync deploy ห้าม `--delete`** (เคยลบ venv ทิ้ง); `pkill -f` เคยฆ่า service ตัวจริง — kill ด้วย PID จาก `ss -tlnp`
12. **แก้เลข UI ทุกครั้งต้อง compute จากข้อมูลดิบก่อนเขียน** แล้วรัน suite — อย่าก็อปจาก comment/ไฟล์เก่า

## 7. ขอบเขตที่ “พิสูจน์แล้วว่าไม่ได้ผล” (อย่าเสียเวลาทำซ้ำ)

- หนีเงินสดจริง (down_frac=1.0): TRAIN 10.27% แย่กว่าไม่หนี • sensor อื่น (foreign/thai breadth, port-DD): v7+v9 sweep แพ้ set50/thr7/frac0 หมด • stop รายกอง, correlation-cap, vol-target, invvol/equal weights, score-smoothing: แพ้หรือไม่คงทน • fee-aware scoring: ปิดแล้ว 2026-09-26 (ข้อมูลครบ 880 กองแล้วแพ้ z12 ใน sub-periods 1/5) • gold sleeve: ตายตอน production parity • per-category regime guard: v5 rejected ทุก threshold • เปลี่ยนเฟส rebalance: ก.พ./ส.ค. ดีสุดใน sample แต่ **นี่คือจุดอ่อนที่สุด — edge ของเฟสนี้ ~5.5pp คือ phase luck, ต้อง re-validate หลังรอบ ก.พ. 2027** (ถ้าพอร์ตจริงตามหลัง 2 รอบติด สงเกตเฟสก่อนแก้พารามิเตอร์อื่น) • pyramiding k=2.0: ผ่าน parity แต่ TRAIN sh แพ้ + sub-periods 2/5 → PARKED (user ไม่เอาเพิ่ม rule ถ้า gain ขอบ)

## 8. ตารางเวลาที่ต้องทำ (calendar)

| เมื่อไหร่ | ทำอะไร |
|---|---|
| ปลาย ก.พ. 2027 | กด Fetch → ทำตามพอร์ตใหม่ → **re-validate เฟส + ตัวเลขทุกตัว** (lesson 51 ใน skill) |
| ปลาย ส.ค. 2027 | รอบ rebalance ถัดไป |
| ทุกครั้งที่แก้ config/UI | รัน suite 52 ข้อ + verify scripts + deploy 2 เครื่อง + แก้ไฟล์นี้ |

## 9. ไฟล์ถูกย้ายไป archive แล้ว (2026-09-28) — ป้องกันหยิบไปคำนวณผิด

`~/workspace/fund-legacy-archive/` (local เท่านั้น, remote ถูกล้างทิ้งหลัง backup):
- webapp เก่า v1–v4 + backups (`webapp_v5_pre*`), engine เก่า v1/v3/v5/v6/v7/v9/v11, fetch เก่า v1–v3, สคริปต์วิจัยยุค results_v6–v13 ทั้งหมด, patch/upgrade ครั้งเดียว, `site_v6/`, `src/`, `fund-system-v3-export/`
- config เก่า: `score_v1.json`, `universe_v1–v4.json`, backups — **universe_v3/v4 มี RMF/กองสถาบัน ถ้าเผลอใช้จะเลือกกองที่ซื้อไม่ได้**
- session เก่า 4 session ของ fund manager → export เป็น MD ไว้ที่ `session_archive/` ก่อนลบ

**กฎเหล็ก:** สิ่งที่อยู่ใน `scripts/` ตอนนี้มีเฉพาะสิ่งที่ระบบใช้จริง — ถ้าจะหยิบของจาก archive มาใช้ ให้ verify กับข้อมูลปัจจุบันก่อนเสมอ

## 10. ที่อยู่อื่น ๆ

- หลักฐานตัวเลข: `results_v14_*.log` (5 ไฟล์) • sweep เดิมของ z12: `results_v11/z12_sweep.log` (คัดลอกเป็น `results_v14_z12_sweep_evidence.log`)
- บทเรียนละเอียด 60+ ข้อ: skill `thai-mutual-fund-data` (~/.hermes/skills/data-science/)
- state จริง: `data/state.json` (locked_chosen/prev_lock_chosen/regime_down) — ลบได้ ระบบ reset เป็น lock ใหม่
- Backup ก่อนการแก้ไขใหญ่: remote `/root/backups/`, local `~/workspace/fund-legacy-archive/`

*เอกสารนี้เขียนจากผลการรันจริง ไม่ใช่ความจำ — ทุกเลขในนี้มี log/สคริปต์อ้างอิง*