@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.
@@ -22,9 +22,7 @@ Usage:
22
22
  import argparse
23
23
  import sys
24
24
  from decimal import Decimal
25
- from typing import List, Optional
26
25
 
27
- from exchange_client import ExchangeClient
28
26
  from funding_tracker import FundingTracker
29
27
  from oi_analyzer import OIAnalyzer
30
28
  from liquidation_monitor import LiquidationMonitor
@@ -44,23 +42,28 @@ def cmd_funding(args):
44
42
  analysis = tracker.analyze(args.symbol)
45
43
 
46
44
  if args.format == "json":
47
- print(json_fmt.funding_report(args.symbol, {
48
- "weighted_avg": analysis.weighted_avg,
49
- "annualized_avg": analysis.annualized_avg,
50
- "spread": analysis.spread,
51
- "sentiment": analysis.sentiment,
52
- "sentiment_strength": analysis.sentiment_strength,
53
- "arbitrage_opportunity": analysis.arbitrage_opportunity,
54
- "rates": [
45
+ print(
46
+ json_fmt.funding_report(
47
+ args.symbol,
55
48
  {
56
- "exchange": r.exchange,
57
- "rate": float(r.rate),
58
- "annualized": r.annualized,
59
- "next_payment": r.time_to_payment_str,
60
- }
61
- for r in analysis.rates
62
- ],
63
- }))
49
+ "weighted_avg": analysis.weighted_avg,
50
+ "annualized_avg": analysis.annualized_avg,
51
+ "spread": analysis.spread,
52
+ "sentiment": analysis.sentiment,
53
+ "sentiment_strength": analysis.sentiment_strength,
54
+ "arbitrage_opportunity": analysis.arbitrage_opportunity,
55
+ "rates": [
56
+ {
57
+ "exchange": r.exchange,
58
+ "rate": float(r.rate),
59
+ "annualized": r.annualized,
60
+ "next_payment": r.time_to_payment_str,
61
+ }
62
+ for r in analysis.rates
63
+ ],
64
+ },
65
+ )
66
+ )
64
67
  return
65
68
 
66
69
  # Console format
@@ -69,24 +72,23 @@ def cmd_funding(args):
69
72
 
70
73
  for rate in sorted(analysis.rates, key=lambda r: r.rate, reverse=True):
71
74
  print(
72
- f"{rate.exchange:<12} "
73
- f"{float(rate.rate):>+9.4%} "
74
- f"{rate.annualized:>+11.2f}% "
75
- f"{rate.time_to_payment_str:>14}"
75
+ f"{rate.exchange:<12} {float(rate.rate):>+9.4%} {rate.annualized:>+11.2f}% {rate.time_to_payment_str:>14}"
76
76
  )
77
77
 
78
78
  print("-" * 50)
79
79
  print(f"\nWeighted Average: {analysis.weighted_avg:+.4%}")
80
80
  print(f"Annualized: {analysis.annualized_avg:+.2f}%")
81
81
  print(f"Spread (max-min): {analysis.spread:.4%}")
82
- print(f"\nSentiment: {console.sentiment_icon(analysis.sentiment)} "
83
- f"{analysis.sentiment_strength.title()} {analysis.sentiment.title()}")
82
+ print(
83
+ f"\nSentiment: {console.sentiment_icon(analysis.sentiment)} "
84
+ f"{analysis.sentiment_strength.title()} {analysis.sentiment.title()}"
85
+ )
84
86
 
85
87
  if analysis.is_extreme:
86
- print(f"\n⚠️ EXTREME FUNDING - Contrarian opportunity")
88
+ print("\n⚠️ EXTREME FUNDING - Contrarian opportunity")
87
89
 
88
90
  if analysis.arbitrage_opportunity:
89
- print(f"\n💰 ARBITRAGE OPPORTUNITY")
91
+ print("\n💰 ARBITRAGE OPPORTUNITY")
90
92
  print(f" Long on {analysis.min_rate.exchange} ({float(analysis.min_rate.rate):+.4%})")
91
93
  print(f" Short on {analysis.max_rate.exchange} ({float(analysis.max_rate.rate):+.4%})")
92
94
  print(f" Profit: {analysis.arbitrage_spread:.4%} per 8h")
@@ -103,28 +105,33 @@ def cmd_oi(args):
103
105
  analysis = analyzer.analyze(args.symbol)
104
106
 
105
107
  if args.format == "json":
106
- print(json_fmt.oi_report(args.symbol, {
107
- "total_oi_usd": float(analysis.total_oi_usd),
108
- "total_oi_contracts": float(analysis.total_oi_contracts),
109
- "avg_change_24h": analysis.avg_change_24h,
110
- "avg_change_7d": analysis.avg_change_7d,
111
- "weighted_long_ratio": analysis.weighted_long_ratio,
112
- "long_percentage": analysis.long_percentage,
113
- "dominant_exchange": analysis.dominant_exchange,
114
- "dominant_share": analysis.dominant_share,
115
- "trend": analysis.trend,
116
- "trend_strength": analysis.trend_strength,
117
- "exchanges": [
108
+ print(
109
+ json_fmt.oi_report(
110
+ args.symbol,
118
111
  {
119
- "exchange": oi.exchange,
120
- "oi_usd": float(oi.oi_usd),
121
- "change_24h": oi.change_24h_pct,
122
- "change_7d": oi.change_7d_pct,
123
- "long_ratio": oi.long_ratio,
124
- }
125
- for oi in analysis.exchanges
126
- ],
127
- }))
112
+ "total_oi_usd": float(analysis.total_oi_usd),
113
+ "total_oi_contracts": float(analysis.total_oi_contracts),
114
+ "avg_change_24h": analysis.avg_change_24h,
115
+ "avg_change_7d": analysis.avg_change_7d,
116
+ "weighted_long_ratio": analysis.weighted_long_ratio,
117
+ "long_percentage": analysis.long_percentage,
118
+ "dominant_exchange": analysis.dominant_exchange,
119
+ "dominant_share": analysis.dominant_share,
120
+ "trend": analysis.trend,
121
+ "trend_strength": analysis.trend_strength,
122
+ "exchanges": [
123
+ {
124
+ "exchange": oi.exchange,
125
+ "oi_usd": float(oi.oi_usd),
126
+ "change_24h": oi.change_24h_pct,
127
+ "change_7d": oi.change_7d_pct,
128
+ "long_ratio": oi.long_ratio,
129
+ }
130
+ for oi in analysis.exchanges
131
+ ],
132
+ },
133
+ )
134
+ )
128
135
  return
129
136
 
130
137
  # Console format
@@ -135,18 +142,17 @@ def cmd_oi(args):
135
142
  share = float(oi.oi_usd) / float(analysis.total_oi_usd) * 100
136
143
  print(
137
144
  f"{oi.exchange:<12} "
138
- f"${float(oi.oi_usd)/1e9:>12.2f}B "
145
+ f"${float(oi.oi_usd) / 1e9:>12.2f}B "
139
146
  f"{oi.change_24h_pct:>+9.1f}% "
140
147
  f"{oi.change_7d_pct:>+9.1f}% "
141
148
  f"{share:>7.1f}%"
142
149
  )
143
150
 
144
151
  print("-" * 60)
145
- print(f"\nTotal OI: ${float(analysis.total_oi_usd)/1e9:.2f}B")
152
+ print(f"\nTotal OI: ${float(analysis.total_oi_usd) / 1e9:.2f}B")
146
153
  print(f"24h Change: {analysis.avg_change_24h:+.1f}%")
147
154
  print(f"7d Change: {analysis.avg_change_7d:+.1f}%")
148
- print(f"\nLong/Short Ratio: {analysis.weighted_long_ratio:.2f} "
149
- f"({analysis.long_percentage:.1f}% long)")
155
+ print(f"\nLong/Short Ratio: {analysis.weighted_long_ratio:.2f} ({analysis.long_percentage:.1f}% long)")
150
156
  print(f"Trend: {analysis.trend_strength.title()} {analysis.trend.title()}")
151
157
  print(f"Dominant Exchange: {analysis.dominant_exchange} ({analysis.dominant_share:.1f}%)")
152
158
 
@@ -155,7 +161,7 @@ def cmd_oi(args):
155
161
  print(console.section("DIVERGENCE ANALYSIS"))
156
162
  divergence = analyzer.detect_divergence(args.symbol, args.price_change)
157
163
  if divergence:
158
- print(f"\n🔍 Divergence Detected!")
164
+ print("\n🔍 Divergence Detected!")
159
165
  print(f" OI: {divergence.oi_direction} ({divergence.oi_change_pct:+.1f}%)")
160
166
  print(f" Price: {divergence.price_direction} ({divergence.price_change_pct:+.1f}%)")
161
167
  print(f" Signal: {divergence.signal.upper()}")
@@ -177,44 +183,47 @@ def cmd_liquidations(args):
177
183
  print(console.header(f"{args.symbol} LIQUIDATION MONITOR"))
178
184
 
179
185
  if args.format == "json":
180
- print(json_fmt.format({
181
- "symbol": summary.symbol,
182
- "current_price": float(summary.current_price),
183
- "total_24h_usd": float(summary.total_24h_usd),
184
- "long_liquidations_usd": float(summary.long_liquidations_usd),
185
- "short_liquidations_usd": float(summary.short_liquidations_usd),
186
- "cascade_risk": summary.cascade_risk,
187
- "long_levels": [
188
- {
189
- "price": float(l.price),
190
- "total_value_usd": float(l.total_value_usd),
191
- "density": l.density,
192
- }
193
- for l in summary.long_levels
194
- ],
195
- "short_levels": [
186
+ print(
187
+ json_fmt.format(
196
188
  {
197
- "price": float(l.price),
198
- "total_value_usd": float(l.total_value_usd),
199
- "density": l.density,
189
+ "symbol": summary.symbol,
190
+ "current_price": float(summary.current_price),
191
+ "total_24h_usd": float(summary.total_24h_usd),
192
+ "long_liquidations_usd": float(summary.long_liquidations_usd),
193
+ "short_liquidations_usd": float(summary.short_liquidations_usd),
194
+ "cascade_risk": summary.cascade_risk,
195
+ "long_levels": [
196
+ {
197
+ "price": float(l.price),
198
+ "total_value_usd": float(l.total_value_usd),
199
+ "density": l.density,
200
+ }
201
+ for l in summary.long_levels
202
+ ],
203
+ "short_levels": [
204
+ {
205
+ "price": float(l.price),
206
+ "total_value_usd": float(l.total_value_usd),
207
+ "density": l.density,
208
+ }
209
+ for l in summary.short_levels
210
+ ],
200
211
  }
201
- for l in summary.short_levels
202
- ],
203
- }))
212
+ )
213
+ )
204
214
  return
205
215
 
206
216
  print(f"\n Current Price: ${summary.current_price:,}")
207
217
  print("-" * 60)
208
218
 
209
219
  # 24h totals
210
- print(f"\n24h Liquidations:")
211
- print(f" Total: ${float(summary.total_24h_usd)/1e6:,.1f}M")
212
- print(f" Longs: ${float(summary.long_liquidations_usd)/1e6:,.1f}M")
213
- print(f" Shorts: ${float(summary.short_liquidations_usd)/1e6:,.1f}M")
220
+ print("\n24h Liquidations:")
221
+ print(f" Total: ${float(summary.total_24h_usd) / 1e6:,.1f}M")
222
+ print(f" Longs: ${float(summary.long_liquidations_usd) / 1e6:,.1f}M")
223
+ print(f" Shorts: ${float(summary.short_liquidations_usd) / 1e6:,.1f}M")
214
224
 
215
225
  # Cascade risk
216
- print(f"\nCascade Risk: {console.risk_icon(summary.cascade_risk)} "
217
- f"{summary.cascade_risk.upper()}")
226
+ print(f"\nCascade Risk: {console.risk_icon(summary.cascade_risk)} {summary.cascade_risk.upper()}")
218
227
 
219
228
  # Heatmap
220
229
  print(console.section("LIQUIDATION HEATMAP"))
@@ -226,7 +235,7 @@ def cmd_liquidations(args):
226
235
  density_mark = "⚠️ " if level.density in ["high", "critical"] else ""
227
236
  print(
228
237
  f" ${float(level.price):>10,.0f} {bar} "
229
- f"${float(level.total_value_usd)/1e6:.0f}M "
238
+ f"${float(level.total_value_usd) / 1e6:.0f}M "
230
239
  f"{density_mark}{level.density.upper()}"
231
240
  )
232
241
 
@@ -237,16 +246,14 @@ def cmd_liquidations(args):
237
246
  density_mark = "⚠️ " if level.density in ["high", "critical"] else ""
238
247
  print(
239
248
  f" ${float(level.price):>10,.0f} {bar} "
240
- f"${float(level.total_value_usd)/1e6:.0f}M "
249
+ f"${float(level.total_value_usd) / 1e6:.0f}M "
241
250
  f"{density_mark}{level.density.upper()}"
242
251
  )
243
252
 
244
253
  # Large liquidations
245
254
  if args.large:
246
255
  print(console.section("RECENT LARGE LIQUIDATIONS"))
247
- large = monitor.get_recent_large_liquidations(
248
- args.symbol, min_value_usd=1_000_000, limit=5
249
- )
256
+ large = monitor.get_recent_large_liquidations(args.symbol, min_value_usd=1_000_000, limit=5)
250
257
  if large:
251
258
  print(f"\n{'Exchange':<10} {'Side':<6} {'Price':>12} {'Value':>12} {'When':>10}")
252
259
  print("-" * 60)
@@ -255,7 +262,7 @@ def cmd_liquidations(args):
255
262
  f"{l['exchange']:<10} "
256
263
  f"{l['side']:<6} "
257
264
  f"${l['price']:>10,.0f} "
258
- f"${l['value_usd']/1e6:>10.1f}M "
265
+ f"${l['value_usd'] / 1e6:>10.1f}M "
259
266
  f"{l['time_ago']:>10}"
260
267
  )
261
268
 
@@ -272,50 +279,55 @@ def cmd_options(args):
272
279
  print(console.header(f"{args.symbol} OPTIONS ANALYSIS"))
273
280
 
274
281
  if args.format == "json":
275
- print(json_fmt.format({
276
- "symbol": analysis.symbol,
277
- "expiry": analysis.snapshot.expiry,
278
- "atm_iv": analysis.snapshot.atm_iv,
279
- "iv_interpretation": analysis.iv_interpretation,
280
- "iv_percentile": analysis.iv_percentile,
281
- "put_call_ratio_volume": analysis.snapshot.put_call_ratio_volume,
282
- "put_call_ratio_oi": analysis.snapshot.put_call_ratio_oi,
283
- "sentiment_from_pcr": analysis.sentiment_from_pcr,
284
- "sentiment_from_skew": analysis.sentiment_from_skew,
285
- "overall_sentiment": analysis.overall_sentiment,
286
- "max_pain": float(analysis.snapshot.max_pain),
287
- "max_pain_distance_pct": analysis.max_pain_distance_pct,
288
- "expiry_pressure": analysis.expiry_pressure,
289
- "total_call_oi": float(analysis.snapshot.total_call_oi),
290
- "total_put_oi": float(analysis.snapshot.total_put_oi),
291
- }))
282
+ print(
283
+ json_fmt.format(
284
+ {
285
+ "symbol": analysis.symbol,
286
+ "expiry": analysis.snapshot.expiry,
287
+ "atm_iv": analysis.snapshot.atm_iv,
288
+ "iv_interpretation": analysis.iv_interpretation,
289
+ "iv_percentile": analysis.iv_percentile,
290
+ "put_call_ratio_volume": analysis.snapshot.put_call_ratio_volume,
291
+ "put_call_ratio_oi": analysis.snapshot.put_call_ratio_oi,
292
+ "sentiment_from_pcr": analysis.sentiment_from_pcr,
293
+ "sentiment_from_skew": analysis.sentiment_from_skew,
294
+ "overall_sentiment": analysis.overall_sentiment,
295
+ "max_pain": float(analysis.snapshot.max_pain),
296
+ "max_pain_distance_pct": analysis.max_pain_distance_pct,
297
+ "expiry_pressure": analysis.expiry_pressure,
298
+ "total_call_oi": float(analysis.snapshot.total_call_oi),
299
+ "total_put_oi": float(analysis.snapshot.total_put_oi),
300
+ }
301
+ )
302
+ )
292
303
  return
293
304
 
294
305
  snap = analysis.snapshot
295
306
  print(f"\nExpiry: {snap.expiry}")
296
307
  print(f"Exchange: {snap.exchange}")
297
308
 
298
- print(f"\nImplied Volatility:")
309
+ print("\nImplied Volatility:")
299
310
  print(f" ATM IV: {snap.atm_iv:.1f}%")
300
311
  print(f" Interpretation: {analysis.iv_interpretation.upper()}")
301
312
  print(f" IV Rank: {analysis.iv_percentile:.0f}th percentile")
302
313
 
303
- print(f"\nPut/Call Analysis:")
314
+ print("\nPut/Call Analysis:")
304
315
  print(f" PCR (Volume): {snap.put_call_ratio_volume:.2f}")
305
316
  print(f" PCR (OI): {snap.put_call_ratio_oi:.2f}")
306
- print(f" Sentiment: {console.sentiment_icon(analysis.sentiment_from_pcr)} "
307
- f"{analysis.sentiment_from_pcr.upper()}")
317
+ print(f" Sentiment: {console.sentiment_icon(analysis.sentiment_from_pcr)} {analysis.sentiment_from_pcr.upper()}")
308
318
 
309
- print(f"\nMax Pain:")
319
+ print("\nMax Pain:")
310
320
  print(f" Price: ${snap.max_pain:,.0f}")
311
321
  print(f" Distance: {analysis.max_pain_distance_pct:+.1f}% from current")
312
322
 
313
- print(f"\nOpen Interest:")
314
- print(f" Calls: ${float(snap.total_call_oi)/1e9:.2f}B")
315
- print(f" Puts: ${float(snap.total_put_oi)/1e9:.2f}B")
323
+ print("\nOpen Interest:")
324
+ print(f" Calls: ${float(snap.total_call_oi) / 1e9:.2f}B")
325
+ print(f" Puts: ${float(snap.total_put_oi) / 1e9:.2f}B")
316
326
 
317
- print(f"\nOverall Sentiment: {console.sentiment_icon(analysis.overall_sentiment)} "
318
- f"{analysis.overall_sentiment.upper()}")
327
+ print(
328
+ f"\nOverall Sentiment: {console.sentiment_icon(analysis.overall_sentiment)} "
329
+ f"{analysis.overall_sentiment.upper()}"
330
+ )
319
331
  print(f"Expiry Pressure: {analysis.expiry_pressure.upper()}")
320
332
 
321
333
  # Max pain levels
@@ -328,8 +340,8 @@ def cmd_options(args):
328
340
  print(
329
341
  f"{lvl['expiry']:<12} "
330
342
  f"${lvl['max_pain']:>10,.0f} "
331
- f"${lvl['call_oi']/1e9:>10.1f}B "
332
- f"${lvl['put_oi']/1e9:>10.1f}B "
343
+ f"${lvl['call_oi'] / 1e9:>10.1f}B "
344
+ f"${lvl['put_oi'] / 1e9:>10.1f}B "
333
345
  f"{lvl['pcr_oi']:>5.2f}"
334
346
  )
335
347
 
@@ -361,26 +373,30 @@ def cmd_basis(args):
361
373
  print(console.header(f"{args.symbol} BASIS ANALYSIS"))
362
374
 
363
375
  if args.format == "json":
364
- print(json_fmt.format({
365
- "symbol": analysis.symbol,
366
- "spot_price": float(analysis.spot_price),
367
- "avg_basis_pct": analysis.avg_basis_pct,
368
- "avg_annualized": analysis.avg_annualized,
369
- "market_structure": analysis.market_structure,
370
- "structure_strength": analysis.structure_strength,
371
- "best_carry_expiry": analysis.best_carry_expiry,
372
- "best_carry_yield": analysis.best_carry_yield,
373
- "basis_data": [
376
+ print(
377
+ json_fmt.format(
374
378
  {
375
- "expiry": b.expiry,
376
- "futures_price": float(b.futures_price),
377
- "basis_pct": b.basis_pct,
378
- "annualized_pct": b.annualized_pct,
379
- "days_to_expiry": b.days_to_expiry,
379
+ "symbol": analysis.symbol,
380
+ "spot_price": float(analysis.spot_price),
381
+ "avg_basis_pct": analysis.avg_basis_pct,
382
+ "avg_annualized": analysis.avg_annualized,
383
+ "market_structure": analysis.market_structure,
384
+ "structure_strength": analysis.structure_strength,
385
+ "best_carry_expiry": analysis.best_carry_expiry,
386
+ "best_carry_yield": analysis.best_carry_yield,
387
+ "basis_data": [
388
+ {
389
+ "expiry": b.expiry,
390
+ "futures_price": float(b.futures_price),
391
+ "basis_pct": b.basis_pct,
392
+ "annualized_pct": b.annualized_pct,
393
+ "days_to_expiry": b.days_to_expiry,
394
+ }
395
+ for b in analysis.basis_data
396
+ ],
380
397
  }
381
- for b in analysis.basis_data
382
- ],
383
- }))
398
+ )
399
+ )
384
400
  return
385
401
 
386
402
  print(f"\n Spot Price: ${analysis.spot_price:,}")
@@ -399,12 +415,10 @@ def cmd_basis(args):
399
415
  )
400
416
 
401
417
  print("-" * 60)
402
- print(f"\nMarket Structure: {analysis.structure_strength.title()} "
403
- f"{analysis.market_structure.title()}")
418
+ print(f"\nMarket Structure: {analysis.structure_strength.title()} {analysis.market_structure.title()}")
404
419
  print(f"Average Basis: {analysis.avg_basis_pct:+.2f}%")
405
420
  print(f"Average Annualized: {analysis.avg_annualized:+.1f}%")
406
- print(f"Best Carry: {analysis.best_carry_expiry} "
407
- f"({analysis.best_carry_yield:+.1f}% annualized)")
421
+ print(f"Best Carry: {analysis.best_carry_expiry} ({analysis.best_carry_yield:+.1f}% annualized)")
408
422
 
409
423
  # Term structure
410
424
  print(console.section("TERM STRUCTURE"))
@@ -422,12 +436,7 @@ def cmd_basis(args):
422
436
  print(f"\n{'Expiry':<12} {'Basis':>8} {'Annual':>10} {'Direction':<12}")
423
437
  print("-" * 50)
424
438
  for opp in opportunities[:5]:
425
- print(
426
- f"{opp.expiry:<12} "
427
- f"{opp.basis_pct:>+7.2f}% "
428
- f"{opp.annualized_yield:>+9.1f}% "
429
- f"{opp.direction:<12}"
430
- )
439
+ print(f"{opp.expiry:<12} {opp.basis_pct:>+7.2f}% {opp.annualized_yield:>+9.1f}% {opp.direction:<12}")
431
440
 
432
441
  print("\nTop Opportunity:")
433
442
  top = opportunities[0]
@@ -440,7 +449,7 @@ def cmd_basis(args):
440
449
  def cmd_dashboard(args):
441
450
  """Handle dashboard subcommand."""
442
451
  console = ConsoleFormatter()
443
- report_gen = ReportGenerator()
452
+ ReportGenerator()
444
453
 
445
454
  funding_tracker = FundingTracker()
446
455
  oi_analyzer = OIAnalyzer()
@@ -456,26 +465,26 @@ def cmd_dashboard(args):
456
465
  try:
457
466
  # Funding
458
467
  funding = funding_tracker.analyze(symbol)
459
- print(f"\n📊 FUNDING")
468
+ print("\n📊 FUNDING")
460
469
  print(f" Rate: {funding.weighted_avg:+.4%} ({funding.annualized_avg:+.1f}% annual)")
461
- print(f" Sentiment: {console.sentiment_icon(funding.sentiment)} "
462
- f"{funding.sentiment.title()}")
470
+ print(f" Sentiment: {console.sentiment_icon(funding.sentiment)} {funding.sentiment.title()}")
463
471
 
464
472
  # OI
465
473
  oi = oi_analyzer.analyze(symbol)
466
- print(f"\n📈 OPEN INTEREST")
467
- print(f" Total: ${float(oi.total_oi_usd)/1e9:.1f}B ({oi.avg_change_24h:+.1f}% 24h)")
474
+ print("\n📈 OPEN INTEREST")
475
+ print(f" Total: ${float(oi.total_oi_usd) / 1e9:.1f}B ({oi.avg_change_24h:+.1f}% 24h)")
468
476
  print(f" Long/Short: {oi.weighted_long_ratio:.2f} ({oi.long_percentage:.0f}% long)")
469
477
  print(f" Trend: {oi.trend_strength.title()} {oi.trend.title()}")
470
478
 
471
479
  # Liquidations
472
480
  liq = liq_monitor.get_summary(symbol)
473
- print(f"\n💥 LIQUIDATIONS (24h)")
474
- print(f" Total: ${float(liq.total_24h_usd)/1e6:.1f}M")
475
- print(f" Longs: ${float(liq.long_liquidations_usd)/1e6:.1f}M | "
476
- f"Shorts: ${float(liq.short_liquidations_usd)/1e6:.1f}M")
477
- print(f" Cascade Risk: {console.risk_icon(liq.cascade_risk)} "
478
- f"{liq.cascade_risk.upper()}")
481
+ print("\n💥 LIQUIDATIONS (24h)")
482
+ print(f" Total: ${float(liq.total_24h_usd) / 1e6:.1f}M")
483
+ print(
484
+ f" Longs: ${float(liq.long_liquidations_usd) / 1e6:.1f}M | "
485
+ f"Shorts: ${float(liq.short_liquidations_usd) / 1e6:.1f}M"
486
+ )
487
+ print(f" Cascade Risk: {console.risk_icon(liq.cascade_risk)} {liq.cascade_risk.upper()}")
479
488
 
480
489
  except Exception as e:
481
490
  print(f"\n⚠️ Error fetching data for {symbol}: {e}")
@@ -505,45 +514,37 @@ Examples:
505
514
  # Funding subcommand
506
515
  funding_parser = subparsers.add_parser("funding", help="Analyze funding rates")
507
516
  funding_parser.add_argument("symbol", help="Trading symbol (e.g., BTC)")
508
- funding_parser.add_argument("--format", choices=["console", "json"],
509
- default="console", help="Output format")
517
+ funding_parser.add_argument("--format", choices=["console", "json"], default="console", help="Output format")
510
518
 
511
519
  # OI subcommand
512
520
  oi_parser = subparsers.add_parser("oi", help="Analyze open interest")
513
521
  oi_parser.add_argument("symbol", help="Trading symbol")
514
- oi_parser.add_argument("--price-change", type=float, dest="price_change",
515
- help="24h price change %% for divergence analysis")
516
- oi_parser.add_argument("--format", choices=["console", "json"],
517
- default="console", help="Output format")
522
+ oi_parser.add_argument(
523
+ "--price-change", type=float, dest="price_change", help="24h price change %% for divergence analysis"
524
+ )
525
+ oi_parser.add_argument("--format", choices=["console", "json"], default="console", help="Output format")
518
526
 
519
527
  # Liquidations subcommand
520
528
  liq_parser = subparsers.add_parser("liquidations", help="Monitor liquidations")
521
529
  liq_parser.add_argument("symbol", help="Trading symbol")
522
530
  liq_parser.add_argument("--price", type=float, help="Current price override")
523
- liq_parser.add_argument("--large", action="store_true",
524
- help="Show recent large liquidations")
525
- liq_parser.add_argument("--format", choices=["console", "json"],
526
- default="console", help="Output format")
531
+ liq_parser.add_argument("--large", action="store_true", help="Show recent large liquidations")
532
+ liq_parser.add_argument("--format", choices=["console", "json"], default="console", help="Output format")
527
533
 
528
534
  # Options subcommand
529
535
  opt_parser = subparsers.add_parser("options", help="Analyze options market")
530
536
  opt_parser.add_argument("symbol", help="Trading symbol")
531
537
  opt_parser.add_argument("--price", type=float, help="Current price override")
532
- opt_parser.add_argument("--max-pain", action="store_true", dest="max_pain",
533
- help="Show max pain by expiry")
534
- opt_parser.add_argument("--flow", action="store_true",
535
- help="Show options flow")
536
- opt_parser.add_argument("--format", choices=["console", "json"],
537
- default="console", help="Output format")
538
+ opt_parser.add_argument("--max-pain", action="store_true", dest="max_pain", help="Show max pain by expiry")
539
+ opt_parser.add_argument("--flow", action="store_true", help="Show options flow")
540
+ opt_parser.add_argument("--format", choices=["console", "json"], default="console", help="Output format")
538
541
 
539
542
  # Basis subcommand
540
543
  basis_parser = subparsers.add_parser("basis", help="Calculate futures basis")
541
544
  basis_parser.add_argument("symbol", help="Trading symbol")
542
545
  basis_parser.add_argument("--price", type=float, help="Spot price override")
543
- basis_parser.add_argument("--carry", action="store_true",
544
- help="Show carry opportunities")
545
- basis_parser.add_argument("--format", choices=["console", "json"],
546
- default="console", help="Output format")
546
+ basis_parser.add_argument("--carry", action="store_true", help="Show carry opportunities")
547
+ basis_parser.add_argument("--format", choices=["console", "json"], default="console", help="Output format")
547
548
 
548
549
  # Dashboard subcommand
549
550
  dash_parser = subparsers.add_parser("dashboard", help="Multi-asset dashboard")
@@ -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