Part One
Quick-start — what to do in the next 5 minutes
If you only have time to read one page, read this one.
Garuda Alpha runs quarterly. On most days, your only task is to monitor. Trading days where a decision is required: the first business day of January, April, July, and October. Today's status: NEUTRAL · gross 1.00×.
5-minute morning routine (before market open, 08:30 WIB)
- 08:30 Open reports/dashboard.html in browser. Confirm "as-of" date = yesterday's trading date.
- 08:31 Read the regime stance badge. If it changed from yesterday, re-check sizing assumptions (see Part 5).
- 08:33 Scan Active long picks panel for any new red flags: stop-distance <3%, or pick newly absent from the currently-investable universe.
- 08:35 If today is a quarterly rebalance Monday (Jan 1st / Apr 1st / Jul 1st / Oct 1st, or next business day), open Part 4 — Quarterly rebalance protocol.
On non-rebalance days the canonical answer is hold. Garuda's edge comes from not trading on noise. Resist the urge to act on intraday signals.
The one number that matters today
Gross exposure target = 1.00× NAV (per regime overlay). Position sizes are derived from this multiplier and the per-position risk budget at the last quarterly rebalance — do not re-size intraday.
Part Two
Daily routine (T+0)
What to do every trading day, in order.
Pre-open (T-0, 07:30 – 08:30 WIB)
- 07:30 Confirm overnight data pipeline succeeded: data/prices.parquet last_date = T-1.
- 07:35 Verify universe_liquidity.parquet updated: count of investable names should be 90–110 (alert if outside).
- 07:40 Run .venv/Scripts/python.exe scripts/build_dashboard.py to refresh reports/dashboard.html.
- 08:00 Read dashboard: stance, top-12 picks, factor-table head, MSCI calendar (any event in next 5 days?).
- 08:15 Cross-check: do the displayed picks match the saved positions in reports/active_positions.csv? Mismatch ⇒ investigate before open.
During market hours (09:00 – 16:00 WIB)
- Monitor active positions for stops touched. If a hard stop triggered, the exit is automatic and the position closes at the prevailing print — record in trade_log.csv.
- Do NOT rebalance intraday. Garuda is a quarterly book. Any rebalance decision is made at end-of-day, not during the session.
- If a position becomes halted (suspended by IDX) or moved to UMA, flag in the log and let the Haircut engine handle next quarterly rebal.
- News-driven moves > ±7% on a single position: do nothing intraday. The trailing stop will catch it if real. Mark for review in the weekly note.
Post-close (16:00 – 22:00 WIB)
- Wait for Yahoo's IDX close ingest (~17:00 WIB).
- Pipeline (cron): build_prices.py → build_universe_liquidity.py → build_dashboard.py.
- If pipeline FAILS: see Part 8 — Troubleshooting. Do NOT manually overwrite parquets.
- Pipeline SUCCESS: dashboard refreshed for tomorrow's pre-open read.
The strategy is quarterly by design. The daily routine exists to catch data pipeline failures and position-level emergencies, NOT to second-guess the next rebalance.
Part Three
Weekly review
Every Friday after close, or Monday before open.
15-minute Friday post-close checklist
- Equity curve vs JCI this week: %WoW and %YTD. Expect Garuda to track JCI within ±3% in any given week.
- Regime stance trajectory: did stance flip this week? If yes, was it transient (1-2 days) or sustained?
- Stop-distance audit: any active position with stop < 5% from current price? Note in weekly log — not actionable, just awareness.
- Sector concentration: any sector > 22% of NAV (3pp from the 25% cap)? Note — will be re-scaled at next quarterly rebal.
- Universe drift: any name in active picks that fell below ADTV20 ≥ Rp 10 Bn threshold this week? Flag for forced rebalance candidate.
- MSCI calendar: any rebalance event in next 30 days? Note the front-run window (T-5 to T-effective).
What to log in the weekly note (5 lines)
- Equity curve: {Garuda WoW%}, {JCI WoW%}, {delta}
- Stance: {current} (was {last week}). Score: {now}, {last week}.
- Risk: {n positions with stop <5%}, {sector at 22%+}, {names lost universe}.
- Events: {MSCI within 30d?}, {earnings season?}, {macro release this week?}.
- Open question / one-thing-to-watch next week.
If you have 30 more minutes (deep weekly)
- Walk-forward sanity: re-run .venv/Scripts/python.exe run_backtest.py. Headline canonical CAGR/Sharpe should reproduce within ±0.1pp. If it drifts more, see Part 8.
- Cross-validation (if HPQuant wired): run scripts/cross_validate.py. L1 stance agreement > 80% with macro_regime is the bar.
- Coverage report: skim reports/coverage_*.json for any new BLOCKED status (foreign-flow / margin-cascade still gracefully blocked is expected).
Resist the urge to "tweak weights" after a bad week. The factor weights (60/5/5/30) won 7/7 walk-forward windows; one bad week is statistical noise. If you want to study alternative weights, do it in scripts/walk_forward_refit.py — never edit factors/composite.py ad-hoc.
Part Four
Quarterly rebalance protocol
The four mornings per year when you make decisions.
Rebalance dates: first business day of January, April, July, October. At-most-12 positions are rotated based on the composite ranking as-of the prior trading day. Sector caps are enforced; over-cap names are rotated out and replaced with the next-highest composite outside the over-cap sector.
T-7 to T-2 (week before)
- Refresh all data sources: prices, macro, corp_actions, universe_liquidity, msci_rebalance_history. See Part 9 for commands.
- Re-run run_backtest.py — sanity-check canonical headline reproduces (current: CAGR +18.4%, Sharpe 0.97).
- If any test in tests/ fails, STOP. Do not rebalance until tests green.
- Review the regime stance trajectory over the last 2 weeks. Stance change between T-7 and T affects the gross multiplier and therefore position sizing.
T-1 (Sunday or last business day before rebal Monday)
- Compute factor snapshot: factors/composite.py → reports/factor_snapshot.csv.
- Compute regime stance as-of T-1: overlay/macro_regime.py.
- Generate target picks: dry-run run_backtest.py. Output: top-12 long candidates with composite scores + suggested weights.
- Compare target list to current portfolio. Identify: adds (in target, not held), drops (held, not in target), resize (held but weight changed > 1pp).
- Write the trade ticket: for each adds/drops/resize, record ticker, target weight, action, expected order size.
T-day (rebalance Monday, 08:00 – 16:00 WIB)
- 08:00 Final sanity check: dashboard's top-12 matches yesterday's dry-run. If differs, investigate before placing orders.
- 08:30 Pre-place limit orders for DROPS at last close + 0.2% (sell side). Aim to liquidate first.
- 09:00–10:00 Place limit orders for ADDS at last close − 0.2% (buy side). VWAP execution preferred for liquidity-sensitive names.
- 10:00–15:00 Monitor fills. Roll any unfilled orders to market if > 80% of session passed.
- 15:30 Confirm all targets achieved. Record realized prices, slippage vs target, residual cash.
T+1 reconciliation
- Update reports/active_positions.csv with realized weights.
- Compute realized turnover: target turnover is < 30% NAV per quarter. If turnover exceeded 50%, document why (regime change? new MSCI ADD? large outflow forced trade?).
- Update trade log with cost realized vs cost assumed (0.46% RT per spec). If slippage exceeded 80bp, escalate.
- Send IC quarterly note (template: templates/quarterly_ic_note.md — equity curve, attribution, stance trajectory, outlook).
Do NOT execute the rebalance if any of: (a) regression test fails, (b) headline CAGR/Sharpe drifts >1pp/0.1, (c) universe lost >3 names in the week, (d) dashboard fails to render. Escalate to IC and pause until resolved — one missed quarter is materially cheaper than executing on broken signals.
Part Five
Regime playbook
What to do in each of the four macro stances.
The macro overlay (overlay/macro_regime.py) computes a composite score from 7 indicators — 4 domestic / rate cluster (IDR, BI, US10Y, Brent), 3 global risk-off (VIX, DXY, gold). Score maps to a 4-state stance with a gross-exposure multiplier:
- Sizing: use full 1.50× gross at next rebal.
- Mindset: ride winners. Loose stops appropriate.
- Watch for: exuberance — rising VIX or weakening IDR signals regime change.
- Common cause: 2025 was largely LONG_BIAS (commodity boom + low VIX + stable IDR).
- Sizing: canonical 1.00× gross. Top-12 by composite.
- Mindset: default state. Trust the cross-sectional signal.
- Watch for: stance flip up (LONG_BIAS) or down (DEFENSIVE).
- Frequency: ~51% of all trading days.
- Sizing: reduce gross to 70% of NAV at next rebal. Hold 30% cash.
- Mindset: capital preservation matters more than upside capture this quarter.
- Watch for: further deterioration → RISK_OFF.
- Common cause: rate-hiking cycle, IDR depreciation, commodity slump. ~44% of days.
- Sizing: half book — gross to 40% of NAV. Hedge flag = ON.
- Mindset: survive. Do not anticipate the reversal — let the stance flip first.
- Watch for: stance recovery to DEFENSIVE = restart gradual deployment.
- Common cause: global stress (2020-Mar COVID, 2018-May taper, 2024-May IDR plunge). ~1.5% of days.
Stance transitions — what to actually do
| From → To | At next rebal | Mid-quarter |
|---|---|---|
| NEUTRAL → LONG_BIAS | Lift gross 1.00 → 1.50×. Pro-rata top up existing positions. | No action. Wait for the rebal. |
| LONG_BIAS → NEUTRAL | Trim gross 1.50 → 1.00×. Pro-rata reduce existing. | No action. |
| NEUTRAL → DEFENSIVE | Reduce gross 1.00 → 0.70×. Hold 30% cash. | No action. (Daily price moves will pull down beta anyway.) |
| DEFENSIVE → RISK_OFF | Reduce 0.70 → 0.40×. Hedge flag ON. Reduce all positions pro-rata. | Exception: if stance lasts > 5 consecutive days mid-quarter AND hedge flag triggers, execute an interim de-grossing. Document and notify IC. |
| RISK_OFF → DEFENSIVE | Lift 0.40 → 0.70×. Hedge flag OFF. | No action. |
| Any ↔ any (single-day flap) | Treat as transient. Use latest stance at T-1. | Ignore. |
The stance is not a stock-picking signal; it is a position-sizing signal. The cross-sectional momentum signal works in all four regimes — what changes is how much capital you deploy. Attempts to use macro indicators as stock-selection inputs have not improved on the cross-sectional signal.
Part Six
Scenario decision rules
Common situations and the correct response.
No. The hard stop is intentional capital protection. Re-entry happens at the next quarterly rebalance if the name re-ranks into top-12 by composite. Manual re-entry mid-quarter is a discretionary override — not in the strategy spec, not validated, do not do.
No. Sector caps are enforced at rebalance, not continuously. Intra-quarter drift up to ~30% is expected and acceptable. The next quarterly rebal will pull it back down through composite-based rotation.
Mark for forced rotation at next rebal regardless of composite score — we will not hold an illiquid name. If liquidity drops below the threshold by >50% (e.g., name halted, UMA-flagged), trigger an off-cycle exit: liquidate at the open the next trading day, hold cash until next rebal. Document.
Only at the next quarterly rebal, and only if the name ranks in top-15 by composite. The MSCI candidate-list boost was empirically tested and contribution = exactly zero on quarterly cadence (announcement window doesn't align with rebal dates). Standalone MSCI front-run sub-strategy is real (+5% CAGR) but capacity-limited; we do not deploy it inside the canonical book.
Yes — that's the right answer. Low turnover is a feature. If picks are identical, no trades happen, costs are zero, taxes are deferred. Garuda's average turnover is 30–40% per quarter, but a "no rotation" quarter is legitimate when momentum persists.
Check three things: (1) does the name have > 252 trading days of history (required for momentum, low-vol)? (2) does it pass ADTV20 ≥ Rp 10 Bn (PIT)? (3) is sector allocation OK if added? If all yes, take. The composite signal already incorporated the relevant history; trust the rank.
Likely data refresh, not a bug. A 0.5pp drift over a week is normal as new prices come in. A 2pp+ drift overnight is a red flag — check tests/ first, then data/prices.parquet for corruption (look at coverage report).
No override. The BI rate signal updates from seeds/bi_rate.csv only on confirmed BI Board of Governors meetings. Emergency cuts will reflect in tomorrow's signal after the seed is updated. Even then, BI direction is one of seven signals — the stance is composite, not single-indicator. Wait for the dashboard to reflect it.
Show them: (a) JCI return Sharpe is negative over the test window even during rallies; (b) the 7-indicator composite caught 2022-Sep + 2024-Apr stress weeks the 4-indicator version missed; (c) the maxDD −15.9% target requires de-grossing in DEFENSIVE periods; (d) WF7 OOS Sharpe +2.15 was achieved with the overlay, not despite. The mandate is risk-adjusted, not raw return.
Part Seven
Risk triggers & kill-switch matrix
When to act, when to escalate, when to halt.
Hard limits (enforced by construction)
| Limit | Value | Enforced by |
|---|---|---|
| Single-name cap | 8% NAV | engine/portfolio.py position sizing |
| Sector concentration | 25% NAV | At rebalance: composite-ranked rotation |
| Max positions | 12 long | Composite top-12 selection |
| Gross exposure ceiling | 150% NAV (LONG_BIAS) / 40% floor (RISK_OFF) | Macro regime overlay |
| Per-position risk | ~0.7% NAV to trailing stop | engine/execution.py ATR-based sizing |
| Hard-loss floor | −20% from entry | Position-level hard stop |
| Trailing stop | 5× ATR(14) | Position-level trailing stop |
Soft triggers (escalation, not halt)
- Drawdown 60% of historical max: at −9.5% from peak (60% of −15.9%), notify IC. No action change.
- Drawdown 80% of historical max: at −12.7% from peak, IC review meeting within 7 days. Document attribution.
- Drawdown at historical max: at −15.9% from peak, IC override decision: continue, reduce, or halt.
- Three consecutive negative quarters: deep methodology review. Walk-forward refit. Don't change weights unless WF refit picks a different winner across multiple windows.
- HPQuant L1 stance disagreement > 30 consecutive days: regime model failure flag. Investigate which side is correct (likely a data-feed issue).
Kill-switch (full halt) — activation requires IC sign-off
| Condition | Severity |
|---|---|
| Three consecutive quarters of underperformance vs JCI AND drawdown exceeds historical max | HALT |
| Test suite fails (any test in tests/ red) on T-2 or later | HALT next rebal |
| Headline canonical drift > 2pp CAGR or > 0.2 Sharpe overnight | PAUSE — investigate before rebal |
| IDX regulatory shock: shorting allowed, tick-size revamp > 50% bands, index-reconstitution rules changed materially | HALT — re-validate spec |
| Data pipeline failed ≥ 5 consecutive days AND no manual workaround possible | PAUSE |
| Independent third-party (HPQuant L1) shows > 20% systematic divergence for 2 consecutive review cycles | PAUSE + reconciliation review |
What is NOT a kill condition
- A single bad quarter, or a single down month.
- One walk-forward window degrading.
- A drawdown within historical maximum — the strategy is designed to operate through this range.
- JCI outperforming Garuda for 1–2 quarters (mean-reversion or commodity-pop windows).
- A single name hit hard stop, even at full 8% cap.
The hardest decisions are not acting when the market is loud. The strategy was validated specifically to handle bad quarters; manual interventions during stress have, historically, made outcomes worse. The kill-switch exists so that real regime breaks — not normal volatility — trigger a structured review, not a panic override.
Part Eight
Troubleshooting
What to do when something doesn't work.
Data pipeline issues
| Symptom | Likely cause | Fix |
|---|---|---|
| prices.parquet last_date < T-1 | Yahoo IDR fetch failed; rate-limited or symbol issue | Re-run scripts/build_prices.py. If still fails, wait 1 hour and retry — Yahoo throttles. Don't manually splice prices. |
| benchmark.parquet (JCI) stale | Yahoo ^JKSE feed issue | Re-run scripts/build_benchmark.py. JCI is single-symbol — rare to fail. If persists > 24h, escalate. |
| macro.parquet shows NaN for VIX/DXY/gold this week | Monthly snapshot not yet posted | The monthly month-end macro snapshot updates at month-end. Daily ffill fills gap. If snapshot > 35 days stale, re-fetch via scripts/extend_macro_with_vix_dxy_gold.py. |
| Universe count dropped by >5 overnight | Yahoo de-listed bulk symbols, or universe_seed got truncated | Diff seeds/universe_seed.txt against git. Re-run scripts/build_universe_liquidity.py. Roll back if seed corrupted. |
| Dashboard fails to render | Missing parquet input or stale backtest_results.json | Re-run in order: run_backtest.py → scripts/build_dashboard.py. Check error log for which input is missing. |
Backtest / regression issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Canonical CAGR drifted > 0.5pp overnight | New price data added 1 trading day to the window | Normal. Expected drift ≤ 1pp per quarter. If > 2pp, see corruption check below. |
| Headline drifted > 2pp | Possible data corruption or accidental engine change | git diff on engine/ and factors/. Re-pull prices for affected window. Run full test suite. If unresolved, revert engine to last green commit. |
| Test default_reproduces_headline fails | Canonical numbers drifted outside test tolerance | Investigate: (a) is the actual headline reasonable? (b) is the test tolerance still appropriate given new data? Update test only after IC review, not silently. |
| Walk-forward picks a non-canonical IS-winner in any WF | Regime shift; canonical may need refit | Single-WF deviation = noise (don't refit). 3+ WF deviations = formally re-evaluate canonical weights via walk_forward_refit.py + IC review. |
Execution issues
- Order didn't fill: roll to market by 15:30 WIB on rebal day. Record slippage in trade log.
- Name halted on rebal day: skip; use next-ranked composite name from factor_snapshot.csv. Document.
- Slippage > 80bp on a single name: review next quarter whether name's ADTV20 should bump the threshold from Rp 10 Bn to Rp 20 Bn for that book size.
- Cash residual > 3% after rebal: hold cash; do NOT force-deploy. Will be absorbed next quarter.
Page the IC if: (a) any kill-switch condition triggers, (b) data pipeline broken > 48 hours, (c) headline drifts > 2pp CAGR without explanation, (d) a name in active book is suspended pending material disclosure. Otherwise, log and continue.
Part Nine
Reference card
Commands, file paths, and external sources.
Daily commands
- Refresh dashboard: .venv/Scripts/python.exe scripts/build_dashboard.py
- Re-run canonical backtest: .venv/Scripts/python.exe run_backtest.py
- Macro stance read: .venv/Scripts/python.exe overlay/macro_regime.py
- Test suite: .venv/Scripts/python.exe tests/test_engine.py · test_factors.py · test_overlay.py
Weekly / quarterly commands
- Walk-forward refit: .venv/Scripts/python.exe scripts/walk_forward_refit.py (~15 min)
- Refresh prices: scripts/build_prices.py
- Refresh JCI: scripts/build_benchmark.py
- Refresh macro daily: scripts/build_macro.py
- Refresh corp_actions: scripts/build_corp_actions.py
- Refresh universe liquidity: scripts/build_universe_liquidity.py
- Refresh MSCI calendar: scripts/fetch_msci_history.py (semi-annual)
- Re-extend macro (VIX/DXY/gold): scripts/extend_macro_with_vix_dxy_gold.py
File paths (what lives where)
| Path | Purpose |
|---|---|
| data/prices.parquet | Daily OHLCV per ticker (Yahoo, survivorship-safe) |
| data/benchmark.parquet | JCI close (^JKSE) |
| data/macro.parquet | USDIDR, Brent, US10Y, BI rate, VIX, DXY, gold (daily, ffilled) |
| data/universe_liquidity.parquet | PIT investability flag per (date, ticker) |
| data/msci_rebalance_history.parquet | 59 MSCI ADD/DELETE events 2018–2026 |
| seeds/universe_seed.txt | 173-ticker universe definition |
| seeds/bi_rate.csv | BI policy rate decisions (hand-entered, verify vs bi.go.id) |
| engine/backtest.py | Backtest engine; run() is the entry point |
| engine/execution.py | Exit logic (trailing stop, hard stop, time stop) |
| engine/portfolio.py | Position sizing, sector caps, haircut integration |
| factors/composite.py | Momentum / Quality / LowVol / Trend → composite score |
| overlay/macro_regime.py | 7-indicator regime classifier → gross multiplier |
| overlay/behavioral.py | MSCI flow + foreign_local + margin_cascade (msci_flow LIVE) |
| reports/backtest_results.json | Saved canonical headline + per-config metrics |
| reports/equity_curve.csv | Daily equity for canonical + JCI |
| reports/walk_forward_refit.csv | Walk-forward IS/OOS table |
| reports/dashboard.html | Operational dashboard (this is what you open daily) |
| docs/Garuda_Alpha_Thesis_v2.html | Research thesis (full methodology + history) |
| docs/Garuda_Alpha_AM_Thesis.html | Institutional thesis (LP / IC audience) |
External data sources
- Yahoo Finance — daily OHLCV per ticker, JCI, USDIDR, Brent. Known caveat: survivorship-biased; we mitigate via delisted_history seed.
- FRED (St. Louis Fed) — US 10Y treasury (DGS10). Authoritative.
- Bank Indonesia bi.go.id/id/statistik/seki — BI policy rate. Manual sync into seeds/bi_rate.csv after each board meeting.
- MSCI Standard Index PDFs — app2.msci.com/eqb/gimi/stdindex/MSCI_{Mmm}{YY}_STPublicList.pdf. Parsed semi-annually.
- Macro history seed — monthly month-end snapshots of VIX / DXY / gold / other risk-off indicators. Forward-filled to daily grid.
Canonical configuration (snapshot at generation time)
| Parameter | Value |
|---|---|
| Universe size | 84 currently-investable / 173 tracked |
| Selection | top-12 by composite |
| Factor weights | Momentum 60 / Quality 5 / LowVol 5 / Trend 30 |
| Rebalance | Quarterly (Jan / Apr / Jul / Oct, 1st business day) |
| Exits | 5x ATR(14) trailing, −20% hard stop, no time stop |
| Position cap | 8% NAV single name |
| Sector cap | 25% NAV |
| Gross range | 40% (RISK_OFF) to 150% (LONG_BIAS) NAV |
| Cost assumption | 0.46% RT + slippage (baked in) |
| Risk-free | ~5.5% (BI 7DRR proxy) |
Read the thesis. docs/Garuda_Alpha_Thesis_v2.html documents why every parameter is what it is. This playbook covers how to operate — for the why, the thesis is canonical.