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…

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:
메타데이터
- 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