mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 01:58:06 +00:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Convolution : AbstractBase
|
||||
{
|
||||
private readonly double[] _kernel;
|
||||
private readonly int _kernelSize;
|
||||
private CircularBuffer _buffer;
|
||||
private double[] _normalizedKernel;
|
||||
|
||||
public Convolution(double[] kernel)
|
||||
{
|
||||
if (kernel == null || kernel.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Kernel must not be null or empty.", nameof(kernel));
|
||||
}
|
||||
_kernel = kernel;
|
||||
_kernelSize = kernel.Length;
|
||||
_buffer = new CircularBuffer(_kernelSize);
|
||||
_normalizedKernel = new double[_kernelSize];
|
||||
Init();
|
||||
}
|
||||
|
||||
public Convolution(object source, double[] kernel) : this(kernel)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
Array.Copy(_kernel, _normalizedKernel, _kernelSize);
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double GetLastValid()
|
||||
{
|
||||
return _lastValidValue;
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
// Normalize kernel on each calculation until buffer is full
|
||||
if (_index <= _kernelSize)
|
||||
{
|
||||
NormalizeKernel();
|
||||
}
|
||||
|
||||
double result = ConvolveBuffer();
|
||||
IsHot = _index >= _kernelSize;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void NormalizeKernel()
|
||||
{
|
||||
int activeLength = Math.Min(_index, _kernelSize);
|
||||
double sum = 0;
|
||||
|
||||
// Calculate the sum of the active kernel elements
|
||||
for (int i = 0; i < activeLength; i++)
|
||||
{
|
||||
sum += _kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel or set equal weights if the sum is zero
|
||||
double normalizationFactor = (sum != 0) ? sum : activeLength;
|
||||
for (int i = 0; i < activeLength; i++)
|
||||
{
|
||||
_normalizedKernel[i] = _kernel[i] / normalizationFactor;
|
||||
}
|
||||
|
||||
// Set the rest of the normalized kernel to zero
|
||||
Array.Clear(_normalizedKernel, activeLength, _kernelSize - activeLength);
|
||||
}
|
||||
|
||||
private double ConvolveBuffer()
|
||||
{
|
||||
double sum = 0;
|
||||
var bufferSpan = _buffer.GetSpan();
|
||||
int activeLength = Math.Min(_index, _kernelSize);
|
||||
|
||||
for (int i = 0; i < activeLength; i++)
|
||||
{
|
||||
sum += bufferSpan[activeLength - 1 - i] * _normalizedKernel[i];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// DWMA: Double Weighted Moving Average
|
||||
/// DWMA is a technical indicator that applies a Weighted Moving Average (WMA) twice to the input data.
|
||||
/// The weights are decreasing over the period with p^2 decay, and the most recent data has the heaviest weight.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Smoothness: ★★★★★ (5/5)
|
||||
/// Sensitivity: ★★★☆☆ (3/5)
|
||||
/// Overshooting: ★★★★☆ (4/5)
|
||||
/// Lag: ★★☆☆☆ (2/5)
|
||||
///
|
||||
/// The DWMA is calculated by applying two WMAs in sequence:
|
||||
/// 1. An inner WMA is applied to the input data.
|
||||
/// 2. An outer WMA is then applied to the result of the inner WMA.
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - The weight distribution follows a p^2 decay, where p is the position of the data point.
|
||||
/// - More recent data points receive higher weights, emphasizing recent price movements.
|
||||
/// - The double application of WMA results in a smoother indicator compared to a single WMA.
|
||||
///
|
||||
/// The formula for DWMA can be expressed as:
|
||||
/// DWMA = WMA(WMA(price, period), period)
|
||||
///
|
||||
/// Where WMA is the Weighted Moving Average function and 'period' is the number of data points used in each WMA calculation.
|
||||
/// </remarks>
|
||||
|
||||
public class Dwma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Wma _innerWma;
|
||||
private readonly Wma _outerWma;
|
||||
|
||||
public Dwma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_innerWma = new Wma(period);
|
||||
_outerWma = new Wma(period);
|
||||
Name = "Wma";
|
||||
WarmupPeriod = 2 * _period - 1;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Dwma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_innerWma.Init();
|
||||
_outerWma.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Calculate inner WMA
|
||||
TValue innerResult = _innerWma.Calc(Input);
|
||||
|
||||
// Calculate outer WMA using the result of inner WMA
|
||||
TValue outerResult = _outerWma.Calc(innerResult);
|
||||
|
||||
double result = outerResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Epma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Epma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateKernel(_period));
|
||||
Name = "Epma";
|
||||
WarmupPeriod = period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Epma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
|
||||
// Adjust for partial periods during warmup
|
||||
if (_index < _period)
|
||||
{
|
||||
double[] partialKernel = GenerateKernel(_index);
|
||||
result /= partialKernel.Sum();
|
||||
}
|
||||
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double[] GenerateKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double weightSum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] = (2 * period - 1) - 3 * i;
|
||||
weightSum += kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] /= weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Fwma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Fwma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateKernel(_period));
|
||||
Name = "Fwma";
|
||||
WarmupPeriod = period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Fwma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public static double[] GenerateKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double[] fibSeries = new double[period];
|
||||
double weightSum = 0;
|
||||
|
||||
// Generate Fibonacci series
|
||||
fibSeries[0] = fibSeries[1] = 1;
|
||||
for (int i = 2; i < period; i++)
|
||||
{
|
||||
fibSeries[i] = fibSeries[i - 1] + fibSeries[i - 2];
|
||||
}
|
||||
|
||||
// Reverse the series to give more weight to recent prices
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] = fibSeries[period - 1 - i];
|
||||
weightSum += kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] /= weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Gma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Gma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateKernel(_period));
|
||||
Name = "Gma";
|
||||
WarmupPeriod = period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Gma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public static double[] GenerateKernel(int period, double sigma = 1.0)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double weightSum = 0;
|
||||
int center = period / 2;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
double x = (i - center) / (double)center;
|
||||
kernel[i] = Math.Exp(-(x * x) / (2 * sigma * sigma));
|
||||
weightSum += kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] /= weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Hma : AbstractBase
|
||||
{
|
||||
private readonly int _period, _sqrtPeriod;
|
||||
private readonly Convolution _wmaHalf, _wmaFull, _wmaFinal;
|
||||
|
||||
public Hma(int period)
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 2.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_sqrtPeriod = (int)Math.Sqrt(period);
|
||||
_wmaHalf = new Convolution(GenerateWmaKernel(period / 2));
|
||||
_wmaFull = new Convolution(GenerateWmaKernel(period));
|
||||
_wmaFinal = new Convolution(GenerateWmaKernel(_sqrtPeriod));
|
||||
Name = "Hma";
|
||||
WarmupPeriod = _period + _sqrtPeriod - 1;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Hma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private static double[] GenerateWmaKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double weightSum = period * (period + 1) / 2.0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] = (period - i) / weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_wmaHalf.Init();
|
||||
_wmaFull.Init();
|
||||
_wmaFinal.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Calculate WMA(n/2) and WMA(n)
|
||||
double wmaHalfResult = _wmaHalf.Calc(Input).Value;
|
||||
double wmaFullResult = _wmaFull.Calc(Input).Value;
|
||||
|
||||
// Calculate 2*WMA(n/2) - WMA(n)
|
||||
double intermediateResult = 2 * wmaHalfResult - wmaFullResult;
|
||||
|
||||
// Calculate final WMA
|
||||
double result = _wmaFinal.Calc(new TValue(Input.Time, intermediateResult, Input.IsNew)).Value;
|
||||
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Sinema : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Sinema(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateKernel(_period));
|
||||
Name = "Sinema";
|
||||
WarmupPeriod = period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Sinema(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double[] GenerateKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double weightSum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
// Use sine function to generate weights
|
||||
kernel[i] = Math.Sin((i + 1) * Math.PI / (period + 1));
|
||||
weightSum += kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] /= weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Trima : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Trima(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateKernel(_period));
|
||||
Name = "Trima";
|
||||
WarmupPeriod = period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Trima(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private static double[] GenerateKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
int halfPeriod = (period + 1) / 2;
|
||||
double weightSum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
if (i < halfPeriod)
|
||||
{
|
||||
kernel[i] = i + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
kernel[i] = period - i;
|
||||
}
|
||||
weightSum += kernel[i];
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] /= weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Wma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Convolution _convolution;
|
||||
|
||||
public Wma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
_convolution = new Convolution(GenerateWmaKernel(_period));
|
||||
Name = "Wma";
|
||||
WarmupPeriod = _period;
|
||||
Init();
|
||||
}
|
||||
|
||||
public Wma(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
private static double[] GenerateWmaKernel(int period)
|
||||
{
|
||||
double[] kernel = new double[period];
|
||||
double weightSum = period * (period + 1) / 2.0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] = (period - i) / weightSum;
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private new void Init()
|
||||
{
|
||||
base.Init();
|
||||
_convolution.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
// Use Convolution for calculation
|
||||
TValue convolutionResult = _convolution.Calc(Input);
|
||||
|
||||
double result = convolutionResult.Value;
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base implementation for financial indicators in the QuanTAlib library.
|
||||
/// This abstract class implements the iTValue interface and defines common properties
|
||||
/// and methods used by inheriting indicator types.
|
||||
/// </summary>
|
||||
public abstract class AbstractBase : iTValue
|
||||
{
|
||||
public DateTime Time { get; set; }
|
||||
public double Value { get; set; }
|
||||
public bool IsNew { get; set; }
|
||||
public bool IsHot { get; set; }
|
||||
|
||||
public TValue Input { get; set; }
|
||||
public String Name { get; set; } = "";
|
||||
public int WarmupPeriod { get; set; }
|
||||
|
||||
public TValue Tick => new(Time, Value, IsNew, IsHot); // Stores the current value of indicator
|
||||
public event ValueSignal Pub = delegate { }; // Publisher of generated values
|
||||
|
||||
protected int _index; //tracking the position of output
|
||||
protected double _lastValidValue;
|
||||
// other _internal vars defined here
|
||||
|
||||
protected AbstractBase()
|
||||
{ //add parameters into constructor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a data source and triggers calculations on new data.
|
||||
/// </summary>
|
||||
/// <param name="source">The class publishing the data.</param>
|
||||
/// <param name="args">The argument containing the new data point.</param>
|
||||
public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick);
|
||||
|
||||
public virtual void Init()
|
||||
{
|
||||
_index = 0;
|
||||
_lastValidValue = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the indicator value based on the input; calls specific Calculation() method
|
||||
/// where implementation is
|
||||
/// </summary>
|
||||
/// <param name="input">The input value for the calculation.</param>
|
||||
/// <returns>A TValue representing the calculated indicator value.</returns>
|
||||
public virtual TValue Calc(TValue input)
|
||||
{
|
||||
Input = input;
|
||||
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
||||
{
|
||||
return Process(new TValue(input.Time, GetLastValid(), input.IsNew, input.IsHot));
|
||||
}
|
||||
this.Value = Calculation();
|
||||
return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot));
|
||||
}
|
||||
|
||||
protected virtual double GetLastValid()
|
||||
{
|
||||
return this.Value;
|
||||
}
|
||||
protected abstract void ManageState(bool isNew);
|
||||
protected abstract double Calculation();
|
||||
|
||||
/// <summary>
|
||||
/// Processes the calculated value, updates the indicator's own state,
|
||||
/// and publishes the result through an event.
|
||||
/// </summary>
|
||||
/// <param name="value">The calculated TValue to process.</param>
|
||||
/// <returns>The processed TValue.</returns>
|
||||
protected virtual TValue Process(TValue value)
|
||||
{
|
||||
this.Time = value.Time;
|
||||
this.Value = value.Value;
|
||||
this.IsNew = value.IsNew;
|
||||
this.IsHot = value.IsHot;
|
||||
Pub?.Invoke(this, new ValueEventArgs(value));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Numerics;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class CircularBuffer : IEnumerable<double>
|
||||
{
|
||||
private readonly double[] _buffer;
|
||||
private int _start = 0;
|
||||
private int _size = 0;
|
||||
|
||||
public int Capacity { get; }
|
||||
public int Count => _size;
|
||||
|
||||
public CircularBuffer(int capacity)
|
||||
{
|
||||
Capacity = capacity;
|
||||
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(double item, bool isNew = true)
|
||||
{
|
||||
if (_size == 0 || isNew)
|
||||
{
|
||||
if (_size < Capacity)
|
||||
{
|
||||
_buffer[(_start + _size) % Capacity] = item;
|
||||
_size++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[_start] = item;
|
||||
_start = (_start + 1) % Capacity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[(_start + _size - 1) % Capacity] = item;
|
||||
}
|
||||
}
|
||||
|
||||
public double this[Index index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value;
|
||||
actualIndex = Math.Clamp(actualIndex, 0, _size - 1);
|
||||
return _buffer[(_start + actualIndex) % Capacity];
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value;
|
||||
actualIndex = Math.Clamp(actualIndex, 0, _size - 1);
|
||||
_buffer[(_start + actualIndex) % Capacity] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeException()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("index", "Index is out of range.");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Newest()
|
||||
{
|
||||
if (_size == 0)
|
||||
return 0;
|
||||
return _buffer[(_start + _size - 1) % Capacity];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Oldest()
|
||||
{
|
||||
if (_size == 0)
|
||||
ThrowInvalidOperationException();
|
||||
return _buffer[_start];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidOperationException()
|
||||
{
|
||||
throw new InvalidOperationException("Buffer is empty.");
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new(this);
|
||||
IEnumerator<double> IEnumerable<double>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public struct Enumerator : IEnumerator<double>
|
||||
{
|
||||
private readonly CircularBuffer _buffer;
|
||||
private int _index;
|
||||
private double _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Enumerator(CircularBuffer buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _buffer._size)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
_current = _buffer[_index];
|
||||
return true;
|
||||
}
|
||||
|
||||
public double Current => _current;
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(double[] destination, int destinationIndex)
|
||||
{
|
||||
if (_size == 0)
|
||||
return;
|
||||
|
||||
if (_start + _size <= Capacity)
|
||||
{
|
||||
Array.Copy(_buffer, _start, destination, destinationIndex, _size);
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstPartLength = Capacity - _start;
|
||||
Array.Copy(_buffer, _start, destination, destinationIndex, firstPartLength);
|
||||
Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _size - firstPartLength);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetSpan()
|
||||
{
|
||||
if (_size == 0)
|
||||
return ReadOnlySpan<double>.Empty;
|
||||
|
||||
if (_start + _size <= Capacity)
|
||||
{
|
||||
return new ReadOnlySpan<double>(_buffer, _start, _size);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ReadOnlySpan<double>(ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public double[] InternalBuffer => _buffer;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetInternalSpan() => _buffer.AsSpan();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(_buffer, 0, _buffer.Length);
|
||||
_start = 0;
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Max()
|
||||
{
|
||||
if (_size == 0)
|
||||
ThrowInvalidOperationException();
|
||||
|
||||
return MaxSimd();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Min()
|
||||
{
|
||||
if (_size == 0)
|
||||
ThrowInvalidOperationException();
|
||||
|
||||
return MinSimd();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Sum()
|
||||
{
|
||||
return SumSimd();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Average()
|
||||
{
|
||||
if (_size == 0)
|
||||
ThrowInvalidOperationException();
|
||||
|
||||
return SumSimd() / _size;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double MaxSimd()
|
||||
{
|
||||
var span = GetSpan();
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var maxVector = new Vector<double>(double.MinValue);
|
||||
|
||||
int i = 0;
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
maxVector = Vector.Max(maxVector, new Vector<double>(span.Slice(i, vectorSize)));
|
||||
}
|
||||
|
||||
double max = double.MinValue;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
max = Math.Max(max, maxVector[j]);
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
max = Math.Max(max, span[i]);
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double MinSimd()
|
||||
{
|
||||
var span = GetSpan();
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var minVector = new Vector<double>(double.MaxValue);
|
||||
|
||||
int i = 0;
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
minVector = Vector.Min(minVector, new Vector<double>(span.Slice(i, vectorSize)));
|
||||
}
|
||||
|
||||
double min = double.MaxValue;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
min = Math.Min(min, minVector[j]);
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
min = Math.Min(min, span[i]);
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double SumSimd()
|
||||
{
|
||||
var span = GetSpan();
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var sumVector = Vector<double>.Zero;
|
||||
|
||||
int i = 0;
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
sumVector += new Vector<double>(span.Slice(i, vectorSize));
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
sum += sumVector[j];
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
sum += span[i];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
public double[] ToArray()
|
||||
{
|
||||
double[] array = new double[_size];
|
||||
CopyTo(array, 0);
|
||||
return array;
|
||||
}
|
||||
|
||||
public void ParallelOperation(Func<double[], int, int, double> operation)
|
||||
{
|
||||
const int MinimumPartitionSize = 1024;
|
||||
|
||||
if (_size < MinimumPartitionSize)
|
||||
{
|
||||
var span = GetSpan();
|
||||
var array = span.ToArray();
|
||||
operation(array, 0, array.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
int partitionCount = Environment.ProcessorCount;
|
||||
int partitionSize = _size / partitionCount;
|
||||
|
||||
if (partitionSize < MinimumPartitionSize)
|
||||
{
|
||||
partitionCount = Math.Max(1, _size / MinimumPartitionSize);
|
||||
partitionSize = _size / partitionCount;
|
||||
}
|
||||
|
||||
var buffer = ToArray();
|
||||
var results = new double[partitionCount];
|
||||
|
||||
Parallel.For(0, partitionCount, i =>
|
||||
{
|
||||
int start = i * partitionSize;
|
||||
int length = (i == partitionCount - 1) ? _size - start : partitionSize;
|
||||
results[i] = operation(buffer, start, length);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using Microsoft.DotNet.Interactive.Formatting;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
|
||||
namespace QuanTAlib;
|
||||
public static class Formatters
|
||||
{
|
||||
const string smallfont = "smaller";
|
||||
const string pad = "18";
|
||||
public static void Initialize()
|
||||
{
|
||||
Formatter.Register<iTValue>((tick, writer) =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<table style='border-collapse: collapse; text-align: left;'><tr>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{tick.Time:yyyy-MM-dd HH:mm:ss}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{tick.Value:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{(tick.IsHot ? "🔥" : "❄️")}</td>");
|
||||
sb.Append("</tr></table>");
|
||||
writer.Write(sb.ToString());
|
||||
}, HtmlFormatter.MimeType);
|
||||
|
||||
Formatter.Register<TSeries>((series, writer) =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<table style='border-collapse: collapse; text-align: right;'></tr>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; text-align: left;'><b>{series.Name}</b></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Index</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Value</i></th>");
|
||||
sb.Append("</tr>");
|
||||
|
||||
for (int i = 0; i < Math.Min(100, series.Count); i++)
|
||||
{
|
||||
TValue item = series[i];
|
||||
sb.Append("<tr>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{item.Time:yyyy-MM-dd HH:mm:ss}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{i}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Value:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{(item.IsHot ? "🔥" : "❄️")}</td>");
|
||||
sb.Append("</tr>");
|
||||
}
|
||||
sb.Append("</table>");
|
||||
if (series.Count > 100)
|
||||
{
|
||||
sb.Append("<p>Showing first 100 items. Total items: " + series.Count + "</p>");
|
||||
}
|
||||
writer.Write(sb.ToString());
|
||||
}, HtmlFormatter.MimeType);
|
||||
|
||||
Formatter.Register<TBar>((bar, writer) =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<table style='border-collapse: collapse; text-align: right;'><tr>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{bar.Time:yyyy-MM-dd HH:mm:ss}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Open:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.High:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Low:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Close:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'> {bar.Volume:F2}</td>");
|
||||
sb.Append("</tr></table>");
|
||||
writer.Write(sb.ToString());
|
||||
}, HtmlFormatter.MimeType);
|
||||
|
||||
Formatter.Register<TBarSeries>((series, writer) =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<table style='border-collapse: collapse; text-align: right;'></tr>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; text-align: left;'><b>{series.Name}</b></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Index</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Open</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>High</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Low</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Close</i></th>");
|
||||
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Volume</i></th>");
|
||||
sb.Append("</tr>");
|
||||
for (int i = 0; i < Math.Min(100, series.Count); i++)
|
||||
{
|
||||
TBar item = series[i];
|
||||
sb.Append("<tr>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{item.Time:yyyy-MM-dd HH:mm:ss}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{i}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Open:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.High:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Low:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Close:F2}</td>");
|
||||
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Volume:F2}</td>");
|
||||
sb.Append("</tr>");
|
||||
}
|
||||
sb.Append("</table>");
|
||||
if (series.Count > 100)
|
||||
{
|
||||
sb.Append("<p>Showing first 100 items. Total items: " + series.Count + "</p>");
|
||||
}
|
||||
writer.Write(sb.ToString());
|
||||
}, HtmlFormatter.MimeType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public interface iTBar
|
||||
{
|
||||
DateTime Time { get; }
|
||||
double Open { get; }
|
||||
double High { get; }
|
||||
double Low { get; }
|
||||
double Close { get; }
|
||||
double Volume { get; }
|
||||
bool IsNew { get; }
|
||||
}
|
||||
|
||||
public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : iTBar
|
||||
{
|
||||
public DateTime Time { get; init; } = Time;
|
||||
public double Open { get; init; } = Open;
|
||||
public double High { get; init; } = High;
|
||||
public double Low { get; init; } = Low;
|
||||
public double Close { get; init; } = Close;
|
||||
public double Volume { get; init; } = Volume;
|
||||
public bool IsNew { get; init; } = IsNew;
|
||||
|
||||
public double HL2 => (High + Low) * 0.5;
|
||||
public double OC2 => (Open + Close) * 0.5;
|
||||
public double OHL3 => (Open + High + Low) / 3;
|
||||
public double HLC3 => (High + Low + Close) / 3;
|
||||
public double OHLC4 => (Open + High + Low + Close) * 0.25;
|
||||
public double HLCC4 => (High + Low + Close + Close) * 0.25;
|
||||
|
||||
public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { }
|
||||
public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { }
|
||||
|
||||
// when TBar casts to double, it returns its Close
|
||||
public static implicit operator double(TBar bar) => bar.Close;
|
||||
public static implicit operator DateTime(TBar tv) => tv.Time;
|
||||
|
||||
// castings for sloppy people - a single double injected into a TBar, and a single TValue injected into a TBar
|
||||
public TBar(double value) : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { }
|
||||
public TBar(TValue value) : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { }
|
||||
|
||||
public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
|
||||
}
|
||||
|
||||
public delegate void BarSignal(object source, in TBarEventArgs args);
|
||||
|
||||
public class TBarEventArgs : EventArgs
|
||||
{
|
||||
public TBar Bar { get; }
|
||||
public TBarEventArgs(TBar bar) { Bar = bar; }
|
||||
}
|
||||
|
||||
public class TBarSeries : List<TBar>
|
||||
{
|
||||
private readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
|
||||
public TSeries Open;
|
||||
public TSeries High;
|
||||
public TSeries Low;
|
||||
public TSeries Close;
|
||||
public TSeries Volume;
|
||||
|
||||
|
||||
public TBar Last => Count > 0 ? this[^1] : Default;
|
||||
public TBar First => Count > 0 ? this[0] : Default;
|
||||
public int Length => Count;
|
||||
public string Name { get; set; }
|
||||
public event BarSignal Pub = delegate { };
|
||||
|
||||
public TBarSeries()
|
||||
{
|
||||
this.Name = "Bar";
|
||||
Open = new();
|
||||
High = new();
|
||||
Low = new();
|
||||
Close = new();
|
||||
Volume = new();
|
||||
}
|
||||
public TBarSeries(object source) : this()
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
|
||||
}
|
||||
|
||||
public new virtual void Add(TBar bar)
|
||||
{
|
||||
if (bar.IsNew) { base.Add(bar); } else { this[^1] = bar; }
|
||||
Pub?.Invoke(this, new TBarEventArgs(bar));
|
||||
|
||||
Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true);
|
||||
High.Add(bar.Time, bar.High, IsNew: bar.IsNew, IsHot: true);
|
||||
Low.Add(bar.Time, bar.Low, IsNew: bar.IsNew, IsHot: true);
|
||||
Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true);
|
||||
Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true);
|
||||
}
|
||||
public void Add(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) =>
|
||||
this.Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew));
|
||||
|
||||
public void Add(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) =>
|
||||
this.Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew));
|
||||
|
||||
public void Add(TBarSeries series)
|
||||
{
|
||||
if (series == this)
|
||||
{
|
||||
// If adding itself, create a copy to avoid modification during enumeration
|
||||
var copy = new TBarSeries { Name = this.Name };
|
||||
copy.AddRange(this);
|
||||
AddRange(copy);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddRange(series);
|
||||
}
|
||||
}
|
||||
public new virtual void AddRange(IEnumerable<TBar> collection)
|
||||
{
|
||||
foreach (var item in collection)
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Sub(object source, in TBarEventArgs args)
|
||||
{
|
||||
Add(args.Bar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public interface iTValue
|
||||
{
|
||||
DateTime Time { get; }
|
||||
double Value { get; }
|
||||
bool IsNew { get; }
|
||||
bool IsHot { get; }
|
||||
}
|
||||
|
||||
public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) : iTValue
|
||||
{
|
||||
public DateTime Time { get; init; } = Time;
|
||||
public double Value { get; init; } = Value;
|
||||
public bool IsNew { get; init; } = IsNew;
|
||||
public bool IsHot { get; init; } = IsHot;
|
||||
public DateTime t => Time;
|
||||
public double v => Value;
|
||||
|
||||
public TValue() : this(DateTime.UtcNow, 0) { }
|
||||
public TValue(double value, bool isNew = true, bool isHot = true) : this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { }
|
||||
public static implicit operator double(TValue tv) => tv.Value;
|
||||
public static implicit operator DateTime(TValue tv) => tv.Time;
|
||||
public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value);
|
||||
|
||||
public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]";
|
||||
}
|
||||
|
||||
public delegate void ValueSignal(object source, in ValueEventArgs args);
|
||||
|
||||
public class ValueEventArgs : EventArgs
|
||||
{
|
||||
public TValue Tick { get; }
|
||||
public ValueEventArgs(TValue value) { Tick = value; }
|
||||
}
|
||||
|
||||
public class TSeries : List<TValue>
|
||||
{
|
||||
private readonly TValue Default = new(DateTime.MinValue, double.NaN);
|
||||
public IEnumerable<DateTime> t => this.Select(item => item.t);
|
||||
public IEnumerable<double> v => this.Select(item => item.v);
|
||||
public TValue Last => Count > 0 ? this[^1] : Default;
|
||||
public TValue First => Count > 0 ? this[0] : Default;
|
||||
public int Length => Count;
|
||||
public string Name { get; set; }
|
||||
public event ValueSignal Pub = delegate { };
|
||||
|
||||
public TSeries() { this.Name = "Data"; }
|
||||
|
||||
public TSeries(object source) : this()
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
if (pubEvent != null)
|
||||
{
|
||||
/*
|
||||
var nameProperty = source.GetType().GetProperty("Name");
|
||||
if (nameProperty != null) {
|
||||
Name = nameProperty.GetValue(nameProperty)?.ToString()!;
|
||||
}
|
||||
*/
|
||||
pubEvent.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
}
|
||||
public static explicit operator List<double>(TSeries series) => series.Select(item => item.Value).ToList();
|
||||
public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray();
|
||||
|
||||
public new virtual void Add(TValue tick)
|
||||
{
|
||||
if (tick.IsNew) { base.Add(tick); }
|
||||
else { this[^1] = tick; }
|
||||
Pub?.Invoke(this, new ValueEventArgs(tick));
|
||||
}
|
||||
public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(Time, Value, IsNew, IsHot));
|
||||
public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot));
|
||||
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
var valueList = values.ToList();
|
||||
int count = valueList.Count;
|
||||
DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
this.Add(startTime, valueList[i]);
|
||||
startTime = startTime.AddHours(1);
|
||||
}
|
||||
}
|
||||
public void Add(TSeries series)
|
||||
{
|
||||
if (series == this)
|
||||
{
|
||||
// If adding itself, create a copy to avoid modification during enumeration
|
||||
var copy = new TSeries { Name = this.Name };
|
||||
copy.AddRange(this);
|
||||
AddRange(copy);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddRange(series);
|
||||
}
|
||||
}
|
||||
public new virtual void AddRange(IEnumerable<TValue> collection)
|
||||
{
|
||||
foreach (var item in collection)
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
}
|
||||
public void Sub(object source, in ValueEventArgs args) { Add(args.Tick); }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.CommandLine.Rendering.Views;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class GbmFeed : TBarSeries
|
||||
{
|
||||
private readonly double _mu, _sigma;
|
||||
private readonly Random _random;
|
||||
private double _lastClose, _lastHigh, _lastLow;
|
||||
|
||||
public GbmFeed(double initialPrice = 100.0, double mu = 0.05, double sigma = 0.2) : base()
|
||||
{
|
||||
_lastClose = _lastHigh = _lastLow = initialPrice;
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
_random = new Random((int)DateTime.Now.Ticks);
|
||||
this.Name = $"GBM({_sigma:F2})";
|
||||
}
|
||||
|
||||
public void Add(bool isNew = true) => Add(time: DateTime.Now, isNew: isNew);
|
||||
public void Add(DateTime time, bool isNew = true) => base.Add(Generate(time, isNew));
|
||||
public void Add(int count)
|
||||
{
|
||||
DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count);
|
||||
TBar lastBar = new();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Add(startTime, true);
|
||||
Add(startTime, false);
|
||||
Add(startTime, false);
|
||||
startTime = startTime.AddHours(1);
|
||||
}
|
||||
}
|
||||
|
||||
public TBar Generate(DateTime time, bool isNew = true)
|
||||
{
|
||||
double dt = 1.0 / 252;
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double diffusion = _sigma * Math.Sqrt(dt) * GenerateNormalRandom();
|
||||
double newClose = _lastClose * Math.Exp(drift + diffusion);
|
||||
|
||||
double open = _lastClose;
|
||||
double high = Math.Max(_lastHigh, Math.Max(open, newClose) * (1 + _random.NextDouble() * 0.01));
|
||||
double low = Math.Min(_lastLow, Math.Min(open, newClose) * (1 - _random.NextDouble() * 0.01));
|
||||
double volume = 1000 + _random.NextDouble() * 1000;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_lastClose = newClose;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = Math.Max(_lastHigh, high);
|
||||
low = Math.Min(_lastLow, low);
|
||||
}
|
||||
_lastHigh = high;
|
||||
_lastLow = low;
|
||||
|
||||
TBar bar = new(time, open, high, low, newClose, volume, isNew);
|
||||
return bar;
|
||||
}
|
||||
|
||||
private double GenerateNormalRandom()
|
||||
{
|
||||
// Box-Muller transform to generate standard normal random variable
|
||||
double u1 = 1.0 - _random.NextDouble(); // Uniform(0,1] random doubles
|
||||
double u2 = 1.0 - _random.NextDouble();
|
||||
return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Title>QuanTAlib</Title>
|
||||
<Product>Library of TA Calculations, Charts and Strategies for Quantower</Product>
|
||||
<Description>Quantitative Technical Analysis Library in C# for Quantower</Description>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/mihakralj/QuanTAlib</RepositoryUrl>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<Authors>Miha Kralj</Authors>
|
||||
<Copyright>Miha Kralj</Copyright>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageReadmeFile>readme.md</PackageReadmeFile>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<DisableImplicitNamespaceImports>false</DisableImplicitNamespaceImports>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<RootNamespace>QuanTAlib</RootNamespace>
|
||||
<AssemblyName>QuanTAlib</AssemblyName>
|
||||
<IsPublishable>True</IsPublishable>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
|
||||
<DebugType>full</DebugType>
|
||||
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageTags>
|
||||
Indicators;Stock;Market;Technical;Analysis;Algorithmic;Trading;Trade;Trend;Momentum;Finance;Algorithm;Algo;
|
||||
AlgoTrading;Financial;Strategy;Chart;Charting;Oscillator;Overlay;Equity;Bitcoin;Crypto;Cryptocurrency;Forex;
|
||||
Quantitative;Historical;Quotes;
|
||||
</PackageTags>
|
||||
<NoWarn>$(NoWarn);NU5104</NoWarn>
|
||||
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageIcon>QuanTAlib2.png</PackageIcon>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/mihakralj/QuanTAlib/main/.github/QuanTAlib2.png</PackageIconUrl>
|
||||
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="readme.md" Pack="true" PackagePath=""/>
|
||||
<None Include="..\.github\QuanTAlib2.png" Pack="true" Visible="false" PackagePath=""/>
|
||||
<PackageReference Include="Microsoft.DotNet.Interactive.Formatting" Version="1.0.0-beta.21459.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="TradingPlatform.BusinessLayer">
|
||||
<HintPath>..\.github\TradingPlatform.BusinessLayer.dll</HintPath>
|
||||
</Reference>
|
||||
<None Include="..\.github\TradingPlatform.BusinessLayer.xml">
|
||||
<Link>TradingPlatform.BusinessLayer.xml</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
**Quan**titative **TA** **lib**rary (QuanTAlib) is a C# library of classess and methods for quantitative technical analysis.
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
// Shannon's Entropy calculation
|
||||
public class Entropy : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Entropy(int period) : base()
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for entropy calculation.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = 2;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Entropy(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Entropy(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double entropy = 0;
|
||||
if (_index > 1) // We need at least two data points for entropy calculation
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
int n = values.Length;
|
||||
|
||||
// Calculate probabilities
|
||||
var groupedValues = values.GroupBy(x => x).Select(g => new { Value = g.Key, Count = g.Count() });
|
||||
|
||||
// Use the actual count of values for probability calculation
|
||||
foreach (var group in groupedValues)
|
||||
{
|
||||
double probability = (double)group.Count / n;
|
||||
entropy -= probability * Math.Log2(probability);
|
||||
}
|
||||
|
||||
// Normalize the entropy based on the current number of unique values
|
||||
int uniqueValueCount = groupedValues.Count();
|
||||
double maxEntropy = Math.Log2(uniqueValueCount);
|
||||
|
||||
entropy = entropy == 0 ? 1 : entropy / maxEntropy;
|
||||
|
||||
}
|
||||
else { entropy = 1; }
|
||||
|
||||
IsHot = _buffer.Count >= Period;
|
||||
return entropy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
// Excess kurtosis calculated with Sheskin Algorithm
|
||||
public class Kurtosis : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Kurtosis(int period) : base()
|
||||
{
|
||||
if (period < 4)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 4 for kurtosis calculation.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = Period - 1;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Kurtosis(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Kurtosis(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double kurtosis = 0;
|
||||
if (_buffer.Count > 3)
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
double mean = values.Average();
|
||||
double n = values.Length;
|
||||
|
||||
double s2 = 0;
|
||||
double s4 = 0;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
double diff = values[i] - mean;
|
||||
s2 += diff * diff;
|
||||
s4 += diff * diff * diff * diff;
|
||||
}
|
||||
|
||||
double variance = s2 / (n - 1);
|
||||
|
||||
// Using the Sheskin Algorithm for kurtosis
|
||||
kurtosis = (n * (n + 1) * s4) / (variance * variance * (n - 3) * (n - 1) * (n - 2))
|
||||
- (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3)));
|
||||
}
|
||||
|
||||
IsHot = _buffer.Count >= Period;
|
||||
return kurtosis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
namespace QuanTAlib
|
||||
{
|
||||
public class Max : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
private readonly double _halfLife;
|
||||
private double _currentMax, _p_currentMax;
|
||||
private int _timeSinceNewMax, _p_timeSinceNewMax;
|
||||
|
||||
public Max(int period, double decay = 0) : base()
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
||||
}
|
||||
if (decay < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = 0;
|
||||
_buffer = new CircularBuffer(period);
|
||||
_halfLife = decay * 0.1;
|
||||
Name = $"Max(period={period}, halfLife={decay:F2})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Max(object source, int period, double decay = 0) : this(period, decay)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_currentMax = double.MinValue;
|
||||
_timeSinceNewMax = 0;
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_currentMax = _currentMax;
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
_timeSinceNewMax++;
|
||||
_p_timeSinceNewMax = _timeSinceNewMax;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentMax = _p_currentMax;
|
||||
_timeSinceNewMax = _p_timeSinceNewMax;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
if (Input.Value >= _currentMax)
|
||||
{
|
||||
_currentMax = Input.Value;
|
||||
_timeSinceNewMax = 0;
|
||||
}
|
||||
|
||||
double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period);
|
||||
_currentMax = _currentMax - decayRate * (_currentMax - _buffer.Average());
|
||||
_currentMax = Math.Min(_currentMax, _buffer.Max());
|
||||
|
||||
IsHot = true;
|
||||
return _currentMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuanTAlib
|
||||
{
|
||||
public class Median : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Median(int period) : base()
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = period;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Median(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Median(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double median;
|
||||
if (_index >= Period)
|
||||
{
|
||||
var sortedValues = _buffer.GetSpan().ToArray();
|
||||
Array.Sort(sortedValues);
|
||||
int middleIndex = sortedValues.Length / 2;
|
||||
|
||||
if (sortedValues.Length % 2 == 0)
|
||||
{
|
||||
median = (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
median = sortedValues[middleIndex];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
median = _buffer.Average(); // Use average until we have enough data points
|
||||
}
|
||||
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
return median;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
namespace QuanTAlib
|
||||
{
|
||||
public class Min : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
private readonly double _halfLife;
|
||||
private double _currentMin, _p_currentMin;
|
||||
private int _timeSinceNewMin, _p_timeSinceNewMin;
|
||||
|
||||
public Min(int period, double decay = 0) : base()
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
||||
}
|
||||
if (decay < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = 0;
|
||||
_buffer = new CircularBuffer(period);
|
||||
_halfLife = decay * 0.1;
|
||||
Name = $"Min(period={period}, halfLife={decay:F2})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Min(object source, int period, double decay = 0) : this(period, decay)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_currentMin = double.MaxValue;
|
||||
_timeSinceNewMin = 0;
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_currentMin = _currentMin;
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
_timeSinceNewMin++;
|
||||
_p_timeSinceNewMin = _timeSinceNewMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentMin = _p_currentMin;
|
||||
_timeSinceNewMin = _p_timeSinceNewMin;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
if (Input.Value <= _currentMin)
|
||||
{
|
||||
_currentMin = Input.Value;
|
||||
_timeSinceNewMin = 0;
|
||||
}
|
||||
|
||||
double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period);
|
||||
_currentMin = _currentMin + decayRate * (_buffer.Average() - _currentMin);
|
||||
_currentMin = Math.Max(_currentMin, _buffer.Min());
|
||||
|
||||
IsHot = true;
|
||||
return _currentMin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class Mode : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Mode(int period) : base()
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = period;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Mode(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Mode(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double mode;
|
||||
if (_index >= Period)
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
var groupedValues = values.GroupBy(v => v)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ThenBy(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
int maxCount = groupedValues.First().Count();
|
||||
var modes = groupedValues.TakeWhile(g => g.Count() == maxCount)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
mode = modes.Average(); // If there are multiple modes, we return their average
|
||||
}
|
||||
else
|
||||
{
|
||||
mode = _buffer.Average(); // Use average until we have enough data points
|
||||
}
|
||||
|
||||
IsHot = _index >= WarmupPeriod;
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
public class Percentile : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
public readonly double Percent;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Percentile(int period, double percent) : base()
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for percentile calculation.");
|
||||
}
|
||||
if (percent < 0 || percent > 100)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(percent), "Percent must be between 0 and 100.");
|
||||
}
|
||||
Period = period;
|
||||
Percent = percent;
|
||||
WarmupPeriod = 2;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Percentile(period={period}, percent={percent})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Percentile(object source, int period, double percent) : this(period, percent)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double result;
|
||||
if (_buffer.Count >= Period)
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
Array.Sort(values);
|
||||
|
||||
double position = (Percent / 100.0) * (values.Length - 1);
|
||||
int lowerIndex = (int)Math.Floor(position);
|
||||
int upperIndex = (int)Math.Ceiling(position);
|
||||
|
||||
if (lowerIndex == upperIndex)
|
||||
{
|
||||
result = values[lowerIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Interpolate between the two nearest values
|
||||
double lowerValue = values[lowerIndex];
|
||||
double upperValue = values[upperIndex];
|
||||
double fraction = position - lowerIndex;
|
||||
result = lowerValue + (upperValue - lowerValue) * fraction;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use average for insufficient data, like the Median class
|
||||
result = _buffer.Average();
|
||||
}
|
||||
|
||||
IsHot = _buffer.Count >= Period;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
public class Skew : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Skew(int period) : base()
|
||||
{
|
||||
if (period < 3)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 3 for skewness calculation.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = 3;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Skew(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Skew(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double skew = 0;
|
||||
if (_buffer.Count >= 3) // We need at least 3 data points for skewness
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
double mean = values.Average();
|
||||
double n = values.Length;
|
||||
|
||||
double sumCubedDeviations = 0;
|
||||
double sumSquaredDeviations = 0;
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
double deviation = value - mean;
|
||||
sumCubedDeviations += Math.Pow(deviation, 3);
|
||||
sumSquaredDeviations += Math.Pow(deviation, 2);
|
||||
}
|
||||
|
||||
// Calculate sample skewness using the adjusted Fisher-Pearson standardized moment coefficient
|
||||
double m3 = sumCubedDeviations / n;
|
||||
double m2 = sumSquaredDeviations / n;
|
||||
double s3 = Math.Pow(m2, 1.5);
|
||||
|
||||
if (s3 != 0) // Avoid division by zero
|
||||
{
|
||||
skew = (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3);
|
||||
}
|
||||
}
|
||||
|
||||
IsHot = _buffer.Count >= Period;
|
||||
return skew;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuanTAlib
|
||||
{
|
||||
public class Stddev : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
public readonly bool IsPopulation;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Stddev(int period, bool isPopulation = false) : base()
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2.");
|
||||
}
|
||||
Period = period;
|
||||
IsPopulation = isPopulation;
|
||||
WarmupPeriod = 0;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Stddev(period={period}, population={isPopulation})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Stddev(object source, int period, bool isPopulation = false) : this(period, isPopulation)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double stddev = 0;
|
||||
if (_buffer.Count > 1)
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
double mean = values.Average();
|
||||
double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2));
|
||||
|
||||
double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1;
|
||||
double variance = sumOfSquaredDifferences / divisor;
|
||||
stddev = Math.Sqrt(variance);
|
||||
}
|
||||
|
||||
IsHot = true; // StdDev calc is valid from bar 1
|
||||
return stddev;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuanTAlib
|
||||
{
|
||||
public class Variance : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
public readonly bool IsPopulation;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Variance(int period, bool isPopulation = false) : base()
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2.");
|
||||
}
|
||||
Period = period;
|
||||
IsPopulation = isPopulation;
|
||||
WarmupPeriod = 0;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"Variance(period={period}, population={isPopulation})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Variance(object source, int period, bool isPopulation = false) : this(period, isPopulation)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double variance = 0;
|
||||
if (_buffer.Count > 1)
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
double mean = values.Average();
|
||||
double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2));
|
||||
|
||||
double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1;
|
||||
variance = sumOfSquaredDifferences / divisor;
|
||||
}
|
||||
|
||||
IsHot = true;
|
||||
return variance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
public class Zscore : AbstractBase
|
||||
{
|
||||
public readonly int Period;
|
||||
private CircularBuffer _buffer;
|
||||
|
||||
public Zscore(int period) : base()
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Z-score calculation.");
|
||||
}
|
||||
Period = period;
|
||||
WarmupPeriod = 2;
|
||||
_buffer = new CircularBuffer(period);
|
||||
Name = $"ZScore(period={period})";
|
||||
Init();
|
||||
}
|
||||
|
||||
public Zscore(object source, int period) : this(period)
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
protected override void ManageState(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_lastValidValue = Input.Value;
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double Calculation()
|
||||
{
|
||||
ManageState(Input.IsNew);
|
||||
|
||||
_buffer.Add(Input.Value, Input.IsNew);
|
||||
|
||||
double zScore = 0;
|
||||
if (_buffer.Count >= 2) // We need at least 2 data points for Z-score
|
||||
{
|
||||
var values = _buffer.GetSpan().ToArray();
|
||||
double mean = values.Average();
|
||||
double n = values.Length;
|
||||
|
||||
double sumSquaredDeviations = values.Sum(x => Math.Pow(x - mean, 2));
|
||||
double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); // Sample standard deviation
|
||||
|
||||
if (standardDeviation != 0) // Avoid division by zero
|
||||
{
|
||||
zScore = (Input.Value - mean) / standardDeviation;
|
||||
}
|
||||
}
|
||||
|
||||
IsHot = _buffer.Count >= Period;
|
||||
return zScore;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user