Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,9 @@ Eastern, and repeats weekdays while the process stays alive:

pnpm collector:moomoo-research-market

RUT and NDX currently need manual spot values before option rows can be selected:
All six configured tickers can run without manual spot input. SPX uses `US.SPY`
as its proxy; RUT uses `US.IWM * 10.0`; NDX uses `US.QQQ * 40.0`. Manual spots
remain available as overrides for unusual sessions:

pnpm collector:moomoo-research-record -- --spot RUT=2150 --spot NDX=18400

Expand All @@ -353,36 +355,59 @@ Automatic Moomoo ML collection is configured through a macOS LaunchAgent:

/Users/sakura/Library/LaunchAgents/com.sakura.gammascope.moomoo-research-recorder.plist

Moomoo OpenD is also managed by a LaunchAgent:

/Users/sakura/Library/LaunchAgents/com.sakura.moomoo-opend.plist

It runs:

/Users/sakura/WebstormProjects/gamma-scope/ops/run_moomoo_research_market.sh

The wrapper starts the market-hours recorder, waits for the regular U.S. session
when needed, repeats across weekdays, and restarts after 5 minutes if the
recorder exits. Logs are written to:
The OpenD agent runs:

/Users/sakura/WebstormProjects/gamma-scope/ops/run_moomoo_opend.sh

The OpenD wrapper checks every 60 seconds and opens
`/Applications/moomoo_OpenD.app` if it is not running. The recorder wrapper
starts the market-hours recorder, waits for the regular U.S. session when
needed, repeats across weekdays, and restarts after 5 minutes if the recorder
exits. Logs are written to:

/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-research-recorder.out.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-research-recorder.err.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-opend.out.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-opend.err.log

Check service status:

launchctl print gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl print gui/$(id -u)/com.sakura.moomoo-opend

Restart after changing recorder arguments:

launchctl kickstart -k gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl kickstart -k gui/$(id -u)/com.sakura.moomoo-opend

Stop automatic collection:

launchctl bootout gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl bootout gui/$(id -u)/com.sakura.moomoo-opend

Optional extra arguments live in:

/Users/sakura/local-ml-data/gamma-ml-research/moomoo-recorder.args

Use that file for RUT/NDX manual spots or a 30-second fallback cadence. The
LaunchAgent cannot wake a sleeping Mac, and Moomoo OpenD still needs to be
available and logged in.
Use that file for optional manual spot overrides or a 30-second fallback cadence. The
LaunchAgents cannot wake a sleeping Mac, and Moomoo OpenD still needs a valid
login session.

To wake the Mac automatically before market open, run the one-time root setup:

sudo /Users/sakura/WebstormProjects/gamma-scope/ops/setup_market_wake_schedule.sh

That installs a weekday wake-or-power-on schedule at 6:20 AM local Mac time. In
Arizona that is before the 9:30 AM Eastern open during daylight-saving months
and still safely before open during standard-time months.

### SPX 0DTE Exposure Heatmap

Expand Down
39 changes: 34 additions & 5 deletions docs/ml-moomoo-research-recorder.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ The recorder uses the same configured Moomoo universe as the collector:
SPX, SPY, QQQ, IWM, RUT, NDX
```

It still selects only the active 0DTE expiry and ATM-centered strike windows. RUT and NDX require manual spot values unless a later source improves index spot resolution:
It still selects only the active 0DTE expiry and ATM-centered strike windows. The default configuration is automatic for all six tickers. SPX, RUT, and NDX use liquid ETF proxy spots for initial ATM selection, then symbols configured with option-pair inference can refine spot from call/put prices after snapshots are available.

Manual spot values remain optional overrides for unusual sessions:

```bash
pnpm collector:moomoo-research-record -- --spot RUT=2150 --spot NDX=18400
Expand All @@ -47,8 +49,8 @@ Moomoo-specific names stay in source-specific fields:
| `SPY` | `US.SPY` | none | ETF ticker maps directly. |
| `QQQ` | `US.QQQ` | none | ETF ticker maps directly. |
| `IWM` | `US.IWM` | none | ETF ticker maps directly. |
| `RUT` | `US..RUT` | `RUTW` | Requires manual spot today. |
| `NDX` | `US..NDX` | `NDXP` | Requires manual spot today. |
| `RUT` | `US..RUT` | `RUTW` | Uses `US.IWM * 10.0` as the automatic initial spot proxy. |
| `NDX` | `US..NDX` | `NDXP` | Uses `US.QQQ * 40.0` as the automatic initial spot proxy. |

Rules:

Expand Down Expand Up @@ -96,29 +98,48 @@ This LaunchAgent starts:
/Users/sakura/WebstormProjects/gamma-scope/ops/run_moomoo_research_market.sh
```

Moomoo OpenD also has a LaunchAgent:

```text
~/Library/LaunchAgents/com.sakura.moomoo-opend.plist
```

It starts:

```text
/Users/sakura/WebstormProjects/gamma-scope/ops/run_moomoo_opend.sh
```

The OpenD wrapper checks every 60 seconds and opens `/Applications/moomoo_OpenD.app` if the OpenD process is not running.

The wrapper runs the market-hours recorder continuously, waits for regular market open, repeats across weekdays, and restarts with a 5-minute backoff if the recorder exits. It writes logs to:

```text
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-research-recorder.out.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-research-recorder.err.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-opend.out.log
/Users/sakura/local-ml-data/gamma-ml-research/logs/moomoo-opend.err.log
```

Check status:

```bash
launchctl print gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl print gui/$(id -u)/com.sakura.moomoo-opend
```

Restart after editing args:

```bash
launchctl kickstart -k gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl kickstart -k gui/$(id -u)/com.sakura.moomoo-opend
```

Stop automatic collection:

```bash
launchctl bootout gui/$(id -u)/com.sakura.gammascope.moomoo-research-recorder
launchctl bootout gui/$(id -u)/com.sakura.moomoo-opend
```

Extra arguments can be placed in:
Expand All @@ -127,9 +148,17 @@ Extra arguments can be placed in:
/Users/sakura/local-ml-data/gamma-ml-research/moomoo-recorder.args
```

Use that file for RUT/NDX manual spots or a 30-second fallback cadence.
Use that file for optional manual spot overrides or a 30-second fallback cadence.

The LaunchAgents start at login and restart the recorder/OpenD wrappers, but LaunchAgents do not wake a sleeping Mac. To add a weekday wake-or-power-on schedule before market open, run this once:

```bash
sudo /Users/sakura/WebstormProjects/gamma-scope/ops/setup_market_wake_schedule.sh
```

The script configures macOS to wake or power on Monday through Friday at 6:20 AM local Mac time.

Continuous capture with manual RUT/NDX spots:
Continuous capture with optional manual spot overrides:

```bash
pnpm collector:moomoo-research-record -- --spot RUT=2150 --spot NDX=18400
Expand Down
22 changes: 22 additions & 0 deletions ops/run_moomoo_opend.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/zsh
set -u

export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

APP_PATH="/Applications/moomoo_OpenD.app"
PROCESS_PATTERN="/Applications/moomoo_OpenD.app/Contents/MacOS/moomoo_OpenD"
DATA_DIR="/Users/sakura/local-ml-data/gamma-ml-research"
LOG_DIR="${DATA_DIR}/logs"

mkdir -p "${LOG_DIR}"

while true; do
if pgrep -f "${PROCESS_PATTERN}" >/dev/null 2>&1; then
sleep 60
continue
fi

echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') starting moomoo OpenD"
/usr/bin/open -ga "${APP_PATH}"
sleep 60
done
11 changes: 11 additions & 0 deletions ops/setup_market_wake_schedule.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/zsh
set -euo pipefail

if [[ "$(id -u)" -ne 0 ]]; then
echo "This script must be run with sudo because pmset power scheduling is system-wide." >&2
echo "Run: sudo /Users/sakura/WebstormProjects/gamma-scope/ops/setup_market_wake_schedule.sh" >&2
exit 1
fi

pmset repeat wakeorpoweron MTWRF 06:20:00
pmset -g sched
8 changes: 6 additions & 2 deletions services/collector/gammascope_collector/moomoo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ def default_moomoo_universe() -> list[MoomooSymbolConfig]:
strike_window_down=20,
strike_window_up=20,
family_filter="RUTW",
requires_manual_spot=True,
spot_proxy_code="US.IWM",
spot_proxy_multiplier=10.0,
infer_spot_from_options=True,
priority=50,
),
MoomooSymbolConfig(
Expand All @@ -119,7 +121,9 @@ def default_moomoo_universe() -> list[MoomooSymbolConfig]:
strike_window_down=50,
strike_window_up=50,
family_filter="NDXP",
requires_manual_spot=True,
spot_proxy_code="US.QQQ",
spot_proxy_multiplier=40.0,
infer_spot_from_options=True,
priority=60,
),
]
Expand Down
21 changes: 16 additions & 5 deletions services/collector/gammascope_collector/moomoo_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,14 +677,15 @@ def _normalize_record(row: object) -> dict[str, object]:


def _resolve_symbol_spot(client: MoomooQuoteClient, symbol_config: MoomooSymbolConfig) -> float | None:
manual_spot = _positive_float_or_none(symbol_config.manual_spot)
if manual_spot is not None:
return manual_spot

if symbol_config.spot_proxy_code:
spot = _snapshot_proxy_spot(client, symbol_config)
if spot is not None:
return spot

manual_spot = _positive_float_or_none(symbol_config.manual_spot)
if manual_spot is not None:
return manual_spot
if symbol_config.requires_manual_spot:
return None
return _snapshot_underlying_spot(client, symbol_config.owner_code)
Expand All @@ -700,7 +701,13 @@ def _snapshot_proxy_spot(client: MoomooQuoteClient, symbol_config: MoomooSymbolC


def _spot_proxy_requests_per_refresh(symbols: Sequence[MoomooSymbolConfig]) -> int:
proxy_codes = sorted({symbol.spot_proxy_code for symbol in symbols if symbol.spot_proxy_code})
proxy_codes = sorted(
{
symbol.spot_proxy_code
for symbol in symbols
if symbol.spot_proxy_code and _positive_float_or_none(symbol.manual_spot) is None
}
)
return len(list(chunked(proxy_codes, SNAPSHOT_CODE_LIMIT))) if proxy_codes else 0


Expand Down Expand Up @@ -729,7 +736,11 @@ def _snapshot_proxy_spots(
symbols: Sequence[MoomooSymbolConfig],
warnings: list[str],
) -> dict[str, float]:
proxy_symbols = [symbol for symbol in symbols if symbol.spot_proxy_code]
proxy_symbols = [
symbol
for symbol in symbols
if symbol.spot_proxy_code and _positive_float_or_none(symbol.manual_spot) is None
]
if not proxy_symbols:
return {}

Expand Down
10 changes: 9 additions & 1 deletion services/collector/tests/test_moomoo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ def test_default_universe_matches_moomoo_design() -> None:
assert spx.spot_proxy_code == "US.SPY"
assert spx.spot_proxy_multiplier == 10.035
assert spx.infer_spot_from_options is True
assert [item.symbol for item in universe if item.requires_manual_spot] == ["RUT", "NDX"]
rut = next(item for item in universe if item.symbol == "RUT")
assert rut.spot_proxy_code == "US.IWM"
assert rut.spot_proxy_multiplier == 10.0
assert rut.infer_spot_from_options is True
ndx = next(item for item in universe if item.symbol == "NDX")
assert ndx.spot_proxy_code == "US.QQQ"
assert ndx.spot_proxy_multiplier == 40.0
assert ndx.infer_spot_from_options is True
assert [item.symbol for item in universe if item.requires_manual_spot] == []


def test_collector_config_defaults_populate_moomoo_universe() -> None:
Expand Down
40 changes: 39 additions & 1 deletion services/collector/tests/test_moomoo_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,44 @@ def test_discover_symbol_contracts_uses_live_spot_proxy_for_index_options() -> N
]


def test_discover_symbol_contracts_prefers_manual_spot_over_proxy() -> None:
client = FakeQuoteClient(
chains={
"US..RUT": [
_option("US.RUTW260427C02050000", strike=2050, option_type="CALL", name="RUTW 2050C"),
_option("US.RUTW260427P02050000", strike=2050, option_type="PUT", name="RUTW 2050P"),
_option("US.RUTW260427C02200000", strike=2200, option_type="CALL", name="RUTW 2200C"),
_option("US.RUTW260427P02200000", strike=2200, option_type="PUT", name="RUTW 2200P"),
]
},
snapshots={
"US.IWM": {
"code": "US.IWM",
"last_price": 220.0,
}
},
)
symbol = MoomooSymbolConfig(
symbol="RUT",
owner_code="US..RUT",
strike_window_down=0,
strike_window_up=0,
family_filter="RUTW",
manual_spot=2050.0,
spot_proxy_code="US.IWM",
spot_proxy_multiplier=10.0,
)

result = discover_symbol_contracts(client, symbol, expiry=date(2026, 4, 27))

assert client.snapshot_calls == []
assert result.spot == pytest.approx(2050.0)
assert [contract.option_code for contract in result.contracts] == [
"US.RUTW260427C02050000",
"US.RUTW260427P02050000",
]


def test_missing_required_manual_spot_skips_symbol_without_fetching_option_chain() -> None:
client = FakeQuoteClient()
symbol = MoomooSymbolConfig(
Expand Down Expand Up @@ -1123,7 +1161,7 @@ def test_main_max_loops_zero_runs_until_interrupted_and_publishes_each_snapshot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = InterruptingQuoteClient(
interrupt_after_snapshots=5,
interrupt_after_snapshots=2,
chains={
"US..SPX": [
_option("US.SPXW260427C07050000", strike=7050, option_type="CALL", name="SPXW 7050C"),
Expand Down
Loading