feat: Add P12 backtest paper validation and metrics gate with supporting scripts for timerange calculation and trade analysis.

pull/12760/head
vijay sharma 3 months ago
parent a0019cabec
commit 97007a1e82

@ -26,7 +26,8 @@ ALL_GATES=(
"p09_options_strategy_accept"
"p09x_universe_scanner_accept"
"p10_execution_surface"
"p11_risk_guardrails"
"p11_risk_guardrails",
"p12_backtest_paper_validation_and_metrics"
)
TARGET=${1:-""}

@ -0,0 +1,79 @@
#!/bin/bash
# P12 Backtest Paper Validation and Metrics
set -euo pipefail
# Identify run context
source scripts/gates/common.sh "p12"
export BREEZE_MOCK=1
export RISK_FORCE_SIGNAL=1
unset BREEZE_API_KEY BREEZE_API_SECRET BREEZE_SESSION_TOKEN
CFG="user_data/generated/config_p09x_v1.json"
if [ ! -f "$CFG" ]; then
echo "ERROR: Config missing: $CFG"
finish_gate 1
fi
PAIR="RELIANCE-20260226-2800-CE/INR"
TF="5m"
DATADIR="user_data/data/icicibreeze"
echo "Step 1: Download Data (7 days)"
freqtrade download-data -c "$CFG" --userdir user_data --timeframes "$TF" --days 7 || finish_gate $?
echo "Step 2: Compute Timerange"
# Use underlying cash pair for timerange computation as it's most reliable
TIMERANGE=$(bash scripts/p12_timerange.sh "RELIANCE/INR" "$TF" "$DATADIR")
echo "Timerange from data: $TIMERANGE"
if [[ ! "$TIMERANGE" =~ ^[0-9]{8}-[0-9]{8}$ ]]; then
echo "ERROR: Invalid timerange format: $TIMERANGE"
finish_gate 1
fi
echo "Step 3: Run Backtesting"
LOG_FILE="$ARTIFACT_DIR/backtest.log"
TRADES_FILE="$ARTIFACT_DIR/backtest_trades.json"
# Create a backtest-specific config with loose ROI to ensure trades close
BT_CFG="$ARTIFACT_DIR/config_bt.json"
jq '.minimal_roi = {"0": 0.0001} | .stoploss = -0.99' "$CFG" > "$BT_CFG"
freqtrade backtesting -c "$BT_CFG" \
--userdir user_data \
-s IndiaOptionsAutoStrategy \
--pairs "$PAIR" \
--timeframe "$TF" \
--timerange "$TIMERANGE" \
--export trades \
--export-filename "$TRADES_FILE" > "$LOG_FILE" 2>&1 || finish_gate $?
echo "Step 4: Generate Metrics"
METRICS_FILE="$ARTIFACT_DIR/metrics_summary.json"
python3 scripts/p12_metrics_from_trades.py \
--trades "$TRADES_FILE" \
--out "$METRICS_FILE" \
--pair "$PAIR" \
--tf "$TF" \
--timerange "$TIMERANGE" || finish_gate $?
echo "Step 5: Final Assertions"
if [ ! -f "$TRADES_FILE" ]; then
echo "ERROR: Missing trades file: $TRADES_FILE"
finish_gate 1
fi
if [ ! -f "$METRICS_FILE" ]; then
echo "ERROR: Missing metrics file: $METRICS_FILE"
finish_gate 1
fi
if grep -i "Traceback" "$LOG_FILE"; then
echo "ERROR: Traceback found in backtest logs"
tail -n 20 "$LOG_FILE"
finish_gate 1
fi
echo "[OK] P12 Backtest Paper Validation and Metrics passed"
finish_gate 0

@ -0,0 +1,50 @@
import json
import argparse
import sys
from pathlib import Path
def generate_metrics(trades_file: str, out_file: str, pair: str, timeframe: str, timerange: str):
try:
if not Path(trades_file).exists():
print(f"ERROR: Trades file not found: {trades_file}", file=sys.stderr)
sys.exit(1)
with open(trades_file, "r") as f:
trades = json.load(f)
trades_count = len(trades)
total_profit = sum(t.get("profit_abs", 0) for t in trades)
metrics = {
"pair": pair,
"timeframe": timeframe,
"timerange": timerange,
"trades_count": trades_count,
"total_profit_abs": total_profit,
}
with open(out_file, "w") as f:
json.dump(metrics, f, indent=4)
print(f"Metrics written to {out_file}")
except Exception as e:
print(f"ERROR: Failed to generate metrics: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Extract metrics from backtest trades.")
parser.add_argument("--trades", required=True, help="Path to backtest-trades.json")
parser.add_argument("--out", required=True, help="Path to output metrics_summary.json")
parser.add_argument("--pair", required=True, help="Pair")
parser.add_argument("--tf", required=True, help="Timeframe")
parser.add_argument("--timerange", required=True, help="Timerange used")
args = parser.parse_args()
generate_metrics(args.trades, args.out, args.pair, args.tf, args.timerange)
if __name__ == "__main__":
main()

@ -0,0 +1,11 @@
#!/bin/bash
# scripts/p12_timerange.sh
# Multi-purpose timerange helper for P12
set -euo pipefail
PAIR=${1:-"RELIANCE/INR"}
TF=${2:-"5m"}
DATADIR=${3:-"user_data/data/icicibreeze"}
# Call the python helper
python3 scripts/p12_timerange_from_data.py --pair "$PAIR" --tf "$TF" --datadir "$DATADIR"

@ -0,0 +1,67 @@
import argparse
import logging
import sys
from pathlib import Path
from datetime import timezone
# Add current directory to path to allow importing from workspace freqtrade
import os
sys.path.append(os.getcwd())
# Avoid rapidjson dependency if possible by mocking it before other imports if they use it
try:
import rapidjson
except ImportError:
import json
sys.modules["rapidjson"] = json
from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger("p12_timerange_from_data")
def get_timerange(pair: str, timeframe: str, datadir: str, data_format: str):
try:
data = load_pair_history(
datadir=Path(datadir),
timeframe=timeframe,
pair=pair,
data_format=data_format,
candle_type=CandleType.SPOT,
)
if data.empty:
print(
f"ERROR: No data found for {pair} {timeframe} in {datadir} (format: {data_format})",
file=sys.stderr,
)
sys.exit(1)
start_date = data.iloc[0]["date"].replace(tzinfo=timezone.utc)
end_date = data.iloc[-1]["date"].replace(tzinfo=timezone.utc)
# Format: YYYYMMDD-YYYYMMDD
print(f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}")
except Exception as e:
print(f"ERROR: Failed to load data: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Compute timerange from stored OHLCV data.")
parser.add_argument("--pair", required=True, help="Pair (e.g., RELIANCE/INR)")
parser.add_argument("--tf", required=True, help="Timeframe (e.g., 5m)")
parser.add_argument("--datadir", required=True, help="Data directory")
parser.add_argument("--format", default="feather", help="Data format (json or feather)")
args = parser.parse_args()
get_timerange(args.pair, args.tf, args.datadir, args.format)
if __name__ == "__main__":
main()

@ -23,7 +23,12 @@ class IndiaOptionsAutoStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "5m"
startup_candle_count = 50
@property
def startup_candle_count(self) -> int:
if os.environ.get("RISK_FORCE_SIGNAL"):
return 0
return 50
minimal_roi = {"0": 0.12}
stoploss = -0.15
@ -124,6 +129,11 @@ class IndiaOptionsAutoStrategy(IStrategy):
dataframe.loc[within_window & bear, "enter_long"] = 1
if os.environ.get("RISK_FORCE_SIGNAL"):
logger.error(
"DEBUG: RISK_FORCE_SIGNAL active for %s. Rows: %d",
metadata.get("pair"),
len(dataframe),
)
dataframe.loc[:, "enter_long"] = 1
return dataframe

Loading…
Cancel
Save