[REFERENCE QC Cloud] Ce notebook utilise QuantBook et necessite l’environnement QuantConnect Cloud. Pour executer : https://www.quantconnect.com/research

QuantConnect Logo

Initializing environment

// We need to load assemblies at the start in their own cell
#load "../Initialize.csx"
// QuantBook C# Research Environment
// For more information see https://www.quantconnect.com/docs/v2/our-platform/research/getting-started
#load "../QuantConnect.csx"

Loading historical BTC prices in daily resolution

// Import necessary namespaces
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Brokerages;
using System;
using System.Linq;

// Initialize QuantBook
var qb = new QuantConnect.Research.QuantBook();

// Set account currency and brokerage model
qb.SetAccountCurrency("USDT");
qb.SetBrokerageModel(BrokerageName.Binance, AccountType.Cash);

// Add a crypto subscription
var btcUsdt = qb.AddCrypto("BTCUSDT", Resolution.Daily, Market.Binance).Symbol;

// Set the date range for historical data
var startDate = new DateTime(2013, 04, 07);
var endDate = new DateTime(2024, 11, 1);

// Request historical data
var btcHistory = qb.History<TradeBar>(btcUsdt, startDate, endDate, Resolution.Daily).ToList();

// Verify data retrieval
Console.WriteLine($"Number of data points retrieved: {btcHistory.Count}");
foreach (var bar in btcHistory.Take(5))
{
    Console.WriteLine($"Date: {bar.EndTime}, Open: {bar.Open}, High: {bar.High}, Low: {bar.Low}, Close: {bar.Close}, Volume: {bar.Volume}");
}
foreach (var bar in btcHistory.Skip(btcHistory.Count-5))
{
    Console.WriteLine($"Date: {bar.EndTime}, Open: {bar.Open}, High: {bar.High}, Low: {bar.Low}, Close: {bar.Close}, Volume: {bar.Volume}");
}

Computing Benchmark return

We assume an initial investment of 5000$

// Calculer le rendement cumulatif du benchmark
decimal initialInvestment = 5000;
Console.WriteLine($"Initial Portfolio Value: 5000$: BTC Close {btcHistory.First().Close}$/BTC");
var cumulativeReturn = new List<(DateTime Time, decimal PortfolioValue)>();
decimal runningTotal = initialInvestment;

foreach (var bar in btcHistory)
{
    runningTotal = initialInvestment * (bar.Close / btcHistory.First().Close);
    cumulativeReturn.Add((bar.EndTime, runningTotal));
}

// Afficher quelques rendements
foreach (var point in cumulativeReturn.Take(5))
{
    Console.WriteLine($"Date: {point.Time}, Portfolio Value: {point.PortfolioValue:C}");
}

Console.WriteLine($"Final Portfolio Value: {cumulativeReturn.Last().Time}: {cumulativeReturn.Last().PortfolioValue:C} BTC Close {btcHistory.Last().Close}$/BTC");

Identifying peaks for various margins

We want to extract local extrema for various margins and select those that yield a target amount of peaks (around 50 peaks for a handful of target yearly trades)

// Generic method to compute peaks from any time series with customizable return fields
public List<TResult> GetPeaks<T, TResult>(
    List<T> data,
    decimal thresholdPercentage,
    Func<T, decimal> valueSelector,
    Func<T, DateTime> timeSelector,
    Func<DateTime, decimal, TResult> resultSelector)
{
    var peaks = new List<TResult>();
    if (data.Count == 0) return peaks;

    decimal threshold = thresholdPercentage / 100m;

    // Start by looking for a maximum (default behavior)
    bool lookingForMax = true;

    // Track the last known pivot
    int pivotIndex = 0;
    decimal pivotValue = valueSelector(data[0]);

    for (int i = 1; i < data.Count; i++)
    {
        var currentValue = valueSelector(data[i]);

        if (lookingForMax)
        {
            // Looking for a peak (local maximum)
            if (currentValue > pivotValue)
            {
                // Update pivot if we are still climbing
                pivotIndex = i;
                pivotValue = currentValue;
            }
            else
            {
                // Calculate the drop from the current pivot
                decimal drop = (pivotValue - currentValue) / pivotValue;
                if (drop > threshold)
                {
                    // Significant drop detected, add the peak
                    peaks.Add(resultSelector(timeSelector(data[pivotIndex]), pivotValue));
                    // Switch to looking for a trough
                    lookingForMax = false;
                    pivotIndex = i;
                    pivotValue = currentValue;
                }
            }
        }
        else
        {
            // Looking for a trough (local minimum)
            if (currentValue < pivotValue)
            {
                // Update pivot if we are still descending
                pivotIndex = i;
                pivotValue = currentValue;
            }
            else
            {
                // Calculate the rise from the current pivot
                decimal rise = (currentValue - pivotValue) / pivotValue;
                if (rise > threshold)
                {
                    // Significant rise detected, add the trough
                    peaks.Add(resultSelector(timeSelector(data[pivotIndex]), pivotValue));
                    // Switch to looking for a peak
                    lookingForMax = true;
                    pivotIndex = i;
                    pivotValue = currentValue;
                }
            }
        }
    }

    return peaks;
}

// Computing peak numbers for various target percentages
var peakPercentages = new List<decimal> { 5, 15, 25, 40 };

var peakNumbers = peakPercentages.ToDictionary(
    percentage => percentage,
    percentage => GetPeaks(
        btcHistory,
        percentage,
        bar => bar.Close, // Select the closing price
        bar => bar.EndTime, // Select the timestamp
        (time, value) => (Time: time, Price: value) // Return named tuple with "Price"
    )
);

// Displaying the peak numbers
foreach (var kvp in peakNumbers)
{
    Console.WriteLine($"Peaks at {kvp.Key}%: {kvp.Value.Count}");
    foreach (var peak in kvp.Value.Take(5))
    {
        Console.WriteLine($"Date: {peak.Time}, Price: {peak.Price:C}");
    }
}

Computing ideal strategy and max theoretical return

We now calculate the theoretical maximum return based on the selected peaks, assuming the ideal strategy of buying at local minima and selling at local maxima.

// Assuming threshold accounting for a target number of trades based on prior analysis
var selectedPeaks = peakNumbers[25];

// Recalculating theoretical maximum return based on the selected peaks, starting with an initial buy
decimal initialInvestment = 5000m;
decimal cash = initialInvestment; // Start with all cash
decimal btcHoldings = 0m;         // Initially no BTC


decimal firstPrice = btcHistory.First().Close; // First day's close price

// Simulate buying at the first day's price
btcHoldings = cash / firstPrice;
cash = 0; // All cash converted to BTC

bool buying = false; // Always start by buying

// Process the peaks
for (int i = 0; i < selectedPeaks.Count; i++)
{
    var peak = selectedPeaks[i];

    if (buying)
    {

        // Buy at the current price using all cash
        Console.WriteLine($"Buying at {peak.Price:C} on {peak.Time}");
        btcHoldings = cash / peak.Price;
        cash = 0; // All cash converted to BTC
    }
    else
    {
        // Sell all BTC at the current price
        Console.WriteLine($"Selling at {peak.Price:C} on {peak.Time}");
        cash = btcHoldings * peak.Price;
        btcHoldings = 0; // All BTC converted to cash
    }

    // Alternate between buying and selling
    buying = !buying;
}

// Final portfolio value is either in cash or converted BTC
decimal finalPortfolioValue = cash + (btcHoldings * btcHistory.Last().Close);
decimal theoreticalReturn = (finalPortfolioValue / initialInvestment) - 1;

// Display the results
Console.WriteLine($"Theoretical Max Return: {theoreticalReturn:P}");
Console.WriteLine($"Number of Trades: {selectedPeaks.Count + 1}"); // Include the initial trade
Console.WriteLine($"Final Portfolio Value: {finalPortfolioValue:C}");

Introducing a Peak class for convenience

// A generic class to represent a peak or trough in any time series
public class Peak
{
    public DateTime Time { get; set; }
    public decimal Value { get; set; }   // Generic field for the peak value (could be Price, ADX, etc.)
    public bool IsMax { get; set; }      // True if it's a local maximum, false if it's a local minimum

    // Dynamic windows based on a percentage of the peak's value
    public DateTime StartWindow { get; set; }
    public DateTime EndWindow { get; set; }

    // Add additional fields if needed
}

// Suppose 'selectedPeaks' is currently a List<(DateTime Time, decimal Value)>.
// We create a new list of Peak objects.
// Logic: We assume an alternation max/min/max/min ...
// For a more robust approach, you'd compare the previous and next values to determine if it's a max or a min.

var peaks = new List<Peak>();
bool isMax = true; 
foreach (var p in selectedPeaks)
{
    peaks.Add(new Peak
    {
        Time = p.Time,
        Value = p.Price,  // Or p.Value, depending on your tuple naming
        IsMax = isMax
    });
    // Toggle between max and min
    isMax = !isMax;
}

// At this stage, each Peak has Time, Value, and IsMax defined.
// StartWindow and EndWindow will be assigned later for dynamic analysis windows.

// Verification: count how many max vs. min
int maxCount = peaks.Count(p => p.IsMax);
int minCount = peaks.Count(p => !p.IsMax);

Console.WriteLine($"Max count: {maxCount}, Min count: {minCount}");

// Print the first few peaks
foreach (var peak in peaks.Take(5))
{
    Console.WriteLine($"Time: {peak.Time}, Value: {peak.Value:C}, IsMax: {peak.IsMax}");
}

Defining dynamic windows around peaks

// Unified helper methods for dynamic window calculation.
// The goal is to return a valid (start, end) interval, ensuring start < end when possible.

(DateTime start, DateTime end) ComputeDynamicPeakWindow(
    Peak peak,
    decimal windowPercent,
    List<DateTime> orderedDates,
    Dictionary<DateTime, decimal> seriesByDate,
    DateTime startBoundary,
    DateTime endBoundary)
{
    decimal threshold = peak.IsMax
        ? peak.Value * (1 - windowPercent)
        : peak.Value * (1 + windowPercent);

    DateTime startWindow = FindCrossingInSegment(
        peak, threshold,
        orderedDates, seriesByDate,
        segmentStart: startBoundary,
        segmentEnd: peak.Time,
        searchingApproach: true);

    DateTime endWindow = FindCrossingInSegment(
        peak, threshold,
        orderedDates, seriesByDate,
        segmentStart: peak.Time,
        segmentEnd: endBoundary,
        searchingApproach: false);

    // Sécurisation et ajustements finaux
    if (startWindow > peak.Time) startWindow = peak.Time;
    if (endWindow < peak.Time)   endWindow = peak.Time;
    if (startWindow > endWindow) endWindow = startWindow;

    return (startWindow, endWindow);
}



DateTime FindCrossingInSegment(
    Peak peak,
    decimal threshold,
    List<DateTime> orderedDates,
    Dictionary<DateTime, decimal> seriesByDate,
    DateTime segmentStart,
    DateTime segmentEnd,
    bool searchingApproach)
{
    // On récupère l'index pour segmentStart et segmentEnd
    int startIndex = orderedDates.BinarySearch(segmentStart);
    if (startIndex < 0) startIndex = ~startIndex;

    int endIndex = orderedDates.BinarySearch(segmentEnd);
    if (endIndex < 0) endIndex = ~endIndex;

    if (startIndex > endIndex) return segmentStart; // Cas incohérent

    if (searchingApproach)
    {
        for (int i = startIndex; i <= endIndex && i < orderedDates.Count; i++)
        {
            var date = orderedDates[i];
            var seriesValue = seriesByDate[date];

            bool crossing = peak.IsMax
                ? seriesValue >= threshold
                : seriesValue <= threshold;

            if (crossing)
            {
                return date;
            }
        }
        return segmentEnd;
    }
    else
    {
        for (int i = endIndex; i >= startIndex; i--)
        {
            var date = orderedDates[i];
            var seriesValue = seriesByDate[date];

            bool crossing = peak.IsMax
                ? seriesValue < threshold
                : seriesValue > threshold;

            if (crossing)
            {
                return date;
            }
        }
        return segmentStart;
    }
}


// Function to compute median value from a list of doubles
double Median(List<double> values)
{
    if (values == null || values.Count == 0)
        throw new InvalidOperationException("Cannot compute median for an empty set.");

    var sortedValues = values.OrderBy(x => x).ToList();
    int count = sortedValues.Count;
    int mid = count / 2;

    if (count % 2 == 0)
    {
        return (sortedValues[mid - 1] + sortedValues[mid]) / 2.0;
    }
    else
    {
        return sortedValues[mid];
    }
}


void ComputeDynamicWindowsForPeaks(
    List<Peak> peaks,
    decimal windowPercent,
    TimeSpan minHalfWidth,
    TimeSpan maxHalfWidth,
    Dictionary<DateTime, decimal> seriesByDate,
    List<DateTime> orderedDates)
{
    var sortedPeaks = peaks.OrderBy(x => x.Time).ToList();

    for (int i = 0; i < sortedPeaks.Count; i++)
    {
        var currentPeak = sortedPeaks[i];

        // Calcul des frontières de la fenêtre (startBoundary, endBoundary)
        DateTime startBoundary = (i == 0)
            ? orderedDates.First()
            : new DateTime((sortedPeaks[i - 1].Time.Ticks + currentPeak.Time.Ticks) / 2);

        DateTime endBoundary = (i == sortedPeaks.Count - 1)
            ? orderedDates.Last()
            : new DateTime((currentPeak.Time.Ticks + sortedPeaks[i + 1].Time.Ticks) / 2);

        (DateTime s, DateTime e) = ComputeDynamicPeakWindow(
            currentPeak,
            windowPercent,
            orderedDates,
            seriesByDate,
            startBoundary,
            endBoundary
        );

        // Application des contraintes minHalfWidth / maxHalfWidth
        TimeSpan preWindow = currentPeak.Time - s;
        if (preWindow < minHalfWidth)
            s = currentPeak.Time - minHalfWidth;
        else if (preWindow > maxHalfWidth)
            s = currentPeak.Time - maxHalfWidth;

        if (s < orderedDates.First())
            s = orderedDates.First();

        TimeSpan postWindow = e - currentPeak.Time;
        if (postWindow < minHalfWidth)
            e = currentPeak.Time + minHalfWidth;
        else if (postWindow > maxHalfWidth)
            e = currentPeak.Time + maxHalfWidth;

        if (e > orderedDates.Last())
            e = orderedDates.Last();

        // Final assignment
        currentPeak.StartWindow = s;
        currentPeak.EndWindow   = e;
    }
}

Computing dynamic windows from new methods

// Global parameters
decimal windowPercent = 0.2m;
TimeSpan maxHalfWidth = TimeSpan.FromDays(30);
TimeSpan minHalfWidth = TimeSpan.FromDays(5);

var timeSeriesByDate = btcHistory.ToDictionary(x => x.EndTime, x => x.Close);
var orderedDates = btcHistory.Select(x => x.EndTime).OrderBy(d => d).ToList();

ComputeDynamicWindowsForPeaks(peaks, windowPercent, minHalfWidth, maxHalfWidth, timeSeriesByDate, orderedDates);

// Now you can display stats again
var preWindowSizes = peaks.Select(p => (p.Time - p.StartWindow).TotalDays).ToList();
var postWindowSizes = peaks.Select(p => (p.EndWindow - p.Time).TotalDays).ToList();

Console.WriteLine($"Pre-window Sizes - Min: {preWindowSizes.Min()} days, Max: {preWindowSizes.Max()} days, Mean: {preWindowSizes.Average():F2} days, Median: {Median(preWindowSizes):F2} days");
Console.WriteLine($"Post-window Sizes - Min: {postWindowSizes.Min()} days, Max: {postWindowSizes.Max()} days, Mean: {postWindowSizes.Average():F2} days, Median: {Median(postWindowSizes):F2} days");

Console.WriteLine("Price Peaks and Their Windows:");
// We sample 5 peaks at the beginning, 5 in the middle and 5 at the end
var samplePeaks = peaks.Take(5).Concat(peaks.Skip(peaks.Count / 2).Take(5)).Concat(peaks.Skip(peaks.Count - 5));
foreach (var peak in samplePeaks) // Display only the first 10 for brevity
{
    Console.WriteLine($"Peak at {peak.Time}, Price: {peak.Value:C}");
    Console.WriteLine($"  Pre-window: {peak.StartWindow} -> {peak.Time}");
    Console.WriteLine($"  Post-window: {peak.Time} -> {peak.EndWindow}");
}

Introducing Indicators

We start by introducing a range of ADX and MACD indicator with various parameters. Our simple starting strategy is defined by:

        var macdHistogram = _macd.Current.Value - _macd.Signal.Current.Value;

        // Détermination des signaux MACD
        var isMacdBullish = macdHistogram > 0;    // Signal haussier si l'histogramme est positif
        var isMacdBearish = macdHistogram < 0;    // Signal baissier si l'histogramme est négatif

        // Récupération de la valeur actuelle de l'ADX
        var adxValue = _adx.Current.Value;

        // Conditions d'entrée en position longue
        if (adxValue >= AdxHigh && isMacdBullish)
        {
            // Si le portefeuille n'est pas déjà investi
            if (!Portfolio.Invested)
            {
                // Investit 100% du capital disponible dans le symbole
                SetHoldings(_symbol, 1);
                // Debug($"Acheté {_symbol} au prix de {currentPrice}");
            }
        }
        // Conditions de sortie de position
        else if (adxValue < AdxLow && isMacdBearish)
        {
            // Si le portefeuille est investi
            if (Portfolio.Invested)
            {
                // Liquide la position sur le symbole
                Liquidate(_symbol);
                // Debug($"Vendu {_symbol} au prix de {currentPrice}");
            }
        }

Accordingly, we want to adjust adx parameters so that they are compatible with our peaks, the adjust MACD accordingly.

Generating ADX with Various Periods

We generate a range of ADX indicators with différent periods. For each indicator, we will observe how its value changes in the window around the detected peaks.

// Extend historical data range to ensure indicators have sufficient warm-up
var extendedStartDate = startDate.AddYears(-1); // Add one extra year for warm-up
var extendedBtcHistory = qb.History<TradeBar>(btcUsdt, extendedStartDate, endDate, Resolution.Daily).ToList();

// Define ADX periods to test
var adxPeriods = new List<int> { 5, 10, 15, 20, 25, 30, 35, 50, 75, 100 };

// Dictionary to store ADX values for each period
var adxIndicators = adxPeriods.ToDictionary(
    period => period,
    period =>
    {
        var adx = qb.ADX(btcUsdt, period, Resolution.Daily);
        var adxValues = new List<(DateTime Time, decimal Value)>();

        foreach (var bar in extendedBtcHistory)
        {
            adx.Update(bar);
            if (adx.IsReady)
            {
                adxValues.Add((bar.EndTime, adx.Current.Value));
            }
        }

        return adxValues;
    }
);

// Compute statistics for ADX periods
Console.WriteLine("ADX Statistics for Different Periods:");
foreach (var period in adxPeriods)
{
    var values = adxIndicators[period].Select(x => x.Value).ToList();
    var min = values.Min();
    var max = values.Max();
    var mean = values.Average();
    var median = values.OrderBy(x => x).ElementAt(values.Count / 2);
    var count = values.Count;

    Console.WriteLine($"ADX Period: {period}");
    Console.WriteLine($"  Count: {count}");
    Console.WriteLine($"  Min: {min:F2}");
    Console.WriteLine($"  Max: {max:F2}");
    Console.WriteLine($"  Mean: {mean:F2}");
    Console.WriteLine($"  Median: {median:F2}");
    Console.WriteLine("---");
}

Computing number of Peaks for the ADX periods

// Define threshold percentages for peaks
var adxThresholds = new List<decimal> { 10, 20, 30, 40, 50, 60 };

// Here we'll store the ADX peaks (with windows) for each period/threshold
var adxPeaksByPeriodThreshold = new Dictionary<int, Dictionary<decimal, List<Peak>>>();

Console.WriteLine("ADX Peak Counts and Window Statistics by Period and Threshold:");

foreach (var period in adxPeriods)
{
    Console.WriteLine($"ADX Period: {period}");
    adxPeaksByPeriodThreshold[period] = new Dictionary<decimal, List<Peak>>();

    var adxValues = adxIndicators[period];
    // Build a dictionary <DateTime, decimal> for the dynamic window function
    var adxByDate = adxValues.ToDictionary(x => x.Time, x => x.Value);

    foreach (var threshold in adxThresholds)
    {
        // Apply the peak detection method to ADX values
        var adxPeaks = GetPeaks(
            data: adxValues,
            thresholdPercentage: threshold,
            valueSelector: x => x.Value,
            timeSelector: x => x.Time,
            // Return a Peak object directly instead of a tuple
            resultSelector: (time, value) => new Peak
            {
                Time = time,
                Value = value,
                IsMax = true // For ADX, we typically interpret peaks as maxima
            }
        );

        // Now we compute windows for each ADX peak using our generic method
        decimal windowPercent = 0.2m;
        TimeSpan minHalfWidth = TimeSpan.FromDays(5);
        TimeSpan maxHalfWidth = TimeSpan.FromDays(30);

        // Sort the ADX peaks in ascending time order
        adxPeaks = adxPeaks.OrderBy(p => p.Time).ToList();

        // We need an orderedDates list consistent with adxByDate
        var orderedAdxDates = adxValues.Select(x => x.Time).OrderBy(d => d).ToList();

        // Compute dynamic windows for ADX peaks
        ComputeDynamicWindowsForPeaks(
            peaks: adxPeaks,
            windowPercent: windowPercent,
            minHalfWidth: minHalfWidth,
            maxHalfWidth: maxHalfWidth,
            seriesByDate: adxByDate,
            orderedDates: orderedAdxDates
        );

        // Store the list of ADX peaks (with dynamic windows) in a dictionary for later use
        adxPeaksByPeriodThreshold[period][threshold] = adxPeaks;

        // Collect window statistics for analysis
        var preWindowSizes = adxPeaks.Select(p => (p.Time - p.StartWindow).TotalDays).ToList();
        var postWindowSizes = adxPeaks.Select(p => (p.EndWindow - p.Time).TotalDays).ToList();

        // Display the statistics
        Console.WriteLine($"  Threshold {threshold}%: {adxPeaks.Count} peaks");
        Console.WriteLine($"    Pre-window Sizes - Min: {preWindowSizes.Min()} days, Max: {preWindowSizes.Max()} days, Mean: {preWindowSizes.Average():F2} days, Median: {Median(preWindowSizes):F2} days");
        Console.WriteLine($"    Post-window Sizes - Min: {postWindowSizes.Min()} days, Max: {postWindowSizes.Max()} days, Mean: {postWindowSizes.Average():F2} days, Median: {Median(postWindowSizes):F2} days");
    }
    Console.WriteLine("---");
}

Correlating ADX peaks with Price peak windows

int OverlappingWindows(List<Peak> pricePeaks, List<Peak> adxPeaks)
{
    int overlapCount = 0;
    foreach (var pricePeak in pricePeaks)
    {
        foreach (var adxPeak in adxPeaks)
        {
            // Condition de recouvrement
            bool isOverlapping = 
                pricePeak.StartWindow < adxPeak.EndWindow &&
                adxPeak.StartWindow < pricePeak.EndWindow;
            if (isOverlapping)
            {
                overlapCount++;
                break; // On compte ce pricePeak comme corrélé à un pic ADX
            }
        }
    }
    return overlapCount;
}


foreach (var period in adxPeriods)
{
    foreach (var threshold in adxThresholds)
    {
        var currentAdxPeaks = adxPeaksByPeriodThreshold[period][threshold];
        int overlapCount = OverlappingWindows(peaks, currentAdxPeaks);
        Console.WriteLine($"Period: {period}, Threshold: {threshold} => Overlap with Price Peaks: {overlapCount}");
    }
}

Generating MACD for various sets of parameters

using QuantConnect.Indicators;
// Define MACD parameter combinations
var macdCombinations = new List<(int Fast, int Slow, int Signal)>
{
    (8, 21, 7), (8, 26, 9), (8, 30, 12),
    (12, 21, 7), (12, 26, 9), (12, 30, 12),
    (15, 21, 7), (15, 26, 9), (15, 30, 12)
};

// Dictionary to store MACD values for each combination
var macdValuesByCombination = new Dictionary<(int Fast, int Slow, int Signal), List<(DateTime Time, decimal Macd, decimal Signal, decimal Histogram)>>();

Console.WriteLine("MACD Computation for Parameter Combinations:");

foreach (var (fast, slow, signal) in macdCombinations)
{
    // Initialize MACD indicator with specified parameters
    var macd = qb.MACD(btcUsdt, fast, slow, signal, MovingAverageType.Exponential, Resolution.Daily);
    var macdValues = new List<(DateTime Time, decimal Macd, decimal Signal, decimal Histogram)>();

    // Calculate MACD values for the extended historical data
    foreach (var bar in extendedBtcHistory)
    {
        macd.Update(bar.EndTime, bar.Close);
        if (macd.IsReady)
        {
            macdValues.Add((
                bar.EndTime,
                macd.Current.Value,        // MACD line
                macd.Signal.Current.Value, // Signal line
                macd.Current.Value - macd.Signal.Current.Value // Histogram
            ));
        }
    }

    macdValuesByCombination[(fast, slow, signal)] = macdValues;

    // Output summary
    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal} => {macdValues.Count} data points");
}

Computing bullish and bearish periods for the MACD indicators

// Dictionary to store bullish and bearish periods for each combination
var macdPeriodsByCombination = new Dictionary<(int Fast, int Slow, int Signal), (List<(DateTime Start, DateTime End)> Bullish, List<(DateTime Start, DateTime End)> Bearish)>();

Console.WriteLine("MACD Bullish and Bearish Periods:");

foreach (var (fast, slow, signal) in macdCombinations)
{
    var macdValues = macdValuesByCombination[(fast, slow, signal)];
    var bullishPeriods = new List<(DateTime Start, DateTime End)>();
    var bearishPeriods = new List<(DateTime Start, DateTime End)>();

    DateTime? currentStart = null;
    bool? currentTrend = null; // True for bullish, False for bearish

    foreach (var (time, macd, signalLine, histogram) in macdValues)
    {
        var isBullish = histogram > 0;

        if (currentTrend == null)
        {
            // Initialize the trend
            currentTrend = isBullish;
            currentStart = time;
        }
        else if (isBullish != currentTrend)
        {
            // End the current period
            if (currentTrend == true)
                bullishPeriods.Add((currentStart.Value, time));
            else
                bearishPeriods.Add((currentStart.Value, time));

            // Start a new period
            currentTrend = isBullish;
            currentStart = time;
        }
    }

    // Handle the last period
    if (currentStart != null)
    {
        if (currentTrend == true)
            bullishPeriods.Add((currentStart.Value, macdValues.Last().Time));
        else
            bearishPeriods.Add((currentStart.Value, macdValues.Last().Time));
    }

    macdPeriodsByCombination[(fast, slow, signal)] = (bullishPeriods, bearishPeriods);

    // Output summary
    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"    Bullish periods: {bullishPeriods.Count}, Total duration: {bullishPeriods.Sum(p => (p.End - p.Start).TotalDays):F1} days");
    Console.WriteLine($"    Bearish periods: {bearishPeriods.Count}, Total duration: {bearishPeriods.Sum(p => (p.End - p.Start).TotalDays):F1} days");
}

Correlating MACD periods and peak periods

// Helper method to calculate overlap between two periods
double CalculateOverlap(DateTime start1, DateTime end1, DateTime start2, DateTime end2)
{
    var overlapStart = start1 > start2 ? start1 : start2;
    var overlapEnd = end1 < end2 ? end1 : end2;
    return (overlapEnd > overlapStart) ? (overlapEnd - overlapStart).TotalDays : 0.0;
}

// Define long-term price trends based on price peaks
var longTermTrends = new List<(DateTime Start, DateTime End, bool IsBullish)>();
for (int i = 1; i < peaks.Count; i++)
{
    var prevPeak = peaks[i - 1];
    var currPeak = peaks[i];

    // Bullish: min -> max, Bearish: max -> min
    if (prevPeak.IsMax && !currPeak.IsMax)
    {
        longTermTrends.Add((prevPeak.Time, currPeak.Time, false)); // Bearish
    }
    else if (!prevPeak.IsMax && currPeak.IsMax)
    {
        longTermTrends.Add((prevPeak.Time, currPeak.Time, true)); // Bullish
    }
}

Console.WriteLine($"Defined {longTermTrends.Count} long-term trends based on price peaks.");

// Compare MACD periods with long-term trends
Console.WriteLine("MACD Period Overlap with Long-Term Trends:");
foreach (var (fast, slow, signal) in macdCombinations)
{
    var (macdBullish, macdBearish) = macdPeriodsByCombination[(fast, slow, signal)];

    double totalBullishOverlap = 0.0;
    double totalBearishOverlap = 0.0;

    foreach (var (trendStart, trendEnd, isBullish) in longTermTrends)
    {
        var macdPeriods = isBullish ? macdBullish : macdBearish;

        foreach (var (macdStart, macdEnd) in macdPeriods)
        {
            totalBullishOverlap += isBullish ? CalculateOverlap(trendStart, trendEnd, macdStart, macdEnd) : 0.0;
            totalBearishOverlap += !isBullish ? CalculateOverlap(trendStart, trendEnd, macdStart, macdEnd) : 0.0;
        }
    }

    double totalTrendBullishDuration = longTermTrends.Where(t => t.IsBullish).Sum(t => (t.End - t.Start).TotalDays);
    double totalTrendBearishDuration = longTermTrends.Where(t => !t.IsBullish).Sum(t => (t.End - t.Start).TotalDays);

    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"    Bullish Overlap: {totalBullishOverlap:F1} days ({(totalTrendBullishDuration > 0 ? totalBullishOverlap / totalTrendBullishDuration * 100 : 0):F2}%)");
    Console.WriteLine($"    Bearish Overlap: {totalBearishOverlap:F1} days ({(totalTrendBearishDuration > 0 ? totalBearishOverlap / totalTrendBearishDuration * 100 : 0):F2}%)");
}

Le MACD est calculé sur les données horaires/journalières de BTC pour détecter les croisements de signal de CSharp-BTC-MACD-ADX.

// Measure temporal distance between MACD signals and trend reversals
Console.WriteLine("Temporal Distance Between MACD Signals and Trend Reversals:");

foreach (var (fast, slow, signal) in macdCombinations)
{
    var (macdBullish, macdBearish) = macdPeriodsByCombination[(fast, slow, signal)];

    double totalDistanceBullish = 0.0;
    double totalDistanceBearish = 0.0;
    int countBullish = 0;
    int countBearish = 0;

    foreach (var (trendStart, trendEnd, isBullish) in longTermTrends)
    {
        var macdPeriods = isBullish ? macdBullish : macdBearish;

        foreach (var (macdStart, macdEnd) in macdPeriods)
        {
            if (macdStart <= trendEnd && macdEnd >= trendStart)
            {
                // Calculate distances
                double startDistance = Math.Abs((macdStart - trendStart).TotalDays);
                double endDistance = Math.Abs((macdEnd - trendEnd).TotalDays);

                if (isBullish)
                {
                    totalDistanceBullish += startDistance + endDistance;
                    countBullish += 2;
                }
                else
                {
                    totalDistanceBearish += startDistance + endDistance;
                    countBearish += 2;
                }
            }
        }
    }

    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"    Avg Distance (Bullish): {(countBullish > 0 ? totalDistanceBullish / countBullish : 0):F2} days");
    Console.WriteLine($"    Avg Distance (Bearish): {(countBearish > 0 ? totalDistanceBearish / countBearish : 0):F2} days");
}

Traitement des données et calcul des variables intermédiaires pour la stratégie CSharp-BTC-MACD-ADX (étape 32).

Selecting ADX and MACD initial sets of parameters

Console.WriteLine("Combining MACD and ADX to Analyze Long-Term Trend Alignment:");

foreach (var (fast, slow, signal) in macdCombinations)
{
    var (macdBullish, macdBearish) = macdPeriodsByCombination[(fast, slow, signal)];
    var totalBullishOverlap = 0.0;
    var totalBearishOverlap = 0.0;

    foreach (var (trendStart, trendEnd, isBullish) in longTermTrends)
    {
        var macdPeriods = isBullish ? macdBullish : macdBearish;
        foreach (var (macdStart, macdEnd) in macdPeriods)
        {
            if (macdStart <= trendEnd && macdEnd >= trendStart)
            {
                // Corriger les comparaisons avec .Ticks
                var overlapStart = macdStart > trendStart ? macdStart : trendStart;
                var overlapEnd = macdEnd < trendEnd ? macdEnd : trendEnd;

                var overlap = (overlapEnd - overlapStart).TotalDays;
                if (overlap > 0)
                {
                    var adxStart = overlapStart;
                    var adxEnd = overlapEnd;

                    var adxOverlap = adxPeaksByPeriodThreshold[20][30]
                        .Count(adx => adx.StartWindow <= adxEnd && adx.EndWindow >= adxStart);

                    if (isBullish) totalBullishOverlap += overlap * adxOverlap;
                    else totalBearishOverlap += overlap * adxOverlap;
                }
            }
        }
    }

    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"    Weighted Bullish Overlap: {totalBullishOverlap:F2} days");
    Console.WriteLine($"    Weighted Bearish Overlap: {totalBearishOverlap:F2} days");
}

Évaluation de l’impact sur les prix pour chaque combinaison MACD (fast, slow, signal) en C# LEAN.

Console.WriteLine("Evaluating Price Impact During Overlap Periods:");


foreach (var (fast, slow, signal) in macdCombinations)
{
    var (macdBullish, macdBearish) = macdPeriodsByCombination[(fast, slow, signal)];
    var bullishPriceImpact = 0.0m;
    var bearishPriceImpact = 0.0m;

    foreach (var (trendStart, trendEnd, isBullish) in longTermTrends)
    {
        var macdPeriods = isBullish ? macdBullish : macdBearish;
        foreach (var (macdStart, macdEnd) in macdPeriods)
        {
            if (macdStart <= trendEnd && macdEnd >= trendStart)
            {
                var overlapStart = macdStart > trendStart ? macdStart : trendStart;
                var overlapEnd = macdEnd < trendEnd ? macdEnd : trendEnd;

                if (overlapEnd > overlapStart)
                {
                    var startPrice = priceByDate[overlapStart];
                    var endPrice = priceByDate[overlapEnd];
                    var priceChange = (endPrice - startPrice) / startPrice;

                    if (isBullish) bullishPriceImpact += priceChange;
                    else bearishPriceImpact += priceChange;
                }
            }
        }
    }

    Console.WriteLine($"  Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"    Bullish Price Impact: {bullishPriceImpact:P}");
    Console.WriteLine($"    Bearish Price Impact: {bearishPriceImpact:P}");
}

L’ADX est calculé pour mesurer la force de la tendance BTC et conditionner les entrées du MACD dans la stratégie CSharp-BTC-MACD-ADX.

List<(DateTime Start, DateTime End)> ExtractBullishPeriods(List<(DateTime Time, decimal Macd, decimal Signal, decimal Histogram)> macdValues)
{
    var bullishPeriods = new List<(DateTime Start, DateTime End)>();
    DateTime? currentStart = null;

    foreach (var macdValue in macdValues)
    {
        if (macdValue.Histogram > 0) // Bullish condition
        {
            if (currentStart == null)
            {
                currentStart = macdValue.Time;
            }
        }
        else if (currentStart != null) // End of bullish period
        {
            bullishPeriods.Add((currentStart.Value, macdValue.Time));
            currentStart = null;
        }
    }

    // Close the last bullish period if still open
    if (currentStart != null)
    {
        bullishPeriods.Add((currentStart.Value, macdValues.Last().Time));
    }

    return bullishPeriods;
}

List<(DateTime Start, DateTime End)> ExtractBearishPeriods(List<(DateTime Time, decimal Macd, decimal Signal, decimal Histogram)> macdValues)
{
    var bearishPeriods = new List<(DateTime Start, DateTime End)>();
    DateTime? currentStart = null;

    foreach (var macdValue in macdValues)
    {
        if (macdValue.Histogram < 0) // Bearish condition
        {
            if (currentStart == null)
            {
                currentStart = macdValue.Time;
            }
        }
        else if (currentStart != null) // End of bearish period
        {
            bearishPeriods.Add((currentStart.Value, macdValue.Time));
            currentStart = null;
        }
    }

    // Close the last bearish period if still open
    if (currentStart != null)
    {
        bearishPeriods.Add((currentStart.Value, macdValues.Last().Time));
    }

    return bearishPeriods;
}

Traitement C# LEAN pour la stratégie CSharp-BTC-MACD-ADX : calcul des métriques et évaluation des signaux.

var bullishPriceImpacts = new Dictionary<(int Fast, int Slow, int Signal), decimal>();
var bearishPriceImpacts = new Dictionary<(int Fast, int Slow, int Signal), decimal>();

foreach (var (fast, slow, signal) in macdCombinations)
{
    var macdValues = macdValuesByCombination[(fast, slow, signal)];

    // Extract bullish and bearish periods
    var bullishPeriods = ExtractBullishPeriods(macdValues);
    var bearishPeriods = ExtractBearishPeriods(macdValues);

    var bullishImpact = 1m;
    var bearishImpact = 1m;

    // Calculate price impacts for bullish periods
    foreach (var (start, end) in bullishPeriods)
    {
        if (priceByDate.ContainsKey(start) && priceByDate.ContainsKey(end))
        {
            bullishImpact *= priceByDate[end] / priceByDate[start];
        }
    }

    // Calculate price impacts for bearish periods
    foreach (var (start, end) in bearishPeriods)
    {
        if (priceByDate.ContainsKey(start) && priceByDate.ContainsKey(end))
        {
            bearishImpact *= priceByDate[start] / priceByDate[end];
        }
    }

    bullishPriceImpacts[(fast, slow, signal)] = (bullishImpact - 1) * 100;
    bearishPriceImpacts[(fast, slow, signal)] = (1 - bearishImpact) * 100;

    Console.WriteLine($"Fast: {fast}, Slow: {slow}, Signal: {signal}");
    Console.WriteLine($"  Bullish Price Impact: {bullishPriceImpacts[(fast, slow, signal)]:F2}%");
    Console.WriteLine($"  Bearish Price Impact: {bearishPriceImpacts[(fast, slow, signal)]:F2}%");
}

Analysing ADX and MACD to prepare optimization of the remaining parameters

Retour au sommet