build(agent): new-agents-4#58ba63 iteration
This commit is contained in:
parent
dc454c9000
commit
48c1f47367
19
AGENTS.md
19
AGENTS.md
|
|
@ -16,7 +16,7 @@ Architecture overview for the EquiCompiler MVP
|
||||||
- LocalProblem: a portfolio-level optimization task expressed in the DSL
|
- LocalProblem: a portfolio-level optimization task expressed in the DSL
|
||||||
- SharedVariables: signals and inter-node communication primitives (simplified in MVP)
|
- SharedVariables: signals and inter-node communication primitives (simplified in MVP)
|
||||||
- PlanDelta: incremental plan changes with versioning (simplified in MVP)
|
- PlanDelta: incremental plan changes with versioning (simplified in MVP)
|
||||||
- Auditability: IR includes a version field; future work will embed cryptographic attestations
|
- Auditability: IR includes versioning, deterministic attestations, lineage digests, and a Graph-of-Contracts skeleton
|
||||||
|
|
||||||
- Repository structure (initial)
|
- Repository structure (initial)
|
||||||
- AGENTS.md: this document
|
- AGENTS.md: this document
|
||||||
|
|
@ -24,15 +24,24 @@ Architecture overview for the EquiCompiler MVP
|
||||||
- pyproject.toml: packaging metadata
|
- pyproject.toml: packaging metadata
|
||||||
- equicompiler_algebraic_portfolio_dsl_to_/
|
- equicompiler_algebraic_portfolio_dsl_to_/
|
||||||
- __init__.py
|
- __init__.py
|
||||||
- core.py: DSL -> IR parser (minimal)
|
- core.py: DSL -> EquiIR compiler, lineage, attestations, and execution-plan synthesis
|
||||||
- cli.py: CLI entry point
|
- cli.py: CLI entry point with optional reference backtest
|
||||||
|
- delta_sync.py: deterministic IR diffing and plan delta summaries
|
||||||
|
- registry.py: in-process Graph-of-Contracts contract registry
|
||||||
|
- goc_registry.py: GoC skeleton generation helpers
|
||||||
|
- backends/python_backtester.py: deterministic Python reference backtester
|
||||||
|
- adapters/: starter feed and broker adapters
|
||||||
- tests/
|
- tests/
|
||||||
- test_core.py: basic unit test for DSL parsing
|
- test_core.py: DSL parsing and core IR assertions
|
||||||
|
- test_execution_policy.py: execution policy parsing
|
||||||
|
- test_goC.py: GoC structure assertions
|
||||||
|
- test_version_override.py: DSL version override coverage
|
||||||
|
- test_compiler_pipeline.py: richer compiler, backtest, and delta-sync coverage
|
||||||
- test.sh: test runner that executes unit tests and builds the package
|
- test.sh: test runner that executes unit tests and builds the package
|
||||||
|
|
||||||
- Testing philosophy
|
- Testing philosophy
|
||||||
- One basic test ensures the DSL is parsed into a structured IR.
|
|
||||||
- Tests should be deterministic and fast, enabling CI checks early.
|
- Tests should be deterministic and fast, enabling CI checks early.
|
||||||
|
- Cover parser normalization, deterministic backtesting, and delta-sync behavior.
|
||||||
|
|
||||||
- Roadmap (high level)
|
- Roadmap (high level)
|
||||||
- Expand the DSL grammar and IR representation
|
- Expand the DSL grammar and IR representation
|
||||||
|
|
|
||||||
170
README.md
170
README.md
|
|
@ -1,91 +1,107 @@
|
||||||
# EquiCompiler: Algebraic Portfolio DSL to Verifiable Low-Latency Compiler (MVP)
|
# EquiCompiler
|
||||||
|
|
||||||
Overview
|
EquiCompiler translates a compact portfolio DSL into a canonical EquiIR graph with deterministic attestations, registry-backed contracts, delta-sync metadata, and a reference Python backtest path.
|
||||||
- This repository contains a minimal, production-ready MVP for a compiler stack that translates a domain-specific language for market strategies into a portable, verifiable execution graph. The MVP focuses on a small, well-formed core to enable safe extension by additional agents in the swarm.
|
|
||||||
|
|
||||||
What you get in this MVP
|
## What It Does
|
||||||
- A small DSL parser that reads a concise DSL and converts it into a canonical in-memory IR (EquiIR) represented as Python dicts.
|
|
||||||
- A minimal Python package equicompiler_algebraic_portfolio_dsl_to_ with a core module and a CLI entry point.
|
|
||||||
- A tiny test suite to validate DSL parsing and IR generation.
|
|
||||||
- A test runner (test.sh) that executes tests and builds the package to verify packaging metadata.
|
|
||||||
|
|
||||||
Usage
|
- Parses portfolio strategy declarations into a normalized IR.
|
||||||
- DSL to IR (programmatic):
|
- Captures assets, objective clauses, constraints, execution policy, risk budgets, and regulatory metadata.
|
||||||
from equicompiler_algebraic_portfolio_dsl_to_.core import parse_dsl_to_ir
|
- Builds a deterministic execution plan with backend targets for Python, C++, and WebAssembly.
|
||||||
dsl = "assets: AAPL, MSFT, GOOG\nobjectives: maximize_return\nconstraints: max_drawdown=0.2, var=0.95"
|
- Emits digest-based attestations and a Graph-of-Contracts skeleton for auditability.
|
||||||
ir = parse_dsl_to_ir(dsl)
|
- Provides a reference Python backtester for offline evaluation and replay.
|
||||||
print(ir)
|
- Computes deterministic delta-sync summaries between IR revisions.
|
||||||
|
|
||||||
- CLI (Python module):
|
## DSL
|
||||||
python -m equicompiler_algebraic_portfolio_dsl_to_.cli path/to/dsl_file.txt
|
|
||||||
|
|
||||||
Verifiable DSL Overview
|
Example:
|
||||||
- The EquiCompiler MVP translates a mathematically precise portfolio DSL into a portable, verifiable IR (EquiIR).
|
|
||||||
- Each transformation step embeds lightweight cryptographic attestations (digest-based) to ensure auditability and replayability without leaking sensitive data.
|
|
||||||
- A Graph-of-Contracts (GoC) skeleton is attached to the IR to enable plug-and-play adapters for data feeds and brokers, while preserving versioned contracts and compatibility.
|
|
||||||
- The system supports offline-first evaluation with deterministic delta-sync, enabling local backtests and plan replay when connectivity resumes.
|
|
||||||
- Backends are designed for cross-runtime portability: Python backtester for offline tests, C++ for live trading, and WebAssembly for browser-based evaluation.
|
|
||||||
- The MVP keeps a minimal, stable surface area while providing clear extension points for future formal verification hooks and delta-sync workflows.
|
|
||||||
|
|
||||||
Example workflow
|
```text
|
||||||
- Parse a DSL to IR (programmatic):
|
version: 0.3
|
||||||
``from equicompiler_algebraic_portfolio_dsl_to_.core import parse_dsl_to_ir``
|
assets: AAPL@0.6, MSFT@0.4
|
||||||
``dsl = "assets: AAPL, MSFT, GOOG\nobjectives: maximize_return\nconstraints: max_drawdown=0.2, var=0.95"``
|
objectives: maximize_return; minimize_risk
|
||||||
``ir = parse_dsl_to_ir(dsl)``
|
constraints: max_drawdown=0.15, var=0.95, cvar=0.9
|
||||||
``print(ir)``
|
risk_budget: max_var=0.1, max_exposure=0.8
|
||||||
- Use the CLI to validate and inspect the IR produced from a file:
|
execution_policy: offline, delta_sync, rebalance=1d
|
||||||
``python -m equicompiler_algebraic_portfolio_dsl_to_.cli path/to/dsl_file.txt``
|
regulatory: venue=us, suitability=true
|
||||||
|
```
|
||||||
|
|
||||||
Roadmap (high level)
|
Supported sections:
|
||||||
- Phase 0: DSL to IR, start backends (Python backtester, minimal C++ live skeleton) and a minimal GoC registry.
|
|
||||||
- Phase 1: Formal verification hooks and delta-sync for offline/online reconciliation.
|
|
||||||
- Phase 2: Cross-backend interoperability tests with toy adapters.
|
|
||||||
- Phase 3: Hardware-in-the-loop (HIL) testing with partitioned portfolios.
|
|
||||||
|
|
||||||
Publishing readiness
|
- `version`
|
||||||
- This repository is designed for packaging as a Python module named
|
- `assets`
|
||||||
``equicompiler_algebraic_portfolio_dsl_to`` with versioning in ``pyproject.toml``.
|
- `objectives`
|
||||||
- The README now includes a marketing-style overview and usage examples to aid publication.
|
- `constraints`
|
||||||
|
- `execution_policy`
|
||||||
|
- `risk_budget`
|
||||||
|
- `market_microstructure`
|
||||||
|
- `regulatory`
|
||||||
|
|
||||||
Project structure
|
## IR Surface
|
||||||
- AGENTS.md: architecture and testing guidelines for future agents
|
|
||||||
- README.md: this file
|
|
||||||
- pyproject.toml: packaging metadata
|
|
||||||
- equicompiler_algebraic_portfolio_dsl_to_/ (package)
|
|
||||||
- __init__.py
|
|
||||||
- core.py: DSL -> IR parser (minimal)
|
|
||||||
- cli.py: CLI entry point
|
|
||||||
- tests/test_core.py: small unit test for DSL parsing
|
|
||||||
- test.sh: test runner to validate test suite and packaging
|
|
||||||
|
|
||||||
Development notes
|
`parse_dsl_to_ir()` returns a dictionary with:
|
||||||
- The MVP intentionally keeps dependencies minimal to ensure fast iterations and deterministic tests.
|
|
||||||
- When adding features, try to keep changes small and focused on a single goal.
|
|
||||||
- Ensure tests cover the new functionality and avoid sensitive data in tests.
|
|
||||||
|
|
||||||
Next steps
|
- canonical assets and optional weights
|
||||||
- Extend the DSL with richer constraints (VaR, VaR-CVaR, liquidity, latency) and ExecutionPolicy primitives.
|
- objectives and constraints
|
||||||
- Integrate the GoC registry and build a canonical EquiIR representation with per-message metadata for replay/verification.
|
- execution policy and risk budget
|
||||||
- Add a lightweight delta-sync coordinator and starter adapters for data feeds and brokers.
|
- compiler metadata and lineage digest
|
||||||
- Expand the test suite to exercise the new backtester and Graph-of-Contracts scaffolds.
|
- execution plan with registry contracts
|
||||||
- Improve packaging and docs to support publishing to a Python package index.
|
- attestations and GoC skeleton
|
||||||
- Implement a more expressive DSL and a richer IR (EquiIR) representation.
|
- delta-sync metadata
|
||||||
- Add more tests for edge cases and simple integration tests for the CLI.
|
|
||||||
- Expand packaging metadata and README with a longer developer and user guide.
|
|
||||||
- Add a first-pass Graph-of-Contracts registry scaffold (GoC) and a minimal adapter registry.
|
|
||||||
- Documentation: GoC overview and how to plug in new adapters.
|
|
||||||
|
|
||||||
Extensibility notes
|
## Reference Backtest
|
||||||
- The repository now includes a small GoC registry module (equicompiler_algebraic_portfolio_dsl_to_/goc_registry.py) and a registry-aware GoC skeleton integrated into the IR formation flow. This provides a stable extension point for future adapters (data feeds, brokers) and verifiable contract graphs.
|
|
||||||
- You can register adapters via the GoCRegistry class and view a digest that helps ensure reproducible builds and auditability.
|
|
||||||
|
|
||||||
- Extend the DSL with richer constraints (VaR, VaR-CVaR, liquidity, latency) and ExecutionPolicy primitives.
|
The Python backend accepts either provided price series or a deterministic synthesized scenario and returns:
|
||||||
- Integrate the GoC registry and build a canonical EquiIR representation with per-message metadata for replay/verification.
|
|
||||||
- Add a lightweight delta-sync coordinator and starter adapters for data feeds and brokers.
|
|
||||||
- Expand the test suite to exercise the new backtester and Graph-of-Contracts scaffolds.
|
|
||||||
- Improve packaging and docs to support publishing to a Python package index.
|
|
||||||
- Implement a more expressive DSL and a richer IR (EquiIR) representation.
|
|
||||||
- Add more tests for edge cases and simple integration tests for the CLI.
|
|
||||||
- Expand packaging metadata and README with a longer developer and user guide.
|
|
||||||
|
|
||||||
This README intentionally stays light; the AGENTS.md contains the deeper architectural notes for the SWARM collaborators.
|
- a portfolio curve
|
||||||
|
- total return
|
||||||
|
- volatility
|
||||||
|
- max drawdown
|
||||||
|
- VaR / CVaR estimates
|
||||||
|
- objective satisfaction
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m equicompiler_algebraic_portfolio_dsl_to_.cli path/to/strategy.dsl
|
||||||
|
python -m equicompiler_algebraic_portfolio_dsl_to_.cli path/to/strategy.dsl --backtest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Programmatic Use
|
||||||
|
|
||||||
|
```python
|
||||||
|
from equicompiler_algebraic_portfolio_dsl_to_.core import parse_dsl_to_ir
|
||||||
|
from equicompiler_algebraic_portfolio_dsl_to_.backends.python_backtester import run_backtest
|
||||||
|
|
||||||
|
ir = parse_dsl_to_ir(dsl_text)
|
||||||
|
result = run_backtest(ir)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
- Python package name: `equicompiler_algebraic_portfolio_dsl_to`
|
||||||
|
- Source package: `equicompiler_algebraic_portfolio_dsl_to_`
|
||||||
|
- Build metadata: `pyproject.toml`
|
||||||
|
- README used as the long-form package description
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run the local verification flow with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
That script installs the package in editable mode, runs the unit tests, and builds a source/wheel distribution.
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
- `core.py`: DSL parsing, IR normalization, lineage, attestations, and plan synthesis
|
||||||
|
- `delta_sync.py`: deterministic IR diffing
|
||||||
|
- `registry.py`: in-process GoC contract registry
|
||||||
|
- `goc_registry.py`: GoC skeleton builder
|
||||||
|
- `backends/python_backtester.py`: deterministic reference backtester
|
||||||
|
- `adapters/`: starter adapter interfaces for feeds and brokers
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This repository now has a coherent compiler pipeline, but it is still a reference implementation rather than a full market execution platform.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
"""EquiCompiler Algebraic Portfolio DSL - package init"""
|
"""EquiCompiler Algebraic Portfolio DSL package."""
|
||||||
|
|
||||||
__all__ = ["core", "cli"]
|
from .core import compile_strategy, parse_dsl_to_ir, to_json
|
||||||
|
|
||||||
|
__all__ = ["compile_strategy", "parse_dsl_to_ir", "to_json"]
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,143 @@
|
||||||
"""Lightweight Python backtester backend scaffold for EquiCompiler MVP."""
|
"""Deterministic Python reference backtester for EquiCompiler."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Dict, Any
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from statistics import mean, pstdev
|
||||||
|
from typing import Any, Dict, Iterable, List, Sequence
|
||||||
|
|
||||||
|
|
||||||
def run_backtest(ir: Dict[str, Any], data: Any = None) -> Dict[str, Any]:
|
def run_backtest(ir: Dict[str, Any], data: Any = None) -> Dict[str, Any]:
|
||||||
"""Deterministic, simple backtest placeholder.
|
"""Run a deterministic reference backtest against provided or synthesized prices."""
|
||||||
|
assets = list(ir.get("assets", []) or [])
|
||||||
This function is a scaffold. It returns a deterministic, small summary based on
|
prices = _normalize_prices(data, assets)
|
||||||
the IR contents (assets and objectives). It is not a real backtester, but it
|
weights = _resolve_weights(ir, assets)
|
||||||
provides a deterministic bridge for MVP tests and integration with the rest of
|
portfolio_curve = _build_portfolio_curve(prices, weights)
|
||||||
the stack.
|
returns = _series_returns(portfolio_curve)
|
||||||
"""
|
|
||||||
assets = ir.get("assets", [])
|
|
||||||
objectives = ir.get("objectives", [])
|
|
||||||
# Very naive placeholder: return a pretend annualized return depending on asset count
|
|
||||||
base_return = 0.05 * max(1, len(assets)) if assets else 0.0
|
|
||||||
summary = {
|
summary = {
|
||||||
"assets": assets,
|
"assets": assets,
|
||||||
"objective_met": any(o for o in objectives if isinstance(o, str) and o.lower().find("maximize") != -1),
|
"weights": weights,
|
||||||
"return": round(base_return, 4),
|
"curve": portfolio_curve,
|
||||||
"trace": [{"step": "init", "value": base_return}],
|
"metrics": _compute_metrics(portfolio_curve, returns),
|
||||||
|
"objective_met": _objective_met(ir, _compute_metrics(portfolio_curve, returns)),
|
||||||
|
"trace": [
|
||||||
|
{"step": "ingest", "assets": assets},
|
||||||
|
{"step": "simulate", "points": len(portfolio_curve)},
|
||||||
|
{"step": "measure", "metrics_digest": _digest({"curve": portfolio_curve})},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
__all__ = ["run_backtest"]
|
|
||||||
|
def _normalize_prices(data: Any, assets: Sequence[str]) -> Dict[str, List[float]]:
|
||||||
|
if isinstance(data, dict) and data:
|
||||||
|
normalized: Dict[str, List[float]] = {}
|
||||||
|
for asset, series in data.items():
|
||||||
|
normalized[str(asset)] = [float(point) for point in series]
|
||||||
|
return normalized
|
||||||
|
return {asset: _synthetic_price_series(asset) for asset in assets}
|
||||||
|
|
||||||
|
|
||||||
|
def _synthetic_price_series(asset: str, periods: int = 16) -> List[float]:
|
||||||
|
seed = int(hashlib.sha256(asset.encode("utf-8")).hexdigest()[:8], 16)
|
||||||
|
base = 100.0 + (seed % 150)
|
||||||
|
series = [base]
|
||||||
|
for idx in range(1, periods):
|
||||||
|
direction = 1 if ((seed >> idx) & 1) else -1
|
||||||
|
magnitude = 0.0025 + ((seed >> (idx % 11)) % 7) * 0.0008
|
||||||
|
next_price = series[-1] * (1.0 + direction * magnitude)
|
||||||
|
series.append(round(max(1.0, next_price), 4))
|
||||||
|
return series
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_weights(ir: Dict[str, Any], assets: Sequence[str]) -> Dict[str, float]:
|
||||||
|
explicit = ir.get("asset_weights", {}) or {}
|
||||||
|
if explicit:
|
||||||
|
return {asset: float(explicit.get(asset, 0.0)) for asset in assets}
|
||||||
|
if not assets:
|
||||||
|
return {}
|
||||||
|
weight = 1.0 / len(assets)
|
||||||
|
return {asset: weight for asset in assets}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_portfolio_curve(prices: Dict[str, List[float]], weights: Dict[str, float]) -> List[float]:
|
||||||
|
if not prices:
|
||||||
|
return [1.0]
|
||||||
|
length = min(len(series) for series in prices.values())
|
||||||
|
curve: List[float] = []
|
||||||
|
for idx in range(length):
|
||||||
|
value = 0.0
|
||||||
|
for asset, series in prices.items():
|
||||||
|
base = series[0] if series and series[0] else 1.0
|
||||||
|
point = series[idx]
|
||||||
|
value += weights.get(asset, 0.0) * (point / base)
|
||||||
|
curve.append(round(value, 6))
|
||||||
|
return curve
|
||||||
|
|
||||||
|
|
||||||
|
def _series_returns(curve: Sequence[float]) -> List[float]:
|
||||||
|
returns: List[float] = []
|
||||||
|
for idx in range(1, len(curve)):
|
||||||
|
prev = curve[idx - 1]
|
||||||
|
curr = curve[idx]
|
||||||
|
if prev == 0:
|
||||||
|
returns.append(0.0)
|
||||||
|
else:
|
||||||
|
returns.append((curr - prev) / prev)
|
||||||
|
return returns
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_metrics(curve: Sequence[float], returns: Sequence[float]) -> Dict[str, float]:
|
||||||
|
start = curve[0] if curve else 1.0
|
||||||
|
end = curve[-1] if curve else 1.0
|
||||||
|
total_return = (end / start) - 1.0 if start else 0.0
|
||||||
|
volatility = pstdev(returns) if len(returns) > 1 else 0.0
|
||||||
|
max_drawdown = _max_drawdown(curve)
|
||||||
|
var_95, cvar_95 = _tail_risk(returns)
|
||||||
|
return {
|
||||||
|
"total_return": round(total_return, 6),
|
||||||
|
"volatility": round(volatility, 6),
|
||||||
|
"max_drawdown": round(max_drawdown, 6),
|
||||||
|
"var_95": round(var_95, 6),
|
||||||
|
"cvar_95": round(cvar_95, 6),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _max_drawdown(curve: Sequence[float]) -> float:
|
||||||
|
peak = curve[0] if curve else 1.0
|
||||||
|
worst = 0.0
|
||||||
|
for value in curve:
|
||||||
|
peak = max(peak, value)
|
||||||
|
drawdown = (peak - value) / peak if peak else 0.0
|
||||||
|
worst = max(worst, drawdown)
|
||||||
|
return worst
|
||||||
|
|
||||||
|
|
||||||
|
def _tail_risk(returns: Sequence[float]) -> tuple[float, float]:
|
||||||
|
if not returns:
|
||||||
|
return 0.0, 0.0
|
||||||
|
ordered = sorted(returns)
|
||||||
|
tail_count = max(1, math.ceil(len(ordered) * 0.05))
|
||||||
|
tail = ordered[:tail_count]
|
||||||
|
return tail[-1], mean(tail)
|
||||||
|
|
||||||
|
|
||||||
|
def _objective_met(ir: Dict[str, Any], metrics: Dict[str, float]) -> bool:
|
||||||
|
objectives = [str(obj).lower() for obj in ir.get("objectives", []) or []]
|
||||||
|
if any("maximize" in objective for objective in objectives):
|
||||||
|
return metrics["total_return"] >= 0
|
||||||
|
if any("minimize" in objective for objective in objectives):
|
||||||
|
return metrics["max_drawdown"] <= float(ir.get("constraints", {}).get("max_drawdown", 1.0))
|
||||||
|
return bool(objectives)
|
||||||
|
|
||||||
|
|
||||||
|
def _digest(payload: Dict[str, Any]) -> str:
|
||||||
|
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def run_reference_backtest(ir: Dict[str, Any], data: Any = None) -> Dict[str, Any]:
|
||||||
|
return run_backtest(ir, data)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["run_backtest", "run_reference_backtest"]
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
"""CLI entry point for the MVP DSL -> IR translator."""
|
"""CLI entry point for the EquiCompiler DSL -> IR translator."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from .backends.python_backtester import run_backtest
|
||||||
from .core import parse_dsl_to_ir
|
from .core import parse_dsl_to_ir
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
if argv is None:
|
parser = argparse.ArgumentParser(prog="equicompiler", description="Compile EquiCompiler DSL into EquiIR")
|
||||||
argv = sys.argv[1:]
|
parser.add_argument("dsl_file", help="Path to a DSL source file")
|
||||||
if len(argv) != 1:
|
parser.add_argument("--backtest", action="store_true", help="Run the Python reference backtester")
|
||||||
print("Usage: python -m equicompiler_algebraic_portfolio_dsl_to_.cli <dsl_file>")
|
args = parser.parse_args(argv)
|
||||||
sys.exit(2)
|
|
||||||
path = argv[0]
|
with open(args.dsl_file, "r", encoding="utf-8") as f:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
dsl = f.read()
|
dsl = f.read()
|
||||||
|
|
||||||
ir = parse_dsl_to_ir(dsl)
|
ir = parse_dsl_to_ir(dsl)
|
||||||
|
if args.backtest:
|
||||||
|
ir = dict(ir)
|
||||||
|
ir["reference_backtest"] = run_backtest(ir)
|
||||||
|
|
||||||
print(json.dumps(ir, indent=2, sort_keys=True))
|
print(json.dumps(ir, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,142 +1,271 @@
|
||||||
"""Core DSL -> IR translation for the EquiCompiler MVP."""
|
"""Core DSL -> IR translation for the EquiCompiler stack."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Dict, List
|
|
||||||
import copy
|
import copy
|
||||||
from datetime import datetime
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Iterable, List, Sequence, Tuple, cast
|
||||||
|
|
||||||
# Optional GoC registry integration (extensible adapters in the future)
|
from .delta_sync import delta_sync
|
||||||
try:
|
from .goc_registry import build_minimal_goC_skeleton
|
||||||
from .goc_registry import GoCRegistry, build_minimal_goC_skeleton
|
from .registry import GraphRegistry
|
||||||
except Exception:
|
|
||||||
GoCRegistry = None # type: ignore
|
COMPILER_NAME = "EquiCompiler"
|
||||||
build_minimal_goC_skeleton = None # type: ignore
|
DEFAULT_VERSION = "0.2"
|
||||||
|
SUPPORTED_BACKENDS = (
|
||||||
|
{"name": "python_backtest", "runtime": "python", "mode": "offline"},
|
||||||
|
{"name": "cpp_live", "runtime": "cpp", "mode": "live"},
|
||||||
|
{"name": "wasm_eval", "runtime": "wasm", "mode": "browser"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_dsl_to_ir(dsl: str) -> Dict[str, object]:
|
def parse_dsl_to_ir(dsl: str) -> Dict[str, object]:
|
||||||
"""Parse a tiny, human-friendly DSL into a canonical IR dict.
|
"""Parse the strategy DSL into a canonical EquiIR dictionary."""
|
||||||
|
sections = _collect_sections(dsl)
|
||||||
|
version = _first_value(sections, "version") or DEFAULT_VERSION
|
||||||
|
assets, asset_weights = _parse_assets(_first_value(sections, "assets"))
|
||||||
|
objectives = _parse_list(_first_value(sections, "objectives"))
|
||||||
|
constraints = _parse_kv_pairs(_first_value(sections, "constraints"), coerce=False)
|
||||||
|
execution_policy = _parse_list(_first_value(sections, "execution_policy"))
|
||||||
|
risk_budget = _parse_kv_pairs(_first_value(sections, "risk_budget"), coerce=True)
|
||||||
|
market_microstructure = _parse_kv_pairs(_first_value(sections, "market_microstructure"), coerce=True)
|
||||||
|
regulatory = _parse_kv_pairs(_first_value(sections, "regulatory"), coerce=True)
|
||||||
|
|
||||||
Expected DSL syntax (very small subset for MVP):
|
metadata = {
|
||||||
assets: AAPL, MSFT, GOOG
|
"compiler": COMPILER_NAME,
|
||||||
objectives: maximize_return; minimize_risk
|
"normalized_sections": sorted(sections.keys()),
|
||||||
constraints: max_drawdown=0.2, var=0.95
|
"unknown_sections": {
|
||||||
|
key: values
|
||||||
|
for key, values in sections.items()
|
||||||
|
if key not in {
|
||||||
|
"assets",
|
||||||
|
"objectives",
|
||||||
|
"constraints",
|
||||||
|
"execution_policy",
|
||||||
|
"risk_budget",
|
||||||
|
"market_microstructure",
|
||||||
|
"regulatory",
|
||||||
|
"version",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
Returns a dict with keys: assets (list[str]), objectives (list[str]), constraints (dict[str,str]), version (str)
|
ir: Dict[str, object] = {
|
||||||
"""
|
"compiler": {
|
||||||
assets: List[str] = []
|
"name": COMPILER_NAME,
|
||||||
objectives: List[str] = []
|
"version": version,
|
||||||
constraints: Dict[str, str] = {}
|
"backend_targets": list(SUPPORTED_BACKENDS),
|
||||||
execution_policy: List[str] = []
|
},
|
||||||
# Optional version override from the DSL
|
"version": version,
|
||||||
parsed_version: str | None = None
|
|
||||||
|
|
||||||
lines = [ln.strip() for ln in dsl.strip().splitlines() if ln.strip()]
|
|
||||||
for line in lines:
|
|
||||||
lower = line.lower()
|
|
||||||
if lower.startswith("assets:"):
|
|
||||||
rest = line[len("assets:"):].strip()
|
|
||||||
assets = [a.strip() for a in rest.split(",") if a.strip()]
|
|
||||||
elif lower.startswith("objectives:"):
|
|
||||||
rest = line[len("objectives:"):].strip()
|
|
||||||
parts = [p.strip() for p in rest.replace(";", ",").split(",") if p.strip()]
|
|
||||||
objectives = parts
|
|
||||||
elif lower.startswith("constraints:"):
|
|
||||||
rest = line[len("constraints:"):].strip()
|
|
||||||
pairs = [p.strip() for p in rest.split(",") if p.strip()]
|
|
||||||
for p in pairs:
|
|
||||||
if "=" in p:
|
|
||||||
k, v = [s.strip() for s in p.split("=", 1)]
|
|
||||||
constraints[k] = v
|
|
||||||
elif lower.startswith("execution_policy:"):
|
|
||||||
# Optional execution policy descriptor for offline/online orchestration
|
|
||||||
rest = line[len("execution_policy:"):].strip()
|
|
||||||
parts = [p.strip() for p in rest.replace(";", ",").split(",") if p.strip()]
|
|
||||||
execution_policy = parts
|
|
||||||
elif lower.startswith("version:"):
|
|
||||||
# Allow users to explicitly set the IR version via the DSL
|
|
||||||
rest = line[len("version:"):].strip()
|
|
||||||
if rest:
|
|
||||||
parsed_version = rest
|
|
||||||
|
|
||||||
ir = {
|
|
||||||
"assets": assets,
|
"assets": assets,
|
||||||
|
"asset_weights": asset_weights,
|
||||||
"objectives": objectives,
|
"objectives": objectives,
|
||||||
"constraints": constraints,
|
"constraints": constraints,
|
||||||
"execution_policy": execution_policy,
|
"execution_policy": execution_policy,
|
||||||
# Default remains 0.1 unless overridden by DSL via `version:` line
|
"risk_budget": risk_budget,
|
||||||
"version": parsed_version if parsed_version is not None else "0.1",
|
"market_microstructure": market_microstructure,
|
||||||
|
"regulatory": regulatory,
|
||||||
|
"metadata": metadata,
|
||||||
}
|
}
|
||||||
# Attach lightweight, deterministic attestations to support auditability.
|
ir["plan"] = _build_execution_plan(ir)
|
||||||
ir = _attach_attestations(ir)
|
ir["lineage"] = _build_lineage(ir)
|
||||||
return ir
|
return _attach_attestations(ir)
|
||||||
|
|
||||||
|
|
||||||
def to_json(ir: Dict[str, object]) -> str:
|
def to_json(ir: Dict[str, object]) -> str:
|
||||||
return json.dumps(ir, indent=2, sort_keys=True)
|
return json.dumps(ir, indent=2, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
def _attach_attestations(ir: Dict[str, object]) -> Dict[str, object]:
|
def compile_strategy(dsl: str) -> Dict[str, object]:
|
||||||
"""Attach deterministic attestations to the IR for auditability.
|
"""Compatibility wrapper for callers that want a compiler-style entry point."""
|
||||||
|
return parse_dsl_to_ir(dsl)
|
||||||
|
|
||||||
This is a lightweight, deterministic placeholder for cryptographic attestations.
|
|
||||||
It computes a SHA-256 digest of the IR excluding the attestations field and
|
def _collect_sections(dsl: str) -> Dict[str, List[str]]:
|
||||||
adds per-step attestations to the IR under the 'attestations' key.
|
sections: Dict[str, List[str]] = {}
|
||||||
"""
|
for raw_line in dsl.splitlines():
|
||||||
# Work on a shallow copy to avoid mutating input in unexpected ways
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if ":" not in line:
|
||||||
|
sections.setdefault("notes", []).append(line)
|
||||||
|
continue
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
key = key.strip().lower().replace("-", "_")
|
||||||
|
sections.setdefault(key, []).append(value.strip())
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def _first_value(sections: Dict[str, List[str]], key: str) -> str:
|
||||||
|
values = sections.get(key, [])
|
||||||
|
return " ".join(v for v in values if v).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_list(value: str) -> List[str]:
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
items = [part.strip() for part in value.replace(";", ",").split(",")]
|
||||||
|
return [item for item in items if item]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_assets(value: str) -> Tuple[List[str], Dict[str, float]]:
|
||||||
|
assets: List[str] = []
|
||||||
|
weights: Dict[str, float] = {}
|
||||||
|
for token in _parse_list(value):
|
||||||
|
symbol, weight = _split_symbol_weight(token)
|
||||||
|
assets.append(symbol)
|
||||||
|
if weight is not None:
|
||||||
|
weights[symbol] = weight
|
||||||
|
return assets, weights
|
||||||
|
|
||||||
|
|
||||||
|
def _split_symbol_weight(token: str) -> Tuple[str, float | None]:
|
||||||
|
for separator in ("@", ":"):
|
||||||
|
if separator in token:
|
||||||
|
symbol, raw_weight = token.split(separator, 1)
|
||||||
|
symbol = symbol.strip()
|
||||||
|
raw_weight = raw_weight.strip()
|
||||||
|
try:
|
||||||
|
return symbol, float(raw_weight)
|
||||||
|
except ValueError:
|
||||||
|
return symbol, None
|
||||||
|
return token.strip(), None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_kv_pairs(value: str, *, coerce: bool) -> Dict[str, object]:
|
||||||
|
parsed: Dict[str, object] = {}
|
||||||
|
if not value:
|
||||||
|
return parsed
|
||||||
|
for token in _parse_list(value):
|
||||||
|
if "=" not in token:
|
||||||
|
parsed[token] = True
|
||||||
|
continue
|
||||||
|
key, raw_value = token.split("=", 1)
|
||||||
|
parsed[key.strip()] = _coerce_scalar(raw_value.strip()) if coerce else raw_value.strip()
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_scalar(value: str) -> object:
|
||||||
|
lowered = value.lower()
|
||||||
|
if lowered in {"true", "false"}:
|
||||||
|
return lowered == "true"
|
||||||
|
try:
|
||||||
|
if "." in value or "e" in lowered:
|
||||||
|
return float(value)
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _build_execution_plan(ir: Dict[str, object]) -> Dict[str, object]:
|
||||||
|
assets = cast(List[object], ir.get("assets", []))
|
||||||
|
objectives = cast(List[object], ir.get("objectives", []))
|
||||||
|
execution_policy = cast(List[object], ir.get("execution_policy", []))
|
||||||
|
registry = GraphRegistry()
|
||||||
|
registry.register_contract("equiir-core", str(ir.get("version", DEFAULT_VERSION)), "compiler")
|
||||||
|
registry.register_contract("python-backtest", "0.1", "python_backtester")
|
||||||
|
registry.register_contract("cpp-live", "0.1", "cpp_live_runtime")
|
||||||
|
registry.register_contract("wasm-eval", "0.1", "wasm_eval_runtime")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kind": "execution_plan",
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"name": "normalize",
|
||||||
|
"actions": [
|
||||||
|
"canonicalize_assets",
|
||||||
|
"canonicalize_constraints",
|
||||||
|
"canonicalize_execution_policy",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "verify",
|
||||||
|
"actions": [
|
||||||
|
"hash_ir",
|
||||||
|
"attach_attestations",
|
||||||
|
"build_graph_of_contracts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dispatch",
|
||||||
|
"actions": [
|
||||||
|
"python_backtest",
|
||||||
|
"cpp_live",
|
||||||
|
"wasm_eval",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"targets": list(SUPPORTED_BACKENDS),
|
||||||
|
"contract_registry": registry.list_contracts(),
|
||||||
|
"summary": {
|
||||||
|
"asset_count": len(assets),
|
||||||
|
"objective_count": len(objectives),
|
||||||
|
"execution_policy_count": len(execution_policy),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
def _build_lineage(ir: Dict[str, object]) -> Dict[str, object]:
|
||||||
|
payload = {
|
||||||
|
"version": ir.get("version"),
|
||||||
|
"assets": ir.get("assets", []),
|
||||||
|
"objectives": ir.get("objectives", []),
|
||||||
|
"constraints": ir.get("constraints", {}),
|
||||||
|
"execution_policy": ir.get("execution_policy", []),
|
||||||
|
"risk_budget": ir.get("risk_budget", {}),
|
||||||
|
}
|
||||||
|
digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
||||||
|
return {"digest": digest, "canonical": payload}
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_attestations(ir: Dict[str, object]) -> Dict[str, object]:
|
||||||
|
"""Attach deterministic attestations to the IR for auditability."""
|
||||||
ir_copy = copy.deepcopy(ir)
|
ir_copy = copy.deepcopy(ir)
|
||||||
base_for_digest = {k: v for k, v in ir_copy.items() if k != "attestations"}
|
base_for_digest = {k: v for k, v in ir_copy.items() if k != "attestations"}
|
||||||
digest = hashlib.sha256(json.dumps(base_for_digest, sort_keys=True).encode("utf-8")).hexdigest()
|
digest = hashlib.sha256(json.dumps(base_for_digest, sort_keys=True).encode("utf-8")).hexdigest()
|
||||||
timestamp = datetime.utcnow().isoformat() + "Z"
|
timestamp = datetime.utcnow().isoformat() + "Z"
|
||||||
|
|
||||||
attestations = [
|
ir_copy["attestations"] = [
|
||||||
{"step": "parse", "timestamp": timestamp, "digest": digest, "algorithm": "SHA-256"},
|
{"step": "parse", "timestamp": timestamp, "digest": digest, "algorithm": "SHA-256"},
|
||||||
|
{"step": "normalize", "timestamp": timestamp, "digest": digest, "algorithm": "SHA-256"},
|
||||||
{"step": "verify", "timestamp": timestamp, "digest": digest, "algorithm": "SHA-256"},
|
{"step": "verify", "timestamp": timestamp, "digest": digest, "algorithm": "SHA-256"},
|
||||||
]
|
]
|
||||||
ir_copy["attestations"] = attestations
|
|
||||||
# Attach a lightweight Graph-of-Contracts (GoC) skeleton to the IR
|
|
||||||
ir_copy["graph_of_contracts"] = _build_goC_skeleton(ir_copy, digest, timestamp)
|
ir_copy["graph_of_contracts"] = _build_goC_skeleton(ir_copy, digest, timestamp)
|
||||||
|
ir_copy["delta_sync"] = {
|
||||||
|
"digest": digest,
|
||||||
|
"mode": "deterministic",
|
||||||
|
"summary": delta_sync({"version": ir_copy.get("version")}, ir_copy),
|
||||||
|
}
|
||||||
return ir_copy
|
return ir_copy
|
||||||
|
|
||||||
|
|
||||||
def _build_goC_skeleton(ir_with_attestations: Dict[str, object], base_digest: str, base_timestamp: str) -> Dict[str, object]:
|
def _build_goC_skeleton(ir_with_attestations: Dict[str, object], base_digest: str, base_timestamp: str) -> Dict[str, object]:
|
||||||
"""Create a minimal Graph-of-Contracts (GoC) skeleton for the IR.
|
"""Create a minimal Graph-of-Contracts skeleton for the IR."""
|
||||||
|
|
||||||
The GoC is a placeholder scaffold intended for early interoperability between
|
|
||||||
adapters (data feeds, brokers) and the EquiIR. It includes a registry digest and
|
|
||||||
a generation timestamp to aid replay and auditability.
|
|
||||||
"""
|
|
||||||
# Attempt to compute a compact digest from core IR parts to seed the GoC metadata
|
|
||||||
try:
|
try:
|
||||||
base_for_goct = {
|
base_for_goc = {
|
||||||
"assets": ir_with_attestations.get("assets", []),
|
"assets": ir_with_attestations.get("assets", []),
|
||||||
"objectives": ir_with_attestations.get("objectives", []),
|
"objectives": ir_with_attestations.get("objectives", []),
|
||||||
"constraints": ir_with_attestations.get("constraints", {}),
|
"constraints": ir_with_attestations.get("constraints", {}),
|
||||||
|
"execution_policy": ir_with_attestations.get("execution_policy", []),
|
||||||
}
|
}
|
||||||
digest = hashlib.sha256(json.dumps(base_for_goct, sort_keys=True).encode("utf-8")).hexdigest()
|
digest = hashlib.sha256(json.dumps(base_for_goc, sort_keys=True).encode("utf-8")).hexdigest()
|
||||||
except Exception:
|
except Exception:
|
||||||
digest = base_digest
|
digest = base_digest
|
||||||
|
|
||||||
# Prefer a registry-driven skeleton when available
|
skeleton = build_minimal_goC_skeleton(digest, base_timestamp)
|
||||||
if build_minimal_goC_skeleton is not None:
|
skeleton["contracts"] = [
|
||||||
try:
|
{
|
||||||
return build_minimal_goC_skeleton(digest, base_timestamp)
|
"name": "equiir-core",
|
||||||
except Exception:
|
"version": str(ir_with_attestations.get("version", DEFAULT_VERSION)),
|
||||||
pass
|
"adapter": "compiler",
|
||||||
|
|
||||||
# Fallback to a lightweight, self-contained skeleton
|
|
||||||
# Include both a source_digest and a registry_digest to enable auditability
|
|
||||||
# and to satisfy downstream tests that expect a registry_digest field.
|
|
||||||
return {
|
|
||||||
"version": "0.1",
|
|
||||||
"contracts": [],
|
|
||||||
"metadata": {
|
|
||||||
"generated_at": base_timestamp,
|
|
||||||
"registry_digest": digest,
|
|
||||||
"source_digest": digest,
|
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
"name": "reference-backtest",
|
||||||
|
"version": "0.1",
|
||||||
|
"adapter": "python_backtester",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
skeleton["metadata"]["source_digest"] = digest
|
||||||
|
return skeleton
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["parse_dsl_to_ir", "to_json"]
|
__all__ = ["compile_strategy", "parse_dsl_to_ir", "to_json"]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ from typing import Dict, Any
|
||||||
|
|
||||||
def _digest(ir: Dict[str, Any]) -> str:
|
def _digest(ir: Dict[str, Any]) -> str:
|
||||||
"""Compute a stable digest for a given IR dictionary."""
|
"""Compute a stable digest for a given IR dictionary."""
|
||||||
return hashlib.sha256(json.dumps(ir, sort_keys=True).encode("utf-8")).hexdigest()
|
canonical = {k: v for k, v in ir.items() if k not in {"attestations", "graph_of_contracts", "delta_sync"}}
|
||||||
|
return hashlib.sha256(json.dumps(canonical, sort_keys=True).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def delta_sync(old_ir: Dict[str, Any], new_ir: Dict[str, Any]) -> Dict[str, Any]:
|
def delta_sync(old_ir: Dict[str, Any], new_ir: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|
@ -30,9 +31,13 @@ def delta_sync(old_ir: Dict[str, Any], new_ir: Dict[str, Any]) -> Dict[str, Any]
|
||||||
"assets_added": sorted(list(new_assets - old_assets)),
|
"assets_added": sorted(list(new_assets - old_assets)),
|
||||||
"assets_removed": sorted(list(old_assets - new_assets)),
|
"assets_removed": sorted(list(old_assets - new_assets)),
|
||||||
"constraints_changed": {}, # naive: compute by key presence and value change
|
"constraints_changed": {}, # naive: compute by key presence and value change
|
||||||
|
"risk_budget_changed": {},
|
||||||
|
"market_microstructure_changed": {},
|
||||||
|
"regulatory_changed": {},
|
||||||
"execution_policy_added": [],
|
"execution_policy_added": [],
|
||||||
"execution_policy_removed": [],
|
"execution_policy_removed": [],
|
||||||
"objectives_changed": {}, # {<objective_key>: {"old": <old>, "new": <new>}}
|
"objectives_changed": {}, # {<objective_key>: {"old": <old>, "new": <new>}}
|
||||||
|
"plan_changed": old_ir.get("plan") != new_ir.get("plan"),
|
||||||
"digest": _digest(new_ir),
|
"digest": _digest(new_ir),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +50,16 @@ def delta_sync(old_ir: Dict[str, Any], new_ir: Dict[str, Any]) -> Dict[str, Any]
|
||||||
if ov != nv:
|
if ov != nv:
|
||||||
delta["constraints_changed"][k] = {"old": ov, "new": nv}
|
delta["constraints_changed"][k] = {"old": ov, "new": nv}
|
||||||
|
|
||||||
|
for field_name in ("risk_budget", "market_microstructure", "regulatory"):
|
||||||
|
old_field = old_ir.get(field_name, {}) or {}
|
||||||
|
new_field = new_ir.get(field_name, {}) or {}
|
||||||
|
all_field_keys = set(old_field.keys()) | set(new_field.keys())
|
||||||
|
for k in all_field_keys:
|
||||||
|
ov = old_field.get(k)
|
||||||
|
nv = new_field.get(k)
|
||||||
|
if ov != nv:
|
||||||
|
delta[f"{field_name}_changed"][k] = {"old": ov, "new": nv}
|
||||||
|
|
||||||
# Execution policy delta (order-insensitive for MVP)
|
# Execution policy delta (order-insensitive for MVP)
|
||||||
old_policy = set(old_ir.get("execution_policy", []) or [])
|
old_policy = set(old_ir.get("execution_policy", []) or [])
|
||||||
new_policy = set(new_ir.get("execution_policy", []) or [])
|
new_policy = set(new_ir.get("execution_policy", []) or [])
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,18 @@ version = "0.1.0"
|
||||||
description = "Algebraic Portfolio DSL to verifiable low-latency market strategy compiler (MVP)"
|
description = "Algebraic Portfolio DSL to verifiable low-latency market strategy compiler (MVP)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = {text = "MIT"}
|
license = "MIT"
|
||||||
authors = [ { name = "OpenCode Team" } ]
|
authors = [ { name = "OpenCode Team" } ]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Office/Business :: Financial",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://example.com/equicompiler"
|
||||||
|
Repository = "https://example.com/equicompiler.git"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
|
|
|
||||||
1
test.sh
1
test.sh
|
|
@ -17,6 +17,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building package..."
|
echo "Building package..."
|
||||||
|
python3 -m pip install build
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
||||||
echo "All tests executed."
|
echo "All tests executed."
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from equicompiler_algebraic_portfolio_dsl_to_.backends.python_backtester import run_backtest
|
||||||
|
from equicompiler_algebraic_portfolio_dsl_to_.core import parse_dsl_to_ir
|
||||||
|
from equicompiler_algebraic_portfolio_dsl_to_.delta_sync import delta_sync
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompilerPipeline(unittest.TestCase):
|
||||||
|
def test_richer_dsl_sections_are_parsed(self):
|
||||||
|
dsl = (
|
||||||
|
"version: 0.3\n"
|
||||||
|
"assets: AAPL@0.6, MSFT@0.4\n"
|
||||||
|
"objectives: maximize_return; minimize_risk\n"
|
||||||
|
"constraints: max_drawdown=0.15, var=0.95, cvar=0.9\n"
|
||||||
|
"risk_budget: max_var=0.1, max_exposure=0.8\n"
|
||||||
|
"execution_policy: offline, delta_sync, rebalance=1d\n"
|
||||||
|
"regulatory: venue=us, suitability=true"
|
||||||
|
)
|
||||||
|
ir = parse_dsl_to_ir(dsl)
|
||||||
|
self.assertEqual(ir["version"], "0.3")
|
||||||
|
self.assertEqual(ir["assets"], ["AAPL", "MSFT"])
|
||||||
|
self.assertEqual(ir["asset_weights"]["AAPL"], 0.6)
|
||||||
|
self.assertEqual(ir["risk_budget"]["max_var"], 0.1)
|
||||||
|
self.assertTrue(ir["regulatory"]["suitability"])
|
||||||
|
self.assertIn("plan", ir)
|
||||||
|
self.assertGreaterEqual(len(ir["plan"]["phases"]), 3)
|
||||||
|
self.assertIn("lineage", ir)
|
||||||
|
|
||||||
|
def test_reference_backtest_produces_metrics(self):
|
||||||
|
ir = parse_dsl_to_ir(
|
||||||
|
"assets: AAPL, MSFT\n"
|
||||||
|
"objectives: maximize_return\n"
|
||||||
|
"constraints: max_drawdown=0.2"
|
||||||
|
)
|
||||||
|
result = run_backtest(ir)
|
||||||
|
self.assertIn("metrics", result)
|
||||||
|
self.assertIn("total_return", result["metrics"])
|
||||||
|
self.assertIn("max_drawdown", result["metrics"])
|
||||||
|
self.assertIsInstance(result["objective_met"], bool)
|
||||||
|
|
||||||
|
def test_delta_sync_detects_plan_changes(self):
|
||||||
|
base = parse_dsl_to_ir(
|
||||||
|
"assets: AAPL, MSFT\n"
|
||||||
|
"objectives: maximize_return\n"
|
||||||
|
"constraints: max_drawdown=0.2"
|
||||||
|
)
|
||||||
|
updated = parse_dsl_to_ir(
|
||||||
|
"assets: AAPL, MSFT, GOOG\n"
|
||||||
|
"objectives: maximize_return\n"
|
||||||
|
"constraints: max_drawdown=0.15"
|
||||||
|
)
|
||||||
|
delta = delta_sync(base, updated)
|
||||||
|
self.assertIn("assets_added", delta)
|
||||||
|
self.assertIn("GOOG", delta["assets_added"])
|
||||||
|
self.assertTrue(delta["plan_changed"])
|
||||||
|
self.assertIn("constraints_changed", delta)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue