Skip to content

Commit 6f1730b

Browse files
committed
feat: add execution windows to runtime settings
1 parent c7e45fb commit 6f1730b

6 files changed

Lines changed: 126 additions & 4 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ python3 scripts/runtime_settings.py apply --yes local/targets/longbridge/sg.json
5454

5555
`RUNTIME_TARGET_JSON` is canonical. Compatibility variables such as `STRATEGY_PROFILE` are generated from it so they cannot drift independently.
5656

57+
For daily strategies that want both a precheck pass and an execution pass, declare them in `runtime_target.execution_windows`. Keep the strategy logic unchanged; let the platform layer decide whether a window is `notify_only`, `dry_run`, `paper`, or `live`.
58+
5759
## Architecture
5860

5961
This repo acts as a small bridge between strategy selection and platform deployment without exposing live assignments:
@@ -116,6 +118,8 @@ python3 scripts/runtime_settings.py apply --yes local/targets/longbridge/sg.json
116118

117119
`RUNTIME_TARGET_JSON` 是唯一 canonical source。兼容变量,例如 `STRATEGY_PROFILE`,由它生成,避免多个配置源互相漂移。
118120

121+
对于希望同时有预检和执行两次运行的日频策略,可以在 `runtime_target.execution_windows` 里显式声明两个窗口。策略逻辑保持不变,由平台层决定某个窗口是 `notify_only``dry_run``paper` 还是 `live`
122+
119123
## 架构
120124

121125
这个仓库在策略选择和平台部署之间提供一个轻量 bridge,同时避免公开真实运行分配:

examples/targets/ibkr/default.example.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@
1414
"account_selector": ["example-default"],
1515
"account_scope": "example-default",
1616
"service_name": "example-ibkr-service",
17-
"execution_mode": "live"
17+
"execution_mode": "live",
18+
"execution_windows": {
19+
"precheck": {
20+
"enabled": true,
21+
"offset_minutes": 15,
22+
"mode": "notify_only"
23+
},
24+
"execution": {
25+
"enabled": true,
26+
"offset_minutes": 15,
27+
"mode": "live"
28+
}
29+
}
1830
},
1931
"plugin_mounts_variable": "IBKR_STRATEGY_PLUGIN_MOUNTS_JSON",
2032
"plugin_mounts": [

examples/targets/longbridge/sg.example.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,19 @@
1515
"account_selector": ["EXAMPLE"],
1616
"account_scope": "EXAMPLE",
1717
"service_name": "example-longbridge-service",
18-
"execution_mode": "live"
18+
"execution_mode": "live",
19+
"execution_windows": {
20+
"precheck": {
21+
"enabled": true,
22+
"offset_minutes": 15,
23+
"mode": "notify_only"
24+
},
25+
"execution": {
26+
"enabled": true,
27+
"offset_minutes": 15,
28+
"mode": "live"
29+
}
30+
}
1931
},
2032
"plugin_mounts_variable": "LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON",
2133
"plugin_mounts": [

examples/targets/schwab/live.example.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@
1414
"account_selector": ["example-schwab"],
1515
"account_scope": "example-schwab",
1616
"service_name": "example-schwab-service",
17-
"execution_mode": "live"
17+
"execution_mode": "live",
18+
"execution_windows": {
19+
"precheck": {
20+
"enabled": true,
21+
"offset_minutes": 15,
22+
"mode": "notify_only"
23+
},
24+
"execution": {
25+
"enabled": true,
26+
"offset_minutes": 15,
27+
"mode": "live"
28+
}
29+
}
1830
},
1931
"plugin_mounts_variable": "SCHWAB_STRATEGY_PLUGIN_MOUNTS_JSON",
2032
"plugin_mounts": [

schemas/runtime-target.schema.json

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,46 @@
7676
"execution_mode": {
7777
"type": "string",
7878
"enum": ["live", "paper", "dry_run"]
79+
},
80+
"execution_windows": {
81+
"type": "object",
82+
"additionalProperties": false,
83+
"properties": {
84+
"precheck": {
85+
"type": "object",
86+
"additionalProperties": false,
87+
"properties": {
88+
"enabled": {
89+
"type": "boolean"
90+
},
91+
"offset_minutes": {
92+
"type": "integer",
93+
"minimum": 0
94+
},
95+
"mode": {
96+
"type": "string",
97+
"enum": ["notify_only", "dry_run"]
98+
}
99+
}
100+
},
101+
"execution": {
102+
"type": "object",
103+
"additionalProperties": false,
104+
"properties": {
105+
"enabled": {
106+
"type": "boolean"
107+
},
108+
"offset_minutes": {
109+
"type": "integer",
110+
"minimum": 0
111+
},
112+
"mode": {
113+
"type": "string",
114+
"enum": ["live", "paper", "dry_run"]
115+
}
116+
}
117+
}
118+
}
79119
}
80120
}
81121
},
@@ -115,4 +155,3 @@
115155
}
116156
}
117157
}
118-

scripts/runtime_settings.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
"service_name",
3333
"execution_mode",
3434
)
35+
WINDOW_MODES = {
36+
"precheck": {"notify_only", "dry_run"},
37+
"execution": {"live", "paper", "dry_run"},
38+
}
3539
GENERATED_VARIABLES = {"RUNTIME_TARGET_JSON", "STRATEGY_PROFILE"}
3640
SECRET_MARKERS = ("PASSWORD", "PRIVATE_KEY", "TOKEN", "API_KEY")
3741

@@ -168,6 +172,45 @@ def validate_runtime_target(target: dict[str, Any], errors: list[str]) -> None:
168172
if execution_mode not in {"live", "paper", "dry_run"}:
169173
errors.append("runtime_target.execution_mode must be live, paper, or dry_run")
170174

175+
execution_windows = runtime_target.get("execution_windows")
176+
if execution_windows is not None:
177+
if not isinstance(execution_windows, dict):
178+
errors.append("runtime_target.execution_windows must be an object when present")
179+
else:
180+
for window_name, allowed_modes in WINDOW_MODES.items():
181+
window = execution_windows.get(window_name)
182+
if window is None:
183+
continue
184+
if not isinstance(window, dict):
185+
errors.append(f"runtime_target.execution_windows.{window_name} must be an object")
186+
continue
187+
for field in window:
188+
if field not in {"enabled", "offset_minutes", "mode"}:
189+
errors.append(
190+
f"runtime_target.execution_windows.{window_name}.{field} is unsupported"
191+
)
192+
if "enabled" in window and not isinstance(window["enabled"], bool):
193+
errors.append(
194+
f"runtime_target.execution_windows.{window_name}.enabled must be boolean"
195+
)
196+
if "offset_minutes" in window:
197+
offset_minutes = window["offset_minutes"]
198+
if not isinstance(offset_minutes, int) or offset_minutes < 0:
199+
errors.append(
200+
f"runtime_target.execution_windows.{window_name}.offset_minutes must be a non-negative integer"
201+
)
202+
mode = window.get("mode")
203+
if mode is not None and mode not in allowed_modes:
204+
errors.append(
205+
f"runtime_target.execution_windows.{window_name}.mode must be one of {sorted(allowed_modes)}"
206+
)
207+
for window_name in execution_windows:
208+
if window_name not in WINDOW_MODES:
209+
errors.append(
210+
"runtime_target.execution_windows only supports precheck and execution"
211+
)
212+
break
213+
171214

172215
def validate_plugin_mounts(target: dict[str, Any], errors: list[str]) -> None:
173216
runtime_target = target.get("runtime_target") if isinstance(target.get("runtime_target"), dict) else {}

0 commit comments

Comments
 (0)