// 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"[REFERENCE QC Cloud] Ce notebook utilise QuantBook et necessite l’environnement QuantConnect Cloud. Pour executer : https://www.quantconnect.com/research

Initializing environment
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}%");
}