← Journal·Tutorials

Backtesting Insider Buy Signals in Python: A Step-by-Step Tutorial

A reproducible backtest of an insider cluster-buy strategy using pandas, vectorbt, and the NexusForm4 API. Includes look-ahead controls and turnover metrics.

·11 min read·NexusForm4 Research

A backtest is a claim about the past. To make it worth trusting, it needs three things: a clean signal timeline, honest execution assumptions, and rigorous look-ahead controls. This walkthrough builds all three on top of NexusForm4's cluster-buy feed.

1. Setup

bash
pip install pandas numpy vectorbt yfinance requests

2. Pull the signal history

Pull the trailing signals and store them locally. The critical field is filed_date — this is when a market participant could first have seen the trade. Never enter based on txn_date, which is the trade date and is not observable until the filing arrives.

python
import pandas as pd, requests
HOST = "nexusform4-sec-insider-trading-c-suite-signals.p.rapidapi.com"
H = {"X-RapidAPI-Key": "...", "X-RapidAPI-Host": HOST}
data = requests.get(f"https://{HOST}/signals/cluster-buys", headers=H).json()
sig = pd.DataFrame(data)
sig["entry_date"] = pd.to_datetime(sig["filed_date"]) + pd.Timedelta(days=1)

3. Pull the price panel

Get adjusted daily closes for every ticker in the signal universe. Adjust for splits and dividends or your return calculation will be wrong.

python
import yfinance as yf
tickers = sig["ticker"].unique().tolist()
px = yf.download(tickers, start="2023-01-01", auto_adjust=True)["Close"]

4. Build the strategy

Enter on next open after filing, hold 60 sessions, equal-weight across concurrent signals, max 20 concurrent positions.

python
entries = pd.DataFrame(False, index=px.index, columns=px.columns)
exits = entries.copy()
for _, row in sig.iterrows():
    d = row["entry_date"]
    t = row["ticker"]
    if t in entries.columns and d in entries.index:
        entries.at[d, t] = True
        exit_idx = entries.index.get_loc(d) + 60
        if exit_idx < len(entries.index):
            exits.iat[exit_idx, entries.columns.get_loc(t)] = True
python
import vectorbt as vbt
pf = vbt.Portfolio.from_signals(px, entries, exits, init_cash=100_000, fees=0.0005, freq="1D")
print(pf.stats())

5. Metrics that matter

  • Sharpe ratio — risk-adjusted return; anything above 0.8 for a single-signal strategy is respectable.
  • Max drawdown — pair with recovery time; a 30% drawdown that recovers in 2 months differs from one that takes 2 years.
  • Turnover — insider strategies naturally low-turnover, so double-check you are not accidentally triggering churn.
  • Hit rate and win/loss asymmetry — cluster buys should skew toward asymmetric right-tail wins, not high hit rates.

6. Common ways to fool yourself

  1. Using txn_date instead of filed_date for entry — pure look-ahead.
  2. Backtesting with survivorship-biased price data — delisted tickers matter.
  3. Optimizing thresholds on the full history then reporting in-sample Sharpe.
  4. Ignoring borrow costs when you extend to a long-short book.
A backtest without look-ahead controls and out-of-sample validation is a story, not evidence.

Continue reading