build(agent): new-agents#a6e6ec iteration
This commit is contained in:
parent
8dd26142d1
commit
31d39c5339
|
|
@ -1,5 +1,13 @@
|
||||||
# ELAC-Plan Agents
|
# ELAC-Plan Agents
|
||||||
|
|
||||||
|
Architecture and MVP changes
|
||||||
|
- Added a FastAPI-based MVP API (elac_plan.api) exposing /problems, /status, and /deltas endpoints to drive the LocalProblem -> PlanDelta flow locally.
|
||||||
|
- In-memory MVP storage for LocalProblem and PlanDelta with a toy LocalSolver and two starter adapters (NBBOFeedAdapter, BrokerGatewayAdapter).
|
||||||
|
- Public Python package surface updated: elac_plan.api is importable via elac_plan.api.app (FastAPI app), and re-exported from elac_plan for convenience.
|
||||||
|
- Tests: Added tests/test_api.py to validate problem creation and status endpoints using FastAPI TestClient.
|
||||||
|
- Test runner: test.sh added to execute pytest and python -m build for packaging validation.
|
||||||
|
- Documentation: README updated with MVP overview (next iteration to expand with Phase 1 governance, phase 2 cross-venue demos, etc.).
|
||||||
|
|
||||||
Architecture overview:
|
Architecture overview:
|
||||||
- Primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, Governance/AuditLog
|
- Primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, Governance/AuditLog
|
||||||
- Adapters: NBBOFeedAdapter (data feed), BrokerGatewayAdapter (execution gateway)
|
- Adapters: NBBOFeedAdapter (data feed), BrokerGatewayAdapter (execution gateway)
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -1 +1,22 @@
|
||||||
# ELAC-Plan (Edge-Latency Aware Cross-Venue Execution Planner)
|
# ELAC-Plan (Edge-Latency Aware Cross-Venue Execution Planner)
|
||||||
|
|
||||||
|
This repository implements a production-oriented MVP of ELAC-Plan: an edge-native federation for cross-venue execution planning with privacy-preserving signals and deterministic delta-sync.
|
||||||
|
|
||||||
|
- Primitives map to canonical primitives used across adapters:
|
||||||
|
- Objects: LocalProblem
|
||||||
|
- Morphisms: SharedVariables
|
||||||
|
- DualVariables: DualVariables
|
||||||
|
- PlanDelta: PlanDelta
|
||||||
|
- Governance/AuditLog: GovernanceAuditLog (stubbed for now)
|
||||||
|
- MVP adapters:
|
||||||
|
- NBBOFeedAdapter: translates NBBO-like feeds into SharedVariables
|
||||||
|
- BrokerGatewayAdapter: simulates broker API publishing of PlanDelta
|
||||||
|
- Solver: LocalSolver (toy solver)
|
||||||
|
- API: FastAPI app exposing /problems and /status to drive LocalProblem -> PlanDelta flow
|
||||||
|
- Packaging: Python packaging metadata in pyproject.toml; tests with pytest; build via python -m build
|
||||||
|
|
||||||
|
How to run (dev):
|
||||||
|
- Run tests: ./test.sh
|
||||||
|
- Run API locally (example): uvicorn elac_plan.api:app --reload
|
||||||
|
|
||||||
|
This is a minimal but production-conscious MVP intended to be extended by adding governance ledger, secure aggregation, and real adapters in subsequent iterations.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables
|
||||||
from .solver import LocalSolver
|
from .solver import LocalSolver
|
||||||
from .adapters import NBBOFeedAdapter, BrokerGatewayAdapter
|
from .adapters import NBBOFeedAdapter, BrokerGatewayAdapter
|
||||||
from .dsl import DSLObject, DSLMorphisms, DSLDualVariables, DSLPlanDelta
|
from .dsl import DSLObject, DSLMorphisms, DSLDualVariables, DSLPlanDelta
|
||||||
|
from .api import app as api_app
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LocalProblem",
|
"LocalProblem",
|
||||||
|
|
@ -17,4 +18,5 @@ __all__ = [
|
||||||
"DSLMorphisms",
|
"DSLMorphisms",
|
||||||
"DSLDualVariables",
|
"DSLDualVariables",
|
||||||
"DSLPlanDelta",
|
"DSLPlanDelta",
|
||||||
|
"api_app",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
154
elac_plan/api.py
154
elac_plan/api.py
|
|
@ -1,17 +1,45 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import FastAPI
|
|
||||||
from pydantic import BaseModel
|
"""ELAC-Plan API
|
||||||
|
|
||||||
|
Simple FastAPI-based MVP to exercise LocalProblem -> PlanDelta flow with two starter adapters.
|
||||||
|
This is intentionally lightweight and in-process to bootstrap cross-venue planning without
|
||||||
|
external dependencies beyond FastAPI, Pydantic (via existing dataclasses), and httpx for tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from .core import LocalProblem, PlanDelta, SharedVariables, DualVariables
|
from .core import LocalProblem, PlanDelta, SharedVariables, DualVariables
|
||||||
from .solver import LocalSolver
|
from .solver import LocalSolver
|
||||||
from .adapters import NBBOFeedAdapter, BrokerGatewayAdapter
|
from .adapters import NBBOFeedAdapter, BrokerGatewayAdapter
|
||||||
|
from .dsl import (
|
||||||
|
DSLObject,
|
||||||
|
DSLMorphisms,
|
||||||
|
DSLDualVariables,
|
||||||
|
DSLPlanDelta,
|
||||||
|
to_dsl_object,
|
||||||
|
to_dsl_morphisms,
|
||||||
|
to_dsl_dual,
|
||||||
|
to_dsl_plan,
|
||||||
|
)
|
||||||
|
|
||||||
app = FastAPI(title="ELAC-Plan API")
|
|
||||||
|
app = FastAPI(title="ELAC-Plan MVP API", version="0.1.0")
|
||||||
|
|
||||||
|
# In-memory stores for MVP: problems and deltas
|
||||||
|
_problems: Dict[str, LocalProblem] = {}
|
||||||
|
_deltas: Dict[str, PlanDelta] = {}
|
||||||
|
|
||||||
|
# Starter adapters (could be used by endpoints or in a factory pattern)
|
||||||
|
_nbbo = NBBOFeedAdapter(contract_id="default-contract")
|
||||||
|
_broker = BrokerGatewayAdapter()
|
||||||
|
|
||||||
_solver = LocalSolver()
|
_solver = LocalSolver()
|
||||||
_feed = NBBOFeedAdapter()
|
|
||||||
_broker = BrokerGatewayAdapter()
|
|
||||||
|
|
||||||
|
|
||||||
class LocalProblemInput(BaseModel):
|
class LocalProblemInput(BaseModel):
|
||||||
|
|
@ -19,27 +47,113 @@ class LocalProblemInput(BaseModel):
|
||||||
asset: str
|
asset: str
|
||||||
venue: str
|
venue: str
|
||||||
objective: str
|
objective: str
|
||||||
constraints: Dict[str, Any]
|
constraints: Optional[Dict[str, Any]] = None
|
||||||
price_target: float
|
price_target: float
|
||||||
tolerance: float
|
tolerance: float
|
||||||
|
|
||||||
|
|
||||||
@app.post("/problems")
|
@app.post("/problems", response_model=Dict[str, Any])
|
||||||
def create_problem(p: LocalProblemInput) -> Dict[str, Any]:
|
def create_problem(problem: LocalProblemInput):
|
||||||
problem = LocalProblem(
|
# Build a LocalProblem from input and run the toy solver to produce a delta
|
||||||
id=p.id,
|
lp = LocalProblem(
|
||||||
asset=p.asset,
|
id=problem.id,
|
||||||
venue=p.venue,
|
asset=problem.asset,
|
||||||
objective=p.objective,
|
venue=problem.venue,
|
||||||
constraints=p.constraints,
|
objective=problem.objective,
|
||||||
price_target=p.price_target,
|
constraints=problem.constraints or {},
|
||||||
tolerance=p.tolerance,
|
price_target=problem.price_target,
|
||||||
|
tolerance=problem.tolerance,
|
||||||
)
|
)
|
||||||
delta = _solver.solve(problem)
|
|
||||||
|
_problems[lp.id] = lp
|
||||||
|
delta = _solver.solve(lp)
|
||||||
|
_deltas[delta.contract_id] = delta
|
||||||
|
|
||||||
|
# Simulate publishing via broker gateway (stringized payload) for MVP
|
||||||
_broker.publish(delta)
|
_broker.publish(delta)
|
||||||
return {"problem": problem.to_json(), "delta": delta.to_json()}
|
|
||||||
|
return {
|
||||||
|
"problem_id": lp.id,
|
||||||
|
"delta": json.dumps(delta.delta),
|
||||||
|
"delta_contract": delta.contract_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/status")
|
@app.get("/status")
|
||||||
def status() -> Dict[str, Any]:
|
def status():
|
||||||
return {"status": "ELAC-Plan API running"}
|
return {
|
||||||
|
"problems_count": len(_problems),
|
||||||
|
"deltas_count": len(_deltas),
|
||||||
|
"latest_delta_contract": max(_deltas.keys()) if _deltas else None,
|
||||||
|
"server_time": datetime.utcnow().isoformat() + "Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/problems/{problem_id}")
|
||||||
|
def get_problem(problem_id: str):
|
||||||
|
lp = _problems.get(problem_id)
|
||||||
|
if not lp:
|
||||||
|
raise HTTPException(status_code=404, detail="problem not found")
|
||||||
|
return {
|
||||||
|
"id": lp.id,
|
||||||
|
"asset": lp.asset,
|
||||||
|
"venue": lp.venue,
|
||||||
|
"objective": lp.objective,
|
||||||
|
"price_target": lp.price_target,
|
||||||
|
"tolerance": lp.tolerance,
|
||||||
|
"constraints": lp.constraints,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/deltas/{contract_id}")
|
||||||
|
def get_delta(contract_id: str):
|
||||||
|
delta = _deltas.get(contract_id)
|
||||||
|
if not delta:
|
||||||
|
raise HTTPException(status_code=404, detail="delta not found")
|
||||||
|
return {
|
||||||
|
"contract_id": delta.contract_id,
|
||||||
|
"delta": delta.delta,
|
||||||
|
"timestamp": delta.timestamp,
|
||||||
|
"author": delta.author,
|
||||||
|
"privacy_budget": delta.privacy_budget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dsl/{problem_id}")
|
||||||
|
def get_problem_dsl(problem_id: str):
|
||||||
|
# Expose a minimal DSL representation for a given problem, including
|
||||||
|
# LocalProblem -> DSLObject, SharedVariables, DualVariables, and PlanDelta.
|
||||||
|
lp = _problems.get(problem_id)
|
||||||
|
if not lp:
|
||||||
|
raise HTTPException(status_code=404, detail="problem not found")
|
||||||
|
|
||||||
|
# Build a deterministic contract_id as used by the MVP
|
||||||
|
contract_id = f"{lp.id}:{lp.venue}"
|
||||||
|
# Rehydrate a minimal SharedVariables and DualVariables for DSL exposure
|
||||||
|
sw = SharedVariables(variables={}, version=1, contract_id=contract_id)
|
||||||
|
dv = DualVariables(shadow_prices={}, version=1, contract_id=contract_id)
|
||||||
|
|
||||||
|
# Resolve latest delta for the problem if available
|
||||||
|
delta = _deltas.get(contract_id)
|
||||||
|
if delta is None:
|
||||||
|
# create a minimal placeholder delta to showcase DSL shape
|
||||||
|
delta = PlanDelta(
|
||||||
|
delta={},
|
||||||
|
timestamp="",
|
||||||
|
author="unknown",
|
||||||
|
contract_id=contract_id,
|
||||||
|
privacy_budget=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transform to DSL structures
|
||||||
|
dsl_lp = to_dsl_object(lp)
|
||||||
|
dsl_sw = to_dsl_morphisms(sw)
|
||||||
|
dsl_dv = to_dsl_dual(dv)
|
||||||
|
dsl_plan = to_dsl_plan(delta)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"lp": dsl_lp.to_dict(),
|
||||||
|
"shared_variables": dsl_sw.to_dict(),
|
||||||
|
"dual_variables": dsl_dv.to_dict(),
|
||||||
|
"plan_delta": dsl_plan.to_dict(),
|
||||||
|
}
|
||||||
|
|
|
||||||
9
test.sh
9
test.sh
|
|
@ -1,12 +1,13 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Run unit tests and packaging build to ensure MVP integrity
|
|
||||||
echo "Installing package in editable mode..."
|
|
||||||
pip install -e .
|
|
||||||
export PYTHONPATH=$(pwd)
|
export PYTHONPATH=$(pwd)
|
||||||
|
|
||||||
echo "Running tests..."
|
echo "Running tests..."
|
||||||
pytest -q
|
pytest -q
|
||||||
|
|
||||||
echo "Building package..."
|
echo "Building package..."
|
||||||
python3 -m build
|
python3 -m build
|
||||||
echo "OK: tests and build completed."
|
|
||||||
|
echo "All tests passed and package built."
|
||||||
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from elac_plan.api import app
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_problem_smoke():
|
||||||
|
payload = {
|
||||||
|
"id": "probe-001",
|
||||||
|
"asset": "AAPL",
|
||||||
|
"venue": "XNAS",
|
||||||
|
"objective": "minimize_spread",
|
||||||
|
"constraints": {"max_slippage": 0.5},
|
||||||
|
"price_target": 150.0,
|
||||||
|
"tolerance": 0.2,
|
||||||
|
}
|
||||||
|
resp = client.post("/problems", json=payload)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["problem_id"] == payload["id"]
|
||||||
|
assert "delta" in data
|
||||||
|
# validate delta payload json can be parsed back
|
||||||
|
delta = json.loads(data["delta"])
|
||||||
|
assert delta["action"] == "place_order"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_endpoint():
|
||||||
|
resp = client.get("/status")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert "problems_count" in body
|
||||||
|
assert "deltas_count" in body
|
||||||
Loading…
Reference in New Issue