All skills
marketcalls avatar

/vectorbt-expert

@3a8c2a3

VectorBT backtesting expert. Use when user asks to backtest strategies, create entry/exit signals, analyze portfolio performance, optimize parameters, fetch historical data, use VectorBT/vectorbt, compare strategies, position sizing, equity curves, drawdown charts, or trade analysis. Also triggers for openalgo.ta helpers (exrem, crossover, crossunder, flip, donchian, supertrend).

Use this Skill: https://skilld.dev/gh/marketcalls/vectorbt-backtesting-skills/vectorbt-expert

This session only. Nothing lands on disk.

ruleswalk-forward.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Walk-Forward Analysis

Walk-forward analysis prevents overfitting by optimizing on in-sample data and validating on unseen out-of-sample data. This is the gold standard for strategy validation.

Why Walk-Forward Matters

A strategy that performs well on historical data but fails live is likely overfit. Walk-forward analysis simulates real-world conditions where you:

  1. Optimize parameters on past data (in-sample)
  2. Trade with those parameters on new data (out-of-sample)
  3. Roll the window forward and repeat

If out-of-sample performance is consistently positive, the strategy has real edge.

Simple Train/Test Split

The simplest form: split data into training (in-sample) and testing (out-of-sample):

from openalgo import ta

# Split data: 70% train, 30% test
split_idx = int(len(close) * 0.7)
close_train = close.iloc[:split_idx]
close_test = close.iloc[split_idx:]

# Optimize on training data
best_return = -np.inf
best_params = {}

for fast in range(5, 30):
    for slow in range(20, 60):
        if fast >= slow:
            continue
        ema_f = ta.ema(close_train, fast)
        ema_s = ta.ema(close_train, slow)

        buy_raw = (ema_f > ema_s) & (ema_f.shift(1) <= ema_s.shift(1))
        sell_raw = (ema_f < ema_s) & (ema_f.shift(1) >= ema_s.shift(1))
        entries = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
        exits = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))

        pf = vbt.Portfolio.from_signals(
            close_train, entries, exits,
            size=0.75, size_type="percent",
            fees=0.00111, fixed_fees=20,
            init_cash=1_000_000, freq="1D",
            min_size=1, size_granularity=1,
        )
        ret = pf.total_return()
        if ret > best_return:
            best_return = ret
            best_params = {'fast': fast, 'slow': slow}

print(f"Best in-sample params: {best_params}, Return: {best_return:.2%}")

# Validate on test data with best parameters
ema_f = ta.ema(close_test, best_params['fast'])
ema_s = ta.ema(close_test, best_params['slow'])

buy_raw = (ema_f > ema_s) & (ema_f.shift(1) <= ema_s.shift(1))
sell_raw = (ema_f < ema_s) & (ema_f.shift(1) >= ema_s.shift(1))
entries_test = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
exits_test = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))

pf_test = vbt.Portfolio.from_signals(
    close_test, entries_test, exits_test,
    size=0.75, size_type="percent",
    fees=0.00111, fixed_fees=20,
    init_cash=1_000_000, freq="1D",
    min_size=1, size_granularity=1,
)
print(f"Out-of-sample return: {pf_test.total_return():.2%}")
print(f"Out-of-sample Sharpe: {pf_test.sharpe_ratio():.2f}")

Rolling Walk-Forward

More rigorous: slide the optimization window forward in steps:

from openalgo import ta

TRAIN_DAYS = 252 * 2    # 2 years training
TEST_DAYS = 63           # 3 months testing (1 quarter)
STEP_DAYS = 63           # Step forward by 1 quarter

oos_results = []
start = 0

while start + TRAIN_DAYS + TEST_DAYS <= len(close):
    train_slice = close.iloc[start:start + TRAIN_DAYS]
    test_slice = close.iloc[start + TRAIN_DAYS:start + TRAIN_DAYS + TEST_DAYS]

    # Optimize on training window
    best_return = -np.inf
    best_fast, best_slow = 10, 20

    for fast in range(5, 25):
        for slow in range(20, 50):
            if fast >= slow:
                continue
            ema_f = ta.ema(train_slice, fast)
            ema_s = ta.ema(train_slice, slow)

            buy_raw = (ema_f > ema_s) & (ema_f.shift(1) <= ema_s.shift(1))
            sell_raw = (ema_f < ema_s) & (ema_f.shift(1) >= ema_s.shift(1))
            entries = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
            exits = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))

            pf = vbt.Portfolio.from_signals(
                train_slice, entries, exits,
                size=0.75, size_type="percent",
                fees=0.00111, fixed_fees=20,
                init_cash=1_000_000, freq="1D",
                min_size=1, size_granularity=1,
            )
            ret = pf.total_return()
            if ret > best_return:
                best_return = ret
                best_fast, best_slow = fast, slow

    # Test on out-of-sample window
    ema_f = ta.ema(test_slice, best_fast)
    ema_s = ta.ema(test_slice, best_slow)

    buy_raw = (ema_f > ema_s) & (ema_f.shift(1) <= ema_s.shift(1))
    sell_raw = (ema_f < ema_s) & (ema_f.shift(1) >= ema_s.shift(1))
    entries_t = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
    exits_t = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))

    pf_t = vbt.Portfolio.from_signals(
        test_slice, entries_t, exits_t,
        size=0.75, size_type="percent",
        fees=0.00111, fixed_fees=20,
        init_cash=1_000_000, freq="1D",
        min_size=1, size_granularity=1,
    )

    oos_results.append({
        'window_start': train_slice.index[0],
        'test_start': test_slice.index[0],
        'test_end': test_slice.index[-1],
        'best_fast': best_fast,
        'best_slow': best_slow,
        'in_sample_return': best_return,
        'oos_return': pf_t.total_return(),
        'oos_sharpe': pf_t.sharpe_ratio(),
        'oos_max_dd': pf_t.max_drawdown(),
    })

    start += STEP_DAYS

results_df = pd.DataFrame(oos_results)
print(results_df.to_string(index=False))
print(f"\nAvg OOS Return: {results_df['oos_return'].mean():.2%}")
print(f"OOS Win Rate: {(results_df['oos_return'] > 0).mean():.0%}")

Walk-Forward Efficiency Ratio

The WFE measures how much in-sample performance carries to out-of-sample:

wfe = results_df['oos_return'].mean() / results_df['in_sample_return'].mean()
print(f"Walk-Forward Efficiency: {wfe:.2%}")
# > 50% is acceptable, > 70% is good

Anchored Walk-Forward

Training window always starts from the beginning (grows over time):

# Same as rolling, but change:
# train_slice = close.iloc[0:start + TRAIN_DAYS]  # Anchored: always from day 0
# This gives more training data in later windows

Best Practices

  • Minimum 2 years of in-sample data for daily strategies
  • Minimum 3 months of out-of-sample per window
  • At least 5 walk-forward windows for statistical significance
  • Consistent OOS performance matters more than high IS performance
  • If IS return is 50% but OOS is -5%, the strategy is overfit
  • Use Sharpe ratio for optimization objective, not total return (more stable)
  • Always include realistic transaction costs (see indian-market-costs)
  • Track parameter stability: if optimal params change wildly each window, strategy is fragile (see pitfalls)

Template

See assets/walk_forward/template.py for a complete production-ready walk-forward script.

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive instructions and production-ready templates for financial strategy backtesting using the VectorBT library. It emphasizes security best practices such as environment-variable-based secret management, read-only database connections, and robustness testing. The analysis found no evidence of malicious patterns, data exfiltration, or obfuscation.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    19/33 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 3a8c2a3. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
user-invocable
false

README badge

README badge for marketcalls/vectorbt-backtesting-skills/vectorbt-expert