Files
PineSript/PineScript/IndicatorEngine.cs

217 lines
9.5 KiB
C#
Raw Permalink Normal View History

2026-04-25 17:32:49 +09:00
namespace PineScriptV6
{
/// <summary>
/// ta.sma / ta.highest / ta.lowest 의 증분(O(1)) 구현체를 관리합니다.
/// 매 봉 최초 호출 시 전체 히스토리로 초기화되고,
/// 이후 봉은 O(1) 증분 업데이트로 처리됩니다.
/// Reset() 은 새 ExecuteOnCandles / WarmUp 시작 시 반드시 호출해야 합니다.
/// </summary>
public static class IndicatorEngine
{
// ValueTuple 키 — 문자열 포맷 할당($"{src}:{len}") 완전 제거
private static readonly Dictionary<(string, int), IncrementalSma> _smaMap = new();
private static readonly Dictionary<(string, int), IncrementalHighest> _highMap = new();
private static readonly Dictionary<(string, int), IncrementalLowest> _lowMap = new();
/// <summary>상태 전체 초기화 (재실행 또는 실시간 모드 전환 시 호출)</summary>
public static void Reset()
{
_smaMap.Clear();
_highMap.Clear();
_lowMap.Clear();
}
// ── SMA ──────────────────────────────────────────────────────────
/// <summary>
/// ta.sma(src, length) 의 증분 값을 반환합니다.
/// 히스토리 부족 시 null 반환 (초기 warmup 구간).
/// </summary>
public static double? QuerySma(string src, int length)
{
var key = (src, length);
if (!_smaMap.TryGetValue(key, out var ind))
_smaMap[key] = ind = new IncrementalSma(src, length);
return ind.GetValue();
}
// ── HIGHEST ──────────────────────────────────────────────────────
public static double? QueryHighest(string src, int length)
{
var key = (src, length);
if (!_highMap.TryGetValue(key, out var ind))
_highMap[key] = ind = new IncrementalHighest(src, length);
return ind.GetValue();
}
// ── LOWEST ───────────────────────────────────────────────────────
public static double? QueryLowest(string src, int length)
{
var key = (src, length);
if (!_lowMap.TryGetValue(key, out var ind))
_lowMap[key] = ind = new IncrementalLowest(src, length);
return ind.GetValue();
}
}
// ════════════════════════════════════════════════════════════════
// 증분 SMA — 누적합 O(1)
// ════════════════════════════════════════════════════════════════
internal sealed class IncrementalSma
{
private readonly string _src;
private readonly int _len;
private double _sum;
private int _lastBar = -1;
private bool _ready;
public IncrementalSma(string src, int len) { _src = src; _len = len; }
public double? GetValue()
{
int bar = PineContext.CurrentBarIndex;
if (bar == _lastBar) return _ready ? _sum / _len : null;
if (!_ready)
{
// 최초: 전체 윈도우 합산으로 초기화
double[] hist = PineContext.GetLast(_src, _len, 0);
if (hist.Length < _len) { _lastBar = bar; return null; }
_sum = 0;
for (int i = 0; i < hist.Length; i++) _sum += hist[i];
_ready = true;
}
else
{
// 증분: 새 값 추가, 만료값 차감 — CircularBuffer 직접 접근 (double[] 할당 0)
var buf = PineContext.GetBuffer(_src);
if (buf == null || buf.Count <= _len) { _lastBar = bar; return null; }
double cur = buf.Get(0); // 현재 봉
double old = buf.Get(_len); // len봉 전 (만료)
if (double.IsNaN(cur) || double.IsNaN(old)) { _lastBar = bar; return null; }
_sum += cur - old;
}
_lastBar = bar;
return _sum / _len;
}
}
// ════════════════════════════════════════════════════════════════
// 증분 최고값 — 단조 덱(Monotonic Deque) O(1) amortized
// ════════════════════════════════════════════════════════════════
internal sealed class IncrementalHighest
{
private readonly string _src;
private readonly int _len;
// 덱: 앞=최대값, 뒤=최소. 원소=(봉인덱스, 값)
private readonly LinkedList<(int Bar, double Val)> _dq = new();
private int _lastBar = -1;
private double? _value;
public IncrementalHighest(string src, int len) { _src = src; _len = len; }
public double? GetValue()
{
int bar = PineContext.CurrentBarIndex;
if (bar == _lastBar) return _value;
if (_lastBar < 0)
{
// 최초: 전체 윈도우로 덱 초기화 (고령→최신 순 처리)
double[] hist = PineContext.GetLast(_src, _len, 0);
if (hist.Length < _len) { _lastBar = bar; _value = null; return null; }
_dq.Clear();
for (int i = _len - 1; i >= 0; i--) // hist[_len-1]=가장 오래된 봉
{
int b = bar - i;
double v = hist[i];
while (_dq.Count > 0 && _dq.Last!.Value.Val <= v)
_dq.RemoveLast();
_dq.AddLast((b, v));
}
}
else
{
// 증분: 현재 봉 1개 추가 — CircularBuffer 직접 접근 (double[] 할당 0)
var buf = PineContext.GetBuffer(_src);
if (buf == null || buf.Count == 0) { _lastBar = bar; _value = null; return null; }
double newVal = buf.Get(0);
if (double.IsNaN(newVal)) { _lastBar = bar; _value = null; return null; }
// 만료 원소 앞에서 제거 (bar - len 이하)
while (_dq.Count > 0 && _dq.First!.Value.Bar <= bar - _len)
_dq.RemoveFirst();
// 새 값보다 작거나 같은 원소 뒤에서 제거
while (_dq.Count > 0 && _dq.Last!.Value.Val <= newVal)
_dq.RemoveLast();
_dq.AddLast((bar, newVal));
}
_value = _dq.Count > 0 ? _dq.First!.Value.Val : null;
_lastBar = bar;
return _value;
}
}
// ════════════════════════════════════════════════════════════════
// 증분 최저값 — 단조 덱 O(1) amortized
// ════════════════════════════════════════════════════════════════
internal sealed class IncrementalLowest
{
private readonly string _src;
private readonly int _len;
private readonly LinkedList<(int Bar, double Val)> _dq = new();
private int _lastBar = -1;
private double? _value;
public IncrementalLowest(string src, int len) { _src = src; _len = len; }
public double? GetValue()
{
int bar = PineContext.CurrentBarIndex;
if (bar == _lastBar) return _value;
if (_lastBar < 0)
{
double[] hist = PineContext.GetLast(_src, _len, 0);
if (hist.Length < _len) { _lastBar = bar; _value = null; return null; }
_dq.Clear();
for (int i = _len - 1; i >= 0; i--)
{
int b = bar - i;
double v = hist[i];
while (_dq.Count > 0 && _dq.Last!.Value.Val >= v)
_dq.RemoveLast();
_dq.AddLast((b, v));
}
}
else
{
// 증분: 현재 봉 1개 추가 — CircularBuffer 직접 접근 (double[] 할당 0)
var buf = PineContext.GetBuffer(_src);
if (buf == null || buf.Count == 0) { _lastBar = bar; _value = null; return null; }
double newVal = buf.Get(0);
if (double.IsNaN(newVal)) { _lastBar = bar; _value = null; return null; }
while (_dq.Count > 0 && _dq.First!.Value.Bar <= bar - _len)
_dq.RemoveFirst();
while (_dq.Count > 0 && _dq.Last!.Value.Val >= newVal)
_dq.RemoveLast();
_dq.AddLast((bar, newVal));
}
_value = _dq.Count > 0 ? _dq.First!.Value.Val : null;
_lastBar = bar;
return _value;
}
}
}