trading-cli 0.3.0__py3-none-any.whl
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.
- fastbooks_cli/__init__.py +3 -0
- fastbooks_cli/__main__.py +6 -0
- fastbooks_cli/client.py +1169 -0
- fastbooks_cli/commands/__init__.py +0 -0
- fastbooks_cli/commands/apikey.py +144 -0
- fastbooks_cli/commands/backtest.py +450 -0
- fastbooks_cli/commands/bots.py +479 -0
- fastbooks_cli/commands/config.py +294 -0
- fastbooks_cli/commands/dex.py +507 -0
- fastbooks_cli/commands/direct.py +150 -0
- fastbooks_cli/commands/execution.py +298 -0
- fastbooks_cli/commands/init_cmd.py +149 -0
- fastbooks_cli/commands/markets.py +270 -0
- fastbooks_cli/commands/mcp_cmd.py +27 -0
- fastbooks_cli/commands/onchain.py +123 -0
- fastbooks_cli/commands/polymarket.py +2215 -0
- fastbooks_cli/commands/repl.py +385 -0
- fastbooks_cli/commands/strategy.py +259 -0
- fastbooks_cli/commands/swap.py +320 -0
- fastbooks_cli/commands/wallet.py +447 -0
- fastbooks_cli/config_store.py +112 -0
- fastbooks_cli/direct.py +209 -0
- fastbooks_cli/main.py +88 -0
- fastbooks_cli/mcp_server.py +171 -0
- fastbooks_cli/output.py +86 -0
- fastbooks_cli/strategies/__init__.py +87 -0
- trading_cli-0.3.0.dist-info/METADATA +309 -0
- trading_cli-0.3.0.dist-info/RECORD +32 -0
- trading_cli-0.3.0.dist-info/WHEEL +5 -0
- trading_cli-0.3.0.dist-info/entry_points.txt +4 -0
- trading_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
- trading_cli-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Swap command group — on-chain DEX swap execution (Jupiter, Uniswap, SushiSwap)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ..client import FastBooksClient
|
|
12
|
+
from ..output import emit, emit_error, format_table, require_experimental
|
|
13
|
+
|
|
14
|
+
# ── Solana token address registry ────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
SOLANA_TOKENS = {
|
|
17
|
+
"SOL": "So11111111111111111111111111111111111111112",
|
|
18
|
+
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
19
|
+
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
|
20
|
+
"BONK": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
|
|
21
|
+
"JUP": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
|
|
22
|
+
"RAY": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
|
|
23
|
+
"ORCA": "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
|
|
24
|
+
"MSOL": "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
|
|
25
|
+
"JITOSOL": "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",
|
|
26
|
+
"WIF": "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# ── EVM token address registry (Ethereum mainnet) ───────────────────────
|
|
30
|
+
|
|
31
|
+
EVM_TOKENS = {
|
|
32
|
+
"ETH": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # native
|
|
33
|
+
"WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
34
|
+
"USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
35
|
+
"USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
36
|
+
"DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
37
|
+
"WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
|
|
38
|
+
"UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
|
39
|
+
"LINK": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
|
|
40
|
+
"AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
|
|
41
|
+
"ARB": "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
CHAINS = ["solana", "ethereum", "arbitrum", "polygon", "base", "optimism"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.group()
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def swap(ctx):
|
|
50
|
+
"""[experimental] On-chain DEX swaps: Jupiter (Solana), Uniswap (EVM), SushiSwap.
|
|
51
|
+
|
|
52
|
+
NOTE: requires an on-chain swap router not enabled on standard FastBooks
|
|
53
|
+
deployments. Gated behind FASTBOOKS_EXPERIMENTAL=1.
|
|
54
|
+
|
|
55
|
+
Execute token swaps directly on-chain through DEX aggregators.
|
|
56
|
+
Requires a wallet private key (stored via `fastbooks wallet add`).
|
|
57
|
+
|
|
58
|
+
Supported DEX aggregators:
|
|
59
|
+
Jupiter: Solana tokens (5000+). Best price routing.
|
|
60
|
+
Uniswap: Ethereum/L2 tokens. V3 concentrated liquidity.
|
|
61
|
+
SushiSwap: Multi-chain (Ethereum, Arbitrum, Polygon).
|
|
62
|
+
"""
|
|
63
|
+
require_experimental(ctx, "swap")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Jupiter swap (Solana) ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
@swap.command("jupiter")
|
|
69
|
+
@click.argument("input_token", type=str)
|
|
70
|
+
@click.argument("output_token", type=str)
|
|
71
|
+
@click.argument("amount", type=float)
|
|
72
|
+
@click.option("--slippage", type=float, default=1.0, help="Max slippage in percent (default: 1%).")
|
|
73
|
+
@click.option("--wallet-key", envvar="SOLANA_PRIVATE_KEY", default=None, help="Solana private key (or set SOLANA_PRIVATE_KEY).")
|
|
74
|
+
@click.option("--rpc-url", envvar="SOLANA_RPC_URL", default="https://api.mainnet-beta.solana.com", help="Solana RPC URL.")
|
|
75
|
+
@click.option("--quote-only", is_flag=True, help="Only get quote, don't execute swap.")
|
|
76
|
+
@click.option("--priority-fee", type=int, default=50000, help="Priority fee in micro-lamports.")
|
|
77
|
+
@click.pass_context
|
|
78
|
+
def jupiter_swap(ctx, input_token, output_token, amount, slippage, wallet_key, rpc_url, quote_only, priority_fee):
|
|
79
|
+
"""Swap tokens on Solana via Jupiter aggregator.
|
|
80
|
+
|
|
81
|
+
INPUT_TOKEN: Token to sell (symbol like SOL, USDC, or mint address).
|
|
82
|
+
OUTPUT_TOKEN: Token to buy (symbol or mint address).
|
|
83
|
+
AMOUNT: Amount of input token to swap.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
fastbooks swap jupiter SOL USDC 1.5
|
|
87
|
+
fastbooks swap jupiter USDC BONK 100 --slippage 2.0
|
|
88
|
+
fastbooks swap jupiter SOL JUP 10 --quote-only
|
|
89
|
+
fastbooks swap jupiter So111...112 EPjF...1v 5.0
|
|
90
|
+
"""
|
|
91
|
+
client: FastBooksClient = ctx.obj
|
|
92
|
+
|
|
93
|
+
input_mint = SOLANA_TOKENS.get(input_token.upper(), input_token)
|
|
94
|
+
output_mint = SOLANA_TOKENS.get(output_token.upper(), output_token)
|
|
95
|
+
slippage_bps = int(slippage * 100)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Get quote from Jupiter
|
|
99
|
+
quote = client.jupiter_quote(input_mint, output_mint, amount, slippage_bps)
|
|
100
|
+
|
|
101
|
+
if quote_only:
|
|
102
|
+
emit(ctx, quote, lambda d: _format_jupiter_quote(d, input_token, output_token, amount))
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if not wallet_key:
|
|
106
|
+
emit_error(ctx, "Wallet key required for swap execution. Use --wallet-key or set SOLANA_PRIVATE_KEY.")
|
|
107
|
+
ctx.exit(1)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Confirm before executing
|
|
111
|
+
if not ctx.find_root().params.get("json_output", False):
|
|
112
|
+
out_amount = quote.get("outAmount", quote.get("expectedOutput", "?"))
|
|
113
|
+
click.echo(f"\n Swap: {amount} {input_token} -> {out_amount} {output_token}")
|
|
114
|
+
click.echo(f" Slippage: {slippage}% Priority fee: {priority_fee} uL")
|
|
115
|
+
if not click.confirm("\n Execute swap?"):
|
|
116
|
+
click.echo(" Cancelled.")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
result = client.jupiter_swap_execute(
|
|
120
|
+
input_mint=input_mint,
|
|
121
|
+
output_mint=output_mint,
|
|
122
|
+
amount=amount,
|
|
123
|
+
slippage_bps=slippage_bps,
|
|
124
|
+
wallet_key=wallet_key,
|
|
125
|
+
priority_fee=priority_fee,
|
|
126
|
+
)
|
|
127
|
+
emit(ctx, result, lambda d: _format_swap_result(d, "Jupiter"))
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
emit_error(ctx, str(e))
|
|
131
|
+
ctx.exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _format_jupiter_quote(data: dict, input_tok: str, output_tok: str, amount: float) -> str:
|
|
135
|
+
lines = [
|
|
136
|
+
"",
|
|
137
|
+
" Jupiter Quote",
|
|
138
|
+
" =============",
|
|
139
|
+
f" Input: {amount} {input_tok}",
|
|
140
|
+
f" Output: {data.get('outAmount', data.get('expectedOutput', '?'))} {output_tok}",
|
|
141
|
+
f" Price Impact: {data.get('priceImpactPct', '?')}%",
|
|
142
|
+
]
|
|
143
|
+
routes = data.get("routePlan", [])
|
|
144
|
+
if routes:
|
|
145
|
+
lines.append(f" Route: {' -> '.join(r.get('swapInfo', {}).get('label', '?') for r in routes)}")
|
|
146
|
+
return "\n".join(lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Uniswap swap (EVM) ──────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
@swap.command("uniswap")
|
|
152
|
+
@click.argument("input_token", type=str)
|
|
153
|
+
@click.argument("output_token", type=str)
|
|
154
|
+
@click.argument("amount", type=float)
|
|
155
|
+
@click.option("--chain", type=click.Choice(["ethereum", "arbitrum", "polygon", "base", "optimism"]), default="ethereum", help="EVM chain.")
|
|
156
|
+
@click.option("--slippage", type=float, default=0.5, help="Max slippage in percent.")
|
|
157
|
+
@click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None, help="EVM private key (or set EVM_PRIVATE_KEY).")
|
|
158
|
+
@click.option("--rpc-url", envvar="EVM_RPC_URL", default=None, help="EVM RPC URL.")
|
|
159
|
+
@click.option("--quote-only", is_flag=True, help="Only get quote, don't execute.")
|
|
160
|
+
@click.option("--gas-limit", type=int, default=None, help="Gas limit override.")
|
|
161
|
+
@click.pass_context
|
|
162
|
+
def uniswap_swap(ctx, input_token, output_token, amount, chain, slippage, wallet_key, rpc_url, quote_only, gas_limit):
|
|
163
|
+
"""Swap tokens on EVM chains via Uniswap V3 / compatible routers.
|
|
164
|
+
|
|
165
|
+
INPUT_TOKEN: Token to sell (symbol like ETH, USDC, or contract address).
|
|
166
|
+
OUTPUT_TOKEN: Token to buy (symbol or contract address).
|
|
167
|
+
AMOUNT: Amount of input token to swap.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
fastbooks swap uniswap ETH USDC 1.0
|
|
171
|
+
fastbooks swap uniswap USDC WBTC 5000 --chain ethereum
|
|
172
|
+
fastbooks swap uniswap ETH ARB 2.0 --chain arbitrum --slippage 1.0
|
|
173
|
+
fastbooks swap uniswap USDC DAI 1000 --quote-only
|
|
174
|
+
"""
|
|
175
|
+
client: FastBooksClient = ctx.obj
|
|
176
|
+
|
|
177
|
+
input_address = EVM_TOKENS.get(input_token.upper(), input_token)
|
|
178
|
+
output_address = EVM_TOKENS.get(output_token.upper(), output_token)
|
|
179
|
+
slippage_bps = int(slippage * 100)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
quote = client.uniswap_quote(
|
|
183
|
+
input_address, output_address, amount,
|
|
184
|
+
chain=chain, slippage_bps=slippage_bps,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if quote_only:
|
|
188
|
+
emit(ctx, quote, lambda d: _format_uniswap_quote(d, input_token, output_token, amount, chain))
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if not wallet_key:
|
|
192
|
+
emit_error(ctx, "Wallet key required. Use --wallet-key or set EVM_PRIVATE_KEY.")
|
|
193
|
+
ctx.exit(1)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if not ctx.find_root().params.get("json_output", False):
|
|
197
|
+
out_amount = quote.get("outputAmount", quote.get("expectedOutput", "?"))
|
|
198
|
+
gas_est = quote.get("gasEstimate", "?")
|
|
199
|
+
click.echo(f"\n Swap: {amount} {input_token} -> {out_amount} {output_token}")
|
|
200
|
+
click.echo(f" Chain: {chain} Slippage: {slippage}% Gas: ~{gas_est}")
|
|
201
|
+
if not click.confirm("\n Execute swap?"):
|
|
202
|
+
click.echo(" Cancelled.")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
result = client.uniswap_swap_execute(
|
|
206
|
+
input_address=input_address,
|
|
207
|
+
output_address=output_address,
|
|
208
|
+
amount=amount,
|
|
209
|
+
chain=chain,
|
|
210
|
+
slippage_bps=slippage_bps,
|
|
211
|
+
wallet_key=wallet_key,
|
|
212
|
+
gas_limit=gas_limit,
|
|
213
|
+
)
|
|
214
|
+
emit(ctx, result, lambda d: _format_swap_result(d, "Uniswap"))
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
emit_error(ctx, str(e))
|
|
218
|
+
ctx.exit(1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _format_uniswap_quote(data: dict, input_tok: str, output_tok: str, amount: float, chain: str) -> str:
|
|
222
|
+
return "\n".join([
|
|
223
|
+
"",
|
|
224
|
+
f" Uniswap Quote ({chain})",
|
|
225
|
+
" ====================",
|
|
226
|
+
f" Input: {amount} {input_tok}",
|
|
227
|
+
f" Output: {data.get('outputAmount', '?')} {output_tok}",
|
|
228
|
+
f" Gas Est: {data.get('gasEstimate', '?')} gwei",
|
|
229
|
+
f" Price Impact: {data.get('priceImpact', '?')}%",
|
|
230
|
+
f" Route: {data.get('route', '?')}",
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ── SushiSwap (multi-chain) ─────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
@swap.command("sushi")
|
|
237
|
+
@click.argument("input_token", type=str)
|
|
238
|
+
@click.argument("output_token", type=str)
|
|
239
|
+
@click.argument("amount", type=float)
|
|
240
|
+
@click.option("--chain", type=click.Choice(["ethereum", "arbitrum", "polygon", "avalanche"]), default="ethereum")
|
|
241
|
+
@click.option("--slippage", type=float, default=0.5, help="Max slippage percent.")
|
|
242
|
+
@click.option("--wallet-key", envvar="EVM_PRIVATE_KEY", default=None)
|
|
243
|
+
@click.option("--quote-only", is_flag=True)
|
|
244
|
+
@click.pass_context
|
|
245
|
+
def sushi_swap(ctx, input_token, output_token, amount, chain, slippage, wallet_key, quote_only):
|
|
246
|
+
"""Swap tokens via SushiSwap on multiple EVM chains.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
fastbooks swap sushi ETH USDC 1.0 --chain ethereum
|
|
250
|
+
fastbooks swap sushi USDC WBTC 5000 --chain arbitrum
|
|
251
|
+
fastbooks swap sushi ETH DAI 2.0 --quote-only
|
|
252
|
+
"""
|
|
253
|
+
client: FastBooksClient = ctx.obj
|
|
254
|
+
|
|
255
|
+
input_address = EVM_TOKENS.get(input_token.upper(), input_token)
|
|
256
|
+
output_address = EVM_TOKENS.get(output_token.upper(), output_token)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
result = client.sushi_swap(
|
|
260
|
+
input_address=input_address,
|
|
261
|
+
output_address=output_address,
|
|
262
|
+
amount=amount,
|
|
263
|
+
chain=chain,
|
|
264
|
+
slippage_bps=int(slippage * 100),
|
|
265
|
+
wallet_key=wallet_key,
|
|
266
|
+
quote_only=quote_only,
|
|
267
|
+
)
|
|
268
|
+
emit(ctx, result, lambda d: _format_swap_result(d, f"SushiSwap ({chain})"))
|
|
269
|
+
except Exception as e:
|
|
270
|
+
emit_error(ctx, str(e))
|
|
271
|
+
ctx.exit(1)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ── token lookup ─────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
@swap.command("tokens")
|
|
277
|
+
@click.option("--chain", type=click.Choice(CHAINS), default="solana", help="Chain to list tokens for.")
|
|
278
|
+
@click.pass_context
|
|
279
|
+
def list_tokens(ctx, chain):
|
|
280
|
+
"""List known token addresses for a chain.
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
fastbooks swap tokens --chain solana
|
|
284
|
+
fastbooks swap tokens --chain ethereum
|
|
285
|
+
"""
|
|
286
|
+
if chain == "solana":
|
|
287
|
+
data = {"chain": chain, "tokens": [{"symbol": k, "address": v} for k, v in SOLANA_TOKENS.items()]}
|
|
288
|
+
else:
|
|
289
|
+
data = {"chain": chain, "tokens": [{"symbol": k, "address": v} for k, v in EVM_TOKENS.items()]}
|
|
290
|
+
|
|
291
|
+
emit(ctx, data, lambda d: _format_token_list(d))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _format_token_list(data: dict) -> str:
|
|
295
|
+
lines = [f"\n Known Tokens ({data['chain']})", " " + "=" * 30]
|
|
296
|
+
for t in data["tokens"]:
|
|
297
|
+
lines.append(f" {t['symbol']:>10} {t['address']}")
|
|
298
|
+
return "\n".join(lines)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── shared formatters ────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def _format_swap_result(data: dict, dex_name: str) -> str:
|
|
304
|
+
lines = [
|
|
305
|
+
"",
|
|
306
|
+
f" {dex_name} Swap Result",
|
|
307
|
+
" " + "=" * 25,
|
|
308
|
+
]
|
|
309
|
+
if data.get("txHash") or data.get("signature"):
|
|
310
|
+
tx = data.get("txHash") or data.get("signature")
|
|
311
|
+
lines.append(f" Tx: {tx}")
|
|
312
|
+
if data.get("inputAmount"):
|
|
313
|
+
lines.append(f" Input: {data['inputAmount']} {data.get('inputToken', '')}")
|
|
314
|
+
if data.get("outputAmount"):
|
|
315
|
+
lines.append(f" Output: {data['outputAmount']} {data.get('outputToken', '')}")
|
|
316
|
+
if data.get("gasUsed"):
|
|
317
|
+
lines.append(f" Gas: {data['gasUsed']}")
|
|
318
|
+
status = data.get("status", "unknown")
|
|
319
|
+
lines.append(f" Status: {status}")
|
|
320
|
+
return "\n".join(lines)
|