first iteration

This commit is contained in:
Miha Kralj
2025-11-25 20:40:46 -08:00
parent b5881b9bb4
commit 33ffd3a37a
594 changed files with 117007 additions and 80111 deletions
+196
View File
@@ -0,0 +1,196 @@
using System.Globalization;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CSV file feed for loading historical OHLCV data.
/// Loads data in constructor and streams through it with Next() or returns batches with Fetch().
/// CSV format: timestamp,open,high,low,close,volume (header required)
/// Timestamp format: YYYY-MM-DD (UTC midnight assumed)
/// </summary>
public class CsvFeed : IFeed
{
private readonly TBarSeries _data;
// Streaming state
private int _currentIndex;
private TBar _currentBar;
private bool _hasCurrentBar;
/// <summary>
/// Loads CSV file and prepares data for streaming.
/// Data is reversed to chronological order (oldest first).
/// </summary>
/// <param name="filePath">Path to CSV file</param>
public CsvFeed(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException($"CSV file not found: {filePath}", filePath);
_data = LoadFromCsv(filePath);
_currentIndex = 0;
}
/// <summary>
/// Parses CSV file into TBarSeries.
/// Expected format: timestamp,open,high,low,close,volume
/// </summary>
private static TBarSeries LoadFromCsv(string filePath)
{
var lines = File.ReadAllLines(filePath);
if (lines.Length == 0)
throw new InvalidDataException("CSV file is empty");
// Skip header, reverse to chronological order (oldest first)
var dataLines = lines.Skip(1).Reverse().ToArray();
if (dataLines.Length == 0)
throw new InvalidDataException("CSV file contains only header, no data");
var series = new TBarSeries(dataLines.Length);
for (int i = 0; i < dataLines.Length; i++)
{
var line = dataLines[i];
if (string.IsNullOrWhiteSpace(line))
continue;
var parts = line.Split(',');
if (parts.Length != 6)
throw new FormatException($"Invalid CSV format at line {i + 2}. Expected 6 columns, found {parts.Length}");
try
{
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
// Parse OHLCV values
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
series.Add(timestamp, open, high, low, close, volume, isNew: true);
}
catch (Exception ex) when (ex is FormatException or OverflowException)
{
throw new FormatException($"Failed to parse CSV line {i + 2}: {line}", ex);
}
}
return series;
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// When end of data reached, returns last bar and sets isNew=false.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
if (_data.Count == 0)
{
isNew = false;
return default;
}
if (isNew || !_hasCurrentBar)
{
// Request for new bar
if (_currentIndex >= _data.Count)
{
// End of data - return last bar and signal no more data
isNew = false;
return _currentBar;
}
_currentBar = _data[_currentIndex];
_currentIndex++;
_hasCurrentBar = true;
}
else
{
// Update current bar - CSV has no intra-bar updates, return same bar
// No change to _currentBar or _currentIndex
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
return Next(ref isNew);
}
/// <summary>
/// Returns a filtered subset of data matching the criteria.
/// Resets streaming position to start of returned data.
/// </summary>
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var result = new TBarSeries(count);
// Find starting index
int startIndex = 0;
for (int i = 0; i < _data.Count; i++)
{
if (_data[i].Time >= startTime)
{
startIndex = i;
break;
}
}
// Collect bars matching interval
long expectedTime = startTime;
int collected = 0;
for (int i = startIndex; i < _data.Count && collected < count; i++)
{
var bar = _data[i];
// Check if bar time matches expected time (within tolerance)
long timeDiff = Math.Abs(bar.Time - expectedTime);
long tolerance = interval.Ticks / 2; // Allow 50% tolerance
if (timeDiff <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
else if (bar.Time > expectedTime)
{
// Gap in data - skip forward
long gaps = (bar.Time - expectedTime) / interval.Ticks;
expectedTime += (gaps + 1) * interval.Ticks;
if (Math.Abs(bar.Time - expectedTime + interval.Ticks) <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
}
}
// Reset streaming to start of returned data
_currentIndex = startIndex;
_hasCurrentBar = false;
return result;
}
}
-76
View File
@@ -1,76 +0,0 @@
using System.Security.Cryptography;
namespace QuanTAlib;
public class GbmFeed : TBarSeries
{
private readonly double _mu, _sigma;
private readonly RandomNumberGenerator _rng;
private double _lastClose;
public GbmFeed(double initialPrice = 100.0, double mu = 0.05, double sigma = 0.2)
{
_lastClose = initialPrice;
_mu = mu;
_sigma = sigma;
_rng = RandomNumberGenerator.Create();
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);
for (int i = 0; i < count; i++)
{
Add(startTime, isNew: true);
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 open = _lastClose;
double close = open * Math.Exp(drift + diffusion);
// Generate intra-bar price movements
double maxMove = Math.Abs(close - open) * 1.5; // Allow for some extra movement within the bar
double high = Math.Max(open, close) + (maxMove * GenerateRandomDouble());
double low = Math.Min(open, close) - (maxMove * GenerateRandomDouble());
// Ensure high is always greater than or equal to both open and close
high = Math.Max(high, Math.Max(open, close));
// Ensure low is always less than or equal to both open and close
low = Math.Min(low, Math.Min(open, close));
double volume = 1000 + (GenerateRandomDouble() * 1000);
if (isNew)
{
_lastClose = close;
}
return new TBar(time, open, high, low, close, volume, isNew);
}
private double GenerateNormalRandom()
{
// Box-Muller transform to generate standard normal random variable
double u1 = 1.0 - GenerateRandomDouble(); // Uniform(0,1] random doubles
double u2 = 1.0 - GenerateRandomDouble();
return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
}
private double GenerateRandomDouble()
{
byte[] bytes = new byte[8];
_rng.GetBytes(bytes);
return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue;
}
}
+35
View File
@@ -0,0 +1,35 @@
namespace QuanTAlib;
/// <summary>
/// Interface for data feeds that provide TBar (OHLCV) data.
/// Implementations include synthetic generators (GBM), API-based feeds (AlphaVantage),
/// file readers (CSV), and real-time streams (WebSocket).
/// </summary>
public interface IFeed
{
/// <summary>
/// Gets the next bar from the feed with full bidirectional control.
/// </summary>
/// <param name="isNew">
/// Input: Request for new bar (true) or update current bar (false).
/// Output: Actual behavior - may differ if feed cannot honor request (e.g., end of data).
/// </param>
/// <returns>The bar (new or updated)</returns>
TBar Next(ref bool isNew);
/// <summary>
/// Gets the next bar from the feed with simple control.
/// </summary>
/// <param name="isNew">Request for new bar (true) or update current bar (false). Defaults to true.</param>
/// <returns>The bar (new or updated)</returns>
TBar Next(bool isNew = true);
/// <summary>
/// Gets multiple bars in batch with explicit time parameters.
/// </summary>
/// <param name="count">Number of bars to retrieve</param>
/// <param name="startTime">Starting timestamp for first bar (in ticks)</param>
/// <param name="interval">Time interval between bars</param>
/// <returns>Series containing the requested bars</returns>
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
+211
View File
@@ -0,0 +1,211 @@
using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
/// Generates realistic price data for testing indicators and strategies.
/// Stateless design - only maintains minimal state needed for price continuity.
/// </summary>
public class GBM : IFeed
{
private readonly Random _rnd = new();
private double _lastPrice;
private long _lastTime;
private readonly double _mu;
private readonly double _sigma;
private readonly double _dt;
// Precomputed GBM constants
private readonly double _drift;
private readonly double _vol;
private readonly long _defaultTimeStep;
// State for streaming bar formation (only when isNew=false)
private TBar _currentBar;
private bool _hasCurrentBar;
// Box-Muller optimization: cache second normal
private double _cachedZ;
private bool _hasCachedZ;
/// <summary>
/// Creates a new GBM generator.
/// </summary>
/// <param name="startPrice">Initial price (default: 100.0)</param>
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
public GBM(
double startPrice = 100.0,
double mu = 0.05,
double sigma = 0.2,
TimeSpan? defaultTimeframe = null)
{
_lastPrice = startPrice;
_lastTime = DateTime.UtcNow.Ticks;
_mu = mu;
_sigma = sigma;
// Use provided timeframe or default to 1 minute
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
_defaultTimeStep = timeframe.Ticks;
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
double minutesPerYear = 252.0 * 6.5 * 60.0;
_dt = timeframe.TotalMinutes / minutesPerYear;
_drift = (mu - 0.5 * sigma * sigma) * _dt;
_vol = sigma * Math.Sqrt(_dt);
}
/// <summary>
/// Generates next standard normal using Box-Muller transform with caching.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextNormal()
{
if (_hasCachedZ)
{
_hasCachedZ = false;
return _cachedZ;
}
double u1 = 1.0 - _rnd.NextDouble();
double u2 = 1.0 - _rnd.NextDouble();
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
double angle = 2.0 * Math.PI * u2;
_cachedZ = mag * Math.Sin(angle);
_hasCachedZ = true;
return mag * Math.Cos(angle);
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// GBM always honors the request - isNew parameter unchanged on return.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
// GBM always honors request - parameter unchanged
if (isNew || !_hasCurrentBar)
{
// Generate new bar
long currentTime = _lastTime + _defaultTimeStep;
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
double open = _lastPrice;
double close = price;
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
_currentBar = new TBar(currentTime, open, high, low, close, volume);
_hasCurrentBar = true;
_lastPrice = close;
_lastTime = currentTime;
}
else
{
// Update current bar (intra-bar tick)
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
var bar = _currentBar;
double newClose = price;
double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, newClose);
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
_lastPrice = newClose;
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
// Delegate to ref version
return Next(ref isNew);
}
/// <summary>
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var series = new TBarSeries(count);
// Pre-allocate arrays for SoA layout
long[] t = new long[count];
double[] o = new double[count];
double[] h = new double[count];
double[] l = new double[count];
double[] c = new double[count];
double[] v = new double[count];
// Calculate dt for this specific interval
double minutesPerYear = 252.0 * 6.5 * 60.0;
double dt = interval.TotalMinutes / minutesPerYear;
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
double vol = _sigma * Math.Sqrt(dt);
long timeStep = interval.Ticks;
double currentPrice = _lastPrice;
long currentTime = startTime;
for (int i = 0; i < count; i++)
{
double z = NextNormal();
double price = currentPrice * Math.Exp(drift + vol * z);
double open = currentPrice;
double close = price;
double rnd1 = _rnd.NextDouble();
double rnd2 = _rnd.NextDouble();
double rnd3 = _rnd.NextDouble();
t[i] = currentTime;
o[i] = open;
c[i] = close;
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
v[i] = 1000 + rnd3 * 1000;
currentPrice = price;
currentTime += timeStep;
}
// Update internal state to continue from end of batch
_lastPrice = currentPrice;
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
// Bulk add to series
series.Add(t, o, h, l, c, v);
// Reset streaming state after batch
_hasCurrentBar = false;
return series;
}
}