From b3cf7c9bc4901750853a8c62beae6534523a3d13 Mon Sep 17 00:00:00 2001 From: Noah Lauffer Date: Wed, 11 Mar 2026 13:38:42 -0300 Subject: [PATCH 1/5] Update CLI to include beta option quote/impact endpoints --- package-lock.json | 8 +- package.json | 2 +- src/commands/index.ts | 4 + src/commands/optionImpact.ts | 135 +++++++++++++++++++++++++++++ src/commands/optionQuote.ts | 104 ++++++++++++++++++++++ src/commands/trade/option/index.ts | 71 ++++++++++++--- 6 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 src/commands/optionImpact.ts create mode 100644 src/commands/optionQuote.ts diff --git a/package-lock.json b/package-lock.json index fa8ade2..604af0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "open": "^10.2.0", "ora": "^8.2.0", "prettier": "^3.6.2", - "snaptrade-typescript-sdk": "^9.0.146", + "snaptrade-typescript-sdk": "^9.0.173", "string-width": "^7.1.0", "yahoo-finance2": "^3.6.0", "zod": "^3.25.76" @@ -2298,9 +2298,9 @@ } }, "node_modules/snaptrade-typescript-sdk": { - "version": "9.0.146", - "resolved": "https://registry.npmjs.org/snaptrade-typescript-sdk/-/snaptrade-typescript-sdk-9.0.146.tgz", - "integrity": "sha512-eVCtEiXMooOwHfjwjZMwht5Ph6PypV+CPT8KzYVUqkVuDjNLbCBJcPMv4OAOCcNj+SdHCXGU0mjU6ptzV3jiVg==", + "version": "9.0.173", + "resolved": "https://registry.npmjs.org/snaptrade-typescript-sdk/-/snaptrade-typescript-sdk-9.0.173.tgz", + "integrity": "sha512-3qWpE6fAqTPrfg72cmDm30oXbhquq1FVogUMnHQug0WBw2cLT7OJYfNNF8UpE4bXc4LQMgqaOgAT49dp7ibIsQ==", "license": "Unlicense", "dependencies": { "axios": "1.10.0" diff --git a/package.json b/package.json index ea3b04a..a093dde 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "open": "^10.2.0", "ora": "^8.2.0", "prettier": "^3.6.2", - "snaptrade-typescript-sdk": "^9.0.146", + "snaptrade-typescript-sdk": "^9.0.173", "string-width": "^7.1.0", "yahoo-finance2": "^3.6.0", "zod": "^3.25.76" diff --git a/src/commands/index.ts b/src/commands/index.ts index ab9d21f..b6dc2f8 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -16,6 +16,8 @@ import { ordersCommand } from "./orders.ts"; import { instrumentsCommand } from "./instruments.ts"; import { profilesCommand } from "./profiles.ts"; import { mcpCommand } from "./mcp.ts"; +import { optionQuoteCommand } from "./optionQuote.ts"; +import { optionImpactCommand } from "./optionImpact.ts"; export function registerCommands(program: Command, snaptrade: Snaptrade): void { program.addCommand(statusCommand(snaptrade)); @@ -30,6 +32,8 @@ export function registerCommands(program: Command, snaptrade: Snaptrade): void { program.addCommand(ordersCommand(snaptrade)); program.addCommand(instrumentsCommand(snaptrade)); program.addCommand(quoteCommand(snaptrade)); + program.addCommand(optionQuoteCommand(snaptrade)); + program.addCommand(optionImpactCommand(snaptrade)); program.addCommand(tradeCommand(snaptrade)); program.addCommand(cancelOrderCommand(snaptrade)); program.addCommand(mcpCommand(snaptrade)); diff --git a/src/commands/optionImpact.ts b/src/commands/optionImpact.ts new file mode 100644 index 0000000..bccc916 --- /dev/null +++ b/src/commands/optionImpact.ts @@ -0,0 +1,135 @@ +import chalk from "chalk"; +import { Command } from "commander"; +import type { MlegActionStrict, MlegInstrumentType, TimeInForceStrict } from "snaptrade-typescript-sdk"; +import { Snaptrade } from "snaptrade-typescript-sdk"; +import { generateOccSymbol } from "../utils/generateOccSymbol.ts"; +import { selectAccount } from "../utils/selectAccount.ts"; +import { loadOrRegisterUser } from "../utils/user.ts"; +import { withDebouncedSpinner } from "../utils/withDebouncedSpinner.ts"; + +export function optionImpactCommand(snaptrade: Snaptrade): Command { + return new Command("option-impact") + .description( + "Simulate an option trade and see estimated cash change and fees" + ) + .requiredOption("--ticker ", "Underlying asset symbol") + .requiredOption("--exp ", "Expiration date (YYYY-MM-DD)") + .requiredOption("--strike ", "Strike price") + .requiredOption("--type ", "Option type: call or put", (input) => { + const normalized = input.toLowerCase(); + if (normalized !== "call" && normalized !== "put") { + console.error('Invalid option type. Must be "call" or "put".'); + process.exit(1); + } + return normalized; + }) + .requiredOption("--action ", "BUY or SELL", (input) => { + const normalized = input.toUpperCase(); + if (normalized !== "BUY" && normalized !== "SELL") { + console.error('Invalid action. Must be "BUY" or "SELL".'); + process.exit(1); + } + return normalized; + }) + .option("--contracts ", "Number of contracts", "1") + .option( + "--orderType ", + "Order type: Market, Limit, Stop, StopLimit", + "Market" + ) + .option("--limitPrice ", "Limit price (required for Limit orders)") + .option("--tif ", "Time in force: Day or GTC", "Day") + .action(async (opts, command) => { + const user = await loadOrRegisterUser(snaptrade); + const account = await selectAccount({ + snaptrade, + useLastAccount: command.parent.opts().useLastAccount, + context: "option_trade", + }); + + const { ticker, exp, strike, type, action, contracts, orderType, limitPrice, tif } = + opts as { + ticker: string; + exp: string; + strike: string; + type: "call" | "put"; + action: "BUY" | "SELL"; + contracts: string; + orderType: string; + limitPrice?: string; + tif: string; + }; + + const optionType = type === "call" ? "CALL" : "PUT"; + const symbol = generateOccSymbol(ticker, exp, Number(strike), optionType); + const units = parseInt(contracts); + + const orderTypeInput = (() => { + switch (orderType) { + case "Market": + return "MARKET"; + case "Limit": + return "LIMIT"; + case "Stop": + return "STOP_LOSS_MARKET"; + case "StopLimit": + return "STOP_LOSS_LIMIT"; + default: + console.error( + `Invalid order type "${orderType}". Allowed: Market, Limit, Stop, StopLimit` + ); + process.exit(1); + } + })(); + + const response = await withDebouncedSpinner( + "Fetching option impact estimate...", + async () => + snaptrade.trading.getOptionImpact({ + ...user, + accountId: account.id, + order_type: orderTypeInput, + time_in_force: tif as TimeInForceStrict, + limit_price: limitPrice, + price_effect: action === "BUY" ? "DEBIT" : "CREDIT", + legs: [ + { + instrument: { + instrument_type: "OPTION" as MlegInstrumentType, + symbol, + }, + action: `${action}_TO_OPEN` as MlegActionStrict, + units, + }, + ], + }) + ); + + const impact = response.data; + + console.log(chalk.bold("\n๐Ÿ“Š Option Impact Estimate\n")); + console.log(` Symbol: ${symbol}`); + console.log(` Action: ${action === "BUY" ? chalk.green(action) : chalk.red(action)}`); + console.log(` Contracts: ${units}`); + console.log(` Order Type: ${orderType}`); + if (limitPrice) { + console.log(` Limit Price: $${limitPrice}`); + } + console.log(); + + const directionLabel = + impact.cash_change_direction === "CREDIT" + ? chalk.green("CREDIT") + : impact.cash_change_direction === "DEBIT" + ? chalk.red("DEBIT") + : impact.cash_change_direction ?? "UNKNOWN"; + + console.log( + ` Cash Change: $${Number(impact.estimated_cash_change).toFixed(2)} ${directionLabel}` + ); + console.log( + ` Est. Fees: $${Number(impact.estimated_fee_total).toFixed(2)}` + ); + console.log(); + }); +} diff --git a/src/commands/optionQuote.ts b/src/commands/optionQuote.ts new file mode 100644 index 0000000..c92a387 --- /dev/null +++ b/src/commands/optionQuote.ts @@ -0,0 +1,104 @@ +import { Command } from "commander"; +import { Snaptrade } from "snaptrade-typescript-sdk"; +import Table from "cli-table3"; +import { generateOccSymbol } from "../utils/generateOccSymbol.ts"; +import { selectAccount } from "../utils/selectAccount.ts"; +import { loadOrRegisterUser } from "../utils/user.ts"; +import { withDebouncedSpinner } from "../utils/withDebouncedSpinner.ts"; + +export function optionQuoteCommand(snaptrade: Snaptrade): Command { + return new Command("option-quote") + .description("Get a real-time quote for an option contract") + .requiredOption("--ticker ", "Underlying asset symbol") + .requiredOption("--exp ", "Expiration date (YYYY-MM-DD)") + .requiredOption("--strike ", "Strike price") + .requiredOption("--type ", "Option type: call or put", (input) => { + const normalized = input.toLowerCase(); + if (normalized !== "call" && normalized !== "put") { + console.error('Invalid option type. Must be "call" or "put".'); + process.exit(1); + } + return normalized; + }) + .action(async (opts, command) => { + const user = await loadOrRegisterUser(snaptrade); + const account = await selectAccount({ + snaptrade, + useLastAccount: command.parent.opts().useLastAccount, + context: "option_trade", + }); + + const { ticker, exp, strike, type } = opts as { + ticker: string; + exp: string; + strike: string; + type: "call" | "put"; + }; + + const optionType = type === "call" ? "CALL" : "PUT"; + const symbol = generateOccSymbol(ticker, exp, Number(strike), optionType); + + const response = await withDebouncedSpinner( + "Fetching option quote...", + async () => + snaptrade.options.getOptionQuote({ + ...user, + accountId: account.id, + symbol, + }) + ); + + const q = response.data; + + const table = new Table({ + head: ["Field", "Value"], + }); + + table.push( + ["Symbol", q.symbol ?? symbol], + [ + "Bid", + q.bid_price != null + ? `$${q.bid_price.toFixed(2)} x${q.bid_size ?? "โ€”"}` + : "N/A", + ], + [ + "Ask", + q.ask_price != null + ? `$${q.ask_price.toFixed(2)} x${q.ask_size ?? "โ€”"}` + : "N/A", + ], + [ + "Last", + q.last_price != null + ? `$${q.last_price.toFixed(2)} x${q.last_size ?? "โ€”"}` + : "N/A", + ], + [ + "Open Interest", + q.open_interest != null + ? q.open_interest.toLocaleString("en-US") + : "N/A", + ], + [ + "Volume", + q.volume != null ? q.volume.toLocaleString("en-US") : "N/A", + ], + [ + "Implied Volatility", + q.implied_volatility != null + ? `${(q.implied_volatility * 100).toFixed(2)}%` + : "N/A", + ], + [ + "Underlying Price", + q.underlying_price != null + ? `$${q.underlying_price.toFixed(2)}` + : "N/A", + ], + ["Timestamp", q.timestamp ?? "N/A"] + ); + + console.log(table.toString()); + }); +} diff --git a/src/commands/trade/option/index.ts b/src/commands/trade/option/index.ts index aad0147..840cd85 100644 --- a/src/commands/trade/option/index.ts +++ b/src/commands/trade/option/index.ts @@ -5,6 +5,7 @@ import type { Balance, MlegActionStrict, MlegInstrumentType, + OptionImpact, } from "snaptrade-typescript-sdk"; import { Snaptrade, type Account } from "snaptrade-typescript-sdk"; import { generateOccSymbol } from "../../../utils/generateOccSymbol.ts"; @@ -126,7 +127,8 @@ export async function confirmTrade( orderType: string, action: string, tif: string, - balance?: Balance + balance?: Balance, + impact?: OptionImpact ) { // Section: Header console.log(chalk.bold("\n๐Ÿ“„ Trade Preview\n")); @@ -276,6 +278,29 @@ export async function confirmTrade( `${formatAmount({ value: effectivePerContract, currency })} ร— ${multiplier} multiplier ร— ${contracts} contract${contracts > 1 ? "s" : ""}` ); + // Section: Broker-provided impact estimate (if available) + if (impact) { + console.log(); + const directionLabel = + impact.cash_change_direction === "CREDIT" + ? chalk.green("CREDIT") + : impact.cash_change_direction === "DEBIT" + ? chalk.red("DEBIT") + : impact.cash_change_direction ?? "UNKNOWN"; + logLine( + "๐Ÿฆ", + "Broker Estimate", + `${formatAmount({ value: Number(impact.estimated_cash_change), currency })} ${directionLabel}` + ); + if (impact.estimated_fee_total) { + logLine( + " ", + "Est. Fees", + formatAmount({ value: Number(impact.estimated_fee_total), currency }) + ); + } + } + printDivider(); const result = await confirm({ @@ -298,17 +323,6 @@ export async function placeTrade( const { ticker, orderType, limitPrice, action, tif, account, balance } = trade; - await confirmTrade( - account, - ticker, - legs, - limitPrice, - orderType, - action, - tif, - balance - ); - const orderTypeInput = (() => { switch (orderType as (typeof ORDER_TYPES)[number]) { case "Market": @@ -333,6 +347,39 @@ export async function placeTrade( units: leg.quantity, })); + // Fetch broker-provided impact estimate (BETA โ€” may not be supported by all brokers) + let impact: OptionImpact | undefined; + try { + const impactResponse = await withDebouncedSpinner( + "Fetching order impact estimate...", + async () => + snaptrade.trading.getOptionImpact({ + ...user, + accountId: account.id, + order_type: orderTypeInput, + time_in_force: tif, + limit_price: limitPrice, + price_effect: action === "BUY" ? "DEBIT" : "CREDIT", + legs: legsInput, + }) + ); + impact = impactResponse.data; + } catch { + // Silently ignore โ€” this endpoint is not supported by all brokers + } + + await confirmTrade( + account, + ticker, + legs, + limitPrice, + orderType, + action, + tif, + balance, + impact + ); + const response = await snaptrade.trading.placeMlegOrder({ ...user, accountId: account.id, From d489e480e932adeaf06c54b440d07c6819f6d9dd Mon Sep 17 00:00:00 2001 From: Noah Lauffer Date: Wed, 11 Mar 2026 14:07:59 -0300 Subject: [PATCH 2/5] x --- src/commands/optionQuote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/optionQuote.ts b/src/commands/optionQuote.ts index c92a387..e878127 100644 --- a/src/commands/optionQuote.ts +++ b/src/commands/optionQuote.ts @@ -8,7 +8,7 @@ import { withDebouncedSpinner } from "../utils/withDebouncedSpinner.ts"; export function optionQuoteCommand(snaptrade: Snaptrade): Command { return new Command("option-quote") - .description("Get a real-time quote for an option contract") + .description("Get a quote for an option contract") .requiredOption("--ticker ", "Underlying asset symbol") .requiredOption("--exp ", "Expiration date (YYYY-MM-DD)") .requiredOption("--strike ", "Strike price") From 8ceb53d131e68c415173a2735beae60078bda3f8 Mon Sep 17 00:00:00 2001 From: Noah Lauffer Date: Thu, 12 Mar 2026 11:11:58 -0300 Subject: [PATCH 3/5] x --- src/commands/trade/option/index.ts | 156 +++++++++++++++-------------- 1 file changed, 83 insertions(+), 73 deletions(-) diff --git a/src/commands/trade/option/index.ts b/src/commands/trade/option/index.ts index 840cd85..17f5c0f 100644 --- a/src/commands/trade/option/index.ts +++ b/src/commands/trade/option/index.ts @@ -17,10 +17,8 @@ import { } from "../../../utils/preview.ts"; import { formatAmount, - formatLastQuote, - getFullQuotes, - getLastQuote, } from "../../../utils/quotes.ts"; +import type { OptionQuote } from "snaptrade-typescript-sdk"; import { selectAccount } from "../../../utils/selectAccount.ts"; import { handlePostTrade } from "../../../utils/trading.ts"; import { loadOrRegisterUser } from "../../../utils/user.ts"; @@ -120,6 +118,8 @@ export async function processCommonOptionArgs( } export async function confirmTrade( + snaptrade: Snaptrade, + user: { userId: string; userSecret: string }, account: Account, ticker: string, legs: Leg[], @@ -137,45 +137,66 @@ export async function confirmTrade( printAccountSection({ account, balance }); console.log(); - // Section: Quote for the underlying ticker - const underlyingQuote = await getLastQuote(ticker); + // Section: Fetch option quotes for each leg via SnapTrade API + const occSymbols = legs.map((leg) => + generateOccSymbol(ticker, leg.expiration, leg.strike, leg.type) + ); + const legQuotes: Record = {}; + await Promise.all( + occSymbols.map(async (symbol) => { + try { + const res = await snaptrade.options.getOptionQuote({ + ...user, + accountId: account.id, + symbol, + }); + legQuotes[symbol] = res.data; + } catch (e: any) { + if (process.argv.includes("--verbose")) { + console.error(`[option-quote] ${symbol}:`, e?.responseBody ?? e?.response?.data ?? e?.message); + } + legQuotes[symbol] = undefined; + } + }) + ); + + const currency = account.balance.total?.currency; + + // Section: Underlying quote (use underlying_price from first available option quote) + const underlyingPrice = Object.values(legQuotes).find( + (q) => q?.underlying_price != null + )?.underlying_price; logLine( "๐Ÿ“ˆ", "Underlying", - underlyingQuote ? `${ticker} ยท ${formatLastQuote(underlyingQuote)}` : ticker + underlyingPrice + ? `${ticker} ยท ${formatAmount({ value: underlyingPrice, currency })}` + : ticker ); // Section: Overall option strategy quote - const occSymbols = legs.map((leg) => - generateOccSymbol(ticker, leg.expiration, leg.strike, leg.type) - ); - const legQuotes = await getFullQuotes(occSymbols); - const currency = account.balance.total?.currency; - - // Compute combined per-contract strategy Bid/Ask and show it prominently const contracts = Math.max(1, ...legs.map((l) => l.quantity || 0)); const perLegForBand = legs.map((leg, idx) => { const q = legQuotes[occSymbols[idx]]; + const bid = q?.bid_price; + const ask = q?.ask_price; + const last = q?.last_price; const mid = - q?.bid != null && q.ask != null ? (q.bid + q.ask) / 2 : undefined; + bid != null && ask != null ? (bid + ask) / 2 : undefined; const ratio = Math.max(0, (leg.quantity || contracts) / contracts); - const bidUsed = (q?.bid ?? mid ?? q?.last ?? 0) * ratio; - const askUsed = (q?.ask ?? mid ?? q?.last ?? 0) * ratio; + const bidUsed = (bid ?? mid ?? last ?? 0) * ratio; + const askUsed = (ask ?? mid ?? last ?? 0) * ratio; const stratBid = leg.action === "BUY" ? +bidUsed : -askUsed; const stratAsk = leg.action === "BUY" ? +askUsed : -bidUsed; - const currency = q?.currency; const price = (() => { if (mid != null) return leg.action === "BUY" ? -mid : mid; - // Fallback: favor conservative side when no mid if (leg.action === "BUY") return -askUsed; return bidUsed; })(); - return { stratBid, stratAsk, currency, price }; + return { stratBid, stratAsk, price }; }); const strategyBid = perLegForBand.reduce((s, l) => s + l.stratBid, 0); const strategyAsk = perLegForBand.reduce((s, l) => s + l.stratAsk, 0); - const strategyCurrency = perLegForBand[0]?.currency; - // Display mapping: if both are negative (net credit), flip so Bid shows smaller credit (abs of ask), Ask shows larger credit (abs of bid) const bothNegative = strategyBid < 0 && strategyAsk < 0; const displayBid = bothNegative ? Math.abs(strategyAsk) @@ -186,16 +207,10 @@ export async function confirmTrade( const bidLabel = chalk.cyan("Bid"); const askLabel = chalk.magenta("Ask"); const bidStr = chalk.cyan( - formatAmount({ - value: displayBid, - currency: strategyCurrency ?? currency, - }) + formatAmount({ value: displayBid, currency }) ); const askStr = chalk.magenta( - formatAmount({ - value: displayAsk, - currency: strategyCurrency ?? currency, - }) + formatAmount({ value: displayAsk, currency }) ); logLine( "๐Ÿ’ต", @@ -214,12 +229,11 @@ export async function confirmTrade( quote: (() => { const q = legQuotes[occSymbols[idx]]; if (!q) return "Quote: N/A"; - // Highlight which side contributes to Strategy Bid (cyan) vs Strategy Ask (magenta) const bidLabel = leg.action === "BUY" ? chalk.cyan("Bid") : chalk.magenta("Bid"); const askLabel = leg.action === "BUY" ? chalk.magenta("Ask") : chalk.cyan("Ask"); - return `${bidLabel}: ${formatAmount({ value: q?.bid, currency: q?.currency })} ยท ${askLabel}: ${formatAmount({ value: q?.ask, currency: q?.currency })} ยท Last: ${formatAmount({ value: q?.last, currency: q?.currency })}`; + return `${bidLabel}: ${formatAmount({ value: q.bid_price ?? 0, currency })} ยท ${askLabel}: ${formatAmount({ value: q.ask_price ?? 0, currency })} ยท Last: ${formatAmount({ value: q.last_price ?? 0, currency })}`; })(), })); const widths = { @@ -240,9 +254,7 @@ export async function confirmTrade( r.quote.padEnd(widths.quote), ].join(" "); if (rows.length > 0) { - // First leg on the same line as the label logLine("๐Ÿงฉ", "Legs", makeLine(rows[0])); - // Subsequent legs aligned under the first leg for (let i = 1; i < rows.length; i++) { logLine(" ", "", makeLine(rows[i])); } @@ -257,30 +269,9 @@ export async function confirmTrade( currency, }); - // Section: Estimated net debit/credit for the strategy - const perContract = Math.abs( - perLegForBand.reduce((sum, l) => sum + l.price, 0) - ); - // If user provided a limit for the overall strategy, prefer that per-contract - const effectivePerContract = - orderType === "Limit" && limitPrice ? Number(limitPrice) : perContract; - - const multiplier = 100; - const total = effectivePerContract * multiplier * contracts; - logLine( - "๐Ÿ“Š", - action === "SELL" ? "Est. Credit" : "Est. Cost", - formatAmount({ value: total, currency }) - ); - logLine( - " ", - "", - `${formatAmount({ value: effectivePerContract, currency })} ร— ${multiplier} multiplier ร— ${contracts} contract${contracts > 1 ? "s" : ""}` - ); - - // Section: Broker-provided impact estimate (if available) + // Section: Estimated cost/credit if (impact) { - console.log(); + // Use broker-provided impact estimate when available const directionLabel = impact.cash_change_direction === "CREDIT" ? chalk.green("CREDIT") @@ -288,8 +279,8 @@ export async function confirmTrade( ? chalk.red("DEBIT") : impact.cash_change_direction ?? "UNKNOWN"; logLine( - "๐Ÿฆ", - "Broker Estimate", + "๐Ÿ“Š", + impact.cash_change_direction === "CREDIT" ? "Est. Credit" : "Est. Cost", `${formatAmount({ value: Number(impact.estimated_cash_change), currency })} ${directionLabel}` ); if (impact.estimated_fee_total) { @@ -299,6 +290,25 @@ export async function confirmTrade( formatAmount({ value: Number(impact.estimated_fee_total), currency }) ); } + } else { + // Fallback: manual estimate from quotes + const perContract = Math.abs( + perLegForBand.reduce((sum, l) => sum + l.price, 0) + ); + const effectivePerContract = + orderType === "Limit" && limitPrice ? Number(limitPrice) : perContract; + const multiplier = 100; + const total = effectivePerContract * multiplier * contracts; + logLine( + "๐Ÿ“Š", + action === "SELL" ? "Est. Credit" : "Est. Cost", + formatAmount({ value: total, currency }) + ); + logLine( + " ", + "", + `${formatAmount({ value: effectivePerContract, currency })} ร— ${multiplier} multiplier ร— ${contracts} contract${contracts > 1 ? "s" : ""}` + ); } printDivider(); @@ -350,25 +360,25 @@ export async function placeTrade( // Fetch broker-provided impact estimate (BETA โ€” may not be supported by all brokers) let impact: OptionImpact | undefined; try { - const impactResponse = await withDebouncedSpinner( - "Fetching order impact estimate...", - async () => - snaptrade.trading.getOptionImpact({ - ...user, - accountId: account.id, - order_type: orderTypeInput, - time_in_force: tif, - limit_price: limitPrice, - price_effect: action === "BUY" ? "DEBIT" : "CREDIT", - legs: legsInput, - }) - ); + const impactResponse = await snaptrade.trading.getOptionImpact({ + ...user, + accountId: account.id, + order_type: orderTypeInput, + time_in_force: tif, + limit_price: limitPrice, + price_effect: action === "BUY" ? "DEBIT" : "CREDIT", + legs: legsInput, + }); impact = impactResponse.data; - } catch { - // Silently ignore โ€” this endpoint is not supported by all brokers + } catch (e: any) { + if (process.argv.includes("--verbose")) { + console.error("[option-impact]:", e?.responseBody ?? e?.response?.data ?? e?.message); + } } await confirmTrade( + snaptrade, + user, account, ticker, legs, From bac3c6e92696a3822865564ae277c37b258655b1 Mon Sep 17 00:00:00 2001 From: Noah Lauffer Date: Fri, 13 Mar 2026 15:43:32 -0300 Subject: [PATCH 4/5] x --- src/commands/index.ts | 4 -- src/commands/optionImpact.ts | 135 ----------------------------------- src/commands/optionQuote.ts | 104 --------------------------- 3 files changed, 243 deletions(-) delete mode 100644 src/commands/optionImpact.ts delete mode 100644 src/commands/optionQuote.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index b6dc2f8..ab9d21f 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -16,8 +16,6 @@ import { ordersCommand } from "./orders.ts"; import { instrumentsCommand } from "./instruments.ts"; import { profilesCommand } from "./profiles.ts"; import { mcpCommand } from "./mcp.ts"; -import { optionQuoteCommand } from "./optionQuote.ts"; -import { optionImpactCommand } from "./optionImpact.ts"; export function registerCommands(program: Command, snaptrade: Snaptrade): void { program.addCommand(statusCommand(snaptrade)); @@ -32,8 +30,6 @@ export function registerCommands(program: Command, snaptrade: Snaptrade): void { program.addCommand(ordersCommand(snaptrade)); program.addCommand(instrumentsCommand(snaptrade)); program.addCommand(quoteCommand(snaptrade)); - program.addCommand(optionQuoteCommand(snaptrade)); - program.addCommand(optionImpactCommand(snaptrade)); program.addCommand(tradeCommand(snaptrade)); program.addCommand(cancelOrderCommand(snaptrade)); program.addCommand(mcpCommand(snaptrade)); diff --git a/src/commands/optionImpact.ts b/src/commands/optionImpact.ts deleted file mode 100644 index bccc916..0000000 --- a/src/commands/optionImpact.ts +++ /dev/null @@ -1,135 +0,0 @@ -import chalk from "chalk"; -import { Command } from "commander"; -import type { MlegActionStrict, MlegInstrumentType, TimeInForceStrict } from "snaptrade-typescript-sdk"; -import { Snaptrade } from "snaptrade-typescript-sdk"; -import { generateOccSymbol } from "../utils/generateOccSymbol.ts"; -import { selectAccount } from "../utils/selectAccount.ts"; -import { loadOrRegisterUser } from "../utils/user.ts"; -import { withDebouncedSpinner } from "../utils/withDebouncedSpinner.ts"; - -export function optionImpactCommand(snaptrade: Snaptrade): Command { - return new Command("option-impact") - .description( - "Simulate an option trade and see estimated cash change and fees" - ) - .requiredOption("--ticker ", "Underlying asset symbol") - .requiredOption("--exp ", "Expiration date (YYYY-MM-DD)") - .requiredOption("--strike ", "Strike price") - .requiredOption("--type ", "Option type: call or put", (input) => { - const normalized = input.toLowerCase(); - if (normalized !== "call" && normalized !== "put") { - console.error('Invalid option type. Must be "call" or "put".'); - process.exit(1); - } - return normalized; - }) - .requiredOption("--action ", "BUY or SELL", (input) => { - const normalized = input.toUpperCase(); - if (normalized !== "BUY" && normalized !== "SELL") { - console.error('Invalid action. Must be "BUY" or "SELL".'); - process.exit(1); - } - return normalized; - }) - .option("--contracts ", "Number of contracts", "1") - .option( - "--orderType ", - "Order type: Market, Limit, Stop, StopLimit", - "Market" - ) - .option("--limitPrice ", "Limit price (required for Limit orders)") - .option("--tif ", "Time in force: Day or GTC", "Day") - .action(async (opts, command) => { - const user = await loadOrRegisterUser(snaptrade); - const account = await selectAccount({ - snaptrade, - useLastAccount: command.parent.opts().useLastAccount, - context: "option_trade", - }); - - const { ticker, exp, strike, type, action, contracts, orderType, limitPrice, tif } = - opts as { - ticker: string; - exp: string; - strike: string; - type: "call" | "put"; - action: "BUY" | "SELL"; - contracts: string; - orderType: string; - limitPrice?: string; - tif: string; - }; - - const optionType = type === "call" ? "CALL" : "PUT"; - const symbol = generateOccSymbol(ticker, exp, Number(strike), optionType); - const units = parseInt(contracts); - - const orderTypeInput = (() => { - switch (orderType) { - case "Market": - return "MARKET"; - case "Limit": - return "LIMIT"; - case "Stop": - return "STOP_LOSS_MARKET"; - case "StopLimit": - return "STOP_LOSS_LIMIT"; - default: - console.error( - `Invalid order type "${orderType}". Allowed: Market, Limit, Stop, StopLimit` - ); - process.exit(1); - } - })(); - - const response = await withDebouncedSpinner( - "Fetching option impact estimate...", - async () => - snaptrade.trading.getOptionImpact({ - ...user, - accountId: account.id, - order_type: orderTypeInput, - time_in_force: tif as TimeInForceStrict, - limit_price: limitPrice, - price_effect: action === "BUY" ? "DEBIT" : "CREDIT", - legs: [ - { - instrument: { - instrument_type: "OPTION" as MlegInstrumentType, - symbol, - }, - action: `${action}_TO_OPEN` as MlegActionStrict, - units, - }, - ], - }) - ); - - const impact = response.data; - - console.log(chalk.bold("\n๐Ÿ“Š Option Impact Estimate\n")); - console.log(` Symbol: ${symbol}`); - console.log(` Action: ${action === "BUY" ? chalk.green(action) : chalk.red(action)}`); - console.log(` Contracts: ${units}`); - console.log(` Order Type: ${orderType}`); - if (limitPrice) { - console.log(` Limit Price: $${limitPrice}`); - } - console.log(); - - const directionLabel = - impact.cash_change_direction === "CREDIT" - ? chalk.green("CREDIT") - : impact.cash_change_direction === "DEBIT" - ? chalk.red("DEBIT") - : impact.cash_change_direction ?? "UNKNOWN"; - - console.log( - ` Cash Change: $${Number(impact.estimated_cash_change).toFixed(2)} ${directionLabel}` - ); - console.log( - ` Est. Fees: $${Number(impact.estimated_fee_total).toFixed(2)}` - ); - console.log(); - }); -} diff --git a/src/commands/optionQuote.ts b/src/commands/optionQuote.ts deleted file mode 100644 index e878127..0000000 --- a/src/commands/optionQuote.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Command } from "commander"; -import { Snaptrade } from "snaptrade-typescript-sdk"; -import Table from "cli-table3"; -import { generateOccSymbol } from "../utils/generateOccSymbol.ts"; -import { selectAccount } from "../utils/selectAccount.ts"; -import { loadOrRegisterUser } from "../utils/user.ts"; -import { withDebouncedSpinner } from "../utils/withDebouncedSpinner.ts"; - -export function optionQuoteCommand(snaptrade: Snaptrade): Command { - return new Command("option-quote") - .description("Get a quote for an option contract") - .requiredOption("--ticker ", "Underlying asset symbol") - .requiredOption("--exp ", "Expiration date (YYYY-MM-DD)") - .requiredOption("--strike ", "Strike price") - .requiredOption("--type ", "Option type: call or put", (input) => { - const normalized = input.toLowerCase(); - if (normalized !== "call" && normalized !== "put") { - console.error('Invalid option type. Must be "call" or "put".'); - process.exit(1); - } - return normalized; - }) - .action(async (opts, command) => { - const user = await loadOrRegisterUser(snaptrade); - const account = await selectAccount({ - snaptrade, - useLastAccount: command.parent.opts().useLastAccount, - context: "option_trade", - }); - - const { ticker, exp, strike, type } = opts as { - ticker: string; - exp: string; - strike: string; - type: "call" | "put"; - }; - - const optionType = type === "call" ? "CALL" : "PUT"; - const symbol = generateOccSymbol(ticker, exp, Number(strike), optionType); - - const response = await withDebouncedSpinner( - "Fetching option quote...", - async () => - snaptrade.options.getOptionQuote({ - ...user, - accountId: account.id, - symbol, - }) - ); - - const q = response.data; - - const table = new Table({ - head: ["Field", "Value"], - }); - - table.push( - ["Symbol", q.symbol ?? symbol], - [ - "Bid", - q.bid_price != null - ? `$${q.bid_price.toFixed(2)} x${q.bid_size ?? "โ€”"}` - : "N/A", - ], - [ - "Ask", - q.ask_price != null - ? `$${q.ask_price.toFixed(2)} x${q.ask_size ?? "โ€”"}` - : "N/A", - ], - [ - "Last", - q.last_price != null - ? `$${q.last_price.toFixed(2)} x${q.last_size ?? "โ€”"}` - : "N/A", - ], - [ - "Open Interest", - q.open_interest != null - ? q.open_interest.toLocaleString("en-US") - : "N/A", - ], - [ - "Volume", - q.volume != null ? q.volume.toLocaleString("en-US") : "N/A", - ], - [ - "Implied Volatility", - q.implied_volatility != null - ? `${(q.implied_volatility * 100).toFixed(2)}%` - : "N/A", - ], - [ - "Underlying Price", - q.underlying_price != null - ? `$${q.underlying_price.toFixed(2)}` - : "N/A", - ], - ["Timestamp", q.timestamp ?? "N/A"] - ); - - console.log(table.toString()); - }); -} From 62eadce56fa21ecc65e9200294100e492767a577 Mon Sep 17 00:00:00 2001 From: Noah Lauffer Date: Wed, 18 Mar 2026 12:57:15 -0300 Subject: [PATCH 5/5] x --- src/commands/trade/option/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/commands/trade/option/index.ts b/src/commands/trade/option/index.ts index 17f5c0f..f7adafa 100644 --- a/src/commands/trade/option/index.ts +++ b/src/commands/trade/option/index.ts @@ -271,23 +271,25 @@ export async function confirmTrade( // Section: Estimated cost/credit if (impact) { - // Use broker-provided impact estimate when available - const directionLabel = - impact.cash_change_direction === "CREDIT" - ? chalk.green("CREDIT") - : impact.cash_change_direction === "DEBIT" - ? chalk.red("DEBIT") - : impact.cash_change_direction ?? "UNKNOWN"; + const estCash = Number(impact.estimated_cash_change); + const estFees = Number(impact.estimated_fee_total ?? 0); + const isCredit = impact.cash_change_direction === "CREDIT"; logLine( "๐Ÿ“Š", - impact.cash_change_direction === "CREDIT" ? "Est. Credit" : "Est. Cost", - `${formatAmount({ value: Number(impact.estimated_cash_change), currency })} ${directionLabel}` + isCredit ? "Est. Credit" : "Est. Cost", + formatAmount({ value: estCash, currency }) ); - if (impact.estimated_fee_total) { + if (estFees) { logLine( " ", "Est. Fees", - formatAmount({ value: Number(impact.estimated_fee_total), currency }) + formatAmount({ value: estFees, currency }) + ); + const total = isCredit ? estCash - estFees : estCash + estFees; + logLine( + " ", + isCredit ? "Net Credit" : "Total Cost", + formatAmount({ value: total, currency }) ); } } else {