DEV Community

Agent Risk Desk
Agent Risk Desk

Posted on

Add a pre-trade check to an Alpaca agent in about 20 lines

If you let an agent place orders on Alpaca, the agent decides the size. Alpaca will reject an order you can't pay for. It won't tell you that one order puts 60% of the account in one stock, or that the stop is so far away a bad day costs 5% of equity.

So I put a check in front of submit_order. The agent builds the order, the check looks at the account and the order, and only an approved order goes out.

The function

This uses alpaca-py for the account and positions and my Agent Financial Advisor API for the check. It works on paper keys.

import os, requests

AFA = "https://agent-financial-advisor.agent-financial-advisor.workers.dev/v1/tools/pre_trade_check"

def checked_order(trading, symbol, side, qty, stop=None, last_price=None):
    acct = trading.get_account()
    positions = [{"symbol": p.symbol, "quantity": float(p.qty), "market_price": float(p.current_price),
                  "avg_cost": float(p.avg_entry_price)} for p in trading.get_all_positions()]
    body = {
        "account": {"type": "margin" if float(acct.multiplier) > 1 else "cash", "equity": float(acct.equity),
                    "cash": float(acct.cash), "buying_power": float(acct.buying_power), "positions": positions},
        "order": {"symbol": symbol, "side": side, "quantity": qty, "order_type": "market",
                  **({"stop_loss_price": stop} if stop else {})},
        **({"market": {"last_price": last_price}} if last_price else {}),
    }
    key = os.environ.get("AFA_API_KEY")
    r = requests.post(AFA, json=body, headers={"authorization": f"Bearer {key}"} if key else {}, timeout=10)
    r.raise_for_status()
    result = r.json()
    for c in result["checks"]:
        if c["status"] != "pass":
            print(f"{c['status']}: {c['id']}: {c['message']}")
    return result
Enter fullscreen mode Exit fullscreen mode

trading is a normal TradingClient(key, secret, paper=True). The multiplier tells you cash or margin. Alpaca reports 1 for a cash account.

Using it

result = checked_order(trading, "NVDA", "buy", 40, stop=170, last_price=185)
if result["decision"] != "reject":
    trading.submit_order(MarketOrderRequest(symbol="NVDA", qty=40, side=OrderSide.BUY,
                                            time_in_force=TimeInForce.DAY))
Enter fullscreen mode Exit fullscreen mode

decision is approve, approve_with_warnings or reject. I let warnings through and log them. You could be stricter.

What it caught

I ran it against a small test account: cash account, $12,000 equity, $3,000 cash, 30 shares of AAPL. The agent wants 40 NVDA at $185 with a stop at $170. Here's what came back (trimmed):

{
  "decision": "reject",
  "summary": "REJECT: 3 failed, 0 warning(s), 2 passed.",
  "suggested_quantity": 12,
  "checks": [
    {"id": "market_hours", "status": "pass", "message": "Regular trading session."},
    {"id": "buying_power", "status": "fail", "message": "Cost $7400 exceeds cash $3000."},
    {"id": "leverage", "status": "pass", "message": "Gross exposure after trade 1.19x equity."},
    {"id": "concentration", "status": "fail", "message": "NVDA would be 61.7% of equity (limit 20%). Max additional quantity: 12."},
    {"id": "risk_per_trade", "status": "fail", "message": "Risk to stop $600 = 5% of equity (limit 2%). Quantity for limit: 16."}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Three problems in one order. Alpaca would have caught the first one. The other two would have gone through.

It also suggests a size, 12 shares. I don't send that blindly. I run the check again with 12:

{
  "decision": "approve",
  "summary": "APPROVE: 0 failed, 0 warning(s), 5 passed.",
  "checks": [
    {"id": "buying_power", "status": "pass", "message": "Cost $2220 within settled cash $3000."},
    {"id": "concentration", "status": "pass", "message": "NVDA would be 18.5% of equity."},
    {"id": "risk_per_trade", "status": "pass", "message": "Risk to stop $180 = 1.5% of equity."}
  ]
}
Enter fullscreen mode Exit fullscreen mode

That one goes out.

Why bother

A model doesnt hold an account's state in its head between turns. It will size a trade off whatever number it last saw, or off nothing. A check like this reads the live account every time and gives a yes or no with reasons, so the agent can't talk itself past it.

Same inputs give the same answer, and every check says which rule it applied. That makes the logs easy to read when you go back and ask why an order didn't go out.

Limits and setup

  • No signup for the first 5 calls a day. POST /v1/keys gives a free key with 25 calls a day. Put it in AFA_API_KEY.
  • The limits above (20% concentration, 2% risk to stop) are defaults. Pass a policy object to change them, for example {"max_position_pct": 0.1}.
  • It doesn't replace the broker's own checks. It runs before them.
  • It's rule-based analysis for software, not investment advice.

The full script, with argument parsing and the submit step, is in the repo: alpaca_pre_trade_guard.py. If your agent talks MCP instead, the server is at https://agent-financial-advisor.agent-financial-advisor.workers.dev/mcp and the tool is pre_trade_check.

Disclosure: I built this. If there's a check you want your agent to run that isn't in there, tell me in the comments.

Top comments (0)