@intentsolutionsio/crypto-derivatives-tracker 1.0.0 → 1.0.6

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.
@@ -30,11 +30,11 @@ def format_currency(value: Union[float, Decimal], decimals: int = 0) -> str:
30
30
  """Format value as currency."""
31
31
  val = float(value)
32
32
  if abs(val) >= 1e9:
33
- return f"${val/1e9:.1f}B"
33
+ return f"${val / 1e9:.1f}B"
34
34
  elif abs(val) >= 1e6:
35
- return f"${val/1e6:.1f}M"
35
+ return f"${val / 1e6:.1f}M"
36
36
  elif abs(val) >= 1e3:
37
- return f"${val/1e3:.1f}K"
37
+ return f"${val / 1e3:.1f}K"
38
38
  else:
39
39
  return f"${val:,.{decimals}f}"
40
40
 
@@ -216,17 +216,17 @@ class ConsoleFormatter:
216
216
  """Format options analysis summary."""
217
217
  lines = []
218
218
 
219
- lines.append(f"Implied Volatility:")
219
+ lines.append("Implied Volatility:")
220
220
  lines.append(f" ATM IV: {analysis.get('atm_iv', 0):.1f}%")
221
221
  lines.append(f" Interpretation: {analysis.get('iv_interpretation', 'unknown').upper()}")
222
222
  lines.append(f" IV Rank: {analysis.get('iv_percentile', 50):.0f}th percentile")
223
223
 
224
- lines.append(f"\nPut/Call Analysis:")
224
+ lines.append("\nPut/Call Analysis:")
225
225
  lines.append(f" PCR (Volume): {analysis.get('pcr_volume', 0):.2f}")
226
226
  lines.append(f" PCR (OI): {analysis.get('pcr_oi', 0):.2f}")
227
227
  lines.append(f" Sentiment: {analysis.get('pcr_sentiment', 'neutral').upper()}")
228
228
 
229
- lines.append(f"\nMax Pain:")
229
+ lines.append("\nMax Pain:")
230
230
  lines.append(f" Price: ${analysis.get('max_pain', 0):,.0f}")
231
231
  lines.append(f" Distance: {analysis.get('max_pain_distance', 0):+.1f}% from current")
232
232
 
@@ -243,10 +243,7 @@ class ConsoleFormatter:
243
243
  annual = point.get("annualized_pct", 0)
244
244
  bar = "+" * min(int(abs(annual) / 2), 20)
245
245
  direction = "▲" if annual > 0 else "▼"
246
- lines.append(
247
- f"{point.get('expiry', 'N/A'):<12} {direction} {bar} "
248
- f"{annual:+.1f}%"
249
- )
246
+ lines.append(f"{point.get('expiry', 'N/A'):<12} {direction} {bar} {annual:+.1f}%")
250
247
 
251
248
  return "\n".join(lines)
252
249
 
@@ -377,32 +374,30 @@ class ReportGenerator:
377
374
  Formatted summary string
378
375
  """
379
376
  if format == "json":
380
- return self.json_fmt.derivatives_dashboard(
381
- symbol, funding, oi, liquidations
382
- )
377
+ return self.json_fmt.derivatives_dashboard(symbol, funding, oi, liquidations)
383
378
 
384
379
  # Console format
385
380
  lines = []
386
381
  lines.append(self.console.header(f"{symbol} DERIVATIVES SUMMARY"))
387
382
 
388
383
  # Funding section
389
- lines.append(f"\n📊 FUNDING RATES")
384
+ lines.append("\n📊 FUNDING RATES")
390
385
  lines.append(f" Weighted Average: {format_percent(funding.get('weighted_avg', 0) * 100, 4)}")
391
386
  lines.append(f" Annualized: {format_percent(funding.get('annualized_avg', 0), 1)}")
392
387
  lines.append(f" Sentiment: {funding.get('sentiment', 'unknown').upper()}")
393
388
 
394
389
  # OI section
395
- lines.append(f"\n📈 OPEN INTEREST")
390
+ lines.append("\n📈 OPEN INTEREST")
396
391
  lines.append(f" Total: {format_currency(oi.get('total_oi_usd', 0))}")
397
392
  lines.append(f" 24h Change: {format_percent(oi.get('avg_change_24h', 0), 1)}")
398
393
  lines.append(f" Trend: {oi.get('trend', 'unknown').title()}")
399
394
 
400
395
  # Liquidations section
401
- lines.append(f"\n💥 LIQUIDATIONS")
396
+ lines.append("\n💥 LIQUIDATIONS")
402
397
  lines.append(f" 24h Total: {format_currency(liquidations.get('total_24h_usd', 0))}")
403
398
  lines.append(f" Longs: {format_currency(liquidations.get('long_liquidations_usd', 0))}")
404
399
  lines.append(f" Shorts: {format_currency(liquidations.get('short_liquidations_usd', 0))}")
405
- risk = liquidations.get('cascade_risk', 'low')
400
+ risk = liquidations.get("cascade_risk", "low")
406
401
  lines.append(f" Cascade Risk: {self.console.risk_icon(risk)} {risk.upper()}")
407
402
 
408
403
  lines.append(f"\n{self.console.H_LINE * self.console.width}")
@@ -450,7 +445,7 @@ def demo():
450
445
  "metrics": {
451
446
  "funding": 0.01,
452
447
  "oi": Decimal("15000000000"),
453
- }
448
+ },
454
449
  }
455
450
  print(json_fmt.format(sample_data))
456
451
 
@@ -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 # Max - min rate
31
- sentiment: str # "bullish", "bearish", "neutral"
32
- sentiment_strength: str # "strong", "moderate", "weak"
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 # Below this is neutral
61
- MODERATE_THRESHOLD = 0.03 # Below this is moderate
62
- EXTREME_THRESHOLD = 0.08 # Above this is extreme
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 # 0.02% minimum for arbitrage
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
- "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
- })
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
- "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
- })
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(f"\n⚠️ EXTREME FUNDING - Contrarian opportunity")
278
+ print("\n⚠️ EXTREME FUNDING - Contrarian opportunity")
279
279
 
280
280
  if analysis.arbitrage_opportunity:
281
- print(f"\n💰 ARBITRAGE OPPORTUNITY")
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 # "low", "medium", "high", "critical"
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 # $500M
54
- HIGH_THRESHOLD = 200_000_000 # $200M
55
- MEDIUM_THRESHOLD = 100_000_000 # $100M
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
- "price": float(level.price),
212
- "value_usd": float(level.total_value_usd),
213
- "distance_pct": round(distance_pct, 1),
214
- "density": level.density,
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
- "price": float(level.price),
222
- "value_usd": float(level.total_value_usd),
223
- "distance_pct": round(distance_pct, 1),
224
- "density": level.density,
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(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")
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 # "increasing", "decreasing", "stable"
34
- trend_strength: str # "strong", "moderate", "weak"
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 # "up" or "down"
59
- price_direction: str # "up" or "down"
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 # "bullish", "bearish", "short_squeeze", "long_liquidation"
62
+ signal: str # "bullish", "bearish", "short_squeeze", "long_liquidation"
63
63
  description: str
64
- confidence: str # "high", "medium", "low"
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 # >10% is strong
80
- MODERATE_CHANGE = 5.0 # >5% is moderate
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
- 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
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
- "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
- })
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(f"\n🔍 Divergence Detected!")
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 # "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"
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 # Distance from current to max pain
32
- expiry_pressure: str # "high", "medium", "low"
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 # "call" or "put"
44
- side: str # "buy" or "sell"
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 # Above this is bearish
69
- BULLISH_PCR = 0.7 # Below this is bullish
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
- "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
- })
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(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
- ))
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(f"\nImplied Volatility:")
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(f"\nPut/Call Analysis:")
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(f"\nMax Pain:")
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(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")
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
 
@@ -1,32 +0,0 @@
1
- #!/bin/bash
2
- # Skill validation helper
3
- # Validates skill activation and functionality
4
-
5
- set -e
6
-
7
- echo "🔍 Validating skill..."
8
-
9
- # Check if SKILL.md exists
10
- if [ ! -f "../SKILL.md" ]; then
11
- echo "❌ Error: SKILL.md not found"
12
- exit 1
13
- fi
14
-
15
- # Validate frontmatter
16
- if ! grep -q "^---$" "../SKILL.md"; then
17
- echo "❌ Error: No frontmatter found"
18
- exit 1
19
- fi
20
-
21
- # Check required fields
22
- if ! grep -q "^name:" "../SKILL.md"; then
23
- echo "❌ Error: Missing 'name' field"
24
- exit 1
25
- fi
26
-
27
- if ! grep -q "^description:" "../SKILL.md"; then
28
- echo "❌ Error: Missing 'description' field"
29
- exit 1
30
- fi
31
-
32
- echo "✅ Skill validation passed"