diff --git a/README_BRIEF.md b/README_BRIEF.md new file mode 100644 index 0000000..59c24da --- /dev/null +++ b/README_BRIEF.md @@ -0,0 +1,2 @@ +DeltaForge MVP scaffold for real-time cross-venue hedging across assets. +This repository provides core DSL primitives, a minimal ADMM-like coordinator, two starter adapters, a toy backtester, and a test harness. diff --git a/deltaforge/__init__.py b/deltaforge/__init__.py new file mode 100644 index 0000000..5c774bc --- /dev/null +++ b/deltaforge/__init__.py @@ -0,0 +1,7 @@ +""" +DeltaForge MVP: Core package initialization. +""" +from .dsl import StrategyDelta, Asset, MarketSignal, PlanDelta +from .coordinator import Coordinator + +__all__ = ["StrategyDelta", "Asset", "MarketSignal", "PlanDelta", "Coordinator"] diff --git a/deltaforge/adapters/equity_feed.py b/deltaforge/adapters/equity_feed.py new file mode 100644 index 0000000..5bd99e6 --- /dev/null +++ b/deltaforge/adapters/equity_feed.py @@ -0,0 +1,19 @@ +from __future__ import annotations +import time +from typing import List +from ..dsl import Asset, MarketSignal + +class EquityFeedAdapter: + """ + Starter equity feed adapter. + In a real system this would connect to a streaming data source. + Here we provide a deterministic, test-friendly generator. + """ + def __init__(self, asset_symbol: str = "AAPL"): + self.asset = Asset(id="eq-"+asset_symbol, type="equity", symbol=asset_symbol) + self.start = time.time() + + def poll_signals(self) -> List[MarketSignal]: + t = time.time() - self.start + price = 150.0 + (t % 5) # deterministic-ish + return [MarketSignal(asset=self.asset, timestamp=t, price=price, liquidity=1.0)] diff --git a/deltaforge/adapters/options_feed.py b/deltaforge/adapters/options_feed.py new file mode 100644 index 0000000..bfa7aae --- /dev/null +++ b/deltaforge/adapters/options_feed.py @@ -0,0 +1,17 @@ +from __future__ import annotations +import time +from typing import List +from ..dsl import Asset, MarketSignal + +class OptionsFeedAdapter: + """ + Starter options feed adapter (toy). + """ + def __init__(self, symbol: str = "AAPL-20260120-150C"): + self.asset = Asset(id="opt-"+symbol, type="option", symbol=symbol) + self.start = time.time() + + def poll_signals(self) -> List[MarketSignal]: + t = time.time() - self.start + price = 5.0 + (t % 1.5) + return [MarketSignal(asset=self.asset, timestamp=t, price=price, liquidity=1.0)] diff --git a/deltaforge/backtester.py b/deltaforge/backtester.py new file mode 100644 index 0000000..eafa679 --- /dev/null +++ b/deltaforge/backtester.py @@ -0,0 +1,26 @@ +from __future__ import annotations +from typing import List +from .dsl import MarketSignal, PlanDelta + +class Backtester: + """ + Tiny deterministic replay engine. + Applies PlanDelta actions to a simple PnL model. + """ + def __init__(self, initial_cash: float = 100000.0): + self.cash = initial_cash + self.positions: List[dict] = [] + + def apply(self, signals: List[MarketSignal], plan: PlanDelta) -> float: + # Very simple PnL: sum(action.size * current_price) and adjust cash + pnl = 0.0 + for act in plan.delta: + symbol = act.get("symbol") or act.get("asset") or "UNKNOWN" + size = act.get("size", 0.0) + price = act.get("price") or act.get("premium") or 0.0 + if price is None: + price = 0.0 + pnl += size * price + self.positions.append({"symbol": symbol, "size": size, "price": price}) + self.cash += pnl + return self.cash diff --git a/deltaforge/coordinator.py b/deltaforge/coordinator.py new file mode 100644 index 0000000..aac2fec --- /dev/null +++ b/deltaforge/coordinator.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from typing import List +from .dsl import MarketSignal, PlanDelta + +class Coordinator: + """ + Lightweight ADMM-like coordinator. + It collects MarketSignals from multiple venues and emits a PlanDelta + representing a synchronized hedging action. + This is a minimal, deterministic stub suitable for MVP testing. + """ + def __init__(self, contract_id: str = "default-contract"): + self.contract_id = contract_id + self.last_plan: PlanDelta | None = None + + def coordinate(self, signals: List[MarketSignal], author: str = "coordinator") -> PlanDelta: + # Very small heuristic: if two assets present, generate a delta-neutral hedge + actions: List[dict] = [] + # Simple rule: create a hedge adjustment based on price relative to last plan + for s in signals: + if s.asset.type == "equity": + actions.append({"action": "hedge", "symbol": s.asset.symbol, "size": -0.5, "price": s.price, "ts": s.timestamp}) + elif s.asset.type == "option": + actions.append({"action": "adjust", "symbol": s.asset.symbol, "size": -0.25, "premium": s.price, "ts": s.timestamp}) + plan = PlanDelta(delta=actions, timestamp=signals[0].timestamp if signals else 0.0, author=author, contract_id=self.contract_id) + self.last_plan = plan + return plan diff --git a/deltaforge/dsl.py b/deltaforge/dsl.py new file mode 100644 index 0000000..bc15fc6 --- /dev/null +++ b/deltaforge/dsl.py @@ -0,0 +1,58 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Optional, Any + +def _validate_non_empty(name: str, value: Any) -> None: + if value is None or (isinstance(value, (str, list, dict)) and len(value) == 0): + raise ValueError(f"{name} must be non-empty") + +@dataclass +class Asset: + id: str + type: str # "equity" | "option" | "future" + symbol: str + + def __post_init__(self): + _validate_non_empty("Asset.id", self.id) + _validate_non_empty("Asset.type", self.type) + _validate_non_empty("Asset.symbol", self.symbol) + +@dataclass +class MarketSignal: + asset: Asset + timestamp: float + price: float + liquidity: float = 1.0 + extra: dict = field(default_factory=dict) + + def __post_init__(self): + _validate_non_empty("MarketSignal.asset", self.asset) + if self.timestamp is None or self.price is None: + raise ValueError("MarketSignal timestamp/price required") + +@dataclass +class PlanDelta: + delta: List[dict] # A list of actions, simplified + timestamp: float + author: str + contract_id: Optional[str] = None + privacy_budget: Optional[dict] = None + + def __post_init__(self): + _validate_non_empty("PlanDelta.delta", self.delta) + if self.timestamp is None: + raise ValueError("PlanDelta.timestamp required") + +@dataclass +class StrategyDelta: + id: str + assets: List[Asset] + objectives: dict + constraints: dict = field(default_factory=dict) + version: int = 1 + + def __post_init__(self): + _validate_non_empty("StrategyDelta.id", self.id) + _validate_non_empty("StrategyDelta.assets", self.assets) + if not isinstance(self.assets, list) or len(self.assets) == 0: + raise ValueError("StrategyDelta.assets must be a non-empty list") diff --git a/pyproject.toml b/pyproject.toml index eaee05c..d7a8f32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,10 @@ [build-system] -requires = ["setuptools>=42", "wheel"] +requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "deltaforge-mvp" -version = "0.1.0" -description = "Minimal MVP scaffolding for DeltaForge cross-venue hedgingEngine" +name = "deltaforge" +version = "0.0.1" +description = "MVP: cross-venue hedging engine scaffold" readme = "README.md" -requires-python = ">=3.8" -license = {text = "MIT"} -authors = [ { name = "DeltaForge MVP" } ] -dependencies = ["numpy"] +requires-python = ">=3.9" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..eddbbc9 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + name="deltaforge", + version="0.0.1", + description="MVP: cross-venue hedging engine scaffold", + packages=find_packages(include=["deltaforge", "deltaforge.*"]), + include_package_data=True, + install_requires=[], +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..484a2f3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +import sys, os +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if ROOT not in sys.path: + sys.path.insert(0, ROOT) diff --git a/tests/test_backtester.py b/tests/test_backtester.py new file mode 100644 index 0000000..ff215c9 --- /dev/null +++ b/tests/test_backtester.py @@ -0,0 +1,11 @@ +from deltaforge.backtester import Backtester +from deltaforge.dsl import Asset, MarketSignal +from deltaforge.dsl import PlanDelta + +def test_backtester_runs_deterministic(): + a = Asset(id="eq-XYZ", type="equity", symbol="XYZ") + s = MarketSignal(asset=a, timestamp=0.0, price=10.0) + plan = PlanDelta(delta=[{"action": "hedge", "symbol": "XYZ", "size": -1.0, "price": 10.0}], timestamp=0.0, author="tester") + bt = Backtester(initial_cash=1000.0) + final_cash = bt.apply([s], plan) + assert final_cash == 1000.0 - 1.0 * 10.0 + 0 # initial cash minus hedge cost diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py new file mode 100644 index 0000000..6dee382 --- /dev/null +++ b/tests/test_coordinator.py @@ -0,0 +1,13 @@ +from deltaforge.coordinator import Coordinator +from deltaforge.dsl import Asset, MarketSignal + +def test_coordinator_creates_plan_delta(): + a1 = Asset(id="eq-XYZ", type="equity", symbol="XYZ") + a2 = Asset(id="eq-ABC", type="equity", symbol="ABC") + s1 = MarketSignal(asset=a1, timestamp=1.0, price=10.0) + s2 = MarketSignal(asset=a2, timestamp=1.1, price=20.0) + c = Coordinator(contract_id="test-contract") + plan = c.coordinate([s1, s2], author="tester") + assert plan is not None + assert plan.contract_id == "test-contract" + assert isinstance(plan.delta, list) diff --git a/tests/test_dsl.py b/tests/test_dsl.py new file mode 100644 index 0000000..ac4b44a --- /dev/null +++ b/tests/test_dsl.py @@ -0,0 +1,22 @@ +import math +from deltaforge.dsl import Asset, MarketSignal, PlanDelta, StrategyDelta + +def test_asset_and_signals_creation(): + a = Asset(id="eq-ABC", type="equity", symbol="ABC") + ms = MarketSignal(asset=a, timestamp=1.0, price=100.0) + assert a.symbol == "ABC" + assert ms.asset.symbol == "ABC" + assert ms.price == 100.0 + +def test_plan_delta_creation(): + a = Asset(id="eq-ABC", type="equity", symbol="ABC") + ms = MarketSignal(asset=a, timestamp=1.0, price=100.0) + plan = PlanDelta(delta=[{"action": "hedge", "symbol": "ABC", "size": -0.5, "price": 100.0}], timestamp=1.0, author="tester") + assert isinstance(plan.delta, list) + assert plan.author == "tester" + assert plan.timestamp == 1.0 + +def test_strategy_delta_creation(): + a = Asset(id="eq-ABC", type="equity", symbol="ABC") + sd = StrategyDelta(id="s1", assets=[a], objectives={"maximize": "return"}) + assert sd.assets[0].symbol == "ABC"