← Back to list

Teaching Your AI Assistant to Backtest Trading Strategies in Seconds

A reusable “skill” for OpenCode that turns plain English into professional backtests — complete with expectancy, profit factor, and…

Unicorn Day · 2026-05-15 22:05 · 53 claps · 8.5 min read paywalled
#algotrading #backtesting #open-code #quantitative-finance #trading-strategy
Open on Medium ↗
Wiki topics: AI · AI · General EDU · Education & Learning

Teaching Your AI Assistant to Backtest Trading Strategies in Seconds

A reusable “skill” for OpenCode that turns plain English into professional backtests — complete with expectancy, profit factor, and annotated charts.

Most retail traders spend more time setting up backtests than actually learning from them. Wrestling with pandas DataFrames, debugging signal logic, hunting down vectorbt documentation — by the time the chart renders, the original hypothesis is half-forgotten.

What if you could just say “Backtest TSLA with RSI 14 over 3 years” and get a fully annotated chart, a trade-by-trade log, and a statistical expectancy score — all in under 30 seconds?

That’s exactly what an OpenCode Skill makes possible. This article walks through exactly how it works, covers two live backtests, and provides everything needed to clone it for any strategy.

What Is an OpenCode Skill?

OpenCode is an open-source agentic coding CLI (Command Line Interface) with a “skills” system: you drop a SKILL.md file and an accompanying script into a skills directory, and Opencode automatically knows how to invoke that tool in response to natural language. It's a way of giving Opencode a persistent, reusable capability — almost like teaching it a new verb.

“A skill is the bridge between what you say and what the code does.”

The skill consists of exactly two files:

SKILL.md — The natural-language contract. It tells Opencode what the skill does, how to invoke it, and how to map English phrases onto CLI.

expectancy_skill.py — The engine. It downloads market data, runs the strategy with vectorbt, computes expectancy metrics, and outputs trade logs.

Anatomy of the SKILL.md

The SKILL.md is deceptively simple. Let's read it carefully, because every line is load-bearing:

---
name: expectancy-skill
description: Backtest trading strategies using vectorbt indicators and calculate mathematical expectancy
---

## What I do
Backtest trading strategies using any indicator from vectorbt and calculate mathematical expectancy.

## How to Call
python expectancy_skill.py --ticker [TICKER] --indicator [INDICATOR] --p1 [VAL] --p2 [VAL] --years [YEARS]

## Natural Language Mapping
- "SMA" / "Simple Moving Average": `--indicator SMA`
- "EMA" / "Exponential Moving Average": `--indicator MA` (use ewm=True for EMA)
- "RSI": `--indicator RSI`
- "Analyze [Symbol]": `--ticker`
- "[Number] years": `--years`
- Two numbers (e.g., 50 and 200): `--p1` and `--p2`

## Example
"Analyze NVDA with a 50 and 200 SMA over 2 years."
-> python expectancy_skill.py --ticker NVDA --indicator SMA --p1 50 --p2 200 --years 2

name: expectancy-skill description: Backtest trading strategies using any indicator from vectorbt and calculate mathematical expectancy.

How to Call:

Natural Language Mapping — This is the secret sauce. Opencode reads this table to translate phrases like “Golden Cross” into --indicator SMA --p1 50 --p2 200, or "RSI" into --indicator RSI --p1 14.

The mapping section is what makes this feel like magic. You don’t need to memorise any flags — you just describe the strategy in English and Opencode handles the translation.

Anatomy of the Python Engine

The Python file does four things in sequence. Understanding each step is key to extending it with your own indicators:

1. Data Acquisition

def get_clean_financial_data(ticker, start_date, end_date):
    """Downloads and prepares stock data via yfinance."""
    data = yf.download(ticker, start=start_date, end=end_date, progress=False)
    if data.empty: return None
    # Flatten MultiIndex columns, forward-fill gaps, strip timezone
    data.columns = data.columns.get_level_values(0)
    data = data.ffill()
    if data.index.tz is not None:
        data.index = data.index.tz_localize(None)
    return data

This wrapper handles the annoying yfinance quirks — MultiIndex columns, timezone-aware indices, and stale data gaps — so downstream code is clean.

2. Signal Generation with vectorbt

if args.indicator == "RSI":
    ind  = vbt.RSI.run(close, window=args.p1)
    entries = ind.rsi_crossed_below(30)   # oversold → buy
    exits   = ind.rsi_crossed_above(70)   # overbought → sell
else:
    fast = vbt.MA.run(close, window=args.p1)
    slow = vbt.MA.run(close, window=args.p2)
    entries = fast.ma_crossed_above(slow)   # golden cross → buy
    exits   = fast.ma_crossed_below(slow)   # death cross → sell

pf = vbt.Portfolio.from_signals(
    close, entries, exits,
    fees=0.002, slippage=0.001, init_cash=10000
)

Notice the realistic assumptions baked in: 0.2% fees and 0.1% slippage per trade. Backtests without friction are fairy tales. The init_cash=10000 normalises dollar figures so expectancy is intuitive.

3. Trade Log Formatting

vectorbt gives you a raw DataFrame, but the format_trade_logs() function cleans it into something you'd actually want to read — entry/exit dates, prices, net PnL after fees, and percentage return.

def format_trade_logs(pf, trade_logs):
    """Formats raw vectorbt trade logs."""
    if trade_logs.empty: return pd.DataFrame()
    formatted_logs = trade_logs.copy()
    formatted_logs['Exit Timestamp'] = pd.to_datetime(formatted_logs['Exit Timestamp'])
    portfolio_values = pf.value()
    formatted_logs['Portfolio Value'] = portfolio_values.reindex(formatted_logs['Exit Timestamp'], method='ffill').values
    formatted_logs['Total Fees'] = formatted_logs['Entry Fees'].abs() + formatted_logs['Exit Fees'].abs()
    formatted_logs['Net PnL'] = formatted_logs['PnL'] - formatted_logs['Total Fees']
    return pd.DataFrame({
        'Entry Date': pd.to_datetime(formatted_logs['Entry Timestamp']).dt.date,
        'Entry Price': formatted_logs['Avg Entry Price'].round(2),
        'Exit Date': pd.to_datetime(formatted_logs['Exit Timestamp']).dt.date,
        'Exit Price': formatted_logs['Avg Exit Price'].round(2),
        'Net PnL ($)': formatted_logs['Net PnL'].round(2),
        'Return (%)': (formatted_logs['Return'] * 100).round(2),
    })

4. Mathematical Expectancy

expectancy = (win_rate * avg_win) + ((1 - win_rate) * avg_loss)
profit_factor = total_profit / total_loss

Why expectancy matters: A strategy with 90% win rate can still be a net loser if losses are catastrophic. Expectancy — the average dollar outcome per trade — is the single most honest number in trading system evaluation. If it’s positive, the edge is real (statistically).

Live Example 1 — TSLA RSI(14) Momentum over 3 Years

Type this in OpenCode’s chat:

Backtest TSLA with RSI (14) over 3 years, print out the transaction table and create a chart to show the buy and sell entries.

Opencode reads the SKILL.md mapping table, translates this to:

--- TSLA RSI (14/0) Results ---
Expectancy: $1232.91
Win Rate: 78.00%
Profit Factor: 3.60
Total Trades: 9

Entry Entry Price Exit Exit Price Net PnL ($) Return (%)
2023-08-07 $251.70 2023-09-11 $273.31 +$773.38 +8.17%
2023-10-20 $212.20 2023-11-15 $242.60 +$1,453.49 +13.90%
2024-01-12 $219.11 2024-07-01 $209.65 -$626.76 -4.71%
2024-08-05 $199.08 2024-09-19 $243.68 +$2,519.65 +21.96%
2024-10-14 $219.38 2024-11-06 $288.24 +$4,350.05 +30.93%
2025-02-07 $361.98 2025-04-29 $291.74 -$3,761.18 -19.77%
2025-06-05 $284.98 2025-08-13 $339.04 +$2,713.87 +18.53%
2025-11-21 $391.48 2025-12-04 $454.08 +$2,688.39 +15.56%
2026-01-07 $431.84 2026-05-15 $444.56 +$437.84 +2.54%

Live Example 2 — BTC-USD Golden Cross (SMA 50/200) over 4 Years

Analyze BTC-USD using a Golden Cross (50 and 200 SMA) for the last 4 years — print transaction table and chart entries

Opencode reads the SKILL.md mapping table, translates this to:

BTC-USD Golden Cross (SMA 50/200) - 4 Year Backtest
Metric Value
Expectancy +$2,009.65 per trade
Win Rate 75.00%
Profit Factor 3.23
Total Trades 4

Transaction Log:

Entry Entry Price Exit Exit Price Net PnL Return
2023-02-07 $23,288 2023-09-12 $25,808 +$996 +10.4%
2023-10-30 $34,537 2024-08-10 $60,885 +$8,282 +75.7%
2024-10-28 $69,978 2025-04-07 $79,156 +$2,372 +12.7%
2025-05-22 $111,785 2025-11-16 $94,083 -$3,611 -16.2%

Extending the Skill — In Plain English

Here’s where the skill pattern really shines. Adding three new indicators — Bollinger Bands, MACD, and Stochastic — requires nothing more than a single sentence in OpenCode’s chat:

Pls update the script with BBands, MACD and STOCH

OpenCode edits the Python file, updates the SKILL.md mapping table, and immediately validates all six indicators against BTC-USD to confirm nothing is broken. The full validation output:

Updated! The script now supports 6 indicators. Here's a validation on BTC-USD (4 years):

Indicator Params Expectancy Win Rate Trades
MA (Golden Cross) 50/200 +$2,009.65 75% 4
RSI 14 +$1,172.08 78% 9
MACD default +$53.92 44% 48
BBANDS 20 +$147.85 59% 17
STOCH 14/3 +$146.16 74% 19

No manual Python edits. No documentation lookup. The entire extension — new signal logic, updated mapping table, and cross-indicator validation — happens in a single conversation turn. That’s the compounding power of the skills pattern.

Key Takeaways

The SKILL.md + Python pattern is a small idea with outsized impact. It encodes domain expertise once and makes it accessible conversationally forever. For quantitative trading the benefits compound:

No more repeating coding. No more Googling vectorbt syntax. The focus shifts to strategy logic — which is the only thing that actually generates alpha. And when something interesting shows up in the expectancy numbers, OpenCode can explain it, iterate on it, or compare it against a benchmark — all without leaving the conversation.

Build the infrastructure once. Generate insight forever.

The Full Script — Copy & Use

Here’s the complete expectancy_skill.py — all 6 indicators, realistic fees and slippage, and the expectancy engine ready to drop into any OpenCode skills directory:

import yfinance as yf
import pandas as pd
import numpy as np
import vectorbt as vbt
import argparse
import warnings
from datetime import datetime, timedelta

warnings.filterwarnings('ignore')

def get_clean_financial_data(ticker, start_date, end_date):
    """Downloads and prepares stock data."""
    data = yf.download(ticker, start=start_date, end=end_date, progress=False)
    if data.empty: return None
    data.columns = data.columns.get_level_values(0) if isinstance(data.columns, pd.MultiIndex) else data.columns
    data = data.ffill()
    if data.index.tz is not None: data.index = data.index.tz_localize(None)
    return data

def format_trade_logs(pf, trade_logs):
    """Formats raw vectorbt trade logs."""
    if trade_logs.empty: return pd.DataFrame()
    formatted_logs = trade_logs.copy()
    formatted_logs['Exit Timestamp'] = pd.to_datetime(formatted_logs['Exit Timestamp'])
    portfolio_values = pf.value()
    formatted_logs['Portfolio Value'] = portfolio_values.reindex(formatted_logs['Exit Timestamp'], method='ffill').values
    formatted_logs['Total Fees'] = formatted_logs['Entry Fees'].abs() + formatted_logs['Exit Fees'].abs()
    formatted_logs['Net PnL'] = formatted_logs['PnL'] - formatted_logs['Total Fees']
    return pd.DataFrame({
        'Entry Date': pd.to_datetime(formatted_logs['Entry Timestamp']).dt.date,
        'Entry Price': formatted_logs['Avg Entry Price'].round(2),
        'Exit Date': pd.to_datetime(formatted_logs['Exit Timestamp']).dt.date,
        'Exit Price': formatted_logs['Avg Exit Price'].round(2),
        'Net PnL ($)': formatted_logs['Net PnL'].round(2),
        'Return (%)': (formatted_logs['Return'] * 100).round(2),
    })

def calculate_expectancy(trade_logs):
    """Calculates trading expectancy based on trade logs."""
    if trade_logs.empty: return None
    winning_trades = trade_logs[trade_logs['Net PnL ($)'] > 0]
    losing_trades = trade_logs[trade_logs['Net PnL ($)'] <= 0]
    win_rate = len(winning_trades) / len(trade_logs)
    avg_win = winning_trades['Net PnL ($)'].mean() if not winning_trades.empty else 0
    avg_loss = losing_trades['Net PnL ($)'].mean() if not losing_trades.empty else 0
    total_profit = winning_trades['Net PnL ($)'].sum()
    total_loss = abs(losing_trades['Net PnL ($)'].sum())
    return {
        'win_rate': win_rate,
        'expectancy': (win_rate * avg_win) + ((1 - win_rate) * avg_loss),
        'total_trades': len(trade_logs),
        'profit_factor': total_profit / total_loss if total_loss > 0 else float('inf')
    }

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ticker", required=True)
    parser.add_argument("--indicator", default="MA")
    parser.add_argument("--p1", type=int, default=10)
    parser.add_argument("--p2", type=int, default=20)
    parser.add_argument("--years", type=int, default=5)
    args = parser.parse_args()

    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=args.years*365)).strftime('%Y-%m-%d')

    df = get_clean_financial_data(args.ticker, start_date, end_date)

    if df is not None:
        close = df['Close']
        high = df['High']
        low = df['Low']

        try:
            indicator_upper = args.indicator.upper()

            if indicator_upper == "RSI":
                ind = vbt.RSI.run(close, window=args.p1)
                entries = ind.rsi_crossed_below(30)
                exits = ind.rsi_crossed_above(70)

            elif indicator_upper == "MACD":
                ind = vbt.MACD.run(close)
                entries = ind.macd_crossed_above(ind.signal)
                exits = ind.macd_crossed_below(ind.signal)

            elif indicator_upper == "BBANDS":
                ind = vbt.BBANDS.run(close, window=args.p1)
                entries = close < ind.lower
                exits = close > ind.upper

            elif indicator_upper == "STOCH":
                ind = vbt.STOCH.run(high, low, close, k_window=args.p1, d_window=args.p2)
                entries = ind.percent_k_crossed_above(ind.percent_d) & (ind.percent_k < 20)
                exits = ind.percent_k_crossed_below(ind.percent_d) & (ind.percent_k > 80)

            else:
                vbt_func = getattr(vbt, indicator_upper)
                fast = vbt_func.run(close, window=args.p1)
                slow = vbt_func.run(close, window=args.p2)
                entries, exits = fast.ma_crossed_above(slow), fast.ma_crossed_below(slow)

            pf = vbt.Portfolio.from_signals(close, entries, exits, fees=0.002, slippage=0.001, init_cash=10000)
            metrics = calculate_expectancy(format_trade_logs(pf, pf.trades.records_readable))

            if metrics:
                print(f"\n--- {args.ticker} {args.indicator} ({args.p1}/{args.p2}) Results ---")
                print(f"Expectancy: ${metrics['expectancy']:.2f}")
                print(f"Win Rate: {metrics['win_rate']:.2%}")
                print(f"Profit Factor: {metrics['profit_factor']:.2f}")
                print(f"Total Trades: {metrics['total_trades']}")
            else:
                print("No trades executed.")
        except AttributeError:
            print(f"Error: Indicator '{args.indicator}' is not supported by vectorbt.")

To use this as an OpenCode skill: Save this file as expectancy_skill.py alongside a SKILL.md in your OpenCode skills directory. OpenCode will automatically discover it and respond to natural language prompts like "Backtest NVDA with BBANDS over 2 years" or "Analyse ETH-USD with STOCH over 3 years."

Further Reading on OpenCode

New to OpenCode? The following articles from the same series cover the tool from first principles through to advanced skill-building:

[embed]How to Use Skills in OpenCode to Build a Trading Research Tool A step-by-step guide to creating a custom AI skill that performs live market research — structured, sourced, and…medium.com

[embed]Your AI Coding Assistant Just Got a Memory — Meet OpenCode Skills What if your AI didn’t need to be re-explained the same thing every session? What if it already knew your stack, your…medium.com

[embed]HOW TO ANALYSE STOCKS WITH OPENCODE Five prompts. Zero Python. A complete AAPL analysis pipeline — charts, RSI, sentiment, and a Buffett scorecard — built…medium.com

[embed]Meet OpenCode: The AI Agent That Does Your Dev Work While You Grab Coffee ☕ There’s a new kind of developer tool in town, and it’s not just autocomplete. It’s not just a chatbot. It’s an AI agent…medium.com

[embed]What Happens When You Let OpenCode Loose Inside an Obsidian Vault Eighteen interconnected notes. Dozens of live links. A trading knowledge graph I’d never have built alone — and it took…medium.com


메타데이터
post_id
dfa1cfb65b1a
slug
teaching-your-ai-assistant-to-backtest-trading-strategies-in-seconds-dfa1cfb65b1a
url
https://medium.com/@wl8380/teaching-your-ai-assistant-to-backtest-trading-strategies-in-seconds-dfa1cfb65b1a
canonical_url
https://medium.com/@wl8380/teaching-your-ai-assistant-to-backtest-trading-strategies-in-seconds-dfa1cfb65b1a
author_url
https://medium.com/@wl8380
status
ok
fetched_at
2026-06-09 15:37:30