-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
209 lines (175 loc) · 9.4 KB
/
Copy pathconfig.py
File metadata and controls
209 lines (175 loc) · 9.4 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
import os
from typing import Dict, Optional
from dotenv import load_dotenv
from hyperliquid.utils import constants
load_dotenv()
def _parse_list(env_val: str) -> list:
"""Parse comma-separated env var into a list, filtering empty strings."""
return [x.strip() for x in env_val.split(",") if x.strip()]
class Config:
ACCOUNT_ADDRESS = os.getenv("HYPERLIQUID_ACCOUNT_ADDRESS")
PRIVATE_KEY = os.getenv("HYPERLIQUID_PRIVATE_KEY")
USE_TESTNET = os.getenv("USE_TESTNET", "true").lower() == "true"
API_URL = constants.TESTNET_API_URL if USE_TESTNET else constants.MAINNET_API_URL
DEFAULT_SLIPPAGE = 0.1
MAX_POSITION_SIZE = 1000
# ------------------------------------------------------------------ #
# HIP-3 Multi-DEX configuration
# ------------------------------------------------------------------ #
# Comma-separated list of HIP-3 DEX names to trade on.
# Known: xyz (trade.xyz), flx (Felix), cash (DreamCash), km (Kinetiq), vntl (Ventuals), hyna (HyENA)
# Leave empty to trade only on standard Hyperliquid.
# Example: TRADING_DEXES=xyz,flx
TRADING_DEXES: list = _parse_list(os.getenv("TRADING_DEXES", ""))
# Whether to also trade standard Hyperliquid perps (non-HIP-3).
ENABLE_STANDARD_HL: bool = os.getenv("ENABLE_STANDARD_HL", "true").lower() == "true"
# Per-DEX coin overrides. If not set, the bot uses coins passed via CLI.
# Format: comma-separated coin names WITHOUT the "dex:" prefix.
# Example: XYZ_COINS=XYZ100,XYZ200
DEX_COINS: dict = {}
for _dex in TRADING_DEXES:
_env_key = f"{_dex.upper()}_COINS"
_val = os.getenv(_env_key, "")
if _val:
DEX_COINS[_dex] = _parse_list(_val)
# ------------------------------------------------------------------ #
# Builder fee configuration (per-DEX)
# ------------------------------------------------------------------ #
# Some HIP-3 deployers require attaching a builder code to every order
# in order for the wallet to be eligible for their rewards program
# (e.g. Dreamcash USDT rewards). Set BUILDER_FEES_<DEX>_ADDRESS to
# enable per-DEX builder attachment.
#
# Two values must stay consistent or every order is rejected:
#
# tenths_bps per-order builder fee, in tenths of a basis point.
# f=10 sends 1 bp (0.01% of notional). Default 10.
# max_fee_rate the pre-approved cap used by approveBuilderFee.
# It must be >= the actual per-order fee or the
# exchange rejects the order. Default "0.05%" leaves
# headroom for f up to 50 (5 bp) without re-approving.
#
# Example for Dreamcash:
# BUILDER_FEES_CASH_ADDRESS=0xabc...
# BUILDER_FEES_CASH_TENTHS_BPS=10 # 1 bp per order
# BUILDER_FEES_CASH_MAX_FEE_RATE=0.05% # cap at 5 bp
BUILDER_FEES: dict = {}
for _dex in TRADING_DEXES:
_addr = os.getenv(f"BUILDER_FEES_{_dex.upper()}_ADDRESS")
if _addr:
BUILDER_FEES[_dex] = {
"address": _addr.lower(),
"tenths_bps": int(os.getenv(
f"BUILDER_FEES_{_dex.upper()}_TENTHS_BPS", "10"
)),
"max_fee_rate": os.getenv(
f"BUILDER_FEES_{_dex.upper()}_MAX_FEE_RATE", "0.05%"
),
}
# ------------------------------------------------------------------ #
# Risk guardrail configuration
# ------------------------------------------------------------------ #
# Max single position as a fraction of account value (0.0–1.0).
MAX_POSITION_PCT: float = float(os.getenv("MAX_POSITION_PCT", "0.2"))
# Stop opening new orders when margin usage exceeds this fraction.
MAX_MARGIN_USAGE: float = float(os.getenv("MAX_MARGIN_USAGE", "0.8"))
# Force close ALL positions when margin usage exceeds this fraction.
# Disabled by default – set explicitly to enable.
FORCE_CLOSE_MARGIN: Optional[float] = (
float(os.getenv("FORCE_CLOSE_MARGIN"))
if os.getenv("FORCE_CLOSE_MARGIN") is not None
else None
)
# Force close ALL positions when leverage exceeds this value.
# Disabled by default – set explicitly to enable.
FORCE_CLOSE_LEVERAGE: Optional[float] = (
float(os.getenv("FORCE_CLOSE_LEVERAGE"))
if os.getenv("FORCE_CLOSE_LEVERAGE") is not None
else None
)
# Absolute dollar daily loss that triggers an automatic bot stop.
# Disabled by default – set explicitly to enable.
DAILY_LOSS_LIMIT: Optional[float] = (
float(os.getenv("DAILY_LOSS_LIMIT"))
if os.getenv("DAILY_LOSS_LIMIT") is not None
else None
)
# Cut losing trades at this percentage (0.0–1.0, e.g. 0.05 = 5%).
# Disabled by default – set explicitly to enable.
PER_TRADE_STOP_LOSS: Optional[float] = (
float(os.getenv("PER_TRADE_STOP_LOSS"))
if os.getenv("PER_TRADE_STOP_LOSS") is not None
else None
)
# Maximum number of concurrent open positions.
MAX_OPEN_POSITIONS: int = int(os.getenv("MAX_OPEN_POSITIONS", "5"))
# Seconds to wait after an emergency stop before resuming trading.
COOLDOWN_AFTER_STOP: int = int(os.getenv("COOLDOWN_AFTER_STOP", "3600"))
# How long (seconds) to cache risk metrics before re-fetching from the API.
METRICS_CACHE_TTL: float = float(os.getenv("METRICS_CACHE_TTL", "2.0"))
# How long (seconds) to cache asset metadata (sz_decimals etc.).
META_CACHE_TTL: float = float(os.getenv("META_CACHE_TTL", "3600"))
# How long (seconds) to cache mid prices in OrderManager.
MIDS_CACHE_TTL: float = float(os.getenv("MIDS_CACHE_TTL", "5.0"))
# Timeout (seconds) for Hyperliquid API calls.
API_TIMEOUT: float = float(os.getenv("API_TIMEOUT", "10"))
# How often (seconds) to run risk checks in the trading loop.
# With fast MAIN_LOOP_INTERVAL (e.g. 3s), risk checks don't need to
# run every cycle. Default 10s matches the previous 10s loop interval.
RISK_CHECK_INTERVAL: float = max(
float(os.getenv("RISK_CHECK_INTERVAL", "10")), 1.0
)
# ------------------------------------------------------------------ #
# Margin validation constants
# ------------------------------------------------------------------ #
# Minimum order value in USD per coin (JSON dict or single float for default).
# Example: MIN_ORDER_VALUE_BTC=100, MIN_ORDER_VALUE_ETH=100, MIN_ORDER_VALUE_DEFAULT=50
MIN_ORDER_VALUE_DEFAULT: float = float(os.getenv("MIN_ORDER_VALUE_DEFAULT", "50"))
MIN_ORDER_VALUE_BTC: float = float(os.getenv("MIN_ORDER_VALUE_BTC", "100"))
MIN_ORDER_VALUE_ETH: float = float(os.getenv("MIN_ORDER_VALUE_ETH", "100"))
# Margin multiplier Hyperliquid applies to initial orders.
INITIAL_MARGIN_MULTIPLIER: float = float(os.getenv("INITIAL_MARGIN_MULTIPLIER", "3.0"))
# Safety buffer multiplier applied on top of margin calculations.
MARGIN_SAFETY_BUFFER: float = float(os.getenv("MARGIN_SAFETY_BUFFER", "1.5"))
@classmethod
def get_min_order_values(cls) -> Dict[str, float]:
"""Return minimum order values dict, merging per-coin env overrides."""
values: Dict[str, float] = {'default': cls.MIN_ORDER_VALUE_DEFAULT}
values['BTC'] = cls.MIN_ORDER_VALUE_BTC
values['ETH'] = cls.MIN_ORDER_VALUE_ETH
# Support arbitrary coins via MIN_ORDER_VALUE_<COIN> env vars
for key, val in os.environ.items():
if key.startswith("MIN_ORDER_VALUE_") and key not in (
"MIN_ORDER_VALUE_DEFAULT", "MIN_ORDER_VALUE_BTC", "MIN_ORDER_VALUE_ETH"
):
coin = key[len("MIN_ORDER_VALUE_"):]
values[coin] = float(val)
return values
# Dynamic risk level: green (100%), yellow (50%), red (pause), black (close all).
# Read at runtime directly from os.getenv("RISK_LEVEL") by RiskManager,
# so it can be changed without restarting the bot.
@classmethod
def validate(cls) -> None:
if not cls.ACCOUNT_ADDRESS:
raise ValueError("HYPERLIQUID_ACCOUNT_ADDRESS not found in environment")
if not cls.PRIVATE_KEY:
raise ValueError("HYPERLIQUID_PRIVATE_KEY not found in environment")
# Validate risk guardrail ranges
if not 0.0 <= cls.MAX_POSITION_PCT <= 1.0:
raise ValueError(f"MAX_POSITION_PCT must be 0.0–1.0, got {cls.MAX_POSITION_PCT}")
if not 0.0 <= cls.MAX_MARGIN_USAGE <= 1.0:
raise ValueError(f"MAX_MARGIN_USAGE must be 0.0–1.0, got {cls.MAX_MARGIN_USAGE}")
if cls.FORCE_CLOSE_MARGIN is not None:
if not 0.0 <= cls.FORCE_CLOSE_MARGIN <= 1.0:
raise ValueError(f"FORCE_CLOSE_MARGIN must be 0.0–1.0, got {cls.FORCE_CLOSE_MARGIN}")
if cls.FORCE_CLOSE_MARGIN < cls.MAX_MARGIN_USAGE:
raise ValueError(
f"FORCE_CLOSE_MARGIN ({cls.FORCE_CLOSE_MARGIN}) must be >= "
f"MAX_MARGIN_USAGE ({cls.MAX_MARGIN_USAGE})"
)
if cls.DAILY_LOSS_LIMIT is not None and cls.DAILY_LOSS_LIMIT < 0:
raise ValueError(f"DAILY_LOSS_LIMIT must be >= 0, got {cls.DAILY_LOSS_LIMIT}")
if cls.PER_TRADE_STOP_LOSS is not None and not 0.0 <= cls.PER_TRADE_STOP_LOSS <= 1.0:
raise ValueError(f"PER_TRADE_STOP_LOSS must be 0.0–1.0, got {cls.PER_TRADE_STOP_LOSS}")
if cls.MAX_OPEN_POSITIONS < 1:
raise ValueError(f"MAX_OPEN_POSITIONS must be >= 1, got {cls.MAX_OPEN_POSITIONS}")