mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
298 lines
10 KiB
Python
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
|
|
"""
|
|
...
|