1. Home
  2. Insights
  3. AI & Chatbots

Backtest a Jev Trading Strategy in Python: Step by Step With Code

A Jev backtest is only useful if it cannot see the future. Here is a pandas workflow with cached decisions, real costs and walk-forward tests.

Abstract dark illustration with the title Backtest a Jev Trading Strategy in Python, and fact chips on next-open fills, cached Jev calls, walk-forward threshold tuning and backtest metrics.
Illustration: Apex Logic
On this page
  1. A Jev backtest at a glance
  2. What you need
  3. Step 1: Load daily bars
  4. Step 2: Compute features without look-ahead bias
  5. Step 3: Cache Jev decisions so runs are repeatable and cheap
  6. Step 4: Positions, next-open fills, fees and slippage
  7. Step 5: Metrics: CAGR, max drawdown, Sharpe and hit rate
  8. Step 6: Walk-forward testing and threshold tuning
  9. From backtest to paper trading
  10. Jev backtesting FAQ

Quick answer

To backtest a Jev trading strategy in Python, build each day's state from past data only, ask Jev through a cache keyed on a pinned model version, and trade at the next day's open with fees and slippage included. Tune the confidence threshold with a walk-forward test, keep a final year as a holdout, and compare the result with buy and hold and with the same rules without Jev. If Jev adds nothing after costs, drop it; if it does, paper trade it next.

  • Build each state from past bars only and trade at the next open
  • Cache Jev answers by model version, questions and state
  • Charge costs and test several levels before trusting a result
  • Tune thresholds walk-forward and keep a final holdout year

Updated 18 October 2026. A complete pandas backtest for a Jev-gated strategy, with cached decisions, next-open fills, trading costs and walk-forward tests.

To backtest a Jev trading strategy in Python, build each day's state from past bars only, turn the numbers into labels, ask Jev about each state through a cache, and trade at the next day's open with fees and slippage included. Then tune the confidence threshold with a walk-forward test and compare the result with buy and hold and with the same rules without Jev. If Jev does not beat the rule it replaces after costs, it has not earned its place.

This guide gives working code for every step, using pandas and TypeSafe's Python SDK, typesafe-sdk 0.7.5. Because labeled states repeat, a ten-year daily backtest needs only a few dozen Jev calls, well under a cent in API fees. It is an educational example, not financial advice: a backtest is a hypothesis, and the next step is paper trading. New to Jev? Start with what Jev AI can and cannot do for trading.

A Jev backtest at a glance

StepRuleCommon mistake
DataDaily bars adjusted for splits and dividendsUnadjusted prices, or only today's index members
FeaturesRolling windows over past barsBucket limits fitted on the whole period
StateShort labels, no ticker, no datesRaw numbers or long price histories
DecisionsCached, keyed on a pinned model versionCalling the API again on every run
ExecutionDecide after the close, trade at the next openTrading at the close you just used
CostsBasis points per trade, tested at several levelsFree, perfect fills
ValidationWalk-forward tests plus a final holdout yearTuning on the full history
MetricsCAGR, max drawdown, Sharpe, hit rate, exposure, tradesTotal return alone

What you need

  • Python 3.10 or later, which the TypeSafe SDK requires.
  • pip install pandas numpy typesafe-sdk, plus alpaca-py if you take data from Alpaca.
  • A TypeSafe API key in the TYPESAFE_API_KEY environment variable.
  • A pinned model version. This guide uses jev-1.13.0; read what changed in Jev 1.14 before you switch.

Step 1: Load daily bars

Any source works if it gives daily open, high, low, close and volume, adjusted for splits and dividends. From a CSV file:

import numpy as np
import pandas as pd


def load_bars(path):
    df = pd.read_csv(path, parse_dates=["date"], index_col="date").sort_index()
    return df[["open", "high", "low", "close", "volume"]].dropna()

Or from Alpaca's market data API, whose free Basic plan includes stock history since 2016. Without a paid plan, consolidated (SIP) data must end at least 15 minutes ago, and paper-only accounts get IEX data. Leave out the feed setting and Alpaca picks the best feed your plan allows; use the same feed in the backtest and the live bot.

import os
from datetime import datetime

import pandas as pd
from alpaca.data.enums import Adjustment
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest
from alpaca.data.timeframe import TimeFrame

data = StockHistoricalDataClient(os.environ["APCA_API_KEY_ID"], os.environ["APCA_API_SECRET_KEY"])
request = StockBarsRequest(symbol_or_symbols="SPY", timeframe=TimeFrame.Day, start=datetime(2016, 1, 1),
                           end=datetime(2026, 10, 16), adjustment=Adjustment.ALL)
bars = data.get_stock_bars(request).df.loc["SPY"]
bars.index = pd.to_datetime(bars.index.date)
bars = bars[["open", "high", "low", "close", "volume"]]

Alpaca stamps each daily bar with the start of the day in New York, but the bar holds the whole session. You only know the close after the market shuts, which is why every decision below waits for the next open.

Step 2: Compute features without look-ahead bias

Look-ahead bias means using information on day t that was not known until later. Rolling windows ending at row t are safe: each value uses that day and earlier days only. Fix bucket limits in advance or compute them from the past only; quantiles over the whole history leak the future into every row.

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}

The state follows TypeSafe's guidance for Jev: do the math in code, send labels instead of raw numbers, and keep the state short. It also leaves out the ticker and the dates. A model should judge the pattern, not recognize the period.

Step 3: Cache Jev decisions so runs are repeatable and cheap

TypeSafe's docs do not promise identical probabilities for identical requests, so store each answer the first time and reuse it. The key covers the pinned model, the questions and the state, so changing any of them fetches a fresh answer.

import hashlib
import json
from pathlib import Path

from typesafe_sdk import Choice, Noul, TypeSafeClient

MODEL = "jev-1.13.0"  # pin a version; never cache answers from an alias
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?"},
}
CACHE = Path("jev_cache.json")
cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
client = TypeSafeClient(model=MODEL)  # reads TYPESAFE_API_KEY


def ask_jev(state):
    key = hashlib.sha256(json.dumps([MODEL, SPEC, state], sort_keys=True).encode()).hexdigest()
    if key not in cache:
        r = client.system_one(
            state=state,
            questions={"position": Choice(**SPEC["position"]), "stress": Noul(**SPEC["stress"])},
        )
        p = r.choices["position"]
        cache[key] = {"model": r.model, "choice": p.choice, "confidence": p.confidence,
                      "p_long": p.probabilities["long"], "stress": r.nouls["stress"].noul,
                      "tokens": r.usage.input_tokens}
        CACHE.write_text(json.dumps(cache, indent=1))
    return cache[key]


bars = load_bars("spy_daily.csv")  # or the Alpaca bars from Step 1
feats = add_features(bars)
states = pd.DataFrame([to_state(row) for row in feats.itertuples()], index=feats.index)
answers = pd.DataFrame([ask_jev(s) for s in states.to_dict("records")], index=feats.index)
px = bars.loc[feats.index]
print(len(cache), "distinct Jev calls for", len(answers), "trading days")

Four labels allow at most 54 distinct states. In a dry run on 12 years of made-up prices only 39 appeared, so the cache needed 39 calls: at roughly 300 input tokens each and $0.042 per 1M input tokens, a tiny fraction of a cent.

Step 4: Positions, next-open fills, fees and slippage

Your code makes the decision. The policy below goes long only when Jev picks "long", its confidence clears a threshold, and the stress answer stays below 0.5. With two options, Jev's confidence equals twice the top probability minus 1, so a threshold of 0.4 means the top option needs at least 70 percent.

COST = 0.0005  # 5 basis points per unit traded: an assumption, so test others


def positions(answers, threshold, max_stress=0.5):
    go = (answers["choice"] == "long") & (answers["confidence"] >= threshold) & (answers["stress"] < max_stress)
    return go.astype(float)


def backtest(px, pos, cost=COST):
    open_ret = px["open"].shift(-1) / px["open"] - 1  # open of day t to open of day t+1
    held = pos.shift(1)                                # decided after the close of t-1, filled at the open of t
    traded = pos.diff().abs().shift(1)
    return pd.DataFrame({"held": held, "net": held * open_ret - traded * cost}).dropna()

The two shift calls carry the no-look-ahead rule. A decision made from day t's close is filled at day t+1's open and earns the return from that open onward, and the trading cost lands on the day of the fill. Trading at the same close you used to decide is the most common way a backtest flatters itself.

COST folds commission, spread and slippage into one number per unit traded. Five basis points is an assumption, not a quote, so the next step tests several levels. Commission-free is not cost-free: Alpaca's paper trading docs say simulated fills ignore market impact, slippage due to latency, queue position, price improvement, regulatory fees and dividends.

Step 5: Metrics: CAGR, max drawdown, Sharpe and hit rate

def metrics(bt):
    net, in_market = bt["net"], bt["held"] > 0
    equity = (1 + net).cumprod()
    return {
        "cagr": equity.iloc[-1] ** (252 / len(net)) - 1,
        "max_drawdown": (equity / equity.cummax() - 1).min(),
        "sharpe": net.mean() / net.std() * np.sqrt(252),
        "hit_rate": (net[in_market] > 0).mean(),
        "exposure": in_market.mean(),
        "trades": int((bt["held"].diff().abs() > 0).sum()),
    }


runs = {
    "buy_and_hold": pd.Series(1.0, index=px.index),
    "rules_only": (states["long_term_trend"] == "up").astype(float),
    "jev_gated": positions(answers, threshold=0.4),
}
print(pd.DataFrame({name: metrics(backtest(px, pos)) for name, pos in runs.items()}).T.round(3))
for c in (0.0, 0.0005, 0.001, 0.002):
    print("cost", c, "sharpe", round(metrics(backtest(px, runs["jev_gated"], cost=c))["sharpe"], 2))
  • CAGR: compound annual growth, using 252 trading days a year.
  • Max drawdown: the worst fall from a peak, and the best test of whether you could live with the strategy.
  • Sharpe ratio: mean daily return over its standard deviation, times the square root of 252, with a zero risk-free rate.
  • Hit rate: the share of days in the market that made money. Read it next to the trade count.
  • Exposure and trades: time invested and position changes, which drive costs.

The baselines matter more than any single number. Buy and hold shows what doing nothing earned; the rules-only run uses the same trend label without Jev, so its gap to the Jev-gated run is what Jev added or took away. Be as honest as one open-source Jev stock forecaster, whose author published a backtest with 54 of 120 next-day calls right; see Jev trading bots on GitHub.

Step 6: Walk-forward testing and threshold tuning

Every parameter you tune on the full history makes the backtest look better than the future will be. A walk-forward test tunes the threshold on a training window, tests it on the year after, then rolls forward, so every test year is out of sample. The code also keeps the final year out of everything and runs it once at the end.

GRID = (0.0, 0.2, 0.4, 0.6)


def walk_forward(px, answers, train_years=3, min_trades=6):
    years = sorted(px.index.year.unique())
    pos = pd.Series(np.nan, index=px.index)
    picks = {}
    for i in range(train_years, len(years)):
        train = px.index.year.isin(years[i - train_years:i])
        scores = {}
        for t in GRID:
            m = metrics(backtest(px[train], positions(answers[train], t)))
            if m["trades"] >= min_trades:
                scores[t] = m["sharpe"]
        if scores:
            picks[years[i]] = max(scores, key=scores.get)
            test = px.index.year == years[i]
            pos[test] = positions(answers[test], picks[years[i]])
    live = pos.notna()
    return backtest(px[live], pos[live]), picks


dev = px.index.year < px.index.year.max()
oos, picks = walk_forward(px[dev], answers[dev])
print(picks, pd.Series(metrics(oos)).round(3).to_dict())
final = backtest(px[~dev], positions(answers[~dev], picks[max(picks)]))
print("holdout:", pd.Series(metrics(final)).round(3).to_dict())

Rules that keep the tuning honest:

  • Keep the grid small and fix it before you look.
  • Demand enough trades in each training window, so one lucky trade cannot win.
  • Prefer stable picks. A best threshold that jumps around each year is probably noise.
  • Run the holdout once. Change things after seeing it and it is no longer a holdout.
  • Count your attempts. The more variants you try, the more likely the best is luck.
  • Test elsewhere: symbols and periods you did not tune on.

A threshold only means something if Jev's probabilities match outcomes in your market. Jev confidence scores explained shows how to check calibration by bucket.

From backtest to paper trading

A good backtest earns a paper trading test, nothing more. Keep the same feature code, labels, questions, threshold and pinned model, and run the strategy on live data with simulated fills for weeks. Paper fills will differ from the backtest's open prices, and that difference is information. Our Jev paper trading bot with Alpaca reuses the functions on this page, and how to build a Jev AI trading bot covers the wider design.

Want a second pair of eyes on a backtest, or a decision log your team can query in plain English? Get in touch.

Jev backtesting FAQ

Can you backtest a Jev strategy on historical data?

Yes. Jev judges whatever state you send, so you can rebuild each past day's state from data available that day and ask Jev about it now. Keep tickers and dates out of the state, cache every answer, and trade only at the next open. The result shows how today's model judges past situations, which is not the same as how it would have traded back then.

How much does a Jev backtest cost?

Very little. Jev costs $0.042 per 1M input tokens as of 18 October 2026, and output is free. With labeled states and a cache, a daily backtest needs only as many calls as there are distinct states, often a few dozen. Even without a cache, 2,500 trading days at about 300 tokens each is roughly 750,000 tokens, or about 3 cents.

Will a Jev backtest give the same result every run?

Only if you cache the answers and pin the model version. TypeSafe's docs do not promise identical probabilities for identical requests, and aliases such as jev-latest move to new versions. Store each answer with the versioned model ID that produced it, and treat a new version as a new experiment with its own cache and its own walk-forward test.

What confidence threshold should I use in a backtest?

Choose it with a walk-forward test, not by looking at the full history. Try a few values chosen in advance, require a minimum number of trades, and prefer a threshold that holds up across most windows. For a two-option choice, confidence equals twice the top probability minus one, so 0.4 means the top option needs at least 70 percent.

What is look-ahead bias in a trading backtest?

It means letting information from the future leak into a past decision. Common forms are trading at the same close used to compute a signal, scaling features with statistics from the whole period, and testing only on today's index members. Shift positions by one bar, compute features with past-only windows, and fix bucket limits in advance.

How long should I paper trade after a backtest?

Long enough to see the strategy trade many times in conditions it was not tuned on. For a daily strategy that usually means weeks to months. Compare paper fills, trade counts and drawdowns with the backtest, and write down in advance what result would make you stop. No fixed period guarantees that a strategy is safe.

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...