Backtesting sim
Skill mahmoud20138/Tradecraft/plugins/tradecraft/skills/backtesting-sim
102 Claude Code skills across 7 categories -- trading strategies, Azure, VSCode extensions, AI prompts, and custom automation skills
npx -y skills add mahmoud20138/Tradecraft --skill backtesting-simAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Backtesting and simulation: vectorized backtesting, paper trading simulation, strategy A/B testing, automated strategy building, natural language to strategy, and trading plan generation. USE FOR: backtest, backtesting, paper trading, simulation, strategy builder, A/B test strategies, natural language strategy, trading plan, equity curve, drawdown analysis, walk-forward, Monte Carlo simulation, performance metrics, Sharpe, Sortino, Calmar, win rate, profit factor, expectancy, strategy validation, overfitting prevention, survivorship bias, look-ahead bias.
SKILL.md
45.2 KB, as published. Nobody here has run it
Skill: Backtesting Sim | Domain: trading | Category: quantitative | Level: intermediate Tags:
trading,quant,backtesting,simulation,paper-trading,vectorized
Vectorized Backtester
Vectorized Backtester
import pandas as pd
import numpy as np
from typing import Callable, Optional
# ── Shared helpers ──────────────────────────────────────────────────────────
def _sharpe(r: np.ndarray, rfr: float = 0.04 / 252) -> float:
std = r.std(ddof=1)
return float((r.mean() - rfr) / std * np.sqrt(252)) if std > 0 else 0.0
def _sortino(r: np.ndarray, rfr: float = 0.04 / 252) -> float:
down = r[r < rfr]
std_dn = down.std(ddof=1) if len(down) > 1 else 0.0
return float((r.mean() - rfr) / std_dn * np.sqrt(252)) if std_dn > 0 else 0.0
def _calmar(r: np.ndarray) -> float:
eq = np.cumprod(1 + r)
peak = np.maximum.accumulate(eq)
max_dd = abs(((eq - peak) / peak).min())
ann_ret = eq[-1] ** (252 / len(r)) - 1
return float(ann_ret / max_dd) if max_dd > 0 else 0.0
def _profit_factor(r: np.ndarray) -> float:
gains = r[r > 0].sum()
losses = abs(r[r < 0].sum())
return float(gains / losses) if losses > 0 else float("inf")
class VectorizedBacktester:
"""
Production-quality vectorised backtester with full performance metrics.
Signals must be pre-shifted (no look-ahead bias).
Supports transaction costs, position sizing, and walk-forward validation.
Example
-------
>>> df["signal"] = np.sign(df["close"].pct_change(5)) # 5-bar momentum
>>> result = VectorizedBacktester.backtest(df, spread_bps=2.0)
>>> print(result["sharpe"], result["max_drawdown_pct"])
1.34 -12.5
"""
@staticmethod
def backtest(
df: pd.DataFrame,
signal_col: str = "signal",
spread_bps: float = 2.0,
slippage_bps: float = 1.0,
initial_capital: float = 10_000.0,
position_size: float = 1.0,
risk_free_annual: float = 0.04,
) -> dict:
"""
Vectorised backtest engine.
Parameters
----------
df : OHLCV DataFrame with a signal column
signal_col : column name for signal (1=long, -1=short, 0=flat)
spread_bps : round-trip spread cost in basis points
slippage_bps : round-trip slippage estimate in basis points
initial_capital : starting capital
position_size : fraction of capital at risk (1.0 = fully invested)
risk_free_annual: used for Sharpe / Sortino calculations
Returns
-------
Comprehensive dict including Sharpe, Sortino, Calmar, max DD,
win rate, profit factor, equity curve, and full annotated DataFrame.
Notes
-----
ALL signals are shifted by 1 bar to prevent look-ahead.
Costs are charged on position changes (not every bar).
"""
if signal_col not in df.columns:
raise KeyError(f"Column '{signal_col}' not found in DataFrame")
if len(df) < 10:
raise ValueError("Need at least 10 bars to backtest")
out = df.copy()
cost_rt = (spread_bps + slippage_bps) / 10_000.0 # Round-trip cost fraction
out["returns"] = out["close"].pct_change()
out["position"] = out[signal_col].shift(1).fillna(0) * position_size
out["trade"] = out["position"].diff().abs().fillna(0)
out["gross_return"] = out["position"] * out["returns"]
out["cost"] = out["trade"] * cost_rt
out["net_return"] = out["gross_return"] - out["cost"]
# Equity curve (multiplicative)
out["equity"] = initial_capital * (1 + out["net_return"]).cumprod()
out["peak"] = out["equity"].cummax()
out["drawdown"] = (out["equity"] - out["peak"]) / out["peak"]
# Trade-level stats
out["trade_entry"] = (out["position"] != 0) & (out["position"].shift(1) == 0)
out["trade_exit"] = (out["position"] == 0) & (out["position"].shift(1) != 0)
n_trades = int(out["trade_entry"].sum())
net = out["net_return"].dropna().values
rfr_d = risk_free_annual / 252
# Compute comprehensive metrics from net returns
total_return = float((out["equity"].iloc[-1] / initial_capital - 1) * 100)
max_dd = float(out["drawdown"].min() * 100)
sharpe = _sharpe(net, rfr_d)
sortino = _sortino(net, rfr_d)
calmar = _calmar(net)
pf = _profit_factor(net)
# Win rate on closed trades (more accurate than bar-level)
exit_returns = out.loc[out["trade_exit"], "net_return"]
win_rate = float((exit_returns > 0).mean() * 100) if len(exit_returns) > 0 else 0.0
# Consecutive loss streak
signs = np.sign(net)
streak = 0
max_streak = 0
for s in signs:
if s < 0:
streak += 1
max_streak = max(max_streak, streak)
else:
streak = 0
return {
"total_return_pct": round(total_return, 2),
"ann_return_pct": round(float((out["equity"].iloc[-1] / initial_capital)
** (252 / max(len(net), 1)) - 1) * 100, 2),
"sharpe": round(sharpe, 3),
"sortino": round(sortino, 3),
"calmar": round(calmar, 3),
"max_drawdown_pct": round(max_dd, 2),
"profit_factor": round(pf, 3),
"n_trades": n_trades,
"win_rate": round(win_rate, 1),
"max_consec_losses": int(max_streak),
"total_costs_pct": round(float(out["cost"].sum() * 100), 2),
"equity_curve": out["equity"],
"drawdown_series": out["drawdown"],
"df": out,
}
@staticmethod
def walk_forward_backtest(
df: pd.DataFrame,
signal_fn: Callable,
optimize_fn: Callable,
train_bars: int = 500,
test_bars: int = 100,
min_folds: int = 3,
**kwargs,
) -> dict:
"""
Anchored walk-forward validation.
Parameters
----------
signal_fn : callable(df, params) → signal Series
optimize_fn : callable(train_df) → params dict
train_bars : in-sample training window
test_bars : out-of-sample test window per fold
min_folds : minimum folds required for a valid result
Returns
-------
dict with per-fold and aggregate OOS statistics.
"""
results: list[dict] = []
all_equity: list[pd.Series] = []
for start in range(0, len(df) - train_bars - test_bars, test_bars):
train = df.iloc[start: start + train_bars]
test = df.iloc[start + train_bars: start + train_bars + test_bars].copy()
try:
params = optimize_fn(train)
test["signal"] = signal_fn(test, params)
bt = VectorizedBacktester.backtest(test, **kwargs)
results.append({
"fold": len(results),
"sharpe": bt["sharpe"],
"sortino": bt["sortino"],
"return": bt["total_return_pct"],
"max_dd": bt["max_drawdown_pct"],
"params": params,
})
all_equity.append(bt["equity_curve"])
except Exception as e:
results.append({"fold": len(results), "error": str(e)})
valid = [r for r in results if "sharpe" in r]
if len(valid) < min_folds:
return {
"method": "walk_forward",
"error": f"Only {len(valid)} valid folds (need {min_folds})",
"n_folds": len(results),
}
sharpes = [r["sharpe"] for r in valid]
sortinos = [r["sortino"] for r in valid]
returns = [r["return"] for r in valid]
# Concatenate OOS equity curves for a continuous equity line
oos_equity = pd.concat(all_equity).sort_index() if all_equity else pd.Series(dtype=float)
return {
"method": "walk_forward",
"n_folds": len(results),
"n_valid_folds": len(valid),
"avg_sharpe": round(float(np.mean(sharpes)), 3),
"median_sharpe": round(float(np.median(sharpes)), 3),
"std_sharpe": round(float(np.std(sharpes, ddof=1)), 3),
"pct_folds_positive": round(float(np.mean([r > 0 for r in returns])) * 100, 1),
"avg_return": round(float(np.mean(returns)), 2),
"avg_sortino": round(float(np.mean(sortinos)), 3),
"fold_results": valid,
"oos_equity": oos_equity,
"WARNING": "Past performance ≠ future results. OOS validation required.",
}
@staticmethod
def compare_strategies(
df: pd.DataFrame,
strategies: dict[str, pd.Series],
spread_bps: float = 2.0,
slippage_bps: float = 1.0,
initial_capital: float = 10_000.0,
) -> pd.DataFrame:
"""
Run multiple strategies on the same data and compare side-by-side.
Parameters
----------
strategies : {"name": signal_series, ...}
Returns
-------
DataFrame ranked by Sharpe ratio.
"""
results: list[dict] = []
for name, signal_series in strategies.items():
df_copy = df.copy()
df_copy["signal"] = signal_series
try:
bt = VectorizedBacktester.backtest(
df_copy,
spread_bps=spread_bps,
slippage_bps=slippage_bps,
initial_capital=initial_capital,
)
results.append({
"strategy": name,
"return": bt["total_return_pct"],
"sharpe": bt["sharpe"],
"sortino": bt["sortino"],
"calmar": bt["calmar"],
"max_dd": bt["max_drawdown_pct"],
"n_trades": bt["n_trades"],
"win_rate": bt["win_rate"],
"profit_factor": bt["profit_factor"],
})
except Exception as e:
results.append({"strategy": name, "error": str(e)})
return pd.DataFrame(results).sort_values("sharpe", ascending=False)
Trade Simulator Paper
Trade Simulator Paper
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Optional
class PaperTradeSimulator:
"""
Realistic paper trading simulator with proper spread/slippage modelling,
margin tracking, and full trade history for post-session analysis.
Uses a reproducible RNG (np.random.default_rng) for slippage simulation.
Example
-------
>>> sim = PaperTradeSimulator(initial_balance=10_000, seed=42)
>>> sim.open_trade("EURUSD", "buy", lots=0.1, price=1.0850, sl=1.0810, tp=1.0920)
>>> closed = sim.check_positions({"EURUSD": 1.0920})
>>> print(closed[0]["reason"], closed[0]["pnl_usd"])
'TP' 70.0
"""
PIP_VALUES: dict[str, float] = {
"default": 10.0, # USD per pip per standard lot
"USDJPY": 9.09, # approximate (varies with rate)
"XAUUSD": 10.0,
}
def __init__(
self,
initial_balance: float = 10_000.0,
leverage: float = 30.0,
pip_factor: float = 10_000.0,
seed: int = 42,
):
"""
Parameters
----------
initial_balance : starting account balance in USD
leverage : maximum leverage (used for margin check)
pip_factor : price-to-pip conversion (10000 for most FX, 100 for JPY pairs)
seed : RNG seed for reproducible slippage
"""
if initial_balance <= 0:
raise ValueError("initial_balance must be positive")
self.balance = initial_balance
self.equity = initial_balance
self.leverage = leverage
self.pip_factor = pip_factor
self._rng = np.random.default_rng(seed)
self.positions: list[dict] = []
self.history: list[dict] = []
self._trade_id = 0
# ── Position management ────────────────────────────────────────────────
def open_trade(
self,
symbol: str,
direction: str,
lots: float,
price: float,
sl: float,
tp: float,
spread: float = 0.0002,
max_slippage: float = 0.00005,
comment: str = "",
) -> dict:
"""
Open a simulated position with spread and random slippage applied.
Parameters
----------
symbol : instrument (e.g. 'EURUSD')
direction : 'buy' or 'sell'
lots : position size in standard lots
price : requested entry price
sl : stop-loss price
tp : take-profit price
spread : half-spread to apply (price units, not pips)
max_slippage : maximum slippage magnitude (price units)
comment : free-text label for this trade
Returns
-------
dict with status, actual_entry, slippage_pips, and trade metadata.
"""
direction = direction.lower()
if direction not in ("buy", "sell"):
raise ValueError(f"direction must be 'buy' or 'sell', got '{direction}'")
if lots <= 0:
raise ValueError("lots must be positive")
if sl <= 0 or tp <= 0:
raise ValueError("sl and tp must be positive price levels")
slippage = float(self._rng.uniform(0, max_slippage))
if direction == "buy":
actual_entry = price + spread + slippage
else:
actual_entry = price - spread - slippage
# Margin check
required_margin = actual_entry * lots * 100_000 / self.leverage
if required_margin > self.equity:
return {"status": "REJECTED", "reason": "Insufficient margin",
"required_margin": round(required_margin, 2), "equity": round(self.equity, 2)}
self._trade_id += 1
pos = {
"id": self._trade_id,
"symbol": symbol,
"direction": direction,
"lots": lots,
"entry": round(actual_entry, 5),
"requested": round(price, 5),
"sl": round(sl, 5),
"tp": round(tp, 5),
"spread_pips": round(spread * self.pip_factor, 1),
"slippage_pips": round(slippage * self.pip_factor, 1),
"open_time": datetime.utcnow().isoformat(),
"comment": comment,
}
self.positions.append(pos)
return {"status": "OPENED", **pos}
def check_positions(self, current_prices: dict[str, float]) -> list[dict]:
"""
Check all open positions against current prices and close those
that have hit SL or TP.
Parameters
----------
current_prices : {symbol: bid_price} — use bid for sells, ask for buys
(simplified: pass mid-price for paper trading)
Returns
-------
list of closed trade dicts (empty if nothing closed this tick).
"""
closed: list[dict] = []
remaining: list[dict] = []
for pos in self.positions:
price = current_prices.get(pos["symbol"])
if price is None:
remaining.append(pos)
continue
hit_sl = hit_tp = False
if pos["direction"] == "buy":
hit_sl = price <= pos["sl"]
hit_tp = price >= pos["tp"]
else:
hit_sl = price >= pos["sl"]
hit_tp = price <= pos["tp"]
if hit_sl or hit_tp:
exit_price = pos["sl"] if hit_sl else pos["tp"]
reason = "SL" if hit_sl else "TP"
pip_val = self.PIP_VALUES.get(pos["symbol"], self.PIP_VALUES["default"])
if pos["direction"] == "buy":
pnl_pips = (exit_price - pos["entry"]) * self.pip_factor
else:
pnl_pips = (pos["entry"] - exit_price) * self.pip_factor
pnl_usd = pnl_pips * pip_val * pos["lots"]
self.balance += pnl_usd
self.equity = self.balance # simplified (no floating P&L)
trade_result = {
**pos,
"exit": round(exit_price, 5),
"reason": reason,
"pnl_pips": round(pnl_pips, 1),
"pnl_usd": round(pnl_usd, 2),
"close_time": datetime.utcnow().isoformat(),
}
closed.append(trade_result)
else:
remaining.append(pos)
self.positions = remaining
self.history.extend(closed)
return closed
def close_all(self, current_prices: dict[str, float]) -> list[dict]:
"""Force-close all open positions at current prices (market close)."""
for pos in self.positions:
price = current_prices.get(pos["symbol"])
if price:
current_prices[pos["symbol"]] = pos["sl"] if pos["direction"] == "buy" else pos["tp"]
return self.check_positions(current_prices)
# ── Analytics ──────────────────────────────────────────────────────────
def account_summary(self) -> dict:
"""Return current account state and session statistics."""
wins = [t for t in self.history if t.get("pnl_usd", 0) > 0]
losses = [t for t in self.history if t.get("pnl_usd", 0) < 0]
total_pnl = sum(t.get("pnl_usd", 0) for t in self.history)
return {
"balance": round(self.balance, 2),
"equity": round(self.equity, 2),
"open_trades": len(self.positions),
"closed_trades": len(self.history),
"win_rate": round(len(wins) / max(len(self.history), 1) * 100, 1),
"total_pnl": round(total_pnl, 2),
"avg_win_usd": round(float(np.mean([t["pnl_usd"] for t in wins])), 2) if wins else 0.0,
"avg_loss_usd": round(float(np.mean([t["pnl_usd"] for t in losses])), 2) if losses else 0.0,
"profit_factor": round(
sum(t["pnl_usd"] for t in wins) / max(abs(sum(t["pnl_usd"] for t in losses)), 0.01), 3
) if losses else float("inf"),
}
Strategy Ab Tester
Strategy A/B Tester
import pandas as pd
import numpy as np
from scipy import stats
class StrategyABTester:
"""
Head-to-head strategy comparison with parametric and non-parametric
significance tests, plus rolling performance analysis.
Example
-------
>>> result = StrategyABTester.compare(returns_a, returns_b)
>>> print(result["winner"], result["statistically_significant"])
'Strategy A' True
"""
@staticmethod
def _metrics(r: pd.Series, name: str, rfr: float = 0.04 / 252) -> dict:
"""Compute a full metric set for one strategy."""
r = r.dropna()
std = float(r.std(ddof=1))
equity = (1 + r).cumprod()
max_dd = float(((equity / equity.cummax()) - 1).min())
sharpe = float((r.mean() - rfr) / std * np.sqrt(252)) if std > 0 else 0.0
# Sortino: penalise downside vol only
dn = r[r < rfr]
dn_std = float(dn.std(ddof=1)) if len(dn) > 1 else 0.0
sortino = float((r.mean() - rfr) / dn_std * np.sqrt(252)) if dn_std > 0 else 0.0
# Calmar: annualised return / max drawdown
ann_ret = float(equity.iloc[-1] ** (252 / max(len(r), 1)) - 1)
calmar = float(ann_ret / abs(max_dd)) if max_dd < 0 else 0.0
return {
"name": name,
"total_return": round(float((equity.iloc[-1] - 1) * 100), 2),
"ann_return": round(ann_ret * 100, 2),
"sharpe": round(sharpe, 3),
"sortino": round(sortino, 3),
"calmar": round(calmar, 3),
"max_drawdown": round(max_dd * 100, 2),
"volatility_ann": round(std * np.sqrt(252) * 100, 2),
"win_rate": round(float((r > 0).mean() * 100), 1),
"skewness": round(float(r.skew()), 3),
"kurtosis": round(float(r.kurtosis()), 3),
"n_periods": len(r),
}
@staticmethod
def compare(
returns_a: pd.Series,
returns_b: pd.Series,
name_a: str = "Strategy A",
name_b: str = "Strategy B",
risk_free: float = 0.04 / 252,
alpha: float = 0.05,
) -> dict:
"""
Comprehensive head-to-head comparison with four significance tests.
Parameters
----------
returns_a / b : daily return Series for each strategy
alpha : significance level (default 5 %)
Returns
-------
dict with metrics for both strategies, statistical test results,
winner, and actionable recommendation.
"""
m_a = StrategyABTester._metrics(returns_a, name_a, risk_free)
m_b = StrategyABTester._metrics(returns_b, name_b, risk_free)
n = min(len(returns_a.dropna()), len(returns_b.dropna()))
ra = returns_a.dropna().values[:n]
rb = returns_b.dropna().values[:n]
# 1. Paired t-test
t_stat, t_pval = stats.ttest_rel(ra, rb)
# 2. Wilcoxon signed-rank (non-parametric)
try:
w_stat, w_pval = stats.wilcoxon(ra, rb)
except Exception:
w_stat, w_pval = 0.0, 1.0
# 3. Kolmogorov-Smirnov (distribution difference)
ks_stat, ks_pval = stats.ks_2samp(ra, rb)
# 4. Jobson-Korkie Sharpe difference test
sharpe_diff = m_a["sharpe"] - m_b["sharpe"]
corr = float(pd.Series(ra).corr(pd.Series(rb)))
se_diff = np.sqrt(
(2 / n) * (1 - corr + 0.5 * m_a["sharpe"]**2 + 0.5 * m_b["sharpe"]**2)
)
jk_z = sharpe_diff / max(se_diff, 1e-10)
jk_pval = float(2 * (1 - stats.norm.cdf(abs(jk_z))))
# Determine winner and significance (require at least t-test significant)
better = name_a if m_a["sharpe"] > m_b["sharpe"] else name_b
significant = float(t_pval) < alpha
return {
"strategy_a": m_a,
"strategy_b": m_b,
"winner": better,
"sharpe_advantage": round(abs(sharpe_diff), 3),
"statistically_significant": significant,
"n_shared_periods": n,
"return_correlation": round(corr, 3),
"tests": {
"paired_ttest": {"t_stat": round(float(t_stat), 3), "p_value": round(float(t_pval), 4), "significant": float(t_pval) < alpha},
"wilcoxon": {"w_stat": round(float(w_stat), 3), "p_value": round(float(w_pval), 4), "significant": float(w_pval) < alpha},
"ks_test": {"ks_stat": round(float(ks_stat), 3), "p_value": round(float(ks_pval), 4), "significant": float(ks_pval) < alpha},
"sharpe_difference": {"z_stat": round(jk_z, 3), "p_value": round(jk_pval, 4), "significant": jk_pval < alpha},
},
"verdict": (
f"{better} is SIGNIFICANTLY better (t-test p={t_pval:.4f}). Confidence is high."
if significant else
f"{better} leads but NOT significantly (p={t_pval:.4f}). Difference may be noise."
),
"recommendation": (
f"Deploy {better}."
if significant else
"Run both over a longer sample or run walk-forward validation before deciding."
),
}
@staticmethod
def rolling_comparison(
returns_a: pd.Series,
returns_b: pd.Series,
window: int = 60,
) -> pd.DataFrame:
"""
Rolling Sharpe comparison to identify regime-dependent leadership.
Returns
-------
DataFrame with columns: sharpe_a, sharpe_b, difference, a_leads (bool),
rolling_alpha (A return - B return, annualised %).
"""
def _rolling_sharpe(r: pd.Series) -> pd.Series:
mu = r.rolling(window).mean()
std = r.rolling(window).std(ddof=1)
return (mu / std.replace(0, np.nan)) * np.sqrt(252)
sa = _rolling_sharpe(returns_a).rename("sharpe_a")
sb = _rolling_sharpe(returns_b).rename("sharpe_b")
# Rolling alpha: annualised outperformance of A over B
rolling_alpha = (
(returns_a - returns_b).rolling(window).mean() * 252 * 100
).rename("rolling_alpha_pct")
return pd.DataFrame({
"sharpe_a": sa,
"sharpe_b": sb,
"difference": sa - sb,
"a_leads": sa > sb,
"rolling_alpha_pct": rolling_alpha,
})
Automated Strategy Builder
Automated Strategy Builder
class AutoStrategyBuilder:
"""
Parse natural-language strategy descriptions and generate executable Python signal code.
Maps trigger phrases to pandas-based code snippets, extracts numeric parameters
from context, infers trade direction, and outputs a ready-to-run signal function.
Example
-------
>>> result = AutoStrategyBuilder.parse_description(
... "Buy when RSI is below 30 and price crosses above the 50 EMA"
... )
>>> print(result["direction"], result["can_auto_generate"])
'long' True
>>> print(result["generated_code"])
"""
CONDITION_MAP: dict[str, str] = {
"rsi above": "df['rsi_{p}'] > {value}",
"rsi below": "df['rsi_{p}'] < {value}",
"price above sma": "df['close'] > df['close'].rolling({value}).mean()",
"price below sma": "df['close'] < df['close'].rolling({value}).mean()",
"price above ema": "df['close'] > df['close'].ewm(span={value}).mean()",
"price below ema": "df['close'] < df['close'].ewm(span={value}).mean()",
"macd cross up": "(df['macd'] > df['signal']) & (df['macd'].shift(1) <= df['signal'].shift(1))",
"macd cross down": "(df['macd'] < df['signal']) & (df['macd'].shift(1) >= df['signal'].shift(1))",
"bollinger lower": "df['close'] < df['close'].rolling(20).mean() - 2 * df['close'].rolling(20).std()",
"bollinger upper": "df['close'] > df['close'].rolling(20).mean() + 2 * df['close'].rolling(20).std()",
"volume above average": "df['volume'] > df['volume'].rolling(20).mean() * 1.5",
"new high": "df['close'] > df['close'].rolling({value}).max().shift(1)",
"new low": "df['close'] < df['close'].rolling({value}).min().shift(1)",
"bullish engulfing": "(df['close'] > df['open']) & (df['close'] > df['open'].shift(1)) & (df['open'] < df['close'].shift(1))",
"bearish engulfing": "(df['close'] < df['open']) & (df['close'] < df['open'].shift(1)) & (df['open'] > df['close'].shift(1))",
"above cloud": "df['close'] > df[['senkou_a','senkou_b']].max(axis=1)",
"below cloud": "df['close'] < df[['senkou_a','senkou_b']].min(axis=1)",
"golden cross": "(df['close'].rolling(50).mean() > df['close'].rolling(200).mean()) & (df['close'].rolling(50).mean().shift(1) <= df['close'].rolling(200).mean().shift(1))",
"death cross": "(df['close'].rolling(50).mean() < df['close'].rolling(200).mean()) & (df['close'].rolling(50).mean().shift(1) >= df['close'].rolling(200).mean().shift(1))",
"atr breakout": "df['high'] - df['low'] > {value} * df['atr']",
}
_LONG_WORDS = {"buy", "long", "bullish", "uptrend", "above", "breakout up"}
_SHORT_WORDS = {"sell", "short", "bearish", "downtrend", "below", "breakdown"}
@staticmethod
def _extract_number(text: str, default: int = 14) -> int:
"""Extract the first integer from a text string."""
import re
m = re.search(r"\b(\d{1,3})\b", text)
return int(m.group(1)) if m else default
@staticmethod
def parse_description(text: str) -> dict:
"""
Parse a natural-language strategy description into structured conditions
and optionally generate executable Python signal code.
Parameters
----------
text : free-form strategy description
Returns
-------
dict with parsed_conditions, direction, generated_code, and completeness assessment.
"""
import re
text_lower = text.lower()
conditions: list[dict] = []
for trigger, code_template in AutoStrategyBuilder.CONDITION_MAP.items():
if trigger in text_lower:
# Extract a numeric parameter from the surrounding context
context = text_lower[max(text_lower.find(trigger) - 20, 0):
text_lower.find(trigger) + 40]
param = AutoStrategyBuilder._extract_number(context)
code = code_template.replace("{value}", str(param)).replace("{p}", str(param))
conditions.append({
"natural": trigger,
"code": code,
"param": param,
})
# Direction inference
long_hits = sum(w in text_lower for w in AutoStrategyBuilder._LONG_WORDS)
short_hits = sum(w in text_lower for w in AutoStrategyBuilder._SHORT_WORDS)
direction = "long" if long_hits > short_hits else "short" if short_hits > long_hits else "unknown"
# Generate executable signal code when conditions are available
generated_code = ""
if len(conditions) >= 1:
signal_val = "1" if direction == "long" else "-1" if direction == "short" else "1"
cond_lines = "\n & ".join(f"({c['code']})" for c in conditions)
generated_code = (
"import numpy as np\n\n"
"def generate_signal(df: pd.DataFrame) -> pd.Series:\n"
f" mask = (\n {cond_lines}\n )\n"
f" signal = pd.Series(0, index=df.index)\n"
f" signal[mask] = {signal_val}\n"
" return signal.shift(1).fillna(0) # Shift to prevent look-ahead\n"
)
return {
"parsed_conditions": conditions,
"direction": direction,
"n_conditions": len(conditions),
"can_auto_generate": len(conditions) >= 2,
"generated_code": generated_code,
"next_step": (
"Generated code ready — add to a DataFrame and backtest with VectorizedBacktester."
if len(conditions) >= 2 else
"Add more specific conditions (e.g., 'RSI below 30', 'price above 50 EMA') for code generation."
),
}
Natural Language To Strategy
Natural Language To Strategy
class NLStrategyParser:
"""
Parse a free-text strategy description into structured entry, exit, risk,
and filter rules — ready for review or downstream code generation.
Example
-------
>>> result = NLStrategyParser.parse(
... "Buy when RSI crosses above 30. Exit when RSI exceeds 70 or SL at 1%. "
... "Only trade during London session. Avoid Fridays."
... )
>>> print(result["completeness"])
'COMPLETE'
"""
_ENTRY_WORDS = {"enter", "buy", "sell", "open", "go long", "go short", "long when", "short when"}
_EXIT_WORDS = {"exit", "close", "take profit", "stop loss", "stop-loss", "tp", "sl at"}
_RISK_WORDS = {"risk", "lot size", "position size", "max loss", "2r", "1r", "r:r", "risk reward"}
_FILTER_WORDS = {"only", "filter", "avoid", "when", "must", "unless", "not on", "session", "news"}
@staticmethod
def parse(text: str) -> dict:
"""
Parse strategy text into structured rule categories.
Parameters
----------
text : multi-sentence strategy description
Returns
-------
dict with categorised rules, completeness score, and a gap analysis
highlighting what is missing before the strategy can be coded.
"""
import re
rules: dict[str, list[str]] = {"entry": [], "exit": [], "risk": [], "filters": []}
sentences = re.split(r"[.;!\n]", text)
for s in sentences:
s_stripped = s.strip()
s_lower = s_stripped.lower()
if not s_lower:
continue
# Categorise by keyword presence (in order of specificity)
if any(w in s_lower for w in NLStrategyParser._RISK_WORDS):
rules["risk"].append(s_stripped)
elif any(w in s_lower for w in NLStrategyParser._EXIT_WORDS):
rules["exit"].append(s_stripped)
elif any(w in s_lower for w in NLStrategyParser._ENTRY_WORDS):
rules["entry"].append(s_stripped)
elif any(w in s_lower for w in NLStrategyParser._FILTER_WORDS):
rules["filters"].append(s_stripped)
# Gap analysis
gaps: list[str] = []
if not rules["entry"]: gaps.append("No entry rule — when do you enter?")
if not rules["exit"]: gaps.append("No exit rule — when do you close?")
if not rules["risk"]: gaps.append("No risk rule — what % do you risk per trade?")
has_entry = bool(rules["entry"])
has_exit = bool(rules["exit"])
has_risk = bool(rules["risk"])
score = sum([has_entry, has_exit, has_risk])
completeness = (
"COMPLETE" if score == 3 else
"MOSTLY COMPLETE" if score == 2 else
"INCOMPLETE"
)
return {
"parsed_rules": rules,
"n_entry_rules": len(rules["entry"]),
"n_exit_rules": len(rules["exit"]),
"n_risk_rules": len(rules["risk"]),
"n_filters": len(rules["filters"]),
"completeness": completeness,
"completeness_score": score,
"gaps": gaps,
"ready_to_code": score == 3,
"next_step": (
"Strategy is well-defined. Use AutoStrategyBuilder to generate signal code."
if score == 3 else
f"Fill in missing rules: {'; '.join(gaps)}"
),
}
Trading Plan Builder
Trading Plan Builder
from datetime import datetime, timedelta
from dataclasses import dataclass, field
@dataclass
class DailyTradingPlan:
"""Structured daily trading plan with session context, risk budget, and rules."""
date: str
session_focus: str
watchlist: list[dict] = field(default_factory=list)
macro_context: str = ""
key_events: list[str] = field(default_factory=list)
bias: dict = field(default_factory=dict)
risk_budget: dict = field(default_factory=dict)
rules_today: list[str] = field(default_factory=list)
checklist: dict = field(default_factory=dict)
notes: str = ""
def to_markdown(self) -> str:
"""Export plan as a readable markdown string for journaling."""
event_lines = [f"- {e}" for e in self.key_events] or ["- None"]
budget_lines = [f"- **{k}:** {v}" for k, v in self.risk_budget.items()]
rule_lines = [f"- {r}" for r in self.rules_today]
watch_lines = [
f"- {w['pair']} | Bias: {w.get('bias','?')} | Priority: {w.get('priority','?')} | Confluence: {w.get('confluence_pct', 0)}%"
for w in self.watchlist
]
lines = (
[f"# Trading Plan \u2014 {self.date}",
f"**Session:** {self.session_focus.upper()}",
f"**Macro Context:** {self.macro_context or 'N/A'}",
"",
"## Key Events Today"]
+ event_lines
+ ["", "## Risk Budget"]
+ budget_lines
+ ["", "## Rules"]
+ rule_lines
+ ["", "## Watchlist"]
+ watch_lines
)
return "\n".join(lines)
class TradingPlanBuilder:
"""
Build, structure, and export daily trading plans with pre-market checklists,
watchlist generation, and end-of-day review templates.
Example
-------
>>> plan = TradingPlanBuilder.build_plan()
>>> print(plan.session_focus)
'london'
>>> md = plan.to_markdown()
"""
SESSION_HOURS = {
"tokyo": (0, 7),
"london": (7, 13),
"overlap": (13, 16),
"new_york": (13, 22),
"off": (22, 24),
}
@staticmethod
def current_session(hour: Optional[int] = None) -> str:
"""Return the current or given-hour trading session name."""
h = hour if hour is not None else datetime.utcnow().hour
if 0 <= h < 7: return "tokyo"
if 7 <= h < 13: return "london"
if 13 <= h < 16: return "overlap"
if 16 <= h < 22: return "new_york"
return "off"
@staticmethod
def pre_market_checklist() -> dict:
"""Return a structured pre-market checklist with estimated times."""
return {
"1_macro_scan": {
"task": "Check DXY, VIX, yield curves, equity futures",
"tools": ["macro-economic-dashboard"],
"time": "5 min",
"why": "Sets the risk-on/risk-off context for the day",
},
"2_news_check": {
"task": "Review upcoming high-impact events and overnight news",
"tools": ["market-news-impact", "risk-calendar-trade-filter"],
"time": "5 min",
"why": "Avoid entering before news that can spike against position",
},
"3_htf_analysis": {
"task": "D1 and H4 analysis on watchlist pairs — set daily directional bias",
"tools": ["mt5-chart-browser", "mtf-confluence-scorer"],
"time": "10 min",
"why": "Higher timeframe bias defines where the order flow is likely going",
},
"4_key_levels": {
"task": "Mark S/R, order blocks, FVGs, and liquidity pools on H1 charts",
"tools": ["trendline-sr-vision", "liquidity-order-flow-mapper"],
"time": "10 min",
"why": "These are the decision points where institutional orders cluster",
},
"5_risk_budget": {
"task": "Set max daily loss, max trades, and check portfolio heat",
"tools": ["risk-and-portfolio"],
"time": "2 min",
"why": "Pre-defined loss limits prevent emotional overtrading",
},
"6_session_check": {
"task": "Verify current session quality — is today suitable for trading?",
"tools": ["session-profiler", "risk-calendar-trade-filter"],
"time": "2 min",
"why": "Thin liquidity sessions (e.g., bank holidays) produce false signals",
},
"7_tilt_check": {
"task": "Run tilt_detector() on last 10 trades — check emotional state",
"tools": ["TradePsychologyCoach.tilt_detector"],
"time": "1 min",
"why": "Emotional residue from prior losses causes the worst trading decisions",
},
"_total_time": "~35 minutes",
}
@staticmethod
def generate_watchlist(
pairs: list[str],
confluence_scores: dict,
max_pairs: int = 6,
) -> list[dict]:
"""
Rank pairs by confluence score and return a focused watchlist.
Parameters
----------
pairs : all candidate pairs
confluence_scores : {pair: {"overall_score": float, "direction": str, "confluence_pct": float}}
max_pairs : maximum number of pairs to include
Returns
-------
List of dicts sorted by confluence_pct descending, with priority grades.
"""
watchlist: list[dict] = []
for pair in pairs:
score = confluence_scores.get(pair, {})
conf = float(score.get("confluence_pct", 0))
watchlist.append({
"pair": pair,
"mtf_score": score.get("overall_score", 0),
"bias": score.get("direction", "NEUTRAL"),
"confluence_pct": round(conf, 1),
"priority": "A+" if conf >= 85 else "A" if conf >= 75 else "B" if conf >= 50 else "C",
"tradeable": conf >= 50,
})
return sorted(watchlist, key=lambda w: w["confluence_pct"], reverse=True)[:max_pairs]
@staticmethod
def end_of_day_review() -> dict:
"""Return a structured end-of-day review template."""
return {
"1_log_trades": "Journal all trades: setup type, entry/exit reasoning, R-multiple achieved",
"2_plan_adherence": "Did you follow the plan exactly? Rate 1–10. Identify deviations.",
"3_what_worked": "List setups that worked and why (be specific about the edge)",
"4_what_failed": "List setups that failed and the true reason (execution vs setup vs bad luck)",
"5_lessons": "One concrete lesson to apply in tomorrow's session",
"6_emotional_state": "Rate emotional discipline 1–10. Flag any tilt episodes.",
"7_plan_adjustments": "Any rule changes for tomorrow? Document and date them.",
"8_equity_review": "Update equity curve. Compare to plan expectations.",
}
@staticmethod
def build_plan(
now: Optional[datetime] = None,
max_risk_pct: float = 4.0,
max_trades: int = 3,
risk_per_trade_pct: float = 1.5,
extra_rules: Optional[list[str]] = None,
) -> DailyTradingPlan:
"""
Build a complete daily trading plan for the current (or given) time.
Parameters
----------
now : UTC datetime (defaults to now)
max_risk_pct : maximum total risk allowed today (% of account)
max_trades : maximum number of trades
risk_per_trade_pct : risk per individual trade (% of account)
extra_rules : additional trader-specific rules to include
Returns
-------
DailyTradingPlan dataclass with all fields populated.
"""
now = now or datetime.utcnow()
session = TradingPlanBuilder.current_session(now.hour)
base_rules = [
"Follow the plan. No improvised trades.",
"Only A and B grade setups — wait for confluence.",
"No revenge trades after a loss. Take a break.",
f"Stop after {max_trades} trades (win or lose) — protect the day.",
"No trading 30 minutes before or after high-impact news.",
"If daily loss exceeds 50% of risk budget — stop for the day."]
if extra_rules:
base_rules.extend(extra_rules)
return DailyTradingPlan(
date = now.strftime("%Y-%m-%d"),
session_focus = session,
risk_budget = {
"max_risk_today_pct": f"{max_risk_pct}%",
"max_trades": max_trades,
"risk_per_trade_pct": f"{risk_per_trade_pct}%",
"max_loss_stop_pct": f"{max_risk_pct / 2:.1f}%",
},
rules_today = base_rules,
checklist = TradingPlanBuilder.pre_market_checklist(),
macro_context = f"Built at {now.strftime('%H:%M')} UTC — {session.upper()} session",
)