Skip to content

Commit b8a5d0b

Browse files
Pigbibicodex
andauthored
fix: drive strategy schedules from settings catalog (#192)
* fix: drive strategy schedules from settings catalog Co-Authored-By: Codex <noreply@openai.com> * fix: scope scheduler plugin overrides by strategy Co-Authored-By: Codex <noreply@openai.com> * ci: sync dependency matrix with consumer locks Co-Authored-By: Codex <noreply@openai.com> * test: track staged QPK rollout pins Co-Authored-By: Codex <noreply@openai.com> * fix: report malformed scheduler references Co-Authored-By: Codex <noreply@openai.com> * fix: validate strategy scheduling domains Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent f3c464c commit b8a5d0b

6 files changed

Lines changed: 311 additions & 106 deletions

File tree

internal_dependency_matrix.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"path": "pyproject.toml",
1414
"package": "quant-platform-kit",
1515
"source_repo": "QuantPlatformKit",
16-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
16+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
1717
},
1818
{
1919
"consumer_repo": "BinancePlatform",
@@ -27,14 +27,14 @@
2727
"path": "uv.lock",
2828
"package": "quant-platform-kit",
2929
"source_repo": "QuantPlatformKit",
30-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
30+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
3131
},
3232
{
3333
"consumer_repo": "CharlesSchwabPlatform",
3434
"path": "pyproject.toml",
3535
"package": "quant-platform-kit",
3636
"source_repo": "QuantPlatformKit",
37-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
37+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
3838
},
3939
{
4040
"consumer_repo": "CharlesSchwabPlatform",
@@ -48,7 +48,7 @@
4848
"path": "uv.lock",
4949
"package": "quant-platform-kit",
5050
"source_repo": "QuantPlatformKit",
51-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
51+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
5252
},
5353
{
5454
"consumer_repo": "CharlesSchwabPlatform",
@@ -118,14 +118,14 @@
118118
"path": "pyproject.toml",
119119
"package": "quant-platform-kit",
120120
"source_repo": "QuantPlatformKit",
121-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
121+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
122122
},
123123
{
124124
"consumer_repo": "CryptoStrategies",
125125
"path": "uv.lock",
126126
"package": "quant-platform-kit",
127127
"source_repo": "QuantPlatformKit",
128-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
128+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
129129
},
130130
{
131131
"consumer_repo": "FirstradePlatform",
@@ -195,7 +195,7 @@
195195
"path": "pyproject.toml",
196196
"package": "quant-platform-kit",
197197
"source_repo": "QuantPlatformKit",
198-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
198+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
199199
},
200200
{
201201
"consumer_repo": "InteractiveBrokersPlatform",
@@ -216,7 +216,7 @@
216216
"path": "uv.lock",
217217
"package": "quant-platform-kit",
218218
"source_repo": "QuantPlatformKit",
219-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
219+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
220220
},
221221
{
222222
"consumer_repo": "InteractiveBrokersPlatform",
@@ -237,7 +237,7 @@
237237
"path": "pyproject.toml",
238238
"package": "quant-platform-kit",
239239
"source_repo": "QuantPlatformKit",
240-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
240+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
241241
},
242242
{
243243
"consumer_repo": "LongBridgePlatform",
@@ -258,7 +258,7 @@
258258
"path": "uv.lock",
259259
"package": "quant-platform-kit",
260260
"source_repo": "QuantPlatformKit",
261-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
261+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
262262
},
263263
{
264264
"consumer_repo": "LongBridgePlatform",
@@ -300,7 +300,7 @@
300300
"path": "pyproject.toml",
301301
"package": "quant-platform-kit",
302302
"source_repo": "QuantPlatformKit",
303-
"ref": "37c81901160c5b31127a27dba1c63944933fb6bf"
303+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
304304
},
305305
{
306306
"consumer_repo": "UsEquitySnapshotPipelines",
@@ -314,14 +314,14 @@
314314
"path": "pyproject.toml",
315315
"package": "us-equity-strategies",
316316
"source_repo": "UsEquityStrategies",
317-
"ref": "17ddb86c72d44b2c7b78ba7a10d8f71b21180166"
317+
"ref": "b0b590ce5ac0233a40b8fb957c249cf375a6ff21"
318318
},
319319
{
320320
"consumer_repo": "UsEquitySnapshotPipelines",
321321
"path": "uv.lock",
322322
"package": "quant-platform-kit",
323323
"source_repo": "QuantPlatformKit",
324-
"ref": "37c81901160c5b31127a27dba1c63944933fb6bf"
324+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
325325
},
326326
{
327327
"consumer_repo": "UsEquitySnapshotPipelines",
@@ -335,21 +335,21 @@
335335
"path": "uv.lock",
336336
"package": "us-equity-strategies",
337337
"source_repo": "UsEquityStrategies",
338-
"ref": "17ddb86c72d44b2c7b78ba7a10d8f71b21180166"
338+
"ref": "b0b590ce5ac0233a40b8fb957c249cf375a6ff21"
339339
},
340340
{
341341
"consumer_repo": "UsEquityStrategies",
342342
"path": "pyproject.toml",
343343
"package": "quant-platform-kit",
344344
"source_repo": "QuantPlatformKit",
345-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
345+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
346346
},
347347
{
348348
"consumer_repo": "UsEquityStrategies",
349349
"path": "uv.lock",
350350
"package": "quant-platform-kit",
351351
"source_repo": "QuantPlatformKit",
352-
"ref": "651c9ac4f37ce6e7fe1bac84dc7646cd5abc9e6e"
352+
"ref": "ff09c889ed21e2eb6fcb37f6cdaa159190ec82da"
353353
}
354354
]
355355
}

platform-config.json

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,75 @@
2727
"domains": {
2828
"us_equity": {
2929
"label_zh": "美股",
30-
"label_en": "US Equity"
30+
"label_en": "US Equity",
31+
"scheduler_profile": "us_daily"
3132
},
3233
"hk_equity": {
3334
"label_zh": "港股",
34-
"label_en": "HK Equity"
35+
"label_en": "HK Equity",
36+
"scheduler_profile": "hk_daily"
3537
},
3638
"cn_equity": {
3739
"label_zh": "A股",
38-
"label_en": "CN A-share"
40+
"label_en": "CN A-share",
41+
"scheduler_profile": "cn_daily"
3942
},
4043
"crypto": {
4144
"label_zh": "加密",
42-
"label_en": "Crypto"
45+
"label_en": "Crypto",
46+
"scheduler_profile": "crypto_intraday"
47+
}
48+
},
49+
"scheduling": {
50+
"profiles": {
51+
"us_daily": {
52+
"timezone": "America/New_York",
53+
"main_time": "45 15 * * *",
54+
"probe_time": "35 9,15 * * *",
55+
"precheck_time": "45 9 * * *"
56+
},
57+
"us_dca_month_end": {
58+
"timezone": "America/New_York",
59+
"main_time": "45 15 25-28 * *",
60+
"probe_time": "35 9,15 25-28 * *",
61+
"precheck_time": "45 9 25-28 * *"
62+
},
63+
"us_snapshot_month_start": {
64+
"timezone": "America/New_York",
65+
"main_time": "45 15 1-7 * *",
66+
"probe_time": "35 9,15 1-7 * *",
67+
"precheck_time": "45 9 1-7 * *"
68+
},
69+
"hk_daily": {
70+
"timezone": "Asia/Hong_Kong",
71+
"main_time": "45 15 * * *",
72+
"probe_time": "35 9,15 * * *",
73+
"precheck_time": "45 9 * * *"
74+
},
75+
"hk_snapshot_month_start": {
76+
"timezone": "Asia/Hong_Kong",
77+
"main_time": "45 15 1-7 * *",
78+
"probe_time": "35 9,15 1-7 * *",
79+
"precheck_time": "45 9 1-7 * *"
80+
},
81+
"cn_daily": {
82+
"timezone": "Asia/Shanghai",
83+
"main_time": "45 15 * * *",
84+
"probe_time": "35 9,15 * * *",
85+
"precheck_time": "45 9 * * *"
86+
},
87+
"cn_snapshot_month_start": {
88+
"timezone": "Asia/Shanghai",
89+
"main_time": "45 15 1-7 * *",
90+
"probe_time": "35 9,15 1-7 * *",
91+
"precheck_time": "45 9 1-7 * *"
92+
},
93+
"crypto_intraday": {
94+
"timezone": "UTC",
95+
"main_time": "0,30 * * * *",
96+
"probe_time": "0 6,18 * * *",
97+
"precheck_time": "55 5 * * *"
98+
}
4399
}
44100
},
45101
"platforms": {
@@ -353,6 +409,7 @@
353409
"label": "纳指标普定投",
354410
"label_en": "NASDAQ/S&P 500 DCA",
355411
"domain": "us_equity",
412+
"scheduler_profile": "us_dca_month_end",
356413
"runtime_enabled": true,
357414
"lifecycle_stage": "runtime_enabled",
358415
"can_switch_live": true,
@@ -377,6 +434,10 @@
377434
"label": "IBIT比特币定投",
378435
"label_en": "IBIT Bitcoin DCA",
379436
"domain": "us_equity",
437+
"scheduler_profile": "us_dca_month_end",
438+
"scheduler_profile_by_plugin": {
439+
"ibit_zscore_exit": "us_daily"
440+
},
380441
"runtime_enabled": true,
381442
"lifecycle_stage": "runtime_enabled",
382443
"can_switch_live": true,
@@ -440,6 +501,7 @@
440501
"label": "罗素Top50领涨",
441502
"label_en": "Russell Top50 Leaders",
442503
"domain": "us_equity",
504+
"scheduler_profile": "us_snapshot_month_start",
443505
"runtime_enabled": true,
444506
"lifecycle_stage": "runtime_enabled",
445507
"can_switch_live": true,
@@ -596,6 +658,7 @@
596658
"label": "港股红利质量",
597659
"label_en": "HK Dividend Quality",
598660
"domain": "hk_equity",
661+
"scheduler_profile": "hk_snapshot_month_start",
599662
"runtime_enabled": true,
600663
"lifecycle_stage": "runtime_enabled",
601664
"can_switch_live": true,
@@ -728,6 +791,7 @@
728791
"label": "A股红利质量",
729792
"label_en": "CN Dividend Quality",
730793
"domain": "cn_equity",
794+
"scheduler_profile": "cn_snapshot_month_start",
731795
"runtime_enabled": false,
732796
"lifecycle_stage": "research_backtest_only",
733797
"can_switch_live": false,
@@ -746,6 +810,7 @@
746810
"label": "创业板成长质量快照",
747811
"label_en": "CN ChiNext Growth Quality Snapshot",
748812
"domain": "cn_equity",
813+
"scheduler_profile": "cn_snapshot_month_start",
749814
"runtime_enabled": false,
750815
"lifecycle_stage": "research_backtest_only",
751816
"can_switch_live": false,

python/scripts/build_config.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import subprocess
2020
import sys
2121
from pathlib import Path
22+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
2223

2324
ROOT = Path(__file__).resolve().parents[2]
2425
CONFIG_PATH = ROOT / "platform-config.json"
@@ -36,6 +37,7 @@
3637
"allowed_execution_modes",
3738
"blocked_live_reason",
3839
}
40+
SCHEDULER_FIELDS = {"timezone", "main_time", "probe_time", "precheck_time"}
3941

4042

4143
def load_config() -> dict:
@@ -45,16 +47,84 @@ def load_config() -> dict:
4547

4648
def validate(config: dict) -> list[str]:
4749
errors: list[str] = []
50+
scheduling = config.get("scheduling")
51+
scheduler_profiles = scheduling.get("profiles") if isinstance(scheduling, dict) else None
52+
if not isinstance(scheduler_profiles, dict) or not scheduler_profiles:
53+
errors.append("scheduling.profiles must be a non-empty object")
54+
scheduler_profiles = {}
55+
for profile, scheduler in scheduler_profiles.items():
56+
if not isinstance(scheduler, dict):
57+
errors.append(f"scheduler profile {profile}: must be an object")
58+
continue
59+
if set(scheduler) != SCHEDULER_FIELDS:
60+
errors.append(
61+
f"scheduler profile {profile}: fields must be {sorted(SCHEDULER_FIELDS)}"
62+
)
63+
continue
64+
timezone = scheduler.get("timezone")
65+
try:
66+
ZoneInfo(str(timezone or ""))
67+
except (ZoneInfoNotFoundError, ValueError):
68+
errors.append(f"scheduler profile {profile}: invalid timezone {timezone!r}")
69+
for field in SCHEDULER_FIELDS - {"timezone"}:
70+
value = scheduler.get(field)
71+
if not isinstance(value, str) or len(value.split()) not in {2, 5}:
72+
errors.append(
73+
f"scheduler profile {profile}: {field} must have 2 time fields or 5 cron fields"
74+
)
4875
for pid, pdata in config.get("platforms", {}).items():
4976
if "capabilities" not in pdata:
5077
errors.append(f"platform {pid}: missing capabilities")
5178
if "default_account" not in pdata:
5279
errors.append(f"platform {pid}: missing default_account")
5380
if "supported_domains" not in pdata:
5481
errors.append(f"platform {pid}: missing supported_domains")
82+
domains = config.get("domains", {})
83+
for domain, domain_data in domains.items():
84+
if not isinstance(domain_data, dict):
85+
errors.append(f"domain {domain}: must be an object")
86+
continue
87+
scheduler_profile = domain_data.get("scheduler_profile")
88+
if not isinstance(scheduler_profile, str) or scheduler_profile not in scheduler_profiles:
89+
errors.append(
90+
f"domain {domain}: unknown scheduler_profile {scheduler_profile!r}"
91+
)
5592
for sid, sdata in config.get("strategies", {}).items():
5693
if "domain" not in sdata:
5794
errors.append(f"strategy {sid}: missing domain")
95+
continue
96+
domain_ref = sdata["domain"]
97+
if not isinstance(domain_ref, str):
98+
errors.append(f"strategy {sid}: domain must be a string")
99+
domain_data = {}
100+
elif domain_ref not in domains:
101+
errors.append(f"strategy {sid}: unknown domain {domain_ref!r}")
102+
domain_data = {}
103+
else:
104+
domain_data = domains[domain_ref]
105+
if not isinstance(domain_data, dict):
106+
domain_data = {}
107+
strategy_scheduler_profile = sdata.get("scheduler_profile")
108+
if strategy_scheduler_profile is not None and not isinstance(strategy_scheduler_profile, str):
109+
errors.append(
110+
f"strategy {sid}: scheduler_profile must be a string"
111+
)
112+
scheduler_profile = None
113+
else:
114+
scheduler_profile = strategy_scheduler_profile or domain_data.get("scheduler_profile")
115+
if not isinstance(scheduler_profile, str) or scheduler_profile not in scheduler_profiles:
116+
errors.append(
117+
f"strategy {sid}: unknown scheduler_profile {scheduler_profile!r}"
118+
)
119+
plugin_overrides = sdata.get("scheduler_profile_by_plugin", {})
120+
if not isinstance(plugin_overrides, dict):
121+
errors.append(f"strategy {sid}: scheduler_profile_by_plugin must be an object")
122+
else:
123+
for plugin, override in plugin_overrides.items():
124+
if not isinstance(override, str) or override not in scheduler_profiles:
125+
errors.append(
126+
f"strategy {sid}: plugin {plugin} references unknown scheduler_profile {override!r}"
127+
)
58128
return errors
59129

60130

0 commit comments

Comments
 (0)