@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.
@@ -12,7 +12,7 @@ Calculates futures basis and spreads with:
12
12
  from dataclasses import dataclass
13
13
  from decimal import Decimal
14
14
  from typing import Dict, List, Optional
15
- from datetime import datetime, date
15
+ from datetime import datetime
16
16
 
17
17
  from exchange_client import ExchangeClient, BasisData, Exchange
18
18
 
@@ -26,8 +26,8 @@ class BasisAnalysis:
26
26
  basis_data: List[BasisData]
27
27
  avg_basis_pct: float
28
28
  avg_annualized: float
29
- market_structure: str # "contango", "backwardation", "mixed"
30
- structure_strength: str # "strong", "moderate", "weak"
29
+ market_structure: str # "contango", "backwardation", "mixed"
30
+ structure_strength: str # "strong", "moderate", "weak"
31
31
  best_carry_expiry: Optional[str]
32
32
  best_carry_yield: float
33
33
  timestamp: datetime
@@ -45,8 +45,8 @@ class CarryOpportunity:
45
45
  basis_pct: float
46
46
  days_to_expiry: int
47
47
  annualized_yield: float
48
- direction: str # "long_basis" or "short_basis"
49
- strategy: str # Trade description
48
+ direction: str # "long_basis" or "short_basis"
49
+ strategy: str # Trade description
50
50
  risk_notes: str
51
51
 
52
52
 
@@ -62,8 +62,8 @@ class BasisCalculator:
62
62
  """
63
63
 
64
64
  # Structure interpretation
65
- STRONG_BASIS = 5.0 # >5% annualized is strong
66
- MODERATE_BASIS = 2.0 # >2% is moderate
65
+ STRONG_BASIS = 5.0 # >5% annualized is strong
66
+ MODERATE_BASIS = 2.0 # >2% is moderate
67
67
 
68
68
  def __init__(
69
69
  self,
@@ -213,19 +213,21 @@ class BasisCalculator:
213
213
  )
214
214
  risk_notes = "Borrowing costs apply; squeeze risk in tight markets"
215
215
 
216
- opportunities.append(CarryOpportunity(
217
- symbol=symbol,
218
- exchange=basis.exchange,
219
- expiry=basis.expiry,
220
- spot_price=analysis.spot_price,
221
- futures_price=basis.futures_price,
222
- basis_pct=basis.basis_pct,
223
- days_to_expiry=basis.days_to_expiry,
224
- annualized_yield=basis.annualized_pct,
225
- direction=direction,
226
- strategy=strategy,
227
- risk_notes=risk_notes,
228
- ))
216
+ opportunities.append(
217
+ CarryOpportunity(
218
+ symbol=symbol,
219
+ exchange=basis.exchange,
220
+ expiry=basis.expiry,
221
+ spot_price=analysis.spot_price,
222
+ futures_price=basis.futures_price,
223
+ basis_pct=basis.basis_pct,
224
+ days_to_expiry=basis.days_to_expiry,
225
+ annualized_yield=basis.annualized_pct,
226
+ direction=direction,
227
+ strategy=strategy,
228
+ risk_notes=risk_notes,
229
+ )
230
+ )
229
231
  except Exception:
230
232
  continue
231
233
 
@@ -251,10 +253,7 @@ class BasisCalculator:
251
253
  analysis = self.analyze(symbol, spot_price)
252
254
 
253
255
  # Sort by days to expiry
254
- sorted_basis = sorted(
255
- analysis.basis_data,
256
- key=lambda b: b.days_to_expiry
257
- )
256
+ sorted_basis = sorted(analysis.basis_data, key=lambda b: b.days_to_expiry)
258
257
 
259
258
  return [
260
259
  {
@@ -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")