@intentsolutionsio/crypto-derivatives-tracker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/agents/derivatives-agent.md +408 -0
- package/package.json +43 -0
- package/skills/skill-adapter/assets/README.md +6 -0
- package/skills/skill-adapter/assets/config-template.json +32 -0
- package/skills/skill-adapter/assets/skill-schema.json +28 -0
- package/skills/skill-adapter/assets/test-data.json +27 -0
- package/skills/skill-adapter/references/README.md +4 -0
- package/skills/skill-adapter/references/best-practices.md +69 -0
- package/skills/skill-adapter/references/examples.md +73 -0
- package/skills/skill-adapter/scripts/README.md +8 -0
- package/skills/skill-adapter/scripts/helper-template.sh +42 -0
- package/skills/skill-adapter/scripts/validation.sh +32 -0
- package/skills/tracking-crypto-derivatives/ARD.md +376 -0
- package/skills/tracking-crypto-derivatives/PRD.md +258 -0
- package/skills/tracking-crypto-derivatives/SKILL.md +127 -0
- package/skills/tracking-crypto-derivatives/config/settings.yaml +152 -0
- package/skills/tracking-crypto-derivatives/references/errors.md +224 -0
- package/skills/tracking-crypto-derivatives/references/examples.md +460 -0
- package/skills/tracking-crypto-derivatives/references/implementation.md +113 -0
- package/skills/tracking-crypto-derivatives/scripts/basis_calculator.py +377 -0
- package/skills/tracking-crypto-derivatives/scripts/derivatives_tracker.py +579 -0
- package/skills/tracking-crypto-derivatives/scripts/formatters.py +459 -0
- package/skills/tracking-crypto-derivatives/scripts/funding_tracker.py +308 -0
- package/skills/tracking-crypto-derivatives/scripts/liquidation_monitor.py +356 -0
- package/skills/tracking-crypto-derivatives/scripts/oi_analyzer.py +338 -0
- package/skills/tracking-crypto-derivatives/scripts/options_analyzer.py +373 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Funding rate tracker and analyzer.
|
|
4
|
+
|
|
5
|
+
Tracks funding rates across exchanges with:
|
|
6
|
+
- Multi-exchange aggregation
|
|
7
|
+
- Historical averages
|
|
8
|
+
- Arbitrage opportunity detection
|
|
9
|
+
- Sentiment analysis
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from exchange_client import ExchangeClient, FundingRate, Exchange
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FundingAnalysis:
|
|
22
|
+
"""Aggregated funding rate analysis."""
|
|
23
|
+
|
|
24
|
+
symbol: str
|
|
25
|
+
rates: List[FundingRate]
|
|
26
|
+
weighted_avg: float
|
|
27
|
+
annualized_avg: float
|
|
28
|
+
min_rate: FundingRate
|
|
29
|
+
max_rate: FundingRate
|
|
30
|
+
spread: float # Max - min rate
|
|
31
|
+
sentiment: str # "bullish", "bearish", "neutral"
|
|
32
|
+
sentiment_strength: str # "strong", "moderate", "weak"
|
|
33
|
+
arbitrage_opportunity: bool
|
|
34
|
+
arbitrage_spread: float
|
|
35
|
+
timestamp: datetime
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_extreme(self) -> bool:
|
|
39
|
+
"""Check if funding is at extreme levels."""
|
|
40
|
+
return abs(self.weighted_avg) > 0.08 # 0.08% 8-hour
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def exchanges_count(self) -> int:
|
|
44
|
+
"""Number of exchanges with data."""
|
|
45
|
+
return len(self.rates)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FundingTracker:
|
|
49
|
+
"""
|
|
50
|
+
Tracks and analyzes funding rates across exchanges.
|
|
51
|
+
|
|
52
|
+
Features:
|
|
53
|
+
- Real-time funding aggregation
|
|
54
|
+
- Weighted average calculation
|
|
55
|
+
- Sentiment analysis
|
|
56
|
+
- Arbitrage detection
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Funding rate interpretation thresholds
|
|
60
|
+
NEUTRAL_THRESHOLD = 0.005 # Below this is neutral
|
|
61
|
+
MODERATE_THRESHOLD = 0.03 # Below this is moderate
|
|
62
|
+
EXTREME_THRESHOLD = 0.08 # Above this is extreme
|
|
63
|
+
|
|
64
|
+
# Arbitrage minimum spread
|
|
65
|
+
ARB_MIN_SPREAD = 0.02 # 0.02% minimum for arbitrage
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
client: Optional[ExchangeClient] = None,
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Initialize funding tracker.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
client: Exchange client for data fetching
|
|
76
|
+
"""
|
|
77
|
+
self.client = client or ExchangeClient(use_mock=True)
|
|
78
|
+
|
|
79
|
+
def analyze(
|
|
80
|
+
self,
|
|
81
|
+
symbol: str,
|
|
82
|
+
exchanges: Optional[List[Exchange]] = None,
|
|
83
|
+
) -> FundingAnalysis:
|
|
84
|
+
"""
|
|
85
|
+
Analyze funding rates for a symbol.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
symbol: Trading symbol (e.g., "BTC")
|
|
89
|
+
exchanges: Exchanges to include
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
FundingAnalysis with all metrics
|
|
93
|
+
"""
|
|
94
|
+
# Fetch rates from all exchanges
|
|
95
|
+
rates = self.client.get_all_funding_rates(symbol, exchanges)
|
|
96
|
+
|
|
97
|
+
if not rates:
|
|
98
|
+
raise ValueError(f"No funding data available for {symbol}")
|
|
99
|
+
|
|
100
|
+
# Calculate weighted average (by estimated OI)
|
|
101
|
+
# For simplicity, use equal weights
|
|
102
|
+
avg_rate = sum(float(r.rate) for r in rates) / len(rates)
|
|
103
|
+
avg_annualized = sum(r.annualized for r in rates) / len(rates)
|
|
104
|
+
|
|
105
|
+
# Find min and max
|
|
106
|
+
min_rate = min(rates, key=lambda r: r.rate)
|
|
107
|
+
max_rate = max(rates, key=lambda r: r.rate)
|
|
108
|
+
spread = float(max_rate.rate - min_rate.rate)
|
|
109
|
+
|
|
110
|
+
# Determine sentiment
|
|
111
|
+
sentiment, strength = self._analyze_sentiment(avg_rate)
|
|
112
|
+
|
|
113
|
+
# Check for arbitrage opportunity
|
|
114
|
+
arb_opportunity = spread >= self.ARB_MIN_SPREAD
|
|
115
|
+
|
|
116
|
+
return FundingAnalysis(
|
|
117
|
+
symbol=symbol,
|
|
118
|
+
rates=rates,
|
|
119
|
+
weighted_avg=round(avg_rate, 6),
|
|
120
|
+
annualized_avg=round(avg_annualized, 2),
|
|
121
|
+
min_rate=min_rate,
|
|
122
|
+
max_rate=max_rate,
|
|
123
|
+
spread=round(spread, 6),
|
|
124
|
+
sentiment=sentiment,
|
|
125
|
+
sentiment_strength=strength,
|
|
126
|
+
arbitrage_opportunity=arb_opportunity,
|
|
127
|
+
arbitrage_spread=round(spread, 6),
|
|
128
|
+
timestamp=datetime.now(),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _analyze_sentiment(
|
|
132
|
+
self,
|
|
133
|
+
avg_rate: float,
|
|
134
|
+
) -> tuple:
|
|
135
|
+
"""
|
|
136
|
+
Analyze market sentiment from funding rate.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
(sentiment, strength) tuple
|
|
140
|
+
"""
|
|
141
|
+
abs_rate = abs(avg_rate)
|
|
142
|
+
|
|
143
|
+
# Determine direction
|
|
144
|
+
if avg_rate > self.NEUTRAL_THRESHOLD:
|
|
145
|
+
sentiment = "bullish"
|
|
146
|
+
elif avg_rate < -self.NEUTRAL_THRESHOLD:
|
|
147
|
+
sentiment = "bearish"
|
|
148
|
+
else:
|
|
149
|
+
sentiment = "neutral"
|
|
150
|
+
|
|
151
|
+
# Determine strength
|
|
152
|
+
if abs_rate >= self.EXTREME_THRESHOLD:
|
|
153
|
+
strength = "extreme"
|
|
154
|
+
elif abs_rate >= self.MODERATE_THRESHOLD:
|
|
155
|
+
strength = "strong"
|
|
156
|
+
elif abs_rate >= self.NEUTRAL_THRESHOLD:
|
|
157
|
+
strength = "moderate"
|
|
158
|
+
else:
|
|
159
|
+
strength = "weak"
|
|
160
|
+
|
|
161
|
+
return sentiment, strength
|
|
162
|
+
|
|
163
|
+
def get_arbitrage_opportunities(
|
|
164
|
+
self,
|
|
165
|
+
symbols: List[str],
|
|
166
|
+
min_spread: float = 0.02,
|
|
167
|
+
) -> List[Dict]:
|
|
168
|
+
"""
|
|
169
|
+
Find funding arbitrage opportunities across symbols.
|
|
170
|
+
|
|
171
|
+
Strategy: Long on exchange with negative/low funding,
|
|
172
|
+
Short on exchange with positive/high funding.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
symbols: Symbols to check
|
|
176
|
+
min_spread: Minimum spread to report
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of arbitrage opportunities
|
|
180
|
+
"""
|
|
181
|
+
opportunities = []
|
|
182
|
+
|
|
183
|
+
for symbol in symbols:
|
|
184
|
+
analysis = self.analyze(symbol)
|
|
185
|
+
|
|
186
|
+
if analysis.spread >= min_spread:
|
|
187
|
+
profit_8h = analysis.spread
|
|
188
|
+
profit_daily = profit_8h * 3
|
|
189
|
+
profit_annual = profit_8h * 365 * 3
|
|
190
|
+
|
|
191
|
+
opportunities.append({
|
|
192
|
+
"symbol": symbol,
|
|
193
|
+
"long_exchange": analysis.min_rate.exchange,
|
|
194
|
+
"long_rate": float(analysis.min_rate.rate),
|
|
195
|
+
"short_exchange": analysis.max_rate.exchange,
|
|
196
|
+
"short_rate": float(analysis.max_rate.rate),
|
|
197
|
+
"spread": analysis.spread,
|
|
198
|
+
"profit_8h_pct": round(profit_8h, 4),
|
|
199
|
+
"profit_daily_pct": round(profit_daily, 4),
|
|
200
|
+
"profit_annual_pct": round(profit_annual, 2),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
# Sort by spread descending
|
|
204
|
+
opportunities.sort(key=lambda x: x["spread"], reverse=True)
|
|
205
|
+
return opportunities
|
|
206
|
+
|
|
207
|
+
def get_extreme_funding(
|
|
208
|
+
self,
|
|
209
|
+
symbols: List[str],
|
|
210
|
+
threshold: float = 0.08,
|
|
211
|
+
) -> List[Dict]:
|
|
212
|
+
"""
|
|
213
|
+
Find symbols with extreme funding rates.
|
|
214
|
+
|
|
215
|
+
Extreme funding often indicates crowded trades and
|
|
216
|
+
potential mean reversion opportunities.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
symbols: Symbols to check
|
|
220
|
+
threshold: Extreme threshold (default 0.08%)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of extreme funding situations
|
|
224
|
+
"""
|
|
225
|
+
extreme = []
|
|
226
|
+
|
|
227
|
+
for symbol in symbols:
|
|
228
|
+
analysis = self.analyze(symbol)
|
|
229
|
+
|
|
230
|
+
if abs(analysis.weighted_avg) >= threshold:
|
|
231
|
+
extreme.append({
|
|
232
|
+
"symbol": symbol,
|
|
233
|
+
"avg_rate": analysis.weighted_avg,
|
|
234
|
+
"annualized": analysis.annualized_avg,
|
|
235
|
+
"sentiment": analysis.sentiment,
|
|
236
|
+
"strength": analysis.sentiment_strength,
|
|
237
|
+
"signal": "short" if analysis.weighted_avg > 0 else "long",
|
|
238
|
+
"signal_reason": "Contrarian: extreme funding often reverts",
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
# Sort by absolute rate descending
|
|
242
|
+
extreme.sort(key=lambda x: abs(x["avg_rate"]), reverse=True)
|
|
243
|
+
return extreme
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def demo():
|
|
247
|
+
"""Demonstrate funding tracker."""
|
|
248
|
+
tracker = FundingTracker()
|
|
249
|
+
|
|
250
|
+
print("=" * 70)
|
|
251
|
+
print("FUNDING RATE TRACKER")
|
|
252
|
+
print("=" * 70)
|
|
253
|
+
|
|
254
|
+
# Analyze BTC funding
|
|
255
|
+
analysis = tracker.analyze("BTC")
|
|
256
|
+
|
|
257
|
+
print(f"\nš {analysis.symbol} FUNDING ANALYSIS")
|
|
258
|
+
print("-" * 50)
|
|
259
|
+
|
|
260
|
+
print(f"\n{'Exchange':<12} {'Current':>10} {'Annualized':>12} {'Next Payment':>14}")
|
|
261
|
+
print("-" * 50)
|
|
262
|
+
|
|
263
|
+
for rate in sorted(analysis.rates, key=lambda r: r.rate, reverse=True):
|
|
264
|
+
print(
|
|
265
|
+
f"{rate.exchange:<12} "
|
|
266
|
+
f"{float(rate.rate):>+9.4%} "
|
|
267
|
+
f"{rate.annualized:>+11.2f}% "
|
|
268
|
+
f"{rate.time_to_payment_str:>14}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
print("-" * 50)
|
|
272
|
+
print(f"\nWeighted Average: {analysis.weighted_avg:+.4%}")
|
|
273
|
+
print(f"Annualized: {analysis.annualized_avg:+.2f}%")
|
|
274
|
+
print(f"Spread (max-min): {analysis.spread:.4%}")
|
|
275
|
+
print(f"\nSentiment: {analysis.sentiment_strength.title()} {analysis.sentiment.title()}")
|
|
276
|
+
|
|
277
|
+
if analysis.is_extreme:
|
|
278
|
+
print(f"\nā ļø EXTREME FUNDING - Contrarian opportunity")
|
|
279
|
+
|
|
280
|
+
if analysis.arbitrage_opportunity:
|
|
281
|
+
print(f"\nš° ARBITRAGE OPPORTUNITY")
|
|
282
|
+
print(f" Long on {analysis.min_rate.exchange} ({float(analysis.min_rate.rate):+.4%})")
|
|
283
|
+
print(f" Short on {analysis.max_rate.exchange} ({float(analysis.max_rate.rate):+.4%})")
|
|
284
|
+
print(f" Profit: {analysis.arbitrage_spread:.4%} per 8h")
|
|
285
|
+
|
|
286
|
+
# Check multiple symbols for arbitrage
|
|
287
|
+
print("\n" + "=" * 70)
|
|
288
|
+
print("FUNDING ARBITRAGE SCANNER")
|
|
289
|
+
print("=" * 70)
|
|
290
|
+
|
|
291
|
+
opportunities = tracker.get_arbitrage_opportunities(["BTC", "ETH", "SOL"])
|
|
292
|
+
if opportunities:
|
|
293
|
+
print(f"\n{'Symbol':<8} {'Long On':<12} {'Short On':<12} {'Spread':>8} {'Annual':>10}")
|
|
294
|
+
print("-" * 50)
|
|
295
|
+
for opp in opportunities:
|
|
296
|
+
print(
|
|
297
|
+
f"{opp['symbol']:<8} "
|
|
298
|
+
f"{opp['long_exchange']:<12} "
|
|
299
|
+
f"{opp['short_exchange']:<12} "
|
|
300
|
+
f"{opp['spread']:>7.4%} "
|
|
301
|
+
f"{opp['profit_annual_pct']:>+9.1f}%"
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
print("\nNo arbitrage opportunities found")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
demo()
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Liquidation monitor and heatmap generator.
|
|
4
|
+
|
|
5
|
+
Tracks liquidations across exchanges with:
|
|
6
|
+
- Real-time liquidation events
|
|
7
|
+
- Liquidation level clustering
|
|
8
|
+
- Cascade risk assessment
|
|
9
|
+
- Heatmap visualization
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
|
|
17
|
+
from exchange_client import (
|
|
18
|
+
ExchangeClient, Liquidation, LiquidationLevel, Exchange
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class LiquidationSummary:
|
|
24
|
+
"""Summary of liquidation activity."""
|
|
25
|
+
|
|
26
|
+
symbol: str
|
|
27
|
+
current_price: Decimal
|
|
28
|
+
total_24h_usd: Decimal
|
|
29
|
+
long_liquidations_usd: Decimal
|
|
30
|
+
short_liquidations_usd: Decimal
|
|
31
|
+
largest_single: Liquidation
|
|
32
|
+
recent_liquidations: List[Liquidation]
|
|
33
|
+
long_levels: List[LiquidationLevel]
|
|
34
|
+
short_levels: List[LiquidationLevel]
|
|
35
|
+
cascade_risk: str # "low", "medium", "high", "critical"
|
|
36
|
+
nearest_long_level: Optional[LiquidationLevel]
|
|
37
|
+
nearest_short_level: Optional[LiquidationLevel]
|
|
38
|
+
timestamp: datetime
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LiquidationMonitor:
|
|
42
|
+
"""
|
|
43
|
+
Monitors liquidation events and levels.
|
|
44
|
+
|
|
45
|
+
Features:
|
|
46
|
+
- Real-time liquidation tracking
|
|
47
|
+
- Heatmap generation
|
|
48
|
+
- Cascade risk assessment
|
|
49
|
+
- Level clustering
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Cascade risk thresholds (USD within 5% of price)
|
|
53
|
+
CRITICAL_THRESHOLD = 500_000_000 # $500M
|
|
54
|
+
HIGH_THRESHOLD = 200_000_000 # $200M
|
|
55
|
+
MEDIUM_THRESHOLD = 100_000_000 # $100M
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
client: Optional[ExchangeClient] = None,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize liquidation monitor.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
client: Exchange client for data fetching
|
|
66
|
+
"""
|
|
67
|
+
self.client = client or ExchangeClient(use_mock=True)
|
|
68
|
+
|
|
69
|
+
def get_summary(
|
|
70
|
+
self,
|
|
71
|
+
symbol: str,
|
|
72
|
+
current_price: Optional[Decimal] = None,
|
|
73
|
+
) -> LiquidationSummary:
|
|
74
|
+
"""
|
|
75
|
+
Get comprehensive liquidation summary.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
symbol: Trading symbol (e.g., "BTC")
|
|
79
|
+
current_price: Current price (fetched if not provided)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
LiquidationSummary with all metrics
|
|
83
|
+
"""
|
|
84
|
+
# Set default price if not provided
|
|
85
|
+
if current_price is None:
|
|
86
|
+
if symbol == "BTC":
|
|
87
|
+
current_price = Decimal("67500")
|
|
88
|
+
elif symbol == "ETH":
|
|
89
|
+
current_price = Decimal("2500")
|
|
90
|
+
else:
|
|
91
|
+
current_price = Decimal("100")
|
|
92
|
+
|
|
93
|
+
# Fetch recent liquidations
|
|
94
|
+
liquidations = self.client.get_recent_liquidations(
|
|
95
|
+
symbol, limit=100, min_value_usd=100000
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Fetch liquidation levels
|
|
99
|
+
levels = self.client.get_liquidation_levels(symbol, current_price)
|
|
100
|
+
|
|
101
|
+
# Separate long and short levels
|
|
102
|
+
long_levels = [l for l in levels if l.side == "long"]
|
|
103
|
+
short_levels = [l for l in levels if l.side == "short"]
|
|
104
|
+
|
|
105
|
+
# Sort by distance from current price
|
|
106
|
+
long_levels.sort(key=lambda x: x.price, reverse=True)
|
|
107
|
+
short_levels.sort(key=lambda x: x.price)
|
|
108
|
+
|
|
109
|
+
# Calculate 24h totals
|
|
110
|
+
cutoff = datetime.now() - timedelta(hours=24)
|
|
111
|
+
recent_24h = [l for l in liquidations if l.timestamp > cutoff]
|
|
112
|
+
|
|
113
|
+
total_24h = sum(float(l.value_usd) for l in recent_24h)
|
|
114
|
+
long_24h = sum(float(l.value_usd) for l in recent_24h if l.side == "long")
|
|
115
|
+
short_24h = sum(float(l.value_usd) for l in recent_24h if l.side == "short")
|
|
116
|
+
|
|
117
|
+
# Find largest single liquidation
|
|
118
|
+
largest = max(liquidations, key=lambda x: x.value_usd) if liquidations else None
|
|
119
|
+
|
|
120
|
+
# Find nearest levels
|
|
121
|
+
nearest_long = long_levels[0] if long_levels else None
|
|
122
|
+
nearest_short = short_levels[0] if short_levels else None
|
|
123
|
+
|
|
124
|
+
# Assess cascade risk
|
|
125
|
+
cascade_risk = self._assess_cascade_risk(
|
|
126
|
+
current_price, long_levels, short_levels
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return LiquidationSummary(
|
|
130
|
+
symbol=symbol,
|
|
131
|
+
current_price=current_price,
|
|
132
|
+
total_24h_usd=Decimal(str(int(total_24h))),
|
|
133
|
+
long_liquidations_usd=Decimal(str(int(long_24h))),
|
|
134
|
+
short_liquidations_usd=Decimal(str(int(short_24h))),
|
|
135
|
+
largest_single=largest,
|
|
136
|
+
recent_liquidations=liquidations[:20],
|
|
137
|
+
long_levels=long_levels,
|
|
138
|
+
short_levels=short_levels,
|
|
139
|
+
cascade_risk=cascade_risk,
|
|
140
|
+
nearest_long_level=nearest_long,
|
|
141
|
+
nearest_short_level=nearest_short,
|
|
142
|
+
timestamp=datetime.now(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _assess_cascade_risk(
|
|
146
|
+
self,
|
|
147
|
+
current_price: Decimal,
|
|
148
|
+
long_levels: List[LiquidationLevel],
|
|
149
|
+
short_levels: List[LiquidationLevel],
|
|
150
|
+
) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Assess cascade risk based on nearby liquidation levels.
|
|
153
|
+
|
|
154
|
+
Considers liquidations within 5% of current price.
|
|
155
|
+
"""
|
|
156
|
+
price = float(current_price)
|
|
157
|
+
lower_bound = price * 0.95
|
|
158
|
+
upper_bound = price * 1.05
|
|
159
|
+
|
|
160
|
+
# Sum liquidations within range
|
|
161
|
+
nearby_value = 0
|
|
162
|
+
|
|
163
|
+
for level in long_levels:
|
|
164
|
+
if float(level.price) >= lower_bound:
|
|
165
|
+
nearby_value += float(level.total_value_usd)
|
|
166
|
+
|
|
167
|
+
for level in short_levels:
|
|
168
|
+
if float(level.price) <= upper_bound:
|
|
169
|
+
nearby_value += float(level.total_value_usd)
|
|
170
|
+
|
|
171
|
+
# Determine risk level
|
|
172
|
+
if nearby_value >= self.CRITICAL_THRESHOLD:
|
|
173
|
+
return "critical"
|
|
174
|
+
elif nearby_value >= self.HIGH_THRESHOLD:
|
|
175
|
+
return "high"
|
|
176
|
+
elif nearby_value >= self.MEDIUM_THRESHOLD:
|
|
177
|
+
return "medium"
|
|
178
|
+
else:
|
|
179
|
+
return "low"
|
|
180
|
+
|
|
181
|
+
def generate_heatmap_data(
|
|
182
|
+
self,
|
|
183
|
+
symbol: str,
|
|
184
|
+
current_price: Decimal,
|
|
185
|
+
levels: int = 5,
|
|
186
|
+
) -> Dict:
|
|
187
|
+
"""
|
|
188
|
+
Generate heatmap visualization data.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
symbol: Trading symbol
|
|
192
|
+
current_price: Current price
|
|
193
|
+
levels: Number of levels above/below
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dict with heatmap data for visualization
|
|
197
|
+
"""
|
|
198
|
+
summary = self.get_summary(symbol, current_price)
|
|
199
|
+
|
|
200
|
+
heatmap = {
|
|
201
|
+
"symbol": symbol,
|
|
202
|
+
"current_price": float(current_price),
|
|
203
|
+
"long_levels": [],
|
|
204
|
+
"short_levels": [],
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Add long levels (below price)
|
|
208
|
+
for level in summary.long_levels[:levels]:
|
|
209
|
+
distance_pct = (float(current_price) - float(level.price)) / float(current_price) * 100
|
|
210
|
+
heatmap["long_levels"].append({
|
|
211
|
+
"price": float(level.price),
|
|
212
|
+
"value_usd": float(level.total_value_usd),
|
|
213
|
+
"distance_pct": round(distance_pct, 1),
|
|
214
|
+
"density": level.density,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
# Add short levels (above price)
|
|
218
|
+
for level in summary.short_levels[:levels]:
|
|
219
|
+
distance_pct = (float(level.price) - float(current_price)) / float(current_price) * 100
|
|
220
|
+
heatmap["short_levels"].append({
|
|
221
|
+
"price": float(level.price),
|
|
222
|
+
"value_usd": float(level.total_value_usd),
|
|
223
|
+
"distance_pct": round(distance_pct, 1),
|
|
224
|
+
"density": level.density,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return heatmap
|
|
228
|
+
|
|
229
|
+
def get_recent_large_liquidations(
|
|
230
|
+
self,
|
|
231
|
+
symbol: str,
|
|
232
|
+
min_value_usd: float = 1_000_000,
|
|
233
|
+
limit: int = 10,
|
|
234
|
+
) -> List[Dict]:
|
|
235
|
+
"""
|
|
236
|
+
Get recent large liquidation events.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
symbol: Trading symbol
|
|
240
|
+
min_value_usd: Minimum liquidation size
|
|
241
|
+
limit: Maximum results
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of large liquidations
|
|
245
|
+
"""
|
|
246
|
+
liquidations = self.client.get_recent_liquidations(
|
|
247
|
+
symbol, limit=limit * 2, min_value_usd=min_value_usd
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Filter by size and limit
|
|
251
|
+
large = [l for l in liquidations if float(l.value_usd) >= min_value_usd]
|
|
252
|
+
large = sorted(large, key=lambda x: x.value_usd, reverse=True)[:limit]
|
|
253
|
+
|
|
254
|
+
return [
|
|
255
|
+
{
|
|
256
|
+
"exchange": l.exchange,
|
|
257
|
+
"side": l.side,
|
|
258
|
+
"price": float(l.price),
|
|
259
|
+
"quantity": float(l.quantity),
|
|
260
|
+
"value_usd": float(l.value_usd),
|
|
261
|
+
"time_ago": self._time_ago(l.timestamp),
|
|
262
|
+
}
|
|
263
|
+
for l in large
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
def _time_ago(self, dt: datetime) -> str:
|
|
267
|
+
"""Format timestamp as time ago string."""
|
|
268
|
+
delta = datetime.now() - dt
|
|
269
|
+
minutes = int(delta.total_seconds() / 60)
|
|
270
|
+
|
|
271
|
+
if minutes < 60:
|
|
272
|
+
return f"{minutes}m ago"
|
|
273
|
+
elif minutes < 1440:
|
|
274
|
+
hours = minutes // 60
|
|
275
|
+
return f"{hours}h ago"
|
|
276
|
+
else:
|
|
277
|
+
days = minutes // 1440
|
|
278
|
+
return f"{days}d ago"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def demo():
|
|
282
|
+
"""Demonstrate liquidation monitor."""
|
|
283
|
+
monitor = LiquidationMonitor()
|
|
284
|
+
|
|
285
|
+
print("=" * 70)
|
|
286
|
+
print("LIQUIDATION MONITOR")
|
|
287
|
+
print("=" * 70)
|
|
288
|
+
|
|
289
|
+
# Get BTC liquidation summary
|
|
290
|
+
summary = monitor.get_summary("BTC", Decimal("67500"))
|
|
291
|
+
|
|
292
|
+
print(f"\nš„ {summary.symbol} LIQUIDATION SUMMARY")
|
|
293
|
+
print(f" Current Price: ${summary.current_price:,}")
|
|
294
|
+
print("-" * 60)
|
|
295
|
+
|
|
296
|
+
# 24h totals
|
|
297
|
+
print(f"\n24h Liquidations:")
|
|
298
|
+
print(f" Total: ${float(summary.total_24h_usd)/1e6:,.1f}M")
|
|
299
|
+
print(f" Longs: ${float(summary.long_liquidations_usd)/1e6:,.1f}M")
|
|
300
|
+
print(f" Shorts: ${float(summary.short_liquidations_usd)/1e6:,.1f}M")
|
|
301
|
+
|
|
302
|
+
# Cascade risk
|
|
303
|
+
risk_emoji = {
|
|
304
|
+
"low": "š¢",
|
|
305
|
+
"medium": "š”",
|
|
306
|
+
"high": "š ",
|
|
307
|
+
"critical": "š“",
|
|
308
|
+
}
|
|
309
|
+
print(f"\nCascade Risk: {risk_emoji[summary.cascade_risk]} {summary.cascade_risk.upper()}")
|
|
310
|
+
|
|
311
|
+
# Heatmap
|
|
312
|
+
print("\n" + "-" * 60)
|
|
313
|
+
print("LIQUIDATION HEATMAP")
|
|
314
|
+
print("-" * 60)
|
|
315
|
+
|
|
316
|
+
print(f"\nLONG LIQUIDATIONS (below ${summary.current_price:,}):")
|
|
317
|
+
for level in summary.long_levels[:4]:
|
|
318
|
+
bar_len = min(int(float(level.total_value_usd) / 10_000_000), 20)
|
|
319
|
+
bar = "ā" * bar_len
|
|
320
|
+
density_mark = "ā ļø " if level.density in ["high", "critical"] else ""
|
|
321
|
+
print(
|
|
322
|
+
f" ${float(level.price):>10,.0f} {bar} "
|
|
323
|
+
f"${float(level.total_value_usd)/1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
print(f"\nSHORT LIQUIDATIONS (above ${summary.current_price:,}):")
|
|
327
|
+
for level in summary.short_levels[:4]:
|
|
328
|
+
bar_len = min(int(float(level.total_value_usd) / 10_000_000), 20)
|
|
329
|
+
bar = "ā" * bar_len
|
|
330
|
+
density_mark = "ā ļø " if level.density in ["high", "critical"] else ""
|
|
331
|
+
print(
|
|
332
|
+
f" ${float(level.price):>10,.0f} {bar} "
|
|
333
|
+
f"${float(level.total_value_usd)/1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Recent large liquidations
|
|
337
|
+
print("\n" + "-" * 60)
|
|
338
|
+
print("RECENT LARGE LIQUIDATIONS (>$1M)")
|
|
339
|
+
print("-" * 60)
|
|
340
|
+
|
|
341
|
+
large = monitor.get_recent_large_liquidations("BTC", min_value_usd=1_000_000, limit=5)
|
|
342
|
+
if large:
|
|
343
|
+
print(f"\n{'Exchange':<10} {'Side':<6} {'Price':>12} {'Value':>12} {'When':>10}")
|
|
344
|
+
print("-" * 60)
|
|
345
|
+
for l in large:
|
|
346
|
+
print(
|
|
347
|
+
f"{l['exchange']:<10} "
|
|
348
|
+
f"{l['side']:<6} "
|
|
349
|
+
f"${l['price']:>10,.0f} "
|
|
350
|
+
f"${l['value_usd']/1e6:>10.1f}M "
|
|
351
|
+
f"{l['time_ago']:>10}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
demo()
|