From 9513871b42dce12080638ffb5907177cdacf3a5c Mon Sep 17 00:00:00 2001 From: Sakura <32687351+zifanzhou1024@users.noreply.github.com> Date: Mon, 18 May 2026 03:54:15 -0700 Subject: [PATCH] Make Moomoo recorder fully automatic --- README.md | 39 ++++++++++++++---- docs/ml-moomoo-research-recorder.md | 39 +++++++++++++++--- ops/run_moomoo_opend.sh | 22 ++++++++++ ops/setup_market_wake_schedule.sh | 11 +++++ .../gammascope_collector/moomoo_config.py | 8 +++- .../gammascope_collector/moomoo_snapshot.py | 21 +++++++--- .../collector/tests/test_moomoo_config.py | 10 ++++- .../collector/tests/test_moomoo_snapshot.py | 40 ++++++++++++++++++- 8 files changed, 169 insertions(+), 21 deletions(-) create mode 100755 ops/run_moomoo_opend.sh create mode 100644 ops/setup_market_wake_schedule.sh diff --git a/README.md b/README.md index 0791af5..635c279 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/ml-moomoo-research-recorder.md b/docs/ml-moomoo-research-recorder.md index a1168e2..5a6992a 100644 --- a/docs/ml-moomoo-research-recorder.md +++ b/docs/ml-moomoo-research-recorder.md @@ -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 @@ -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: @@ -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: @@ -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 diff --git a/ops/run_moomoo_opend.sh b/ops/run_moomoo_opend.sh new file mode 100755 index 0000000..254f7db --- /dev/null +++ b/ops/run_moomoo_opend.sh @@ -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 diff --git a/ops/setup_market_wake_schedule.sh b/ops/setup_market_wake_schedule.sh new file mode 100644 index 0000000..506b28e --- /dev/null +++ b/ops/setup_market_wake_schedule.sh @@ -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 diff --git a/services/collector/gammascope_collector/moomoo_config.py b/services/collector/gammascope_collector/moomoo_config.py index 91443c9..4313cad 100644 --- a/services/collector/gammascope_collector/moomoo_config.py +++ b/services/collector/gammascope_collector/moomoo_config.py @@ -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( @@ -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, ), ] diff --git a/services/collector/gammascope_collector/moomoo_snapshot.py b/services/collector/gammascope_collector/moomoo_snapshot.py index cf9e68f..e2f184c 100644 --- a/services/collector/gammascope_collector/moomoo_snapshot.py +++ b/services/collector/gammascope_collector/moomoo_snapshot.py @@ -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) @@ -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 @@ -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 {} diff --git a/services/collector/tests/test_moomoo_config.py b/services/collector/tests/test_moomoo_config.py index b44846f..55b90eb 100644 --- a/services/collector/tests/test_moomoo_config.py +++ b/services/collector/tests/test_moomoo_config.py @@ -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: diff --git a/services/collector/tests/test_moomoo_snapshot.py b/services/collector/tests/test_moomoo_snapshot.py index 2bd54c5..79f89c0 100644 --- a/services/collector/tests/test_moomoo_snapshot.py +++ b/services/collector/tests/test_moomoo_snapshot.py @@ -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( @@ -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"),