forked from warproxxx/poly-maker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathquote_engine.py
More file actions
147 lines (121 loc) · 5.05 KB
/
Copy pathquote_engine.py
File metadata and controls
147 lines (121 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Quote construction using the Avellaneda-Stoikov model.
This module implements the A-S model to derive optimal bid/ask quotes based on
inventory, time, and volatility.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from decimal import Decimal, ROUND_DOWN, ROUND_UP
from config import BotConfig
from polymarket_adapter import PolymarketAdapter, Position, Quote, TokenConfig
LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True)
class TargetOrder:
token: TokenConfig
side: str
price: Decimal
shares: Decimal
reason: str
@property
def notional(self) -> Decimal:
return self.price * self.shares
def to_dict(self) -> dict:
return {
"token_id": self.token.token_id,
"outcome": self.token.outcome,
"side": self.side,
"price": self.price,
"shares": self.shares,
"notional": self.notional,
"reason": self.reason,
}
@dataclass(frozen=True)
class QuoteDiagnostics:
"""Holds all intermediate values for debugging the A-S model quoting logic."""
token: TokenConfig
side: str
mid_price: Decimal
indifference_price: Decimal
optimal_spread: Decimal
final_bid: Decimal
final_ask: Decimal
# Configs and state used
gamma: Decimal
volatility: Decimal
time_to_expiry: Decimal
inventory: Decimal
class QuoteEngine:
"""Generates optimal quotes based on the Avellaneda-Stoikov model."""
def __init__(self, config: BotConfig, adapter: PolymarketAdapter) -> None:
self.config = config
self.adapter = adapter
def calculate_limit_price(
self,
token: TokenConfig,
side: str,
quote: Quote,
position: Position,
time_to_expiry: Decimal,
gamma: Decimal, # Dynamic gamma from toxicity filter
) -> tuple[Decimal, QuoteDiagnostics]:
"""
Calculates the optimal limit price based on the A-S model.
Args:
time_to_expiry: Time remaining in the market, in annualized units (e.g., fraction of a year).
gamma: The risk aversion parameter, potentially modified by a toxicity filter.
"""
if time_to_expiry <= 0:
# Handle case where market is at or past expiry
indifference_price = quote.mid - position.size * gamma
total_spread = Decimal("0.1") # Use a wide, fixed spread at expiry
half_spread = total_spread / 2
else:
# 1. Calculate Indifference Price (Reservation Price) from A-S model
# r = s - q * γ * σ² * T
vol_squared = self.config.as_volatility ** 2
indifference_price = quote.mid - position.size * gamma * vol_squared * time_to_expiry
# 2. Calculate Optimal Spread from A-S model
# We use the simplified form: Spread = γ * σ² * T
total_spread = gamma * vol_squared * time_to_expiry
half_spread = total_spread / 2
# 3. Determine the raw bid and ask around the indifference price
raw_ask = indifference_price + half_spread
raw_bid = indifference_price - half_spread
# 4. Apply Post-Only logic to ensure we are makers
post_only_bid = min(raw_bid, quote.ask - token.tick_size)
post_only_ask = max(raw_ask, quote.bid + token.tick_size)
# 5. Clip and round to valid price levels
clipped_bid = min(self.config.max_price, max(self.config.min_price, post_only_bid))
final_bid = self.adapter.round_price(clipped_bid, token.tick_size, ROUND_DOWN)
clipped_ask = min(self.config.max_price, max(self.config.min_price, post_only_ask))
final_ask = self.adapter.round_price(clipped_ask, token.tick_size, ROUND_UP)
diagnostics = QuoteDiagnostics(
token=token,
side=side,
mid_price=quote.mid,
indifference_price=indifference_price,
optimal_spread=total_spread,
final_bid=final_bid,
final_ask=final_ask,
gamma=gamma,
volatility=self.config.as_volatility,
time_to_expiry=time_to_expiry,
inventory=position.size,
)
limit_price = final_bid if side == "BUY" else final_ask
return limit_price, diagnostics
def log_diagnostics(self, diagnostics: QuoteDiagnostics) -> None:
LOGGER.info(
"[QUOTE_AS] %s %s | Mid: %s, Inv: %s -> Indiff: %s, Spread: %s -> Bid: %s, Ask: %s | Params: (γ=%.2f, σ=%.2f, T=%.4f)",
diagnostics.side,
diagnostics.token.outcome,
diagnostics.mid_price.quantize(Decimal("0.0001")),
diagnostics.inventory,
diagnostics.indifference_price.quantize(Decimal("0.0001")),
diagnostics.optimal_spread.quantize(Decimal("0.0001")),
diagnostics.final_bid.quantize(Decimal("0.0001")),
diagnostics.final_ask.quantize(Decimal("0.0001")),
diagnostics.gamma,
diagnostics.volatility,
diagnostics.time_to_expiry,
)