using Serilog; namespace PineScriptV6 { public static class Pine { // ════════════════════════════════════════════════════════════════ #region 내부 헬퍼 // ════════════════════════════════════════════════════════════════ private static double ToDouble(dynamic? v) { try { return v == null ? 0.0 : Convert.ToDouble(v); } catch { return 0.0; } } /// /// "close[1]" → ("close", 1), "high" → ("high", 0) /// private static (string name, int offset) ParseSeriesRef(string source) { int b = source.IndexOf('['); if (b < 0) return (source.Trim(), 0); string name = source.Substring(0, b).Trim(); string offStr = source.Substring(b + 1).TrimEnd(']').Trim(); return (name, int.TryParse(offStr, out int o) ? o : 0); } /// 동적 값을 double로 변환 (PineContext 변수명도 해석) private static double ResolveDouble(dynamic? v) { if (v is double d) return d; if (v is int i) return i; if (v is float f) return f; if (v is decimal m) return (double)m; string s = v?.ToString() ?? ""; if (double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double parsed)) return parsed; double? ctx = PineContext.GetVariable(s); return ctx ?? double.NaN; } /// EMA 계산 (src: 오름차순 배열, src[0]=가장 오래된 값) private static double CalcEmaFromChron(double[] src, int length) { if (src.Length == 0) return double.NaN; double k = 2.0 / (length + 1); double ema = src[0]; for (int i = 1; i < src.Length; i++) ema = src[i] * k + ema * (1 - k); return ema; } #endregion // ════════════════════════════════════════════════════════════════ #region 내부 상태 (barssince / valuewhen 호출 인덱스) // ════════════════════════════════════════════════════════════════ internal static int _barssinceIdx = 0; internal static int _valuewhenIdx = 0; /// 매 봉 시작 전 호출 — 상태 함수의 호출 인덱스를 초기화합니다. internal static void ResetBarState() { _barssinceIdx = 0; _valuewhenIdx = 0; } #endregion // ════════════════════════════════════════════════════════════════ #region OHLCV 전역 변수 (히스토리 Push 포함) // ════════════════════════════════════════════════════════════════ private static double _open, _high, _low, _close, _volume; private static long _time; public static dynamic open { get => _open; set { _open = ToDouble(value); PineContext.Push("open", _open); } } public static dynamic high { get => _high; set { _high = ToDouble(value); PineContext.Push("high", _high); } } public static dynamic low { get => _low; set { _low = ToDouble(value); PineContext.Push("low", _low); } } public static dynamic close { get => _close; set { _close = ToDouble(value); PineContext.Push("close", _close); } } public static dynamic volume { get => _volume; set { _volume = ToDouble(value); PineContext.Push("volume", _volume); } } public static dynamic time { get => _time; set { _time = Convert.ToInt64(value); } } // ── 파생 시간 변수 ─────────────────────────────────────────────── private static DateTime UtcBar => _time > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(_time).UtcDateTime : DateTime.UtcNow; /// 현재 봉 시각의 시 (0-23, UTC) public static int hour => UtcBar.Hour; /// 현재 봉 시각의 분 (0-59) public static int minute => UtcBar.Minute; /// 현재 봉 시각의 초 (0-59) public static int second => UtcBar.Second; /// 요일 — Pine Script 기준 (1=일요일, 2=월요일, ..., 7=토요일) public static int dayofweek => (int)UtcBar.DayOfWeek + 1; /// 월의 일 (1-31) public static int dayofmonth => UtcBar.Day; /// 월 (1-12) public static int month => UtcBar.Month; /// 연도 public static int year => UtcBar.Year; /// 현재 Unix 시각 (밀리초) public static long timenow => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); /// 특정 날짜/시각 → Unix 밀리초 타임스탬프 (UTC 기준) public static long timestamp(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) => new DateTimeOffset(year, month, day, hour, minute, second, TimeSpan.Zero).ToUnixTimeMilliseconds(); #endregion // ════════════════════════════════════════════════════════════════ #region Global Functions // ════════════════════════════════════════════════════════════════ public static void indicator(string title = "", string shorttitle = "", bool overlay = false, string timeframe = "", dynamic? timeframe_gaps = null, string format = "", int precision = 0, dynamic? scale = null) { Log.Debug("[indicator] title={title}, overlay={overlay}", title, overlay); } public static dynamic plot(dynamic series, string title = "", dynamic? color = null, int linewidth = 1, dynamic? style = null, dynamic? trackprice = null, dynamic? offset = null, dynamic? display = null) { Log.Debug("[plot] title={title}", title); return null!; } public static dynamic plotshape(dynamic series, string title = "", dynamic? location = null, dynamic? color = null, dynamic? style = null, string text = "", dynamic? size = null, dynamic? textcolor = null, dynamic? offset = null, dynamic? display = null) { Log.Debug("[plotshape] title={title}, text={text}", title, text); bool isActive; if (series is bool bv) isActive = bv; else if (series is string sv) { var resolved = PineScriptV6.PineStateVars.Get(sv); isActive = PineScriptV6.PineEvaluator.ToBool(resolved); } else { try { isActive = Convert.ToBoolean((object?)series); } catch { isActive = false; } } if (isActive && TradingSignal.IsSignal(text)) { decimal price = 0m; try { price = (decimal)_close; } catch { } DateTimeOffset barTime = _time > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(_time) : DateTimeOffset.Now; TradingSignal.DetectAndRaise(text, price, barTime); } return null!; } public static dynamic plotchar(dynamic series, string title = "", string charact = "", dynamic? color = null, dynamic? location = null, dynamic? size = null) { Log.Debug("[plotchar] title={title}", title); return null!; } public static void bgcolor(dynamic color, double transp = 90, string title = "", dynamic? display = null) { Log.Debug("[bgcolor] title={title}", title); } public static void fill(dynamic plot1, dynamic plot2, dynamic? color = null, string title = "") { Log.Debug("[fill] title={title}", title); } public static void alertcondition(dynamic condition, string title = "", string message = "") { Log.Debug("[alertcondition] title={title}", title); } // ── input (전역 축약형) ── public static int input_int(int defval = 0, string title = "", int minval = 0, int maxval = 0, string tooltip = "") { Log.Debug("[input_int] title={title}, defval={defval}", title, defval); return defval; } public static double input_float(double defval = 0, string title = "", string tooltip = "") { Log.Debug("[input_float] title={title}, defval={defval}", title, defval); return defval; } public static bool input_bool(bool defval = false, string title = "", string tooltip = "") { Log.Debug("[input_bool] title={title}", title); return defval; } public static string input_string(string defval = "", string title = "", string tooltip = "") { Log.Debug("[input_string] title={title}", title); return defval; } public static dynamic input_timeframe(string defval = "", string title = "", string tooltip = "") { Log.Debug("[input_timeframe] title={title}", title); return defval!; } #endregion // ════════════════════════════════════════════════════════════════ #region input (중첩 클래스) // ════════════════════════════════════════════════════════════════ public static class input { public static int @int(int defval = 0, string title = "", int minval = int.MinValue, int maxval = int.MaxValue, string tooltip = "", dynamic? options = null) { Log.Debug("[input.int] title={title}, defval={defval}", title, defval); return defval; } public static double @float(double defval = 0, string title = "", string tooltip = "") { Log.Debug("[input.float] title={title}, defval={defval}", title, defval); return defval; } public static bool @bool(bool defval = false, string title = "", string tooltip = "") { Log.Debug("[input.bool] title={title}", title); return defval; } public static string @string(string defval = "", string title = "", string tooltip = "") { Log.Debug("[input.string] title={title}", title); return defval; } public static string timeframe(string defval = "", string title = "", string tooltip = "") { Log.Debug("[input.timeframe] title={title}", title); return defval; } } #endregion // ════════════════════════════════════════════════════════════════ #region label (중첩 클래스) // ════════════════════════════════════════════════════════════════ public static class label { public static dynamic style_label_up { get; } = "label_up"; public static dynamic style_label_down { get; } = "label_down"; public static dynamic style_label_left { get; } = "label_left"; public static dynamic style_label_right { get; } = "label_right"; public static dynamic style_label_center { get; } = "label_center"; public static dynamic style_none { get; } = "none"; public static dynamic style_triangleup { get; } = "triangleup"; public static dynamic style_triangledown { get; } = "triangledown"; public static dynamic style_arrowup { get; } = "arrowup"; public static dynamic style_arrowdown { get; } = "arrowdown"; public static dynamic style_cross { get; } = "cross"; public static dynamic style_xcross { get; } = "xcross"; public static dynamic style_circle { get; } = "circle"; public static dynamic style_diamond { get; } = "diamond"; public static dynamic style_square { get; } = "square"; public static dynamic style_flag { get; } = "flag"; public static dynamic @new(dynamic bar_index, dynamic y, string text = "", dynamic? color = null, dynamic? style = null, dynamic? size = null, dynamic? textcolor = null, string xloc = "", string yloc = "", dynamic? tooltip = null) { Log.Debug("[label.new] text={text}", text); return null!; } public static void delete(dynamic id) { } public static void set_text(dynamic id, string text) { } public static void set_color(dynamic id, dynamic color) { } } #endregion // ════════════════════════════════════════════════════════════════ #region ta (Technical Analysis) — 실제 계산 구현 // ════════════════════════════════════════════════════════════════ public static class ta { // ── SMA ────────────────────────────────────────────────────── public static dynamic sma(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); if (offset == 0) { double? incr = IndicatorEngine.QuerySma(name, length); if (incr.HasValue) { Log.Debug("[ta.sma] {Name}({Length}) = {Result:F6} [incr]", name, length, incr.Value); return incr.Value; } Log.Debug("[ta.sma] 데이터 부족(warmup): {Name}({Length})", name, length); return null!; } double[] hist = PineContext.GetLast(name, length, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.sma] 데이터 부족: {Name}[{Offset}] {Have}/{Need}", name, offset, hist.Length, length); return null!; } double result = hist.Average(); Log.Debug("[ta.sma] {Name}[{Offset}]({Length}) = {Result:F6}", name, offset, length, result); return result; } // ── EMA ────────────────────────────────────────────────────── public static dynamic ema(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length * 3, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.ema] 데이터 부족: {Name} {Have}/{Need}", name, hist.Length, length); return null!; } double k = 2.0 / (length + 1); double ema = hist[hist.Length - 1]; for (int i = hist.Length - 2; i >= 0; i--) ema = hist[i] * k + ema * (1 - k); Log.Debug("[ta.ema] {Name}({Length}) = {Result:F6}", name, length, ema); return ema; } // ── RMA (Wilder's Smoothing) ────────────────────────────────── public static dynamic rma(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length * 3, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.rma] 데이터 부족: {Name}", name); return null!; } double alpha = 1.0 / length; double rma = hist[hist.Length - 1]; for (int i = hist.Length - 2; i >= 0; i--) rma = alpha * hist[i] + (1 - alpha) * rma; Log.Debug("[ta.rma] {Name}({Length}) = {Result:F6}", name, length, rma); return rma; } // ── WMA ────────────────────────────────────────────────────── public static dynamic wma(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.wma] 데이터 부족: {Name}", name); return null!; } double wsum = 0, denom = 0; for (int i = 0; i < length; i++) { double w = length - i; wsum += hist[i] * w; denom += w; } double result = wsum / denom; Log.Debug("[ta.wma] {Name}({Length}) = {Result:F6}", name, length, result); return result; } // ── HIGHEST ────────────────────────────────────────────────── public static dynamic highest(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); if (offset == 0) { double? incr = IndicatorEngine.QueryHighest(name, length); if (incr.HasValue) { Log.Debug("[ta.highest] {Name}({Length}) = {Result:F6} [incr]", name, length, incr.Value); return incr.Value; } Log.Debug("[ta.highest] 데이터 부족(warmup): {Name}({Length})", name, length); return null!; } double[] hist = PineContext.GetLast(name, length, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.highest] 데이터 부족: {Name} {Have}/{Need}", name, hist.Length, length); return null!; } double result = hist.Max(); Log.Debug("[ta.highest] {Name}[{Offset}]({Length}) = {Result:F6}", name, offset, length, result); return result; } // ── LOWEST ─────────────────────────────────────────────────── public static dynamic lowest(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); if (offset == 0) { double? incr = IndicatorEngine.QueryLowest(name, length); if (incr.HasValue) { Log.Debug("[ta.lowest] {Name}({Length}) = {Result:F6} [incr]", name, length, incr.Value); return incr.Value; } Log.Debug("[ta.lowest] 데이터 부족(warmup): {Name}({Length})", name, length); return null!; } double[] hist = PineContext.GetLast(name, length, offset); if (length <= 0 || hist.Length < length) { Log.Debug("[ta.lowest] 데이터 부족: {Name} {Have}/{Need}", name, hist.Length, length); return null!; } double result = hist.Min(); Log.Debug("[ta.lowest] {Name}[{Offset}]({Length}) = {Result:F6}", name, offset, length, result); return result; } // ── RSI ─────────────────────────────────────────────────────── public static dynamic rsi(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length + 1, offset); if (length <= 0 || hist.Length < length + 1) return null!; double avgGain = 0, avgLoss = 0; for (int i = 0; i < length; i++) { double diff = hist[i] - hist[i + 1]; if (diff > 0) avgGain += diff; else avgLoss += Math.Abs(diff); } avgGain /= length; avgLoss /= length; if (avgLoss == 0) return 100.0; double rs = avgGain / avgLoss; double result = 100 - (100 / (1 + rs)); Log.Debug("[ta.rsi] {Name}({Length}) = {Result:F2}", name, length, result); return result; } // ── ATR ─────────────────────────────────────────────────────── public static dynamic atr(int length) { double[] highs = PineContext.GetLast("high", length + 1); double[] lows = PineContext.GetLast("low", length + 1); double[] closes = PineContext.GetLast("close", length + 1); if (length <= 0 || highs.Length < length + 1) return null!; double sum = 0; for (int i = 0; i < length; i++) { double tr = Math.Max(highs[i] - lows[i], Math.Max(Math.Abs(highs[i] - closes[i + 1]), Math.Abs(lows[i] - closes[i + 1]))); sum += tr; } double result = sum / length; Log.Debug("[ta.atr] length={Length} = {Result:F6}", length, result); return result; } // ── CHANGE ──────────────────────────────────────────────────── public static dynamic change(dynamic source, int length = 1) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length + 1, offset); if (length <= 0 || hist.Length < length + 1) return null!; double result = hist[0] - hist[length]; Log.Debug("[ta.change] {Name} = {Result:F6}", name, result); return result; } // ── CROSSOVER / CROSSUNDER ──────────────────────────────────── public static bool crossover(dynamic source1, dynamic source2) { var (n1, o1) = ParseSeriesRef(((object?)source1)?.ToString() ?? ""); var (n2, o2) = ParseSeriesRef(((object?)source2)?.ToString() ?? ""); double[] h1 = PineContext.GetLast(n1, 2, o1); double[] h2 = PineContext.GetLast(n2, 2, o2); if (h1.Length < 2 || h2.Length < 2) return false; bool result = h1[1] <= h2[1] && h1[0] > h2[0]; Log.Debug("[ta.crossover] {N1}/{N2} = {Result}", n1, n2, result); return result; } public static bool crossunder(dynamic source1, dynamic source2) { var (n1, o1) = ParseSeriesRef(((object?)source1)?.ToString() ?? ""); var (n2, o2) = ParseSeriesRef(((object?)source2)?.ToString() ?? ""); double[] h1 = PineContext.GetLast(n1, 2, o1); double[] h2 = PineContext.GetLast(n2, 2, o2); if (h1.Length < 2 || h2.Length < 2) return false; bool result = h1[1] >= h2[1] && h1[0] < h2[0]; Log.Debug("[ta.crossunder] {N1}/{N2} = {Result}", n1, n2, result); return result; } // ── MACD ───────────────────────────────────────────────────── /// /// MACD 계산. /// 반환값: double[3] { macdLine, signalLine, histogram } /// Pine Script: [macd, signal, hist] = ta.macd(close, 12, 26, 9) /// public static dynamic macd(dynamic source, int fastlen, int slowlen, int siglen) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); int needed = Math.Max(fastlen, slowlen) * 3 + siglen; double[] hist = PineContext.GetLast(name, needed, offset); if (hist.Length < slowlen + siglen) return null!; // hist[0]=최신 → 역순 정렬(오름차순) 후 EMA 계산 double[] src = hist.Reverse().ToArray(); double kFast = 2.0 / (fastlen + 1); double kSlow = 2.0 / (slowlen + 1); double kSig = 2.0 / (siglen + 1); double fastEma = src[0], slowEma = src[0]; var macdSeries = new List(); for (int i = 1; i < src.Length; i++) { fastEma = src[i] * kFast + fastEma * (1 - kFast); slowEma = src[i] * kSlow + slowEma * (1 - kSlow); macdSeries.Add(fastEma - slowEma); } if (macdSeries.Count < siglen) return null!; double sigEma = macdSeries[0]; for (int i = 1; i < macdSeries.Count; i++) sigEma = macdSeries[i] * kSig + sigEma * (1 - kSig); double macdLine = macdSeries[^1]; double signalLine = sigEma; double histogram = macdLine - signalLine; Log.Debug("[ta.macd] macd={M:F6} signal={S:F6} hist={H:F6}", macdLine, signalLine, histogram); return new double[] { macdLine, signalLine, histogram }; } // ── BB (Bollinger Bands) ────────────────────────────────────── /// /// 볼린저밴드. /// 반환값: double[3] { upper, basis, lower } /// public static dynamic bb(dynamic source, int length, double mult = 2.0) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (hist.Length < length) return null!; double basis = hist.Average(); double stddev = Math.Sqrt(hist.Select(x => (x - basis) * (x - basis)).Average()); double upper = basis + mult * stddev; double lower = basis - mult * stddev; Log.Debug("[ta.bb] upper={U:F6} basis={B:F6} lower={L:F6}", upper, basis, lower); return new double[] { upper, basis, lower }; } // ── STOCH ───────────────────────────────────────────────────── public static dynamic stoch(dynamic source, dynamic high, dynamic low, int length) { var (srcName, srcOff) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); var (hiName, hiOff) = ParseSeriesRef(((object?)high) ?.ToString() ?? ""); var (loName, loOff) = ParseSeriesRef(((object?)low) ?.ToString() ?? ""); double[] srcH = PineContext.GetLast(srcName, 1, srcOff); double[] hiH = PineContext.GetLast(hiName, length, hiOff); double[] loH = PineContext.GetLast(loName, length, loOff); if (srcH.Length < 1 || hiH.Length < length || loH.Length < length) return null!; double cur = srcH[0]; double highest = hiH.Max(); double lowest = loH.Min(); double range = highest - lowest; if (range == 0) return 0.0; double result = 100.0 * (cur - lowest) / range; Log.Debug("[ta.stoch] = {Result:F2}", result); return result; } // ── CCI ────────────────────────────────────────────────────── public static dynamic cci(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (hist.Length < length) return null!; double mean = hist.Average(); double meanDev = hist.Select(x => Math.Abs(x - mean)).Average(); if (meanDev == 0) return null!; double result = (hist[0] - mean) / (0.015 * meanDev); Log.Debug("[ta.cci] {Name}({Length}) = {Result:F2}", name, length, result); return result; } // ── BARSSINCE ──────────────────────────────────────────────── /// /// 조건이 마지막으로 true였던 봉 수. /// 각 호출 위치에 고유 인덱스를 부여하여 상태를 추적합니다. /// (매 봉 시작 전 Pine.ResetBarState() 호출 필요) /// public static dynamic barssince(dynamic condition) { int callIdx = _barssinceIdx++; string seriesKey = $"__bs_{callIdx}__"; bool isTrue = false; if (condition is bool b) isTrue = b; else if (condition != null) try { isTrue = Convert.ToBoolean((object)condition); } catch { } PineContext.Push(seriesKey, isTrue ? 1.0 : 0.0); var buf = PineContext.GetBuffer(seriesKey); if (buf == null) return null!; for (int i = 0; i < buf.Count; i++) if (buf.Get(i) != 0.0) return i; return null!; // 아직 한 번도 true가 없었음 } // ── VALUEWHEN ──────────────────────────────────────────────── /// /// 조건이 true였을 때의 source 값 (occurrence번째). /// 0 = 가장 최근 true, 1 = 그 전 true, ... /// public static dynamic valuewhen(dynamic condition, dynamic source, int occurrence = 0) { int callIdx = _valuewhenIdx++; string condKey = $"__vw_{callIdx}__"; bool isTrue = false; if (condition is bool b) isTrue = b; else if (condition != null) try { isTrue = Convert.ToBoolean((object)condition); } catch { } PineContext.Push(condKey, isTrue ? 1.0 : 0.0); var (srcName, srcOff) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); int need = PineContext.CurrentBarIndex + 1; double[] srcHist = PineContext.GetLast(srcName, need, srcOff); var condBuf = PineContext.GetBuffer(condKey); if (condBuf == null || srcHist.Length == 0) return null!; int found = 0; int limit = Math.Min(condBuf.Count, srcHist.Length); for (int i = 0; i < limit; i++) { if (condBuf.Get(i) != 0.0) { if (found == occurrence) return srcHist[i]; found++; } } return null!; } // ── VWAP ───────────────────────────────────────────────────── /// /// 거래량 가중 평균가 (모든 가용 봉 기준 누적). /// source 파라미터는 무시되며 내부적으로 hlc3를 사용합니다. /// public static dynamic vwap(dynamic? source = null) { int bars = PineContext.CurrentBarIndex + 1; double[] hiH = PineContext.GetLast("high", bars); double[] loH = PineContext.GetLast("low", bars); double[] clH = PineContext.GetLast("close", bars); double[] volH = PineContext.GetLast("volume", bars); int count = Math.Min(Math.Min(hiH.Length, loH.Length), Math.Min(clH.Length, volH.Length)); if (count == 0) return null!; double cumPV = 0, cumV = 0; for (int i = count - 1; i >= 0; i--) { double hlc3 = (hiH[i] + loH[i] + clH[i]) / 3.0; cumPV += hlc3 * volH[i]; cumV += volH[i]; } if (cumV == 0) return null!; double result = cumPV / cumV; Log.Debug("[ta.vwap] = {Result:F6}", result); return result; } // ── DEV (표준편차) ──────────────────────────────────────────── public static dynamic dev(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (hist.Length < length) return null!; double mean = hist.Average(); double result = Math.Sqrt(hist.Select(x => (x - mean) * (x - mean)).Average()); Log.Debug("[ta.dev] {Name}({Length}) = {Result:F6}", name, length, result); return result; } // ── VARIANCE ────────────────────────────────────────────────── public static dynamic variance(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (hist.Length < length) return null!; double mean = hist.Average(); double result = hist.Select(x => (x - mean) * (x - mean)).Average(); Log.Debug("[ta.variance] {Name}({Length}) = {Result:F6}", name, length, result); return result; } // ── LINREG ──────────────────────────────────────────────────── /// /// 선형 회귀 값. /// offset: 0=현재 봉, 1=한 봉 전, ... /// public static dynamic linreg(dynamic source, int length, int offset = 0) { var (name, srcOff) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length + offset, srcOff); if (hist.Length < length + offset) return null!; // hist[offset..offset+length-1] 구간을 시간 오름차순으로 변환 double[] y = hist.Skip(offset).Take(length).Reverse().ToArray(); double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; for (int i = 0; i < length; i++) { sumX += i; sumY += y[i]; sumXY += i * y[i]; sumX2 += (double)i * i; } double denom = length * sumX2 - sumX * sumX; if (denom == 0) return y[^1]; double slope = (length * sumXY - sumX * sumY) / denom; double intercept = (sumY - slope * sumX) / length; double result = slope * (length - 1) + intercept; Log.Debug("[ta.linreg] {Name}({Length},{Offset}) = {Result:F6}", name, length, offset, result); return result; } // ── PIVOTHIGH ───────────────────────────────────────────────── /// /// 피벗 고점. 현재 봉 기준 rightbars 전 봉이 고점이면 값 반환, 아니면 na. /// public static dynamic pivothigh(dynamic source, int leftbars, int rightbars) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); int needed = leftbars + rightbars + 1; double[] hist = PineContext.GetLast(name, needed, offset); if (hist.Length < needed) return null!; double candidate = hist[rightbars]; for (int i = 0; i < rightbars; i++) if (hist[i] >= candidate) return null!; for (int i = rightbars + 1; i < needed; i++) if (hist[i] >= candidate) return null!; return candidate; } // ── PIVOTLOW ────────────────────────────────────────────────── /// /// 피벗 저점. 현재 봉 기준 rightbars 전 봉이 저점이면 값 반환, 아니면 na. /// public static dynamic pivotlow(dynamic source, int leftbars, int rightbars) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); int needed = leftbars + rightbars + 1; double[] hist = PineContext.GetLast(name, needed, offset); if (hist.Length < needed) return null!; double candidate = hist[rightbars]; for (int i = 0; i < rightbars; i++) if (hist[i] <= candidate) return null!; for (int i = rightbars + 1; i < needed; i++) if (hist[i] <= candidate) return null!; return candidate; } // ── PERCENTRANK ─────────────────────────────────────────────── public static dynamic percentrank(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length + 1, offset); if (hist.Length < length + 1) return null!; double current = hist[0]; int count = 0; for (int i = 1; i <= length; i++) if (hist[i] < current) count++; double result = (double)count / length * 100.0; Log.Debug("[ta.percentrank] {Name}({Length}) = {Result:F2}", name, length, result); return result; } // ── MOM ────────────────────────────────────────────────────── public static dynamic mom(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length + 1, offset); if (length <= 0 || hist.Length < length + 1) return null!; return hist[0] - hist[length]; } } #endregion // ════════════════════════════════════════════════════════════════ #region math — 실제 계산 구현 // ════════════════════════════════════════════════════════════════ public static class math { public static dynamic max(params dynamic[] values) { var nums = values.Select(ResolveDouble).Where(x => !double.IsNaN(x)).ToList(); if (nums.Count == 0) { Log.Debug("[math.max] 계산 불가"); return null!; } double result = nums.Max(); Log.Debug("[math.max] = {Result:F6}", result); return result; } public static dynamic min(params dynamic[] values) { var nums = values.Select(ResolveDouble).Where(x => !double.IsNaN(x)).ToList(); if (nums.Count == 0) { Log.Debug("[math.min] 계산 불가"); return null!; } double result = nums.Min(); Log.Debug("[math.min] = {Result:F6}", result); return result; } public static dynamic avg(params dynamic[] values) { var nums = values.Select(ResolveDouble).Where(x => !double.IsNaN(x)).ToList(); if (nums.Count == 0) return null!; return nums.Average(); } public static dynamic abs(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Abs(v); } public static dynamic ceil(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Ceiling(v); } public static dynamic floor(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Floor(v); } /// 정수 반올림 (소수점 없음) public static dynamic round(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Round(v, MidpointRounding.AwayFromZero); } /// 소수점 자릿수 지정 반올림 public static dynamic round(dynamic number, int decimals) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Round(v, decimals, MidpointRounding.AwayFromZero); } public static dynamic sqrt(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Sqrt(v); } public static dynamic pow(dynamic base_val, dynamic exponent) { double b = ResolveDouble(base_val), e = ResolveDouble(exponent); return (double.IsNaN(b) || double.IsNaN(e)) ? null! : Math.Pow(b, e); } public static dynamic log(dynamic number) { double v = ResolveDouble(number); return (double.IsNaN(v) || v <= 0) ? null! : Math.Log(v); } public static dynamic log10(dynamic number) { double v = ResolveDouble(number); return (double.IsNaN(v) || v <= 0) ? null! : Math.Log10(v); } public static dynamic exp(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : Math.Exp(v); } public static dynamic sign(dynamic number) { double v = ResolveDouble(number); return double.IsNaN(v) ? null! : (double)Math.Sign(v); } public static dynamic todegrees(dynamic radians) { double v = ResolveDouble(radians); return double.IsNaN(v) ? null! : v * (180.0 / Math.PI); } public static dynamic toradians(dynamic degrees) { double v = ResolveDouble(degrees); return double.IsNaN(v) ? null! : v * (Math.PI / 180.0); } public static dynamic sum(dynamic source, int length) { var (name, offset) = ParseSeriesRef(((object?)source)?.ToString() ?? ""); double[] hist = PineContext.GetLast(name, length, offset); if (length <= 0 || hist.Length < length) return null!; return hist.Sum(); } } #endregion // ════════════════════════════════════════════════════════════════ #region array — 실제 구현 (PineContext 배열 스토어 사용) // ════════════════════════════════════════════════════════════════ public static class array { public static dynamic new_float(int size = 0, dynamic? initial_value = null) { Log.Debug("[array.new_float] size={size}", size); return null!; } public static dynamic new_int(int size = 0, dynamic? initial_value = null) { Log.Debug("[array.new_int] size={size}", size); return null!; } public static dynamic new_bool(int size = 0, dynamic? initial_value = null) { Log.Debug("[array.new_bool] size={size}", size); return null!; } public static dynamic new_string(int size = 0, dynamic? initial_value = null) { Log.Debug("[array.new_string] size={size}", size); return null!; } public static void push(dynamic id, dynamic value) { string key = id?.ToString() ?? ""; double val = ToDoubleVal(value); PineContext.GetOrCreateFloat(key).Add(val); Log.Debug("[array.push] {Id} ← {Value} (size={Size})", key, val, PineContext.GetOrCreateFloat(key).Count); } public static dynamic pop(dynamic id) { string key = id?.ToString() ?? ""; var list = PineContext.GetOrCreateFloat(key); if (list.Count == 0) return null!; double last = list[list.Count - 1]; list.RemoveAt(list.Count - 1); return last; } public static int size(dynamic id) { string key = id?.ToString() ?? ""; int s = PineContext.GetOrCreateFloat(key).Count; Log.Debug("[array.size] {Id} = {Size}", key, s); return s; } public static dynamic get(dynamic id, int index) { string key = id?.ToString() ?? ""; var list = PineContext.GetAnyAsFloat(key); if (index < 0 || index >= list.Count) { Log.Debug("[array.get] {Id}[{Index}] 범위 초과 (size={Size})", key, index, list.Count); return null!; } double val = list[index]; Log.Debug("[array.get] {Id}[{Index}] = {Val}", key, index, val); return val; } public static void set(dynamic id, int index, dynamic value) { string key = id?.ToString() ?? ""; var list = PineContext.GetOrCreateFloat(key); while (list.Count <= index) list.Add(0.0); list[index] = ToDoubleVal(value); Log.Debug("[array.set] {Id}[{Index}] = {Val}", key, index, list[index]); } public static void unshift(dynamic id, dynamic value) { string key = id?.ToString() ?? ""; PineContext.GetOrCreateFloat(key).Insert(0, ToDoubleVal(value)); } public static dynamic shift(dynamic id) { string key = id?.ToString() ?? ""; var list = PineContext.GetOrCreateFloat(key); if (list.Count == 0) return null!; double v = list[0]; list.RemoveAt(0); return v; } public static void insert(dynamic id, int index, dynamic value) { string key = id?.ToString() ?? ""; var list = PineContext.GetOrCreateFloat(key); while (list.Count < index) list.Add(0.0); list.Insert(index, ToDoubleVal(value)); } public static void remove(dynamic id, int index) { string key = id?.ToString() ?? ""; var list = PineContext.GetOrCreateFloat(key); if (index >= 0 && index < list.Count) list.RemoveAt(index); } public static void clear(dynamic id) { PineContext.GetOrCreateFloat(id?.ToString() ?? "").Clear(); } private static double ToDoubleVal(dynamic v) { if (v is double d) return d; if (v is int i) return i; if (v is float f) return f; string s = v?.ToString() ?? ""; if (double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double p)) return p; double? ctx = PineContext.GetVariable(s); return ctx ?? 0.0; } } #endregion // ════════════════════════════════════════════════════════════════ #region strategy // ════════════════════════════════════════════════════════════════ public static class strategy { public static dynamic position_size { get; set; } = null!; public static dynamic position_avg_price { get; set; } = null!; public static dynamic openprofit { get; set; } = null!; public static dynamic equity { get; set; } = null!; public static dynamic netprofit { get; set; } = null!; public static dynamic grossprofit { get; set; } = null!; public static dynamic grossloss { get; set; } = null!; public static void entry(string id, string direction, dynamic? qty = null, dynamic? limit = null, dynamic? stop = null, string comment = "") => Log.Information("[strategy.entry] id={id}, direction={direction}", id, direction); public static void close(string id, string comment = "", dynamic? qty = null, dynamic? qty_percent = null) => Log.Information("[strategy.close] id={id}", id); public static void close_all(string comment = "") => Log.Information("[strategy.close_all]"); public static void exit(string id, string from_entry = "", dynamic? qty = null, dynamic? profit = null, dynamic? limit = null, dynamic? loss = null, dynamic? stop = null, string comment = "") => Log.Information("[strategy.exit] id={id}", id); public static void order(string id, string direction, dynamic? qty = null, dynamic? limit = null, dynamic? stop = null, string comment = "") => Log.Information("[strategy.order] id={id}, direction={direction}", id, direction); public static void cancel(string id) => Log.Information("[strategy.cancel] id={id}", id); public static void cancel_all() => Log.Information("[strategy.cancel_all]"); } #endregion // ════════════════════════════════════════════════════════════════ #region request // ════════════════════════════════════════════════════════════════ public static class request { public static dynamic security(string symbol, string timeframe, dynamic expression, dynamic? gaps = null, dynamic? lookahead = null) { Log.Debug("[request.security] symbol={symbol}, tf={timeframe}", symbol, timeframe); return null!; } public static dynamic financial(string symbol, string financial_id, string period, dynamic? gaps = null) => null!; public static dynamic dividends(string symbol, dynamic? gaps = null) => null!; public static dynamic earnings(string symbol, dynamic? gaps = null) => null!; public static dynamic splits(string symbol, dynamic? gaps = null) => null!; } #endregion // ════════════════════════════════════════════════════════════════ #region syminfo — 심볼 정보 // ════════════════════════════════════════════════════════════════ /// /// 심볼 메타 정보. /// BitgetConfig에서 Symbol 설정 후 syminfo.ticker 등을 초기화하여 사용합니다. /// public static class syminfo { /// 심볼 코드 (예: XRPUSDT) public static string ticker { get; set; } = ""; /// 심볼 전체 ID (예: BITGET:XRPUSDT.P) public static string tickerid { get; set; } = ""; /// 거래소 이름 (예: BITGET) public static string exchange { get; set; } = ""; /// 기준 통화 (예: USDT) public static string currency { get; set; } = "USDT"; /// 심볼 설명 public static string description { get; set; } = ""; /// 상품 유형 (예: futures) public static string type { get; set; } = "futures"; /// 최소 틱 크기 public static double mintick { get; set; } = 0.0001; /// 포인트 당 가치 public static double pointvalue { get; set; } = 1.0; /// 최소 이동 단위 public static double minmove { get; set; } = 1.0; /// 가격 스케일 (mintick = minmove / pricescale) public static int pricescale { get; set; } = 10000; /// 타임존 (예: UTC) public static string timezone { get; set; } = "UTC"; /// 세션 문자열 (예: 24x7) public static string session { get; set; } = "24x7"; public static string prefix { get; set; } = ""; public static string root { get; set; } = ""; public static string suffix { get; set; } = ""; public static bool isstandard { get; set; } = true; } #endregion // ════════════════════════════════════════════════════════════════ #region map // ════════════════════════════════════════════════════════════════ public static class map { public static dynamic new_map() { Log.Debug("[map.new_map]"); return null!; } public static void put(dynamic id, dynamic key, dynamic value) => Log.Debug("[map.put] id={id}, key={key}", id, key); public static dynamic get(dynamic id, dynamic key) { Log.Debug("[map.get]"); return null!; } public static bool contains(dynamic id, dynamic key) { return false; } public static void remove(dynamic id, dynamic key) { } public static void clear(dynamic id) { } public static int size(dynamic id) { return 0; } } #endregion // ════════════════════════════════════════════════════════════════ #region @string // ════════════════════════════════════════════════════════════════ public static class @string { public static int length(string source) => source?.Length ?? 0; public static string replace_all(string source, string target, string replacement) => source?.Replace(target, replacement) ?? ""; public static string tolower(string source) => source?.ToLower() ?? ""; public static string toupper(string source) => source?.ToUpper() ?? ""; public static dynamic split(string source, string separator) => source?.Split(separator) ?? Array.Empty(); public static string format(string format, params dynamic[] args) { try { return string.Format(format, args.Cast().ToArray()); } catch { return format; } } } #endregion // ════════════════════════════════════════════════════════════════ #region timeframe // ════════════════════════════════════════════════════════════════ public static class timeframe { public static string period { get; set; } = ""; public static bool isintraday { get; set; } = false; public static bool isdaily { get; set; } = false; public static bool isweekly { get; set; } = false; public static bool ismonthly { get; set; } = false; public static int multiplier { get; set; } = 0; } #endregion // ════════════════════════════════════════════════════════════════ #region str — 문자열 변환 함수 // ════════════════════════════════════════════════════════════════ public static class str { /// 숫자 → 문자열. format: "#.##" 등 Pine Script 포맷 지원 public static string tostring(dynamic value, string format = "") { try { double d = Convert.ToDouble(value); if (string.IsNullOrEmpty(format)) return d.ToString(System.Globalization.CultureInfo.InvariantCulture); string fmt = format.Replace("#", "0"); return d.ToString(fmt, System.Globalization.CultureInfo.InvariantCulture); } catch { return value?.ToString() ?? ""; } } /// 문자열 → 숫자. 변환 불가 시 na 반환 public static dynamic tonumber(dynamic source) { string s = source?.ToString() ?? ""; if (double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) return d; return null!; } /// 포맷 문자열 (C# string.Format 문법) public static string format(string formatStr, params dynamic[] args) { try { return string.Format(formatStr, args.Cast().ToArray()); } catch { return formatStr; } } public static int length(string source) => source?.Length ?? 0; /// 부분 문자열 (end 미지정 시 끝까지) public static string substring(string source, int start, int end = -1) { if (source == null) return ""; if (start < 0) start = 0; if (end < 0 || end > source.Length) end = source.Length; if (start >= end) return ""; return source.Substring(start, end - start); } public static bool contains(string source, string str) => source?.Contains(str, StringComparison.Ordinal) ?? false; public static bool startswith(string source, string str) => source?.StartsWith(str, StringComparison.Ordinal) ?? false; public static bool endswith(string source, string str) => source?.EndsWith(str, StringComparison.Ordinal) ?? false; public static string trim(string source) => source?.Trim() ?? ""; /// 정규식 첫 번째 매칭 반환. 매칭 없으면 빈 문자열 public static string match(string source, string regex) { try { var m = System.Text.RegularExpressions.Regex.Match(source ?? "", regex); return m.Success ? m.Value : ""; } catch { return ""; } } public static string replace(string source, string target, string replacement) => source?.Replace(target, replacement) ?? ""; public static string lower(string source) => source?.ToLower() ?? ""; public static string upper(string source) => source?.ToUpper() ?? ""; public static dynamic split(string source, string separator) => source?.Split(separator) ?? Array.Empty(); } #endregion // ════════════════════════════════════════════════════════════════ #region color — 색상 스텁 (시각화 무시) // ════════════════════════════════════════════════════════════════ public static class color { public static dynamic green { get; } = null!; public static dynamic red { get; } = null!; public static dynamic blue { get; } = null!; public static dynamic white { get; } = null!; public static dynamic black { get; } = null!; public static dynamic yellow { get; } = null!; public static dynamic orange { get; } = null!; public static dynamic purple { get; } = null!; public static dynamic teal { get; } = null!; public static dynamic gray { get; } = null!; public static dynamic silver { get; } = null!; public static dynamic lime { get; } = null!; public static dynamic aqua { get; } = null!; public static dynamic fuchsia { get; } = null!; public static dynamic navy { get; } = null!; public static dynamic maroon { get; } = null!; public static dynamic @new(dynamic baseColor, double transp = 0) => null!; public static dynamic rgb(double r, double g, double b, double transp = 0) => null!; } #endregion // ════════════════════════════════════════════════════════════════ #region na / nz / bar_index — 내장 함수 및 변수 // ════════════════════════════════════════════════════════════════ /// na(x) — x 가 na(null)이면 true public static bool na(dynamic? x) => x == null; /// nz(x, y?) — x가 na이면 y(기본 0) 반환 public static dynamic nz(dynamic? x, dynamic? y = null) => x == null ? (y ?? (dynamic)0.0) : x; /// 현재 봉 인덱스 (0-based) public static int bar_index => PineContext.CurrentBarIndex; /// size 상수 스텁 public static class size { public static dynamic tiny { get; } = "tiny"; public static dynamic small { get; } = "small"; public static dynamic normal { get; } = "normal"; public static dynamic large { get; } = "large"; public static dynamic huge { get; } = "huge"; public static dynamic auto { get; } = "auto"; } /// location 상수 스텁 public static class location { public static dynamic abovebar { get; } = "abovebar"; public static dynamic belowbar { get; } = "belowbar"; public static dynamic top { get; } = "top"; public static dynamic bottom { get; } = "bottom"; public static dynamic automatic { get; } = "automatic"; } /// shape 상수 스텁 public static class shape { public static dynamic labelup { get; } = "labelup"; public static dynamic labeldown { get; } = "labeldown"; public static dynamic arrowup { get; } = "arrowup"; public static dynamic arrowdown { get; } = "arrowdown"; public static dynamic circle { get; } = "circle"; public static dynamic cross { get; } = "cross"; public static dynamic diamond { get; } = "diamond"; public static dynamic flag { get; } = "flag"; public static dynamic square { get; } = "square"; public static dynamic triangleup { get; } = "triangleup"; public static dynamic triangledown { get; } = "triangledown"; public static dynamic xcross { get; } = "xcross"; } #endregion } }