Skip to content

KB / mcp

MCP tool: plan_trade

Last verified

plan_trade is the first half of the two-step trade flow. It quotes a price (real Schwab pricing), locks it for 60 seconds, and returns a trade_id that execute_trade consumes. It does NOT execute the trade — without a matching execute_trade call within 60 seconds, the lock expires and the trade is silently cancelled. This is by design: every position change is two deliberate calls, never one.

Signature

plan_trade(
  symbol:     str,                       # ticker, e.g. "AAPL"
  side:       str,                       # "buy" or "sell"
  quantity:   int,                       # shares (stocks) or contracts (options); >= 1
  option_type: str | None = None,        # "call" or "put"   ─┐
  strike:      float | None = None,      # strike price       ├─ options-only triplet
  expiration:  str | None = None,        # "YYYY-MM-DD"       ─┘
) -> str                                 # JSON-serialised dict

Validation (mcp/mcp_server/tools/trading.py:plan_trade):

The options-triplet detection (is_option = any(v is not None for v in (option_type, strike, expiration))) is what splits the call between the stocks route and the options route. Pass zero option fields for a stock trade; pass all three for an option trade. Anything in between is a ValueError.

Returns

Stock plan

{
  "trade_id": "trd-abc123",
  "asset_type": "stock",
  "symbol": "AAPL",
  "side": "buy",
  "quantity": 10,
  "price": 215.40,
  "total": 2154.00,
  "expires_at": "2026-05-26T14:15:33+00:00",
  "status": "pending"
}

Option plan

{
  "trade_id": "trd-def456",
  "asset_type": "option",
  "symbol": "SPY",
  "side": "buy",
  "quantity": 2,
  "option_type": "call",
  "strike": 540.0,
  "expiration": "2026-06-20",
  "price": 4.25,
  "total": 850.00,
  "expires_at": "2026-05-26T14:15:33+00:00",
  "status": "pending"
}

total = price × quantity × multiplier (1 for stocks, 100 for options — option contracts cover 100 shares). expires_at is UTC ISO 8601 with offset for TZ-independent SQL comparison against _utc_now_iso() (see app/trading/routes.py).

Behaviour

Examples

Buy 10 shares of AAPL

Agent: plan_trade(symbol="AAPL", side="buy", quantity=10)
  -> trade_id "trd-abc123", price 215.40, expires_at <UTC 60s out>
Agent: execute_trade(trade_id="trd-abc123", action="confirm")
  -> filled at 215.40, new balance, etc.

Buy 2 SPY 540 calls expiring 2026-06-20

Agent: plan_trade(
  symbol="SPY",
  side="buy",
  quantity=2,
  option_type="call",
  strike=540.0,
  expiration="2026-06-20"
)
  -> trade_id "trd-def456", price 4.25, total 850.00
Agent: execute_trade(trade_id="trd-def456", action="confirm")

Cancel rather than confirm

Agent: plan_trade(symbol="AAPL", side="buy", quantity=10)
  -> trade_id "trd-abc123"
Operator: "Actually, wait."
Agent: execute_trade(trade_id="trd-abc123", action="cancel")

Partial-options-triplet rejection

Agent: plan_trade(
  symbol="SPY", side="buy", quantity=2,
  option_type="call"            # strike + expiration missing
)
-> ValueError("Option trades require all of: option_type, strike, expiration.")

When to use

When NOT to use

See also