@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,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Open Interest analyzer.
|
|
4
|
+
|
|
5
|
+
Analyzes open interest across exchanges with:
|
|
6
|
+
- Multi-exchange aggregation
|
|
7
|
+
- Trend analysis
|
|
8
|
+
- OI vs price divergence detection
|
|
9
|
+
- Long/short ratio tracking
|
|
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, OpenInterest, Exchange
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class OIAnalysis:
|
|
22
|
+
"""Aggregated open interest analysis."""
|
|
23
|
+
|
|
24
|
+
symbol: str
|
|
25
|
+
exchanges: List[OpenInterest]
|
|
26
|
+
total_oi_usd: Decimal
|
|
27
|
+
total_oi_contracts: Decimal
|
|
28
|
+
avg_change_24h: float
|
|
29
|
+
avg_change_7d: float
|
|
30
|
+
weighted_long_ratio: float
|
|
31
|
+
dominant_exchange: str
|
|
32
|
+
dominant_share: float
|
|
33
|
+
trend: str # "increasing", "decreasing", "stable"
|
|
34
|
+
trend_strength: str # "strong", "moderate", "weak"
|
|
35
|
+
timestamp: datetime
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_increasing(self) -> bool:
|
|
39
|
+
"""Check if OI is increasing."""
|
|
40
|
+
return self.avg_change_24h > 2.0
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_decreasing(self) -> bool:
|
|
44
|
+
"""Check if OI is decreasing."""
|
|
45
|
+
return self.avg_change_24h < -2.0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def long_percentage(self) -> float:
|
|
49
|
+
"""Calculate long percentage from ratio."""
|
|
50
|
+
return self.weighted_long_ratio / (1 + self.weighted_long_ratio) * 100
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class OIDivergence:
|
|
55
|
+
"""OI vs Price divergence signal."""
|
|
56
|
+
|
|
57
|
+
symbol: str
|
|
58
|
+
oi_direction: str # "up" or "down"
|
|
59
|
+
price_direction: str # "up" or "down"
|
|
60
|
+
oi_change_pct: float
|
|
61
|
+
price_change_pct: float
|
|
62
|
+
signal: str # "bullish", "bearish", "short_squeeze", "long_liquidation"
|
|
63
|
+
description: str
|
|
64
|
+
confidence: str # "high", "medium", "low"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OIAnalyzer:
|
|
68
|
+
"""
|
|
69
|
+
Analyzes open interest patterns and signals.
|
|
70
|
+
|
|
71
|
+
Features:
|
|
72
|
+
- Multi-exchange aggregation
|
|
73
|
+
- Trend detection
|
|
74
|
+
- Divergence analysis
|
|
75
|
+
- Long/short positioning
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Trend thresholds
|
|
79
|
+
STRONG_CHANGE = 10.0 # >10% is strong
|
|
80
|
+
MODERATE_CHANGE = 5.0 # >5% is moderate
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
client: Optional[ExchangeClient] = None,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Initialize OI analyzer.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
client: Exchange client for data fetching
|
|
91
|
+
"""
|
|
92
|
+
self.client = client or ExchangeClient(use_mock=True)
|
|
93
|
+
|
|
94
|
+
def analyze(
|
|
95
|
+
self,
|
|
96
|
+
symbol: str,
|
|
97
|
+
exchanges: Optional[List[Exchange]] = None,
|
|
98
|
+
) -> OIAnalysis:
|
|
99
|
+
"""
|
|
100
|
+
Analyze open interest for a symbol.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
symbol: Trading symbol (e.g., "BTC")
|
|
104
|
+
exchanges: Exchanges to include
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
OIAnalysis with all metrics
|
|
108
|
+
"""
|
|
109
|
+
# Fetch OI from all exchanges
|
|
110
|
+
oi_list = self.client.get_all_open_interest(symbol, exchanges)
|
|
111
|
+
|
|
112
|
+
if not oi_list:
|
|
113
|
+
raise ValueError(f"No OI data available for {symbol}")
|
|
114
|
+
|
|
115
|
+
# Calculate totals
|
|
116
|
+
total_usd = sum(float(oi.oi_usd) for oi in oi_list)
|
|
117
|
+
total_contracts = sum(float(oi.oi_contracts) for oi in oi_list)
|
|
118
|
+
|
|
119
|
+
# Calculate weighted averages
|
|
120
|
+
avg_24h = sum(
|
|
121
|
+
oi.change_24h_pct * float(oi.oi_usd) for oi in oi_list
|
|
122
|
+
) / total_usd
|
|
123
|
+
avg_7d = sum(
|
|
124
|
+
oi.change_7d_pct * float(oi.oi_usd) for oi in oi_list
|
|
125
|
+
) / total_usd
|
|
126
|
+
|
|
127
|
+
# Weighted long ratio
|
|
128
|
+
weighted_long = sum(
|
|
129
|
+
oi.long_ratio * float(oi.oi_usd) for oi in oi_list
|
|
130
|
+
) / total_usd
|
|
131
|
+
|
|
132
|
+
# Find dominant exchange
|
|
133
|
+
dominant = max(oi_list, key=lambda x: x.oi_usd)
|
|
134
|
+
dominant_share = float(dominant.oi_usd) / total_usd * 100
|
|
135
|
+
|
|
136
|
+
# Determine trend
|
|
137
|
+
trend, strength = self._analyze_trend(avg_24h, avg_7d)
|
|
138
|
+
|
|
139
|
+
return OIAnalysis(
|
|
140
|
+
symbol=symbol,
|
|
141
|
+
exchanges=oi_list,
|
|
142
|
+
total_oi_usd=Decimal(str(int(total_usd))),
|
|
143
|
+
total_oi_contracts=Decimal(str(int(total_contracts))),
|
|
144
|
+
avg_change_24h=round(avg_24h, 2),
|
|
145
|
+
avg_change_7d=round(avg_7d, 2),
|
|
146
|
+
weighted_long_ratio=round(weighted_long, 2),
|
|
147
|
+
dominant_exchange=dominant.exchange,
|
|
148
|
+
dominant_share=round(dominant_share, 1),
|
|
149
|
+
trend=trend,
|
|
150
|
+
trend_strength=strength,
|
|
151
|
+
timestamp=datetime.now(),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _analyze_trend(
|
|
155
|
+
self,
|
|
156
|
+
change_24h: float,
|
|
157
|
+
change_7d: float,
|
|
158
|
+
) -> tuple:
|
|
159
|
+
"""
|
|
160
|
+
Analyze OI trend from changes.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
(trend, strength) tuple
|
|
164
|
+
"""
|
|
165
|
+
# Determine direction from 24h change
|
|
166
|
+
if change_24h > 2.0:
|
|
167
|
+
trend = "increasing"
|
|
168
|
+
elif change_24h < -2.0:
|
|
169
|
+
trend = "decreasing"
|
|
170
|
+
else:
|
|
171
|
+
trend = "stable"
|
|
172
|
+
|
|
173
|
+
# Determine strength from magnitude
|
|
174
|
+
abs_change = abs(change_24h)
|
|
175
|
+
if abs_change >= self.STRONG_CHANGE:
|
|
176
|
+
strength = "strong"
|
|
177
|
+
elif abs_change >= self.MODERATE_CHANGE:
|
|
178
|
+
strength = "moderate"
|
|
179
|
+
else:
|
|
180
|
+
strength = "weak"
|
|
181
|
+
|
|
182
|
+
return trend, strength
|
|
183
|
+
|
|
184
|
+
def detect_divergence(
|
|
185
|
+
self,
|
|
186
|
+
symbol: str,
|
|
187
|
+
price_change_24h: float,
|
|
188
|
+
) -> Optional[OIDivergence]:
|
|
189
|
+
"""
|
|
190
|
+
Detect OI vs price divergence.
|
|
191
|
+
|
|
192
|
+
Classic interpretation:
|
|
193
|
+
- Rising OI + Rising Price = Strong bullish trend
|
|
194
|
+
- Rising OI + Falling Price = Strong bearish trend
|
|
195
|
+
- Falling OI + Rising Price = Short covering (weak rally)
|
|
196
|
+
- Falling OI + Falling Price = Long liquidation (weak selloff)
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
symbol: Trading symbol
|
|
200
|
+
price_change_24h: Price change in last 24h (%)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
OIDivergence if divergence detected
|
|
204
|
+
"""
|
|
205
|
+
analysis = self.analyze(symbol)
|
|
206
|
+
oi_change = analysis.avg_change_24h
|
|
207
|
+
|
|
208
|
+
oi_dir = "up" if oi_change > 0 else "down"
|
|
209
|
+
price_dir = "up" if price_change_24h > 0 else "down"
|
|
210
|
+
|
|
211
|
+
# Determine signal
|
|
212
|
+
if oi_change > 2 and price_change_24h > 2:
|
|
213
|
+
signal = "bullish"
|
|
214
|
+
desc = "Rising OI confirms bullish trend - new longs entering"
|
|
215
|
+
confidence = "high" if oi_change > 5 else "medium"
|
|
216
|
+
elif oi_change > 2 and price_change_24h < -2:
|
|
217
|
+
signal = "bearish"
|
|
218
|
+
desc = "Rising OI during selloff - new shorts entering"
|
|
219
|
+
confidence = "high" if oi_change > 5 else "medium"
|
|
220
|
+
elif oi_change < -2 and price_change_24h > 2:
|
|
221
|
+
signal = "short_squeeze"
|
|
222
|
+
desc = "Falling OI during rally - short covering, may be weak"
|
|
223
|
+
confidence = "medium"
|
|
224
|
+
elif oi_change < -2 and price_change_24h < -2:
|
|
225
|
+
signal = "long_liquidation"
|
|
226
|
+
desc = "Falling OI during selloff - long liquidations, may find support"
|
|
227
|
+
confidence = "medium"
|
|
228
|
+
else:
|
|
229
|
+
return None # No significant divergence
|
|
230
|
+
|
|
231
|
+
return OIDivergence(
|
|
232
|
+
symbol=symbol,
|
|
233
|
+
oi_direction=oi_dir,
|
|
234
|
+
price_direction=price_dir,
|
|
235
|
+
oi_change_pct=oi_change,
|
|
236
|
+
price_change_pct=price_change_24h,
|
|
237
|
+
signal=signal,
|
|
238
|
+
description=desc,
|
|
239
|
+
confidence=confidence,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def get_market_share(
|
|
243
|
+
self,
|
|
244
|
+
symbol: str,
|
|
245
|
+
) -> List[Dict]:
|
|
246
|
+
"""
|
|
247
|
+
Get OI market share by exchange.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
symbol: Trading symbol
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of exchange market shares
|
|
254
|
+
"""
|
|
255
|
+
analysis = self.analyze(symbol)
|
|
256
|
+
total = float(analysis.total_oi_usd)
|
|
257
|
+
|
|
258
|
+
shares = []
|
|
259
|
+
for oi in analysis.exchanges:
|
|
260
|
+
share = float(oi.oi_usd) / total * 100
|
|
261
|
+
shares.append({
|
|
262
|
+
"exchange": oi.exchange,
|
|
263
|
+
"oi_usd": float(oi.oi_usd),
|
|
264
|
+
"share_pct": round(share, 1),
|
|
265
|
+
"change_24h": oi.change_24h_pct,
|
|
266
|
+
"long_ratio": oi.long_ratio,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return sorted(shares, key=lambda x: x["oi_usd"], reverse=True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def demo():
|
|
273
|
+
"""Demonstrate OI analyzer."""
|
|
274
|
+
analyzer = OIAnalyzer()
|
|
275
|
+
|
|
276
|
+
print("=" * 70)
|
|
277
|
+
print("OPEN INTEREST ANALYZER")
|
|
278
|
+
print("=" * 70)
|
|
279
|
+
|
|
280
|
+
# Analyze BTC OI
|
|
281
|
+
analysis = analyzer.analyze("BTC")
|
|
282
|
+
|
|
283
|
+
print(f"\n📈 {analysis.symbol} OPEN INTEREST ANALYSIS")
|
|
284
|
+
print("-" * 60)
|
|
285
|
+
|
|
286
|
+
print(f"\n{'Exchange':<12} {'OI (USD)':>14} {'24h Chg':>10} {'7d Chg':>10} {'Share':>8}")
|
|
287
|
+
print("-" * 60)
|
|
288
|
+
|
|
289
|
+
for oi in sorted(analysis.exchanges, key=lambda x: x.oi_usd, reverse=True):
|
|
290
|
+
share = float(oi.oi_usd) / float(analysis.total_oi_usd) * 100
|
|
291
|
+
print(
|
|
292
|
+
f"{oi.exchange:<12} "
|
|
293
|
+
f"${float(oi.oi_usd)/1e9:>12.2f}B "
|
|
294
|
+
f"{oi.change_24h_pct:>+9.1f}% "
|
|
295
|
+
f"{oi.change_7d_pct:>+9.1f}% "
|
|
296
|
+
f"{share:>7.1f}%"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
print("-" * 60)
|
|
300
|
+
print(f"\nTotal OI: ${float(analysis.total_oi_usd)/1e9:.2f}B")
|
|
301
|
+
print(f"24h Change: {analysis.avg_change_24h:+.1f}%")
|
|
302
|
+
print(f"7d Change: {analysis.avg_change_7d:+.1f}%")
|
|
303
|
+
print(f"\nLong/Short Ratio: {analysis.weighted_long_ratio:.2f} ({analysis.long_percentage:.1f}% long)")
|
|
304
|
+
print(f"Trend: {analysis.trend_strength.title()} {analysis.trend.title()}")
|
|
305
|
+
print(f"Dominant Exchange: {analysis.dominant_exchange} ({analysis.dominant_share:.1f}%)")
|
|
306
|
+
|
|
307
|
+
# Check for divergence
|
|
308
|
+
print("\n" + "=" * 70)
|
|
309
|
+
print("DIVERGENCE ANALYSIS")
|
|
310
|
+
print("=" * 70)
|
|
311
|
+
|
|
312
|
+
# Simulate price change
|
|
313
|
+
price_change = 3.5 # Example: +3.5% price move
|
|
314
|
+
divergence = analyzer.detect_divergence("BTC", price_change)
|
|
315
|
+
|
|
316
|
+
if divergence:
|
|
317
|
+
print(f"\n🔍 Divergence Detected!")
|
|
318
|
+
print(f" OI: {divergence.oi_direction} ({divergence.oi_change_pct:+.1f}%)")
|
|
319
|
+
print(f" Price: {divergence.price_direction} ({divergence.price_change_pct:+.1f}%)")
|
|
320
|
+
print(f" Signal: {divergence.signal.upper()}")
|
|
321
|
+
print(f" {divergence.description}")
|
|
322
|
+
print(f" Confidence: {divergence.confidence}")
|
|
323
|
+
else:
|
|
324
|
+
print("\nNo significant divergence detected")
|
|
325
|
+
|
|
326
|
+
# Market share breakdown
|
|
327
|
+
print("\n" + "=" * 70)
|
|
328
|
+
print("MARKET SHARE BREAKDOWN")
|
|
329
|
+
print("=" * 70)
|
|
330
|
+
|
|
331
|
+
shares = analyzer.get_market_share("BTC")
|
|
332
|
+
for s in shares:
|
|
333
|
+
bar = "█" * int(s["share_pct"] / 2)
|
|
334
|
+
print(f"{s['exchange']:<12} {bar} {s['share_pct']:.1f}%")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
demo()
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Options market analyzer.
|
|
4
|
+
|
|
5
|
+
Analyzes crypto options markets with:
|
|
6
|
+
- Implied volatility tracking
|
|
7
|
+
- Put/call ratio analysis
|
|
8
|
+
- Max pain calculation
|
|
9
|
+
- Options flow detection
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
from datetime import datetime, date
|
|
16
|
+
|
|
17
|
+
from exchange_client import ExchangeClient, OptionsSnapshot
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class OptionsAnalysis:
|
|
22
|
+
"""Comprehensive options analysis."""
|
|
23
|
+
|
|
24
|
+
symbol: str
|
|
25
|
+
snapshot: OptionsSnapshot
|
|
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
|
+
overall_sentiment: str
|
|
31
|
+
max_pain_distance_pct: float # Distance from current to max pain
|
|
32
|
+
expiry_pressure: str # "high", "medium", "low"
|
|
33
|
+
timestamp: datetime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class OptionsFlow:
|
|
38
|
+
"""Significant options trade."""
|
|
39
|
+
|
|
40
|
+
symbol: str
|
|
41
|
+
expiry: str
|
|
42
|
+
strike: Decimal
|
|
43
|
+
option_type: str # "call" or "put"
|
|
44
|
+
side: str # "buy" or "sell"
|
|
45
|
+
size_contracts: int
|
|
46
|
+
premium_usd: Decimal
|
|
47
|
+
iv_at_trade: float
|
|
48
|
+
interpretation: str
|
|
49
|
+
timestamp: datetime
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OptionsAnalyzer:
|
|
53
|
+
"""
|
|
54
|
+
Analyzes crypto options markets.
|
|
55
|
+
|
|
56
|
+
Features:
|
|
57
|
+
- IV analysis and percentile ranking
|
|
58
|
+
- Put/call ratio interpretation
|
|
59
|
+
- Max pain calculation
|
|
60
|
+
- Flow analysis
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# IV interpretation thresholds
|
|
64
|
+
HIGH_IV = 70.0
|
|
65
|
+
LOW_IV = 40.0
|
|
66
|
+
|
|
67
|
+
# Put/call interpretation
|
|
68
|
+
BEARISH_PCR = 1.2 # Above this is bearish
|
|
69
|
+
BULLISH_PCR = 0.7 # Below this is bullish
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
client: Optional[ExchangeClient] = None,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Initialize options analyzer.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
client: Exchange client for data fetching
|
|
80
|
+
"""
|
|
81
|
+
self.client = client or ExchangeClient(use_mock=True)
|
|
82
|
+
|
|
83
|
+
def analyze(
|
|
84
|
+
self,
|
|
85
|
+
symbol: str,
|
|
86
|
+
expiry: Optional[str] = None,
|
|
87
|
+
current_price: Optional[Decimal] = None,
|
|
88
|
+
) -> OptionsAnalysis:
|
|
89
|
+
"""
|
|
90
|
+
Analyze options market for a symbol.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
symbol: Trading symbol (e.g., "BTC")
|
|
94
|
+
expiry: Target expiry date (default: nearest)
|
|
95
|
+
current_price: Current spot price
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
OptionsAnalysis with all metrics
|
|
99
|
+
"""
|
|
100
|
+
# Fetch options snapshot
|
|
101
|
+
snapshot = self.client.get_options_snapshot(symbol, expiry)
|
|
102
|
+
|
|
103
|
+
if snapshot is None:
|
|
104
|
+
raise ValueError(f"No options data available for {symbol}")
|
|
105
|
+
|
|
106
|
+
# Set default price if not provided
|
|
107
|
+
if current_price is None:
|
|
108
|
+
if symbol == "BTC":
|
|
109
|
+
current_price = Decimal("67500")
|
|
110
|
+
elif symbol == "ETH":
|
|
111
|
+
current_price = Decimal("2500")
|
|
112
|
+
else:
|
|
113
|
+
current_price = Decimal("100")
|
|
114
|
+
|
|
115
|
+
# Interpret IV
|
|
116
|
+
iv_interp, iv_pctl = self._interpret_iv(snapshot.atm_iv, symbol)
|
|
117
|
+
|
|
118
|
+
# Interpret put/call ratio
|
|
119
|
+
pcr_sentiment = self._interpret_pcr(snapshot.put_call_ratio_volume)
|
|
120
|
+
|
|
121
|
+
# Interpret skew (simplified - just using PCR OI as proxy)
|
|
122
|
+
skew_sentiment = self._interpret_pcr(snapshot.put_call_ratio_oi)
|
|
123
|
+
|
|
124
|
+
# Overall sentiment (combine signals)
|
|
125
|
+
overall = self._combine_sentiment(pcr_sentiment, skew_sentiment)
|
|
126
|
+
|
|
127
|
+
# Max pain distance
|
|
128
|
+
max_pain_dist = (
|
|
129
|
+
(float(snapshot.max_pain) - float(current_price))
|
|
130
|
+
/ float(current_price) * 100
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Expiry pressure (days until expiry)
|
|
134
|
+
expiry_pressure = self._assess_expiry_pressure(snapshot.expiry)
|
|
135
|
+
|
|
136
|
+
return OptionsAnalysis(
|
|
137
|
+
symbol=symbol,
|
|
138
|
+
snapshot=snapshot,
|
|
139
|
+
iv_interpretation=iv_interp,
|
|
140
|
+
iv_percentile=iv_pctl,
|
|
141
|
+
sentiment_from_pcr=pcr_sentiment,
|
|
142
|
+
sentiment_from_skew=skew_sentiment,
|
|
143
|
+
overall_sentiment=overall,
|
|
144
|
+
max_pain_distance_pct=round(max_pain_dist, 2),
|
|
145
|
+
expiry_pressure=expiry_pressure,
|
|
146
|
+
timestamp=datetime.now(),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _interpret_iv(
|
|
150
|
+
self,
|
|
151
|
+
iv: float,
|
|
152
|
+
symbol: str,
|
|
153
|
+
) -> tuple:
|
|
154
|
+
"""
|
|
155
|
+
Interpret implied volatility.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
(interpretation, estimated_percentile)
|
|
159
|
+
"""
|
|
160
|
+
# Adjust thresholds by asset (crypto generally high IV)
|
|
161
|
+
high = self.HIGH_IV
|
|
162
|
+
low = self.LOW_IV
|
|
163
|
+
|
|
164
|
+
if iv >= high:
|
|
165
|
+
interp = "high"
|
|
166
|
+
pctl = min(100, 50 + (iv - high))
|
|
167
|
+
elif iv <= low:
|
|
168
|
+
interp = "low"
|
|
169
|
+
pctl = max(0, 50 - (low - iv))
|
|
170
|
+
else:
|
|
171
|
+
interp = "normal"
|
|
172
|
+
pctl = 50 + ((iv - 55) / 15 * 30) # Scale around 55
|
|
173
|
+
|
|
174
|
+
return interp, round(pctl, 0)
|
|
175
|
+
|
|
176
|
+
def _interpret_pcr(self, pcr: float) -> str:
|
|
177
|
+
"""Interpret put/call ratio."""
|
|
178
|
+
if pcr >= self.BEARISH_PCR:
|
|
179
|
+
return "bearish"
|
|
180
|
+
elif pcr <= self.BULLISH_PCR:
|
|
181
|
+
return "bullish"
|
|
182
|
+
else:
|
|
183
|
+
return "neutral"
|
|
184
|
+
|
|
185
|
+
def _combine_sentiment(self, pcr_sent: str, skew_sent: str) -> str:
|
|
186
|
+
"""Combine sentiment signals."""
|
|
187
|
+
if pcr_sent == skew_sent:
|
|
188
|
+
return pcr_sent
|
|
189
|
+
elif pcr_sent == "neutral":
|
|
190
|
+
return skew_sent
|
|
191
|
+
elif skew_sent == "neutral":
|
|
192
|
+
return pcr_sent
|
|
193
|
+
else:
|
|
194
|
+
return "mixed"
|
|
195
|
+
|
|
196
|
+
def _assess_expiry_pressure(self, expiry: str) -> str:
|
|
197
|
+
"""Assess expiry pressure based on days remaining."""
|
|
198
|
+
try:
|
|
199
|
+
exp_date = datetime.strptime(expiry, "%Y-%m-%d").date()
|
|
200
|
+
days = (exp_date - date.today()).days
|
|
201
|
+
|
|
202
|
+
if days <= 2:
|
|
203
|
+
return "high"
|
|
204
|
+
elif days <= 7:
|
|
205
|
+
return "medium"
|
|
206
|
+
else:
|
|
207
|
+
return "low"
|
|
208
|
+
except Exception:
|
|
209
|
+
return "unknown"
|
|
210
|
+
|
|
211
|
+
def get_max_pain_levels(
|
|
212
|
+
self,
|
|
213
|
+
symbol: str,
|
|
214
|
+
expiries: Optional[List[str]] = None,
|
|
215
|
+
) -> List[Dict]:
|
|
216
|
+
"""
|
|
217
|
+
Get max pain levels for multiple expiries.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
symbol: Trading symbol
|
|
221
|
+
expiries: List of expiry dates
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of max pain levels by expiry
|
|
225
|
+
"""
|
|
226
|
+
if expiries is None:
|
|
227
|
+
# Default expiries (next few Fridays)
|
|
228
|
+
expiries = ["2025-01-17", "2025-01-24", "2025-01-31"]
|
|
229
|
+
|
|
230
|
+
levels = []
|
|
231
|
+
for exp in expiries:
|
|
232
|
+
try:
|
|
233
|
+
snapshot = self.client.get_options_snapshot(symbol, exp)
|
|
234
|
+
if snapshot:
|
|
235
|
+
levels.append({
|
|
236
|
+
"expiry": exp,
|
|
237
|
+
"max_pain": float(snapshot.max_pain),
|
|
238
|
+
"call_oi": float(snapshot.total_call_oi),
|
|
239
|
+
"put_oi": float(snapshot.total_put_oi),
|
|
240
|
+
"pcr_oi": snapshot.put_call_ratio_oi,
|
|
241
|
+
})
|
|
242
|
+
except Exception:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
return levels
|
|
246
|
+
|
|
247
|
+
def generate_mock_flow(
|
|
248
|
+
self,
|
|
249
|
+
symbol: str,
|
|
250
|
+
count: int = 5,
|
|
251
|
+
) -> List[OptionsFlow]:
|
|
252
|
+
"""
|
|
253
|
+
Generate mock options flow data.
|
|
254
|
+
|
|
255
|
+
In production, this would track actual large trades.
|
|
256
|
+
"""
|
|
257
|
+
import random
|
|
258
|
+
|
|
259
|
+
if symbol == "BTC":
|
|
260
|
+
base_price = 67500
|
|
261
|
+
elif symbol == "ETH":
|
|
262
|
+
base_price = 2500
|
|
263
|
+
else:
|
|
264
|
+
base_price = 100
|
|
265
|
+
|
|
266
|
+
flows = []
|
|
267
|
+
for i in range(count):
|
|
268
|
+
opt_type = random.choice(["call", "put"])
|
|
269
|
+
# Strikes around current price
|
|
270
|
+
strike = base_price * (1 + random.uniform(-0.15, 0.15))
|
|
271
|
+
strike = round(strike / 1000) * 1000 # Round to nearest 1000
|
|
272
|
+
|
|
273
|
+
size = random.randint(100, 2000)
|
|
274
|
+
premium = size * random.uniform(500, 5000)
|
|
275
|
+
|
|
276
|
+
# Interpretation based on type and likely direction
|
|
277
|
+
if opt_type == "call":
|
|
278
|
+
interp = "Bullish positioning" if random.random() > 0.3 else "Covered call selling"
|
|
279
|
+
else:
|
|
280
|
+
interp = "Bearish bet" if random.random() > 0.5 else "Protective put"
|
|
281
|
+
|
|
282
|
+
flows.append(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
|
+
|
|
295
|
+
return sorted(flows, key=lambda x: x.premium_usd, reverse=True)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def demo():
|
|
299
|
+
"""Demonstrate options analyzer."""
|
|
300
|
+
analyzer = OptionsAnalyzer()
|
|
301
|
+
|
|
302
|
+
print("=" * 70)
|
|
303
|
+
print("OPTIONS MARKET ANALYZER")
|
|
304
|
+
print("=" * 70)
|
|
305
|
+
|
|
306
|
+
# Analyze BTC options
|
|
307
|
+
analysis = analyzer.analyze("BTC", current_price=Decimal("67500"))
|
|
308
|
+
|
|
309
|
+
print(f"\n📊 {analysis.symbol} OPTIONS ANALYSIS")
|
|
310
|
+
print("-" * 50)
|
|
311
|
+
|
|
312
|
+
snap = analysis.snapshot
|
|
313
|
+
print(f"\nExpiry: {snap.expiry}")
|
|
314
|
+
print(f"Exchange: {snap.exchange}")
|
|
315
|
+
|
|
316
|
+
print(f"\nImplied Volatility:")
|
|
317
|
+
print(f" ATM IV: {snap.atm_iv:.1f}%")
|
|
318
|
+
print(f" Interpretation: {analysis.iv_interpretation.upper()}")
|
|
319
|
+
print(f" IV Rank: {analysis.iv_percentile:.0f}th percentile")
|
|
320
|
+
|
|
321
|
+
print(f"\nPut/Call Analysis:")
|
|
322
|
+
print(f" PCR (Volume): {snap.put_call_ratio_volume:.2f}")
|
|
323
|
+
print(f" PCR (OI): {snap.put_call_ratio_oi:.2f}")
|
|
324
|
+
print(f" Sentiment: {analysis.sentiment_from_pcr.upper()}")
|
|
325
|
+
|
|
326
|
+
print(f"\nMax Pain:")
|
|
327
|
+
print(f" Price: ${snap.max_pain:,.0f}")
|
|
328
|
+
print(f" Distance: {analysis.max_pain_distance_pct:+.1f}% from current")
|
|
329
|
+
|
|
330
|
+
print(f"\nOpen Interest:")
|
|
331
|
+
print(f" Calls: ${float(snap.total_call_oi)/1e9:.2f}B")
|
|
332
|
+
print(f" Puts: ${float(snap.total_put_oi)/1e9:.2f}B")
|
|
333
|
+
|
|
334
|
+
print(f"\nOverall Sentiment: {analysis.overall_sentiment.upper()}")
|
|
335
|
+
print(f"Expiry Pressure: {analysis.expiry_pressure.upper()}")
|
|
336
|
+
|
|
337
|
+
# Max pain levels
|
|
338
|
+
print("\n" + "-" * 50)
|
|
339
|
+
print("MAX PAIN BY EXPIRY")
|
|
340
|
+
print("-" * 50)
|
|
341
|
+
|
|
342
|
+
levels = analyzer.get_max_pain_levels("BTC")
|
|
343
|
+
print(f"\n{'Expiry':<12} {'Max Pain':>12} {'Call OI':>12} {'Put OI':>12} {'PCR':>6}")
|
|
344
|
+
print("-" * 50)
|
|
345
|
+
for lvl in levels:
|
|
346
|
+
print(
|
|
347
|
+
f"{lvl['expiry']:<12} "
|
|
348
|
+
f"${lvl['max_pain']:>10,.0f} "
|
|
349
|
+
f"${lvl['call_oi']/1e9:>10.1f}B "
|
|
350
|
+
f"${lvl['put_oi']/1e9:>10.1f}B "
|
|
351
|
+
f"{lvl['pcr_oi']:>5.2f}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Options flow
|
|
355
|
+
print("\n" + "-" * 50)
|
|
356
|
+
print("SIGNIFICANT OPTIONS FLOW (Simulated)")
|
|
357
|
+
print("-" * 50)
|
|
358
|
+
|
|
359
|
+
flows = analyzer.generate_mock_flow("BTC", count=5)
|
|
360
|
+
print(f"\n{'Type':<6} {'Strike':>10} {'Size':>8} {'Premium':>12} {'Interpretation':<25}")
|
|
361
|
+
print("-" * 70)
|
|
362
|
+
for flow in flows:
|
|
363
|
+
print(
|
|
364
|
+
f"{flow.option_type.upper():<6} "
|
|
365
|
+
f"${float(flow.strike):>8,.0f} "
|
|
366
|
+
f"{flow.size_contracts:>8} "
|
|
367
|
+
f"${float(flow.premium_usd):>10,.0f} "
|
|
368
|
+
f"{flow.interpretation:<25}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == "__main__":
|
|
373
|
+
demo()
|