1. Home
  2. Insights
  3. AI & Chatbots

Jev Trading Bot With Alpaca: Paper Trade in Python Step by Step

Jev decides, code trades. This guide builds a complete Alpaca paper trading bot in Python, with a broker-side stop, decision logs and a kill switch.

Abstract dark illustration with the title Jev Trading Bot With Alpaca, and fact chips on free paper trading, the two Python SDKs, a broker-side stop on every entry and the kill switch.
Illustration: Apex Logic
On this page
  1. The Jev Alpaca bot at a glance
  2. How the bot works
  3. Step 1: Accounts, keys and environment variables
  4. Step 2: The full bot
  5. Step 3: Market data
  6. Step 4: The Jev decision and position sizing
  7. Step 5: Orders, stops and Alpaca's rules
  8. Step 6: Scheduling
  9. Step 7: Logging
  10. Step 8: The kill switch
  11. Checklist before going live
  12. Jev Alpaca trading bot FAQ

Quick answer

A Jev trading bot on Alpaca reads daily bars, turns them into labels, asks Jev a typed question, and lets code decide, size and place the orders through Alpaca's free paper trading API. The bot in this guide uses alpaca-py 0.44.0 and typesafe-sdk 0.7.5, attaches a broker-side stop to every entry, logs each decision with the model version, and has a kill switch and a daily loss limit. Paper trade it for weeks before you consider live money.

  • Jev answers; code sizes and places every order
  • Every entry carries a stop held at the broker
  • A kill file and a 2 percent daily loss limit stop the bot
  • Paper trade for weeks; paper fills are not live fills

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

PartThis guideChecked detail
BrokerAlpaca Trading API, paper accountTradingClient(key, secret, paper=True); paper keys differ from live keys
SDKsalpaca-py 0.44.0 and typesafe-sdk 0.7.5Python 3.10 or later
DataCompleted daily bars for SPYFree Basic plan; paper-only accounts get IEX data
DecisionJev choice (long or flat) plus a stress checkPinned to jev-1.13.0
OrdersMarket buy with an attached stop loss (OTO), good till canceledThe stop sits at the broker
Sizing10 percent of equity, whole sharesA rule in code, never a model output
ScheduleEvery 15 minutes on weekdays, one decision a dayAlpaca's market clock decides when
SafetyKill file, 2 percent daily loss limit, account checksclose_all_positions(cancel_orders=True)

How the bot works

  1. Schedule: cron starts the script every 15 minutes on weekdays.
  2. Safety: a kill file, a blocked account or a daily loss over the limit stops everything first.
  3. Clock: act only when Alpaca says the market is open, once per day.
  4. State: completed daily bars become four labels, with the same code as our Jev backtest in Python.
  5. Decision: Jev answers; code applies the threshold.
  6. Orders: buy with a stop attached, or close the position.
  7. 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.OTO and a stop_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 KILL next 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 KILL file itself.
  • Account flags: if Alpaca marks the account as blocked, the bot logs it and stops.

Checklist before going live

  1. Paper trade for weeks; a daily bot makes only about 20 decisions a month.
  2. Compare paper results with the backtest: trade count, fills and drawdown. Explain every gap.
  3. Test every exit: a triggered stop, the kill file, the daily loss limit and a Jev outage.
  4. Remember what paper leaves out: Alpaca lists market impact, information leakage, slippage due to latency, queue position, price improvement, regulatory fees and dividends.
  5. Keep live keys apart from paper keys. TradingClient defaults to paper=True, so going live needs a deliberate change.
  6. Pin the Jev version and plan for updates; see what changed in Jev 1.14.
  7. 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.
  8. Set alerts for errors, rejected orders and kill switch events.
  9. 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

Want an AI tool like this built for your business?

We build custom AI tools on Gemini, GPT and Claude, connected to the software you already use. Fixed quote, live in weeks, and you own the code.

Written by Abdul Ghani

Apex Logic is a software studio in Multan, Pakistan that builds custom web apps, AI tools and automation for clients worldwide.

More from Abdul Ghani

Editor's note: Facts checked against official announcements and documentation on 18 October 2026. These topics are moving fast; prices, availability and features may change.

Comments

Loading comments...