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..e6774b7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# FLASHPLAN AGENTS + +Overview +- FlashPlan is a federated edge planner for cross-venue execution. This repository provides a minimal MVP core to bootstrap development and testing. + +Architecture +- Core primitives: LocalProblem, SharedVariables, PlanDelta +- Edge planner: lightweight optimization with a simple ADMM-lite coordinator +- Adapters: venue A, venue B; mock risk center +- Transport: TLS-stub boundary to illustrate secure channels (no real network in MVP) +- Delta-sync: deterministic reconciliation on reconnects + +Tech Stack +- Python 3.11+ (typing, dataclasses, small in-process simulations) +- Tests with pytest +- Packaging via setuptools (pyproject.toml) + +How to run tests +- ./test.sh +- The script runs pytest and python -m build to verify packaging. + +Development Rules +- Implement smallest correct change first; small, well-scoped PRs preferred. +- Favor plain Python modules with clear interfaces; add tests for new behavior. +- Do not publish or push to remote without explicit user instruction. diff --git a/README.md b/README.md index 785dff5..9fe3c0f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1 @@ -# flashplan-edge-optimized-cross-venue-exe - -Problem: In multi-venue trading, optimal execution and hedging decisions are created in centralized systems with round-trip latencies that miss microsecond opportunities and expose sensitive inventory signals. There is a clear gap for an edge-native \ No newline at end of file +# FlashPlan Edge-Optimized Cross-Venue Executor diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..292fdec --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "flashplan-edge-optimized-cross-venue-exe" +version = "0.1.0" +description = "Edge-native cross-venue execution planner with federated coordination for ultra-low-latency trading" +authors = [ { name = "OpenCode" } ] +dependencies = [] +readme = "README.md" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/flashplan_edge_optimized_cross_venue_exe/__init__.py b/src/flashplan_edge_optimized_cross_venue_exe/__init__.py new file mode 100644 index 0000000..cc88697 --- /dev/null +++ b/src/flashplan_edge_optimized_cross_venue_exe/__init__.py @@ -0,0 +1,5 @@ +"""FlashPlan Edge package initializer""" + +from .core import LocalProblem, SharedVariables, PlanDelta, EdgePlanner, admm_step + +__all__ = ["LocalProblem", "SharedVariables", "PlanDelta", "EdgePlanner", "admm_step"] diff --git a/src/flashplan_edge_optimized_cross_venue_exe/core.py b/src/flashplan_edge_optimized_cross_venue_exe/core.py new file mode 100644 index 0000000..3e4ee85 --- /dev/null +++ b/src/flashplan_edge_optimized_cross_venue_exe/core.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import List, Dict, Optional + + +@dataclass +class LocalProblem: + asset_id: str + objective: str + target_metric: float + constraints: Dict[str, float] = field(default_factory=dict) + + +@dataclass +class SharedVariables: + version: int + signals: Dict[str, float] = field(default_factory=dict) + latency_budget: Optional[float] = None + + +@dataclass +class PlanDelta: + # Each trade/adjustment is a dict with a stable id for reconciliation + trades: List[Dict] = field(default_factory=list) + hedge_updates: List[Dict] = field(default_factory=list) + metadata: Dict[str, object] = field(default_factory=dict) + + +class EdgePlanner: + def __init__(self, venues: List[str], assets: List[str], alpha: float = 0.5): + self.venues = venues + self.assets = assets + self.alpha = alpha + + def solve(self, local_problem: LocalProblem, shared: SharedVariables) -> PlanDelta: + # Toy yet deterministic solver: generate a small set of trades, one per asset + delta = PlanDelta(metadata={"version": shared.version, "solver": "toy-admm-lite"}) + for i, asset in enumerate(self.assets): + venue = self.venues[i % len(self.venues)] + trade = { + "id": str(uuid.uuid4()), + "venue": venue, + "asset": asset, + "side": "BUY" if local_problem.target_metric >= 0 else "SELL", + "qty": max(1, int(abs(local_problem.target_metric or 1))), + "price_limit": None, + } + delta.trades.append(trade) + + # Include a tiny hedge update for demonstration + delta.hedge_updates.append({"id": str(uuid.uuid4()), "asset": self.assets[0], "adjustment": 0.0}) + return delta + + # Simple deterministic reconciliation helper used by tests + def delta_signature(self, delta: PlanDelta) -> str: + # Create a lightweight signature based on trades ids and metadata + ids = [t.get("id", "") for t in delta.trades] + meta = str(delta.metadata) + return str(hash(tuple(ids))) + meta + + +def admm_step(local_problem: LocalProblem, shared: SharedVariables, planner: EdgePlanner) -> PlanDelta: + return planner.solve(local_problem, shared) diff --git a/src/flashplan_edge_optimized_cross_venue_exe/mock_risk_center.py b/src/flashplan_edge_optimized_cross_venue_exe/mock_risk_center.py new file mode 100644 index 0000000..91e0192 --- /dev/null +++ b/src/flashplan_edge_optimized_cross_venue_exe/mock_risk_center.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from flashplan_edge_optimized_cross_venue_exe.core import PlanDelta + + +class MockRiskCenter: + """A toy risk center that mildly adjusts the delta for cross-venue coupling.""" + + def __init__(self, factor: float = 0.1): + self.factor = factor + + def apply(self, delta: PlanDelta) -> PlanDelta: + # Apply a tiny scaling to each trade quantity as a mock risk constraint + for t in delta.trades: + if "qty" in t and isinstance(t["qty"], int): + t["qty"] = max(1, int(t["qty"] * (1.0 + self.factor))) + return delta diff --git a/src/flashplan_edge_optimized_cross_venue_exe/venue_a.py b/src/flashplan_edge_optimized_cross_venue_exe/venue_a.py new file mode 100644 index 0000000..edccac4 --- /dev/null +++ b/src/flashplan_edge_optimized_cross_venue_exe/venue_a.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +from flashplan_edge_optimized_cross_venue_exe.core import LocalProblem, SharedVariables, PlanDelta, EdgePlanner + + +@dataclass +class VenueAAdapter: + name: str = "VenueA" + assets: List[str] = None + planner: EdgePlanner = None + + def __post_init__(self): + if self.assets is None: + self.assets = ["AAPL", "MSFT"] + if self.planner is None: + self.planner = EdgePlanner(venues=[self.name], assets=self.assets) + + def build_local_problem(self, asset: str, target_metric: float) -> LocalProblem: + return LocalProblem(asset_id=f"{self.name}:{asset}", objective="minimize_cost", target_metric=target_metric, constraints={}) + + def plan(self, shared: SharedVariables, target_metric: float) -> PlanDelta: + lp = self.build_local_problem(self.assets[0], target_metric) + return self.planner.solve(lp, shared) diff --git a/src/flashplan_edge_optimized_cross_venue_exe/venue_b.py b/src/flashplan_edge_optimized_cross_venue_exe/venue_b.py new file mode 100644 index 0000000..f39d9c6 --- /dev/null +++ b/src/flashplan_edge_optimized_cross_venue_exe/venue_b.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +from flashplan_edge_optimized_cross_venue_exe.core import LocalProblem, SharedVariables, PlanDelta, EdgePlanner + + +@dataclass +class VenueBAdapter: + name: str = "VenueB" + assets: List[str] = None + planner: EdgePlanner = None + + def __post_init__(self): + if self.assets is None: + self.assets = ["AAPL", "GOOG"] + if self.planner is None: + self.planner = EdgePlanner(venues=[self.name], assets=self.assets) + + def build_local_problem(self, asset: str, target_metric: float) -> LocalProblem: + return LocalProblem(asset_id=f"{self.name}:{asset}", objective="minimize_cost", target_metric=target_metric, constraints={}) + + def plan(self, shared: SharedVariables, target_metric: float) -> PlanDelta: + lp = self.build_local_problem(self.assets[0], target_metric) + return self.planner.solve(lp, shared) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..ed61c52 --- /dev/null +++ b/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Running pytest tests..." +pytest -q + +echo "Building package (Python) with build..." +python3 -m build + +echo "All tests passed and packaging build succeeded." diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..c67991a --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,29 @@ +import os +import sys + +# Ensure src is on PYTHONPATH for pytest in this environment +src_path = os.path.join(os.path.dirname(__file__), "..", "src") +sys.path.insert(0, os.path.abspath(src_path)) + +from flashplan_edge_optimized_cross_venue_exe import LocalProblem, SharedVariables, EdgePlanner, admm_step +from flashplan_edge_optimized_cross_venue_exe.core import PlanDelta + + +def test_basic_plan_generation(): + planner = EdgePlanner(venues=["VenueA", "VenueB"], assets=["AAPL", "MSFT"]) + lp = LocalProblem(asset_id="VenueA:AAPL", objective="min", target_metric=5.0, constraints={}) + sv = SharedVariables(version=1, signals={"liquidity": 1.0}, latency_budget=0.5) + delta = planner.solve(lp, sv) + assert isinstance(delta, PlanDelta) + assert len(delta.trades) >= 1 + + +def test_admm_step_returns_delta_signature(): + planner = EdgePlanner(venues=["VenueA"], assets=["AAPL"]) + lp = LocalProblem(asset_id="VenueA:AAPL", objective="min", target_metric=2.0, constraints={}) + sv = SharedVariables(version=1, signals={}, latency_budget=None) + delta = admm_step(lp, sv, planner) + assert isinstance(delta, PlanDelta) + # Ensure at least one trade is created and has an id + assert len(delta.trades) >= 1 + assert "id" in delta.trades[0]