diff --git a/.github/workflows/rootfile-runtime.yml b/.github/workflows/rootfile-runtime.yml index a1024e1..7fcc674 100644 --- a/.github/workflows/rootfile-runtime.yml +++ b/.github/workflows/rootfile-runtime.yml @@ -28,6 +28,9 @@ jobs: python -m pip install --upgrade pip python -m pip install -r requirements.txt + - name: Rootfile manifest validation + run: python -m tools.validate_rootfile --repo-root . + - name: Rootfile validators and token flow run: python -m pytest tests\rootfile -q diff --git a/core/economics/__init__.py b/core/economics/__init__.py new file mode 100644 index 0000000..f262e7a --- /dev/null +++ b/core/economics/__init__.py @@ -0,0 +1,5 @@ +"""Economics overlays for lawful-collapse artifacts.""" + +from core.economics.qpt_token import mint_qpt_if_applicable + +__all__ = ["mint_qpt_if_applicable"] diff --git a/core/economics/qpt_token.py b/core/economics/qpt_token.py new file mode 100644 index 0000000..9f12a5d --- /dev/null +++ b/core/economics/qpt_token.py @@ -0,0 +1,81 @@ +"""Disabled-by-default QPT minting for accepted lawful-collapse reports.""" + +from __future__ import annotations + +import hashlib +import json +import os +import time +from pathlib import Path +from typing import Optional + +from core.meta import OperatorMeta +from core.orchestration.reconciliation import ReconciliationReport + + +META = OperatorMeta( + tier="rootfile", + layer="core.economics", + operator_type="qpt_minter", + canonical_law="H12", +) + + +def _ledger_path() -> Path: + return Path(os.getenv("APEX_QPT_LEDGER", "data/qpt_ledger.jsonl")) + + +def _append_jsonl(path: Path, record: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + line = json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n" + with path.open("a", encoding="utf-8") as handle: + handle.write(line) + handle.flush() + os.fsync(handle.fileno()) + + +def _eligible(report: ReconciliationReport) -> bool: + threshold = float(os.getenv("QPT_INFORMATION_GAIN_THRESHOLD", "0.0") or 0.0) + return ( + report.accepted + and report.admissible + and report.information_gain >= threshold + and report.scheduler_authorized + and report.evidence_valid + ) + + +def mint_qpt_if_applicable(report: ReconciliationReport) -> Optional[str]: + """Persist a local QPT token ID only when all collapse proof gates pass.""" + if os.getenv("ENABLE_QPT", "0") != "1": + return None + if not _eligible(report): + return None + + timestamp = time.time() + identity = { + "broker": report.broker, + "execution_id": report.execution_id, + "information_gain": report.information_gain, + "realized_pnl": report.realized_pnl, + "status": report.status, + "symbol": report.symbol, + "timestamp": timestamp, + } + token_id = "qpt_" + hashlib.sha256( + json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest()[:24] + _append_jsonl( + _ledger_path(), + { + "token_id": token_id, + **identity, + "conditions": { + "admissible": report.admissible, + "evidence_valid": report.evidence_valid, + "reconciliation_accepted": report.accepted, + "scheduler_authorized": report.scheduler_authorized, + }, + }, + ) + return token_id diff --git a/core/meta.py b/core/meta.py new file mode 100644 index 0000000..3b98c95 --- /dev/null +++ b/core/meta.py @@ -0,0 +1,48 @@ +"""Operator metadata helpers for rootfile law introspection.""" + +from __future__ import annotations + +from typing import Any, Callable, Mapping, NamedTuple, TypeVar + + +class OperatorMeta(NamedTuple): + """Canonical operator metadata attached to modules, classes, or functions.""" + + tier: str + layer: str + operator_type: str + canonical_law: str = "" + + +T = TypeVar("T") + + +def normalize_meta(meta: OperatorMeta | Mapping[str, Any]) -> OperatorMeta: + """Convert legacy dict metadata or OperatorMeta into OperatorMeta.""" + if isinstance(meta, OperatorMeta): + return meta + return OperatorMeta( + tier=str(meta.get("tier", "")), + layer=str(meta.get("layer", "")), + operator_type=str(meta.get("operator_type", "")), + canonical_law=str(meta.get("canonical_law", "")), + ) + + +def declare_operator(meta: OperatorMeta | Mapping[str, Any]) -> Callable[[T], T]: + """Attach canonical rootfile metadata to a function or class.""" + normalized = normalize_meta(meta) + + def decorator(obj: T) -> T: + setattr(obj, "__operator_meta__", normalized) + return obj + + return decorator + + +def get_declared_meta(obj: Any) -> OperatorMeta | None: + """Return metadata declared with @declare_operator, if present.""" + meta = getattr(obj, "__operator_meta__", None) + if meta is None: + return None + return normalize_meta(meta) diff --git a/core/orchestration/__init__.py b/core/orchestration/__init__.py index 7b6aea3..79bda4d 100644 --- a/core/orchestration/__init__.py +++ b/core/orchestration/__init__.py @@ -4,11 +4,15 @@ from trading.kernel.apex_engine import ApexEngine, ExecutionMode, ExecutionOutcome, ExecutionResult from trading.kernel.scheduler import CollapseDecision, ExecutionToken, Scheduler -META = { - "tier": "rootfile", - "layer": "core.orchestration", - "operator_type": "orchestration_adapter", -} +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="orchestration_adapter", + canonical_law="H10", +) __all__ = [ "ApexEngine", diff --git a/core/orchestration/apex_engine.py b/core/orchestration/apex_engine.py index 29e7d5a..43ebdd6 100644 --- a/core/orchestration/apex_engine.py +++ b/core/orchestration/apex_engine.py @@ -2,9 +2,13 @@ from trading.kernel.apex_engine import * # noqa: F401,F403 -META = { - "tier": "rootfile", - "layer": "core.orchestration", - "operator_type": "engine_adapter", -} +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="engine_adapter", + canonical_law="H10", +) diff --git a/core/orchestration/constraints.py b/core/orchestration/constraints.py index 91bb49f..0a1c823 100644 --- a/core/orchestration/constraints.py +++ b/core/orchestration/constraints.py @@ -2,9 +2,13 @@ from trading.kernel.H_constraints import * # noqa: F401,F403 -META = { - "tier": "rootfile", - "layer": "core.orchestration", - "operator_type": "constraint_adapter", -} +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="constraint_adapter", + canonical_law="H8", +) diff --git a/core/orchestration/evidence.py b/core/orchestration/evidence.py new file mode 100644 index 0000000..4dcebcf --- /dev/null +++ b/core/orchestration/evidence.py @@ -0,0 +1,58 @@ +"""Unified H13 evidence facade.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + +from core.meta import OperatorMeta +from tachyonic_chain.audit_log import append_execution_evidence + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="evidence_facade", + canonical_law="H13", +) + + +@dataclass +class EvidenceEvent: + """Canonical evidence event accepted by the facade.""" + + event_type: str + execution_id: str + operation: str + payload: Dict[str, Any] = field(default_factory=dict) + symbol: Optional[str] = None + outcome: str = "recorded" + token_status: str = "not_applicable" + + +def _external_anchor_status(payload: Dict[str, Any]) -> Dict[str, Any]: + """Return optional external anchor status without blocking pipeline work.""" + if os.getenv("ENABLE_ANCHORING", "0") != "1": + return {"anchor_status": "disabled"} + endpoint = os.getenv("APEX_ANCHOR_ENDPOINT", "").strip() + if not endpoint: + return {"anchor_status": "failed", "anchor_error": "missing_endpoint"} + # Network anchoring is deliberately not performed in this offline-safe adapter. + return {"anchor_status": "configured", "anchor_endpoint": endpoint, "anchor_txid": None} + + +def emit_evidence(event: EvidenceEvent, *, log_path: str | None = None) -> str: + """Append runtime evidence and include optional non-blocking anchor status.""" + payload = dict(event.payload) + payload.update(_external_anchor_status(payload)) + return append_execution_evidence( + event_type=event.event_type, + execution_id=event.execution_id, + operation=event.operation, + symbol=event.symbol, + outcome=event.outcome, + token_status=event.token_status, + payload=payload, + log_path=log_path, + ) diff --git a/core/orchestration/meta.py b/core/orchestration/meta.py new file mode 100644 index 0000000..c31504e --- /dev/null +++ b/core/orchestration/meta.py @@ -0,0 +1,42 @@ +"""H-meta registry for rootfile operator jurisdiction.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable + +from core.meta import OperatorMeta, normalize_meta +from core.rootfile_manifest import LAWS + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="operator_registry", + canonical_law="H10", +) + + +@dataclass +class OperatorRegistry: + """In-memory registry of operator metadata keyed by stable name.""" + + operators: Dict[str, OperatorMeta] = field(default_factory=dict) + + def register(self, name: str, meta: OperatorMeta | dict) -> OperatorMeta: + normalized = normalize_meta(meta) + if normalized.canonical_law and normalized.canonical_law not in LAWS: + raise ValueError(f"{name} declares unknown canonical law {normalized.canonical_law}") + self.operators[name] = normalized + return normalized + + def register_many(self, items: Iterable[tuple[str, OperatorMeta | dict]]) -> None: + for name, meta in items: + self.register(name, meta) + + def assert_jurisdiction(self, name: str, law_id: str) -> None: + meta = self.operators.get(name) + if meta is None: + raise KeyError(f"operator not registered: {name}") + if meta.canonical_law and meta.canonical_law != law_id: + raise PermissionError(f"{name} belongs to {meta.canonical_law}, not {law_id}") diff --git a/core/orchestration/reconciliation.py b/core/orchestration/reconciliation.py new file mode 100644 index 0000000..39d8ab1 --- /dev/null +++ b/core/orchestration/reconciliation.py @@ -0,0 +1,51 @@ +"""Broker-neutral H12 reconciliation facade.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List + +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="reconciliation_facade", + canonical_law="H12", +) + + +@dataclass +class ReconciliationReport: + """Canonical reconciliation summary consumed by QPT/evidence layers.""" + + broker: str + accepted: bool + status: str + realized_pnl: float | None = None + evidence_valid: bool = False + admissible: bool = False + information_gain: float = 0.0 + scheduler_authorized: bool = False + execution_id: str = "" + symbol: str = "" + payload: Dict[str, Any] = field(default_factory=dict) + + +def summarize_settlement_report(broker: str, report: Any) -> ReconciliationReport: + """Convert existing MT5/Deriv settlement report objects into a canonical report.""" + records: List[Any] = list(getattr(report, "records", []) or []) + closed = [record for record in records if getattr(record, "status", "") == "closed"] + realized = [getattr(record, "realized_pnl", None) for record in closed] + realized_values = [float(value) for value in realized if value is not None] + return ReconciliationReport( + broker=broker, + accepted=bool(closed), + status="accepted" if closed else "no_closed_records", + realized_pnl=sum(realized_values) if realized_values else None, + payload={ + "record_count": len(records), + "closed_count": len(closed), + }, + ) diff --git a/core/orchestration/root.py b/core/orchestration/root.py new file mode 100644 index 0000000..d4ea67a --- /dev/null +++ b/core/orchestration/root.py @@ -0,0 +1,34 @@ +"""H-root runtime setup helpers for lawful-collapse state.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="root_runtime_setup", + canonical_law="H1", +) + + +DEFAULT_RUNTIME_DIRS = ( + "logs", + "data/models", + "trading_data/pnl", +) + + +def setup_runtime(root: str | Path = ".", dirs: Iterable[str] = DEFAULT_RUNTIME_DIRS) -> list[Path]: + """Create required runtime directories and return the created/resolved paths.""" + base = Path(root) + paths = [] + for rel in dirs: + path = base / rel + path.mkdir(parents=True, exist_ok=True) + paths.append(path) + return paths diff --git a/core/orchestration/scheduler.py b/core/orchestration/scheduler.py index 8349700..7930292 100644 --- a/core/orchestration/scheduler.py +++ b/core/orchestration/scheduler.py @@ -1,10 +1,20 @@ """Rootfile adapter for scheduler collapse authority.""" from trading.kernel.scheduler import * # noqa: F401,F403 +from trading.kernel.scheduler import Scheduler -META = { - "tier": "rootfile", - "layer": "core.orchestration", - "operator_type": "scheduler_adapter", -} +from core.meta import OperatorMeta + + +META = OperatorMeta( + tier="rootfile", + layer="core.orchestration", + operator_type="scheduler_adapter", + canonical_law="H10", +) + + +def authorize_collapse(scheduler: Scheduler, **kwargs): + """Explicit H10 lambda-law adapter over Scheduler.authorize_collapse.""" + return scheduler.authorize_collapse(**kwargs) diff --git a/core/rootfile_manifest.py b/core/rootfile_manifest.py new file mode 100644 index 0000000..8d66697 --- /dev/null +++ b/core/rootfile_manifest.py @@ -0,0 +1,153 @@ +"""Canonical rootfile law manifest for the lawful-collapse runtime.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Tuple + + +@dataclass(frozen=True) +class RootfileLaw: + """One canonical law and its repository jurisdiction.""" + + law_id: str + name: str + directory: str + description: str + invariants: Tuple[str, ...] + allowed_couplings: Tuple[str, ...] = () + + +ROOT = Path(__file__).resolve().parents[1] + + +LAWS: Dict[str, RootfileLaw] = { + "H1": RootfileLaw( + "H1", + "State Space", + "trading/pipeline", + "Raw market data becomes typed runtime state.", + ("state_exists_before_execution",), + ("H2", "H5"), + ), + "H2": RootfileLaw( + "H2", + "Geometry", + "trading/geometry", + "Liquidity structure becomes metric geometry.", + ("geometry_is_derived_from_state",), + ("H3", "H4", "H5"), + ), + "H3": RootfileLaw( + "H3", + "Connection", + "trading/geometry", + "Liquidity gradients bend trajectory evolution.", + ("connection_depends_on_metric_derivatives",), + ("H4", "H5"), + ), + "H4": RootfileLaw( + "H4", + "Curvature", + "trading/geometry", + "Curvature classifies market regime stress.", + ("curvature_reports_regime_stress",), + ("H5", "H7"), + ), + "H5": RootfileLaw( + "H5", + "Path Space", + "trading/path_integral", + "Possible market futures are generated as candidate paths.", + ("future_paths_precede_selection",), + ("H6", "H7", "H8"), + ), + "H6": RootfileLaw( + "H6", + "Ramanujan Compression", + "trading/pipeline", + "Candidate paths compress into deterministic behavior families.", + ("path_families_are_deterministic",), + ("H7", "H8"), + ), + "H7": RootfileLaw( + "H7", + "Action", + "trading/action", + "Paths receive weighted cost and action scores.", + ("action_scores_are_deterministic",), + ("H8", "H9"), + ), + "H8": RootfileLaw( + "H8", + "Admissibility", + "trading/risk", + "Forbidden proposals are refused before collapse.", + ("forbidden_paths_do_not_reach_scheduler",), + ("H9", "H10"), + ), + "H9": RootfileLaw( + "H9", + "Entropy", + "trading/pipeline", + "Information gain is measured before scheduler authority.", + ("entropy_gate_precedes_scheduler",), + ("H10",), + ), + "H10": RootfileLaw( + "H10", + "Scheduler Authority", + "core/orchestration", + "Only scheduler authority may issue execution tokens.", + ("no_operator_self_authorizes_collapse",), + ("H11", "H13"), + ), + "H11": RootfileLaw( + "H11", + "Collapse Execution", + "core/execution", + "Authorized proposals become controlled side effects.", + ("execution_requires_valid_token",), + ("H12", "H13"), + ), + "H12": RootfileLaw( + "H12", + "Reconciliation", + "trading/feedback", + "Broker reality reconciles intended and realized state.", + ("realized_outcomes_drive_feedback",), + ("H13",), + ), + "H13": RootfileLaw( + "H13", + "Evidence", + "tachyonic_chain", + "Every collapse or refusal leaves verifiable evidence.", + ("evidence_is_hash_chained",), + (), + ), +} + + +def get_law(law_id: str) -> RootfileLaw: + """Return law metadata for a canonical law id.""" + normalized = law_id.strip().upper() + if normalized not in LAWS: + raise KeyError(f"Unknown rootfile law: {law_id}") + return LAWS[normalized] + + +def get_law_dir(law_id: str) -> Path: + """Return the repository directory that owns a canonical law.""" + return ROOT / get_law(law_id).directory + + +def validate_manifest_paths(root: Path | None = None) -> list[str]: + """Return missing manifest directories, if any.""" + base = root or ROOT + missing = [] + for law in LAWS.values(): + if not (base / law.directory).exists(): + missing.append(f"{law.law_id}:{law.directory}") + return missing diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..cad5111 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +## Rootfile Runtime Checks + +Before opening a PR that touches runtime architecture, run: + +```powershell +python -m tools.validate_rootfile --repo-root . +python -m pytest tests/rootfile -q +``` + +The GitHub workflow also runs rootfile manifest validation. New operators should +declare `OperatorMeta` with a valid `canonical_law` from H1 through H13. + +## Safety Defaults + +Validation and test work must be offline and non-mutating by default. Broker +network checks, state clearing, canaries, and real-money execution require +explicit opt-in commands and review. + +## Runtime Artifacts + +Do not commit `.env`, raw `logs/`, raw `data/`, or broker canary artifacts. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..06c8694 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,53 @@ +# Developer Guide + +## Rootfile Operators + +New runtime operators should declare their lawful-collapse position with +`OperatorMeta`: + +```python +from core.meta import OperatorMeta, declare_operator + +META = OperatorMeta( + tier="rootfile", + layer="trading.risk", + operator_type="risk_projector", + canonical_law="H8", +) + +@declare_operator(META) +class RiskProjector: + ... +``` + +Existing dict-style `META` declarations remain valid. The compatibility layer in +`core/meta.py` normalizes both forms. + +## Invariant-First Design + +Add new behavior as a refusal-first operator: + +- Define which canonical law owns it. +- State the invariant it enforces. +- Emit evidence for refusal and success paths. +- Keep side effects behind scheduler authority and execution-token checks. + +## Validation + +Run rootfile validation before publishing architecture changes: + +```powershell +python -m tools.validate_rootfile --repo-root . +python -m pytest tests/rootfile -q +``` + +`tools/validate_rootfile.py` verifies declared operator metadata against +`core/rootfile_manifest.py`. Use `--strict-missing` only for focused cleanup +branches because much of the older code is still metadata-optional. + +## Adding A Hamiltonian Adapter + +Prefer an adapter over a disruptive directory move. Put broker-neutral law APIs +under `core/orchestration` or `trading/kernel`, keep existing imports working, +and wire the adapter into the pipeline behind an environment flag if behavior is +still being calibrated. diff --git a/docs/ROOTFILE_HAMILTONIAN_CANON.md b/docs/ROOTFILE_HAMILTONIAN_CANON.md index 3bcd63f..c354f37 100644 --- a/docs/ROOTFILE_HAMILTONIAN_CANON.md +++ b/docs/ROOTFILE_HAMILTONIAN_CANON.md @@ -18,6 +18,14 @@ raw state -> geometry -> paths -> action -> projectors -> entropy -> scheduler -> execution -> reconciliation -> evidence -> ML feedback ``` +The lawful-collapse overlay makes that flow self-describing: + +```text +raw state -> geometry -> field tensor -> paths -> action -> projectors +-> entropy -> lambda scheduler -> execution -> reconciliation +-> evidence -> optional QPT minting -> ML feedback +``` + ## One-To-One Rootfile Map | Canon law | Existing rootfile home | Current implementation meaning | @@ -36,6 +44,10 @@ raw state -> geometry -> paths -> action -> projectors -> entropy | H12 Reconciliation | Pipeline reconciliation plus MT5 and Deriv settlement modules | Intended vs actual execution is checked; realized closed-trade PnL feeds settlement and learning. | | H13 Evidence | `tachyonic_chain/audit_log.py`, `trading/evidence/evidence_chain.py`, audit CLIs | Runtime evidence is a SHA-256 JSONL hash chain; the separate evidence bundle path supports Merkle roots and Ed25519 signatures where used. | +`core/rootfile_manifest.py` is the source of truth for law ownership, +invariants, and allowed couplings. `tools/validate_rootfile.py` checks declared +operator metadata against that manifest in CI. + ## Active Pipeline Alignment The active `PipelineOrchestrator` runs nineteen decision stage handlers plus @@ -46,6 +58,7 @@ because `COMPLETED` is counted as a terminal stage result. |---|---| | Data ingestion and state construction | H1 | | ICT extraction, liquidity field, metric, connection, curvature | H2, H3, H4 | +| Field tensor diagnostics | H8-adjacent admissibility overlay | | Trajectory generation and path-family compression | H5, H6 | | Action evaluation, path integral, interference, path selection | H7 | | Proposal generation and admissibility checks | H8 | @@ -77,6 +90,34 @@ GitHub commits then publicly anchor the code and curated reports. That is not a public blockchain transaction; it is public source-control anchoring layered on top of the local evidence hash chain. +`core/orchestration/evidence.py` is the unified evidence facade. It writes to the +runtime SHA-256 chain and records optional anchoring status. External anchoring is +disabled by default; when enabled without a configured endpoint, the pipeline +records `anchor_status=failed` and continues without blocking collapse handling. + +## Field Hamiltonian + +`trading/fields` and `trading/kernel/H_field.py` add a diagnostics-first field +layer between geometry and path generation: + +- Maxwell tensor: electric impulse and magnetic liquidity diagnostics. +- Minkowski causality: whether proposed displacement is reachable under the + current field. +- Polarity detection: directional phase inference. +- Magnetoelectric coupling: field strength summary. + +By default, the field stage records diagnostics only. Setting +`ENABLE_FIELD_HAMILTONIAN=1` turns field inadmissibility into a hard refusal input +before scheduler collapse. This preserves safety while allowing calibration. + +## QPT Minting + +`core/economics/qpt_token.py` is disabled unless `ENABLE_QPT=1`. When enabled, it +mints a local `qpt_*.jsonl` record only if reconciliation is accepted, +admissibility passed, information gain meets threshold, scheduler authority was +present, and evidence is valid. A QPT token is therefore an artifact of proven +collapse, not a substitute for proof. + ## What We Have Now The merged rootfile system has proven: diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..1c141b6 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,25 @@ +# Security Notes + +## Secrets + +Never commit broker tokens, private keys, `.env`, runtime logs, or generated data +artifacts. Deriv tokens pasted into chat or logs should be treated as compromised +and rotated or revoked before future canary work. + +## External Anchoring + +External evidence anchoring is disabled by default. If enabled, credentials must +come from environment variables or a secrets manager, not source files. Missing +anchor configuration records `anchor_status=failed` and must not block local +evidence-chain append. + +## QPT Ledger + +QPT minting is disabled by default with `ENABLE_QPT=0`. The local QPT ledger is a +post-reconciliation artifact and should not contain secrets. It is not live-trade +approval and must not relax execution-token, demo-account, or risk boundaries. + +## Live Trading Gate + +Real-money execution remains locked until the full forward-proof, falsification, +risk, evidence, and operational safety gates are satisfied and reviewed. diff --git a/tests/rootfile/test_lawful_collapse_universe.py b/tests/rootfile/test_lawful_collapse_universe.py new file mode 100644 index 0000000..5119fea --- /dev/null +++ b/tests/rootfile/test_lawful_collapse_universe.py @@ -0,0 +1,206 @@ +"""Lawful-collapse overlay integration tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from core.economics.qpt_token import mint_qpt_if_applicable +from core.meta import OperatorMeta, declare_operator, get_declared_meta, normalize_meta +from core.orchestration.evidence import EvidenceEvent, emit_evidence +from core.orchestration.reconciliation import ReconciliationReport +from core.rootfile_manifest import LAWS, get_law_dir, validate_manifest_paths +from tachyonic_chain.audit_log import verify_execution_evidence_chain +from tools.validate_rootfile import validate_file, validate_tree +from trading.fields import compute_maxwell_tensor +from trading.fields.minkowski_causality import check_causal_reach +from trading.kernel.H_field import FieldHamiltonian +from trading.pipeline.orchestrator import PipelineContext, PipelineOrchestrator + + +class _RiskManager: + max_position_size = 1.0 + + def check_all_limits(self, **kwargs): + class Result: + passed = True + message = "ok" + + class Level: + value = "low" + + level = Level() + + return Result() + + +def test_rootfile_manifest_resolves_all_laws(): + assert set(LAWS) == {f"H{index}" for index in range(1, 14)} + assert validate_manifest_paths(Path.cwd()) == [] + for law_id in LAWS: + assert get_law_dir(law_id).exists() + + +def test_operator_meta_accepts_dict_and_decorator(): + meta = normalize_meta( + { + "tier": "rootfile", + "layer": "test", + "operator_type": "unit", + "canonical_law": "H8", + } + ) + assert meta == OperatorMeta("rootfile", "test", "unit", "H8") + + @declare_operator(meta) + class Projector: + pass + + assert get_declared_meta(Projector).canonical_law == "H8" + + +def test_validate_rootfile_accepts_operator_meta_and_rejects_bad_law(tmp_path): + good = tmp_path / "good.py" + good.write_text( + "from core.meta import OperatorMeta\n" + "META = OperatorMeta(tier='rootfile', layer='x', operator_type='y', canonical_law='H9')\n", + encoding="utf-8", + ) + bad = tmp_path / "bad.py" + bad.write_text( + "META = {'tier': 'rootfile', 'layer': 'x', 'operator_type': 'y', 'canonical_law': 'H99'}\n", + encoding="utf-8", + ) + + assert validate_file(good).valid + bad_report = validate_file(bad) + assert not bad_report.valid + assert "unknown canonical_law" in bad_report.issues[0].message + + +def test_repository_rootfile_validation_passes(): + assert validate_tree(Path.cwd()).valid + + +def _market_state(): + return { + "ohlcv": { + "high": [1.1010, 1.1020], + "low": [1.0990, 1.1000], + "close": [1.1000, 1.1015], + }, + "microstructure": {"mid": 1.1015}, + } + + +def test_field_tensor_is_deterministic(): + geometry = {"phi": 0.2, "curvature": {"gaussian_curvature": 0.01}} + + first = compute_maxwell_tensor(_market_state(), geometry) + second = compute_maxwell_tensor(_market_state(), geometry) + + assert first == second + assert first["field_energy"] > 0 + + +def test_causal_violation_marks_field_inadmissible(): + tensor = {"magnetic_liquidity": 0.00001, "electric_impulse": 0.00001} + result = check_causal_reach({"entry": 1.0, "target": 2.0}, tensor) + + assert result["causal"] is False + assert result["reason"] == "causal_violation" + + +def test_field_hamiltonian_default_stage_records_diagnostics(monkeypatch): + monkeypatch.delenv("ENABLE_FIELD_HAMILTONIAN", raising=False) + orchestrator = PipelineOrchestrator(risk_manager=_RiskManager(), use_weight_learning=False) + context = PipelineContext(symbol="EURUSD", timestamp=1.0, source="test") + context.market_state = _market_state() + context.geometry_data = {"phi": 0.0, "curvature": {"gaussian_curvature": 0.0}} + + result = orchestrator._stage_field_evaluation(context) + + assert result["field_evaluated"] is True + assert result["field_enabled"] is False + assert "field_tensor" in context.field_data + + +def test_enabled_field_refusal_blocks_admissibility(monkeypatch): + monkeypatch.setenv("ENABLE_FIELD_HAMILTONIAN", "1") + orchestrator = PipelineOrchestrator(risk_manager=_RiskManager(), use_weight_learning=False) + context = PipelineContext(symbol="EURUSD", timestamp=1.0, source="test") + context.proposal = {"direction": "buy", "entry": 1.0, "size": 0.1} + context.field_admissible = False + context.field_reason = "causal_violation" + + result = orchestrator._stage_admissibility_check(context) + + assert result["admissible"] is False + assert result["reason"] == "field_hamiltonian_refusal:causal_violation" + + +def _accepted_report(): + return ReconciliationReport( + broker="test", + accepted=True, + status="match", + realized_pnl=1.0, + evidence_valid=True, + admissible=True, + information_gain=0.75, + scheduler_authorized=True, + execution_id="exec-qpt", + symbol="EURUSD", + ) + + +def test_qpt_mints_only_when_enabled_and_all_conditions_pass(tmp_path, monkeypatch): + ledger = tmp_path / "qpt_ledger.jsonl" + monkeypatch.setenv("ENABLE_QPT", "1") + monkeypatch.setenv("APEX_QPT_LEDGER", str(ledger)) + + token_id = mint_qpt_if_applicable(_accepted_report()) + + assert token_id and token_id.startswith("qpt_") + record = json.loads(ledger.read_text(encoding="utf-8").strip()) + assert record["token_id"] == token_id + assert record["conditions"]["reconciliation_accepted"] is True + + +def test_qpt_does_not_mint_on_refusal(tmp_path, monkeypatch): + ledger = tmp_path / "qpt_ledger.jsonl" + monkeypatch.setenv("ENABLE_QPT", "1") + monkeypatch.setenv("APEX_QPT_LEDGER", str(ledger)) + report = _accepted_report() + report.accepted = False + + assert mint_qpt_if_applicable(report) is None + assert not ledger.exists() + + +def test_evidence_facade_appends_runtime_chain_and_records_anchor_failure(tmp_path, monkeypatch): + log_path = tmp_path / "execution_evidence.jsonl" + monkeypatch.setenv("ENABLE_ANCHORING", "1") + monkeypatch.delenv("APEX_ANCHOR_ENDPOINT", raising=False) + + record_hash = emit_evidence( + EvidenceEvent( + event_type="unit_test", + execution_id="evidence-facade", + operation="test", + payload={"value": 1}, + ), + log_path=str(log_path), + ) + + assert record_hash + assert verify_execution_evidence_chain(log_path).valid + payload = json.loads(log_path.read_text(encoding="utf-8").strip())["payload"] + assert payload["anchor_status"] == "failed" + + +def test_field_hamiltonian_direct_flat_field_reason(): + result = FieldHamiltonian().evaluate({}, {}, proposal=None) + + assert result.field_admissible is False + assert "flat_field" in result.reasons diff --git a/tests/rootfile/test_mt5_position_close_tracker.py b/tests/rootfile/test_mt5_position_close_tracker.py new file mode 100644 index 0000000..6d1c5eb --- /dev/null +++ b/tests/rootfile/test_mt5_position_close_tracker.py @@ -0,0 +1,55 @@ +"""MT5 close-tracker feedback tests.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from trading.brokers import mt5_broker +from trading.brokers.mt5_broker import MT5PositionCloseTracker + + +def _deal(ticket: int = 353158067, *, entry: int = 1, reason: int = 5, + profit: float = 0.0, swap: float = 0.0, commission: float = 0.0, + time: int = 1777912994): + return SimpleNamespace( + position_id=ticket, + entry=entry, + reason=reason, + profit=profit, + swap=swap, + commission=commission, + time=time, + ) + + +def test_tracker_uses_full_lifecycle_net_pnl(monkeypatch): + deals = [ + _deal(entry=0, reason=3, profit=0.0, commission=-0.02, time=1), + _deal(entry=1, reason=5, profit=2.03, commission=-0.02, time=2), + ] + monkeypatch.setattr(mt5_broker.mt5, "history_deals_get", lambda *args: deals) + + tracker = MT5PositionCloseTracker() + + assert tracker._fetch_realized_pnl(353158067, fallback=0.2) == pytest.approx(1.99) + + +def test_tracker_falls_back_without_closing_deal(monkeypatch): + deals = [_deal(entry=0, reason=3, profit=0.0, commission=-0.02)] + monkeypatch.setattr(mt5_broker.mt5, "history_deals_get", lambda *args: deals) + + tracker = MT5PositionCloseTracker() + + assert tracker._fetch_realized_pnl(353158067, fallback=0.2) == 0.2 + + +def test_tracker_close_reason_mapping(monkeypatch): + tracker = MT5PositionCloseTracker() + + for reason, expected in [(5, "TP"), (4, "SL"), (3, "CLIENT")]: + deals = [_deal(entry=1, reason=reason)] + monkeypatch.setattr(mt5_broker.mt5, "history_deals_get", lambda *args, deals=deals: deals) + + assert tracker._fetch_close_reason(353158067) == expected diff --git a/tools/validate_rootfile.py b/tools/validate_rootfile.py new file mode 100644 index 0000000..60c6505 --- /dev/null +++ b/tools/validate_rootfile.py @@ -0,0 +1,150 @@ +"""Validate rootfile law metadata against the canonical manifest.""" + +from __future__ import annotations + +import argparse +import ast +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable, List + +from core.meta import normalize_meta +from core.rootfile_manifest import LAWS, validate_manifest_paths + + +@dataclass +class RootfileValidationIssue: + """A single rootfile manifest validation issue.""" + + path: str + message: str + + +@dataclass +class RootfileValidationReport: + """Rootfile metadata validation report.""" + + valid: bool + issues: List[RootfileValidationIssue] = field(default_factory=list) + + +DEFAULT_ROOTS = ("core", "trading", "tachyonic_chain", "tools") +OPERATOR_META_FIELDS = ("tier", "layer", "operator_type", "canonical_law") + + +def _call_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return node.attr + return "" + + +def _operator_meta_call(node: ast.AST) -> dict | None: + if not isinstance(node, ast.Call) or _call_name(node.func) != "OperatorMeta": + return None + + values: dict = {} + try: + for index, arg in enumerate(node.args): + if index >= len(OPERATOR_META_FIELDS): + return {"__invalid__": "OperatorMeta has too many positional arguments"} + values[OPERATOR_META_FIELDS[index]] = ast.literal_eval(arg) + + for keyword in node.keywords: + if keyword.arg is None: + return {"__invalid__": "OperatorMeta does not support **kwargs in META"} + values[keyword.arg] = ast.literal_eval(keyword.value) + except (ValueError, SyntaxError): + return {"__invalid__": "OperatorMeta META values must be literal"} + + return values + + +def _literal_meta(path: Path) -> dict | None: + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if not any(isinstance(target, ast.Name) and target.id == "META" for target in node.targets): + continue + call_meta = _operator_meta_call(node.value) + if call_meta is not None: + return call_meta + try: + value = ast.literal_eval(node.value) + except (ValueError, SyntaxError): + return {"__invalid__": "META must be literal for rootfile validation"} + return value if isinstance(value, dict) else {"__invalid__": "META must be a dict"} + return None + + +def validate_file(path: str | Path, *, strict_missing: bool = False) -> RootfileValidationReport: + """Validate one Python file's optional rootfile metadata.""" + path = Path(path) + issues: List[RootfileValidationIssue] = [] + try: + meta = _literal_meta(path) + except SyntaxError as exc: + return RootfileValidationReport(False, [RootfileValidationIssue(str(path), f"syntax error: {exc}")]) + + if meta is None: + if strict_missing: + issues.append(RootfileValidationIssue(str(path), "missing META")) + return RootfileValidationReport(not issues, issues) + + if "__invalid__" in meta: + issues.append(RootfileValidationIssue(str(path), str(meta["__invalid__"]))) + return RootfileValidationReport(False, issues) + + normalized = normalize_meta(meta) + if normalized.canonical_law and normalized.canonical_law not in LAWS: + issues.append( + RootfileValidationIssue( + str(path), + f"unknown canonical_law: {normalized.canonical_law}", + ) + ) + + return RootfileValidationReport(not issues, issues) + + +def validate_tree( + repo_root: str | Path = ".", + roots: Iterable[str] = DEFAULT_ROOTS, + *, + strict_missing: bool = False, +) -> RootfileValidationReport: + """Validate manifest directories and Python metadata under selected roots.""" + repo_root = Path(repo_root) + issues = [ + RootfileValidationIssue(str(repo_root), f"manifest path missing: {missing}") + for missing in validate_manifest_paths(repo_root) + ] + + for prefix in roots: + base = repo_root / prefix + if not base.exists(): + continue + for path in base.rglob("*.py"): + report = validate_file(path, strict_missing=strict_missing) + issues.extend(report.issues) + + return RootfileValidationReport(not issues, issues) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Validate rootfile canonical law metadata") + parser.add_argument("--repo-root", default=".") + parser.add_argument("--strict-missing", action="store_true") + args = parser.parse_args(argv) + + report = validate_tree(args.repo_root, strict_missing=args.strict_missing) + for issue in report.issues: + print(f"{issue.path}: {issue.message}") + return 0 if report.valid else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/trading/brokers/mt5_broker.py b/trading/brokers/mt5_broker.py index 80dff02..5af57c2 100644 --- a/trading/brokers/mt5_broker.py +++ b/trading/brokers/mt5_broker.py @@ -722,11 +722,11 @@ def _check_closed_positions(self) -> None: self._fire(ticket, trade_id, callback, realized) def _fetch_realized_pnl(self, ticket: int, fallback: float) -> float: - """Query history_deals_get for DEAL_ENTRY_OUT profit+swap. Falls back to predicted.""" + """Return full lifecycle net PnL from MT5 history, falling back if unverified.""" try: date_from = datetime.now(timezone.utc) - timedelta(days=1) - date_to = datetime.now(timezone.utc) + timedelta(seconds=10) - deals = mt5.history_deals_get(date_from, date_to, group="*") + date_to = datetime.now(timezone.utc) + timedelta(days=1) + deals = mt5.history_deals_get(date_from, date_to) except Exception as exc: logger.warning("MT5Tracker: history_deals_get failed ticket=%d: %s", ticket, exc) return fallback @@ -737,26 +737,35 @@ def _fetch_realized_pnl(self, ticket: int, fallback: float) -> float: ) return fallback - DEAL_ENTRY_OUT = 1 - closing = [d for d in deals if d.position_id == ticket and d.entry == DEAL_ENTRY_OUT] + deal_entry_out = 1 + lifecycle = [d for d in deals if getattr(d, "position_id", None) == ticket] + closing = [d for d in lifecycle if getattr(d, "entry", None) == deal_entry_out] if not closing: logger.warning( "MT5Tracker: no DEAL_ENTRY_OUT for ticket=%d — fallback $%.2f", ticket, fallback ) return fallback - return float(sum(d.profit + getattr(d, 'swap', 0.0) for d in closing)) + return float( + sum( + getattr(d, "profit", 0.0) + + getattr(d, "swap", 0.0) + + getattr(d, "commission", 0.0) + for d in lifecycle + ) + ) def _fetch_close_reason(self, ticket: int) -> str: """Best-effort close reason for logging. Swallows all errors.""" - REASON_MAP = {0: "CLIENT", 1: "SL", 2: "TP", 3: "CLIENT", 4: "STOP_OUT"} + REASON_MAP = {0: "CLIENT", 3: "CLIENT", 4: "SL", 5: "TP"} try: date_from = datetime.now(timezone.utc) - timedelta(days=1) - date_to = datetime.now(timezone.utc) + timedelta(seconds=10) - deals = mt5.history_deals_get(date_from, date_to, group="*") + date_to = datetime.now(timezone.utc) + timedelta(days=1) + deals = mt5.history_deals_get(date_from, date_to) if deals: cd = [d for d in deals if d.position_id == ticket and d.entry == 1] if cd: + cd.sort(key=lambda deal: getattr(deal, "time", 0)) return REASON_MAP.get(cd[-1].reason, str(cd[-1].reason)) except Exception: pass diff --git a/trading/fields/__init__.py b/trading/fields/__init__.py new file mode 100644 index 0000000..c8bb8e6 --- /dev/null +++ b/trading/fields/__init__.py @@ -0,0 +1,13 @@ +"""Field diagnostics for the lawful-collapse Hamiltonian overlay.""" + +from .magnetoelectric_coupling import compute_magnetoelectric_coupling +from .maxwell_tensor import compute_maxwell_tensor +from .minkowski_causality import check_causal_reach +from .polarity_detector import detect_polarity + +__all__ = [ + "check_causal_reach", + "compute_magnetoelectric_coupling", + "compute_maxwell_tensor", + "detect_polarity", +] diff --git a/trading/fields/magnetoelectric_coupling.py b/trading/fields/magnetoelectric_coupling.py new file mode 100644 index 0000000..4be1d4f --- /dev/null +++ b/trading/fields/magnetoelectric_coupling.py @@ -0,0 +1,19 @@ +"""Magnetoelectric coupling diagnostics for field admissibility.""" + +from __future__ import annotations + +from typing import Dict + + +def compute_magnetoelectric_coupling(field_tensor: Dict) -> Dict[str, float]: + """Compute D/H style coupling values from field components.""" + electric = float(field_tensor.get("electric_impulse", 0.0) or 0.0) + magnetic = float(field_tensor.get("magnetic_liquidity", 0.0) or 0.0) + displacement_field = electric + 0.5 * magnetic + magnetic_field = magnetic - 0.5 * electric + coupling_strength = abs(electric * magnetic) + return { + "D": displacement_field, + "H": magnetic_field, + "coupling_strength": coupling_strength, + } diff --git a/trading/fields/maxwell_tensor.py b/trading/fields/maxwell_tensor.py new file mode 100644 index 0000000..1efc779 --- /dev/null +++ b/trading/fields/maxwell_tensor.py @@ -0,0 +1,38 @@ +"""Deterministic Maxwell-style field tensor for market geometry diagnostics.""" + +from __future__ import annotations + +from typing import Dict, Iterable + + +def _last(values: Iterable[float], default: float = 0.0) -> float: + seq = list(values or []) + return float(seq[-1]) if seq else default + + +def compute_maxwell_tensor(market_state: Dict, geometry_data: Dict) -> Dict[str, float]: + """Compute a compact field tensor from OHLCV, microstructure, and geometry.""" + ohlcv = market_state.get("ohlcv", {}) if isinstance(market_state, dict) else {} + micro = market_state.get("microstructure", {}) if isinstance(market_state, dict) else {} + close = [float(value) for value in ohlcv.get("close", []) if value is not None] + high = [float(value) for value in ohlcv.get("high", []) if value is not None] + low = [float(value) for value in ohlcv.get("low", []) if value is not None] + + latest_close = _last(close, float(micro.get("mid", 0.0) or 0.0)) + prev_close = close[-2] if len(close) >= 2 else latest_close + impulse = latest_close - prev_close + spread_proxy = abs(_last(high, latest_close) - _last(low, latest_close)) + phi = float(geometry_data.get("phi", 0.0) or 0.0) + curvature = geometry_data.get("curvature", {}) or {} + curvature_k = float(curvature.get("gaussian_curvature", curvature.get("K", 0.0)) or 0.0) + + electric_impulse = impulse + 0.1 * phi + magnetic_liquidity = spread_proxy + abs(curvature_k) + field_energy = electric_impulse * electric_impulse + magnetic_liquidity * magnetic_liquidity + return { + "F_qt": electric_impulse, + "F_tq": -electric_impulse, + "electric_impulse": electric_impulse, + "magnetic_liquidity": magnetic_liquidity, + "field_energy": field_energy, + } diff --git a/trading/fields/minkowski_causality.py b/trading/fields/minkowski_causality.py new file mode 100644 index 0000000..7063822 --- /dev/null +++ b/trading/fields/minkowski_causality.py @@ -0,0 +1,22 @@ +"""Minkowski-style causal reach checks for proposal displacement.""" + +from __future__ import annotations + +from typing import Dict + + +def check_causal_reach(proposal: Dict, field_tensor: Dict, *, liquidity_speed: float = 1.0) -> Dict: + """Return whether a proposal displacement is reachable under the field.""" + entry = float(proposal.get("entry", 0.0) or 0.0) + target = float(proposal.get("target", entry) or entry) + displacement = abs(target - entry) + magnetic = abs(float(field_tensor.get("magnetic_liquidity", 0.0) or 0.0)) + electric = abs(float(field_tensor.get("electric_impulse", 0.0) or 0.0)) + reach = max(magnetic * float(liquidity_speed), electric, 1e-12) + causal = displacement <= reach * 10.0 + return { + "causal": causal, + "displacement": displacement, + "causal_reach": reach * 10.0, + "reason": "causal" if causal else "causal_violation", + } diff --git a/trading/fields/polarity_detector.py b/trading/fields/polarity_detector.py new file mode 100644 index 0000000..fe2b2b6 --- /dev/null +++ b/trading/fields/polarity_detector.py @@ -0,0 +1,24 @@ +"""Wave polarity diagnostics from the field tensor.""" + +from __future__ import annotations + +from typing import Dict + + +def detect_polarity(field_tensor: Dict) -> Dict[str, str | float]: + """Infer coarse polarity and phase from electric impulse.""" + impulse = float(field_tensor.get("electric_impulse", 0.0) or 0.0) + if impulse > 0: + polarity = "positive" + phase = "expansion" + elif impulse < 0: + polarity = "negative" + phase = "contraction" + else: + polarity = "neutral" + phase = "flat" + return { + "polarity": polarity, + "phase": phase, + "impulse": impulse, + } diff --git a/trading/kernel/H_field.py b/trading/kernel/H_field.py new file mode 100644 index 0000000..921228f --- /dev/null +++ b/trading/kernel/H_field.py @@ -0,0 +1,73 @@ +"""Field Hamiltonian overlay for geometry-aware admissibility diagnostics.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List + +from core.meta import OperatorMeta +from trading.fields import ( + check_causal_reach, + compute_magnetoelectric_coupling, + compute_maxwell_tensor, + detect_polarity, +) + + +META = OperatorMeta( + tier="rootfile", + layer="trading.kernel", + operator_type="field_hamiltonian", + canonical_law="H8", +) + + +@dataclass +class FieldEvaluation: + """Result emitted by the field Hamiltonian.""" + + field_tensor: Dict[str, float] + polarity: Dict[str, str | float] + causal_status: Dict + coupling: Dict[str, float] + field_admissible: bool + reasons: List[str] = field(default_factory=list) + + def to_dict(self) -> Dict: + return { + "field_tensor": self.field_tensor, + "polarity": self.polarity, + "causal_status": self.causal_status, + "coupling": self.coupling, + "field_admissible": self.field_admissible, + "reasons": self.reasons, + } + + +class FieldHamiltonian: + """Combine field tensor, polarity, causality, and coupling into one gate.""" + + def evaluate(self, market_state: Dict, geometry_data: Dict, proposal: Dict | None = None) -> FieldEvaluation: + proposal = proposal or {} + tensor = compute_maxwell_tensor(market_state, geometry_data) + polarity = detect_polarity(tensor) + causal = check_causal_reach(proposal, tensor) if proposal else { + "causal": True, + "displacement": 0.0, + "causal_reach": 0.0, + "reason": "no_proposal_yet", + } + coupling = compute_magnetoelectric_coupling(tensor) + reasons = [] + if not causal.get("causal", False): + reasons.append("causal_violation") + if polarity.get("polarity") == "neutral" and coupling.get("coupling_strength", 0.0) == 0.0: + reasons.append("flat_field") + return FieldEvaluation( + field_tensor=tensor, + polarity=polarity, + causal_status=causal, + coupling=coupling, + field_admissible=not reasons, + reasons=reasons, + ) diff --git a/trading/pipeline/orchestrator.py b/trading/pipeline/orchestrator.py index 2240188..106f5e0 100644 --- a/trading/pipeline/orchestrator.py +++ b/trading/pipeline/orchestrator.py @@ -10,6 +10,7 @@ Each stage is a checkpointed transformation with typed inputs/outputs. """ +import os import time import logging import numpy as np @@ -31,6 +32,7 @@ class PipelineStage(Enum): # Riemannian Geometry GEOMETRY_COMPUTATION = "geometry_computation" + FIELD_EVALUATION = "field_evaluation" # Path Generation TRAJECTORY_GENERATION = "trajectory_generation" @@ -87,6 +89,9 @@ class PipelineContext: hft_signals: Dict = field(default_factory=dict) ict_geometry: Dict = field(default_factory=dict) geometry_data: Dict = field(default_factory=dict) # Riemannian geometry + field_data: Dict = field(default_factory=dict) + field_admissible: bool = True + field_reason: str = "" trajectories: List[Dict] = field(default_factory=list) path_families: Dict[str, List[str]] = field(default_factory=dict) path_signatures: Dict[str, Dict[str, str]] = field(default_factory=dict) @@ -99,6 +104,7 @@ class PipelineContext: execution_result: Dict = field(default_factory=dict) reconciliation_status: str = "" evidence_hash: str = "" + qpt_token_id: Optional[str] = None weight_update_result: Dict = field(default_factory=dict) risk_check_passed: bool = False risk_check_message: str = "" @@ -198,6 +204,7 @@ def __init__(self, PipelineStage.STATE_CONSTRUCTION: self._stage_state_construction, PipelineStage.ICT_EXTRACTION: self._stage_ict_extraction, PipelineStage.GEOMETRY_COMPUTATION: self._stage_geometry_computation, + PipelineStage.FIELD_EVALUATION: self._stage_field_evaluation, PipelineStage.TRAJECTORY_GENERATION: self._stage_trajectory_generation, PipelineStage.RAMANUJAN_COMPRESSION: self._stage_ramanujan_compression, PipelineStage.ADMISSIBILITY_FILTERING: self._stage_admissibility_filtering, @@ -249,6 +256,7 @@ def execute(self, raw_data: Dict, symbol: str, source: str = 'MT5', adapted_para PipelineStage.STATE_CONSTRUCTION, PipelineStage.ICT_EXTRACTION, PipelineStage.GEOMETRY_COMPUTATION, + PipelineStage.FIELD_EVALUATION, PipelineStage.TRAJECTORY_GENERATION, PipelineStage.RAMANUJAN_COMPRESSION, PipelineStage.ADMISSIBILITY_FILTERING, @@ -762,6 +770,37 @@ def _stage_geometry_computation(self, context: PipelineContext) -> Dict: # Continue without geometry (graceful degradation) context.geometry_data = {} return {'geometry_computed': False, 'error': str(e)} + + def _stage_field_evaluation(self, context: PipelineContext) -> Dict: + """Stage 4b: Field Hamiltonian diagnostics after geometry computation.""" + from ..kernel.H_field import FieldHamiltonian + + result = FieldHamiltonian().evaluate( + context.market_state, + context.geometry_data, + context.proposal, + ) + context.field_data = result.to_dict() + context.field_admissible = bool(result.field_admissible) + context.field_reason = ",".join(result.reasons) if result.reasons else "field_admissible" + enabled = os.getenv("ENABLE_FIELD_HAMILTONIAN", "0") == "1" + status = "passed" if context.field_admissible or not enabled else "failed" + self._audit_gate( + "stage4b_field", + status, + enabled=enabled, + field_admissible=context.field_admissible, + field_reason=context.field_reason, + polarity=result.polarity.get("polarity"), + coupling_strength=f"{result.coupling.get('coupling_strength', 0.0):.6g}", + symbol=context.symbol, + ) + return { + "field_evaluated": True, + "field_enabled": enabled, + "field_admissible": context.field_admissible, + "field_reason": context.field_reason, + } def _stage_trajectory_generation(self, context: PipelineContext) -> Dict: """Stage 5: Generate candidate trajectory families with regime-aware parameters. @@ -1277,6 +1316,17 @@ def _stage_admissibility_check(self, context: PipelineContext) -> Dict: self._audit_gate("stage12_admissibility", "failed", reason="no_proposal") return {'admissible': False, 'risk_ok': False, 'reason': 'no_proposal'} + if os.getenv("ENABLE_FIELD_HAMILTONIAN", "0") == "1" and not context.field_admissible: + context.risk_check_passed = False + reason = f"field_hamiltonian_refusal:{context.field_reason}" + self._audit_gate( + "stage12_admissibility", + "failed", + reason=reason, + symbol=context.symbol, + ) + return {'admissible': False, 'risk_ok': False, 'reason': reason} + # Π_total: path-wise step validation before any risk computation if context.selected_path: path_ok, path_reason = self._validate_path_stepwise( @@ -1863,18 +1913,51 @@ def _stage_evidence_emission(self, context: PipelineContext) -> Dict: 'decision': context.collapse_decision, 'execution': context.execution_result, 'reconciliation': context.reconciliation_status, + 'field': context.field_data, + 'qpt_token_id': None, } evidence_str = str(evidence_data) - context.evidence_hash = hashlib.sha256(evidence_str.encode()).hexdigest()[:32] + provisional_hash = hashlib.sha256(evidence_str.encode()).hexdigest()[:32] + + try: + from core.economics.qpt_token import mint_qpt_if_applicable + from core.orchestration.reconciliation import ReconciliationReport + + report = ReconciliationReport( + execution_id=str( + context.execution_result.get("order_id") + or context.execution_result.get("ticket") + or context.execution_result.get("contract_id") + or provisional_hash + ), + symbol=context.symbol, + accepted=context.reconciliation_status == "match", + status=context.reconciliation_status or "unknown", + realized_pnl=context.execution_result.get("pnl"), + admissible=bool(context.risk_check_passed), + information_gain=float(context.action_scores.get("delta_s", 0.0) or 0.0), + scheduler_authorized=context.collapse_decision == "AUTHORIZED", + evidence_valid=bool(provisional_hash), + broker=context.source, + payload={"provisional_evidence_hash": provisional_hash}, + ) + context.qpt_token_id = mint_qpt_if_applicable(report) + except Exception as exc: + logger.warning("Stage 18: QPT minting skipped: %s", exc) + context.qpt_token_id = None + + evidence_data['qpt_token_id'] = context.qpt_token_id + context.evidence_hash = hashlib.sha256(str(evidence_data).encode()).hexdigest()[:32] self._audit_gate( "stage18_evidence", "passed", evidence_hash=context.evidence_hash, + qpt_token_id=context.qpt_token_id, symbol=context.symbol, ) - return {'evidence_hash': context.evidence_hash} + return {'evidence_hash': context.evidence_hash, 'qpt_token_id': context.qpt_token_id} def _stage_weight_update(self, context: PipelineContext) -> Dict: """Stage 19: Backward learning - update action weights"""