@intentsolutionsio/crypto-derivatives-tracker 1.0.0 → 1.0.3
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/README.md +16 -1
- package/agents/derivatives-agent.md +29 -0
- package/package.json +1 -1
- package/skills/skill-adapter/references/README.md +0 -1
- package/skills/skill-adapter/references/examples.md +6 -0
- package/skills/tracking-crypto-derivatives/ARD.md +22 -0
- package/skills/tracking-crypto-derivatives/PRD.md +26 -0
- package/skills/tracking-crypto-derivatives/SKILL.md +28 -6
- package/skills/tracking-crypto-derivatives/references/errors.md +24 -0
- package/skills/tracking-crypto-derivatives/references/examples.md +19 -0
- package/skills/tracking-crypto-derivatives/references/implementation.md +8 -0
- package/skills/tracking-crypto-derivatives/scripts/basis_calculator.py +23 -24
- package/skills/tracking-crypto-derivatives/scripts/derivatives_tracker.py +176 -175
- package/skills/tracking-crypto-derivatives/scripts/formatters.py +13 -18
- package/skills/tracking-crypto-derivatives/scripts/funding_tracker.py +34 -34
- package/skills/tracking-crypto-derivatives/scripts/liquidation_monitor.py +31 -35
- package/skills/tracking-crypto-derivatives/scripts/oi_analyzer.py +23 -27
- package/skills/tracking-crypto-derivatives/scripts/options_analyzer.py +42 -41
|
@@ -10,7 +10,6 @@ Tracks funding rates across exchanges with:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from decimal import Decimal
|
|
14
13
|
from typing import Dict, List, Optional
|
|
15
14
|
from datetime import datetime
|
|
16
15
|
|
|
@@ -27,9 +26,9 @@ class FundingAnalysis:
|
|
|
27
26
|
annualized_avg: float
|
|
28
27
|
min_rate: FundingRate
|
|
29
28
|
max_rate: FundingRate
|
|
30
|
-
spread: float
|
|
31
|
-
sentiment: str
|
|
32
|
-
sentiment_strength: str
|
|
29
|
+
spread: float # Max - min rate
|
|
30
|
+
sentiment: str # "bullish", "bearish", "neutral"
|
|
31
|
+
sentiment_strength: str # "strong", "moderate", "weak"
|
|
33
32
|
arbitrage_opportunity: bool
|
|
34
33
|
arbitrage_spread: float
|
|
35
34
|
timestamp: datetime
|
|
@@ -57,12 +56,12 @@ class FundingTracker:
|
|
|
57
56
|
"""
|
|
58
57
|
|
|
59
58
|
# Funding rate interpretation thresholds
|
|
60
|
-
NEUTRAL_THRESHOLD = 0.005
|
|
61
|
-
MODERATE_THRESHOLD = 0.03
|
|
62
|
-
EXTREME_THRESHOLD = 0.08
|
|
59
|
+
NEUTRAL_THRESHOLD = 0.005 # Below this is neutral
|
|
60
|
+
MODERATE_THRESHOLD = 0.03 # Below this is moderate
|
|
61
|
+
EXTREME_THRESHOLD = 0.08 # Above this is extreme
|
|
63
62
|
|
|
64
63
|
# Arbitrage minimum spread
|
|
65
|
-
ARB_MIN_SPREAD = 0.02
|
|
64
|
+
ARB_MIN_SPREAD = 0.02 # 0.02% minimum for arbitrage
|
|
66
65
|
|
|
67
66
|
def __init__(
|
|
68
67
|
self,
|
|
@@ -188,17 +187,19 @@ class FundingTracker:
|
|
|
188
187
|
profit_daily = profit_8h * 3
|
|
189
188
|
profit_annual = profit_8h * 365 * 3
|
|
190
189
|
|
|
191
|
-
opportunities.append(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
190
|
+
opportunities.append(
|
|
191
|
+
{
|
|
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
|
+
)
|
|
202
203
|
|
|
203
204
|
# Sort by spread descending
|
|
204
205
|
opportunities.sort(key=lambda x: x["spread"], reverse=True)
|
|
@@ -228,15 +229,17 @@ class FundingTracker:
|
|
|
228
229
|
analysis = self.analyze(symbol)
|
|
229
230
|
|
|
230
231
|
if abs(analysis.weighted_avg) >= threshold:
|
|
231
|
-
extreme.append(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
extreme.append(
|
|
233
|
+
{
|
|
234
|
+
"symbol": symbol,
|
|
235
|
+
"avg_rate": analysis.weighted_avg,
|
|
236
|
+
"annualized": analysis.annualized_avg,
|
|
237
|
+
"sentiment": analysis.sentiment,
|
|
238
|
+
"strength": analysis.sentiment_strength,
|
|
239
|
+
"signal": "short" if analysis.weighted_avg > 0 else "long",
|
|
240
|
+
"signal_reason": "Contrarian: extreme funding often reverts",
|
|
241
|
+
}
|
|
242
|
+
)
|
|
240
243
|
|
|
241
244
|
# Sort by absolute rate descending
|
|
242
245
|
extreme.sort(key=lambda x: abs(x["avg_rate"]), reverse=True)
|
|
@@ -262,10 +265,7 @@ def demo():
|
|
|
262
265
|
|
|
263
266
|
for rate in sorted(analysis.rates, key=lambda r: r.rate, reverse=True):
|
|
264
267
|
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}"
|
|
268
|
+
f"{rate.exchange:<12} {float(rate.rate):>+9.4%} {rate.annualized:>+11.2f}% {rate.time_to_payment_str:>14}"
|
|
269
269
|
)
|
|
270
270
|
|
|
271
271
|
print("-" * 50)
|
|
@@ -275,10 +275,10 @@ def demo():
|
|
|
275
275
|
print(f"\nSentiment: {analysis.sentiment_strength.title()} {analysis.sentiment.title()}")
|
|
276
276
|
|
|
277
277
|
if analysis.is_extreme:
|
|
278
|
-
print(
|
|
278
|
+
print("\n⚠️ EXTREME FUNDING - Contrarian opportunity")
|
|
279
279
|
|
|
280
280
|
if analysis.arbitrage_opportunity:
|
|
281
|
-
print(
|
|
281
|
+
print("\n💰 ARBITRAGE OPPORTUNITY")
|
|
282
282
|
print(f" Long on {analysis.min_rate.exchange} ({float(analysis.min_rate.rate):+.4%})")
|
|
283
283
|
print(f" Short on {analysis.max_rate.exchange} ({float(analysis.max_rate.rate):+.4%})")
|
|
284
284
|
print(f" Profit: {analysis.arbitrage_spread:.4%} per 8h")
|
|
@@ -14,9 +14,7 @@ from decimal import Decimal
|
|
|
14
14
|
from typing import Dict, List, Optional
|
|
15
15
|
from datetime import datetime, timedelta
|
|
16
16
|
|
|
17
|
-
from exchange_client import
|
|
18
|
-
ExchangeClient, Liquidation, LiquidationLevel, Exchange
|
|
19
|
-
)
|
|
17
|
+
from exchange_client import ExchangeClient, Liquidation, LiquidationLevel
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
@dataclass
|
|
@@ -32,7 +30,7 @@ class LiquidationSummary:
|
|
|
32
30
|
recent_liquidations: List[Liquidation]
|
|
33
31
|
long_levels: List[LiquidationLevel]
|
|
34
32
|
short_levels: List[LiquidationLevel]
|
|
35
|
-
cascade_risk: str
|
|
33
|
+
cascade_risk: str # "low", "medium", "high", "critical"
|
|
36
34
|
nearest_long_level: Optional[LiquidationLevel]
|
|
37
35
|
nearest_short_level: Optional[LiquidationLevel]
|
|
38
36
|
timestamp: datetime
|
|
@@ -50,9 +48,9 @@ class LiquidationMonitor:
|
|
|
50
48
|
"""
|
|
51
49
|
|
|
52
50
|
# Cascade risk thresholds (USD within 5% of price)
|
|
53
|
-
CRITICAL_THRESHOLD = 500_000_000
|
|
54
|
-
HIGH_THRESHOLD = 200_000_000
|
|
55
|
-
MEDIUM_THRESHOLD = 100_000_000
|
|
51
|
+
CRITICAL_THRESHOLD = 500_000_000 # $500M
|
|
52
|
+
HIGH_THRESHOLD = 200_000_000 # $200M
|
|
53
|
+
MEDIUM_THRESHOLD = 100_000_000 # $100M
|
|
56
54
|
|
|
57
55
|
def __init__(
|
|
58
56
|
self,
|
|
@@ -91,9 +89,7 @@ class LiquidationMonitor:
|
|
|
91
89
|
current_price = Decimal("100")
|
|
92
90
|
|
|
93
91
|
# Fetch recent liquidations
|
|
94
|
-
liquidations = self.client.get_recent_liquidations(
|
|
95
|
-
symbol, limit=100, min_value_usd=100000
|
|
96
|
-
)
|
|
92
|
+
liquidations = self.client.get_recent_liquidations(symbol, limit=100, min_value_usd=100000)
|
|
97
93
|
|
|
98
94
|
# Fetch liquidation levels
|
|
99
95
|
levels = self.client.get_liquidation_levels(symbol, current_price)
|
|
@@ -122,9 +118,7 @@ class LiquidationMonitor:
|
|
|
122
118
|
nearest_short = short_levels[0] if short_levels else None
|
|
123
119
|
|
|
124
120
|
# Assess cascade risk
|
|
125
|
-
cascade_risk = self._assess_cascade_risk(
|
|
126
|
-
current_price, long_levels, short_levels
|
|
127
|
-
)
|
|
121
|
+
cascade_risk = self._assess_cascade_risk(current_price, long_levels, short_levels)
|
|
128
122
|
|
|
129
123
|
return LiquidationSummary(
|
|
130
124
|
symbol=symbol,
|
|
@@ -207,22 +201,26 @@ class LiquidationMonitor:
|
|
|
207
201
|
# Add long levels (below price)
|
|
208
202
|
for level in summary.long_levels[:levels]:
|
|
209
203
|
distance_pct = (float(current_price) - float(level.price)) / float(current_price) * 100
|
|
210
|
-
heatmap["long_levels"].append(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
heatmap["long_levels"].append(
|
|
205
|
+
{
|
|
206
|
+
"price": float(level.price),
|
|
207
|
+
"value_usd": float(level.total_value_usd),
|
|
208
|
+
"distance_pct": round(distance_pct, 1),
|
|
209
|
+
"density": level.density,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
216
212
|
|
|
217
213
|
# Add short levels (above price)
|
|
218
214
|
for level in summary.short_levels[:levels]:
|
|
219
215
|
distance_pct = (float(level.price) - float(current_price)) / float(current_price) * 100
|
|
220
|
-
heatmap["short_levels"].append(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
216
|
+
heatmap["short_levels"].append(
|
|
217
|
+
{
|
|
218
|
+
"price": float(level.price),
|
|
219
|
+
"value_usd": float(level.total_value_usd),
|
|
220
|
+
"distance_pct": round(distance_pct, 1),
|
|
221
|
+
"density": level.density,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
226
224
|
|
|
227
225
|
return heatmap
|
|
228
226
|
|
|
@@ -243,9 +241,7 @@ class LiquidationMonitor:
|
|
|
243
241
|
Returns:
|
|
244
242
|
List of large liquidations
|
|
245
243
|
"""
|
|
246
|
-
liquidations = self.client.get_recent_liquidations(
|
|
247
|
-
symbol, limit=limit * 2, min_value_usd=min_value_usd
|
|
248
|
-
)
|
|
244
|
+
liquidations = self.client.get_recent_liquidations(symbol, limit=limit * 2, min_value_usd=min_value_usd)
|
|
249
245
|
|
|
250
246
|
# Filter by size and limit
|
|
251
247
|
large = [l for l in liquidations if float(l.value_usd) >= min_value_usd]
|
|
@@ -294,10 +290,10 @@ def demo():
|
|
|
294
290
|
print("-" * 60)
|
|
295
291
|
|
|
296
292
|
# 24h totals
|
|
297
|
-
print(
|
|
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")
|
|
293
|
+
print("\n24h Liquidations:")
|
|
294
|
+
print(f" Total: ${float(summary.total_24h_usd) / 1e6:,.1f}M")
|
|
295
|
+
print(f" Longs: ${float(summary.long_liquidations_usd) / 1e6:,.1f}M")
|
|
296
|
+
print(f" Shorts: ${float(summary.short_liquidations_usd) / 1e6:,.1f}M")
|
|
301
297
|
|
|
302
298
|
# Cascade risk
|
|
303
299
|
risk_emoji = {
|
|
@@ -320,7 +316,7 @@ def demo():
|
|
|
320
316
|
density_mark = "⚠️ " if level.density in ["high", "critical"] else ""
|
|
321
317
|
print(
|
|
322
318
|
f" ${float(level.price):>10,.0f} {bar} "
|
|
323
|
-
f"${float(level.total_value_usd)/1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
319
|
+
f"${float(level.total_value_usd) / 1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
324
320
|
)
|
|
325
321
|
|
|
326
322
|
print(f"\nSHORT LIQUIDATIONS (above ${summary.current_price:,}):")
|
|
@@ -330,7 +326,7 @@ def demo():
|
|
|
330
326
|
density_mark = "⚠️ " if level.density in ["high", "critical"] else ""
|
|
331
327
|
print(
|
|
332
328
|
f" ${float(level.price):>10,.0f} {bar} "
|
|
333
|
-
f"${float(level.total_value_usd)/1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
329
|
+
f"${float(level.total_value_usd) / 1e6:.0f}M {density_mark}{level.density.upper()}"
|
|
334
330
|
)
|
|
335
331
|
|
|
336
332
|
# Recent large liquidations
|
|
@@ -347,7 +343,7 @@ def demo():
|
|
|
347
343
|
f"{l['exchange']:<10} "
|
|
348
344
|
f"{l['side']:<6} "
|
|
349
345
|
f"${l['price']:>10,.0f} "
|
|
350
|
-
f"${l['value_usd']/1e6:>10.1f}M "
|
|
346
|
+
f"${l['value_usd'] / 1e6:>10.1f}M "
|
|
351
347
|
f"{l['time_ago']:>10}"
|
|
352
348
|
)
|
|
353
349
|
|
|
@@ -30,8 +30,8 @@ class OIAnalysis:
|
|
|
30
30
|
weighted_long_ratio: float
|
|
31
31
|
dominant_exchange: str
|
|
32
32
|
dominant_share: float
|
|
33
|
-
trend: str
|
|
34
|
-
trend_strength: str
|
|
33
|
+
trend: str # "increasing", "decreasing", "stable"
|
|
34
|
+
trend_strength: str # "strong", "moderate", "weak"
|
|
35
35
|
timestamp: datetime
|
|
36
36
|
|
|
37
37
|
@property
|
|
@@ -55,13 +55,13 @@ class OIDivergence:
|
|
|
55
55
|
"""OI vs Price divergence signal."""
|
|
56
56
|
|
|
57
57
|
symbol: str
|
|
58
|
-
oi_direction: str
|
|
59
|
-
price_direction: str
|
|
58
|
+
oi_direction: str # "up" or "down"
|
|
59
|
+
price_direction: str # "up" or "down"
|
|
60
60
|
oi_change_pct: float
|
|
61
61
|
price_change_pct: float
|
|
62
|
-
signal: str
|
|
62
|
+
signal: str # "bullish", "bearish", "short_squeeze", "long_liquidation"
|
|
63
63
|
description: str
|
|
64
|
-
confidence: str
|
|
64
|
+
confidence: str # "high", "medium", "low"
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class OIAnalyzer:
|
|
@@ -76,8 +76,8 @@ class OIAnalyzer:
|
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
78
|
# Trend thresholds
|
|
79
|
-
STRONG_CHANGE = 10.0
|
|
80
|
-
MODERATE_CHANGE = 5.0
|
|
79
|
+
STRONG_CHANGE = 10.0 # >10% is strong
|
|
80
|
+
MODERATE_CHANGE = 5.0 # >5% is moderate
|
|
81
81
|
|
|
82
82
|
def __init__(
|
|
83
83
|
self,
|
|
@@ -117,17 +117,11 @@ class OIAnalyzer:
|
|
|
117
117
|
total_contracts = sum(float(oi.oi_contracts) for oi in oi_list)
|
|
118
118
|
|
|
119
119
|
# Calculate weighted averages
|
|
120
|
-
avg_24h = sum(
|
|
121
|
-
|
|
122
|
-
) / total_usd
|
|
123
|
-
avg_7d = sum(
|
|
124
|
-
oi.change_7d_pct * float(oi.oi_usd) for oi in oi_list
|
|
125
|
-
) / total_usd
|
|
120
|
+
avg_24h = sum(oi.change_24h_pct * float(oi.oi_usd) for oi in oi_list) / total_usd
|
|
121
|
+
avg_7d = sum(oi.change_7d_pct * float(oi.oi_usd) for oi in oi_list) / total_usd
|
|
126
122
|
|
|
127
123
|
# Weighted long ratio
|
|
128
|
-
weighted_long = sum(
|
|
129
|
-
oi.long_ratio * float(oi.oi_usd) for oi in oi_list
|
|
130
|
-
) / total_usd
|
|
124
|
+
weighted_long = sum(oi.long_ratio * float(oi.oi_usd) for oi in oi_list) / total_usd
|
|
131
125
|
|
|
132
126
|
# Find dominant exchange
|
|
133
127
|
dominant = max(oi_list, key=lambda x: x.oi_usd)
|
|
@@ -258,13 +252,15 @@ class OIAnalyzer:
|
|
|
258
252
|
shares = []
|
|
259
253
|
for oi in analysis.exchanges:
|
|
260
254
|
share = float(oi.oi_usd) / total * 100
|
|
261
|
-
shares.append(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
255
|
+
shares.append(
|
|
256
|
+
{
|
|
257
|
+
"exchange": oi.exchange,
|
|
258
|
+
"oi_usd": float(oi.oi_usd),
|
|
259
|
+
"share_pct": round(share, 1),
|
|
260
|
+
"change_24h": oi.change_24h_pct,
|
|
261
|
+
"long_ratio": oi.long_ratio,
|
|
262
|
+
}
|
|
263
|
+
)
|
|
268
264
|
|
|
269
265
|
return sorted(shares, key=lambda x: x["oi_usd"], reverse=True)
|
|
270
266
|
|
|
@@ -290,14 +286,14 @@ def demo():
|
|
|
290
286
|
share = float(oi.oi_usd) / float(analysis.total_oi_usd) * 100
|
|
291
287
|
print(
|
|
292
288
|
f"{oi.exchange:<12} "
|
|
293
|
-
f"${float(oi.oi_usd)/1e9:>12.2f}B "
|
|
289
|
+
f"${float(oi.oi_usd) / 1e9:>12.2f}B "
|
|
294
290
|
f"{oi.change_24h_pct:>+9.1f}% "
|
|
295
291
|
f"{oi.change_7d_pct:>+9.1f}% "
|
|
296
292
|
f"{share:>7.1f}%"
|
|
297
293
|
)
|
|
298
294
|
|
|
299
295
|
print("-" * 60)
|
|
300
|
-
print(f"\nTotal OI: ${float(analysis.total_oi_usd)/1e9:.2f}B")
|
|
296
|
+
print(f"\nTotal OI: ${float(analysis.total_oi_usd) / 1e9:.2f}B")
|
|
301
297
|
print(f"24h Change: {analysis.avg_change_24h:+.1f}%")
|
|
302
298
|
print(f"7d Change: {analysis.avg_change_7d:+.1f}%")
|
|
303
299
|
print(f"\nLong/Short Ratio: {analysis.weighted_long_ratio:.2f} ({analysis.long_percentage:.1f}% long)")
|
|
@@ -314,7 +310,7 @@ def demo():
|
|
|
314
310
|
divergence = analyzer.detect_divergence("BTC", price_change)
|
|
315
311
|
|
|
316
312
|
if divergence:
|
|
317
|
-
print(
|
|
313
|
+
print("\n🔍 Divergence Detected!")
|
|
318
314
|
print(f" OI: {divergence.oi_direction} ({divergence.oi_change_pct:+.1f}%)")
|
|
319
315
|
print(f" Price: {divergence.price_direction} ({divergence.price_change_pct:+.1f}%)")
|
|
320
316
|
print(f" Signal: {divergence.signal.upper()}")
|
|
@@ -23,13 +23,13 @@ class OptionsAnalysis:
|
|
|
23
23
|
|
|
24
24
|
symbol: str
|
|
25
25
|
snapshot: OptionsSnapshot
|
|
26
|
-
iv_interpretation: str
|
|
27
|
-
iv_percentile: float
|
|
28
|
-
sentiment_from_pcr: str
|
|
29
|
-
sentiment_from_skew: str
|
|
26
|
+
iv_interpretation: str # "high", "normal", "low"
|
|
27
|
+
iv_percentile: float # Estimated percentile (0-100)
|
|
28
|
+
sentiment_from_pcr: str # "bullish", "bearish", "neutral"
|
|
29
|
+
sentiment_from_skew: str # "bullish", "bearish", "neutral"
|
|
30
30
|
overall_sentiment: str
|
|
31
|
-
max_pain_distance_pct: float
|
|
32
|
-
expiry_pressure: str
|
|
31
|
+
max_pain_distance_pct: float # Distance from current to max pain
|
|
32
|
+
expiry_pressure: str # "high", "medium", "low"
|
|
33
33
|
timestamp: datetime
|
|
34
34
|
|
|
35
35
|
|
|
@@ -40,8 +40,8 @@ class OptionsFlow:
|
|
|
40
40
|
symbol: str
|
|
41
41
|
expiry: str
|
|
42
42
|
strike: Decimal
|
|
43
|
-
option_type: str
|
|
44
|
-
side: str
|
|
43
|
+
option_type: str # "call" or "put"
|
|
44
|
+
side: str # "buy" or "sell"
|
|
45
45
|
size_contracts: int
|
|
46
46
|
premium_usd: Decimal
|
|
47
47
|
iv_at_trade: float
|
|
@@ -65,8 +65,8 @@ class OptionsAnalyzer:
|
|
|
65
65
|
LOW_IV = 40.0
|
|
66
66
|
|
|
67
67
|
# Put/call interpretation
|
|
68
|
-
BEARISH_PCR = 1.2
|
|
69
|
-
BULLISH_PCR = 0.7
|
|
68
|
+
BEARISH_PCR = 1.2 # Above this is bearish
|
|
69
|
+
BULLISH_PCR = 0.7 # Below this is bullish
|
|
70
70
|
|
|
71
71
|
def __init__(
|
|
72
72
|
self,
|
|
@@ -125,10 +125,7 @@ class OptionsAnalyzer:
|
|
|
125
125
|
overall = self._combine_sentiment(pcr_sentiment, skew_sentiment)
|
|
126
126
|
|
|
127
127
|
# Max pain distance
|
|
128
|
-
max_pain_dist = (
|
|
129
|
-
(float(snapshot.max_pain) - float(current_price))
|
|
130
|
-
/ float(current_price) * 100
|
|
131
|
-
)
|
|
128
|
+
max_pain_dist = (float(snapshot.max_pain) - float(current_price)) / float(current_price) * 100
|
|
132
129
|
|
|
133
130
|
# Expiry pressure (days until expiry)
|
|
134
131
|
expiry_pressure = self._assess_expiry_pressure(snapshot.expiry)
|
|
@@ -232,13 +229,15 @@ class OptionsAnalyzer:
|
|
|
232
229
|
try:
|
|
233
230
|
snapshot = self.client.get_options_snapshot(symbol, exp)
|
|
234
231
|
if snapshot:
|
|
235
|
-
levels.append(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
levels.append(
|
|
233
|
+
{
|
|
234
|
+
"expiry": exp,
|
|
235
|
+
"max_pain": float(snapshot.max_pain),
|
|
236
|
+
"call_oi": float(snapshot.total_call_oi),
|
|
237
|
+
"put_oi": float(snapshot.total_put_oi),
|
|
238
|
+
"pcr_oi": snapshot.put_call_ratio_oi,
|
|
239
|
+
}
|
|
240
|
+
)
|
|
242
241
|
except Exception:
|
|
243
242
|
continue
|
|
244
243
|
|
|
@@ -279,18 +278,20 @@ class OptionsAnalyzer:
|
|
|
279
278
|
else:
|
|
280
279
|
interp = "Bearish bet" if random.random() > 0.5 else "Protective put"
|
|
281
280
|
|
|
282
|
-
flows.append(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
281
|
+
flows.append(
|
|
282
|
+
OptionsFlow(
|
|
283
|
+
symbol=symbol,
|
|
284
|
+
expiry="2025-01-31",
|
|
285
|
+
strike=Decimal(str(int(strike))),
|
|
286
|
+
option_type=opt_type,
|
|
287
|
+
side=random.choice(["buy", "sell"]),
|
|
288
|
+
size_contracts=size,
|
|
289
|
+
premium_usd=Decimal(str(int(premium))),
|
|
290
|
+
iv_at_trade=round(55 + random.uniform(-10, 15), 1),
|
|
291
|
+
interpretation=interp,
|
|
292
|
+
timestamp=datetime.now(),
|
|
293
|
+
)
|
|
294
|
+
)
|
|
294
295
|
|
|
295
296
|
return sorted(flows, key=lambda x: x.premium_usd, reverse=True)
|
|
296
297
|
|
|
@@ -313,23 +314,23 @@ def demo():
|
|
|
313
314
|
print(f"\nExpiry: {snap.expiry}")
|
|
314
315
|
print(f"Exchange: {snap.exchange}")
|
|
315
316
|
|
|
316
|
-
print(
|
|
317
|
+
print("\nImplied Volatility:")
|
|
317
318
|
print(f" ATM IV: {snap.atm_iv:.1f}%")
|
|
318
319
|
print(f" Interpretation: {analysis.iv_interpretation.upper()}")
|
|
319
320
|
print(f" IV Rank: {analysis.iv_percentile:.0f}th percentile")
|
|
320
321
|
|
|
321
|
-
print(
|
|
322
|
+
print("\nPut/Call Analysis:")
|
|
322
323
|
print(f" PCR (Volume): {snap.put_call_ratio_volume:.2f}")
|
|
323
324
|
print(f" PCR (OI): {snap.put_call_ratio_oi:.2f}")
|
|
324
325
|
print(f" Sentiment: {analysis.sentiment_from_pcr.upper()}")
|
|
325
326
|
|
|
326
|
-
print(
|
|
327
|
+
print("\nMax Pain:")
|
|
327
328
|
print(f" Price: ${snap.max_pain:,.0f}")
|
|
328
329
|
print(f" Distance: {analysis.max_pain_distance_pct:+.1f}% from current")
|
|
329
330
|
|
|
330
|
-
print(
|
|
331
|
-
print(f" Calls: ${float(snap.total_call_oi)/1e9:.2f}B")
|
|
332
|
-
print(f" Puts: ${float(snap.total_put_oi)/1e9:.2f}B")
|
|
331
|
+
print("\nOpen Interest:")
|
|
332
|
+
print(f" Calls: ${float(snap.total_call_oi) / 1e9:.2f}B")
|
|
333
|
+
print(f" Puts: ${float(snap.total_put_oi) / 1e9:.2f}B")
|
|
333
334
|
|
|
334
335
|
print(f"\nOverall Sentiment: {analysis.overall_sentiment.upper()}")
|
|
335
336
|
print(f"Expiry Pressure: {analysis.expiry_pressure.upper()}")
|
|
@@ -346,8 +347,8 @@ def demo():
|
|
|
346
347
|
print(
|
|
347
348
|
f"{lvl['expiry']:<12} "
|
|
348
349
|
f"${lvl['max_pain']:>10,.0f} "
|
|
349
|
-
f"${lvl['call_oi']/1e9:>10.1f}B "
|
|
350
|
-
f"${lvl['put_oi']/1e9:>10.1f}B "
|
|
350
|
+
f"${lvl['call_oi'] / 1e9:>10.1f}B "
|
|
351
|
+
f"${lvl['put_oi'] / 1e9:>10.1f}B "
|
|
351
352
|
f"{lvl['pcr_oi']:>5.2f}"
|
|
352
353
|
)
|
|
353
354
|
|