Updated 18 October 2026. A complete Python paper trading bot that asks Jev once a day and trades through Alpaca's paper API, checked against alpaca-py 0.44.0 and typesafe-sdk 0.7.5.
A Jev trading bot with Alpaca is a short Python script. Once a day it pulls daily bars from Alpaca's market data API, turns them into labels, asks Jev one typed question, applies your threshold and risk rules in code, and sends orders to Alpaca's paper trading endpoint, paper-api.alpaca.markets. Jev never touches an order. Alpaca says paper trading is free, and new paper accounts start with $100,000 of simulated money by default.
Below: the architecture, setup, the full bot, scheduling, logging, a kill switch and a go-live checklist. This is educational code, not financial advice. Run it on paper for weeks, expect live results to differ, and treat no bot as a promise of returns.
The Jev Alpaca bot at a glance
| Part | This guide | Checked detail |
|---|---|---|
| Broker | Alpaca Trading API, paper account | TradingClient(key, secret, paper=True); paper keys differ from live keys |
| SDKs | alpaca-py 0.44.0 and typesafe-sdk 0.7.5 | Python 3.10 or later |
| Data | Completed daily bars for SPY | Free Basic plan; paper-only accounts get IEX data |
| Decision | Jev choice (long or flat) plus a stress check | Pinned to jev-1.13.0 |
| Orders | Market buy with an attached stop loss (OTO), good till canceled | The stop sits at the broker |
| Sizing | 10 percent of equity, whole shares | A rule in code, never a model output |
| Schedule | Every 15 minutes on weekdays, one decision a day | Alpaca's market clock decides when |
| Safety | Kill file, 2 percent daily loss limit, account checks | close_all_positions(cancel_orders=True) |
How the bot works
- Schedule: cron starts the script every 15 minutes on weekdays.
- Safety: a kill file, a blocked account or a daily loss over the limit stops everything first.
- Clock: act only when Alpaca says the market is open, once per day.
- State: completed daily bars become four labels, with the same code as our Jev backtest in Python.
- Decision: Jev answers; code applies the threshold.
- Orders: buy with a stop attached, or close the position.
- Log: one JSON line per decision, with the model version.
This split follows the rule from how to build a Jev AI trading bot: Jev judges, code executes.
Step 1: Accounts, keys and environment variables
Anyone can open an Alpaca paper-only account with an email address. Paper accounts have their own API keys; live credentials do not work on the paper API, or the other way round. Create a TypeSafe key in the TypeSafe console, and keep all three secrets in environment variables, never in code or Git:
export APCA_API_KEY_ID="your-paper-key-id"
export APCA_API_SECRET_KEY="your-paper-secret-key"
export TYPESAFE_API_KEY="your-typesafe-key"
python -m pip install "alpaca-py==0.44.0" "typesafe-sdk==0.7.5" pandas numpy
The TypeSafe SDK reads TYPESAFE_API_KEY itself; the bot passes the two Alpaca variables to its clients. alpaca-py 0.44.0 (11 August 2026) added client-side checks of order prices, flagged as a breaking change, so pin the versions you tested. For secrets on a server, see how to secure a web application.
Step 2: The full bot
Save this as bot.py. The feature and label functions match the backtest guide exactly, so the bot trades the strategy you tested:
import json
import math
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
import numpy as np
import pandas as pd
from alpaca.data.enums import Adjustment
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest, StockLatestTradeRequest
from alpaca.data.timeframe import TimeFrame
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderClass, OrderSide, QueryOrderStatus, TimeInForce
from alpaca.trading.requests import GetOrdersRequest, MarketOrderRequest, StopLossRequest
from typesafe_sdk import Choice, Noul, TypeSafeClient
SYMBOL = "SPY"
MODEL = "jev-1.13.0" # pinned: change only after testing a new version
THRESHOLD = 0.4 # from your walk-forward test, not a recommendation
POSITION_FRACTION = 0.10 # share of equity per position
STOP_PCT = 0.05 # broker-side stop 5% below the entry price
MAX_DAILY_LOSS = 0.02 # flatten and stop if equity drops 2% in a day
LOG, KILL = Path("decisions.jsonl"), Path("KILL")
SPEC = {
"position": {
"instructions": "Should a long-only daily trend strategy hold the index for the next session?",
"criteria": {"long": "Hold a long position", "flat": "Stay in cash"},
},
"stress": {"instructions": "Does the state describe a stressed, disorderly market?"},
}
KEY, SECRET = os.environ["APCA_API_KEY_ID"], os.environ["APCA_API_SECRET_KEY"]
trading = TradingClient(KEY, SECRET, paper=True)
data = StockHistoricalDataClient(KEY, SECRET)
jev = TypeSafeClient(model=MODEL) # reads TYPESAFE_API_KEY
def log(record):
record["logged_at"] = datetime.now(timezone.utc).isoformat()
with LOG.open("a") as f:
f.write(json.dumps(record, default=str) + "\n")
def decided_today(day):
lines = LOG.read_text().splitlines() if LOG.exists() else []
return any(json.loads(line).get("day") == day for line in lines)
def add_features(df):
out = df.copy()
close = out["close"]
out["sma_50"] = close.rolling(50).mean()
out["sma_200"] = close.rolling(200).mean()
out["ret_20"] = close.pct_change(20)
out["vol_20"] = close.pct_change().rolling(20).std() * np.sqrt(252)
out["vol_ref"] = out["vol_20"].rolling(252).median()
out["dd_252"] = close / close.rolling(252).max() - 1
return out.dropna()
def to_state(row):
momentum = "rising" if row.ret_20 > 0.03 else ("falling" if row.ret_20 < -0.03 else "flat")
volatility = "high" if row.vol_20 > 1.25 * row.vol_ref else ("low" if row.vol_20 < 0.8 * row.vol_ref else "normal")
drawdown = "near the 1-year high" if row.dd_252 > -0.05 else ("moderate pullback" if row.dd_252 > -0.15 else "deep drawdown")
return {"long_term_trend": "up" if row.sma_50 > row.sma_200 else "down", "momentum_20d": momentum,
"volatility_vs_past_year": volatility, "drawdown": drawdown}
def ask_jev(state):
r = jev.system_one(state=state, questions={"position": Choice(**SPEC["position"]), "stress": Noul(**SPEC["stress"])})
p = r.choices["position"]
return {"model": r.model, "choice": p.choice, "confidence": p.confidence, "stress": r.nouls["stress"].noul}
def main():
if KILL.exists():
trading.close_all_positions(cancel_orders=True)
log({"event": "kill switch on: orders canceled, positions closed"})
return
clock = trading.get_clock()
day = clock.timestamp.date().isoformat()
if not clock.is_open or decided_today(day):
return
account = trading.get_account()
if account.trading_blocked or account.account_blocked:
log({"event": "account blocked"})
return
equity, last_equity = float(account.equity), float(account.last_equity)
if equity < last_equity * (1 - MAX_DAILY_LOSS):
trading.close_all_positions(cancel_orders=True)
KILL.touch()
log({"event": "daily loss limit hit: flattened, kill switch on", "equity": equity})
return
midnight = datetime(clock.timestamp.year, clock.timestamp.month, clock.timestamp.day)
bars = data.get_stock_bars(StockBarsRequest(
symbol_or_symbols=SYMBOL, timeframe=TimeFrame.Day, start=midnight - timedelta(days=550),
end=midnight, adjustment=Adjustment.ALL)).df.loc[SYMBOL]
feats = add_features(bars)
if (clock.timestamp.date() - feats.index[-1].date()).days > 5:
log({"event": "stale data", "last_bar": feats.index[-1]})
return
state = to_state(feats.iloc[-1])
try:
answer = ask_jev(state)
except Exception as error: # fail closed: no answer, no new entry
log({"event": "jev error", "error": repr(error)})
return
want_long = answer["choice"] == "long" and answer["confidence"] >= THRESHOLD and answer["stress"] < 0.5
held = {p.symbol for p in trading.get_all_positions()}
pending = trading.get_orders(filter=GetOrdersRequest(status=QueryOrderStatus.OPEN, symbols=[SYMBOL]))
record = {"day": day, "state": state, "answer": answer, "want_long": want_long, "equity": equity}
try:
if want_long and SYMBOL not in held and not pending:
price = data.get_stock_latest_trade(StockLatestTradeRequest(symbol_or_symbols=SYMBOL))[SYMBOL].price
qty = math.floor(equity * POSITION_FRACTION / price)
if qty >= 1:
order = trading.submit_order(MarketOrderRequest(
symbol=SYMBOL, qty=qty, side=OrderSide.BUY, time_in_force=TimeInForce.GTC,
order_class=OrderClass.OTO, stop_loss=StopLossRequest(stop_price=round(price * (1 - STOP_PCT), 2)),
client_order_id="jev-" + day + "-" + SYMBOL))
record.update(order_id=order.id, qty=qty, entry_ref=price)
elif not want_long and SYMBOL in held:
trading.close_all_positions(cancel_orders=True)
record["closed"] = True
except Exception as error: # a rejected order is logged, not retried today
record["order_error"] = repr(error)
log(record)
if __name__ == "__main__":
main()
Step 3: Market data
The bot requests daily bars ending at midnight UTC today, so it sees only completed sessions, and it skips trading if the newest bar is more than five days old. Without a feed setting, Alpaca uses the best feed your plan allows. The free Basic plan gives real-time stock data from IEX only, and consolidated (SIP) history must end at least 15 minutes ago. Algo Trader Plus, $99 a month as of 18 October 2026, adds all US exchanges; a once-a-day bot does not need it.
Step 4: The Jev decision and position sizing
The policy is three conditions: Jev chooses "long", its confidence is at least the threshold, and the stress answer is below 0.5. The threshold of 0.4 is a placeholder; set yours with the walk-forward test in the backtest guide, and check calibration with Jev confidence scores explained.
Sizing never comes from the model. The bot puts 10 percent of equity into whole shares at the latest trade price. If the Jev call fails or times out, the bot logs the error and makes no new entry, while the stop already at the broker keeps protecting any open position.
Step 5: Orders, stops and Alpaca's rules
- Entry: a market buy with
order_class=OrderClass.OTOand astop_loss, so the stop is placed with the entry and works without the bot. These orders take a time in force of day or GTC. - Stop price: 5 percent below the latest trade, in cents. Alpaca rejects a stop-loss less than $0.01 below the market price; the bot logs the rejection and does not retry that day.
- Exit:
close_all_positions(cancel_orders=True)cancels the stop and closes the position in one call, which suits a one-symbol bot. - No double entries: the bot checks positions and open orders before it buys.
- GTC expiry: Alpaca cancels GTC orders 90 days after creation, so a longer hold needs a fresh stop.
- Wash trades: Alpaca rejects orders that could trade against your own open orders with HTTP 403, in paper too, and recommends bracket or OCO orders for a take-profit and stop-loss pair.
Paper fills are simulated: an order fills only when it becomes marketable, its size is not checked against the quoted size, and Alpaca applies a random partial fill 10 percent of the time.
Step 6: Scheduling
Alpaca's clock says whether the market is open right now, so holidays need no special code. On a Linux server set to UTC, this cron line runs the bot every 15 minutes across both the summer and winter opening times:
*/15 13-20 * * 1-5 cd /opt/jev-bot && ./run.sh >> cron.log 2>&1
Cron jobs start with a bare environment, so run.sh should load the keys from a file only your user can read, then start Python from the project's virtual environment. A small VPS is plenty; see VPS, serverless or managed hosting.
Step 7: Logging
Each decision becomes one JSON line in decisions.jsonl: the day, the state, Jev's answer with the versioned model ID, the decision, equity and any order ID. Errors and kill switch events get their own lines. This log is how you check calibration, find out whether Jev helped at all, and spot the day a model update changed behavior.
Step 8: The kill switch
- Manual: create a file named
KILLnext to the script. On its next run the bot cancels all orders, closes all positions and does nothing else until a person deletes the file. - Automatic: if equity falls more than 2 percent below the previous day's closing equity, the bot flattens and creates the
KILLfile itself. - Account flags: if Alpaca marks the account as blocked, the bot logs it and stops.
Checklist before going live
- Paper trade for weeks; a daily bot makes only about 20 decisions a month.
- Compare paper results with the backtest: trade count, fills and drawdown. Explain every gap.
- Test every exit: a triggered stop, the kill file, the daily loss limit and a Jev outage.
- Remember what paper leaves out: Alpaca lists market impact, information leakage, slippage due to latency, queue position, price improvement, regulatory fees and dividends.
- Keep live keys apart from paper keys.
TradingClientdefaults topaper=True, so going live needs a deliberate change. - Pin the Jev version and plan for updates; see what changed in Jev 1.14.
- Check the trading rules for your account. Alpaca's docs explain that FINRA is replacing the pattern day trader rule with an intraday margin rule, with a 12-month transition after SEC approval.
- Set alerts for errors, rejected orders and kill switch events.
- Start with the smallest size you can, and write down in advance what would make you stop.
If you want a Jev tool like this built and tested with proper logs and risk controls, talk to us.
Jev Alpaca trading bot FAQ
Is Alpaca paper trading free?
Yes. Alpaca says paper trading is free and available to all of its users, and anyone in the world can open a paper-only account with an email address. New paper accounts start with $100,000 of simulated money by default, and paper-only accounts get IEX market data. Fills are simulated from live quotes, so results will differ from live trading.
Can Jev place trades on Alpaca by itself?
No. Jev is a decision model that returns typed answers with probabilities, and it has no connection to any broker. Your code reads the answer, applies thresholds and risk rules, and calls Alpaca's API to place or cancel orders. Keeping orders in code means a stop loss or kill switch still works when the Jev API is down.
What is the Alpaca paper trading API URL?
The paper Trading API lives at paper-api.alpaca.markets, while market data comes from data.alpaca.markets for paper and live accounts alike. With alpaca-py you do not type the URL: create TradingClient with paper set to true and pass your paper keys. Live keys do not work with the paper API, and paper keys do not work with the live one.
Do I need a paid market data plan for this bot?
No. A once-a-day bot works on Alpaca's free Basic plan, which includes historical stock data since 2016 and real-time IEX data. Consolidated SIP history is available without a subscription when the request ends at least 15 minutes ago, though paper-only accounts are entitled to IEX data. Algo Trader Plus costs $99 a month as of 18 October 2026 and adds real-time data from all US exchanges.
How often should a Jev trading bot run?
As often as your strategy needs and no more. This bot decides once a day, which matches a daily backtest and keeps API costs near zero. TypeSafe quotes 70 to 500 milliseconds per Jev call, so faster loops are possible, but each needs its own backtest, rate limit planning and much tighter risk controls before it touches money.
How do I stop the bot in an emergency?
Create the KILL file next to the script. On its next run the bot calls close_all_positions with cancel_orders set to true, which cancels every open order and closes every position, then does nothing until you delete the file. You can also disable the cron job. Test this path in paper trading before you ever rely on it.
Sources
- Alpaca docs: Paper Trading
- Alpaca docs: Authentication
- Alpaca docs: About Market Data API
- Alpaca docs: Market Data FAQ
- Alpaca docs: Placing Orders
- Alpaca docs: User Protection
- Alpaca docs: FINRA's new intraday margin rule
- alpaca-py docs: Trading
- alpaca-py docs: Market Data
- alpaca-py releases on GitHub
- TypeSafe docs: Python SDK basic usage
- TypeSafe: Introducing System One models and Jev
Loading comments...