71b6343e92
Refresh the benchmark and performance surface across the repo. This updates the benchmark wrappers and helper scripts, regenerates the checked-in benchmark and perf-contract artifacts, and folds in the related roadmap, compatibility, and example notebook changes that belong with this performance-focused pass. Harden the Python CI and local pre-push flow so the same checks pass reliably in both places. The workflow and pre-push script now use module-safe uv typecheck invocations, the Python test environment installs the optional MCP dependency needed by the MCP server tests, and one-off root benchmark outputs are ignored to keep the repo clean. Align local tooling with the current project configuration by updating the Ruff pre-commit hook, tightening the API typing and MCP server helpers, and refreshing the lockfile to pick up the audited PyJWT fix while preserving the rest of the staged source changes.
201 lines
5.3 KiB
Plaintext
201 lines
5.3 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Backtesting with ferro-ta\n",
|
|
"\n",
|
|
"This notebook demonstrates the minimal backtesting harness and the\n",
|
|
"indicator pipeline, together with the configuration defaults API.\n",
|
|
"\n",
|
|
"Install:\n",
|
|
"```bash\n",
|
|
"pip install ferro-ta\n",
|
|
"```"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import ferro_ta.config as config\n",
|
|
"import numpy as np\n",
|
|
"from ferro_ta.backtest import backtest\n",
|
|
"from ferro_ta.pipeline import Pipeline\n",
|
|
"\n",
|
|
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
|
"\n",
|
|
"# Synthetic data\n",
|
|
"np.random.seed(42)\n",
|
|
"n = 300\n",
|
|
"close = np.cumprod(1 + np.random.randn(n) * 0.01) * 100\n",
|
|
"volume = np.random.randint(1000, 10000, n).astype(float)\n",
|
|
"print(f\"Generated {n} bars, final price: {close[-1]:.2f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## RSI 30/70 Strategy"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"result = backtest(close, strategy=\"rsi_30_70\", timeperiod=14)\n",
|
|
"print(\"Strategy: RSI 30/70\")\n",
|
|
"print(f\"Final equity: {result.final_equity:.4f}\")\n",
|
|
"print(f\"Number of trades: {result.n_trades}\")\n",
|
|
"print(f\"Return: {(result.final_equity - 1.0) * 100:.2f}%\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## SMA Crossover Strategy"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"result2 = backtest(close, strategy=\"sma_crossover\", fast=10, slow=30)\n",
|
|
"print(\"Strategy: SMA Crossover (10/30)\")\n",
|
|
"print(f\"Final equity: {result2.final_equity:.4f}\")\n",
|
|
"print(f\"Number of trades: {result2.n_trades}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Configuration Defaults\n",
|
|
"\n",
|
|
"Set global defaults for indicator parameters to avoid repeating them."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Set global defaults\n",
|
|
"config.set_default(\"timeperiod\", 20) # global default for all indicators\n",
|
|
"config.set_default(\"RSI.timeperiod\", 14) # RSI-specific override\n",
|
|
"\n",
|
|
"print(\"Current defaults:\", config.list_defaults())\n",
|
|
"print(\"RSI defaults:\", config.get_defaults_for(\"RSI\"))\n",
|
|
"print(\"SMA defaults:\", config.get_defaults_for(\"SMA\"))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Context manager for temporary overrides\n",
|
|
"with config.Config(timeperiod=5):\n",
|
|
" temp_default = config.get_default(\"timeperiod\")\n",
|
|
" print(f\"Inside context: timeperiod={temp_default}\")\n",
|
|
"\n",
|
|
"print(f\"After context: timeperiod={config.get_default('timeperiod')}\") # back to 20\n",
|
|
"\n",
|
|
"# Clean up\n",
|
|
"config.reset()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Multi-indicator Pipeline for Feature Engineering"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"pipe = (\n",
|
|
" Pipeline()\n",
|
|
" .add(\"sma_10\", SMA, timeperiod=10)\n",
|
|
" .add(\"sma_30\", SMA, timeperiod=30)\n",
|
|
" .add(\"ema_10\", EMA, timeperiod=10)\n",
|
|
" .add(\"rsi_14\", RSI, timeperiod=14)\n",
|
|
" .add(\n",
|
|
" \"bb\",\n",
|
|
" BBANDS,\n",
|
|
" output_keys=[\"bb_upper\", \"bb_mid\", \"bb_lower\"],\n",
|
|
" timeperiod=20,\n",
|
|
" nbdevup=2.0,\n",
|
|
" nbdevdn=2.0,\n",
|
|
" )\n",
|
|
")\n",
|
|
"\n",
|
|
"features = pipe.run(close)\n",
|
|
"print(\"Feature columns:\", list(features.keys()))\n",
|
|
"\n",
|
|
"# Build a simple feature matrix (last 5 complete rows)\n",
|
|
"valid_start = 30 # warmup\n",
|
|
"feature_matrix = np.column_stack([v[valid_start:] for v in features.values()])\n",
|
|
"print(f\"Feature matrix shape: {feature_matrix.shape}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Simple Manual Backtest Using the Pipeline"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Signal: long when RSI < 40 AND close > SMA_30; flat otherwise\n",
|
|
"rsi_vals = features[\"rsi_14\"]\n",
|
|
"sma30_vals = features[\"sma_30\"]\n",
|
|
"\n",
|
|
"signal = np.where((rsi_vals < 40) & (close > sma30_vals), 1.0, 0.0)\n",
|
|
"position = np.roll(signal, 1) # trade on next bar open\n",
|
|
"position[0] = 0.0\n",
|
|
"\n",
|
|
"returns = np.diff(close) / close[:-1]\n",
|
|
"strategy_returns = returns * position[1:]\n",
|
|
"\n",
|
|
"equity = np.cumprod(1 + strategy_returns)\n",
|
|
"print(f\"Final equity: {equity[-1]:.4f}\")\n",
|
|
"print(f\"Number of signal bars: {int(signal.sum())}\")"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python",
|
|
"version": "3.11.0"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|