Files
QuanTAlib/lib/core/tseries/tseries.cs
T

239 lines
7.9 KiB
C#

using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Tag type indicating that a TSeries constructor should share storage with the provided lists
/// rather than making defensive copies. Used internally for synchronized sub-series.
/// </summary>
public readonly struct ShareStorageTag
{
/// <summary>
/// Singleton instance for the share storage tag.
/// </summary>
public static readonly ShareStorageTag Instance = default;
}
/// <summary>
/// High-performance enumerator for TSeries.
/// </summary>
public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumerator>
{
private readonly List<long> _t;
private readonly List<double> _v;
private readonly int _count;
private int _index;
private TValue _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TSeriesEnumerator(List<long> t, List<double> v)
{
_t = t;
_v = v;
_count = v.Count;
_index = -1;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
_current = new TValue(_t[_index], _v[_index]);
return true;
}
public readonly TValue Current => _current;
readonly object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_index = -1;
_current = default;
}
public readonly void Dispose() { }
public readonly bool Equals(TSeriesEnumerator other) =>
ReferenceEquals(_t, other._t) &&
ReferenceEquals(_v, other._v) &&
_count == other._count &&
_index == other._index;
public override readonly bool Equals(object? obj) =>
obj is TSeriesEnumerator other && Equals(other);
public override readonly int GetHashCode() =>
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_v), _count, _index);
public static bool operator ==(TSeriesEnumerator left, TSeriesEnumerator right) => left.Equals(right);
public static bool operator !=(TSeriesEnumerator left, TSeriesEnumerator right) => !left.Equals(right);
}
/// <summary>
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
/// Supports "New Bar" vs "Update Last" streaming semantics.
/// </summary>
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
{
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
protected readonly List<long> _t;
protected readonly List<double> _v;
#pragma warning restore MA0016
public string Name { get; set; } = "Data";
public event TValuePublishedHandler? Pub;
public TSeries() : this(0)
{
}
public TSeries(int capacity)
{
_t = new List<long>(capacity);
_v = new List<double>(capacity);
}
public TSeries(IReadOnlyList<long> time, IReadOnlyList<double> values)
{
// Always make defensive copies to prevent external mutation of internal state
_t = [.. time];
_v = [.. values];
}
/// <summary>
/// Internal constructor for creating views that share underlying storage.
/// Used by TBarSeries to create synchronized OHLCV sub-series.
/// </summary>
/// <param name="time">The time list to share (not copied).</param>
/// <param name="values">The values list to share (not copied).</param>
/// <param name="_">Tag type indicating intentional storage sharing.</param>
internal TSeries(List<long> time, List<double> values, ShareStorageTag _)
{
// Direct assignment for intentional storage sharing (internal use only)
_t = time;
_v = values;
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count;
}
public TValue this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new TValue(_t[index], _v[index]);
}
public TValue Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
}
public double LastValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? _v[^1] : double.NaN;
}
public long LastTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _t.Count > 0 ? _t[^1] : 0;
}
/// <summary>
/// Direct access to the underlying Value array as a Span for SIMD operations.
/// </summary>
/// <remarks>
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
/// </remarks>
public ReadOnlySpan<double> Values
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_v);
}
/// <summary>
/// Direct access to the underlying Time array as a Span.
/// </summary>
/// <remarks>
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
/// </remarks>
public ReadOnlySpan<long> Times
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_t);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value, bool isNew)
{
if (isNew || _v.Count == 0)
{
_t.Add(value.Time);
_v.Add(value.Value);
}
else
{
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
// Overload for backward compatibility (assumes isNew=true)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value) => Add(value, isNew: true);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
/// <remarks>
/// <b>Synthetic timestamps:</b> Each element receives a fabricated timestamp starting at
/// <see cref="DateTime.UtcNow"/> (captured once at call time) and incrementing by one minute
/// per item. These timestamps do not reflect real market time. Use the typed overloads
/// (<see cref="Add(TValue)"/>, <see cref="Add(DateTime,double,bool)"/>) when real timestamps matter.
/// </remarks>
public void Add(IEnumerable<double> values)
{
long t = DateTime.UtcNow.Ticks;
foreach (var v in values)
{
Add(new TValue(t, v), isNew: true);
t += TimeSpan.TicksPerMinute;
}
}
// IEnumerable implementation with struct enumerator for zero-allocation iteration
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TSeriesEnumerator GetEnumerator() => new(_t, _v);
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}