build(agent): new-agents-4#58ba63 iteration
This commit is contained in:
parent
6400e755f5
commit
92b3b72cf5
|
|
@ -6,10 +6,37 @@ from .dsl import PlanDelta
|
|||
|
||||
|
||||
class Backtester:
|
||||
"""Toy deterministic replay-based backtester for MVP."""
|
||||
"""Toy deterministic replay-based backtester for MVP.
|
||||
|
||||
Exposes an apply() method that consumes a Signals stream and a PlanDelta
|
||||
to produce a final cash amount, suitable for the tests in this repo.
|
||||
"""
|
||||
|
||||
def __init__(self, initial_cash: float = 0.0):
|
||||
self.initial_cash = initial_cash
|
||||
|
||||
def run(self, plan: PlanDelta) -> Dict[str, float]:
|
||||
# Deterministic pseudo-PnL based on number of hedges and total_cost
|
||||
hedge_count = len(plan.hedges)
|
||||
pnl = max(0.0, 100.0 - plan.total_cost * 2.0) if hedge_count > 0 else 0.0
|
||||
# Backwards-compatible helper using the same simple cost model as apply()
|
||||
hedge_count = len(plan.delta) if plan and plan.delta else 0
|
||||
total_cost = 0.0
|
||||
if plan and plan.delta:
|
||||
for entry in plan.delta:
|
||||
size = abs(float(entry.get("size", 0.0)))
|
||||
price = float(entry.get("price", 0.0))
|
||||
total_cost += size * price
|
||||
pnl = max(0.0, 0.0 - total_cost) # placeholder deterministic path
|
||||
return {"deterministic_pnl": pnl, "hedge_count": hedge_count}
|
||||
|
||||
def apply(self, signals, plan: PlanDelta) -> float:
|
||||
"""Apply a sequence of MarketSignals against a PlanDelta to compute final cash.
|
||||
Cost is modeled as sum(|size| * price) for each hedge-like action in plan.delta.
|
||||
Final cash = initial_cash - total_cost.
|
||||
"""
|
||||
total_cost = 0.0
|
||||
if plan and plan.delta:
|
||||
for entry in plan.delta:
|
||||
size = abs(float(entry.get("size", 0.0)))
|
||||
price = float(entry.get("price", 0.0))
|
||||
total_cost += size * price
|
||||
final_cash = float(self.initial_cash) - total_cost
|
||||
return final_cash
|
||||
|
|
|
|||
|
|
@ -9,31 +9,33 @@ class Curator:
|
|||
with a naive ADMM-lite style constraint. This is a minimal stub for MVP.
|
||||
"""
|
||||
|
||||
def __init__(self, assets: List[Asset] = None):
|
||||
def __init__(self, assets: List[Asset] = None, contract_id: str | None = None):
|
||||
self.assets = assets or []
|
||||
self.contract_id = contract_id
|
||||
|
||||
def synthesize_plan(self, signals: List[MarketSignal], objectives: List[StrategyDelta]) -> PlanDelta:
|
||||
"""Generate a PlanDelta given per-venue signals and desired strategy blocks.
|
||||
|
||||
The simplest cross-venue constraint: enforce sum of target_delta across blocks is ~0.
|
||||
This is intentionally minimal for MVP demonstration.
|
||||
Minimal MVP: translate each StrategyDelta into a hedge action targeting
|
||||
the first asset listed, producing a simple delta-neutral plan if possible.
|
||||
"""
|
||||
# naive aggregation: align target deltas to neutralize total delta
|
||||
total_target = sum([sd.target_delta for sd in objectives]) if objectives else 0.0
|
||||
# If not neutral, scale down deltas proportionally to achieve neutrality if possible
|
||||
if total_target != 0:
|
||||
factor = -total_target / max(1e-6, len(objectives))
|
||||
adjusted = [StrategyDelta(
|
||||
asset=sd.asset,
|
||||
target_delta=sd.target_delta + factor,
|
||||
risk_budget=sd.risk_budget,
|
||||
objective=sd.objective,
|
||||
timestamp=sd.timestamp,
|
||||
) for sd in objectives]
|
||||
else:
|
||||
adjusted = objectives
|
||||
hedges: List[Dict] = []
|
||||
latest_price = signals[-1].price if signals else 0.0
|
||||
latest_ts = signals[-1].timestamp if signals else 0.0
|
||||
for idx, sd in enumerate(objectives or []):
|
||||
if getattr(sd, 'assets', None):
|
||||
asset = sd.assets[0]
|
||||
else:
|
||||
# Fallback: skip if no asset available
|
||||
continue
|
||||
size = 1.0 * (-1 if idx % 2 == 1 else 1)
|
||||
hedges.append({
|
||||
"action": "hedge",
|
||||
"symbol": asset.symbol,
|
||||
"size": float(size),
|
||||
"price": float(latest_price),
|
||||
"ts": float(latest_ts),
|
||||
})
|
||||
|
||||
plan = PlanDelta()
|
||||
plan.hedges = adjusted
|
||||
plan.total_cost = sum(abs(sd.target_delta) * 0.1 for sd in adjusted) # simplistic cost proxy
|
||||
plan = PlanDelta(delta=hedges, timestamp=float(latest_ts), author="curator", contract_id=self.contract_id)
|
||||
return plan
|
||||
|
|
|
|||
|
|
@ -5,39 +5,58 @@ from typing import List, Dict, Optional
|
|||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Asset:
|
||||
symbol: str
|
||||
asset_type: str # e.g., "equity", "option", "futures"
|
||||
venue: str
|
||||
"""Canonical Asset representation used by DeltaForge DSL.
|
||||
|
||||
def __post_init__(self):
|
||||
if self.asset_type not in {"equity", "option", "futures"}:
|
||||
raise ValueError("asset_type must be one of 'equity', 'option', or 'futures'")
|
||||
Supports two constructor styles for compatibility:
|
||||
- Modern: Asset(id=..., type=..., symbol=...)
|
||||
- Legacy: Asset(symbol=..., asset_type=..., venue=...)
|
||||
"""
|
||||
def __init__(self, id: str | None = None, type: str | None = None, symbol: str | None = None, venue: str | None = None, **kwargs):
|
||||
# Preferred explicit constructor
|
||||
if id is not None and type is not None and symbol is not None:
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.symbol = symbol
|
||||
else:
|
||||
# Legacy constructor path: expect symbol and asset_type (and optionally venue)
|
||||
self.symbol = kwargs.get("symbol")
|
||||
self.type = kwargs.get("asset_type")
|
||||
self.id = kwargs.get("id")
|
||||
# If legacy path didn't supply id, derive a simple one if possible
|
||||
if self.id is None and self.symbol is not None and self.type is not None:
|
||||
self.id = f"{self.type}-{self.symbol}"
|
||||
if self.symbol is None or self.type is None:
|
||||
raise ValueError("Asset requires either (id, type, symbol) or (symbol, asset_type)")
|
||||
|
||||
# Validate type
|
||||
if self.type not in {"equity", "option", "futures"}:
|
||||
raise ValueError("type must be one of 'equity', 'option', or 'futures'")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MarketSignal:
|
||||
asset_symbol: str
|
||||
venue: str
|
||||
asset: Asset
|
||||
price: float
|
||||
timestamp: float = field(default_factory=lambda: 0.0)
|
||||
delta: Optional[float] = None # local delta proxy if available
|
||||
timestamp: datetime = field(default_factory=datetime.utcnow)
|
||||
meta: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StrategyDelta:
|
||||
asset: Asset
|
||||
target_delta: float # desired delta exposure for this block
|
||||
risk_budget: float # allowed risk budget for this block
|
||||
objective: str # e.g., "delta-neutral", "calendar-spread"
|
||||
timestamp: datetime = field(default_factory=datetime.utcnow)
|
||||
id: str
|
||||
assets: List[Asset]
|
||||
objectives: Dict
|
||||
timestamp: float = field(default_factory=lambda: 0.0)
|
||||
# kept minimal; can extend with per-block budgets if needed
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
hedges: List[StrategyDelta] = field(default_factory=list)
|
||||
delta: List = field(default_factory=list) # list of hedge/trade actions (dicts)
|
||||
actions: List[Dict] = field(default_factory=list) # provenance and trade actions
|
||||
total_cost: float = 0.0
|
||||
timestamp: datetime = field(default_factory=datetime.utcnow)
|
||||
timestamp: float = field(default_factory=lambda: 0.0)
|
||||
author: Optional[str] = None
|
||||
contract_id: Optional[str] = None
|
||||
|
|
|
|||
16
test.sh
16
test.sh
|
|
@ -20,19 +20,17 @@ python3 - <<'PY'
|
|||
from deltaforge.dsl import Asset, MarketSignal, StrategyDelta
|
||||
from deltaforge.core import Curator
|
||||
from deltaforge.backtester import Backtester
|
||||
from deltaforge.adapters.equity_feed import EquityFeedAdapter
|
||||
from deltaforge.adapters.options_feed import OptionsFeedAdapter
|
||||
from deltaforge.dsl import PlanDelta
|
||||
|
||||
asset_a = Asset(symbol="AAPL", asset_type="equity", venue="VENUE-A")
|
||||
asset_b = Asset(symbol="MSFT", asset_type="equity", venue="VENUE-B")
|
||||
sig = MarketSignal(asset_symbol="AAPL", venue="VENUE-A", price=150.0)
|
||||
asset_a = Asset(id="eq-AAPL", type="equity", symbol="AAPL")
|
||||
asset_b = Asset(id="eq-MSFT", type="equity", symbol="MSFT")
|
||||
sig = MarketSignal(asset=asset_a, timestamp=0.0, price=150.0)
|
||||
curator = Curator([asset_a, asset_b])
|
||||
plan = curator.synthesize_plan([sig], [
|
||||
StrategyDelta(asset=asset_a, target_delta=1.0, risk_budget=0.5, objective="delta-neutral"),
|
||||
StrategyDelta(asset=asset_b, target_delta=-1.0, risk_budget=0.5, objective="delta-neutral"),
|
||||
StrategyDelta(id="s1", assets=[asset_a], objectives={"maximize": "return"}),
|
||||
StrategyDelta(id="s2", assets=[asset_b], objectives={"maximize": "return"}),
|
||||
])
|
||||
bt = Backtester()
|
||||
res = bt.run(plan)
|
||||
bt = Backtester(initial_cash=1000.0)
|
||||
res = bt.apply([sig], plan)
|
||||
print(res)
|
||||
PY
|
||||
|
|
|
|||
Loading…
Reference in New Issue