Vectorized backtesting & forward-testing framework for intraday strategies
Project description
mtrader
Vectorized backtesting & forward/live-testing framework for intraday trading strategies.
38+ indicators, 7 exit types, position sizing, risk controls, walk-forward optimization, auto strategy discovery.
import mtrader as mt
result = mt.run_backtest(df, entry_conditions, buy_or_sell="buy")
result.to_html("report.html")
Quick start
import pandas as pd
import numpy as np
import mtrader as mt
# 1. Load & clean data
df = pd.read_csv("ticks.csv")
df = mt.clean_data(df, start_time="09:15", end_time="15:30")
# 2. Define strategy (indicators auto-detected from column names)
entry = mt.cross_above("can1_sma1_p20", "close")
exit_cond = mt.cross_below("can1_rsi_p14", "close")
# 3. Run backtest
result = mt.run_backtest(
df, entry, buy_or_sell="buy", exit_conditions=exit_cond,
target_delta_normalized=0.5, stoploss_delta_normalized=0.25,
)
print(result.metrics)
result.to_html("backtest.html")
Pipeline
raw CSV/DataFrame
│
▼
clean_data() — auto-detect types, normalize OHLCV, fill gaps, handle splits
│
▼
run_backtest() — auto-detect indicators from column names (can1_{indicator}_p{period})
│ no need to pass indicators/rolling_minutes explicitly
▼
add_trailing_stop_column() — trailing stop price (optional)
add_time_filter_column() — restrict to trading hours (optional)
add_regime_filter_column() — ADX-based trend/ranging filter (optional)
│
▼
precalculate_exit_time_amount_profit()
│ — exit signals: conditional, target, stoploss, trailing
│
▼
take_trade_on_condition*()
│ — capital simulation (with sizing_fn, risk controls, hold filters)
│
▼
backtest_report() — Sharpe, Sortino, Calmar, win rate, profit factor, drawdown
equity_curve() — per-bar equity + drawdown
html_backtest_report() — standalone HTML with SVG charts
trade_log() — per-trade log with exit reason, hold bars
One-call backtest
from mtrader import run_backtest
result = run_backtest(
df,
entry_conditions=mt.cross_above("can1_sma1_p20", "close"),
buy_or_sell="buy",
exit_conditions=mt.cross_below("can1_ema1_p9", "can1_ema1_p21"),
target_delta_normalized=0.5,
stoploss_delta_normalized=0.25,
initial_capital=100000,
)
result.final_capital # 112345.67
result.metrics # {'Sharpe Ratio': 1.23, 'sortino_ratio': 1.45, 'win_rate_pct': 55.0, ...}
result.report # backtest_report dict (extended)
result.equity # equity_curve DataFrame
result.trades # trade_log DataFrame (with exit_reason, hold_bars)
result.to_html("report.html")
Position Sizing
Control how much capital is deployed per trade — fixed fraction or dynamic callable.
Fixed fraction
result = run_backtest(df, entry,
initial_capital=100000, capital_per_trade_pct=0.25)
# Only 25% of capital at risk per trade. Remaining 75% stays as cash.
Dynamic sizing callable
Use sizing_fn for ATR-based, Kelly, or equity-curve sizing:
def atr_sizing(entry_idx, capital_before, df):
atr_pct = df.loc[entry_idx, "can1_atr_p14"] / df.loc[entry_idx, "close"]
return min(0.5, 0.02 / max(atr_pct, 0.001)) # risk 2% per ATR unit
result = run_backtest(df, entry,
initial_capital=100000, sizing_fn=atr_sizing)
Position sizing utilities
from mtrader import (
fixed_quantity_size, fixed_capital_size,
percent_equity_size, atr_risk_size,
)
qty = atr_risk_size(price, atr_values, equity=100000, risk_pct=0.01, atr_multiple=2)
# Returns quantity of shares to take: equity * risk_pct / (atr * atr_multiple)
Risk Controls
Apply max-trades-per-day, cooldown, and max-daily-loss directly in run_backtest:
result = run_backtest(
df, entry,
capital_per_trade_pct=0.5,
max_trades_per_day=3, # at most 3 entries per day
cooldown_bars=5, # wait 5 bars between trades
max_daily_loss_pct=2.0, # stop trading for the day if -2%
)
Post-hoc filtering also available:
from mtrader import apply_risk_controls
controlled = apply_risk_controls(trades, max_trades_per_day=3, cooldown_bars=5)
filtered = trades[controlled["allowed"]]
Holding Period Filters
Skip trades that exit too quickly or hold too long:
result = run_backtest(df, entry,
min_hold_bars=5, # skip trades < 5 bars
max_hold_bars=50) # skip trades > 50 bars
Exit Strategies
Target & stoploss
# Fixed price delta
result = run_backtest(df, entry, target_delta=200, stoploss_delta=100)
# Normalized (% of price / 10000)
result = run_backtest(df, entry, target_delta_normalized=1.0, stoploss_delta_normalized=0.5)
# Dynamic column-based (e.g., ATR multiples)
df["target_2atr"] = df["can1_atr_p14"] * 2.0
df["stoploss_1atr"] = df["can1_atr_p14"]
result = run_backtest(df, entry, target_delta_column="target_2atr",
stoploss_delta_column="stoploss_1atr")
Trailing stop
from mtrader import add_trailing_stop_column
df = add_trailing_stop_column(df, trail_pct=0.5, lookback=20)
df["trail_delta"] = df["close"] - df["trailing_stop_price"]
result = run_backtest(df, entry, stoploss_delta_column="trail_delta")
Condition-based exit
# Exit when RSI crosses below 70
exit_cond = mt.cross_below("can1_rsi_p14", "close")
result = run_backtest(df, entry, exit_conditions=exit_cond)
Filters
Time filter (trading hours)
from mtrader import add_time_filter_column
df = add_time_filter_column(df, start_time="09:45", end_time="14:30")
entry = [[
condition("close", "can1_sma1_p20", lower=0),
condition("time_filter", lower=1), # only trade 09:45–14:30
]]
result = run_backtest(df, entry)
Regime filter (ADX trend detection)
from mtrader import add_regime_filter_column
df = add_regime_filter_column(df, adx_period=14, adx_threshold=25.0)
entry = [[
condition("close", "can1_ema1_p20", lower=0),
condition("regime_filter", lower=1), # only trade trending markets
]]
result = run_backtest(df, entry)
# Invert for ranging markets: condition("regime_filter", upper=1)
Indicators
Moving averages
| Code | Description |
|---|---|
sma |
Simple Moving Average |
ema |
Exponential Moving Average |
wma |
Weighted Moving Average |
ssma |
Smoothed Simple Moving Average |
Popular technical indicators
| Code | Description |
|---|---|
rsi |
Relative Strength Index |
atr |
Average True Range |
macd |
MACD line, signal, histogram |
stochk / stochd |
Stochastic %K / %D |
bbp |
Bollinger Band %B |
cci |
Commodity Channel Index |
willr |
Williams %R |
adx |
Average Directional Index |
mfi |
Money Flow Index (volume-weighted) |
obv |
On-Balance Volume |
psar |
Parabolic SAR |
supertrend |
SuperTrend (direction 1/-1) |
ichimoku |
Ichimoku Cloud (5 lines) |
ha |
Heikin Ashi candles |
vwap / ewap / iwap |
Volume / Equal / Incremental WAP |
Candlestick patterns
| Code | Description |
|---|---|
inside_bar |
1 when bar inside previous bar's range |
engulfing |
1 bullish, -1 bearish engulfing |
Session-aware
| Code | Description |
|---|---|
or_high / or_low |
Opening range expanding high/low |
prev_day |
Previous day high/low/close |
pivot |
Classic pivot points (P, S1, S2, R1, R2) |
gap |
Gap up/down detection |
Feature codes (base signals)
| Code | Signal |
|---|---|
0, 1 |
close |
2 |
av2 (H+L)/2 |
3 |
av3 (H+L+C)/3 |
4 |
av4 (O+H+L+C)/4 |
5 |
open |
6 |
high |
7 |
low |
8..34 |
dif, ret, lret for 1/3/5/7/10/15/20/30/60 bars |
Normalized indicators
Prefix for ratio/statistical forms: SMN_, EMN_, WMN_, SSMN_, SVN_, EVN_, WVN_, SSVN_, Z_, BRN_, TMN_.
Suffix controls denominator: ""/F = signal itself, P/0 = close, B = base signal, numeric code = that feature.
df = add_indicators(df, add=["Z_sma1", "SMN_ema1", "EMN_rsi"], rolling_minutes=[14])
# can1_Z_sma1_p14 — Z-score of SMA(14)
# can1_SMN_ema1_p14 — EMA(14) / SMA(14)
Distance indicators
df = add_indicators(df, add=["smadis1", "emadis1"], rolling_minutes=[14])
# can1_smadis1_p14 — close - SMA(14)
# can1_emadis1_p14 — close - EMA(14)
Column naming
can1_{indicator}_p{period}
can1_{normalization}_{indicator}_p{period}
Examples:
can1_sma1_p20— SMA(20) of closecan1_rsi_p14— RSI(14)can1_macd— MACD line (no period)can1_supertrend_dir_p10— SuperTrend directioncan1_Z_sma1_p14— Z-score of SMA(14)can1_ichi_span_a— Ichimoku cloud span A
Conditions
Conditions compare two columns: (first - second) ∈ [lower, upper].
{
"first_column_name": "can1_sma1_p20",
"second_column_name": "close",
"shift_down_first": 0,
"shift_down_second": 0,
"lower_range_of_difference": 0, # first - second >= lower
"upper_range_of_difference": np.inf, # first - second <= upper
"perform_normalization_of_diff": False,
}
Helper functions
from mtrader import condition, cross_above, cross_below
cond = condition("close", "can1_sma1_p20", lower=0) # close > sma
crossover = cross_above("can1_macd", "can1_macdsignal") # MACD crossover
crossunder = cross_below("can1_stochk_p14", "can1_stochd_p14") # Stoch crossunder
AND / OR logic
AND within a group, OR across groups:
entry = [
# Group 1: both must be true
[
condition("close", "can1_sma1_p20", lower=0),
condition("can1_rsi_p14", lower=30, upper=70),
],
# Group 2: OR with Group 1
[
condition("can1_macd", "can1_macdsignal", lower=0),
],
]
update_cond helper
Modify conditions programmatically:
from mtrader import update_cond
base_entry = [[condition("ema9", "ema21", lower=0)]]
# Update ALL conditions (default behavior)
updated = update_cond(base_entry, "ema20", "ema50", lower=-5)
# Or update only matching a specific first_column_name
updated = update_cond(base_entry, "ema20", "ema50",
match_first="ema9") # only updates conditions with first="ema9"
Strategy object (serializable)
from mtrader import Strategy
strat = Strategy(
name="SMA Crossover",
indicators=["sma1", "ema1"],
rolling_minutes=[20, 50],
entry_conditions=cross_above("can1_sma1_p20", "can1_sma1_p50"),
exit_conditions=cross_below("can1_sma1_p20", "can1_sma1_p50"),
side="buy",
target_delta_normalized=0.5,
stoploss_delta_normalized=0.25,
capital_per_trade_pct=0.5,
max_trades_per_day=3,
cooldown_bars=3,
)
result = strat.run(df)
strat.save("strat.json")
loaded = Strategy.load("strat.json")
live_engine = strat.to_live(df) # convert to live engine with warmup
Exit Optimization
Find the best target/stoploss combination via grid search:
from mtrader import find_best_exit
best, results = find_best_exit(
df, entry_conditions, buy_or_sell="buy",
target_deltas=[50, 100, 150, 200],
stoploss_deltas=[25, 50, 75, 100],
metric="sharpe",
verbose=True,
)
print(best) # {'target_delta': 150, 'stoploss_delta': 75}
Entry condition optimization
Grid-search for the best entry threshold values:
from mtrader import find_best_entry_conditions
entry = [[
{"first_column_name": "can1_rsi_p14", "second_column_name": "zero",
"lower_range_of_difference": -np.inf, "upper_range_of_difference": 30, ...}
]]
best, results = find_best_entry_conditions(
df, entry,
condition_ranges={0: ([-np.inf, -np.inf], [20, 30])}, # try RSI < 20, RSI < 30
metric="sharpe",
)
print(best) # {'cond_0_lower': -inf, 'cond_0_upper': 20}
Walk-Forward Optimization
from mtrader import walk_forward_optimize, Strategy
results = walk_forward_optimize(
df,
strategy_factory=lambda params: Strategy(**params),
param_grid={"rolling_minutes": [[10, 30], [20, 50]]},
train_days=60,
test_days=20,
metric="sharpe",
)
Random parameter search
from mtrader import random_parameter_search
def factory(**kw):
return Strategy(name="Opt", indicators=["ema1"], rolling_minutes=[20],
entry_conditions=[[condition("can1_ema1_p20", "close", lower=0)]],
**kw)
best, results_df = random_parameter_search(
df, factory,
param_space={"target_delta_normalized": [0.25, 0.5, 0.75, 1.0],
"stoploss_delta_normalized": [0.125, 0.25, 0.5]},
n_iter=20, metric="sharpe",
)
Scenario Sweeper
Run the same strategy across multiple parameter combos and compare results:
from mtrader import run_scenarios
base = {
"entry_conditions": entry,
"initial_capital": 100000,
}
grid = {
"target_delta_normalized": [0.25, 0.5, 1.0, 2.0],
"stoploss_delta_normalized": [0.125, 0.25, 0.5],
}
scenarios = run_scenarios(df, base, grid, metric="sharpe", verbose=True)
# Returns sorted DataFrame: target, stoploss, final_capital, sharpe, sortino,
# calmar, win_rate, profit_factor, total_trades
best_params = scenarios.iloc[0]
Auto Strategy Discovery
Automatically generate, evaluate, and rank trading strategies from 7 indicator families:
from mtrader import discover_strategies
results, best_candidates = discover_strategies(
df,
train_days=5, # training window per fold
test_days=1, # test window per fold
strategy_types=["crossover", "threshold", "macd", "psar", "supertrend",
"vwap", "price_action"],
exit_targets=[0.5, 1.0], # try these target levels
exit_stops=[0.25, 0.5], # try these stoploss levels
metric="sharpe", # ranking metric
top_n=10, # walk-forward validate top 10
verbose=True,
)
# results: DataFrame with train/test scores per candidate
# best_candidates: list of StrategyCandidate objects for the top strategies
Quick-rank pre-defined strategies
Faster than discover_strategies — pre-computes all indicators once:
from mtrader import quick_rank_strategies, StrategyCandidate
strategies = [
StrategyCandidate(name="SMA Crossover", indicators=["sma1"],
rolling_minutes=[20, 50],
entry_conditions=[[_cond("can1_sma1_p20", "can1_sma1_p50", lower=0)]]),
StrategyCandidate(name="RSI Oversold", indicators=["rsi"],
rolling_minutes=[14],
entry_conditions=[[_cond("can1_rsi_p14", upper=30)]]),
]
ranked = quick_rank_strategies(df, strategies, metric="sharpe")
Trade log (enhanced)
The trade log now includes exit_reason and hold_bars columns:
result = run_backtest(df, entry, exit_conditions=exit_cond,
target_delta_normalized=1.0, stoploss_delta_normalized=0.5)
print(result.trades.columns)
# ['entry_index', 'entry_time', 'exit_index', 'exit_time', 'side',
# 'entry_price', 'exit_price', 'profit', 'return_pct', 'capital_at_exit',
# 'capital_before', 'capital_return_pct', 'exit_reason', 'hold_bars']
print(result.trades["exit_reason"].value_counts())
# target 14
# stoploss 7
# condition 3
# end 1
Exit reasons: "target" (hit profit target), "stoploss" (hit stop), "condition" (exit condition triggered), "end" (ran to end of data).
Performance reports
from mtrader import backtest_report, equity_curve, html_backtest_report
report = backtest_report(df, initial_capital=1000)
# total_trades, win_rate_pct, profit_factor, avg_win/loss_pct,
# sharpe_ratio, sortino_ratio, calmar_ratio, max_drawdown_pct,
# max_consecutive_wins/losses, ...
# Enhanced BacktestResult.metrics includes:
# Sharpe Ratio, Volatility, Max Drawdown, sortino_ratio,
# calmar_ratio, win_rate_pct, profit_factor, total_trades
eq = equity_curve(df)
# columns: datetime, equity, drawdown_pct, trade
html_backtest_report(result, output_path="report.html")
# standalone HTML with SVG charts, no external dependencies
Live trading
from mtrader import live_strategy_from_history, stream_live_signals
engine = live_strategy_from_history(
df, indicators=["sma1", "ema1", "rsi"],
periods=[20, 50, 14],
entry_conditions=cross_above("can1_sma1_p20", "can1_sma1_p50"),
side="buy",
)
for signal in stream_live_signals(engine, candle_feed):
if signal["action"] == "BUY":
place_order(signal)
Incremental indicators (SMA, EMA, RSI, ATR, VWAP) update in O(1) per bar.
Data cleaning
from mtrader import clean_data
cleaned = clean_data(
raw_df,
start_time="09:15", end_time="15:30",
start_date="2024-01-01", end_date="2024-12-31",
fill_gap=True, adjustsplit=True, multiplier=100,
)
Auto-detects: datetime columns, OHLCV columns, stock splits/reverse mergers, gap-fills to 1-min frequency.
Multi-timeframe indicators
from mtrader import add_higher_timeframe_indicators
# Compute 15-min EMA(5) and SMA(15) merged onto 1-min bars
df = add_higher_timeframe_indicators(
df, rule="15min", add=["sma1", "ema1"],
rolling_minutes=[5, 15], prefix="can15",
)
# Columns: can15_sma1_p5, can15_ema1_p15
# Entry: 1-min EMA > 15-min SMA AND 15-min EMA > 60-min SMA
entry = [[
condition("can1_ema1_p20", "can15_sma1_p15", lower=0),
condition("can15_ema1_p15", "can60_ema1_p5", lower=0),
]]
Signal Functions
crossover(A, B)
Detect crossovers between two arrays. Returns integer signals:
+N: A crossed above B N bars ago (A still above B)0: No recent crossover-N: A crossed below B N bars ago (A still below B)
import numpy as np
from mtrader.strategy_finder import crossover, just_crossed_up, just_crossed_down
# Example 1: Basic crossover detection
fast_ema = np.array([10, 12, 14, 13, 15, 17, 16, 14, 12, 15])
slow_ema = np.array([13, 13, 13, 13, 13, 13, 13, 13, 13, 13])
cross = crossover(fast_ema, slow_ema, lookback=5)
# → [ 0 0 1 0 1 2 3 4 -1 1]
# +1 = just crossed up (bar 2)
# +2 = crossed up 1 bar ago (bar 4)
# -1 = just crossed down (bar 8)
# Example 2: Just crossed (current bar only)
enter_now = just_crossed_up(fast_ema, slow_ema)
# → [0 0 1 0 1 0 0 0 0 1]
# Example 3: Entry within N bars of crossover
entry_window = (cross >= 1) & (cross <= 3)
# → Enter within 3 bars of bullish cross
pathsignal(raw, value1, value2)
Detect when a raw signal crosses between two levels. Auto-detects direction:
value1 < value2(bullish): raw was <= value1, now > value2value1 > value2(bearish): raw was >= value1, now < value2
import numpy as np
from mtrader.strategy_finder import pathsignal, pathsignal_cross
# Example 1: RSI oversold to recovery (bullish)
rsi = np.array([25, 28, 35, 42, 48, 55, 52, 45, 30, 58])
entry = pathsignal(rsi, value1=30, value2=50, lookback=10)
# → [0 0 0 0 0 1 1 0 0 1]
# RSI was <=30 at index 0-1, then crossed above 50 at index 5
# Example 2: RSI overbought to decline (bearish)
rsi2 = np.array([75, 72, 65, 58, 52, 45, 48, 55, 70, 42])
exit_sig = pathsignal(rsi2, value1=70, value2=50, lookback=10)
# → [0 0 0 0 0 1 1 0 0 1]
# RSI was >=70 at index 0-1, then dropped below 50 at index 5
# Example 3: Exact cross bar only
entry_exact = pathsignal_cross(rsi, value1=30, value2=50, lookback=10)
# → [0 0 0 0 0 1 0 0 0 1]
Limit parameter
Only generate signal when raw is within a specific range:
# Signal only when RSI is between 45-65
rsi = np.array([25, 28, 35, 42, 55, 65, 75, 55, 45, 58])
entry = pathsignal(rsi, value1=30, value2=50, limit=(45, 65))
# → [0 0 0 0 1 1 0 0 0 0]
# Index 4: RSI=55, in limit -> signal
# Index 6: RSI=75, out of limit -> no signal
Reset parameter
Control when the "was in state" condition resets:
# Default with limit: reset when RSI goes outside limit
rsi = np.array([25, 28, 35, 42, 55, 45, 35, 28, 42, 55])
entry = pathsignal(rsi, value1=30, value2=50, limit=(40, 60))
# → [0 0 0 0 1 0 0 0 0 1]
# Index 4: RSI=55, was<=30, in limit -> signal
# Index 6: RSI=35, outside limit -> reset!
# Index 9: RSI=55, was<=30 at index 7 -> signal again
# Explicit reset zones: reset if RSI < 20 or RSI > 80
rsi2 = np.array([25, 28, 35, 42, 55, 85, 45, 30, 42, 55])
entry2 = pathsignal(rsi2, value1=30, value2=50, reset=[(0, 20), (80, 100)])
# → [0 0 0 0 1 0 0 0 0 1]
# Index 5: RSI=85, in reset zone -> state resets!
Combining crossover + pathsignal
import numpy as np
from mtrader.strategy_finder import crossover, pathsignal
fast = np.array([10, 12, 14, 13, 15, 17, 16, 14, 12, 15])
slow = np.array([13, 13, 13, 13, 13, 13, 13, 13, 13, 13])
rsi = np.array([25, 28, 35, 42, 48, 55, 52, 45, 30, 58])
# Entry: EMA cross up + RSI was oversold
ema_cross = crossover(fast, slow, lookback=5)
rsi_signal = pathsignal(rsi, value1=30, value2=50, lookback=10)
combined_entry = (ema_cross >= 1) & (rsi_signal == 1)
# → Both conditions must be true for entry
Strategy Templates
from mtrader.strategy_finder import (
EMACrossover, SMACrossover, RSIReversion,
MACDCrossover, DonchianBreakout, BollingerBreakout,
)
# All templates have default_param_space() and generate_signals()
template = EMACrossover()
ps = template.default_param_space()
params = {k: v[0] for k, v in ps.to_dict().items()}
# Generate signals for a single symbol
entries, exits = template.generate_signals(df, params)
# Generate signals for multiple symbols at once
entries, exits = template.generate_signals_batch(dfs, params, n_bars=500)
Multi-symbol portfolio
from mtrader import run_portfolio
data = {"AAPL": df_aapl, "TSLA": df_tsla, "GOOG": df_goog}
result = run_portfolio(data, strategy, initial_capital=300000)
# result.equity — combined portfolio equity curve
# result.trades — all trades with symbol column
# result.results — per-symbol BacktestResult objects
Batch signal generation (100+ symbols)
Generate signals for multiple symbols at once:
from mtrader.strategy_finder import (
prepare_multi_symbol_data,
build_multi_symbol_arrays,
EMACrossover,
)
# Load all symbol DataFrames
dfs = {"SYM1": df1, "SYM2": df2, ..., "SYM100": df100}
# Align to same bar count
sliced = prepare_multi_symbol_data(dfs, n_bars=500)
# Generate signals for all symbols at once
template = EMACrossover()
entries, exits = template.generate_signals_batch(sliced, params, n_bars=500)
# entries.shape = (100, 500)
# exits.shape = (100, 500)
# Build kernel-ready arrays
closes, highs, lows, ent, ext, tps, sds, trd = build_multi_symbol_arrays(
sliced, entries, exits, target_pcts, stop_deltas, trail_deltas
)
Complete Strategy Examples
0. Multi-symbol v8 Donchian Breakout (recommended)
The v8 Donchian strategy is the best-tested multi-symbol strategy in mtrader.
It is a trend-following strategy that buys on a 20-day high breakout above a
100-day SMA and exits on a 3.8% target, a 4.0 * ATR trailing stop from
entry, a 10-day low breakdown, or a 103-day max hold. It is designed for
multi-symbol portfolio use with very low drawdown.
Quick start (single command):
import mtrader as mt
result = mt.run_v8_portfolio(
data_dir=r"C:\trade_data\sharemarket\all_nse_pure",
symbols=None, # None = use all CSVs in data_dir
timeframe="1D",
lookback_days=1825, # 5 years
initial_capital=1000.0,
trail_atr_mult=4.0, # looser stop = better return
verbose=True,
)
print(result["summary"])
Or use the strategy preset directly in a custom backtest:
from mtrader import (
make_v8_donchian_breakout, run_portfolio, add_indicators, add_atr_trailing_stop_from_entry_column
)
strat = make_v8_donchian_breakout(
entry_n=20, exit_n=10, sma_period=100, atr_period=10,
target_pct=0.038, max_hold_bars=103,
)
# Load and prep data per symbol...
# result = run_portfolio(data_by_symbol, strat, initial_capital=1000)
Typical results on 30+ NSE stocks over 5 years (research 2026-07-03):
- Total return:
+5%to+12%per year (depends on stock basket) - Max drawdown from peak:
1%to5% - Win rate:
60-80% - Profitable symbols:
70-95%of30
The trailing stop in the v8 strategy is critical. The default
trail_atr_mult=4.0 was found to be the best across two different
30-symbol universes; tighter values like 2.05 are more conservative
but give lower returns. The add_atr_trailing_stop_from_entry_column
helper is the recommended way to pre-compute the trailing stop on the
dataframe before backtest.
1. Short selling (RSI overbought fade)
result = run_backtest(
df, entry_conditions=[[condition("can1_rsi_p14", lower=70)]],
buy_or_sell="sell",
exit_conditions=[[condition("can1_rsi_p14", upper=40)]],
target_delta_normalized=0.5, stoploss_delta_normalized=0.25,
)
2. MACD + Bollinger Band squeeze
entry = [[
condition("can1_macd", "can1_macdsignal", lower=0),
condition("can1_bbp_p20", lower=0.2, upper=0.8),
]]
result = run_backtest(df, entry,
exit_conditions=[[condition("can1_macdsignal", "can1_macd", lower=0)]],
capital_per_trade_pct=0.5, max_trades_per_day=3)
3. Multi-TF trend with trailing stop
df = add_higher_timeframe_indicators(df, "15min", add=["ema1"], ...)
df = add_trailing_stop_column(df, trail_pct=0.5, lookback=20)
df["trail_delta"] = df["close"] - df["trailing_stop_price"]
df = add_time_filter_column(df, start_time="09:45", end_time="15:00")
df = add_regime_filter_column(df, adx_threshold=20)
entry = [[
condition("can1_ema1_p20", "can15_ema1_p15", lower=0),
condition("time_filter", lower=1),
condition("regime_filter", lower=1),
]]
result = run_backtest(df, entry, stoploss_delta_column="trail_delta",
capital_per_trade_pct=0.3, max_trades_per_day=2)
4. Ichimoku cloud breakout
entry = [[
condition("close", "can1_ichi_span_a", lower=0),
condition("close", "can1_ichi_span_b", lower=0),
]]
result = run_backtest(df, entry,
exit_conditions=[[condition("can1_ichi_kijun", "close", lower=0)]])
5. Opening range breakout with ATR exits
df = add_indicators(df, add=["or_high", "atr"], rolling_minutes=[15, 14])
df["target_2atr"] = df["can1_atr_p14"] * 2.0
df["stoploss_1atr"] = df["can1_atr_p14"]
entry = [[condition("close", "can1_or_high_p15", lower=0)]]
result = run_backtest(df, entry, target_delta_column="target_2atr",
stoploss_delta_column="stoploss_1atr")
6. SuperTrend + PSAR dual confirmation
entry = [[
condition("can1_supertrend_dir_p10", lower=1),
condition("close", "can1_psar", lower=0),
]]
result = run_backtest(df, entry,
exit_conditions=[[condition("can1_psar", "close", lower=0)]])
7. Long-short combined
r_long = run_backtest(df, long_entry, buy_or_sell="buy")
r_short = run_backtest(df, short_entry, buy_or_sell="sell")
combined_pnl = r_long.final_capital + r_short.final_capital - 2 * initial_capital
Modules
| Module | Key exports |
|---|---|
data_cleaner |
clean_data, detect_data_types_with_formats, fill_missing_rows |
indicators |
All standalone indicator functions (ema, rsi, macd, psar, ichimoku, ...) |
indicator_engine |
add_indicators, add_indicators_on_group, FEATURE_CODE |
exit_strategy |
precalculate_exit_time_amount_profit |
trading |
take_trade_on_condition*, update_cond |
optimize_exit |
find_best_exit |
backtest |
run_backtest, BacktestResult, condition, cross_above/below, walk_forward_splits, parameter_grid, trade_log, run_scenarios, find_best_entry_conditions |
advanced |
Strategy, CostModel, sizing functions, run_portfolio, walk_forward_optimize, random_parameter_search, resample_ohlcv, add_higher_timeframe_indicators, |
add_trailing_stop_column, add_atr_trailing_stop_from_entry_column, add_time_filter_column, add_regime_filter_column, apply_risk_controls, make_v8_donchian_breakout, run_v8_portfolio |
|
strategy_discovery |
discover_strategies, quick_rank_strategies, StrategyCandidate |
live |
LiveIndicatorEngine, LiveStrategyEngine, stream_live_signals, live_strategy_from_history |
report |
backtest_report, equity_curve, html_backtest_report |
monotonic_stack |
monotonic_stack_for_value1_gt/lt_value2 |
utils |
timenum |
Testing
python -m pytest tests/ -v
60+ tests cover: data cleaning, all indicators, exit precalculation, trade simulation, exit optimization, performance reports, strategy scenarios, edge cases, position sizing, risk controls, trailing stop, time/regime filters, entry optimization, scenario sweeper, exit reason tagging, auto-detect indicators, and strategy pipelines.
Dependencies
- Required:
numpy>=1.21,pandas>=1.3,inspecty>=0.1 - Optional:
numba>=0.58(monotonic stack),cupy-cuda12x(GPU trading)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mtrader-0.9.6.tar.gz.
File metadata
- Download URL: mtrader-0.9.6.tar.gz
- Upload date:
- Size: 124.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac5cc48bb259dab61d9f900cdc0364c9d548c661282c64e440ae58fddf710462
|
|
| MD5 |
33bd12d6cd676eb2bf0773f18a8c93f8
|
|
| BLAKE2b-256 |
15ac473062ad848fa0748e5fd33bf44697c480f4c7553c2820d253badec47af6
|
File details
Details for the file mtrader-0.9.6-py3-none-any.whl.
File metadata
- Download URL: mtrader-0.9.6-py3-none-any.whl
- Upload date:
- Size: 105.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad73450c532496e87f8704572cade05a67a8d32fb1d3b5be038987b75c9b4cc2
|
|
| MD5 |
333e651fff6b582daabc68786ee48df2
|
|
| BLAKE2b-256 |
74ed4c33e647b37d51ffefa094d4f52a5bccc9e2b6f262c95ea465e4cfc65349
|