SniperGold_ML/engine/config.py

128 lines
No EOL
5.2 KiB
Python

"""Strict engine configuration (spec 6.3).
Every identity-affecting field participates in ``config_sha256``; operational
fields are recorded in the snapshot but excluded from the hash. The exact
field classification is fixed in v1 and documented in the implementation map.
"""
import os
from . import versions
from .util import canonical_json_sha256
# Identity-affecting fields (participate in config_sha256). Changing any of
# these changes the reproducibility identity of produced data.
IDENTITY_FIELDS = (
"source_path",
"source_grammar", # amended P3-DE-004: grammar preset binding (identity)
"source_tz_offset_minutes",
"has_volume",
"timeframes",
"chunk_bytes_nominal",
"workload_bytes_nominal",
)
# Operational fields (in snapshot only, never in config_sha256).
OPERATIONAL_FIELDS = (
"output_root",
"workers_requested",
"checkpoint_every_workload",
"retry_limit",
"memory_limits_mb",
"disk_floor_bytes",
"certify_timeout_sec",
"reference_now_ms", # test override of certify time; operational
)
ALL_FIELDS = sorted(set(IDENTITY_FIELDS) | set(OPERATIONAL_FIELDS))
class ConfigError(Exception):
pass
def _check(cond, msg):
if not cond:
raise ConfigError(msg)
def default_config(source_path, output_root=None, source_tz_offset_minutes=0,
**overrides):
"""Produce a fully-populated config dict with v1 defaults.
``source_tz_offset_minutes`` is an explicit, call-site established value
(G-6: never silently invented; production value must be established at
source initialization in the pilot phase). Synthetic fixtures use 0 (UTC).
``source_grammar`` (amended P3-DE-004) defaults to the dotted synthetic/
corpus preset; the production preset ``tickstory_mt5`` is set explicitly
for the real six-column Tickstory MT5 source and is validated against the
certificate ``grammar_id`` at init (fail-closed; never silent).
"""
cfg = {
"source_path": os.path.abspath(source_path),
"source_grammar": versions.GRAMMAR_DOTTED,
"source_tz_offset_minutes": int(source_tz_offset_minutes),
"has_volume": False,
"timeframes": list(versions.DEFAULT_TIMEFRAMES),
"chunk_bytes_nominal": versions.CHUNK_BYTES_NOMINAL,
"workload_bytes_nominal": versions.WORKLOAD_BYTES_NOMINAL,
"output_root": output_root,
"workers_requested": versions.WORKERS_REQUESTED_DEFAULT,
"checkpoint_every_workload": True,
"retry_limit": 2,
"memory_limits_mb": {"parent": 512, "worker": 1024},
"disk_floor_bytes": 4 * 2 ** 30,
"certify_timeout_sec": 3600,
"reference_now_ms": None,
}
for k, v in overrides.items():
if k not in ALL_FIELDS:
raise ConfigError("unknown config field %r" % k)
cfg[k] = v
validate_config(cfg)
return cfg
def validate_config(cfg):
_check(isinstance(cfg, dict), "config must be a JSON object")
_check(isinstance(cfg.get("source_path"), str) and os.path.isabs(cfg["source_path"]),
"source_path must be an absolute path")
grammar = cfg.get("source_grammar", versions.GRAMMAR_DOTTED)
_check(grammar in versions.SUPPORTED_GRAMMARS,
"source_grammar must be one of %s" % (versions.SUPPORTED_GRAMMARS,))
_check(isinstance(cfg.get("source_tz_offset_minutes"), int),
"source_tz_offset_minutes must be an integer")
_check(isinstance(cfg.get("has_volume"), bool), "has_volume must be bool")
_check(not (grammar == versions.GRAMMAR_TICKSTORY_MT5 and not cfg.get("has_volume")),
"grammar tickstory_mt5 requires has_volume=true (mandatory volume column)")
tfs = cfg.get("timeframes")
_check(isinstance(tfs, list) and len(tfs) >= 1, "timeframes must be a non-empty list")
_check(all(tf in versions.TIMEFRAMES for tf in tfs), "timeframe not in v1 set M1/M5/M15/M30/H1")
_check(len(set(tfs)) == len(tfs), "duplicate timeframe in timeframes")
_check(isinstance(cfg.get("chunk_bytes_nominal"), int) and cfg["chunk_bytes_nominal"] >= 1,
"chunk_bytes_nominal must be a positive integer")
_check(isinstance(cfg.get("workload_bytes_nominal"), int)
and cfg["workload_bytes_nominal"] >= cfg["chunk_bytes_nominal"],
"workload_bytes_nominal must be >= chunk_bytes_nominal")
_check(isinstance(cfg.get("workers_requested"), int) and cfg["workers_requested"] >= 1,
"workers_requested must be >= 1")
_check(isinstance(cfg.get("retry_limit"), int) and cfg["retry_limit"] >= 1,
"retry_limit must be >= 1")
for f in ("output_root",):
v = cfg.get(f)
_check(v is None or (isinstance(v, str) and os.path.isabs(v)),
"%s must be an absolute path or null" % f)
if "timeframes" in cfg:
cfg["timeframes"] = [tf for tf in versions.DEFAULT_TIMEFRAMES if tf in tfs]
def config_sha256(cfg):
"""Hash over the identity-affecting subset only (spec 6.3)."""
id_subset = {k: cfg[k] for k in IDENTITY_FIELDS if k in cfg}
return canonical_json_sha256(id_subset)
def config_snapshot(cfg):
"""Full config snapshot recorded in manifests (identity + operational)."""
return {k: cfg[k] for k in ALL_FIELDS if k in cfg}