feat: Add P12 backtest paper validation and metrics gate with supporting scripts for timerange calculation and trade analysis.
parent
a0019cabec
commit
97007a1e82
@ -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()
|
||||
Loading…
Reference in new issue