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):
sideoutside{"buy", "sell"}→ValueError.quantity < 1→ValueError.- Any of the options triplet (
option_type,strike,expiration) supplied implies all three required →ValueErrorif partial. option_typeoutside{"call", "put"}→ValueError.- Symbol is upper-cased; side is lower-cased.
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
- 60-second lock. The price is captured at plan time and held for exactly 60 seconds.
execute_trade(trade_id, "confirm")after the window returns an error; the operator mustplan_tradeagain to re-quote. - No cache. Mutation tool — every call hits the backend. The tool also invalidates the
portfolio_statuscache so the freshly-planned trade shows up in the nextportfolio_statusread. - Single-claim semantics. The backend’s confirm path uses
BEGIN IMMEDIATE+ a status guard so two concurrentexecute_tradecalls cannot both succeed against the sametrade_id. - Market hours enforced. The backend rejects plan calls outside RTH (Mon–Fri 09:30–16:00 ET); the tool surfaces that as an error.
- Auth required. Both backend routes (
POST /api/v1/trading/tradesfor stocks,POST /api/v1/trading/trades/optionsfor options) require the bearer token.
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
- Operator has approved a trade and you’re ready to lock a price.
- You want to show the operator the exact quoted price before committing.
- Either step of the two-step “plan → confirm” flow.
When NOT to use
- Market is closed (the backend will reject; check first via
market_pulseor/api/v1/trading/market/status). - You haven’t checked conditions yet (run
market_pulseandportfolio_statusfirst — every trade should be a deliberate action with context). - You only want a price display — use
get_quoteinstead;plan_tradereserves cash / buying power.
See also
execute_trade— the second half of the flow.portfolio_status— confirm cash + exposure before planning.get_quote— non-reserving price check.option_chain— pick a strike / expiration before planning an option trade.- Implementation:
mcp/mcp_server/tools/trading.py:plan_trade, backend routesPOST /api/v1/trading/tradesandPOST /api/v1/trading/trades/options.