Two programs do everything. strike.py is the trader — it runs on your command (the weekly "re-strike"), scores the whole universe, and writes a frozen book. refresh.py is the bookkeeper — every 30 min it only marks that frozen book to live prices (P&L, NAV, stops). refresh.py never trades. A trade happens only when you run a strike.
A strike is a deliberate weekly event — you run python strike.py. Between strikes the book is frozen; the 30-min auto-refresh only updates prices and P&L. So the fund does not day-trade or react intraday — it re-decides on a weekly cadence, exactly like the master-prompt pulse.
It downloads 1 year of daily closes from yfinance (free) for the entire universe — 30 US ETFs, 45 Bursa names, plus S&P, KLCI and USD/MYR. Then it enforces the fairness rule you insisted on:
So a US session that finishes overnight KL-time can't leak into a decision dated the evening before. Every price used is a completed bar.
First it computes the regime from the S&P vs its 200-day and breadth:
| Regime | Condition | Base unit |
|---|---|---|
| GREEN | S&P > 200D & breadth ≥55% & RSP/SPY rising | 8% of NAV |
| YELLOW | > 200D but breadth diverging | 4% of NAV |
| RED | S&P < 200D | 0% — no new buys |
The base unit is the size of one "full" conviction slot. Today it's GREEN → 8%.
Each of the ~75 names gets a scorecard row. The trend gate is mandatory and comes first — nothing below trend can be bought, no matter how good it looks:
If it clears the gate, it earns 1 point per pillar:
| 🟢 Momentum | EMA20 > EMA50 (short trend up) |
| 🟢 Relative strength | 3-month return beats its benchmark (SPY / KLCI) |
| 🟢 Flow | money flowing IN (weekly research read) |
| 🟢 Value | CHEAP or NORMAL — not RICH (weekly read) |
| 🟢 Catalyst | a named, dated catalyst (weekly read) |
| 🟢 Small-cap (MY only) | market cap < RM3B — your upside bonus |
Momentum, RS and the trend gate are computed from price. Flow, Value and Catalyst are the weekly human layer (a research read stored in the engine). The score becomes a size multiplier:
| Score | Multiplier | Meaning |
|---|---|---|
| 5–6, or TRIPLE | ×2.0 | TRIPLE = gate + momentum + flow IN + CHEAP |
| 4 | ×1.5 | strong |
| 3 | ×1.0 | one full unit |
| 2 (only if Mom + RS) | ×0.5 | starter |
| ≤1 or gate fail | ×0 | no position |
Example: a score-4 name in GREEN → 8% × 1.5 = 12% → capped to 10%. A score-3 → 8% × 1.0 = 8%.
Qualifiers are ranked by score, then by relative strength, and added top-down until a limit bites:
Anything that qualifies but doesn't fit (cap or 12-slot limit) goes to the watch list, not the trash.
It diffs the new book against the frozen one and emits real orders in this order:
Every order is charged real costs, per side:
A new position's stop is set to max(200-day, entry × 0.92) and flagged provisional; a kept position keeps its original frozen stop.
The result is written to book.json (the frozen truth) and book-data.js (what the dashboard reads). From then until the next strike, refresh.py only marks it — recomputing P&L, NAV and whether price has hit a stop (the exit-ladder rung). A kept position never has its entry re-written by later prices — that's the point-in-time discipline.
Two kinds of names sit on watch: (a) qualified-but-no-room (a slot/cap was full), and (b) strong scorers that only failed the 200-day gate — these carry a concrete trigger like close > 152.40 (200D). When a watched name crosses back above its 200-day, it re-arms for the next strike.
Source of truth: strike.py (the trader) and refresh.py (the marker) in your app folder. This page describes the engine exactly as coded on 2026-07-11; if the rules change, re-read the engine — it, not this page, is authoritative. Paper/practice system — not investment advice, not a broker.