@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.
- package/README.md +16 -1
- package/agents/derivatives-agent.md +58 -3
- package/package.json +1 -1
- package/skills/skill-adapter/references/README.md +0 -1
- package/skills/skill-adapter/references/examples.md +6 -0
- package/skills/tracking-crypto-derivatives/ARD.md +22 -0
- package/skills/tracking-crypto-derivatives/PRD.md +26 -0
- package/skills/tracking-crypto-derivatives/SKILL.md +28 -6
- package/skills/tracking-crypto-derivatives/references/errors.md +24 -0
- package/skills/tracking-crypto-derivatives/references/examples.md +19 -0
- package/skills/tracking-crypto-derivatives/references/implementation.md +8 -0
- package/skills/tracking-crypto-derivatives/scripts/basis_calculator.py +23 -24
- package/skills/tracking-crypto-derivatives/scripts/derivatives_tracker.py +176 -175
- package/skills/tracking-crypto-derivatives/scripts/formatters.py +13 -18
- package/skills/tracking-crypto-derivatives/scripts/funding_tracker.py +34 -34
- package/skills/tracking-crypto-derivatives/scripts/liquidation_monitor.py +31 -35
- package/skills/tracking-crypto-derivatives/scripts/oi_analyzer.py +23 -27
- package/skills/tracking-crypto-derivatives/scripts/options_analyzer.py +42 -41
- package/skills/skill-adapter/scripts/validation.sh +0 -32
|
@@ -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
|
|
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
|
|
30
|
-
structure_strength: str
|
|
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
|
|
49
|
-
strategy: str
|
|
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
|
|
66
|
-
MODERATE_BASIS = 2.0
|
|
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(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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(
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
83
|
-
|
|
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(
|
|
88
|
+
print("\nā ļø EXTREME FUNDING - Contrarian opportunity")
|
|
87
89
|
|
|
88
90
|
if analysis.arbitrage_opportunity:
|
|
89
|
-
print(
|
|
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(
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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(
|
|
181
|
-
|
|
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
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
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
|
-
|
|
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(
|
|
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(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
318
|
-
|
|
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(
|
|
365
|
-
|
|
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
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
474
|
-
print(f" Total: ${float(liq.total_24h_usd)/1e6:.1f}M")
|
|
475
|
-
print(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
534
|
-
opt_parser.add_argument("--
|
|
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
|
-
|
|
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")
|