mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
refactor structs to records, add new classes for financial calculations, implement EMA and SMA with circular buffer. Generate random financial data using GBM model. Also, test the SMA calculation with sample data.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
namespace QuanTAlib;
|
||||
public class GBM_Feed
|
||||
{
|
||||
private readonly double _mu;
|
||||
private readonly double _sigma;
|
||||
private readonly Random _random;
|
||||
private double _lastClose;
|
||||
private double _lastHigh;
|
||||
private double _lastLow;
|
||||
|
||||
public GBM_Feed(double initialPrice, double mu, double sigma)
|
||||
{
|
||||
_lastClose = initialPrice;
|
||||
_lastHigh = initialPrice;
|
||||
_lastLow = initialPrice;
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
_random = Random.Shared;
|
||||
}
|
||||
|
||||
public TBar Generate(bool IsNew = true)
|
||||
{
|
||||
DateTime time = DateTime.UtcNow;
|
||||
double dt = 1.0 / 252; // Assuming daily steps in a trading year of 252 days
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double diffusion = _sigma * Math.Sqrt(dt) * NormalRandom();
|
||||
double newClose = _lastClose * Math.Exp(drift + diffusion);
|
||||
|
||||
double open = _lastClose;
|
||||
double high = Math.Max(open, newClose) * (1 + _random.NextDouble() * 0.01);
|
||||
double low = Math.Min(open, newClose) * (1 - _random.NextDouble() * 0.01);
|
||||
double volume = 1000 + _random.NextDouble() * 1000; // Random volume between 1000 and 2000
|
||||
|
||||
if (!IsNew)
|
||||
{
|
||||
high = Math.Max(_lastHigh, high);
|
||||
low = Math.Min(_lastLow, low);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastClose = newClose;
|
||||
}
|
||||
|
||||
_lastHigh = high;
|
||||
_lastLow = low;
|
||||
|
||||
return new TBar(time, open, high, low, newClose, volume, IsNew);
|
||||
}
|
||||
|
||||
private double NormalRandom()
|
||||
{
|
||||
// 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,42 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class EMA
|
||||
{
|
||||
private double lastEma, lastEmaCandidate, k;
|
||||
private int period, i;
|
||||
public TValue Value { get; private set; }
|
||||
public bool IsHot { get; private set; }
|
||||
|
||||
public EMA(int period)
|
||||
{
|
||||
Init(period);
|
||||
}
|
||||
|
||||
public void Init(int period)
|
||||
{
|
||||
this.period = period;
|
||||
this.k = 2.0 / (period + 1);
|
||||
this.lastEma = this.lastEmaCandidate = double.NaN;
|
||||
this.i = 0;
|
||||
}
|
||||
public TValue Update(TValue input, bool IsNew = true)
|
||||
{
|
||||
double ema;
|
||||
|
||||
if (double.IsNaN(lastEma)) { lastEma = input.Value; }
|
||||
|
||||
if (IsNew)
|
||||
{
|
||||
lastEma = lastEmaCandidate;
|
||||
i++;
|
||||
}
|
||||
|
||||
double kk = (i < period) ? (2.0 / (i + 1)) : k;
|
||||
ema = lastEma + kk * (input.Value - lastEma);
|
||||
lastEmaCandidate = ema;
|
||||
|
||||
IsHot = i >= period;
|
||||
Value = new TValue(input.Time, ema, IsNew, IsHot);
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class SMA
|
||||
{
|
||||
private CircularBuffer buffer = null!;
|
||||
private int period;
|
||||
private double sum;
|
||||
public TValue Value { get; private set; }
|
||||
public bool IsHot { get; private set; }
|
||||
|
||||
public SMA(int period)
|
||||
{
|
||||
Init(period);
|
||||
}
|
||||
|
||||
public void Init(int period)
|
||||
{
|
||||
this.period = period;
|
||||
this.buffer = new CircularBuffer(period);
|
||||
this.sum = 0;
|
||||
this.IsHot = false;
|
||||
this.Value = default;
|
||||
}
|
||||
|
||||
public TValue Update(TValue input, bool IsNew = true)
|
||||
{
|
||||
if (buffer.Count == 0 || isNew)
|
||||
{
|
||||
if (buffer.Count == period)
|
||||
{
|
||||
sum -= buffer[0];
|
||||
}
|
||||
buffer.Add(input);
|
||||
sum += input.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
sum -= buffer[buffer.Count - 1];
|
||||
sum += input.Value;
|
||||
buffer[buffer.Count - 1] = input;
|
||||
}
|
||||
|
||||
double sma = sum / buffer.Count;
|
||||
Value = new TValue(input.Time, sma, isNew, IsHot);
|
||||
return Value;
|
||||
}
|
||||
|
||||
double sma = buffer.Count > 0 ? sum / buffer.Count : double.NaN;
|
||||
IsHot = buffer.Count >= period;
|
||||
Value = new TValue(input.Time, sma, IsNew, IsHot);
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class WMA
|
||||
{
|
||||
private CircularBuffer buffer = null!;
|
||||
private CircularBuffer weights = null!;
|
||||
private int period;
|
||||
public TValue Value { get; private set; }
|
||||
public bool IsHot { get; private set; }
|
||||
|
||||
public WMA(int period)
|
||||
{
|
||||
Init(period);
|
||||
}
|
||||
|
||||
public void Init(int period)
|
||||
{
|
||||
this.period = period;
|
||||
this.buffer = new CircularBuffer(period);
|
||||
this.weights = new CircularBuffer(period);
|
||||
CalculateWeights();
|
||||
this.IsHot = false;
|
||||
this.Value = default;
|
||||
}
|
||||
|
||||
public TValue Update(TValue input, bool IsNew = true)
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
buffer.Add(input);
|
||||
}
|
||||
else if (buffer.Count > 0)
|
||||
{
|
||||
buffer[buffer.Count - 1] = input;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Add(input);
|
||||
}
|
||||
|
||||
double wma = 0;
|
||||
double totalWeights = 0;
|
||||
|
||||
for (int i = 0; i < buffer.Count; i++)
|
||||
{
|
||||
wma += buffer[i] * weights[i];
|
||||
totalWeights += weights[i];
|
||||
}
|
||||
|
||||
wma /= totalWeights;
|
||||
|
||||
IsHot = buffer.Count >= period;
|
||||
Value = new TValue(input.Time, wma, IsNew, IsHot);
|
||||
return Value;
|
||||
}
|
||||
|
||||
private void CalculateWeights()
|
||||
{
|
||||
for (int i = 1; i <= period; i++)
|
||||
{
|
||||
weights.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class CircularBuffer
|
||||
{
|
||||
private double[] _buffer = null!;
|
||||
private int _start;
|
||||
private int _size;
|
||||
|
||||
public CircularBuffer(int capacity)
|
||||
{
|
||||
_buffer = new double[capacity];
|
||||
_start = 0;
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
public int Capacity => _buffer.Length;
|
||||
public int Count => _size;
|
||||
|
||||
public void Add(double item, bool isNew)
|
||||
{
|
||||
if (_size == 0 || isNew)
|
||||
{
|
||||
// If buffer is empty or isNew is true, add new item
|
||||
if (_size < Capacity)
|
||||
{
|
||||
_buffer[(_start + _size) % Capacity] = item;
|
||||
_size++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[_start] = item;
|
||||
_start = (_start + 1) % Capacity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If isNew is false, just update the last item
|
||||
_buffer[(_start + _size - 1) % Capacity] = item;
|
||||
}
|
||||
}
|
||||
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= _size)
|
||||
throw new IndexOutOfRangeException();
|
||||
return _buffer[(_start + index) % Capacity];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index < 0 || index >= _size)
|
||||
throw new IndexOutOfRangeException();
|
||||
_buffer[(_start + index) % Capacity] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace QuanTAlib;
|
||||
public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true)
|
||||
{
|
||||
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 TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { }
|
||||
public TBar(double open, double high, double low, double close, double volume) : this(DateTime.UtcNow, open, high, low, close, volume) { }
|
||||
public TBar((DateTime time, double open, double high, double low, double close, double volume) tuple) : this(tuple.time, tuple.open, tuple.high, tuple.low, tuple.close, tuple.volume) { }
|
||||
|
||||
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}]";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true)
|
||||
{
|
||||
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 TValue() : this(DateTime.UtcNow, 0) { }
|
||||
public TValue(double value) : this(DateTime.UtcNow, value) { }
|
||||
public TValue((DateTime time, double value) tuple) : this(tuple.time, tuple.value) { }
|
||||
|
||||
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}]";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AlgoType>Indicator</AlgoType>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<QuantowerPath>D:\Quantower\TradingPlatform</QuantowerPath>
|
||||
<QuantowerVersion Condition="'$(QuantowerVersion)' == ''">v1.140.8</QuantowerVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="TradingPlatform.BusinessLayer">
|
||||
<HintPath>$(QuantowerPath)\$(QuantowerVersion)\bin\TradingPlatform.BusinessLayer.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(QuantowerPath)\$(QuantowerVersion)\bin\TradingPlatform.BusinessLayer.xml">
|
||||
<Link>TradingPlatform.BusinessLayer.xml</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
#r ".\bin\Debug\calculations.dll"
|
||||
using QuanTAlib;
|
||||
|
||||
#!csharp
|
||||
|
||||
public class CircularBuffer
|
||||
{
|
||||
private double[] _buffer;
|
||||
private int _start;
|
||||
private int _size;
|
||||
|
||||
public CircularBuffer(int capacity)
|
||||
{
|
||||
_buffer = new double[capacity];
|
||||
_start = 0;
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
public int Capacity => _buffer.Length;
|
||||
public int Count => _size;
|
||||
|
||||
public void Add(double item, bool isNew)
|
||||
{
|
||||
if (!isNew)
|
||||
{
|
||||
// Add new item
|
||||
if (_size < Capacity)
|
||||
{
|
||||
_buffer[(_start + _size) % Capacity] = item;
|
||||
_size++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_start = (_start + 1) % Capacity;
|
||||
_buffer[(_start + _size - 1) % Capacity] = item;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the last item
|
||||
if (_size > 0)
|
||||
{
|
||||
_buffer[(_start + _size - 1) % Capacity] = item;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If buffer is empty, add the item even if isNew is true
|
||||
_buffer[0] = item;
|
||||
_size = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= _size)
|
||||
throw new IndexOutOfRangeException();
|
||||
return _buffer[(_start + index) % Capacity];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index < 0 || index >= _size)
|
||||
throw new IndexOutOfRangeException();
|
||||
_buffer[(_start + index) % Capacity] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
public class SMA1
|
||||
{
|
||||
private CircularBuffer buffer;
|
||||
private int period;
|
||||
private double sum;
|
||||
public TValue Value { get; private set; }
|
||||
public bool IsHot => buffer.Count >= period;
|
||||
|
||||
public SMA1(int period)
|
||||
{
|
||||
Init(period);
|
||||
}
|
||||
|
||||
public void Init(int period)
|
||||
{
|
||||
this.period = period;
|
||||
this.buffer = new CircularBuffer(period);
|
||||
this.sum = 0;
|
||||
this.Value = default;
|
||||
}
|
||||
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (!isNew)
|
||||
{
|
||||
if (buffer.Count == period)
|
||||
{
|
||||
sum -= buffer[0];
|
||||
}
|
||||
sum += input.Value;
|
||||
buffer.Add(input.Value, isNew);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.Count > 0)
|
||||
{
|
||||
sum -= buffer[buffer.Count - 1];
|
||||
sum += input.Value;
|
||||
buffer.Add(input.Value, isNew);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If buffer is empty, add the item even if isNew is true
|
||||
sum += input.Value;
|
||||
buffer.Add(input.Value, false);
|
||||
}
|
||||
}
|
||||
|
||||
double sma = buffer.Count > 0 ? sum / buffer.Count : double.NaN;
|
||||
Value = new TValue(input.Time, sma, isNew, IsHot);
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
GBM_Feed feed = new(initialPrice: 100, mu: 0.1, sigma: 0.9);
|
||||
int i=10;
|
||||
SMA1 ma = new(i);
|
||||
Console.WriteLine($"{"Close",10} {"MA(" + i + ")",10}");
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
TValue c =(double)feed.Generate().Close;
|
||||
ma.Update(1000,false);
|
||||
ma.Update(-10000,false);
|
||||
|
||||
ma.Update(c,true);
|
||||
|
||||
Console.WriteLine($"{i+1} {(double)c,10:F2} {(double)ma.Value,10:F2}");
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
public class Emitter {
|
||||
private Random random = new Random();
|
||||
public event EventHandler<EventArg<TValue>> Pub;
|
||||
public void Emit() {
|
||||
DateTime now = DateTime.Now;
|
||||
double randomValue = random.NextDouble() * 100; // Generates a random number between 0 and 100
|
||||
TValue value = new TValue(now, randomValue);
|
||||
|
||||
EventArg<TValue> eventArg = new EventArg<TValue>(value, true, true);
|
||||
OnValuePub(eventArg);
|
||||
}
|
||||
protected virtual void OnValuePub(EventArg<TValue> eventArg) {
|
||||
Pub?.Invoke(this, eventArg);
|
||||
}
|
||||
}
|
||||
|
||||
public class BarEmitter
|
||||
{
|
||||
private Random random = new Random();
|
||||
public event EventHandler<EventArg<TBar>> Pub;
|
||||
private double lastClose = 100.0; // Starting price
|
||||
|
||||
public void Emit()
|
||||
{
|
||||
double open = lastClose;
|
||||
double close = open * (1 + (random.NextDouble() - 0.5) * 0.02); // +/- 1% change
|
||||
double high = Math.Max(open, close) * (1 + random.NextDouble() * 0.005); // Up to 0.5% higher
|
||||
double low = Math.Min(open, close) * (1 - random.NextDouble() * 0.005); // Up to 0.5% lower
|
||||
double volume = random.NextDouble() * 1000000; // Random volume between 0 and 1,000,000
|
||||
|
||||
TBar bar = new TBar(DateTime.Now, open, high, low, close, volume);
|
||||
lastClose = close;
|
||||
|
||||
EventArg<TBar> eventArg = new EventArg<TBar>(bar, true, true);
|
||||
OnBarPub(eventArg);
|
||||
}
|
||||
|
||||
protected virtual void OnBarPub(EventArg<TBar> eventArg)
|
||||
{
|
||||
Pub?.Invoke(this, eventArg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Listener
|
||||
{
|
||||
public void Sub(object sender, EventArgs e)
|
||||
{
|
||||
if (e is EventArg<TValue> tValueArg) {
|
||||
Console.WriteLine($"TValue: {tValueArg.Data.Value:F2}");
|
||||
} else if (e is EventArg<TBar> tBarArg) {
|
||||
Console.WriteLine($"TBar: o={tBarArg.Data.Open:F2}, v={tBarArg.Data.Volume:F2}");
|
||||
} else {
|
||||
Console.WriteLine($"Unknown type: {e.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
Emitter em1 = new();
|
||||
BarEmitter em2 = new();
|
||||
Listener list = new();
|
||||
|
||||
em1.Pub += list.Sub;
|
||||
em2.Pub += list.Sub;
|
||||
|
||||
// Emit 5 random values
|
||||
for (int i = 0; i < 3; i++) {
|
||||
em1.Emit();
|
||||
em2.Emit();
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
public abstract class Indicator {
|
||||
protected Indicator() {
|
||||
Init(); }
|
||||
public virtual void Init() {}
|
||||
public virtual TValue Calc(TValue input, bool isNew=true, bool isHot=true) {
|
||||
return new TValue();
|
||||
}
|
||||
}
|
||||
|
||||
public class EMA : Indicator
|
||||
{
|
||||
private double lastEma, lastEmaCandidate, k;
|
||||
private int period, i;
|
||||
|
||||
public EMA(int period) {
|
||||
Init(period);
|
||||
}
|
||||
|
||||
public void Init(int period)
|
||||
{
|
||||
this.period = period;
|
||||
this.k = 2.0 / (period + 1);
|
||||
this.lastEma = this.lastEmaCandidate = double.NaN;
|
||||
this.i = 0;
|
||||
}
|
||||
|
||||
public override TValue Calc(TValue input, bool isNew = true, bool isHot = true) {
|
||||
double ema;
|
||||
|
||||
if (double.IsNaN(lastEma)) { lastEma = lastEmaCandidate = input.Value; }
|
||||
|
||||
if (isNew) {
|
||||
lastEma = lastEmaCandidate;
|
||||
i++;
|
||||
}
|
||||
|
||||
double kk = (i>=period)?k:(2.0/(i+1));
|
||||
ema = lastEma + kk * (input.Value - lastEma);
|
||||
lastEmaCandidate = ema;
|
||||
|
||||
return new TValue(input.Timestamp, ema);
|
||||
}
|
||||
}
|
||||
|
||||
#!csharp
|
||||
|
||||
EMA ema = new(3);
|
||||
display(ema.Calc(100));
|
||||
display(ema.Calc(0,false));
|
||||
display(ema.Calc(100,false));
|
||||
display(ema.Calc(0));
|
||||
Reference in New Issue
Block a user