Files
PineSript/PineScript/Pine.cs
2026-04-26 19:00:54 +09:00

1504 lines
71 KiB
C#

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; }
}
/// <summary>
/// "close[1]" → ("close", 1), "high" → ("high", 0)
/// </summary>
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);
}
/// <summary>동적 값을 double로 변환 (PineContext 변수명도 해석)</summary>
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;
}
/// <summary>EMA 계산 (src: 오름차순 배열, src[0]=가장 오래된 값)</summary>
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;
/// <summary>매 봉 시작 전 호출 — 상태 함수의 호출 인덱스를 초기화합니다.</summary>
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;
/// <summary>현재 봉 시각의 시 (0-23, UTC)</summary>
public static int hour => UtcBar.Hour;
/// <summary>현재 봉 시각의 분 (0-59)</summary>
public static int minute => UtcBar.Minute;
/// <summary>현재 봉 시각의 초 (0-59)</summary>
public static int second => UtcBar.Second;
/// <summary>요일 — Pine Script 기준 (1=일요일, 2=월요일, ..., 7=토요일)</summary>
public static int dayofweek => (int)UtcBar.DayOfWeek + 1;
/// <summary>월의 일 (1-31)</summary>
public static int dayofmonth => UtcBar.Day;
/// <summary>월 (1-12)</summary>
public static int month => UtcBar.Month;
/// <summary>연도</summary>
public static int year => UtcBar.Year;
/// <summary>현재 Unix 시각 (밀리초)</summary>
public static long timenow => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
/// <summary>특정 날짜/시각 → Unix 밀리초 타임스탬프 (UTC 기준)</summary>
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 ─────────────────────────────────────────────────────
/// <summary>
/// MACD 계산.
/// 반환값: double[3] { macdLine, signalLine, histogram }
/// Pine Script: [macd, signal, hist] = ta.macd(close, 12, 26, 9)
/// </summary>
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<double>();
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) ──────────────────────────────────────
/// <summary>
/// 볼린저밴드.
/// 반환값: double[3] { upper, basis, lower }
/// </summary>
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 ────────────────────────────────────────────────
/// <summary>
/// 조건이 마지막으로 true였던 봉 수.
/// 각 호출 위치에 고유 인덱스를 부여하여 상태를 추적합니다.
/// (매 봉 시작 전 Pine.ResetBarState() 호출 필요)
/// </summary>
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 ────────────────────────────────────────────────
/// <summary>
/// 조건이 true였을 때의 source 값 (occurrence번째).
/// 0 = 가장 최근 true, 1 = 그 전 true, ...
/// </summary>
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 ─────────────────────────────────────────────────────
/// <summary>
/// 거래량 가중 평균가 (모든 가용 봉 기준 누적).
/// source 파라미터는 무시되며 내부적으로 hlc3를 사용합니다.
/// </summary>
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 ────────────────────────────────────────────────────
/// <summary>
/// 선형 회귀 값.
/// offset: 0=현재 봉, 1=한 봉 전, ...
/// </summary>
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 ─────────────────────────────────────────────────
/// <summary>
/// 피벗 고점. 현재 봉 기준 rightbars 전 봉이 고점이면 값 반환, 아니면 na.
/// </summary>
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 ──────────────────────────────────────────────────
/// <summary>
/// 피벗 저점. 현재 봉 기준 rightbars 전 봉이 저점이면 값 반환, 아니면 na.
/// </summary>
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);
}
/// <summary>정수 반올림 (소수점 없음)</summary>
public static dynamic round(dynamic number)
{
double v = ResolveDouble(number);
return double.IsNaN(v) ? null! : Math.Round(v, MidpointRounding.AwayFromZero);
}
/// <summary>소수점 자릿수 지정 반올림</summary>
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 — 심볼 정보
// ════════════════════════════════════════════════════════════════
/// <summary>
/// 심볼 메타 정보.
/// BitgetConfig에서 Symbol 설정 후 syminfo.ticker 등을 초기화하여 사용합니다.
/// </summary>
public static class syminfo
{
/// <summary>심볼 코드 (예: XRPUSDT)</summary>
public static string ticker { get; set; } = "";
/// <summary>심볼 전체 ID (예: BITGET:XRPUSDT.P)</summary>
public static string tickerid { get; set; } = "";
/// <summary>거래소 이름 (예: BITGET)</summary>
public static string exchange { get; set; } = "";
/// <summary>기준 통화 (예: USDT)</summary>
public static string currency { get; set; } = "USDT";
/// <summary>심볼 설명</summary>
public static string description { get; set; } = "";
/// <summary>상품 유형 (예: futures)</summary>
public static string type { get; set; } = "futures";
/// <summary>최소 틱 크기</summary>
public static double mintick { get; set; } = 0.0001;
/// <summary>포인트 당 가치</summary>
public static double pointvalue { get; set; } = 1.0;
/// <summary>최소 이동 단위</summary>
public static double minmove { get; set; } = 1.0;
/// <summary>가격 스케일 (mintick = minmove / pricescale)</summary>
public static int pricescale { get; set; } = 10000;
/// <summary>타임존 (예: UTC)</summary>
public static string timezone { get; set; } = "UTC";
/// <summary>세션 문자열 (예: 24x7)</summary>
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<string>();
public static string format(string format, params dynamic[] args)
{
try { return string.Format(format, args.Cast<object>().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
{
/// <summary>숫자 → 문자열. format: "#.##" 등 Pine Script 포맷 지원</summary>
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() ?? ""; }
}
/// <summary>문자열 → 숫자. 변환 불가 시 na 반환</summary>
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!;
}
/// <summary>포맷 문자열 (C# string.Format 문법)</summary>
public static string format(string formatStr, params dynamic[] args)
{
try { return string.Format(formatStr, args.Cast<object>().ToArray()); }
catch { return formatStr; }
}
public static int length(string source) => source?.Length ?? 0;
/// <summary>부분 문자열 (end 미지정 시 끝까지)</summary>
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() ?? "";
/// <summary>정규식 첫 번째 매칭 반환. 매칭 없으면 빈 문자열</summary>
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<string>();
}
#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 — 내장 함수 및 변수
// ════════════════════════════════════════════════════════════════
/// <summary>na(x) — x 가 na(null)이면 true</summary>
public static bool na(dynamic? x) => x == null;
/// <summary>nz(x, y?) — x가 na이면 y(기본 0) 반환</summary>
public static dynamic nz(dynamic? x, dynamic? y = null)
=> x == null ? (y ?? (dynamic)0.0) : x;
/// <summary>현재 봉 인덱스 (0-based)</summary>
public static int bar_index => PineContext.CurrentBarIndex;
/// <summary>size 상수 스텁</summary>
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";
}
/// <summary>location 상수 스텁</summary>
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";
}
/// <summary>shape 상수 스텁</summary>
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
}
}