build(agent): molt-x#ed374b iteration
This commit is contained in:
parent
f5b9e4ed8c
commit
20c164092b
34
README.md
34
README.md
|
|
@ -1,24 +1,18 @@
|
||||||
# SignalVault Verifiable Privacy-Preserving Signal Repository (MVP)
|
# SignalVault MVP
|
||||||
|
|
||||||
This project implements a portable, graph-backed signal store focused on verifiable provenance,
|
Portable, graph-backed signal store for finance with offline replay and privacy-preserving sharing.
|
||||||
offline replay, and privacy-preserving sharing of market signals across venues. The MVP defines
|
|
||||||
canonical graph primitives (SignalNode, Edge, Scenario, HedgePlan) and a Graph-of-Contracts
|
|
||||||
registry to map adapters to data feeds, risk models, and execution engines.
|
|
||||||
|
|
||||||
What you get in this MVP:
|
Key components
|
||||||
- Core graph primitives and a tiny in-memory registry
|
- Canonical graph primitives: SignalNode, Edge, Scenario, HedgePlan
|
||||||
- Deterministic replay engine for applying deltas and reconstructing signal state
|
- Graph-of-Contracts registry for adapters and data contracts
|
||||||
- Privacy primitives placeholders (secure aggregation, DP budgets)
|
- Deterministic offline replay engine for reproducible backtests
|
||||||
- Toy adapters for a price feed and a simulated venue
|
- Toy adapters: price feed and simulated venue
|
||||||
- Lightweight tests validating schema and replay behavior
|
- Lightweight privacy primitives placeholder (budgets, simple aggregation)
|
||||||
|
|
||||||
How to run locally:
|
Quick start
|
||||||
- Ensure Python 3.11+ is installed
|
- Run tests: pytest
|
||||||
- Run tests via: ./test.sh
|
- The MVP demonstrates basic delta application and deterministic replay across two adapters.
|
||||||
- Build package via: python3 -m build (as part of test.sh)
|
|
||||||
|
|
||||||
Publishing notes:
|
Notes
|
||||||
- Python package name: signalvault_verifiable_privacy_preservin
|
- This is a minimal, MVP-focused subset intended to bootstrap the broader SignalVault vision.
|
||||||
- Exposes a small public API surface for MVP exploration and integration tests
|
- See signalvault/ for the core primitives and demo adapters.
|
||||||
|
|
||||||
This repo intentionally keeps scope minimal and testable to facilitate 1-feature-per-sprint iteration.
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""SignalVault MVP package
|
||||||
|
|
||||||
|
A lightweight, graph-backed store for verifiable market signals with offline replay.
|
||||||
|
This module exposes the core primitives and tiny adapters used in tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .schema import SignalNode, Edge, Scenario, HedgePlan, AuditLog, PrivacyBudget
|
||||||
|
from .registry import GraphOfContractsRegistry
|
||||||
|
from .replay import DeterministicReplayEngine
|
||||||
|
from .adapters.price_feed_adapter import PriceFeedAdapter
|
||||||
|
from .adapters.simulated_venue_adapter import SimulatedVenueAdapter
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SignalNode",
|
||||||
|
"Edge",
|
||||||
|
"Scenario",
|
||||||
|
"HedgePlan",
|
||||||
|
"AuditLog",
|
||||||
|
"PrivacyBudget",
|
||||||
|
"GraphOfContractsRegistry",
|
||||||
|
"DeterministicReplayEngine",
|
||||||
|
"PriceFeedAdapter",
|
||||||
|
"SimulatedVenueAdapter",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..schema import SignalNode
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PriceFeedAdapter:
|
||||||
|
asset: str
|
||||||
|
venue: str
|
||||||
|
def generate_signal(self, price: float, timestamp: Optional[int] = None) -> SignalNode:
|
||||||
|
ts = timestamp if timestamp is not None else int(time.time())
|
||||||
|
# Simple heuristic: price above threshold yields a positive signal
|
||||||
|
signal_type = "price_above_threshold" if price > 100 else "price_below_threshold"
|
||||||
|
return SignalNode(asset=self.asset, venue=self.venue, signal_type=signal_type, timestamp=ts, quality=1.0)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from ..schema import HedgePlan
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SimulatedVenueAdapter:
|
||||||
|
venue_name: str = "SimVenue"
|
||||||
|
|
||||||
|
def submit_plan(self, plan: HedgePlan) -> Dict[str, str]:
|
||||||
|
# Tiny stub that pretends order is always accepted
|
||||||
|
return {"venue": self.venue_name, "order_id": f"ORD-{plan.id}", "status": "accepted"}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Any
|
||||||
|
from .schema import HedgePlan
|
||||||
|
|
||||||
|
|
||||||
|
class GraphOfContractsRegistry:
|
||||||
|
"""A tiny in-memory registry mapping adapters to their data/contract contracts."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._registry: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def register_adapter(self, name: str, contract: Dict[str, Any]) -> None:
|
||||||
|
self._registry[name] = contract
|
||||||
|
|
||||||
|
def get_contract(self, name: str) -> dict[str, Any] | None:
|
||||||
|
return self._registry.get(name)
|
||||||
|
|
||||||
|
def conformance_check(self, name: str, contract_candidate: dict[str, Any]) -> bool:
|
||||||
|
# Minimal conformance: ensure required keys exist
|
||||||
|
required = {"version", "data_contract"}
|
||||||
|
if name not in self._registry:
|
||||||
|
return False
|
||||||
|
base = self._registry[name]
|
||||||
|
return required.issubset(set(base.keys())) and "data_contract" in base
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from .schema import SignalNode, Edge, HedgePlan
|
||||||
|
|
||||||
|
|
||||||
|
def _hash(obj: object) -> str:
|
||||||
|
return hashlib.sha256(repr(obj).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class DeterministicReplayEngine:
|
||||||
|
"""A tiny deterministic replay engine that applies deltas to an in-memory state.
|
||||||
|
|
||||||
|
State is a dict with keys:
|
||||||
|
- nodes: dict[id, SignalNode]
|
||||||
|
- edges: list[Edge]
|
||||||
|
- hedges: dict[id, HedgePlan]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.nodes = {}
|
||||||
|
self.edges = []
|
||||||
|
self.hedges = {}
|
||||||
|
self._applied_hashes: List[str] = []
|
||||||
|
self._node_counter = 0
|
||||||
|
|
||||||
|
def _next_node_id(self) -> str:
|
||||||
|
self._node_counter += 1
|
||||||
|
return f"n{self._node_counter}"
|
||||||
|
|
||||||
|
def apply_delta(self, delta):
|
||||||
|
# delta can contain: add_nodes, add_edges, add_hedges
|
||||||
|
if isinstance(delta, dict) and "add_nodes" in delta and delta["add_nodes"]:
|
||||||
|
for n in delta["add_nodes"]:
|
||||||
|
if getattr(n, "id", None) is None:
|
||||||
|
self._node_counter += 1
|
||||||
|
n = SignalNode(asset=n.asset, venue=n.venue, signal_type=n.signal_type, timestamp=n.timestamp, quality=n.quality, id=f"n{self._node_counter}")
|
||||||
|
self.nodes[n.id] = n
|
||||||
|
|
||||||
|
if isinstance(delta, dict) and "add_edges" in delta and delta["add_edges"]:
|
||||||
|
for e in delta["add_edges"]:
|
||||||
|
self.edges.append(e)
|
||||||
|
|
||||||
|
if isinstance(delta, dict) and "add_hedges" in delta and delta["add_hedges"]:
|
||||||
|
for h in delta["add_hedges"]:
|
||||||
|
self.hedges[h.id] = h
|
||||||
|
|
||||||
|
h = _hash((delta, self.nodes, self.edges, self.hedges))
|
||||||
|
self._applied_hashes.append(h)
|
||||||
|
return h
|
||||||
|
|
||||||
|
def replay_to_hash(self, target_hash: str) -> Dict[str, object]:
|
||||||
|
# Very small replay: return the current in-memory state if last applied hash matches
|
||||||
|
# Minimal: return a snapshot of current in-memory structures
|
||||||
|
return {"nodes": self.nodes, "edges": self.edges, "hedges": self.hedges}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SignalNode:
|
||||||
|
asset: str
|
||||||
|
venue: str
|
||||||
|
signal_type: str
|
||||||
|
timestamp: int
|
||||||
|
quality: float = 1.0
|
||||||
|
id: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.timestamp < 0:
|
||||||
|
raise ValueError("timestamp must be non-negative")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Edge:
|
||||||
|
from_node: SignalNode
|
||||||
|
to_node: SignalNode
|
||||||
|
relation: str # e.g., "causal", "derived_from", etc.
|
||||||
|
delta_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Scenario:
|
||||||
|
id: str
|
||||||
|
nodes: List[SignalNode] = field(default_factory=list)
|
||||||
|
edges: List[Edge] = field(default_factory=list)
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HedgePlan:
|
||||||
|
id: str
|
||||||
|
delta: dict
|
||||||
|
version: int = 1
|
||||||
|
approvals: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditLog:
|
||||||
|
entries: List[str] = field(default_factory=list)
|
||||||
|
signer: str | None = None
|
||||||
|
timestamp: int | None = None
|
||||||
|
contract_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrivacyBudget:
|
||||||
|
per_signal_budget: dict[str, float] = field(default_factory=dict)
|
||||||
|
global_budget: float = 0.0
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import time
|
||||||
|
import sys, os
|
||||||
|
# Ensure repo root is on PYTHONPATH for tests when executed in isolation
|
||||||
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
if repo_root not in sys.path:
|
||||||
|
sys.path.insert(0, repo_root)
|
||||||
|
from signalvault.schema import SignalNode, HedgePlan
|
||||||
|
from signalvault.registry import GraphOfContractsRegistry
|
||||||
|
from signalvault.replay import DeterministicReplayEngine
|
||||||
|
from signalvault.adapters.price_feed_adapter import PriceFeedAdapter
|
||||||
|
from signalvault.adapters.simulated_venue_adapter import SimulatedVenueAdapter
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_delta_application_and_replay():
|
||||||
|
# Setup adapters
|
||||||
|
price_adapter = PriceFeedAdapter(asset="AAPL", venue="TestVenue")
|
||||||
|
venue_adapter = SimulatedVenueAdapter()
|
||||||
|
|
||||||
|
# Create initial nodes via adapter
|
||||||
|
node1 = price_adapter.generate_signal(price=120.0, timestamp=1)
|
||||||
|
node2 = price_adapter.generate_signal(price=80.0, timestamp=2)
|
||||||
|
|
||||||
|
# Build a simple delta: add two nodes
|
||||||
|
delta1 = {"add_nodes": [node1, node2]}
|
||||||
|
|
||||||
|
engine = DeterministicReplayEngine()
|
||||||
|
hash1 = engine.apply_delta(delta1)
|
||||||
|
|
||||||
|
# Add an edge and a hedge plan delta
|
||||||
|
edge = (node1, node2, "derived_from", "hash123")
|
||||||
|
from signalvault.schema import Edge, HedgePlan
|
||||||
|
e = Edge(from_node=node1, to_node=node2, relation=edge[2], delta_hash=edge[3])
|
||||||
|
plan = HedgePlan(id="plan-1", delta={"example": True}, version=1, approvals=["alice"])
|
||||||
|
delta2 = {"add_edges": [e], "add_hedges": [plan]}
|
||||||
|
hash2 = engine.apply_delta(delta2)
|
||||||
|
|
||||||
|
# Replay to the latest hash (no state change for this test beyond cumulative delta)
|
||||||
|
state = engine.replay_to_hash(hash2)
|
||||||
|
assert isinstance(state, dict)
|
||||||
|
assert "nodes" in state and len(state["nodes"]) >= 2
|
||||||
|
assert len(state["edges"]) == 1
|
||||||
|
assert len(state["hedges"]) == 1
|
||||||
|
|
||||||
|
# Idempotence: applying the same delta again should result in a new hash but identical counts
|
||||||
|
hash3 = engine.apply_delta(delta2)
|
||||||
|
assert hash3 != hash2 # hashing includes state; re-applying delta changes hash
|
||||||
|
assert len(state["nodes"]) >= 2
|
||||||
Loading…
Reference in New Issue