Garuda Alpha · Operational Playbook

The Operator's Manual

Daily, weekly, and quarterly procedures for running the IDX cross-sectional momentum strategy in production. Audience: portfolio manager / strategy operator.

Generated 2026-06-16 23:45 WIB · Live data as-of 2026-06-02 · Refresh cadence: nightly
Current Stance
NEUTRAL · gross 1.00x
macro score +0 (range −6..+7) · domestic IDR -1 BI +0 US10Y -1 Brent +1 · risk-off VIX +0 DXY +0 Gold +1
Canonical Track Record (2018→2026)
CAGR +18.4% · Sharpe 0.97
maxDD -15.3% · PF 1.91 · win +43% · gates 5/7 · JCI -3.3%/Sharpe -0.26

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)

  1. 08:30 Open reports/dashboard.html in browser. Confirm "as-of" date = yesterday's trading date.
  2. 08:31 Read the regime stance badge. If it changed from yesterday, re-check sizing assumptions (see Part 5).
  3. 08:33 Scan Active long picks panel for any new red flags: stop-distance <3%, or pick newly absent from the currently-investable universe.
  4. 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.
Default action

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)

  1. 07:30 Confirm overnight data pipeline succeeded: data/prices.parquet last_date = T-1.
  2. 07:35 Verify universe_liquidity.parquet updated: count of investable names should be 90–110 (alert if outside).
  3. 07:40 Run .venv/Scripts/python.exe scripts/build_dashboard.py to refresh reports/dashboard.html.
  4. 08:00 Read dashboard: stance, top-12 picks, factor-table head, MSCI calendar (any event in next 5 days?).
  5. 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)

  1. 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.
  2. Do NOT rebalance intraday. Garuda is a quarterly book. Any rebalance decision is made at end-of-day, not during the session.
  3. 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.
  4. 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)

  1. Wait for Yahoo's IDX close ingest (~17:00 WIB).
  2. Pipeline (cron): build_prices.py → build_universe_liquidity.py → build_dashboard.py.
  3. If pipeline FAILS: see Part 8 — Troubleshooting. Do NOT manually overwrite parquets.
  4. Pipeline SUCCESS: dashboard refreshed for tomorrow's pre-open read.
Daily principle

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

  1. Equity curve vs JCI this week: %WoW and %YTD. Expect Garuda to track JCI within ±3% in any given week.
  2. Regime stance trajectory: did stance flip this week? If yes, was it transient (1-2 days) or sustained?
  3. Stop-distance audit: any active position with stop < 5% from current price? Note in weekly log — not actionable, just awareness.
  4. Sector concentration: any sector > 22% of NAV (3pp from the 25% cap)? Note — will be re-scaled at next quarterly rebal.
  5. Universe drift: any name in active picks that fell below ADTV20 ≥ Rp 10 Bn threshold this week? Flag for forced rebalance candidate.
  6. 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)

  1. Equity curve: {Garuda WoW%}, {JCI WoW%}, {delta}
  2. Stance: {current} (was {last week}). Score: {now}, {last week}.
  3. Risk: {n positions with stop <5%}, {sector at 22%+}, {names lost universe}.
  4. Events: {MSCI within 30d?}, {earnings season?}, {macro release this week?}.
  5. Open question / one-thing-to-watch next week.

If you have 30 more minutes (deep weekly)

Weekly anti-pattern

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)

  1. Refresh all data sources: prices, macro, corp_actions, universe_liquidity, msci_rebalance_history. See Part 9 for commands.
  2. Re-run run_backtest.py — sanity-check canonical headline reproduces (current: CAGR +18.4%, Sharpe 0.97).
  3. If any test in tests/ fails, STOP. Do not rebalance until tests green.
  4. 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)

  1. Compute factor snapshot: factors/composite.pyreports/factor_snapshot.csv.
  2. Compute regime stance as-of T-1: overlay/macro_regime.py.
  3. Generate target picks: dry-run run_backtest.py. Output: top-12 long candidates with composite scores + suggested weights.
  4. Compare target list to current portfolio. Identify: adds (in target, not held), drops (held, not in target), resize (held but weight changed > 1pp).
  5. 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)

  1. 08:00 Final sanity check: dashboard's top-12 matches yesterday's dry-run. If differs, investigate before placing orders.
  2. 08:30 Pre-place limit orders for DROPS at last close + 0.2% (sell side). Aim to liquidate first.
  3. 09:00–10:00 Place limit orders for ADDS at last close − 0.2% (buy side). VWAP execution preferred for liquidity-sensitive names.
  4. 10:00–15:00 Monitor fills. Roll any unfilled orders to market if > 80% of session passed.
  5. 15:30 Confirm all targets achieved. Record realized prices, slippage vs target, residual cash.

T+1 reconciliation

  1. Update reports/active_positions.csv with realized weights.
  2. 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?).
  3. Update trade log with cost realized vs cost assumed (0.46% RT per spec). If slippage exceeded 80bp, escalate.
  4. Send IC quarterly note (template: templates/quarterly_ic_note.md — equity curve, attribution, stance trajectory, outlook).
Pre-trade kill criteria

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:

LONG_BIAS
Macro score ≥ +3 · gross 1.50× NAV
Trigger: at least 3 of the 7 signals positive net (e.g., IDR strong + commodity tailwind + low VIX)
  • 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).
NEUTRAL
Macro score 0 to +2 · gross 1.00× NAV
Trigger: mixed signal — some positive, some negative, net mildly positive
  • 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.
DEFENSIVE
Macro score −1 to −3 · gross 0.70× NAV
Trigger: 2–3 negative signals (e.g., IDR weak + US10Y rising + VIX elevated)
  • 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.
RISK_OFF
Macro score ≤ −4 · gross 0.40× NAV + hedge
Trigger: most signals negative simultaneously (stress cluster)
  • 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 → ToAt next rebalMid-quarter
NEUTRAL → LONG_BIASLift gross 1.00 → 1.50×. Pro-rata top up existing positions.No action. Wait for the rebal.
LONG_BIAS → NEUTRALTrim gross 1.50 → 1.00×. Pro-rata reduce existing.No action.
NEUTRAL → DEFENSIVEReduce gross 1.00 → 0.70×. Hold 30% cash.No action. (Daily price moves will pull down beta anyway.)
DEFENSIVE → RISK_OFFReduce 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 → DEFENSIVELift 0.40 → 0.70×. Hedge flag OFF.No action.
Any ↔ any (single-day flap)Treat as transient. Use latest stance at T-1.Ignore.
Why this works

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.

Position X hit hard stop on a bad macro day. Do I re-enter when it recovers?

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.

A sector exceeded the 25% cap mid-quarter (e.g., banking names rallied hard). Do I re-balance now?

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.

An active pick fell below the ADTV20 liquidity threshold (e.g., daily volume dropped). What now?

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.

MSCI announced an ADD for a name we don't hold, effective in 2 weeks. Should we front-run?

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.

Today's dashboard shows the same top-12 picks as last quarter — literally identical. Do I just hold?

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.

A new name appeared in top-12 (e.g., post-IPO) — is it safe to take?

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.

The headline CAGR drifted from the last run (was 23.2%, now 22.7%) — is something broken?

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).

News claim: "Bank Indonesia just held an emergency rate cut." Stance still shows DEFENSIVE. Should I override?

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.

An LP / IC member asks "why is gross only 70% during a rally?" Defensive stance feels too cautious.

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)

LimitValueEnforced by
Single-name cap8% NAVengine/portfolio.py position sizing
Sector concentration25% NAVAt rebalance: composite-ranked rotation
Max positions12 longComposite top-12 selection
Gross exposure ceiling150% NAV (LONG_BIAS) / 40% floor (RISK_OFF)Macro regime overlay
Per-position risk~0.7% NAV to trailing stopengine/execution.py ATR-based sizing
Hard-loss floor−20% from entryPosition-level hard stop
Trailing stop5× ATR(14)Position-level trailing stop

Soft triggers (escalation, not halt)

Kill-switch (full halt) — activation requires IC sign-off

ConditionSeverity
Three consecutive quarters of underperformance vs JCI AND drawdown exceeds historical maxHALT
Test suite fails (any test in tests/ red) on T-2 or laterHALT next rebal
Headline canonical drift > 2pp CAGR or > 0.2 Sharpe overnightPAUSE — investigate before rebal
IDX regulatory shock: shorting allowed, tick-size revamp > 50% bands, index-reconstitution rules changed materiallyHALT — re-validate spec
Data pipeline failed ≥ 5 consecutive days AND no manual workaround possiblePAUSE
Independent third-party (HPQuant L1) shows > 20% systematic divergence for 2 consecutive review cyclesPAUSE + reconciliation review

What is NOT a kill condition

Discipline over discretion

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

SymptomLikely causeFix
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.pyscripts/build_dashboard.py. Check error log for which input is missing.

Backtest / regression issues

SymptomLikely causeFix
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

Escalation thresholds

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

Weekly / quarterly commands

File paths (what lives where)

PathPurpose
data/prices.parquetDaily OHLCV per ticker (Yahoo, survivorship-safe)
data/benchmark.parquetJCI close (^JKSE)
data/macro.parquetUSDIDR, Brent, US10Y, BI rate, VIX, DXY, gold (daily, ffilled)
data/universe_liquidity.parquetPIT investability flag per (date, ticker)
data/msci_rebalance_history.parquet59 MSCI ADD/DELETE events 2018–2026
seeds/universe_seed.txt173-ticker universe definition
seeds/bi_rate.csvBI policy rate decisions (hand-entered, verify vs bi.go.id)
engine/backtest.pyBacktest engine; run() is the entry point
engine/execution.pyExit logic (trailing stop, hard stop, time stop)
engine/portfolio.pyPosition sizing, sector caps, haircut integration
factors/composite.pyMomentum / Quality / LowVol / Trend → composite score
overlay/macro_regime.py7-indicator regime classifier → gross multiplier
overlay/behavioral.pyMSCI flow + foreign_local + margin_cascade (msci_flow LIVE)
reports/backtest_results.jsonSaved canonical headline + per-config metrics
reports/equity_curve.csvDaily equity for canonical + JCI
reports/walk_forward_refit.csvWalk-forward IS/OOS table
reports/dashboard.htmlOperational dashboard (this is what you open daily)
docs/Garuda_Alpha_Thesis_v2.htmlResearch thesis (full methodology + history)
docs/Garuda_Alpha_AM_Thesis.htmlInstitutional thesis (LP / IC audience)

External data sources

Canonical configuration (snapshot at generation time)

ParameterValue
Universe size84 currently-investable / 173 tracked
Selectiontop-12 by composite
Factor weightsMomentum 60 / Quality 5 / LowVol 5 / Trend 30
RebalanceQuarterly (Jan / Apr / Jul / Oct, 1st business day)
Exits5x ATR(14) trailing, −20% hard stop, no time stop
Position cap8% NAV single name
Sector cap25% NAV
Gross range40% (RISK_OFF) to 150% (LONG_BIAS) NAV
Cost assumption0.46% RT + slippage (baked in)
Risk-free~5.5% (BI 7DRR proxy)
When in doubt

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.