Files
PineSript/PineScript/Program.cs

191 lines
8.1 KiB
C#
Raw Normal View History

2026-04-25 17:32:49 +09:00
using Serilog;
using PineScriptV6;
using System.Diagnostics;
// ── Serilog 초기화 ──────────────────────────────────────────────────────────
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(a => a.File(
path: "logs/pine_.log",
rollingInterval: RollingInterval.Day,
buffered: true,
flushToDiskInterval: TimeSpan.FromSeconds(1),
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"))
.CreateLogger();
// ════════════════════════════════════════════════════════════════
// ★ 분석 기간 설정 (여기서 날짜 범위를 변경하세요)
// ════════════════════════════════════════════════════════════════
DateTimeOffset dateFrom = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.FromHours(0));
DateTimeOffset dateTo = new DateTimeOffset(2025, 1, 31, 23, 59, 59, TimeSpan.FromHours(0));
// ── 매매 핸들러 등록 (로그만, 실제 구현은 나중에) ──────────────────────────
TradingSignal.OnBuy = (price) =>
{
Log.Information("★ [BUY 실행] 롱 진입 | 현재가: {Price}", price);
// TODO: await BrokerApi.BuyAsync(symbol, qty, price);
};
TradingSignal.OnSell = (price) =>
{
Log.Information("★ [SELL 실행] 롱 청산 | 현재가: {Price}", price);
// TODO: await BrokerApi.SellAsync(symbol, qty, price);
};
TradingSignal.OnShort = (price) =>
{
Log.Information("★ [SHORT 실행] 숏 진입 | 현재가: {Price}", price);
// TODO: await BrokerApi.ShortAsync(symbol, qty, price);
};
TradingSignal.OnCover = (price) =>
{
Log.Information("★ [COVER 실행] 숏 청산 | 현재가: {Price}", price);
// TODO: await BrokerApi.CoverAsync(symbol, qty, price);
};
// ── 실행 ────────────────────────────────────────────────────────────────────
try
{
Stopwatch sw = Stopwatch.StartNew();
// 1) CSV 로드 (전체)
const string csvPath = "./BITGET_XRPUSDT, 30.csv";
List<CandleData> allCandles = CsvLoader.Load(csvPath);
if (allCandles.Count == 0)
{
Log.Error("봉 데이터가 없습니다. CSV 파일을 확인하세요: {Path}", csvPath);
return;
}
// ── 워밍업 + 분석 구간 슬라이싱 ─────────────────────────────
// 지표 최대 필요 봉 수:
// SMA72 → 72봉
// 일목구름 span2 → 52봉 + displacement 26 = 78봉
// rangeThreshold → ta.sma(longRatio, 240) + lowest 12 = 252봉
// 여유분 포함 → 300봉
const int WarmupBars = 300;
// dateFrom 바로 앞 인덱스를 찾아 warmup 시작점 결정
int fromIdx = allCandles.FindIndex(c => c.Time >= dateFrom);
if (fromIdx < 0)
{
Log.Error("분석 기간 내 봉 데이터가 없습니다. dateFrom 을 확인하세요.");
return;
}
int startIdx = Math.Max(0, fromIdx - WarmupBars);
// dateTo 까지만 포함
List<CandleData> candles = allCandles
.Skip(startIdx)
.TakeWhile(c => c.Time <= dateTo)
.ToList();
int warmupCount = fromIdx - startIdx;
int rangeCount = candles.Count - warmupCount;
Log.Information("CSV 전체: {Total}봉", allCandles.Count);
Log.Information("실행 봉: {Exec}봉 (워밍업 {Warmup}봉 + 분석 {Range}봉)",
candles.Count, warmupCount, rangeCount);
Log.Information("분석 기간: {From} ~ {To}",
dateFrom.ToString("yyyy-MM-dd"), dateTo.ToString("yyyy-MM-dd"));
if (rangeCount <= 0)
{
Log.Error("분석 기간 내 봉 데이터가 없습니다. dateFrom/dateTo 를 확인하세요.");
return;
}
// 2) Pine Script 실행 — 워밍업 포함 구간만 실행
// signalFrom 이후 봉에만 매 봉 상세 추적 로그가 출력됩니다
var executor = new PineScriptExecutor("./example.pine");
executor.ExecuteOnCandles(candles, signalFrom: dateFrom);
// 3) 날짜 범위 필터 후 결과 출력 (워밍업 중 발생한 신호 제거)
var filteredResults = TradingSignal.Results
.Where(r => r.Time >= dateFrom && r.Time <= dateTo)
.ToList();
sw.Stop();
PrintAndSaveResults(filteredResults, dateFrom, dateTo);
}
catch (Exception ex)
{
Log.Fatal(ex, "예상치 못한 오류가 발생했습니다.");
}
finally
{
Log.CloseAndFlush();
}
// ── 결과 출력 메서드 ─────────────────────────────────────────────────────────
static void PrintAndSaveResults(List<TradeSignalEvent> results,
DateTimeOffset dateFrom, DateTimeOffset dateTo)
{
string separator = " " + new string('-', 56);
Log.Information("");
Log.Information("══════════════════════════════════════════════════════════");
Log.Information(" 매매 신호 결과 요약 ({From} ~ {To})",
dateFrom.ToString("yyyy-MM-dd"), dateTo.ToString("yyyy-MM-dd"));
Log.Information("══════════════════════════════════════════════════════════");
if (results.Count == 0)
{
Log.Information(" 감지된 신호 없음");
Log.Information("══════════════════════════════════════════════════════════");
return;
}
// 헤더
Log.Information(" {0,-20} {1,-12} {2,10}", "시간", "신호", "가격");
Log.Information(separator);
int buyCount = 0, sellCount = 0, shortCount = 0, coverCount = 0;
foreach (var e in results)
{
Log.Information(" {0,-20} {1,-12} {2,10:F4}",
e.Time.ToString("yyyy-MM-dd HH:mm"),
e.Signal.ToString(),
e.Price);
switch (e.Signal)
{
case SignalType.Buy: buyCount++; break;
case SignalType.Sell: sellCount++; break;
case SignalType.Short: shortCount++; break;
case SignalType.Cover: coverCount++; break;
}
}
Log.Information(separator);
Log.Information(" 합계 | BUY={Buy} SELL={Sell} SHORT={Short} COVER={Cover} TOTAL={Total}",
buyCount, sellCount, shortCount, coverCount, results.Count);
Log.Information("══════════════════════════════════════════════════════════");
SaveResultsToCsv(results);
}
static void SaveResultsToCsv(List<TradeSignalEvent> results)
{
string outputPath = "signal_results.csv";
// TradingView Strategy Tester는 신호 발생 봉의 '다음 봉 Open'에서 체결을 기록합니다.
// 30분봉 기준으로 +30분 = TV 기준 체결 시각(tv_time).
const int BarMinutes = 30;
if (File.Exists(outputPath))
{
File.Delete(outputPath);
Log.Debug(" 기존 결과 파일 삭제: {Path}", outputPath);
}
var lines = new List<string> { "time,signal,price,tv_time" };
lines.AddRange(results.Select(e =>
{
var tvTime = e.Time.AddMinutes(BarMinutes);
return $"{e.Time:yyyy-MM-dd HH:mm},{e.Signal},{e.Price:F4},{tvTime:yyyy-MM-dd HH:mm}";
}));
File.WriteAllLines(outputPath, lines);
Log.Information(" 결과 CSV 저장 완료: {Path} ({Count}건)", outputPath, results.Count);
}