mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""
|
|
Posted price mechanism for retail dynamic pricing.
|
|
|
|
In this mechanism, the agent posts a single price per instrument.
|
|
Buyers decide whether to purchase based on the posted price.
|
|
This is the standard e-commerce dynamic pricing model.
|
|
"""
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
import numpy as np
|
|
from ..types import Quote, Opportunity, Execution, InstrumentSet, MarketState
|
|
from ..constants import Side
|
|
from ..math_util import clamp
|
|
|
|
@dataclass
|
|
class PostedPriceConfig:
|
|
"""Configuration for posted price mechanism.
|
|
|
|
Attributes:
|
|
min_price: Absolute minimum price
|
|
max_price: Absolute maximum price
|
|
max_delta_pct: Maximum price change per step as fraction of previous
|
|
min_margin_pct: Minimum margin over cost basis
|
|
round_to: Price rounding granularity (None = no rounding)
|
|
"""
|
|
min_price: float = 0.01
|
|
max_price: float = 1000.0
|
|
max_delta_pct: float = 0.2
|
|
min_margin_pct: float = 0.05
|
|
round_to: float | None = 0.01
|
|
|
|
class PostedPriceMechanism:
|
|
"""Posted price mechanism for retail dynamic pricing.
|
|
|
|
The agent posts a single price per product. Constraints enforced:
|
|
- Prices within [min_price, max_price]
|
|
- Margin at least min_margin_pct above cost
|
|
- Price changes limited to max_delta_pct per step
|
|
- Prices rounded to round_to granularity
|
|
|
|
Only BUY-side opportunities are processed (customers purchasing).
|
|
"""
|
|
|
|
def __init__(self, cfg: PostedPriceConfig | None = None):
|
|
self.cfg = cfg or PostedPriceConfig()
|
|
|
|
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
|
|
rng: np.random.Generator) -> Quote:
|
|
prices = quote.prices.copy()
|
|
costs = instruments.costs
|
|
refs = instruments.refs
|
|
c = self.cfg
|
|
|
|
# enforce min margin
|
|
min_prices = costs * (1 + c.min_margin_pct)
|
|
prices = np.maximum(prices, min_prices)
|
|
|
|
# enforce absolute bounds
|
|
prices = clamp(prices, c.min_price, c.max_price)
|
|
|
|
# enforce max delta if we have history
|
|
if 'prev_prices' in quote.metadata:
|
|
prev = quote.metadata['prev_prices']
|
|
max_change = prev * c.max_delta_pct
|
|
prices = clamp(prices, prev - max_change, prev + max_change)
|
|
|
|
# round prices
|
|
if c.round_to:
|
|
prices = np.round(prices / c.round_to) * c.round_to
|
|
|
|
return Quote(prices=prices, propensity=quote.propensity,
|
|
metadata={**quote.metadata, 'prev_prices': prices})
|
|
|
|
def process_opportunity(self, opp: Opportunity, quote: Quote,
|
|
instruments: InstrumentSet, market: MarketState | None,
|
|
rng: np.random.Generator) -> Execution | None:
|
|
if opp.side != Side.BUY: return None # posted price is buy-only
|
|
idx = int(opp.instrument_id)
|
|
price = float(quote.prices[idx])
|
|
return Execution(
|
|
opportunity_id=opp.id, instrument_id=opp.instrument_id,
|
|
side=opp.side, size_requested=opp.size, size_filled=opp.size,
|
|
price=price, propensity=quote.propensity, t=opp.t
|
|
)
|