diff --git a/scripts/accept_all.sh b/scripts/accept_all.sh index 101f45b5e..44b4c541d 100755 --- a/scripts/accept_all.sh +++ b/scripts/accept_all.sh @@ -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:-""} diff --git a/scripts/gates/p12_backtest_paper_validation_and_metrics.sh b/scripts/gates/p12_backtest_paper_validation_and_metrics.sh new file mode 100644 index 000000000..ab623cf8f --- /dev/null +++ b/scripts/gates/p12_backtest_paper_validation_and_metrics.sh @@ -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 diff --git a/scripts/p12_metrics_from_trades.py b/scripts/p12_metrics_from_trades.py new file mode 100644 index 000000000..427fed43f --- /dev/null +++ b/scripts/p12_metrics_from_trades.py @@ -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() diff --git a/scripts/p12_timerange.sh b/scripts/p12_timerange.sh new file mode 100644 index 000000000..e0d78c4ff --- /dev/null +++ b/scripts/p12_timerange.sh @@ -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" diff --git a/scripts/p12_timerange_from_data.py b/scripts/p12_timerange_from_data.py new file mode 100644 index 000000000..fafd278fb --- /dev/null +++ b/scripts/p12_timerange_from_data.py @@ -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() diff --git a/user_data/strategies/IndiaOptionsAutoStrategy.py b/user_data/strategies/IndiaOptionsAutoStrategy.py index b9196e945..1a053aa18 100644 --- a/user_data/strategies/IndiaOptionsAutoStrategy.py +++ b/user_data/strategies/IndiaOptionsAutoStrategy.py @@ -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