Overall Statistics
Total Orders
2343
Average Win
0.38%
Average Loss
-0.30%
Compounding Annual Return
10.225%
Drawdown
9.300%
Expectancy
0.175
Start Equity
100000.00
End Equity
162865.86
Net Profit
62.866%
Sharpe Ratio
0.49
Sortino Ratio
0.583
Probabilistic Sharpe Ratio
23.409%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.29
Alpha
0.051
Beta
0.093
Annual Standard Deviation
0.097
Annual Variance
0.009
Information Ratio
0.74
Tracking Error
0.112
Treynor Ratio
0.513
Total Fees
$0.00
Estimated Strategy Capacity
$950000.00
Lowest Capacity Asset
AUDUSD 8G
Portfolio Turnover
72.17%
Drawdown Recovery
662
#region === Imports ===
from AlgorithmImports import *
import pywt
import numpy as np
from sklearn.svm import SVR
from sklearn.model_selection import GridSearchCV
#endregion


class FX_Daily_Wavelet_SVR_DePrado(QCAlgorithm):
    """
    DAILY FX — Lopez de Prado style:
    - Daily bars only
    - Wavelet denoise + SVR next-day forecast
    - Meta-label: skip on high daily vol
    - Walk-forward SVR re-training
    - Simple daily signal: trade daily open
    - Risk: fixed drawdown + trailing TP
    """

    def initialize(self):
        self.set_start_date(2020, 1, 1)
        self.set_end_date(2025, 1, 1)
        self.set_cash(100000)

        self._period = 50  # daily window, ~6 months of daily bars
        self._leverage = 100

        self.set_brokerage_model(BrokerageName.OANDA_BROKERAGE, AccountType.MARGIN)
        self.set_benchmark(SecurityType.FOREX, "EURUSD") 
        self.universe_settings.leverage = self._leverage
        self.universe_settings.resolution = Resolution.DAILY

        pairs = ["AUDUSD", "EURAUD", "EURCAD", "EURJPY", "GBPNZD", "AUDCAD", "NZDCHF", "AUDCHF", "CHFJPY", "AUDJPY", "NZDJPY"] 
        symbols = [Symbol.create(pair, SecurityType.FOREX, Market.OANDA) for pair in pairs]
        self.set_universe_selection(ManualUniverseSelectionModel(symbols))

        self.set_alpha(DailyWaveletAlphaModelDePrado(self._period))
        self.set_portfolio_construction(
            LeveragedWeightingPortfolioConstructionModel(Resolution.DAILY, self._leverage)
        )

        self.add_risk_management(MaximumDrawdownPercentPerSecurity(0.02))  # ~2% daily max SL
        self.add_risk_management(TrailingStopRiskManagementModel(0.02))     # ~2% trailing TP


class DailyWaveletAlphaModelDePrado(AlphaModel):
    def __init__(self, period):
        self._period = period
        self._wavelet = SVMWavelet()
        self._symbol_data = {}
        self._last_day = -1
        self._retrain_freq = 5  # re-train every 5 days

    def update(self, algorithm, data):
        insights = []
        day = algorithm.time.day
        if self._last_day == day: return []
        self._last_day = day

        for symbol, sd in self._symbol_data.items():
            prices = sd.prices()
            if len(prices) < self._period: continue

            forecast = self._wavelet.forecast(prices)
            ret = (forecast / prices[-1]) - 1

            daily_vol = np.std(prices[-20:]) / prices[-1]
            if daily_vol < 0.002 or daily_vol > 0.01:  
                algorithm.debug(f"Meta-label: skip {symbol} — daily vol {daily_vol:.4f}")
                continue

            if sd.retrain_counter % self._retrain_freq == 0:
                sd.train_wavelet = True
            sd.retrain_counter += 1

            if ret > 0.005:
                insights.append(
                    Insight.price(symbol, timedelta(days=1), InsightDirection.UP, weight=min(abs(ret), 0.05))
                )
            elif ret < -0.005:
                insights.append(
                    Insight.price(symbol, timedelta(days=1), InsightDirection.DOWN, weight=min(abs(ret), 0.05))
                )

            algorithm.insights.cancel([symbol])

        return insights

    def on_securities_changed(self, algorithm, changes):
        for security in changes.added_securities:
            self._symbol_data[security.symbol] = SymbolDataDePrado(
                algorithm, security.symbol, self._period)
        for security in changes.removed_securities:
            data = self._symbol_data.pop(security.symbol, None)
            if data:
                data.dispose()


class SymbolDataDePrado:
    def __init__(self, algorithm, symbol, period):
        self._algorithm = algorithm
        self._symbol = symbol
        self._close = RollingWindow[float](period)
        self.retrain_counter = 0

        self._consolidator = QuoteBarConsolidator(timedelta(days=1))
        self._consolidator.data_consolidated += self._on_consolidated
        algorithm.subscription_manager.add_consolidator(symbol, self._consolidator)

        history = algorithm.history[QuoteBar](symbol, period, Resolution.DAILY)
        for bar in history:
            self._consolidator.update(bar)

    def _on_consolidated(self, _, bar):
        self._close.add(bar.close)

    def prices(self):
        return np.array(list(self._close))[::-1]

    def dispose(self):
        self._close.reset()
        self._algorithm.subscription_manager.remove_consolidator(self._symbol, self._consolidator)


class LeveragedWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
    def __init__(self, rebalance=Resolution.DAILY, leverage=100):
        super().__init__(rebalance)
        self.leverage = leverage

    def should_create_target_for_insight(self, insight):
        return insight.weight is not None

    def determine_target_percent(self, active_insights):
        result = {}
        for insight in active_insights:
            direction = insight.direction if self.respect_portfolio_bias(insight) else InsightDirection.FLAT
            result[insight] = direction * insight.weight * self.leverage
        return result


class SVMWavelet:
    def forecast(self, data):
        w = pywt.Wavelet('sym10')
        coeffs = pywt.wavedec(data, w)
        threshold = 0.75

        for i in range(len(coeffs)):
            if i > 0:
                coeffs[i] = pywt.threshold(coeffs[i], threshold * max(coeffs[i]))
            forecast = self._svm_forecast(coeffs[i])
            coeffs[i] = np.roll(coeffs[i], -1)
            coeffs[i][-1] = forecast

        return pywt.waverec(coeffs, w)[-1]

    def _svm_forecast(self, data, sample_size=20):
        X, y = self._partition_array(data, size=sample_size)
        grid = {'C': [.05, .1, .5, 1, 5, 10], 'epsilon': [0.001, 0.005, 0.01, 0.05, 0.1]}
        model = GridSearchCV(SVR(), grid, scoring='neg_mean_squared_error').fit(X, y).best_estimator_
        return model.predict(data[np.newaxis, -sample_size:])[0]

    def _partition_array(self, arr, size=None, splits=None):
        arrs, values = [], []
        if not (bool(size is None) ^ bool(splits is None)):
            raise ValueError('Size XOR Splits must be set')
        if size:
            arrs = [arr[i:i + size] for i in range(len(arr) - size)]
            values = [arr[i] for i in range(size, len(arr))]
        elif splits:
            size = len(arr) // splits
            arrs = [arr[i:i + size] for i in range(size - 1, len(arr) - 1, size)]
            values = [arr[i] for i in range(2 * size - 1, len(arr), size)]
        return np.array(arrs), np.array(values)