namespace PineScriptV6 { /// /// ta.sma / ta.highest / ta.lowest 의 증분(O(1)) 구현체를 관리합니다. /// 매 봉 최초 호출 시 전체 히스토리로 초기화되고, /// 이후 봉은 O(1) 증분 업데이트로 처리됩니다. /// Reset() 은 새 ExecuteOnCandles / WarmUp 시작 시 반드시 호출해야 합니다. /// 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(); /// 상태 전체 초기화 (재실행 또는 실시간 모드 전환 시 호출) public static void Reset() { _smaMap.Clear(); _highMap.Clear(); _lowMap.Clear(); } // ── SMA ────────────────────────────────────────────────────────── /// /// ta.sma(src, length) 의 증분 값을 반환합니다. /// 히스토리 부족 시 null 반환 (초기 warmup 구간). /// 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; } } }