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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ python3 scripts/runtime_settings.py apply --yes local/targets/longbridge/sg.json

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

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`.

## Architecture

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

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

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

## 架构

这个仓库在策略选择和平台部署之间提供一个轻量 bridge,同时避免公开真实运行分配:
Expand Down
14 changes: 13 additions & 1 deletion examples/targets/ibkr/default.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@
"account_selector": ["example-default"],
"account_scope": "example-default",
"service_name": "example-ibkr-service",
"execution_mode": "live"
"execution_mode": "live",
"execution_windows": {
"precheck": {
"enabled": true,
"offset_minutes": 15,
"mode": "notify_only"
},
"execution": {
"enabled": true,
"offset_minutes": 15,
"mode": "live"
}
}
},
"plugin_mounts_variable": "IBKR_STRATEGY_PLUGIN_MOUNTS_JSON",
"plugin_mounts": [
Expand Down
14 changes: 13 additions & 1 deletion examples/targets/longbridge/sg.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@
"account_selector": ["EXAMPLE"],
"account_scope": "EXAMPLE",
"service_name": "example-longbridge-service",
"execution_mode": "live"
"execution_mode": "live",
"execution_windows": {
"precheck": {
"enabled": true,
"offset_minutes": 15,
"mode": "notify_only"
},
"execution": {
"enabled": true,
"offset_minutes": 15,
"mode": "live"
}
}
},
"plugin_mounts_variable": "LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON",
"plugin_mounts": [
Expand Down
14 changes: 13 additions & 1 deletion examples/targets/schwab/live.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@
"account_selector": ["example-schwab"],
"account_scope": "example-schwab",
"service_name": "example-schwab-service",
"execution_mode": "live"
"execution_mode": "live",
"execution_windows": {
"precheck": {
"enabled": true,
"offset_minutes": 15,
"mode": "notify_only"
},
"execution": {
"enabled": true,
"offset_minutes": 15,
"mode": "live"
}
}
},
"plugin_mounts_variable": "SCHWAB_STRATEGY_PLUGIN_MOUNTS_JSON",
"plugin_mounts": [
Expand Down
41 changes: 40 additions & 1 deletion schemas/runtime-target.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,46 @@
"execution_mode": {
"type": "string",
"enum": ["live", "paper", "dry_run"]
},
"execution_windows": {
"type": "object",
"additionalProperties": false,
"properties": {
"precheck": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"offset_minutes": {
"type": "integer",
"minimum": 0
},
"mode": {
"type": "string",
"enum": ["notify_only", "dry_run"]
}
}
},
"execution": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"offset_minutes": {
"type": "integer",
"minimum": 0
},
"mode": {
"type": "string",
"enum": ["live", "paper", "dry_run"]
}
}
}
}
}
}
},
Expand Down Expand Up @@ -115,4 +155,3 @@
}
}
}

43 changes: 43 additions & 0 deletions scripts/runtime_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"service_name",
"execution_mode",
)
WINDOW_MODES = {
"precheck": {"notify_only", "dry_run"},
"execution": {"live", "paper", "dry_run"},
}
GENERATED_VARIABLES = {"RUNTIME_TARGET_JSON", "STRATEGY_PROFILE"}
SECRET_MARKERS = ("PASSWORD", "PRIVATE_KEY", "TOKEN", "API_KEY")

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

execution_windows = runtime_target.get("execution_windows")
if execution_windows is not None:
if not isinstance(execution_windows, dict):
errors.append("runtime_target.execution_windows must be an object when present")
else:
for window_name, allowed_modes in WINDOW_MODES.items():
window = execution_windows.get(window_name)
if window is None:
continue
if not isinstance(window, dict):
errors.append(f"runtime_target.execution_windows.{window_name} must be an object")
continue
for field in window:
if field not in {"enabled", "offset_minutes", "mode"}:
errors.append(
f"runtime_target.execution_windows.{window_name}.{field} is unsupported"
)
if "enabled" in window and not isinstance(window["enabled"], bool):
errors.append(
f"runtime_target.execution_windows.{window_name}.enabled must be boolean"
)
if "offset_minutes" in window:
offset_minutes = window["offset_minutes"]
if not isinstance(offset_minutes, int) or offset_minutes < 0:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject boolean values for offset_minutes

offset_minutes is intended to be numeric, but this check treats booleans as valid because bool is a subclass of int in Python. As a result, configs like "offset_minutes": true pass validate_target() even though the schema declares this field as an integer, so invalid runtime targets can be accepted and propagated to downstream systems.

Useful? React with 👍 / 👎.

errors.append(
f"runtime_target.execution_windows.{window_name}.offset_minutes must be a non-negative integer"
)
mode = window.get("mode")
if mode is not None and mode not in allowed_modes:
errors.append(
f"runtime_target.execution_windows.{window_name}.mode must be one of {sorted(allowed_modes)}"
)
for window_name in execution_windows:
if window_name not in WINDOW_MODES:
errors.append(
"runtime_target.execution_windows only supports precheck and execution"
)
break


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