forked from warproxxx/poly-maker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
231 lines (190 loc) · 10.8 KB
/
Copy pathconfig.py
File metadata and controls
231 lines (190 loc) · 10.8 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"""轻量级 Polymarket 做市机器人配置。
所有参数都从环境变量读取,避免把私钥、地址或实盘参数写死在代码里。
默认值按约 100 USDC 小资金账户设计:单市场风险 25 美元,全局风险 80 美元。
"""
from __future__ import annotations
import os
import logging
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
from dotenv import load_dotenv
load_dotenv()
LOGGER = logging.getLogger(__name__)
def _env(name: str, default: Optional[str] = None) -> Optional[str]:
value = os.getenv(name)
if value is None or value == "":
return default
return value
def _env_any(*names: str, default: Optional[str] = None) -> Optional[str]:
for name in names:
value = _env(name)
if value is not None:
return value
return default
def _decimal(name: str, default: str) -> Decimal:
return Decimal(_env(name, default) or default)
def _int(name: str, default: str) -> int:
return int(_env(name, default) or default)
def _float(name: str, default: str) -> float:
return float(_env(name, default) or default)
def _bool(name: str, default: str = "false") -> bool:
return (_env(name, default) or default).strip().lower() in {"1", "true", "yes", "y"}
def _decimal_from_env(key: str, default: str) -> Decimal:
return Decimal(os.environ.get(key, default))
@dataclass(frozen=True)
class BotConfig:
"""机器人运行参数。
token_yes/token_no 优先;如果只给 condition_id,会自动从 CLOB market info 解析 token。
price_spread_bps 是旧报价参数;当前模式使用预测价格进行挂单。
主模式使用 GTC Post-only 预测限价单,通过 Post-only 确保只做 Maker,不产生 Taker 手续费。
order_notional_usdc 是每个订单希望投入的美元金额,代码会换算为 shares。
"""
host: str = _env("POLYMARKET_CLOB_HOST", "https://clob.polymarket.com") or ""
chain_id: int = _int("CHAIN_ID", "137")
private_key: str = _env("PK", "") or ""
funder_address: str = _env("FUNDER_ADDRESS", _env("BROWSER_ADDRESS", "")) or ""
signature_type: int = _int("SIGNATURE_TYPE", "2")
clob_api_key: str = _env("CLOB_API_KEY", "") or ""
clob_secret: str = _env_any("CLOB_API_SECRET", "CLOB_SECRET", default="") or ""
clob_passphrase: str = _env_any("CLOB_API_PASSPHRASE", "CLOB_PASS_PHRASE", default="") or ""
condition_id: str = _env("CONDITION_ID", "") or ""
token_yes: str = _env("TOKEN_ID_YES", "") or ""
token_no: str = _env("TOKEN_ID_NO", "") or ""
market_source: str = _env("MARKET_SOURCE", "env") or "env"
google_sheet_worksheet: str = _env("GOOGLE_SHEET_WORKSHEET", "Selected Markets") or "Selected Markets"
google_sheet_limit: int = _int("GOOGLE_SHEET_LIMIT", "5")
auto_select_reward_market: bool = _bool("AUTO_SELECT_REWARD_MARKET", "false")
prefer_sports: bool = _bool("PREFER_SPORTS", "true")
price_spread_bps: Decimal = _decimal("PRICE_SPREAD_BPS", "120")
order_notional_usdc: Decimal = _decimal("ORDER_NOTIONAL_USDC", "5")
refresh_interval_seconds: int = _int("REFRESH_INTERVAL_SECONDS", "8")
min_price: Decimal = _decimal("MIN_PRICE", "0.03")
max_price: Decimal = _decimal("MAX_PRICE", "0.97")
max_market_exposure_usdc: Decimal = _decimal("MAX_MARKET_EXPOSURE_USDC", "25")
max_token_exposure_usdc: Decimal = _decimal("MAX_TOKEN_EXPOSURE_USDC", "30")
max_global_exposure_usdc: Decimal = _decimal("MAX_GLOBAL_EXPOSURE_USDC", "80")
count_existing_positions_in_global_limit: bool = _bool("COUNT_EXISTING_POSITIONS_IN_GLOBAL_LIMIT", "false")
inventory_skew_threshold_usdc: Decimal = _decimal("INVENTORY_SKEW_THRESHOLD_USDC", "18")
# --- Avellaneda-Stoikov Model Parameters ---
# Risk aversion parameter (gamma). Higher value means wider spreads and more inventory aversion.
risk_aversion: Decimal = _decimal("RISK_AVERSION", "0.1")
# Annualized volatility of the instrument (sigma). e.g., 0.8 for 80% annual vol.
as_volatility: Decimal = _decimal("AS_VOLATILITY", "0.8")
# --- Toxicity Filter Parameters ---
# If abs(imbalance) exceeds this, activate toxicity modifier.
toxicity_imbalance_threshold: Decimal = _decimal("TOXICITY_IMBALANCE_THRESHOLD", "0.8")
# Multiplier for risk_aversion when market is deemed toxic.
toxicity_gamma_multiplier: Decimal = _decimal("TOXICITY_GAMMA_MULTIPLIER", "2.0")
# --- Momentum Rider Mode ---
# When enabled, the bot will place an aggressive order if a large price jump is detected.
enable_momentum_rider_mode: bool = _bool("ENABLE_MOMENTUM_RIDER_MODE", "true")
# The price threshold (e.g., 0.8 for 80%) to trigger a momentum order.
# It triggers if price > threshold or price < (1 - threshold).
momentum_entry_threshold: Decimal = _decimal("MOMENTUM_ENTRY_THRESHOLD", "0.80")
# The notional value of the momentum order.
momentum_order_notional: Decimal = _decimal("MOMENTUM_ORDER_NOTIONAL", "10.0")
inventory_skew_bps: Decimal = _decimal("INVENTORY_SKEW_BPS", "60")
stop_loss_pct: Decimal = _decimal("STOP_LOSS_PCT", "12")
take_profit_pct: Decimal = _decimal("TAKE_PROFIT_PCT", "8")
max_market_loss_usdc: Decimal = _decimal("MAX_MARKET_LOSS_USDC", "8")
max_midpoint_move_bps: Decimal = _decimal("MAX_MIDPOINT_MOVE_BPS", "350")
close_only_hours_before_end: float = _float("CLOSE_ONLY_HOURS_BEFORE_END", "24")
close_only_on_game_start: bool = _bool("CLOSE_ONLY_ON_GAME_START", "true")
risk_off_after_stop: bool = _bool("RISK_OFF_AFTER_STOP", "true")
latch_on_stop_loss: bool = _bool("LATCH_ON_STOP_LOSS", "true")
emergency_exit_enabled: bool = _bool("EMERGENCY_EXIT_ENABLED", "true")
emergency_exit_loss_pct: Decimal = _decimal("EMERGENCY_EXIT_LOSS_PCT", "6")
emergency_exit_midpoint_move_bps: Decimal = _decimal("EMERGENCY_EXIT_MIDPOINT_MOVE_BPS", "800")
emergency_exit_imbalance_threshold: Decimal = _decimal("EMERGENCY_EXIT_IMBALANCE_THRESHOLD", "0.75")
emergency_exit_max_shares: Decimal = _decimal("EMERGENCY_EXIT_MAX_SHARES", "1000000000")
cancel_on_start: bool = _bool("CANCEL_ON_START", "true")
order_type: str = (_env("ORDER_TYPE", "GTC") or "GTC").upper()
post_only: bool = _bool("POST_ONLY", "true")
# Reconciliation tolerances for defending queue position
reconcile_price_tolerance_bps: Decimal = _decimal("RECONCILE_PRICE_TOLERANCE_BPS", "10")
reconcile_size_tolerance_pct: Decimal = _decimal("RECONCILE_SIZE_TOLERANCE_PCT", "20")
fok_price_buffer_bps: Decimal = _decimal("FOK_PRICE_BUFFER_BPS", "5")
fok_min_edge_bps: Decimal = _decimal("FOK_MIN_EDGE_BPS", "1")
cancel_unfilled_after_ms: int = _int("CANCEL_UNFILLED_AFTER_MS", "0")
prediction_latency_ms: int = _int("PREDICTION_LATENCY_MS", "180")
orderbook_imbalance_levels: int = _int("ORDERBOOK_IMBALANCE_LEVELS", "3")
dry_run: bool = _bool("DRY_RUN", "true")
log_file: str = _env("LOG_FILE", "logs/market_maker.log") or "logs/market_maker.log"
@property
def spread_fraction(self) -> Decimal:
return self.price_spread_bps / Decimal("10000")
@property
def inventory_skew_fraction(self) -> Decimal:
return self.inventory_skew_bps / Decimal("10000")
@property
def stop_loss_fraction(self) -> Decimal:
return self.stop_loss_pct / Decimal("100")
@property
def take_profit_fraction(self) -> Decimal:
return self.take_profit_pct / Decimal("100")
@property
def emergency_exit_loss_fraction(self) -> Decimal:
return self.emergency_exit_loss_pct / Decimal("100")
@property
def max_midpoint_move_fraction(self) -> Decimal:
return self.max_midpoint_move_bps / Decimal("10000")
@property
def fok_price_buffer_fraction(self) -> Decimal:
return self.fok_price_buffer_bps / Decimal("10000")
@property
def fok_min_edge_fraction(self) -> Decimal:
return self.fok_min_edge_bps / Decimal("10000")
def validate(self) -> None:
if not self.private_key and not self.dry_run:
raise ValueError("PK is required when DRY_RUN=false")
if self.refresh_interval_seconds < 1:
LOGGER.warning("High frequency trading enabled (REFRESH_INTERVAL_SECONDS < 1)")
if self.order_notional_usdc <= 0:
raise ValueError("ORDER_NOTIONAL_USDC must be positive")
if self.order_type not in {"FOK", "GTC"}:
raise ValueError("ORDER_TYPE must be FOK or GTC")
if self.order_type != "GTC":
LOGGER.warning(
"ORDER_TYPE=%s is experimental; production maker mode is GTC PostOnly",
self.order_type,
)
if self.order_type == "GTC" and not self.post_only:
LOGGER.warning(
"GTC without POST_ONLY can take liquidity; recommended production setting is POST_ONLY=true"
)
if self.fok_price_buffer_bps < 0:
raise ValueError("FOK_PRICE_BUFFER_BPS must be >= 0")
if self.fok_min_edge_bps < 0:
raise ValueError("FOK_MIN_EDGE_BPS must be >= 0")
if self.cancel_unfilled_after_ms < 0:
raise ValueError("CANCEL_UNFILLED_AFTER_MS must be >= 0")
if self.prediction_latency_ms < 0:
raise ValueError("PREDICTION_LATENCY_MS must be >= 0")
if self.orderbook_imbalance_levels <= 0:
raise ValueError("ORDERBOOK_IMBALANCE_LEVELS must be positive")
if self.emergency_exit_loss_pct < 0:
raise ValueError("EMERGENCY_EXIT_LOSS_PCT must be >= 0")
if self.emergency_exit_midpoint_move_bps < 0:
raise ValueError("EMERGENCY_EXIT_MIDPOINT_MOVE_BPS must be >= 0")
if not Decimal("0") <= self.emergency_exit_imbalance_threshold <= Decimal("1"):
raise ValueError("EMERGENCY_EXIT_IMBALANCE_THRESHOLD must be between 0 and 1")
if self.emergency_exit_max_shares <= 0:
raise ValueError("EMERGENCY_EXIT_MAX_SHARES must be positive")
if self.max_market_exposure_usdc > Decimal("30"):
raise ValueError("MAX_MARKET_EXPOSURE_USDC should stay <= 30 for the small-cap profile")
if self.max_global_exposure_usdc > Decimal("100"):
raise ValueError("MAX_GLOBAL_EXPOSURE_USDC should stay <= total bankroll")
has_tokens = bool(self.token_yes and self.token_no)
if self.market_source not in {"env", "google_sheet", "auto_rewards"}:
raise ValueError("MARKET_SOURCE must be env, google_sheet, or auto_rewards")
if self.market_source == "google_sheet":
if self.google_sheet_limit <= 0:
raise ValueError("GOOGLE_SHEET_LIMIT must be positive")
return
if self.market_source == "auto_rewards" or self.auto_select_reward_market:
return
if not has_tokens and not self.condition_id:
raise ValueError(
"Set TOKEN_ID_YES + TOKEN_ID_NO, CONDITION_ID, MARKET_SOURCE=google_sheet, or MARKET_SOURCE=auto_rewards"
)