diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0d15981 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# APPS Agents Documentation + +Architecture: Python MVP for Algebraic Portfolio Provenance Studio (APPS) +- Core DSL: algebraic_portfolio_provenance_studio_ve.dsl +- Deterministic Backtester: algebraic_portfolio_provenance_studio_ve.simulator +- Graph-of-Contracts registry: algebraic_portfolio_provenance_studio_ve.registry +- Adapters: algebraic_portfolio_provenance_studio_ve.adapters +- Tests: tests/test_basic.py + +How to run locally: +- pytest -q +- python -m build + +Packaging integration: +- pyproject.toml defines the package name algebraic_portfolio_provenance_studio_ve + +Conventions: +- Minimal, well-scoped MVP. Each module is a stepping stone toward the full APPS architecture. diff --git a/README.md b/README.md index 57f8fab..f67dda2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ -# algebraic-portfolio-provenance-studio-ve +APPS: Algebraic Portfolio Provenance Studio -Gap addressed: existing investment tooling often relies on opaque, hard-to-audit solver code, with limited offline testing, restricted data-sharing, and weak cross-venue governance. There is a need for a lightweight, open, end-to-end toolchain that l \ No newline at end of file +Overview +- Lightweight, end-to-end DSL for assets, objectives, risk budgets, and per-step plan deltas. +- Verifiable, audit-friendly backtesting with offline-first capabilities and a minimal Graph-of-Contracts registry scaffold. +- MVP: Python-based implementation suitable for local testing, with deterministic backtests and two toy adapters. + +How to run +- Install tooling: python -m pip install -e . +- Run tests: pytest -q +- Build package: python -m build + +Project layout (high level) +- algebraic_portfolio_provenance_studio_ve/: core library (dsl, simulator, registry, adapters) +- tests/: unit tests for MVP +- AGENTS.md: architecture and testing commands +- test.sh: test runner script (generated in this repo) +- READY_TO_PUBLISH: marker for publishing (created at finish) diff --git a/algebraic_portfolio_provenance_studio_ve/__init__.py b/algebraic_portfolio_provenance_studio_ve/__init__.py new file mode 100644 index 0000000..2789fa7 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/__init__.py @@ -0,0 +1,21 @@ +"""algebraic_portfolio_provenance_studio_ve +Minimal MVP scaffolding for APPS: Algebraic Portfolio Provenance Studio. + +This package provides a tiny DSL representation, a deterministic backtester, +and simple adapters to bootstrap offline-first testing and cross-venue ideas. +""" + +from .dsl import LocalAsset, Objective, RiskBudget, PlanDelta, SharedSignals, AuditLog +from .simulator import DeterministicBacktest +from .registry import GoCRegistry + +__all__ = [ + "LocalAsset", + "Objective", + "RiskBudget", + "PlanDelta", + "SharedSignals", + "AuditLog", + "DeterministicBacktest", + "GoCRegistry", +] diff --git a/algebraic_portfolio_provenance_studio_ve/adapters/__init__.py b/algebraic_portfolio_provenance_studio_ve/adapters/__init__.py new file mode 100644 index 0000000..09de462 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapters package for APPS MVP.""" diff --git a/algebraic_portfolio_provenance_studio_ve/adapters/equities.py b/algebraic_portfolio_provenance_studio_ve/adapters/equities.py new file mode 100644 index 0000000..70616e0 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/adapters/equities.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from typing import Dict, List + + +def price_series_equities(symbols: List[str], seed: int = 1) -> Dict[str, List[float]]: + # Simple deterministic series: start at 100 and apply a tiny walk + prices: Dict[str, List[float]] = {} + base = 100.0 + for s in symbols: + series: List[float] = [] + val = base + for i in range(steps := 10): + val = max(1.0, val * (1.0 + ((i * 13 + len(s)) % 5 - 2) * 0.01)) + series.append(round(val, 2)) + prices[s] = series + return prices diff --git a/algebraic_portfolio_provenance_studio_ve/adapters/fixed_income.py b/algebraic_portfolio_provenance_studio_ve/adapters/fixed_income.py new file mode 100644 index 0000000..5f9606d --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/adapters/fixed_income.py @@ -0,0 +1,11 @@ +from __future__ import annotations +from typing import Dict, List + + +def price_series_fixed_income(bond_symbols: List[str]) -> Dict[str, List[float]]: + # Simple deterministic coupon-like par values with small drift + prices: Dict[str, List[float]] = {} + for s in bond_symbols: + series = [100.0, 99.5, 99.8, 100.2, 100.5, 100.2, 99.9, 100.1, 100.3, 100.0] + prices[s] = series + return prices diff --git a/algebraic_portfolio_provenance_studio_ve/dsl.py b/algebraic_portfolio_provenance_studio_ve/dsl.py new file mode 100644 index 0000000..732c211 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/dsl.py @@ -0,0 +1,77 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict, Optional, Any, List + +@dataclass +class LocalAsset: + symbol: str + asset_class: str # e.g., Equity, Bond + notional: float + constraints: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "symbol": self.symbol, + "asset_class": self.asset_class, + "notional": self.notional, + "constraints": self.constraints, + } + + +@dataclass +class Objective: + target_return: Optional[float] = None + target_vol: Optional[float] = None + target_sharpe: Optional[float] = None + liquidity_budget: Optional[float] = None + + def to_dict(self) -> Dict[str, Any]: + return { + "target_return": self.target_return, + "target_vol": self.target_vol, + "target_sharpe": self.target_sharpe, + "liquidity_budget": self.liquidity_budget, + } + + +@dataclass +class RiskBudget: + max_drawdown: Optional[float] = None + tail_risk: Optional[float] = None + exposure_caps: Dict[str, float] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "max_drawdown": self.max_drawdown, + "tail_risk": self.tail_risk, + "exposure_caps": self.exposure_caps, + } + + +@dataclass +class PlanDelta: + step: int + deltas: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return {"step": self.step, "deltas": self.deltas} + + +@dataclass +class SharedSignals: + signals: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return {"signals": self.signals} + + +@dataclass +class AuditLog: + events: List[Dict[str, Any]] = field(default_factory=list) + version: str = "0.0.1" + + def log(self, event: Dict[str, Any]) -> None: + self.events.append(event) + + def to_dict(self) -> Dict[str, Any]: + return {"events": self.events, "version": self.version} diff --git a/algebraic_portfolio_provenance_studio_ve/registry.py b/algebraic_portfolio_provenance_studio_ve/registry.py new file mode 100644 index 0000000..84be231 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/registry.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from typing import Dict, Any + + +class GoCRegistry: + """Graph-of-Contracts registry scaffold. + + Keeps a tiny in-memory map of canonical contract versions and adapter stubs. + This is a minimal MVP placeholder to exercise the architecture. + """ + + def __init__(self) -> None: + self._registry: Dict[str, Dict[str, Any]] = {} + + def register(self, contract_id: str, version: str, meta: Dict[str, Any]) -> None: + self._registry[contract_id] = {"version": version, "meta": meta} + + def get(self, contract_id: str) -> Dict[str, Any]: + return self._registry.get(contract_id, {}) diff --git a/algebraic_portfolio_provenance_studio_ve/simulator.py b/algebraic_portfolio_provenance_studio_ve/simulator.py new file mode 100644 index 0000000..4d02c39 --- /dev/null +++ b/algebraic_portfolio_provenance_studio_ve/simulator.py @@ -0,0 +1,49 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, List, Any + + +@dataclass +class DeterministicBacktest: + assets: List[str] + initial_notional: float + steps: int + deltas: List[Dict[str, float]] # per-step rebalancing deltas in allocation fractions + + def run(self) -> Dict[str, Any]: + # Simple deterministic backtest: start with equal allocation (or specified by deltas[0]), then apply deltas. + n = len(self.assets) + # Initialize equal weights if not provided + if self.deltas and len(self.deltas) >= 1: + weights = [self.deltas[0].get(a, 0.0) for a in self.assets] + else: + weights = [1.0 / n] * n + + # Normalize + total = sum(weights) or 1.0 + weights = [w / total for w in weights] + + history = [] + cash = 0.0 + notional = self.initial_notional + for step in range(self.steps): + # Apply delta if provided for this step + if step < len(self.deltas): + d = self.deltas[step] + for i, a in enumerate(self.assets): + if a in d: + weights[i] = max(0.0, d[a]) + # renormalize + total = sum(weights) or 1.0 + weights = [w / total for w in weights] + + # compute position values + values = {a: notional * w for a, w in zip(self.assets, weights)} + step_entry = { + "step": step, + "weights": dict(zip(self.assets, weights)), + "values": values, + } + history.append(step_entry) + + return {"initial_notional": self.initial_notional, "steps": self.steps, "history": history} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..72fa6e7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "algebraic_portfolio_provenance_studio_ve" +version = "0.1.0" +description = "MVP: Algebraic Portfolio Provenance Studio (APPS) in Python" +authors = [{name = "OpenCode", email = "devnull@example.com"}] +readme = "README.md" + +[tool.setuptools] +package-dir = { "" = "." } + +[tool.setuptools.packages.find] +where = ["."] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..3ab4e88 --- /dev/null +++ b/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure the repository root is on PYTHONPATH so tests can import the local package +export PYTHONPATH="$(pwd):${PYTHONPATH:-}" + +echo "Running tests..." +pytest -q + +echo "Building package (verify pyproject)..." +python3 -m build + +echo "All tests passed and build completed." diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..8c937f7 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,27 @@ +from algebraic_portfolio_provenance_studio_ve.dsl import LocalAsset, Objective, RiskBudget, PlanDelta, SharedSignals, AuditLog +from algebraic_portfolio_provenance_studio_ve.simulator import DeterministicBacktest + + +def test_basic_dsl_construction_and_backtest(): + # Build a tiny DSL example + a1 = LocalAsset(symbol="AAPL", asset_class="Equity", notional=50000.0) + a2 = LocalAsset(symbol="TBOND", asset_class="FixedIncome", notional=50000.0) + obj = Objective(target_return=0.08, target_vol=0.15) + rb = RiskBudget(max_drawdown=0.2, tail_risk=0.05, exposure_caps={"AAPL": 0.6, "TBOND": 0.5}) + delta = PlanDelta(step=0, deltas={"AAPL": 0.5, "TBOND": 0.5}) + + # Run a tiny simulated backtest + backtest = DeterministicBacktest( + assets=[a1.symbol, a2.symbol], + initial_notional=a1.notional + a2.notional, + steps=3, + deltas=[{"AAPL": 0.6, "TBOND": 0.4}, {"AAPL": 0.4, "TBOND": 0.6}, {"AAPL": 0.5, "TBOND": 0.5}], + ) + result = backtest.run() + assert isinstance(result, dict) + assert "history" in result + assert len(result["history"]) == 3 + # sanity: history steps correspond to allocated weights that sum to 1.0 + for st in result["history"]: + w = st["weights"] + assert abs(sum(w.values()) - 1.0) < 1e-6