Skip to content

Commit d0c555e

Browse files
Pigbibiclaude
andcommitted
feat(common): add runtime_config helpers and engineering standards
- Add resolve_optional_bool_env, resolve_optional_ratio_env, resolve_optional_positive_float_env, resolve_optional_dca_mode_env, resolve_optional_ibit_zscore_exit_mode_env, resolve_optional_symbol_env, resolve_optional_int, resolve_split_env_list to runtime_config.py - Rename old mapping-based resolve_optional_bool_env to _resolve_optional_bool_env - Add coverage config to pyproject.toml - Move ruff lint config to [tool.ruff.lint] - Add pytest-cov to CI workflow - Add uv.lock Co-Authored-By: Claude <noreply@anthropic.com>
1 parent bde4ca0 commit d0c555e

4 files changed

Lines changed: 135 additions & 7 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
run: |
2626
set -euo pipefail
2727
python -m pip install --upgrade pip
28-
python -m pip install -e . numpy pandas pytest ruff
28+
python -m pip install -e . numpy pandas pytest pytest-cov ruff
2929
3030
- name: Run Ruff
3131
run: |
@@ -35,4 +35,4 @@ jobs:
3535
- name: Run unit tests
3636
run: |
3737
set -euo pipefail
38-
PYTHONPATH=src python -m pytest -q tests
38+
PYTHONPATH=src python -m pytest -q --cov --cov-report=term --cov-report=xml tests

‎pyproject.toml‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,15 @@ package-dir = { "" = "src" }
2121
where = ["src"]
2222

2323
[tool.ruff]
24-
ignore = ["E701", "E702", "E741", "F841", "F821", "B008", "F401", "F601", "E402"]
2524
target-version = "py310"
25+
26+
[tool.ruff.lint]
27+
ignore = ["E701", "E702", "E741", "F841", "F821", "B008", "F401", "F601", "E402"]
28+
29+
[tool.coverage.run]
30+
branch = true
31+
source = ["src"]
32+
omit = ["tests/*", "*/external/*"]
33+
34+
[tool.coverage.report]
35+
show_missing = true

‎src/quant_platform_kit/common/runtime_config.py‎

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from dataclasses import dataclass
45
from pathlib import Path
56
from typing import Mapping
@@ -39,10 +40,11 @@ def resolve_bool_value(raw_value: str | None) -> bool:
3940
return str(raw_value or "").strip().lower() in {"1", "true", "yes", "y", "on"}
4041

4142

42-
def resolve_optional_bool_env(
43+
def _resolve_optional_bool_env(
4344
env: Mapping[str, str | None],
4445
name: str,
4546
) -> bool | None:
47+
"""Internal helper: resolve bool from a Mapping (not os.environ)."""
4648
raw_value = env.get(name)
4749
if raw_value is None or str(raw_value).strip() == "":
4850
return None
@@ -65,7 +67,7 @@ def resolve_cash_only_execution_env(
6567
if legacy:
6668
candidates.append(legacy)
6769
for name in candidates:
68-
value = resolve_optional_bool_env(env, name)
70+
value = _resolve_optional_bool_env(env, name)
6971
if value is not None:
7072
return value
7173
return default
@@ -124,6 +126,116 @@ def resolve_quantity_step_env(
124126
return float(fractional_step) if fractional_enabled else 1.0
125127

126128

129+
def resolve_optional_bool_env(name: str, default: bool = False) -> bool:
130+
"""Read env var *name* and parse as bool. Returns *default* when unset/empty."""
131+
raw_value = os.getenv(name)
132+
if raw_value is None or str(raw_value).strip() == "":
133+
return default
134+
value = str(raw_value).strip().lower()
135+
if value in {"1", "true", "yes", "y", "on"}:
136+
return True
137+
if value in {"0", "false", "no", "n", "off"}:
138+
return False
139+
raise ValueError(f"{name} must be boolean, got {raw_value!r}")
140+
141+
142+
def resolve_optional_ratio_env(name: str, default: float | None = None) -> float | None:
143+
"""Read env var *name* and parse as a float ratio in [0, 1]. Returns *default* when unset/empty."""
144+
raw_value = os.getenv(name)
145+
if raw_value is None or str(raw_value).strip() == "":
146+
return default
147+
value = float(raw_value)
148+
if not (0.0 <= value <= 1.0):
149+
raise ValueError(f"{name} must be in [0,1], got {value}")
150+
return value
151+
152+
153+
def resolve_optional_positive_float_env(name: str, default: float | None = None) -> float | None:
154+
"""Read env var *name* and parse as a positive float. Returns *default* when unset/empty."""
155+
raw_value = os.getenv(name)
156+
if raw_value is None or str(raw_value).strip() == "":
157+
return default
158+
value = float(raw_value)
159+
if value <= 0.0:
160+
raise ValueError(f"{name} must be positive, got {value}")
161+
return value
162+
163+
164+
def resolve_optional_dca_mode_env(name: str, default: str | None = None) -> str | None:
165+
"""Read env var *name* and resolve to 'fixed', 'smart', or *default* when unset."""
166+
raw_value = os.getenv(name)
167+
if raw_value is None or str(raw_value).strip() == "":
168+
return default
169+
value = str(raw_value).strip().lower()
170+
aliases = {
171+
"ordinary": "fixed",
172+
"ordinary_dca": "fixed",
173+
"fixed_dca": "fixed",
174+
"smart_dca": "smart",
175+
}
176+
mode = aliases.get(value, value)
177+
if mode not in {"fixed", "smart"}:
178+
raise ValueError(f"{name} must be fixed or smart, got {raw_value!r}")
179+
return mode
180+
181+
182+
def resolve_optional_ibit_zscore_exit_mode_env(name: str, default: str | None = None) -> str | None:
183+
"""Read env var *name* and resolve to 'disabled', 'paper', 'live', or *default* when unset."""
184+
raw_value = os.getenv(name)
185+
if raw_value is None or str(raw_value).strip() == "":
186+
return default
187+
value = str(raw_value).strip().lower()
188+
aliases = {
189+
"off": "disabled",
190+
"none": "disabled",
191+
"false": "disabled",
192+
"disable": "disabled",
193+
"enabled": "live",
194+
"shadow": "paper",
195+
"dry_run": "paper",
196+
"dry-run": "paper",
197+
}
198+
mode = aliases.get(value, value)
199+
if mode not in {"disabled", "paper", "live"}:
200+
raise ValueError(f"{name} must be disabled, paper, or live, got {raw_value!r}")
201+
return mode
202+
203+
204+
def resolve_optional_symbol_env(name: str, default: str | None = None) -> str | None:
205+
"""Read env var *name* and validate as a ticker symbol. Returns *default* when unset/empty."""
206+
raw_value = os.getenv(name)
207+
if raw_value is None or str(raw_value).strip() == "":
208+
return default
209+
value = str(raw_value).strip().upper()
210+
if len(value) > 16 or not value.replace(".", "").replace("-", "").isalnum():
211+
raise ValueError(f"{name} must be a symbol, got {raw_value!r}")
212+
return value
213+
214+
215+
def resolve_optional_int(name: str, default: int | None = None) -> int | None:
216+
"""Read env var *name* and parse as int. Returns *default* when unset/empty."""
217+
raw_value = os.getenv(name)
218+
if raw_value is None or str(raw_value).strip() == "":
219+
return default
220+
return int(raw_value)
221+
222+
223+
def resolve_split_env_list(name: str, separator: str = ",") -> tuple[str, ...]:
224+
"""Read env var *name*, split by *separator*, return deduplicated tuple."""
225+
raw_value = os.getenv(name)
226+
if raw_value is None:
227+
return ()
228+
items = []
229+
seen = set()
230+
for value in str(raw_value).replace(";", ",").replace("\n", ",").split(separator):
231+
item = value.strip()
232+
if not item or item in seen:
233+
continue
234+
items.append(item)
235+
seen.add(item)
236+
return tuple(items)
237+
238+
127239
def resolve_strategy_config_path(
128240
*,
129241
explicit_path: str | None,

‎uv.lock‎

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)