mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 08:33:36 +00:00
169 lines
6.5 KiB
Python
169 lines
6.5 KiB
Python
"""
|
|
Arrival models for generating demand opportunities.
|
|
|
|
This module provides different arrival processes:
|
|
- PoissonArrivalModel: Constant-rate memoryless arrivals
|
|
- HawkesArrivalModel: Self-exciting clustered arrivals (market orders)
|
|
- SessionArrivalModel: Retail browsing sessions with multi-product views
|
|
|
|
Each model implements the ArrivalModel protocol and generates Opportunity objects
|
|
that flow through the execution pipeline.
|
|
"""
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
from typing import Callable
|
|
import numpy as np
|
|
from uuid import uuid4
|
|
from ..outlet.types import Opportunity, InstrumentSet, MarketState, HiddenState
|
|
from ..outlet.constants import Side, OpportunityType
|
|
from ..outlet.math_util import poisson_arrivals, hawkes_intensity
|
|
|
|
@dataclass
|
|
class PoissonArrivalConfig:
|
|
"""Configuration for Poisson arrival process.
|
|
|
|
Attributes:
|
|
base_rate: Expected arrivals per unit time (scaled by hidden.true_demand_intensity)
|
|
side_probs: Probability distribution over BUY/SELL sides
|
|
"""
|
|
base_rate: float = 10.0
|
|
side_probs: dict[Side, float] = None
|
|
|
|
def __post_init__(self):
|
|
if self.side_probs is None:
|
|
self.side_probs = {Side.BUY: 1.0}
|
|
|
|
class PoissonArrivalModel:
|
|
"""Homogeneous Poisson arrival process.
|
|
|
|
Generates arrivals at a constant rate (modulated by demand intensity).
|
|
Suitable for stationary demand or as a baseline model.
|
|
|
|
The actual arrival count follows Poisson(rate * dt * intensity).
|
|
"""
|
|
|
|
def __init__(self, cfg: PoissonArrivalConfig | None = None):
|
|
self.cfg = cfg or PoissonArrivalConfig()
|
|
|
|
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
market: MarketState | None, hidden: HiddenState,
|
|
rng: np.random.Generator) -> list[Opportunity]:
|
|
n_arrivals = poisson_arrivals(self.cfg.base_rate * hidden.true_demand_intensity, dt, rng)
|
|
opps = []
|
|
for _ in range(n_arrivals):
|
|
inst_id = rng.integers(0, instruments.n)
|
|
side = rng.choice(list(self.cfg.side_probs.keys()),
|
|
p=list(self.cfg.side_probs.values()))
|
|
opps.append(Opportunity(
|
|
id=str(uuid4())[:8], type=OpportunityType.SESSION,
|
|
side=side, instrument_id=inst_id, size=1.0, t=t,
|
|
context={'segment': 'default'}
|
|
))
|
|
return opps
|
|
|
|
@dataclass
|
|
class HawkesArrivalConfig:
|
|
"""Configuration for Hawkes self-exciting process.
|
|
|
|
Attributes:
|
|
base_rate: Baseline arrival intensity
|
|
alpha: Excitation strength (how much each arrival increases intensity)
|
|
beta: Decay rate (how quickly excitation fades)
|
|
side_probs: Probability distribution over BUY/SELL sides
|
|
"""
|
|
base_rate: float = 5.0
|
|
alpha: float = 0.5
|
|
beta: float = 1.0
|
|
side_probs: dict[Side, float] = None
|
|
|
|
def __post_init__(self):
|
|
if self.side_probs is None:
|
|
self.side_probs = {Side.BUY: 0.5, Side.SELL: 0.5}
|
|
|
|
class HawkesArrivalModel:
|
|
"""Self-exciting Hawkes point process for clustered arrivals.
|
|
|
|
Models order flow where arrivals cluster in time (momentum, herding).
|
|
Intensity: lambda(t) = base + alpha * sum(exp(-beta * (t - t_i)))
|
|
|
|
Used for market making scenarios where orders arrive in bursts.
|
|
"""
|
|
|
|
def __init__(self, cfg: HawkesArrivalConfig | None = None):
|
|
self.cfg = cfg or HawkesArrivalConfig()
|
|
self._history: np.ndarray = np.array([])
|
|
|
|
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
market: MarketState | None, hidden: HiddenState,
|
|
rng: np.random.Generator) -> list[Opportunity]:
|
|
intensity = hawkes_intensity(
|
|
self.cfg.base_rate * hidden.true_demand_intensity,
|
|
self._history, self.cfg.alpha, self.cfg.beta, t
|
|
)
|
|
n_arrivals = poisson_arrivals(intensity, dt, rng)
|
|
opps = []
|
|
for i in range(n_arrivals):
|
|
arr_t = t + rng.uniform(0, dt)
|
|
self._history = np.append(self._history, arr_t)
|
|
inst_id = rng.integers(0, instruments.n)
|
|
side = rng.choice(list(self.cfg.side_probs.keys()),
|
|
p=list(self.cfg.side_probs.values()))
|
|
opps.append(Opportunity(
|
|
id=str(uuid4())[:8], type=OpportunityType.MARKET_ORDER,
|
|
side=side, instrument_id=inst_id,
|
|
size=rng.exponential(1.0), t=arr_t,
|
|
context={'intensity': intensity}
|
|
))
|
|
# decay old history
|
|
self._history = self._history[self._history > t - 10]
|
|
return opps
|
|
|
|
@dataclass
|
|
class SessionArrivalConfig:
|
|
"""Configuration for retail session arrivals.
|
|
|
|
Attributes:
|
|
sessions_per_step: Number of browsing sessions per step
|
|
views_per_session: (min, max) product views per session
|
|
contamination: Fraction of sessions that are scrapers/bots
|
|
"""
|
|
sessions_per_step: int = 20
|
|
views_per_session: tuple[int, int] = (1, 5)
|
|
contamination: float = 0.0
|
|
|
|
class SessionArrivalModel:
|
|
"""Retail browsing session model with multi-product views.
|
|
|
|
Each session views multiple products, generating one opportunity per view.
|
|
Scraper sessions (controlled by contamination) view more products
|
|
but convert at lower rates (handled by ExecutionModel).
|
|
"""
|
|
|
|
def __init__(self, cfg: SessionArrivalConfig | None = None):
|
|
self.cfg = cfg or SessionArrivalConfig()
|
|
|
|
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
market: MarketState | None, hidden: HiddenState,
|
|
rng: np.random.Generator) -> list[Opportunity]:
|
|
n_sessions = self.cfg.sessions_per_step
|
|
contamination = hidden.contamination if hidden else self.cfg.contamination
|
|
opps = []
|
|
|
|
for _ in range(n_sessions):
|
|
is_scraper = rng.random() < contamination
|
|
n_views = rng.integers(*self.cfg.views_per_session)
|
|
sid = str(uuid4())[:8]
|
|
|
|
# scrapers view more products
|
|
if is_scraper:
|
|
n_views = min(instruments.n, n_views * 3)
|
|
|
|
viewed = rng.choice(instruments.n, size=min(n_views, instruments.n), replace=False)
|
|
for inst_id in viewed:
|
|
opps.append(Opportunity(
|
|
id=f"{sid}-{inst_id}", type=OpportunityType.SESSION,
|
|
side=Side.BUY, instrument_id=int(inst_id), size=1.0, t=t,
|
|
context={'session_id': sid, 'is_scraper': is_scraper, 'n_views': n_views}
|
|
))
|
|
return opps
|