Files
PHANTOM/lab/outlet/protocols.py

298 lines
10 KiB
Python

"""
Protocol definitions for pluggable simulator components.
This module defines the interfaces (Protocols) that allow swapping different
implementations for each stage of the Quote -> Arrival -> Execution -> Position
pipeline. All protocols use structural subtyping (duck typing).
Protocols:
Mechanism: How quotes translate to executions (posted price, two-sided, auction)
ArrivalModel: How opportunities arrive (Poisson, Hawkes, sessions)
ExecutionModel: Acceptance probability given quote (elasticity, intensity)
PositionModel: Inventory/position management and censorship
MarketModel: Competitor/market dynamics
ObservationBuilder: Constructs agent observations with censoring
Objective: Computes reward from metrics
"""
from __future__ import annotations
from typing import Protocol, Any, TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from .types import (Quote, Opportunity, Execution, InstrumentSet, StepLogs,
StepMetrics, HiddenState, Observation, MarketState)
from .constants import LogLevel
class Mechanism(Protocol):
"""Defines how quotes translate to executions.
The Mechanism is the core abstraction that differentiates pricing domains:
- PostedPrice: single price, buyer decides to purchase or not
- TwoSided: bid/ask spread, execution depends on distance from mid
- Auction: reserve price affects win probability and clearing price
Methods:
apply_quote: Enforce constraints and return valid quote
process_opportunity: Determine execution given opportunity and quote
"""
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
rng: np.random.Generator) -> Quote:
"""Apply mechanism-specific constraints to a quote.
Args:
quote: Raw quote from policy
instruments: Current instrument set with costs/refs
rng: Random generator for stochastic constraints
Returns:
Constrained quote satisfying mechanism rules (min margin, max delta, etc.)
"""
...
def process_opportunity(self, opp: Opportunity, quote: Quote,
instruments: InstrumentSet, market: MarketState | None,
rng: np.random.Generator) -> Execution | None:
"""Process an opportunity against the current quote.
Args:
opp: Incoming opportunity (session, order, request)
quote: Current posted quote
instruments: Instrument set
market: Current market state (competitor prices, mid-prices)
rng: Random generator
Returns:
Execution if opportunity converts, None otherwise
"""
...
class ArrivalModel(Protocol):
"""Generates opportunities (demand arrivals) for each step.
Different arrival models capture different demand dynamics:
- Poisson: constant rate, memoryless
- Hawkes: self-exciting, clustered arrivals
- Session: retail browsing with multi-product views
Methods:
sample: Generate opportunities for a time interval
"""
def sample(self, t: float, dt: float, instruments: InstrumentSet,
market: MarketState | None, hidden: HiddenState,
rng: np.random.Generator) -> list[Opportunity]:
"""Sample opportunities for time interval [t, t+dt).
Args:
t: Current time
dt: Time interval length
instruments: Available instruments
market: Current market state
hidden: Hidden state (contains demand intensity, contamination)
rng: Random generator
Returns:
List of opportunities arriving in this interval
"""
...
class ExecutionModel(Protocol):
"""Computes acceptance/execution probability given quote and context.
Different models capture different demand responses:
- Elasticity: price sensitivity with competitor cross-effects
- Intensity: distance-based fill probability (market making)
- Logit: discrete choice model
Methods:
prob: Compute acceptance probability
uncensor: Estimate true demand from censored fills
"""
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
market: MarketState | None, rng: np.random.Generator) -> float:
"""Compute probability that opportunity accepts the quote.
Args:
opp: Opportunity to evaluate
quote: Current quote
instruments: Instrument set
market: Market state (competitor prices affect cross-elasticity)
rng: Random generator
Returns:
Probability in [0, 1] that opportunity executes
"""
...
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet,
context: dict[str, Any] | None = None) -> np.ndarray:
"""Estimate true demand from censored fills.
Used for demand estimation research under inventory censorship.
Args:
fills: Observed (censored) fill counts
instruments: Instrument set
context: Additional context (exposures, prices shown)
Returns:
Estimated true demand counts
"""
...
class PositionModel(Protocol):
"""Manages inventory (retail) or position (finance).
Handles:
- Position constraints and censorship
- Holding costs (retail) or inventory risk (finance)
- Replenishment and order receipt
Methods:
reset: Initialize position state
available: Query available capacity for a trade
apply_execution: Censor execution by available position
step: Process time-based updates (replenishment, holding cost)
Properties:
position: Current position vector
holding_cost: Cost incurred this step from holding position
"""
def reset(self, instruments: InstrumentSet, rng: np.random.Generator) -> None:
"""Initialize position state for new episode."""
...
def available(self, instrument_id: int, side: Any) -> float:
"""Query available capacity for a trade.
Args:
instrument_id: Which instrument
side: BUY or SELL
Returns:
Maximum tradeable size given current position
"""
...
def apply_execution(self, exe: Execution) -> Execution:
"""Apply position constraints to an execution.
Args:
exe: Proposed execution with size_requested
Returns:
Censored execution with size_filled <= available capacity
"""
...
def step(self, t: float) -> None:
"""Process time-based position updates.
Handles replenishment receipt, holding cost calculation, etc.
"""
...
@property
def position(self) -> np.ndarray:
"""Current position vector (positive=long/inventory, negative=short)."""
...
@property
def holding_cost(self) -> float:
"""Holding cost incurred this step."""
...
class MarketModel(Protocol):
"""Models external market dynamics and competitor behavior.
For retail: competitor price dynamics (static, reactive, stochastic)
For finance: mid-price process (GBM, mean-reverting)
Methods:
step: Update market state given agent's quotes
"""
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
rng: np.random.Generator) -> MarketState:
"""Update market state for this timestep.
Args:
t: Current time
self_quotes: Agent's current quotes (competitors may react)
hidden: Hidden state (regime info)
rng: Random generator
Returns:
Updated market state with competitor prices, mid-prices, volatility
"""
...
class ObservationBuilder(Protocol):
"""Constructs agent observations with appropriate censoring.
Critical for research: ensures agent only sees censored fills,
never true demand (which goes in info dict).
Methods:
build: Construct observation from step data
"""
def build(self, quote: Quote, instruments: InstrumentSet, logs: StepLogs,
metrics: StepMetrics, market: MarketState | None,
hidden: HiddenState, mask_demand: bool, t: int) -> Observation:
"""Build observation for agent.
Args:
quote: Current quote
instruments: Instrument set with positions
logs: Step logs with true_demand and censored_fills
metrics: Computed metrics
market: Market state
hidden: Hidden state (not included in obs)
mask_demand: If True, exclude true demand from observation
t: Current timestep
Returns:
Observation containing only observable quantities
"""
...
class Objective(Protocol):
"""Computes reward from step metrics.
Supports composite objectives with weighted terms:
- PnL (profit)
- Position costs (holding, inventory risk)
- Lost opportunity (stockouts)
- Volatility penalty (UX)
- Spread capture (market making)
Methods:
reward: Compute scalar reward
breakdown: Get per-term contribution for analysis
"""
def reward(self, quote: Quote, instruments: InstrumentSet,
metrics: StepMetrics, hidden: HiddenState,
obs: Observation) -> float:
"""Compute scalar reward for this step.
Args:
quote: Current quote
instruments: Instrument set
metrics: Step metrics (pnl, costs, etc.)
hidden: Hidden state
obs: Agent observation
Returns:
Scalar reward value
"""
...
def breakdown(self, quote: Quote, instruments: InstrumentSet,
metrics: StepMetrics, hidden: HiddenState,
obs: Observation) -> dict[str, float]:
"""Get reward breakdown by component.
Useful for analyzing which terms dominate the reward.
Returns:
Dict mapping term names to their contributions
"""
...