업데이트
This commit is contained in:
30
.dockerignore
Normal file
30
.dockerignore
Normal file
@@ -0,0 +1,30 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
363
.gitignore
vendored
Normal file
363
.gitignore
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
75
Buyer/BitgetConfig.cs
Normal file
75
Buyer/BitgetConfig.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// Bitget 실시간 트레이더 설정.
|
||||
/// 모든 외부 의존 값은 이 클래스에서 변수화합니다.
|
||||
/// </summary>
|
||||
public class BitgetConfig
|
||||
{
|
||||
// ── 종목 설정 ─────────────────────────────────────────────────
|
||||
/// <summary>거래 심볼 (예: XRPUSDT)</summary>
|
||||
public string Symbol { get; set; } = "XRPUSDT";
|
||||
|
||||
/// <summary>상품 타입 (USDT-FUTURES | COIN-FUTURES)</summary>
|
||||
public string ProductType { get; set; } = "USDT-FUTURES";
|
||||
|
||||
/// <summary>
|
||||
/// 캔들 시간 단위 — Bitget API 형식 그대로 입력합니다.
|
||||
/// 지원 값: 1m 3m 5m 15m 30m 1H 2H 4H 6H 12H 1D 1W 1M
|
||||
/// </summary>
|
||||
public string Granularity { get; set; } = BitgetGranularity.Min30;
|
||||
|
||||
// ── WarmUp 설정 ───────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 시작 전 로드할 과거 봉 수.
|
||||
/// Pine Script 지표의 최대 윈도우(sma72 등)보다 충분히 커야 합니다.
|
||||
/// </summary>
|
||||
public int WarmUpBars { get; set; } = 500;
|
||||
|
||||
// ── REST API ─────────────────────────────────────────────────
|
||||
/// <summary>Bitget REST API 베이스 URL</summary>
|
||||
public string RestBaseUrl { get; set; } = "https://api.bitget.com";
|
||||
|
||||
/// <summary>REST 요청 타임아웃 (초)</summary>
|
||||
public int RestTimeoutSec { get; set; } = 10;
|
||||
|
||||
// ── 폴링 설정 ─────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// REST API 호출 간격 (밀리초).
|
||||
/// 1000 = 1초마다 호출. Bitget 레이트 리밋 여유 있음.
|
||||
/// </summary>
|
||||
public int PollIntervalMs { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 폴링 1회에 조회할 최근 봉 수.
|
||||
/// 현재 진행 중인 봉 1개 + 완성 봉 확인용 2개 = 3 권장.
|
||||
/// </summary>
|
||||
public int PollCandleCount { get; set; } = 3;
|
||||
|
||||
// ── Pine Script ───────────────────────────────────────────────
|
||||
/// <summary>실행할 .pine 스크립트 파일 경로</summary>
|
||||
public string PineScriptPath { get; set; } = "./example.pine";
|
||||
|
||||
// ── 파생 속성 ─────────────────────────────────────────────────
|
||||
/// <summary>REST API에 전달할 granularity — Granularity 그대로 사용</summary>
|
||||
public string RestGranularity => Granularity;
|
||||
|
||||
/// <summary>Granularity 문자열 → 분(minutes)</summary>
|
||||
public int IntervalMinutes => Granularity switch
|
||||
{
|
||||
"1m" => 1,
|
||||
"3m" => 3,
|
||||
"5m" => 5,
|
||||
"15m" => 15,
|
||||
"30m" => 30,
|
||||
"1H" => 60,
|
||||
"2H" => 120,
|
||||
"4H" => 240,
|
||||
"6H" => 360,
|
||||
"12H" => 720,
|
||||
"1D" => 1440,
|
||||
"1W" => 10080,
|
||||
_ => 30
|
||||
};
|
||||
}
|
||||
}
|
||||
28
Buyer/BitgetGranularity.cs
Normal file
28
Buyer/BitgetGranularity.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// Bitget API 캔들 시간 단위 상수.
|
||||
/// BitgetConfig.Granularity에 이 값을 사용합니다.
|
||||
/// </summary>
|
||||
public static class BitgetGranularity
|
||||
{
|
||||
// ── 분봉 ──────────────────────────────────────────────────────
|
||||
public const string Min1 = "1m";
|
||||
public const string Min3 = "3m";
|
||||
public const string Min5 = "5m";
|
||||
public const string Min15 = "15m";
|
||||
public const string Min30 = "30m";
|
||||
|
||||
// ── 시간봉 ────────────────────────────────────────────────────
|
||||
public const string Hour1 = "1H";
|
||||
public const string Hour2 = "2H";
|
||||
public const string Hour4 = "4H";
|
||||
public const string Hour6 = "6H";
|
||||
public const string Hour12 = "12H";
|
||||
|
||||
// ── 일/주/월봉 ────────────────────────────────────────────────
|
||||
public const string Day1 = "1D";
|
||||
public const string Week1 = "1W";
|
||||
public const string Month1 = "1M";
|
||||
}
|
||||
}
|
||||
126
Buyer/BitgetRestClient.cs
Normal file
126
Buyer/BitgetRestClient.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json;
|
||||
using PineScriptV6;
|
||||
using Serilog;
|
||||
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// Bitget REST API — 캔들 데이터 조회.
|
||||
/// GET /api/v2/mix/market/candles
|
||||
/// </summary>
|
||||
public class BitgetRestClient : IDisposable
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly BitgetConfig _cfg;
|
||||
|
||||
public BitgetRestClient(BitgetConfig cfg)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_http = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(cfg.RestBaseUrl),
|
||||
Timeout = TimeSpan.FromSeconds(cfg.RestTimeoutSec)
|
||||
};
|
||||
_http.DefaultRequestHeaders.Add("User-Agent", "Buyer/1.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 최근 <paramref name="limit"/>개 봉을 가져옵니다.
|
||||
/// 반환값은 과거→현재 오름차순으로 정렬됩니다.
|
||||
/// </summary>
|
||||
public async Task<List<CandleData>> GetCandlesAsync(int limit = 500)
|
||||
{
|
||||
int safeLimit = Math.Min(limit, 1000);
|
||||
|
||||
// 기존 동작 확인된 파라미터 순서: symbol, granularity, limit, productType
|
||||
string url = "/api/v2/mix/market/candles"
|
||||
+ $"?symbol={_cfg.Symbol}"
|
||||
+ $"&granularity={_cfg.RestGranularity}"
|
||||
+ $"&limit={safeLimit}"
|
||||
+ $"&productType={_cfg.ProductType}";
|
||||
|
||||
Log.Debug("[REST] 요청: {Url}", url);
|
||||
|
||||
// GetStringAsync는 4xx/5xx 시 본문 없이 예외 → GetAsync로 변경하여 본문 확인
|
||||
HttpResponseMessage response;
|
||||
string body;
|
||||
try
|
||||
{
|
||||
response = await _http.GetAsync(url);
|
||||
body = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "[REST] HTTP 요청 자체 실패 (네트워크 오류)");
|
||||
throw;
|
||||
}
|
||||
|
||||
// HTTP 오류 시 응답 본문을 로그에 찍고 예외
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Error("[REST] HTTP {Status} — 응답 본문: {Body}",
|
||||
(int)response.StatusCode, body);
|
||||
throw new HttpRequestException(
|
||||
$"HTTP {(int)response.StatusCode}: {body}");
|
||||
}
|
||||
|
||||
// JSON 파싱
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
|
||||
string code = root.TryGetProperty("code", out var codeEl)
|
||||
? codeEl.GetString() ?? "" : "";
|
||||
|
||||
if (code != "00000")
|
||||
{
|
||||
string msg = root.TryGetProperty("msg", out var msgEl)
|
||||
? msgEl.GetString() ?? "" : "";
|
||||
Log.Error("[REST] API 오류 code={Code} msg={Msg} body={Body}",
|
||||
code, msg, body);
|
||||
throw new Exception($"Bitget API 오류 code={code} msg={msg}");
|
||||
}
|
||||
|
||||
var candles = new List<CandleData>();
|
||||
|
||||
foreach (var item in root.GetProperty("data").EnumerateArray())
|
||||
{
|
||||
var arr = item.EnumerateArray().ToList();
|
||||
if (arr.Count < 6) continue;
|
||||
|
||||
long ts = long.Parse(arr[0].GetString()!);
|
||||
decimal o = decimal.Parse(arr[1].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal h = decimal.Parse(arr[2].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal l = decimal.Parse(arr[3].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal c = decimal.Parse(arr[4].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal vol = decimal.Parse(arr[5].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
candles.Add(new CandleData
|
||||
{
|
||||
Time = DateTimeOffset.FromUnixTimeMilliseconds(ts),
|
||||
Open = o,
|
||||
High = h,
|
||||
Low = l,
|
||||
Close = c,
|
||||
Volume = vol
|
||||
});
|
||||
}
|
||||
|
||||
// Bitget은 최신→과거 순으로 반환 → 오름차순 정렬
|
||||
candles.Sort((a, b) => a.Time.CompareTo(b.Time));
|
||||
|
||||
Log.Debug("[REST] 수신 {Count}봉 {From} ~ {To}",
|
||||
candles.Count,
|
||||
candles.FirstOrDefault()?.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "-",
|
||||
candles.LastOrDefault() ?.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "-");
|
||||
|
||||
return candles;
|
||||
}
|
||||
|
||||
public void Dispose() => _http.Dispose();
|
||||
}
|
||||
}
|
||||
239
Buyer/BitgetWebSocketClient.cs
Normal file
239
Buyer/BitgetWebSocketClient.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using PineScriptV6;
|
||||
using Serilog;
|
||||
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// Bitget Public WebSocket — 실시간 캔들 스트림 수신.
|
||||
/// confirm="1" 인 완성 봉만 OnCandle 이벤트로 발행합니다.
|
||||
/// 연결 끊김 시 자동 재연결합니다.
|
||||
/// </summary>
|
||||
public class BitgetWebSocketClient : IAsyncDisposable
|
||||
{
|
||||
private readonly BitgetConfig _cfg;
|
||||
|
||||
/// <summary>완성된 봉(confirm=1)이 도착했을 때 발생합니다.</summary>
|
||||
public event Action<CandleData>? OnCandle;
|
||||
|
||||
/// <summary>WebSocket 연결/재연결 상태 변경 시 발생합니다.</summary>
|
||||
public event Action<bool>? OnConnectionChanged;
|
||||
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
public BitgetWebSocketClient(BitgetConfig cfg) => _cfg = cfg;
|
||||
|
||||
/// <summary>
|
||||
/// 연결을 시작하고 수신 루프를 백그라운드로 실행합니다.
|
||||
/// </summary>
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
_ = Task.Run(() => RunLoop(_cts.Token), _cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 내부 — 연결 루프 (재연결 포함)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
private async Task RunLoop(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using var ws = new ClientWebSocket();
|
||||
try
|
||||
{
|
||||
await ws.ConnectAsync(new Uri(_cfg.WsUrl), ct);
|
||||
Log.Information("[WS] 연결됨 → {Url}", _cfg.WsUrl);
|
||||
OnConnectionChanged?.Invoke(true);
|
||||
|
||||
await SubscribeAsync(ws, ct);
|
||||
await Task.WhenAll(
|
||||
PingLoopAsync(ws, ct),
|
||||
ReceiveLoopAsync(ws, ct));
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
break; // 정상 종료
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "[WS] 연결 오류 — {Delay}초 후 재연결",
|
||||
_cfg.WsReconnectDelaySec);
|
||||
}
|
||||
|
||||
OnConnectionChanged?.Invoke(false);
|
||||
|
||||
if (!ct.IsCancellationRequested)
|
||||
await Task.Delay(_cfg.WsReconnectDelaySec * 1000, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
Log.Information("[WS] 연결 루프 종료");
|
||||
}
|
||||
|
||||
// ── 구독 요청 ─────────────────────────────────────────────────
|
||||
|
||||
private async Task SubscribeAsync(ClientWebSocket ws, CancellationToken ct)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
op = "subscribe",
|
||||
args = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
instType = "USDT-FUTURES",
|
||||
channel = _cfg.WsChannel,
|
||||
instId = _cfg.Symbol
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(payload);
|
||||
await SendAsync(ws, json, ct);
|
||||
Log.Information("[WS] 구독 요청: {Channel} {Symbol}",
|
||||
_cfg.WsChannel, _cfg.Symbol);
|
||||
}
|
||||
|
||||
// ── Ping 루프 ────────────────────────────────────────────────
|
||||
|
||||
private async Task PingLoopAsync(ClientWebSocket ws, CancellationToken ct)
|
||||
{
|
||||
while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_cfg.WsPingInterval * 1000, ct);
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
await SendAsync(ws, "ping", ct);
|
||||
Log.Debug("[WS] ping 전송");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 수신 루프 ────────────────────────────────────────────────
|
||||
|
||||
private async Task ReceiveLoopAsync(ClientWebSocket ws, CancellationToken ct)
|
||||
{
|
||||
var buf = new byte[65536];
|
||||
var sb = new StringBuilder();
|
||||
|
||||
while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
|
||||
{
|
||||
WebSocketReceiveResult result;
|
||||
try
|
||||
{
|
||||
result = await ws.ReceiveAsync(new ArraySegment<byte>(buf), ct);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
Log.Warning("[WS] 수신 오류: {Msg}", ex.Message);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
Log.Warning("[WS] 서버 Close 프레임 수신");
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append(Encoding.UTF8.GetString(buf, 0, result.Count));
|
||||
|
||||
if (!result.EndOfMessage) continue;
|
||||
|
||||
string msg = sb.ToString();
|
||||
sb.Clear();
|
||||
|
||||
if (msg == "pong") { Log.Debug("[WS] pong 수신"); continue; }
|
||||
ProcessMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 메시지 처리 ──────────────────────────────────────────────
|
||||
|
||||
private void ProcessMessage(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(msg);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// 이벤트 응답 (subscribe 확인 등) 무시
|
||||
if (root.TryGetProperty("event", out _))
|
||||
{
|
||||
Log.Debug("[WS] 이벤트: {Msg}", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("data", out var dataEl)) return;
|
||||
|
||||
foreach (var item in dataEl.EnumerateArray())
|
||||
{
|
||||
var arr = item.EnumerateArray().ToList();
|
||||
// Bitget candle 배열: [ts, o, h, l, c, baseVol, quoteVol, usdtVol, confirm]
|
||||
if (arr.Count < 6) continue;
|
||||
|
||||
// confirm: 마지막 원소 "1" = 완성 봉
|
||||
string confirm = arr[^1].GetString() ?? "0";
|
||||
if (confirm != "1")
|
||||
{
|
||||
Log.Debug("[WS] 미완성 봉 수신 (무시): {Ts}", arr[0].GetString());
|
||||
continue;
|
||||
}
|
||||
|
||||
long ts = long.Parse(arr[0].GetString()!);
|
||||
decimal o = decimal.Parse(arr[1].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal h = decimal.Parse(arr[2].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal l = decimal.Parse(arr[3].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal c = decimal.Parse(arr[4].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
decimal vol = decimal.Parse(arr[5].GetString()!,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
var candle = new CandleData
|
||||
{
|
||||
Time = DateTimeOffset.FromUnixTimeMilliseconds(ts),
|
||||
Open = o,
|
||||
High = h,
|
||||
Low = l,
|
||||
Close = c,
|
||||
Volume = vol
|
||||
};
|
||||
|
||||
Log.Information("[WS] ✅ 완성 봉: {Time} O={O} H={H} L={L} C={C} V={V}",
|
||||
candle.Time.ToString("yyyy-MM-dd HH:mm"),
|
||||
candle.Open, candle.High, candle.Low, candle.Close, candle.Volume);
|
||||
|
||||
OnCandle?.Invoke(candle);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug("[WS] 메시지 파싱 오류: {Err} | Raw={Msg}", ex.Message, msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 전송 헬퍼 ────────────────────────────────────────────────
|
||||
|
||||
private static async Task SendAsync(ClientWebSocket ws, string text, CancellationToken ct)
|
||||
{
|
||||
if (ws.State != WebSocketState.Open) return;
|
||||
byte[] data = Encoding.UTF8.GetBytes(text);
|
||||
await ws.SendAsync(new ArraySegment<byte>(data),
|
||||
WebSocketMessageType.Text, true, ct);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Buyer/Buyer.csproj
Normal file
34
Buyer/Buyer.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Buyer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- PineScript 엔진 프로젝트 참조 -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PineScript\PineScript.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- WebSocket 파일 제외 (폴링 방식으로 전환) -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="BitgetWebSocketClient.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- NuGet 패키지 -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="example.pine">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
148
Buyer/CandlePoller.cs
Normal file
148
Buyer/CandlePoller.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using PineScriptV6;
|
||||
using Serilog;
|
||||
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// REST API 1초 폴링 방식 캔들 감지기.
|
||||
///
|
||||
/// 동작 원리:
|
||||
/// - 1초마다 최신 N봉을 REST API로 조회한다.
|
||||
/// - 가장 최신 봉(현재 진행 중인 봉)의 timestamp를 추적한다.
|
||||
/// - timestamp가 이전과 달라지면 → 이전 봉이 완성된 것.
|
||||
/// - 완성된 봉 중 아직 처리하지 않은 봉을 OnCandle로 발행한다.
|
||||
///
|
||||
/// 시간 계산에 의존하지 않고 서버 응답 데이터 기준으로 판단하므로
|
||||
/// 서버 클럭 오차, API 지연, 네트워크 지연에 영향받지 않는다.
|
||||
/// </summary>
|
||||
public class CandlePoller
|
||||
{
|
||||
private readonly BitgetConfig _cfg;
|
||||
private readonly BitgetRestClient _rest;
|
||||
|
||||
/// <summary>새로운 완성 봉이 감지됐을 때 발생합니다.</summary>
|
||||
public event Action<CandleData>? OnCandle;
|
||||
|
||||
/// <summary>WarmUp 마지막 봉 시각 — 이 시각 이전 봉은 처리 건너뜀</summary>
|
||||
private DateTimeOffset _lastProcessedTime = DateTimeOffset.MinValue;
|
||||
|
||||
/// <summary>현재 진행 중인 봉의 시작 시각 (직전 폴링 기준)</summary>
|
||||
private DateTimeOffset _currentBarTime = DateTimeOffset.MinValue;
|
||||
|
||||
public CandlePoller(BitgetConfig cfg, BitgetRestClient rest)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_rest = rest;
|
||||
}
|
||||
|
||||
/// <summary>WarmUp 완료 후 호출 — 이미 처리된 봉 시각을 설정합니다.</summary>
|
||||
public void SetLastProcessedTime(DateTimeOffset time)
|
||||
{
|
||||
_lastProcessedTime = time;
|
||||
_currentBarTime = time; // 현재 진행 봉도 초기화
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 폴링 루프
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
Log.Information("[Poller] 시작 — {Interval}ms 간격 조회봉수={N}",
|
||||
_cfg.PollIntervalMs, _cfg.PollCandleCount);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
await PollOnceAsync();
|
||||
|
||||
// 정확히 PollIntervalMs 간격 유지 (API 호출 시간 보정)
|
||||
int elapsed = (int)sw.ElapsedMilliseconds;
|
||||
int wait = _cfg.PollIntervalMs - elapsed;
|
||||
if (wait > 0)
|
||||
{
|
||||
try { await Task.Delay(wait, ct); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
Log.Information("[Poller] 종료");
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 단일 폴링
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
private async Task PollOnceAsync()
|
||||
{
|
||||
List<CandleData> candles;
|
||||
try
|
||||
{
|
||||
candles = await _rest.GetCandlesAsync(_cfg.PollCandleCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("[Poller] REST 조회 실패 — 다음 주기에 재시도: {Err}", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (candles.Count == 0) return;
|
||||
|
||||
// candles는 오름차순 (과거→현재) 정렬됨
|
||||
// candles[^1] = 현재 진행 중인 봉 (아직 완성되지 않음)
|
||||
// candles[^2] 이하 = 완성된 봉들
|
||||
var latestBar = candles[^1];
|
||||
|
||||
// ── 최초 초기화 ────────────────────────────────────────────
|
||||
if (_currentBarTime == DateTimeOffset.MinValue)
|
||||
{
|
||||
_currentBarTime = latestBar.Time;
|
||||
Log.Information("[Poller] 현재 진행 봉 감지: {Time}",
|
||||
latestBar.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 봉 바뀜 감지 ─────────────────────────────────────────
|
||||
if (latestBar.Time != _currentBarTime)
|
||||
{
|
||||
Log.Information("[Poller] 봉 전환 감지: {Prev} → {Next}",
|
||||
_currentBarTime.ToLocalTime().ToString("HH:mm"),
|
||||
latestBar.Time.ToLocalTime().ToString("HH:mm"));
|
||||
|
||||
_currentBarTime = latestBar.Time;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 진행 중인 봉 유지 중 — 매 분마다 한 번씩 상태 로그 (디버그용)
|
||||
if (DateTimeOffset.UtcNow.Second == 0)
|
||||
{
|
||||
Log.Debug("[Poller] 진행 중: 현재봉={Current} 마지막처리={Last} 서버최신={Latest}",
|
||||
_currentBarTime.ToLocalTime().ToString("HH:mm"),
|
||||
_lastProcessedTime.ToLocalTime().ToString("HH:mm"),
|
||||
latestBar.Time.ToLocalTime().ToString("HH:mm"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 완성 봉 처리 ─────────────────────────────────────────
|
||||
// 현재 진행 중인 봉(latestBar) 제외, 나머지 중 처리 안 된 봉 발행
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
// 현재 진행 중인 봉 스킵
|
||||
if (candle.Time >= _currentBarTime) continue;
|
||||
|
||||
// 이미 처리된 봉 스킵
|
||||
if (candle.Time <= _lastProcessedTime) continue;
|
||||
|
||||
Log.Information("[Poller] ✅ 완성 봉: {Time} O={O} H={H} L={L} C={C}",
|
||||
candle.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
|
||||
candle.Open, candle.High, candle.Low, candle.Close);
|
||||
|
||||
_lastProcessedTime = candle.Time;
|
||||
|
||||
try { OnCandle?.Invoke(candle); }
|
||||
catch (Exception ex) { Log.Error(ex, "[Poller] OnCandle 핸들러 오류"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Buyer/Dockerfile
Normal file
28
Buyer/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# 디버그 컨테이너를 사용자 지정하는 방법과 Visual Studio 이 Dockerfile을 사용하여 더 빠른 디버깅을 위해 이미지를 빌드하는 방법을 알아보려면 https://aka.ms/customizecontainer를 참조하세요.
|
||||
|
||||
# 이 스테이지는 VS에서 빠른 모드로 실행할 때 사용됩니다(디버그 구성의 기본값).
|
||||
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
# 이 스테이지는 서비스 프로젝트를 빌드하는 데 사용됩니다.
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Buyer/Buyer.csproj", "Buyer/"]
|
||||
RUN dotnet restore "./Buyer/Buyer.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Buyer"
|
||||
RUN dotnet build "./Buyer.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# 이 스테이지는 최종 스테이지에 복사할 서비스 프로젝트를 게시하는 데 사용됩니다.
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Buyer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# 이 스테이지는 프로덕션에서 사용되거나 VS에서 일반 모드로 실행할 때 사용됩니다(디버그 구성을 사용하지 않는 경우 기본값).
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Buyer.dll"]
|
||||
85
Buyer/Program.cs
Normal file
85
Buyer/Program.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Buyer;
|
||||
using Serilog;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 로거 초기화
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.WriteTo.Async(a => a.Console(
|
||||
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"))
|
||||
.WriteTo.Async(a => a.File(
|
||||
path: "logs/buyer-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"))
|
||||
.CreateLogger();
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 설정 — 변수를 여기서 관리
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
var config = new BitgetConfig
|
||||
{
|
||||
// ── 종목 ──────────────────────────────────────────────────────
|
||||
Symbol = "XRPUSDT",
|
||||
ProductType = "USDT-FUTURES", // USDT-FUTURES | COIN-FUTURES
|
||||
Granularity = BitgetGranularity.Min30, // BitgetGranularity 상수 사용
|
||||
|
||||
// ── WarmUp ────────────────────────────────────────────────────
|
||||
// Pine Script 지표 최대 윈도우(sma72 등)보다 충분히 크게 설정
|
||||
WarmUpBars = 500,
|
||||
|
||||
// ── REST API ──────────────────────────────────────────────────
|
||||
RestBaseUrl = "https://api.bitget.com",
|
||||
RestTimeoutSec = 30,
|
||||
|
||||
// ── 폴링 ──────────────────────────────────────────────────────
|
||||
// REST API 호출 간격 (밀리초). 1000 = 1초마다 호출.
|
||||
// 봉 완성 시점을 시간 계산 없이 서버 응답 기준으로 감지합니다.
|
||||
PollIntervalMs = 1000,
|
||||
|
||||
// 폴링 1회에 조회할 봉 수 (3이면 충분, 누락 방지)
|
||||
PollCandleCount = 3,
|
||||
|
||||
// ── Pine Script 경로 ──────────────────────────────────────────
|
||||
// 실행 환경에 맞게 절대 경로 또는 상대 경로로 수정하세요.
|
||||
PineScriptPath = @"example.pine"
|
||||
};
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// Ctrl+C 핸들러
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
Log.Information("종료 요청 (Ctrl+C)...");
|
||||
cts.Cancel();
|
||||
};
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 실행
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
Log.Information("══════════════════════════════════════════");
|
||||
Log.Information(" Buyer 실시간 트레이더 시작 (REST 폴링)");
|
||||
Log.Information(" 심볼: {Symbol}", config.Symbol);
|
||||
Log.Information(" 타임프레임: {Gran}", config.Granularity);
|
||||
Log.Information(" 상품: {Product}", config.ProductType);
|
||||
Log.Information(" WarmUp 봉수: {N}", config.WarmUpBars);
|
||||
Log.Information(" 폴링 간격: {D}ms", config.PollIntervalMs);
|
||||
Log.Information(" 스크립트: {Path}", config.PineScriptPath);
|
||||
Log.Information("══════════════════════════════════════════");
|
||||
|
||||
try
|
||||
{
|
||||
using var trader = new RealtimeTrader(config);
|
||||
await trader.RunAsync(cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "치명적 오류 발생");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.Information("프로그램 종료");
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
10
Buyer/Properties/launchSettings.json
Normal file
10
Buyer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Buyer": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
102
Buyer/RealtimeTrader.cs
Normal file
102
Buyer/RealtimeTrader.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using PineScriptV6;
|
||||
using Serilog;
|
||||
|
||||
namespace Buyer
|
||||
{
|
||||
/// <summary>
|
||||
/// 실시간 트레이더.
|
||||
/// 1) REST API로 과거 봉을 받아 WarmUp (지표 초기화)
|
||||
/// 2) CandlePoller로 봉 완성 시각마다 REST 폴링
|
||||
/// 3) 새 완성 봉 → ProcessCandle → 신호 발생 시 로그 출력
|
||||
/// </summary>
|
||||
public class RealtimeTrader : IDisposable
|
||||
{
|
||||
private readonly BitgetConfig _cfg;
|
||||
private readonly PineScriptExecutor _executor;
|
||||
private readonly BitgetRestClient _rest;
|
||||
private readonly CandlePoller _poller;
|
||||
|
||||
public RealtimeTrader(BitgetConfig cfg)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_executor = new PineScriptExecutor(cfg.PineScriptPath);
|
||||
_rest = new BitgetRestClient(cfg);
|
||||
_poller = new CandlePoller(cfg, _rest);
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct = default)
|
||||
{
|
||||
// ── Step 1: 과거 봉으로 WarmUp ───────────────────────────
|
||||
Log.Information("[Trader] ═══ WarmUp 시작 ═══");
|
||||
Log.Information("[Trader] 심볼={Symbol} 타임프레임={Gran} 봉수={N}",
|
||||
_cfg.Symbol, _cfg.Granularity, _cfg.WarmUpBars);
|
||||
|
||||
var warmupCandles = await _rest.GetCandlesAsync(_cfg.WarmUpBars);
|
||||
|
||||
if (warmupCandles.Count == 0)
|
||||
{
|
||||
Log.Error("[Trader] WarmUp 데이터 없음 — 종료");
|
||||
return;
|
||||
}
|
||||
|
||||
_executor.WarmUp(warmupCandles);
|
||||
|
||||
// WarmUp 마지막 봉 시각을 폴러에 전달 → 중복 처리 방지
|
||||
var lastWarmupTime = warmupCandles[^1].Time;
|
||||
_poller.SetLastProcessedTime(lastWarmupTime);
|
||||
|
||||
Log.Information("[Trader] ═══ WarmUp 완료 ═══");
|
||||
Log.Information("[Trader] 마지막 WarmUp 봉: {Time}",
|
||||
lastWarmupTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm"));
|
||||
Log.Information("[Trader] 실시간 폴링 모드 진입 (Ctrl+C로 종료)");
|
||||
|
||||
// ── Step 2: 신호 델리게이트 등록 ────────────────────────
|
||||
TradingSignal.OnBuy = price => LogOrder("BUY 롱 진입", price);
|
||||
TradingSignal.OnSell = price => LogOrder("SELL 롱 청산", price);
|
||||
TradingSignal.OnShort = price => LogOrder("SHORT 숏 진입", price);
|
||||
TradingSignal.OnCover = price => LogOrder("COVER 숏 청산", price);
|
||||
|
||||
// ── Step 3: 폴러 콜백 등록 ──────────────────────────────
|
||||
_poller.OnCandle += candle =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var signals = _executor.ProcessCandle(candle);
|
||||
|
||||
Log.Information("[Trader] ProcessCandle 완료: {Time} 신호={N}개",
|
||||
candle.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
|
||||
signals.Count);
|
||||
// 신호는 TradingSignal 델리게이트를 통해 자동으로 LogOrder 호출됨
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "[Trader] ProcessCandle 오류: {Time}",
|
||||
candle.Time.ToString("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
};
|
||||
|
||||
// ── Step 4: 폴링 루프 실행 (블로킹) ─────────────────────
|
||||
await _poller.RunAsync(ct);
|
||||
|
||||
Log.Information("[Trader] 종료");
|
||||
}
|
||||
|
||||
// ── 주문 로그 ────────────────────────────────────────────────
|
||||
|
||||
private void LogOrder(string action, decimal price)
|
||||
{
|
||||
string bar = new string('═', 42);
|
||||
Log.Information(bar);
|
||||
Log.Information(" ▶ {Action} | {Symbol} | {Price:F5}",
|
||||
action, _cfg.Symbol, price);
|
||||
Log.Information(" 시각: {Time}",
|
||||
DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
Log.Information(bar);
|
||||
|
||||
// TODO: 실제 주문 코드는 여기에 추가
|
||||
// await _orderClient.PlaceMarketOrderAsync(_cfg.Symbol, side, qty);
|
||||
}
|
||||
|
||||
public void Dispose() => _rest.Dispose();
|
||||
}
|
||||
}
|
||||
211
Buyer/example.pine
Normal file
211
Buyer/example.pine
Normal file
@@ -0,0 +1,211 @@
|
||||
//@version=5
|
||||
indicator("LJH - 테스트 (Table 제거 버전)", overlay=true)
|
||||
|
||||
// === high close, low close ===
|
||||
highClose = ta.highest(high, 12)
|
||||
lowClose = ta.lowest(low, 4)
|
||||
|
||||
// === SMA4 Bands ===
|
||||
sma4 = (close + close[1] + close[2] + close[3]) / 4
|
||||
prevSma4 = (close[4] + close[1] + close[2] + close[3]) / 4
|
||||
prev2Sma4 = (close[4] + close[5] + close[2] + close[3]) / 4
|
||||
prev3Sma4 = (close[4] + close[5] + close[6] + close[3]) / 4
|
||||
|
||||
sma72 = ta.sma(close, 72)
|
||||
sma24 = ta.sma(close, 24)
|
||||
prevSma72 = ta.sma(close, 72)
|
||||
prev2Sma72 = ta.sma(close, 72)
|
||||
prevSma24 = ta.sma(close, 24)
|
||||
prev2Sma24 = ta.sma(close, 24)
|
||||
|
||||
sma4High = ta.highest(sma4, 12)
|
||||
sma4Middle = ta.sma(sma4, 12)
|
||||
sma4Low = ta.lowest(sma4, 12)
|
||||
|
||||
prevSma4High = ta.highest(prevSma4, 12)
|
||||
prevSma4Middle = ta.sma(prevSma4, 12)
|
||||
prevSma4Low = ta.lowest(prevSma4, 12)
|
||||
|
||||
prev2Sma4High = ta.highest(prev2Sma4, 12)
|
||||
prev2Sma4Middle = ta.sma(prev2Sma4, 12)
|
||||
prev2Sma4Low = ta.lowest(prev2Sma4, 12)
|
||||
|
||||
prev3Sma4High = ta.highest(prev3Sma4, 12)
|
||||
prev3Sma4Middle = ta.sma(prev3Sma4, 12)
|
||||
prev3Sma4Low = ta.lowest(prev3Sma4, 12)
|
||||
|
||||
// === 상태 추적 ===
|
||||
var bool inLong = false
|
||||
var bool inShort = false
|
||||
|
||||
// === 새 조건을 위한 상태 변수 ===
|
||||
var int sma4LongTouchBar = na
|
||||
var int sma4ShortTouchBar = na
|
||||
|
||||
// sma4가 sma4Low에 닿은 시점 저장
|
||||
if (sma4 == sma4Low and prevSma4 >= prevSma4Low)
|
||||
sma4LongTouchBar := bar_index
|
||||
|
||||
// sma4가 sma4High에 닿은 시점 저장
|
||||
if (sma4 == sma4High and prevSma4 <= prevSma4High)
|
||||
sma4ShortTouchBar := bar_index
|
||||
|
||||
//전저가 전고가 처리
|
||||
maLength = input.int(4, title="이동평균 기간")
|
||||
ma = ta.sma(close, maLength)
|
||||
|
||||
// 이동평균의 방향 비교
|
||||
maPrev = ma
|
||||
maPrev2 = ma
|
||||
|
||||
// 꼭지점 조건: 상승에서 하락으로 전환 (고점)
|
||||
isTop = maPrev2 < maPrev and maPrev > ma
|
||||
// 꼭지점 조건: 하락에서 상승으로 전환 (저점)
|
||||
isBottom = maPrev2 > maPrev and maPrev < ma
|
||||
|
||||
var float[] topCloses = array.new_float()
|
||||
var float[] bottomCloses = array.new_float()
|
||||
|
||||
if isTop
|
||||
array.push(topCloses, maPrev)
|
||||
if isBottom
|
||||
array.push(bottomCloses, maPrev)
|
||||
|
||||
// === 구름떼 (Ichimoku Cloud) 추가 ===
|
||||
conversionPeriods = 9
|
||||
basePeriods = 26
|
||||
laggingSpan2Periods = 52
|
||||
displacement = 26
|
||||
|
||||
conversionLine = (ta.highest(high, conversionPeriods) + ta.lowest(low, conversionPeriods)) / 2
|
||||
baseLine = (ta.highest(high, basePeriods) + ta.lowest(low, basePeriods)) / 2
|
||||
leadingSpan1 = (conversionLine + baseLine) / 2
|
||||
leadingSpan2 = (ta.highest(high, laggingSpan2Periods) + ta.lowest(low, laggingSpan2Periods)) / 2
|
||||
|
||||
cloudTopNow = math.max(leadingSpan1[displacement], leadingSpan2[displacement])
|
||||
cloudBottomNow = math.min(leadingSpan1[displacement], leadingSpan2[displacement])
|
||||
|
||||
cloudTopAfter = math.max(leadingSpan1, leadingSpan2)
|
||||
cloudBottomAfter = math.min(leadingSpan1, leadingSpan2)
|
||||
|
||||
isSma4BelowCloud = low < cloudBottomNow
|
||||
isSma4AboveCloud = high > cloudTopNow
|
||||
|
||||
var bool hasEnteredShortAfterCloudBreak = false
|
||||
|
||||
// 매 봉마다 상태 업데이트
|
||||
if not isSma4BelowCloud
|
||||
hasEnteredShortAfterCloudBreak := false
|
||||
|
||||
// 상태 변수
|
||||
var bool allowNewShortEntry = true
|
||||
var bool wasBelowCloud = false
|
||||
|
||||
// 구름 위/아래 상태 추적
|
||||
if sma4 < cloudTopNow and not wasBelowCloud
|
||||
allowNewShortEntry := true
|
||||
wasBelowCloud := true
|
||||
|
||||
if not (sma4 < cloudTopNow)
|
||||
allowNewShortEntry := false
|
||||
wasBelowCloud := false
|
||||
|
||||
// === 횡보 구간 감지 ===
|
||||
lengthWindow = 12
|
||||
rangePeriod = 240
|
||||
shortLengthWindow = 4
|
||||
shortRangePeriod = 40
|
||||
|
||||
// Long 기준 비율 계산식
|
||||
longRatio = (high - ta.lowest(low, lengthWindow)) / ta.lowest(low, lengthWindow) * 100
|
||||
// Short 기준 비율 계산식
|
||||
shortRatio = (ta.highest(high, shortLengthWindow) - low) / ta.highest(high, shortLengthWindow) * 100
|
||||
|
||||
// 평균 계산
|
||||
rangeThresholdLongPercent = ta.sma(longRatio, rangePeriod)
|
||||
rangeThresholdShortPercent = ta.sma(shortRatio, shortRangePeriod)
|
||||
|
||||
rangeLookback = 12
|
||||
highestHigh = ta.highest(high, rangeLookback)
|
||||
lowestLow = ta.lowest(low, rangeLookback)
|
||||
|
||||
priceRangeLong = high - lowestLow
|
||||
priceRangeShort = highestHigh - low
|
||||
|
||||
isSidewaysLong = priceRangeLong > (lowestLow * rangeThresholdLongPercent * 1 / 100) and sma4 > cloudTopNow
|
||||
isSidewaysShort = priceRangeShort > (highestHigh * (rangeThresholdShortPercent) / 100) and sma4 < cloudTopNow
|
||||
|
||||
isLongRecentTouch = not na(sma4LongTouchBar) and (bar_index - sma4LongTouchBar <= 10)
|
||||
isShortRecentTouch = not na(sma4ShortTouchBar) and (bar_index - sma4ShortTouchBar <= 5)
|
||||
|
||||
// === 진입/청산 조건 ===
|
||||
rawNewLong = highClose == high and open < close and isLongRecentTouch and isSidewaysLong and prevSma4 <= sma4 and sma4High == sma4 and (prev2Sma4Middle - prevSma4Middle) > (prevSma4Middle - sma4Middle) and sma4Middle > sma72 and sma4 > cloudTopNow
|
||||
rawLongSell = (sma4Middle < sma72 and prevSma4Middle > prevSma72) or (sma4 == sma4Low)
|
||||
|
||||
rawNewShort = (sma72 < cloudTopNow and prevSma4Middle > prevSma24 and sma4Middle > cloudBottomNow and sma4 < sma72 and sma4High > sma72 and sma24 > sma4Middle and sma4 == sma4Low and isSidewaysShort and prevSma4 > sma4 and sma4 < cloudBottomNow) or (sma4 < sma72 and sma4High > sma72 and sma24 < sma4Middle and sma4 == sma4Low and isSidewaysShort and open > close and sma4 < cloudBottomNow and sma4Middle < cloudTopNow)
|
||||
rawShortSell = (sma4High <= sma4 and sma4High < sma72) or sma4 > sma72 or (prevSma4Middle > prevSma4 and sma4Middle < sma4) or (isSidewaysShort and prevSma4Low == prevSma4 and sma4Low < sma4)
|
||||
|
||||
// === 시그널 변수 ===
|
||||
newLongSignal = false
|
||||
longSellSignal = false
|
||||
newShortSignal = false
|
||||
shortSellSignal = false
|
||||
|
||||
// === 롱 진입/청산 로직 ===
|
||||
if not inLong and rawNewLong
|
||||
newLongSignal := true
|
||||
inLong := true
|
||||
|
||||
if inLong and rawLongSell
|
||||
longSellSignal := true
|
||||
inLong := false
|
||||
|
||||
// === 쇼트 진입/청산 로직 ===
|
||||
if not inShort and rawNewShort
|
||||
newShortSignal := true
|
||||
inShort := true
|
||||
|
||||
if inShort and rawShortSell
|
||||
shortSellSignal := true
|
||||
inShort := false
|
||||
|
||||
// === 수익률 계산 및 라벨 표시용 변수 ===
|
||||
var float longEntryPrice = na
|
||||
var float shortEntryPrice = na
|
||||
|
||||
if newLongSignal
|
||||
longEntryPrice := close
|
||||
|
||||
if longSellSignal and not na(longEntryPrice)
|
||||
lastLongProfit = (close - longEntryPrice) / longEntryPrice * 100
|
||||
label.new(bar_index, high, text="롱: " + str.tostring(lastLongProfit, "#.##") + "%", color=color.green, textcolor=color.white, size=size.small)
|
||||
longEntryPrice := na
|
||||
|
||||
if newShortSignal
|
||||
shortEntryPrice := close
|
||||
|
||||
if shortSellSignal and not na(shortEntryPrice)
|
||||
lastShortProfit = (shortEntryPrice - close) / shortEntryPrice * 100
|
||||
label.new(bar_index, low, text="숏: " + str.tostring(lastShortProfit, "#.##") + "%", color=color.red, textcolor=color.white, size=size.small, style=label.style_label_up)
|
||||
shortEntryPrice := na
|
||||
|
||||
// === 시각화 (Plot) ===
|
||||
bgcolor(isSidewaysLong ? color.new(#458558, 85) : na, title="Sideways Long Background")
|
||||
bgcolor(isSidewaysShort ? color.new(#964c4c, 85) : na, title="Sideways Short Background")
|
||||
|
||||
plot(sma4High, color=color.green, linewidth=2, title="SMA(4) High")
|
||||
plot(sma4Low, color=color.red, linewidth=2, title="SMA(4) Low")
|
||||
plot(sma4Middle, color=color.rgb(130, 98, 247), linewidth=2, title="SMA(4) Middle")
|
||||
plot(sma72, color=color.rgb(0, 0, 0), linewidth=2, title="SMA(72)")
|
||||
plot(sma24, color=color.rgb(0, 4, 255), linewidth=2, title="SMA(24)")
|
||||
|
||||
plotshape(newLongSignal, title="Long Entry", location=location.belowbar, color=color.green, style=shape.labelup, text="Long")
|
||||
plotshape(longSellSignal, title="Long Exit", location=location.abovebar, color=color.red, style=shape.labeldown, text="Long Sell")
|
||||
|
||||
plotshape(newShortSignal, title="Short Entry", location=location.abovebar, color=color.rgb(153, 180, 91), style=shape.labeldown, text="Short")
|
||||
plotshape(shortSellSignal, title="Short Exit", location=location.belowbar, color=color.rgb(165, 62, 122), style=shape.labelup, text="Short Sell")
|
||||
|
||||
// 일목균형표 구름대 시각화
|
||||
p1 = plot(leadingSpan1, offset = displacement, title="Leading Span 1", color=color.new(color.green, 30))
|
||||
p2 = plot(leadingSpan2, offset = displacement, title="Leading Span 2", color=color.new(color.red, 30))
|
||||
fill(p1, p2, color = leadingSpan1 > leadingSpan2 ? color.new(color.green, 85) : color.new(color.red, 85), title="Ichimoku Cloud")
|
||||
31
PineScript.sln
Normal file
31
PineScript.sln
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36429.23
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PineScript", "PineScript\PineScript.csproj", "{17E3AD9A-3FE0-498F-894C-4EE756073F85}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Buyer", "Buyer\Buyer.csproj", "{AAC4D4C3-7F70-4134-871C-B6818BBCF113}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{17E3AD9A-3FE0-498F-894C-4EE756073F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{17E3AD9A-3FE0-498F-894C-4EE756073F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{17E3AD9A-3FE0-498F-894C-4EE756073F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{17E3AD9A-3FE0-498F-894C-4EE756073F85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AAC4D4C3-7F70-4134-871C-B6818BBCF113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AAC4D4C3-7F70-4134-871C-B6818BBCF113}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAC4D4C3-7F70-4134-871C-B6818BBCF113}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AAC4D4C3-7F70-4134-871C-B6818BBCF113}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {9F10EEAA-41DF-4E45-8B55-88661F8B8856}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
22878
PineScript/BITGET_XRPUSDT, 30.csv
Normal file
22878
PineScript/BITGET_XRPUSDT, 30.csv
Normal file
File diff suppressed because it is too large
Load Diff
19
PineScript/CandleData.cs
Normal file
19
PineScript/CandleData.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// OHLCV 봉 데이터 모델
|
||||
/// CSV 컬럼: time, open, high, low, close, Volume
|
||||
/// </summary>
|
||||
public class CandleData
|
||||
{
|
||||
public DateTimeOffset Time { get; set; }
|
||||
public decimal Open { get; set; }
|
||||
public decimal High { get; set; }
|
||||
public decimal Low { get; set; }
|
||||
public decimal Close { get; set; }
|
||||
public decimal Volume { get; set; }
|
||||
|
||||
public override string ToString() =>
|
||||
$"{Time:yyyy-MM-dd HH:mm} | O={Open} H={High} L={Low} C={Close} V={Volume}";
|
||||
}
|
||||
}
|
||||
89
PineScript/CsvLoader.cs
Normal file
89
PineScript/CsvLoader.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Serilog;
|
||||
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// OHLCV CSV 파일 로더
|
||||
/// 지원 형식: time,open,high,low,close,Volume
|
||||
/// 시간 형식: 2025-01-01T09:00:00+09:00 (ISO 8601) 또는 UTC 무표기
|
||||
/// 타임존 미표기 시 UTC로 간주 → KST(+09:00) 변환 적용
|
||||
/// </summary>
|
||||
public static class CsvLoader
|
||||
{
|
||||
public static List<CandleData> Load(string filePath)
|
||||
{
|
||||
var result = new List<CandleData>();
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Log.Error("[CsvLoader] 파일을 찾을 수 없습니다: {Path}", filePath);
|
||||
return result;
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(filePath);
|
||||
|
||||
// 헤더 행 스킵 (첫 번째 줄)
|
||||
int errorCount = 0;
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
string line = lines[i].Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
CandleData candle = ParseLine(line);
|
||||
result.Add(candle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorCount++;
|
||||
Log.Warning("[CsvLoader] {Line}번째 줄 파싱 실패: {Error} | 내용: {Content}",
|
||||
i + 1, ex.Message, line);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Information("[CsvLoader] 로드 완료 | 총 {Total}개 봉 | 실패 {Error}건 | 파일: {Path}",
|
||||
result.Count, errorCount, filePath);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CandleData ParseLine(string line)
|
||||
{
|
||||
// CSV는 콤마 구분 — 시간 값에 콤마가 없으므로 단순 Split 사용
|
||||
string[] cols = line.Split(',');
|
||||
|
||||
if (cols.Length < 6)
|
||||
throw new FormatException($"컬럼 수 부족: {cols.Length}개 (최소 6개 필요)");
|
||||
|
||||
// ── 시간 파싱 ───────────────────────────────────────────────────
|
||||
// 타임존 표기가 없으면 UTC로 간주하고 KST(+09:00)로 변환.
|
||||
// 이미 +09:00 이 붙어 있으면 ToOffset 이 같은 시각을 KST 표기로 유지.
|
||||
var rawTime = cols[0].Trim();
|
||||
var kst = TimeSpan.FromHours(9);
|
||||
DateTimeOffset parsedTime;
|
||||
if (DateTimeOffset.TryParse(rawTime, out var dto))
|
||||
{
|
||||
// 오프셋이 0(UTC)이거나 미표기(Kind==Unspecified → offset 0으로 파싱됨)이면 UTC 간주 후 KST 변환
|
||||
parsedTime = dto.Offset == TimeSpan.Zero
|
||||
? new DateTimeOffset(dto.DateTime, TimeSpan.Zero).ToOffset(kst) // UTC → KST 변환
|
||||
: dto.ToOffset(kst); // 다른 오프셋이면 KST로 변환
|
||||
}
|
||||
else
|
||||
{
|
||||
// 파싱 실패 시 예외 발생 (상위 catch에서 처리)
|
||||
throw new FormatException($"시간 파싱 실패: {rawTime}");
|
||||
}
|
||||
|
||||
return new CandleData
|
||||
{
|
||||
Time = parsedTime,
|
||||
Open = decimal.Parse(cols[1].Trim(), System.Globalization.CultureInfo.InvariantCulture),
|
||||
High = decimal.Parse(cols[2].Trim(), System.Globalization.CultureInfo.InvariantCulture),
|
||||
Low = decimal.Parse(cols[3].Trim(), System.Globalization.CultureInfo.InvariantCulture),
|
||||
Close = decimal.Parse(cols[4].Trim(), System.Globalization.CultureInfo.InvariantCulture),
|
||||
Volume = decimal.Parse(cols[5].Trim(), System.Globalization.CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
131
PineScript/DependencyAnalyzer.cs
Normal file
131
PineScript/DependencyAnalyzer.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// Pine Script 문(Statement) 목록을 정적 분석하여
|
||||
/// Pass1(지표 사전계산) / Pass2(상태머신)로 자동 분류합니다.
|
||||
///
|
||||
/// Pass1 조건: 순수 AssignStmt이며, 참조 변수가 모두
|
||||
/// OHLCV 또는 이전에 분류된 다른 Pass1 변수여야 합니다.
|
||||
/// Pass2 조건: var 선언, := 재할당, if 블록, ExprStmt(side-effect),
|
||||
/// 또는 상태 변수를 참조하는 AssignStmt.
|
||||
///
|
||||
/// Pine Script 문은 이미 위상 정렬된 순서로 작성되므로
|
||||
/// 별도의 위상 정렬 없이 선형 스캔으로 분류할 수 있습니다.
|
||||
/// </summary>
|
||||
public static class DependencyAnalyzer
|
||||
{
|
||||
// ── OHLCV 및 내장 변수 (항상 Pass1에서 접근 가능) ─────────────
|
||||
private static readonly HashSet<string> _builtins =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"close", "open", "high", "low", "volume",
|
||||
"bar_index", "time"
|
||||
};
|
||||
|
||||
// ── 표현식 내 무시할 키워드/리터럴 ───────────────────────────
|
||||
private static readonly HashSet<string> _keywords =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"true", "false", "na", "and", "or", "not"
|
||||
};
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 공개 API
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 분석 결과를 담는 레코드.
|
||||
/// Pass1Stmts → 증분 지표 계산 (매 봉, 순서 유지)
|
||||
/// Pass2Stmts → 상태 머신 (매 봉, 순서 유지)
|
||||
/// </summary>
|
||||
public record ExecutionPlan(
|
||||
List<PineStmt> Pass1Stmts,
|
||||
List<PineStmt> Pass2Stmts,
|
||||
HashSet<string> Pass1Vars,
|
||||
HashSet<string> StateVars
|
||||
);
|
||||
|
||||
public static ExecutionPlan Analyze(List<PineStmt> stmts)
|
||||
{
|
||||
// ── Step 1: 상태 변수 수집 ──────────────────────────────────
|
||||
var stateVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
CollectStateVars(stmts, stateVars);
|
||||
|
||||
// ── Step 2: 순서대로 분류 ─────────────────────────────────
|
||||
var pass1Vars = new HashSet<string>(_builtins, StringComparer.OrdinalIgnoreCase);
|
||||
var pass1 = new List<PineStmt>();
|
||||
var pass2 = new List<PineStmt>();
|
||||
|
||||
foreach (var stmt in stmts)
|
||||
{
|
||||
if (TryPass1(stmt, stateVars, pass1Vars, out string? varName))
|
||||
{
|
||||
pass1.Add(stmt);
|
||||
if (varName != null) pass1Vars.Add(varName);
|
||||
}
|
||||
else
|
||||
{
|
||||
pass2.Add(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
return new ExecutionPlan(pass1, pass2, pass1Vars, stateVars);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 내부 구현
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>모든 var 선언과 := 재할당 변수를 재귀적으로 수집합니다.</summary>
|
||||
private static void CollectStateVars(List<PineStmt> stmts, HashSet<string> result)
|
||||
{
|
||||
foreach (var stmt in stmts)
|
||||
{
|
||||
switch (stmt)
|
||||
{
|
||||
case VarDeclStmt vd:
|
||||
result.Add(vd.Name);
|
||||
break;
|
||||
case ReassignStmt r:
|
||||
result.Add(r.Name);
|
||||
break;
|
||||
case IfStmt ifs:
|
||||
CollectStateVars(ifs.ThenBody, result);
|
||||
if (ifs.ElseBody != null)
|
||||
CollectStateVars(ifs.ElseBody, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// stmt가 Pass1 조건을 만족하면 true 반환.
|
||||
/// varName에는 이 문이 생성하는 변수명이 들어옵니다 (없으면 null).
|
||||
/// </summary>
|
||||
private static bool TryPass1(
|
||||
PineStmt stmt,
|
||||
HashSet<string> stateVars,
|
||||
HashSet<string> pass1Vars,
|
||||
out string? varName)
|
||||
{
|
||||
varName = null;
|
||||
|
||||
// Pass1 대상은 순수 대입문(AssignStmt)만
|
||||
if (stmt is not AssignStmt a) return false;
|
||||
|
||||
// 변수 자체가 상태 변수이면 Pass2
|
||||
if (stateVars.Contains(a.Name)) return false;
|
||||
|
||||
// 표현식 내 참조 변수가 모두 Pass1 범위 안이어야 함
|
||||
var refs = PineEvaluator.GetReferencedVars(a.Expr);
|
||||
foreach (var r in refs)
|
||||
{
|
||||
if (_keywords.Contains(r)) continue;
|
||||
if (!pass1Vars.Contains(r)) return false; // 상태변수 or 미분류 → Pass2
|
||||
}
|
||||
|
||||
varName = a.Name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
PineScript/Dockerfile
Normal file
28
PineScript/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# 디버그 컨테이너를 사용자 지정하는 방법과 Visual Studio 이 Dockerfile을 사용하여 더 빠른 디버깅을 위해 이미지를 빌드하는 방법을 알아보려면 https://aka.ms/customizecontainer를 참조하세요.
|
||||
|
||||
# 이 스테이지는 VS에서 빠른 모드로 실행할 때 사용됩니다(디버그 구성의 기본값).
|
||||
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
# 이 스테이지는 서비스 프로젝트를 빌드하는 데 사용됩니다.
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["PineScript/PineScript.csproj", "PineScript/"]
|
||||
RUN dotnet restore "./PineScript/PineScript.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/PineScript"
|
||||
RUN dotnet build "./PineScript.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# 이 스테이지는 최종 스테이지에 복사할 서비스 프로젝트를 게시하는 데 사용됩니다.
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./PineScript.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# 이 스테이지는 프로덕션에서 사용되거나 VS에서 일반 모드로 실행할 때 사용됩니다(디버그 구성을 사용하지 않는 경우 기본값).
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "PineScript.dll"]
|
||||
216
PineScript/IndicatorEngine.cs
Normal file
216
PineScript/IndicatorEngine.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// ta.sma / ta.highest / ta.lowest 의 증분(O(1)) 구현체를 관리합니다.
|
||||
/// 매 봉 최초 호출 시 전체 히스토리로 초기화되고,
|
||||
/// 이후 봉은 O(1) 증분 업데이트로 처리됩니다.
|
||||
/// Reset() 은 새 ExecuteOnCandles / WarmUp 시작 시 반드시 호출해야 합니다.
|
||||
/// </summary>
|
||||
public static class IndicatorEngine
|
||||
{
|
||||
// ValueTuple 키 — 문자열 포맷 할당($"{src}:{len}") 완전 제거
|
||||
private static readonly Dictionary<(string, int), IncrementalSma> _smaMap = new();
|
||||
private static readonly Dictionary<(string, int), IncrementalHighest> _highMap = new();
|
||||
private static readonly Dictionary<(string, int), IncrementalLowest> _lowMap = new();
|
||||
|
||||
/// <summary>상태 전체 초기화 (재실행 또는 실시간 모드 전환 시 호출)</summary>
|
||||
public static void Reset()
|
||||
{
|
||||
_smaMap.Clear();
|
||||
_highMap.Clear();
|
||||
_lowMap.Clear();
|
||||
}
|
||||
|
||||
// ── SMA ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// ta.sma(src, length) 의 증분 값을 반환합니다.
|
||||
/// 히스토리 부족 시 null 반환 (초기 warmup 구간).
|
||||
/// </summary>
|
||||
public static double? QuerySma(string src, int length)
|
||||
{
|
||||
var key = (src, length);
|
||||
if (!_smaMap.TryGetValue(key, out var ind))
|
||||
_smaMap[key] = ind = new IncrementalSma(src, length);
|
||||
return ind.GetValue();
|
||||
}
|
||||
|
||||
// ── HIGHEST ──────────────────────────────────────────────────────
|
||||
|
||||
public static double? QueryHighest(string src, int length)
|
||||
{
|
||||
var key = (src, length);
|
||||
if (!_highMap.TryGetValue(key, out var ind))
|
||||
_highMap[key] = ind = new IncrementalHighest(src, length);
|
||||
return ind.GetValue();
|
||||
}
|
||||
|
||||
// ── LOWEST ───────────────────────────────────────────────────────
|
||||
|
||||
public static double? QueryLowest(string src, int length)
|
||||
{
|
||||
var key = (src, length);
|
||||
if (!_lowMap.TryGetValue(key, out var ind))
|
||||
_lowMap[key] = ind = new IncrementalLowest(src, length);
|
||||
return ind.GetValue();
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 증분 SMA — 누적합 O(1)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class IncrementalSma
|
||||
{
|
||||
private readonly string _src;
|
||||
private readonly int _len;
|
||||
private double _sum;
|
||||
private int _lastBar = -1;
|
||||
private bool _ready;
|
||||
|
||||
public IncrementalSma(string src, int len) { _src = src; _len = len; }
|
||||
|
||||
public double? GetValue()
|
||||
{
|
||||
int bar = PineContext.CurrentBarIndex;
|
||||
if (bar == _lastBar) return _ready ? _sum / _len : null;
|
||||
|
||||
if (!_ready)
|
||||
{
|
||||
// 최초: 전체 윈도우 합산으로 초기화
|
||||
double[] hist = PineContext.GetLast(_src, _len, 0);
|
||||
if (hist.Length < _len) { _lastBar = bar; return null; }
|
||||
_sum = 0;
|
||||
for (int i = 0; i < hist.Length; i++) _sum += hist[i];
|
||||
_ready = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 증분: 새 값 추가, 만료값 차감 — CircularBuffer 직접 접근 (double[] 할당 0)
|
||||
var buf = PineContext.GetBuffer(_src);
|
||||
if (buf == null || buf.Count <= _len) { _lastBar = bar; return null; }
|
||||
double cur = buf.Get(0); // 현재 봉
|
||||
double old = buf.Get(_len); // len봉 전 (만료)
|
||||
if (double.IsNaN(cur) || double.IsNaN(old)) { _lastBar = bar; return null; }
|
||||
_sum += cur - old;
|
||||
}
|
||||
|
||||
_lastBar = bar;
|
||||
return _sum / _len;
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 증분 최고값 — 단조 덱(Monotonic Deque) O(1) amortized
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class IncrementalHighest
|
||||
{
|
||||
private readonly string _src;
|
||||
private readonly int _len;
|
||||
// 덱: 앞=최대값, 뒤=최소. 원소=(봉인덱스, 값)
|
||||
private readonly LinkedList<(int Bar, double Val)> _dq = new();
|
||||
private int _lastBar = -1;
|
||||
private double? _value;
|
||||
|
||||
public IncrementalHighest(string src, int len) { _src = src; _len = len; }
|
||||
|
||||
public double? GetValue()
|
||||
{
|
||||
int bar = PineContext.CurrentBarIndex;
|
||||
if (bar == _lastBar) return _value;
|
||||
|
||||
if (_lastBar < 0)
|
||||
{
|
||||
// 최초: 전체 윈도우로 덱 초기화 (고령→최신 순 처리)
|
||||
double[] hist = PineContext.GetLast(_src, _len, 0);
|
||||
if (hist.Length < _len) { _lastBar = bar; _value = null; return null; }
|
||||
|
||||
_dq.Clear();
|
||||
for (int i = _len - 1; i >= 0; i--) // hist[_len-1]=가장 오래된 봉
|
||||
{
|
||||
int b = bar - i;
|
||||
double v = hist[i];
|
||||
while (_dq.Count > 0 && _dq.Last!.Value.Val <= v)
|
||||
_dq.RemoveLast();
|
||||
_dq.AddLast((b, v));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 증분: 현재 봉 1개 추가 — CircularBuffer 직접 접근 (double[] 할당 0)
|
||||
var buf = PineContext.GetBuffer(_src);
|
||||
if (buf == null || buf.Count == 0) { _lastBar = bar; _value = null; return null; }
|
||||
double newVal = buf.Get(0);
|
||||
if (double.IsNaN(newVal)) { _lastBar = bar; _value = null; return null; }
|
||||
|
||||
// 만료 원소 앞에서 제거 (bar - len 이하)
|
||||
while (_dq.Count > 0 && _dq.First!.Value.Bar <= bar - _len)
|
||||
_dq.RemoveFirst();
|
||||
// 새 값보다 작거나 같은 원소 뒤에서 제거
|
||||
while (_dq.Count > 0 && _dq.Last!.Value.Val <= newVal)
|
||||
_dq.RemoveLast();
|
||||
_dq.AddLast((bar, newVal));
|
||||
}
|
||||
|
||||
_value = _dq.Count > 0 ? _dq.First!.Value.Val : null;
|
||||
_lastBar = bar;
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 증분 최저값 — 단조 덱 O(1) amortized
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class IncrementalLowest
|
||||
{
|
||||
private readonly string _src;
|
||||
private readonly int _len;
|
||||
private readonly LinkedList<(int Bar, double Val)> _dq = new();
|
||||
private int _lastBar = -1;
|
||||
private double? _value;
|
||||
|
||||
public IncrementalLowest(string src, int len) { _src = src; _len = len; }
|
||||
|
||||
public double? GetValue()
|
||||
{
|
||||
int bar = PineContext.CurrentBarIndex;
|
||||
if (bar == _lastBar) return _value;
|
||||
|
||||
if (_lastBar < 0)
|
||||
{
|
||||
double[] hist = PineContext.GetLast(_src, _len, 0);
|
||||
if (hist.Length < _len) { _lastBar = bar; _value = null; return null; }
|
||||
|
||||
_dq.Clear();
|
||||
for (int i = _len - 1; i >= 0; i--)
|
||||
{
|
||||
int b = bar - i;
|
||||
double v = hist[i];
|
||||
while (_dq.Count > 0 && _dq.Last!.Value.Val >= v)
|
||||
_dq.RemoveLast();
|
||||
_dq.AddLast((b, v));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 증분: 현재 봉 1개 추가 — CircularBuffer 직접 접근 (double[] 할당 0)
|
||||
var buf = PineContext.GetBuffer(_src);
|
||||
if (buf == null || buf.Count == 0) { _lastBar = bar; _value = null; return null; }
|
||||
double newVal = buf.Get(0);
|
||||
if (double.IsNaN(newVal)) { _lastBar = bar; _value = null; return null; }
|
||||
|
||||
while (_dq.Count > 0 && _dq.First!.Value.Bar <= bar - _len)
|
||||
_dq.RemoveFirst();
|
||||
while (_dq.Count > 0 && _dq.Last!.Value.Val >= newVal)
|
||||
_dq.RemoveLast();
|
||||
_dq.AddLast((bar, newVal));
|
||||
}
|
||||
|
||||
_value = _dq.Count > 0 ? _dq.First!.Value.Val : null;
|
||||
_lastBar = bar;
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
}
|
||||
1065
PineScript/Pine.cs
Normal file
1065
PineScript/Pine.cs
Normal file
File diff suppressed because it is too large
Load Diff
191
PineScript/PineContext.cs
Normal file
191
PineScript/PineContext.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// Pine Script의 시리즈(Series) 개념을 C#으로 구현합니다.
|
||||
/// - 봉마다 갱신되는 OHLCV 히스토리
|
||||
/// - 계산된 변수값 저장 (sma72, sma4 등)
|
||||
/// - 배열 백킹 스토어 (array.new_float 등)
|
||||
///
|
||||
/// 시리즈 히스토리는 원형 버퍼(CircularBuffer)를 사용해 Push가 O(1).
|
||||
/// 기존 List 기반 RemoveAt(0) O(n) 방식 대비 최대 수백 배 빠름.
|
||||
/// </summary>
|
||||
public static class PineContext
|
||||
{
|
||||
private const int MaxHistory = 600;
|
||||
|
||||
/// <summary>현재 실행 중인 봉 인덱스 (0-based). bar_index 변수에 매핑됩니다.</summary>
|
||||
public static int CurrentBarIndex { get; set; } = 0;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 원형 버퍼 — Push O(1), Get O(1)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class CircularBuffer
|
||||
{
|
||||
private readonly double[] _data;
|
||||
private int _head; // 다음 쓰기 위치 (0-based)
|
||||
private int _count; // 현재 쌓인 원소 수 (최대 _capacity)
|
||||
private readonly int _capacity;
|
||||
|
||||
public CircularBuffer(int capacity)
|
||||
{
|
||||
_data = new double[capacity];
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
/// <summary>현재 쌓인 봉 수</summary>
|
||||
public int Count => _count;
|
||||
|
||||
/// <summary>새 값 추가 — O(1)</summary>
|
||||
public void Push(double value)
|
||||
{
|
||||
_data[_head] = value;
|
||||
_head = (_head + 1) % _capacity;
|
||||
if (_count < _capacity) _count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// offset 봉 전 값 반환 — O(1)
|
||||
/// offset=0: 가장 최근, offset=1: 1봉 전, ...
|
||||
/// </summary>
|
||||
public double Get(int offset)
|
||||
{
|
||||
if (offset < 0 || offset >= _count) return double.NaN;
|
||||
// _head - 1 이 가장 최근 쓴 위치
|
||||
int idx = ((_head - 1 - offset) % _capacity + _capacity) % _capacity;
|
||||
return _data[idx];
|
||||
}
|
||||
}
|
||||
|
||||
// ── 시리즈 히스토리 (원형 버퍼) ───────────────────────────────
|
||||
private static readonly Dictionary<string, CircularBuffer> _series = new();
|
||||
|
||||
// ── 현재 봉 변수값 (함수 반환값 저장) ─────────────────────────
|
||||
private static readonly Dictionary<string, double> _variables = new();
|
||||
|
||||
// ── 배열 백킹 스토어 ──────────────────────────────────────────
|
||||
private static readonly Dictionary<string, List<double>> _floatArrays = new();
|
||||
private static readonly Dictionary<string, List<int>> _intArrays = new();
|
||||
private static readonly Dictionary<string, List<bool>> _boolArrays = new();
|
||||
private static readonly Dictionary<string, List<string>> _strArrays = new();
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 시리즈 히스토리
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>시리즈에 현재 값 추가 (봉 교체 시 자동 호출) — O(1)</summary>
|
||||
public static void Push(string name, double value)
|
||||
{
|
||||
if (!_series.TryGetValue(name, out var buf))
|
||||
{
|
||||
buf = new CircularBuffer(MaxHistory);
|
||||
_series[name] = buf;
|
||||
}
|
||||
buf.Push(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시리즈의 최근 n개 값을 반환합니다.
|
||||
/// offset: 0 = 현재 봉, 1 = 이전 봉 (close[1] 같은 경우)
|
||||
/// 반환 배열 index 0 = 가장 최근 값
|
||||
/// </summary>
|
||||
public static double[] GetLast(string name, int n, int offset = 0)
|
||||
{
|
||||
if (!_series.TryGetValue(name, out var buf))
|
||||
return Array.Empty<double>();
|
||||
|
||||
int needed = n + offset;
|
||||
if (buf.Count < needed)
|
||||
return Array.Empty<double>();
|
||||
|
||||
var result = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
result[i] = buf.Get(offset + i);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>현재 시리즈에 쌓인 봉 수</summary>
|
||||
public static int BarCount(string name) =>
|
||||
_series.TryGetValue(name, out var buf) ? buf.Count : 0;
|
||||
|
||||
/// <summary>
|
||||
/// CircularBuffer 직접 반환 — 내부 최적화용.
|
||||
/// 배열 할당 없이 Get(offset) 으로 단일 값을 읽을 때 사용합니다.
|
||||
/// </summary>
|
||||
internal static CircularBuffer? GetBuffer(string name) =>
|
||||
_series.TryGetValue(name, out var buf) ? buf : null;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 변수 스토어 (함수 반환값 캡처)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 계산된 변수값을 저장합니다.
|
||||
/// 히스토리에도 함께 Push해서 ta.sma(sma4, 12) 같은 호출도 지원합니다.
|
||||
/// </summary>
|
||||
public static void SetVariable(string name, double value)
|
||||
{
|
||||
_variables[name] = value;
|
||||
Push(name, value);
|
||||
}
|
||||
|
||||
public static double? GetVariable(string name) =>
|
||||
_variables.TryGetValue(name, out double v) ? v : null;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 배열 스토어
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
public static List<double> GetOrCreateFloat(string key)
|
||||
{
|
||||
if (!_floatArrays.TryGetValue(key, out var list))
|
||||
_floatArrays[key] = list = new List<double>();
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<int> GetOrCreateInt(string key)
|
||||
{
|
||||
if (!_intArrays.TryGetValue(key, out var list))
|
||||
_intArrays[key] = list = new List<int>();
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<bool> GetOrCreateBool(string key)
|
||||
{
|
||||
if (!_boolArrays.TryGetValue(key, out var list))
|
||||
_boolArrays[key] = list = new List<bool>();
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<string> GetOrCreateString(string key)
|
||||
{
|
||||
if (!_strArrays.TryGetValue(key, out var list))
|
||||
_strArrays[key] = list = new List<string>();
|
||||
return list;
|
||||
}
|
||||
|
||||
// 타입 무관하게 double 리스트로 반환
|
||||
public static List<double> GetAnyAsFloat(string key)
|
||||
{
|
||||
if (_floatArrays.TryGetValue(key, out var fa)) return fa;
|
||||
if (_intArrays.TryGetValue(key, out var ia))
|
||||
return ia.Select(x => (double)x).ToList();
|
||||
return GetOrCreateFloat(key);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 전체 초기화 (재실행 시 반드시 호출)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
_series.Clear();
|
||||
_variables.Clear();
|
||||
_floatArrays.Clear();
|
||||
_intArrays.Clear();
|
||||
_boolArrays.Clear();
|
||||
_strArrays.Clear();
|
||||
CurrentBarIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
1124
PineScript/PineEvaluator.cs
Normal file
1124
PineScript/PineEvaluator.cs
Normal file
File diff suppressed because it is too large
Load Diff
31
PineScript/PineScript.csproj
Normal file
31
PineScript/PineScript.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="BITGET_XRPUSDT, 30.csv">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="example.pine">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="TradingResult.csv">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
607
PineScript/PineScriptExecutor.cs
Normal file
607
PineScript/PineScriptExecutor.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
using System.Diagnostics;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PineScriptV6
|
||||
{
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 문 (Statement) 타입 정의
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>Pine Script 한 문(statement)의 추상 기반 클래스</summary>
|
||||
public abstract record PineStmt;
|
||||
|
||||
/// <summary>var bool inLong = false → 최초 1회만 초기화</summary>
|
||||
public record VarDeclStmt(string Name, string TypeHint, string Expr) : PineStmt;
|
||||
|
||||
/// <summary>sma4 = expr 또는 newLongSignal = false</summary>
|
||||
public record AssignStmt(string Name, string Expr) : PineStmt;
|
||||
|
||||
/// <summary>inLong := true (상태 변수 재할당)</summary>
|
||||
public record ReassignStmt(string Name, string Expr) : PineStmt;
|
||||
|
||||
/// <summary>plotshape(...), bgcolor(...) 등 반환값 없는 호출</summary>
|
||||
public record ExprStmt(string Expr) : PineStmt;
|
||||
|
||||
/// <summary>if condition\n body</summary>
|
||||
public record IfStmt(string Condition, List<PineStmt> ThenBody, List<PineStmt>? ElseBody) : PineStmt;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 실행기
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
public class PineScriptExecutor
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private List<PineStmt>? _program; // 최초 1회 파싱
|
||||
private DependencyAnalyzer.ExecutionPlan? _plan; // 최초 1회 분석
|
||||
|
||||
// 실시간 모드용 — WarmUp 이후 ProcessCandle에서 사용
|
||||
private int _realtimeBarIndex = 0;
|
||||
|
||||
public PineScriptExecutor(string filePath = "./example.pine")
|
||||
{
|
||||
_filePath = filePath;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 배치 모드 (CSV 히스토리 전체 실행)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 봉 데이터를 순서대로 주입하며 매 봉마다 스크립트를 실행합니다.
|
||||
/// Two-Pass: Pass1(지표) → Pass2(상태머신) 순으로 실행.
|
||||
/// signalFrom: 이 시각 이후 봉에만 상세 추적 로그를 출력합니다.
|
||||
/// </summary>
|
||||
public void ExecuteOnCandles(List<CandleData> candles,
|
||||
DateTimeOffset? signalFrom = null)
|
||||
{
|
||||
ResetState();
|
||||
PineEvaluator.ResetStats();
|
||||
EnsurePlan();
|
||||
|
||||
if (_program!.Count == 0)
|
||||
{
|
||||
Log.Error("[Executor] 스크립트 로드 실패: {Path}", _filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information("[Executor] Two-Pass 실행 시작 | 총 {Count}봉 " +
|
||||
"(Pass1={P1} Pass2={P2}) | 추적={From}",
|
||||
candles.Count,
|
||||
_plan!.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
|
||||
signalFrom.HasValue ? signalFrom.Value.ToString("yyyy-MM-dd") : "전체");
|
||||
|
||||
long swPass1Ms = 0, swPass2Ms = 0, swInjectMs = 0, swLogMs = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (int i = 0; i < candles.Count; i++)
|
||||
{
|
||||
CandleData c = candles[i];
|
||||
bool inSignalRange = !signalFrom.HasValue || c.Time >= signalFrom.Value;
|
||||
|
||||
var t0 = sw.ElapsedTicks;
|
||||
InjectOhlcv(c, i);
|
||||
var t1 = sw.ElapsedTicks;
|
||||
|
||||
// Pass 1 — 지표 계산 (O(1) 증분)
|
||||
ExecuteStmts(_plan.Pass1Stmts);
|
||||
var t2 = sw.ElapsedTicks;
|
||||
|
||||
// Pass 2 — 상태 머신 (순차)
|
||||
ExecuteStmts(_plan.Pass2Stmts);
|
||||
var t3 = sw.ElapsedTicks;
|
||||
|
||||
if (inSignalRange)
|
||||
LogBarDetail(c, i + 1, candles.Count);
|
||||
else
|
||||
LogWarmupSnap(c, i + 1, candles.Count);
|
||||
var t4 = sw.ElapsedTicks;
|
||||
|
||||
swInjectMs += t1 - t0;
|
||||
swPass1Ms += t2 - t1;
|
||||
swPass2Ms += t3 - t2;
|
||||
swLogMs += t4 - t3;
|
||||
}
|
||||
|
||||
double ticksPerMs = (double)Stopwatch.Frequency / 1000.0;
|
||||
Log.Information("[Executor] 실행 완료 | {Count}봉 처리", candles.Count);
|
||||
Log.Information("═══ Pass별 소요 시간 ═══════════════");
|
||||
Log.Information(" Inject OHLCV : {Ms,8:F1} ms", swInjectMs / ticksPerMs);
|
||||
Log.Information(" Pass1(지표) : {Ms,8:F1} ms", swPass1Ms / ticksPerMs);
|
||||
Log.Information(" Pass2(상태) : {Ms,8:F1} ms", swPass2Ms / ticksPerMs);
|
||||
Log.Information(" Logging : {Ms,8:F1} ms", swLogMs / ticksPerMs);
|
||||
Log.Information("════════════════════════════════════");
|
||||
PineEvaluator.DumpStats();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 실시간 모드 (스트리밍)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 실시간 연결 전, 과거 봉으로 지표 상태를 초기화합니다.
|
||||
/// warmupCandles: 최근 WarmupBars개 봉 (지표 최대 윈도우 이상이어야 함).
|
||||
/// </summary>
|
||||
public void WarmUp(List<CandleData> warmupCandles)
|
||||
{
|
||||
ResetState();
|
||||
EnsurePlan();
|
||||
|
||||
Log.Information("[Executor] WarmUp 시작 | {Count}봉", warmupCandles.Count);
|
||||
|
||||
for (int i = 0; i < warmupCandles.Count; i++)
|
||||
{
|
||||
InjectOhlcv(warmupCandles[i], i);
|
||||
ExecuteStmts(_plan!.Pass1Stmts);
|
||||
ExecuteStmts(_plan.Pass2Stmts);
|
||||
}
|
||||
|
||||
_realtimeBarIndex = warmupCandles.Count;
|
||||
Log.Information("[Executor] WarmUp 완료 | 다음 bar_index={Idx}", _realtimeBarIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실시간 단일 봉 처리.
|
||||
/// WarmUp() 이후에 호출해야 합니다.
|
||||
/// 반환값: 이 봉에서 발생한 신호 목록 (없으면 빈 리스트).
|
||||
/// </summary>
|
||||
public List<TradeSignalEvent> ProcessCandle(CandleData candle)
|
||||
{
|
||||
EnsurePlan();
|
||||
|
||||
int prevCount = TradingSignal.Results.Count;
|
||||
|
||||
InjectOhlcv(candle, _realtimeBarIndex++);
|
||||
ExecuteStmts(_plan!.Pass1Stmts);
|
||||
ExecuteStmts(_plan.Pass2Stmts);
|
||||
|
||||
// 이번 봉에서 새로 추가된 신호만 반환
|
||||
return TradingSignal.Results.Count > prevCount
|
||||
? TradingSignal.Results.GetRange(prevCount,
|
||||
TradingSignal.Results.Count - prevCount)
|
||||
: new List<TradeSignalEvent>();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 내부 헬퍼
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
PineContext.Reset();
|
||||
PineStateVars.Clear();
|
||||
TradingSignal.ClearResults();
|
||||
IndicatorEngine.Reset();
|
||||
_realtimeBarIndex = 0;
|
||||
}
|
||||
|
||||
private void EnsurePlan()
|
||||
{
|
||||
_program ??= LoadScript();
|
||||
if (_plan != null) return;
|
||||
_plan = DependencyAnalyzer.Analyze(_program);
|
||||
Log.Information("[Executor] 의존성 분석 완료 | Pass1={P1}문 Pass2={P2}문 | " +
|
||||
"Pass1변수={Vars}개 StateVars={State}개",
|
||||
_plan.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
|
||||
_plan.Pass1Vars.Count, _plan.StateVars.Count);
|
||||
}
|
||||
|
||||
private static void InjectOhlcv(CandleData c, int barIndex)
|
||||
{
|
||||
Pine.open = (double)c.Open;
|
||||
Pine.high = (double)c.High;
|
||||
Pine.low = (double)c.Low;
|
||||
Pine.close = (double)c.Close;
|
||||
Pine.volume = (double)c.Volume;
|
||||
Pine.time = c.Time.ToUnixTimeMilliseconds();
|
||||
PineContext.CurrentBarIndex = barIndex;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 추적 로그
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>분석 구간 매 봉: 핵심 변수 한 줄 + 신호·NearMiss 상세</summary>
|
||||
private static void LogBarDetail(CandleData c, int barNum, int total)
|
||||
{
|
||||
bool debugOn = Log.IsEnabled(LogEventLevel.Debug);
|
||||
|
||||
// ── ① 신호 발생 여부 최우선 확인 (4개 조회) ─────────────
|
||||
bool sigL = PineEvaluator.ToBool(PineStateVars.Get("newLongSignal"));
|
||||
bool sigLS = PineEvaluator.ToBool(PineStateVars.Get("longSellSignal"));
|
||||
bool sigS = PineEvaluator.ToBool(PineStateVars.Get("newShortSignal"));
|
||||
bool sigSS = PineEvaluator.ToBool(PineStateVars.Get("shortSellSignal"));
|
||||
|
||||
if (sigL || sigLS || sigS || sigSS)
|
||||
{
|
||||
Log.Information(
|
||||
"★[SIGNAL] {Time} Bar={B}/{T} | " +
|
||||
"NewLong={SL} LongSell={SLS} NewShort={SS} ShortSell={SSS} | " +
|
||||
"Close={C} sma72={S72} cloudTop={CT}",
|
||||
c.Time.ToString("yyyy-MM-dd HH:mm"), barNum, total,
|
||||
sigL, sigLS, sigS, sigSS,
|
||||
FV(c.Close),
|
||||
FV(PineStateVars.Get("sma72")),
|
||||
FV(PineStateVars.Get("cloudTopNow")));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── ② rawNew* 확인 (NearMiss) ─────────────────────────────
|
||||
bool rawL = PineEvaluator.ToBool(PineStateVars.Get("rawNewLong"));
|
||||
bool rawS = PineEvaluator.ToBool(PineStateVars.Get("rawNewShort"));
|
||||
|
||||
// ── ③ NearMiss도 없고 Debug도 꺼져있으면 즉시 리턴 ───────
|
||||
// → 나머지 10개 Dict 조회·포맷팅 완전 생략
|
||||
if (!rawL && !rawS && !debugOn) return;
|
||||
|
||||
// ── ④ 나머지 변수 (필요한 경우에만 조회) ─────────────────
|
||||
bool inL = PineEvaluator.ToBool(PineStateVars.Get("inLong"));
|
||||
bool inS = PineEvaluator.ToBool(PineStateVars.Get("inShort"));
|
||||
bool sidewaysL = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysLong"));
|
||||
bool sidewaysS = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysShort"));
|
||||
bool touchL = PineEvaluator.ToBool(PineStateVars.Get("isLongRecentTouch"));
|
||||
bool touchS = PineEvaluator.ToBool(PineStateVars.Get("isShortRecentTouch"));
|
||||
|
||||
if (rawL || rawS)
|
||||
{
|
||||
Log.Information(
|
||||
"△[NEAR ] {Time} | rawL={RL} rawS={RS} inL={IL} inS={IS} | " +
|
||||
"sideL={SL} sideS={SS} touchL={TL} touchS={TS}",
|
||||
c.Time.ToString("yyyy-MM-dd HH:mm"),
|
||||
rawL, rawS, inL, inS, sidewaysL, sidewaysS, touchL, touchS);
|
||||
}
|
||||
|
||||
// ── ⑤ 매 봉 상세: Debug 레벨 (기본 꺼짐 → 스킵) ─────────
|
||||
if (!debugOn) return;
|
||||
|
||||
string sma72 = FV(PineStateVars.Get("sma72"));
|
||||
string sma4M = FV(PineStateVars.Get("sma4Middle"));
|
||||
string cloudTop = FV(PineStateVars.Get("cloudTopNow"));
|
||||
string rangeTh = FV(PineStateVars.Get("rangeThresholdLongPercent"));
|
||||
bool rawLS = PineEvaluator.ToBool(PineStateVars.Get("rawLongSell"));
|
||||
bool rawSS = PineEvaluator.ToBool(PineStateVars.Get("rawShortSell"));
|
||||
|
||||
Log.Debug(
|
||||
"[BAR ] {Time} | C={C} s72={S72} s4M={S4M} cTop={CT} rTh={RT} | " +
|
||||
"sideL={SL} sideS={SS} tL={TL} tS={TS} | " +
|
||||
"rawL={RL} rawS={RS} rawLS={RLS} rawSS={RSS} | inL={IL} inS={IS}",
|
||||
c.Time.ToString("yyyy-MM-dd HH:mm"),
|
||||
FV(c.Close), sma72, sma4M, cloudTop, rangeTh,
|
||||
B(sidewaysL), B(sidewaysS), B(touchL), B(touchS),
|
||||
B(rawL), B(rawS), B(rawLS), B(rawSS),
|
||||
B(inL), B(inS));
|
||||
}
|
||||
|
||||
/// <summary>워밍업 구간: 50봉마다 지표 준비 상황만 출력</summary>
|
||||
private static void LogWarmupSnap(CandleData c, int barNum, int total)
|
||||
{
|
||||
if (barNum % 50 != 0 && barNum != 1) return;
|
||||
|
||||
Log.Information(
|
||||
"[WARM] Bar={B}/{T} {Time} | close={C} sma72={S72} cloudTop={CT} rangeTh={RT}",
|
||||
barNum, total, c.Time.ToString("yyyy-MM-dd HH:mm"),
|
||||
FV(c.Close),
|
||||
FV(PineStateVars.Get("sma72")),
|
||||
FV(PineStateVars.Get("cloudTopNow")),
|
||||
FV(PineStateVars.Get("rangeThresholdLongPercent")));
|
||||
}
|
||||
|
||||
// ── 포맷 헬퍼 ────────────────────────────────────────────────────
|
||||
private static string FV(object? v)
|
||||
{
|
||||
if (v == null) return "na";
|
||||
if (v is double d) return d.ToString("F4");
|
||||
if (v is decimal m) return m.ToString("F4");
|
||||
return v.ToString() ?? "na";
|
||||
}
|
||||
private static string FV(decimal m) => m.ToString("F4");
|
||||
private static string B(bool b) => b ? "T" : "F";
|
||||
|
||||
// ── 스크립트 로드 & 파싱 ──────────────────────────────────────
|
||||
|
||||
private List<PineStmt> LoadScript()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
Log.Error("[Executor] 파일 없음: {Path}", _filePath);
|
||||
return new List<PineStmt>();
|
||||
}
|
||||
|
||||
// 들여쓰기 보존 → (indent, content) 쌍으로 수집
|
||||
var lines = new List<(int Indent, string Raw)>();
|
||||
foreach (string raw in File.ReadAllLines(_filePath))
|
||||
{
|
||||
string trimEnd = raw.TrimEnd();
|
||||
if (string.IsNullOrWhiteSpace(trimEnd)) continue;
|
||||
|
||||
string content = trimEnd.TrimStart();
|
||||
|
||||
// 주석·버전 어노테이션 제외
|
||||
if (content.StartsWith("//") || content.StartsWith("//@")) continue;
|
||||
|
||||
int indent = trimEnd.Length - content.Length;
|
||||
lines.Add((indent, content));
|
||||
}
|
||||
|
||||
Log.Information("[Executor] 스크립트 로드 | 유효 라인 {Count}개 | {Path}",
|
||||
lines.Count, _filePath);
|
||||
|
||||
int pos = 0;
|
||||
var stmts = BuildBlock(lines, ref pos, -1);
|
||||
Log.Information("[Executor] 문(Statement) 파싱 완료 | {Count}개", stmts.Count);
|
||||
return stmts;
|
||||
}
|
||||
|
||||
// ── 블록 빌더 (재귀) ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// parentIndent 보다 큰 들여쓰기 수준의 라인들을 문 목록으로 변환합니다.
|
||||
/// </summary>
|
||||
private List<PineStmt> BuildBlock(List<(int Indent, string Raw)> lines,
|
||||
ref int pos, int parentIndent)
|
||||
{
|
||||
var stmts = new List<PineStmt>();
|
||||
|
||||
while (pos < lines.Count)
|
||||
{
|
||||
var (indent, raw) = lines[pos];
|
||||
|
||||
// 상위 블록으로 복귀
|
||||
if (indent <= parentIndent) break;
|
||||
|
||||
// ── if 처리 ──
|
||||
if (raw.StartsWith("if ", StringComparison.OrdinalIgnoreCase) ||
|
||||
raw.StartsWith("if(", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string condition = ExtractCondition(raw);
|
||||
int myIndent = indent;
|
||||
pos++;
|
||||
|
||||
// then 블록: 다음 라인들 중 indent > myIndent
|
||||
var thenBody = pos < lines.Count && lines[pos].Indent > myIndent
|
||||
? BuildBlock(lines, ref pos, myIndent)
|
||||
: new List<PineStmt>();
|
||||
|
||||
// else 블록
|
||||
List<PineStmt>? elseBody = null;
|
||||
if (pos < lines.Count
|
||||
&& lines[pos].Indent == myIndent
|
||||
&& lines[pos].Raw.Equals("else", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
pos++;
|
||||
elseBody = pos < lines.Count && lines[pos].Indent > myIndent
|
||||
? BuildBlock(lines, ref pos, myIndent)
|
||||
: new List<PineStmt>();
|
||||
}
|
||||
|
||||
stmts.Add(new IfStmt(condition, thenBody, elseBody));
|
||||
continue; // pos는 BuildBlock 내부에서 이미 이동
|
||||
}
|
||||
|
||||
// ── 일반 문 ──
|
||||
var stmt = ParseStatement(raw);
|
||||
if (stmt != null) stmts.Add(stmt);
|
||||
pos++;
|
||||
}
|
||||
|
||||
return stmts;
|
||||
}
|
||||
|
||||
// ── 문 파싱 ───────────────────────────────────────────────────
|
||||
|
||||
private static PineStmt? ParseStatement(string line)
|
||||
{
|
||||
// 1) var 선언: var bool inLong = false
|
||||
if (line.StartsWith("var ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string rest = line.Substring(4).Trim();
|
||||
|
||||
// type hint 제거
|
||||
string typeHint = "";
|
||||
string[] types = { "float[]", "int[]", "bool[]", "string[]",
|
||||
"bool", "int", "float", "string" };
|
||||
foreach (var t in types)
|
||||
{
|
||||
if (rest.StartsWith(t + " ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
typeHint = t;
|
||||
rest = rest.Substring(t.Length).Trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int eqIdx = FindAssignEq(rest);
|
||||
if (eqIdx > 0)
|
||||
{
|
||||
string name = rest.Substring(0, eqIdx).Trim();
|
||||
string expr = rest.Substring(eqIdx + 1).Trim();
|
||||
return new VarDeclStmt(name, typeHint, expr);
|
||||
}
|
||||
// var foo (초기화 없음)
|
||||
string justName = rest.Split(' ')[0].Trim();
|
||||
return new VarDeclStmt(justName, typeHint, "na");
|
||||
}
|
||||
|
||||
// 2) := 재할당: inLong := true
|
||||
{
|
||||
int cIdx = FindReassignOp(line);
|
||||
if (cIdx > 0)
|
||||
{
|
||||
string name = line.Substring(0, cIdx).Trim();
|
||||
string expr = line.Substring(cIdx + 2).Trim();
|
||||
if (IsSimpleIdent(name))
|
||||
return new ReassignStmt(name, expr);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 대입: sma4 = expr
|
||||
{
|
||||
int eqIdx = FindAssignEq(line);
|
||||
if (eqIdx > 0)
|
||||
{
|
||||
string lhs = line.Substring(0, eqIdx).Trim();
|
||||
string rhs = line.Substring(eqIdx + 1).Trim();
|
||||
if (IsSimpleIdent(lhs) && !string.IsNullOrEmpty(rhs))
|
||||
return new AssignStmt(lhs, rhs);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 표현식 문: plotshape(...), plot(...) 등
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
return new ExprStmt(line);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 문 실행 ───────────────────────────────────────────────────
|
||||
|
||||
private static void ExecuteStmts(List<PineStmt> stmts)
|
||||
{
|
||||
foreach (var stmt in stmts)
|
||||
ExecuteOne(stmt);
|
||||
}
|
||||
|
||||
private static void ExecuteOne(PineStmt stmt)
|
||||
{
|
||||
switch (stmt)
|
||||
{
|
||||
// ── var 선언 (최초 1회만 초기화) ──
|
||||
case VarDeclStmt vd:
|
||||
{
|
||||
if (!PineStateVars.Has(vd.Name))
|
||||
{
|
||||
object? val = PineEvaluator.Evaluate(vd.Expr);
|
||||
PineStateVars.Declare(vd.Name, val);
|
||||
Log.Debug("[Exec] var {Name} = {Val}", vd.Name, val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ── 일반 대입 (매 봉마다 재평가) ──
|
||||
case AssignStmt a:
|
||||
{
|
||||
object? val = PineEvaluator.Evaluate(a.Expr);
|
||||
PineStateVars.Set(a.Name, val);
|
||||
|
||||
// double 값은 PineContext 시리즈에도 Push (ta.sma 등이 읽음)
|
||||
double dv = PineEvaluator.ToDouble(val);
|
||||
if (!double.IsNaN(dv))
|
||||
PineContext.SetVariable(a.Name, dv);
|
||||
|
||||
Log.Debug("[Exec] {Name} = {Val}", a.Name, val);
|
||||
break;
|
||||
}
|
||||
|
||||
// ── := 재할당 ──
|
||||
case ReassignStmt r:
|
||||
{
|
||||
object? val = PineEvaluator.Evaluate(r.Expr);
|
||||
PineStateVars.Set(r.Name, val);
|
||||
|
||||
double dv = PineEvaluator.ToDouble(val);
|
||||
if (!double.IsNaN(dv))
|
||||
PineContext.SetVariable(r.Name, dv);
|
||||
|
||||
Log.Debug("[Exec] {Name} := {Val}", r.Name, val);
|
||||
break;
|
||||
}
|
||||
|
||||
// ── 표현식 문 (side-effect 목적) ──
|
||||
case ExprStmt e:
|
||||
{
|
||||
PineEvaluator.Evaluate(e.Expr);
|
||||
break;
|
||||
}
|
||||
|
||||
// ── if / else ──
|
||||
case IfStmt ifs:
|
||||
{
|
||||
bool cond = PineEvaluator.EvaluateBool(ifs.Condition);
|
||||
Log.Debug("[Exec] if ({Cond}) → {Result}", ifs.Condition, cond);
|
||||
if (cond)
|
||||
ExecuteStmts(ifs.ThenBody);
|
||||
else if (ifs.ElseBody != null)
|
||||
ExecuteStmts(ifs.ElseBody);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 파싱 헬퍼
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>if 라인에서 조건 문자열 추출</summary>
|
||||
private static string ExtractCondition(string line)
|
||||
{
|
||||
// "if " or "if(" 이후 텍스트
|
||||
string rest = line.Length > 3 ? line.Substring(3).Trim() : "";
|
||||
// 바깥 괄호 제거: "(cond)" → "cond"
|
||||
if (rest.StartsWith("(") && rest.EndsWith(")"))
|
||||
{
|
||||
// 매칭 괄호 확인
|
||||
int depth = 0;
|
||||
bool matched = false;
|
||||
for (int i = 0; i < rest.Length; i++)
|
||||
{
|
||||
if (rest[i] == '(') depth++;
|
||||
else if (rest[i] == ')') { depth--; if (depth == 0 && i == rest.Length - 1) { matched = true; break; } }
|
||||
}
|
||||
if (matched) rest = rest.Substring(1, rest.Length - 2).Trim();
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 순수 대입 '=' 위치 반환.
|
||||
/// ==, !=, <=, >=, := 는 제외.
|
||||
/// 괄호/대괄호 안은 탐색 제외.
|
||||
/// </summary>
|
||||
private static int FindAssignEq(string line)
|
||||
{
|
||||
int depth = 0;
|
||||
for (int i = 1; i < line.Length; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (c == '(' || c == '[') { depth++; continue; }
|
||||
if (c == ')' || c == ']') { depth--; continue; }
|
||||
if (depth > 0) continue;
|
||||
|
||||
if (c == '=')
|
||||
{
|
||||
char prev = line[i - 1];
|
||||
char next = i + 1 < line.Length ? line[i + 1] : '\0';
|
||||
bool compound = prev is '!' or '<' or '>' or '=' or ':';
|
||||
if (!compound && next != '=') return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>:= 위치 반환 (없으면 -1), 괄호 밖만 탐색</summary>
|
||||
private static int FindReassignOp(string line)
|
||||
{
|
||||
int depth = 0;
|
||||
for (int i = 0; i < line.Length - 1; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (c == '(' || c == '[') { depth++; continue; }
|
||||
if (c == ')' || c == ']') { depth--; continue; }
|
||||
if (depth > 0) continue;
|
||||
if (c == ':' && line[i + 1] == '=') return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>단순 변수명 여부 (영문자·숫자·밑줄만, 첫 글자는 영문자/밑줄)</summary>
|
||||
private static bool IsSimpleIdent(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return false;
|
||||
if (!char.IsLetter(s[0]) && s[0] != '_') return false;
|
||||
foreach (char c in s)
|
||||
if (!char.IsLetterOrDigit(c) && c != '_') return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
PineScript/PineStateVars.cs
Normal file
35
PineScript/PineStateVars.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>
|
||||
/// Pine Script의 var 선언 변수 및 := 재할당 변수를 저장합니다.
|
||||
/// 봉 간에 상태가 유지됩니다 (inLong, inShort 등).
|
||||
/// </summary>
|
||||
public static class PineStateVars
|
||||
{
|
||||
private static readonly Dictionary<string, object?> _vars = new();
|
||||
|
||||
/// <summary>
|
||||
/// var 선언 전용 — 최초 1회만 초기화 (이미 존재하면 무시)
|
||||
/// </summary>
|
||||
public static void Declare(string name, object? initialValue)
|
||||
{
|
||||
if (!_vars.ContainsKey(name))
|
||||
_vars[name] = initialValue;
|
||||
}
|
||||
|
||||
/// <summary>일반 대입 / := 재할당 모두 사용</summary>
|
||||
public static void Set(string name, object? value) => _vars[name] = value;
|
||||
|
||||
public static object? Get(string name) =>
|
||||
_vars.TryGetValue(name, out var v) ? v : null;
|
||||
|
||||
public static bool Has(string name) => _vars.ContainsKey(name);
|
||||
|
||||
/// <summary>단일 딕셔너리 조회 — Has+Get 이중 조회 대신 사용</summary>
|
||||
public static bool TryGet(string name, out object? value) =>
|
||||
_vars.TryGetValue(name, out value);
|
||||
|
||||
/// <summary>전체 초기화 (재실행 시)</summary>
|
||||
public static void Clear() => _vars.Clear();
|
||||
}
|
||||
}
|
||||
190
PineScript/Program.cs
Normal file
190
PineScript/Program.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
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);
|
||||
}
|
||||
10
PineScript/Properties/launchSettings.json
Normal file
10
PineScript/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"PineScript": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
407
PineScript/TradingResult.csv
Normal file
407
PineScript/TradingResult.csv
Normal file
@@ -0,0 +1,407 @@
|
||||
거래 #,타입,날짜 및 시간,신호,가격 USDT,크기 (수량),크기 (값),순손익 USDT,순손익 %,유리한 변동 USDT,유리한 변동 %,불리한 변동 USDT,불리한 변동 %,누적 손익 USDT,누적 손익 %
|
||||
1,매수 청산,2025-01-07 06:30,Long Sell,2.413,4115.1,9995.98941,-74.22,-0.74,113.28,1.13,-119.22,-1.19,-74.22,-0.74
|
||||
1,매수 진입,2025-01-07 00:00,Long,2.4291,4115.1,9995.98941,-74.22,-0.74,113.28,1.13,-119.22,-1.19,-74.22,-0.74
|
||||
2,매수 청산,2025-01-07 22:00,Long Sell,2.4135,4056.8,9921.71576,-138.51,-1.40,86.09,0.87,-162.18,-1.63,-212.74,-2.13
|
||||
2,매수 진입,2025-01-07 16:00,Long,2.4457,4056.8,9921.71576,-138.51,-1.40,86.09,0.87,-162.18,-1.63,-212.74,-2.13
|
||||
3,매도 청산,2025-01-08 06:30,Short Sell,2.2913,4174.1,9783.25558,211.4,2.16,353.81,3.62,-7.67,-0.08,-1.34,-0.01
|
||||
3,매도 진입,2025-01-08 01:30,Short,2.3438,4174.1,9783.25558,211.4,2.16,353.81,3.62,-7.67,-0.08,-1.34,-0.01
|
||||
4,매도 청산,2025-01-10 07:30,Short Sell,2.2767,4412.8,9994.55072,-60.09,-0.60,39.25,0.39,-65.34,-0.65,-61.42,-0.61
|
||||
4,매도 진입,2025-01-10 07:00,Short,2.2649,4412.8,9994.55072,-60.09,-0.60,39.25,0.39,-65.34,-0.65,-61.42,-0.61
|
||||
5,매도 청산,2025-01-11 02:30,Short Sell,2.296,4372,9934.4956,-111.61,-1.12,12.2,0.12,-118.08,-1.19,-173.03,-1.73
|
||||
5,매도 진입,2025-01-11 01:30,Short,2.2723,4372,9934.4956,-111.61,-1.12,12.2,0.12,-118.08,-1.19,-173.03,-1.73
|
||||
6,매수 청산,2025-01-11 13:00,Long Sell,2.3309,4169.9,9823.03343,-111.23,-1.13,27.76,0.28,-135.7,-1.38,-284.26,-2.84
|
||||
6,매수 진입,2025-01-11 06:00,Long,2.3557,4169.9,9823.03343,-111.23,-1.13,27.76,0.28,-135.7,-1.38,-284.26,-2.84
|
||||
7,매수 청산,2025-01-13 07:30,Long Sell,2.4981,3823.2,9711.69264,-168.66,-1.74,50.02,0.51,-196.96,-2.03,-452.92,-4.53
|
||||
7,매수 진입,2025-01-13 01:30,Long,2.5402,3823.2,9711.69264,-168.66,-1.74,50.02,0.51,-196.96,-2.03,-452.92,-4.53
|
||||
8,매수 청산,2025-01-15 20:30,Long Sell,2.7342,3703.1,9543.25901,573.89,6.01,1163.03,12.18,-117.13,-1.23,120.97,1.21
|
||||
8,매수 진입,2025-01-14 18:30,Long,2.5771,3703.1,9543.25901,573.89,6.01,1163.03,12.18,-117.13,-1.23,120.97,1.21
|
||||
9,매수 청산,2025-01-17 07:00,Long Sell,3.292,3343.2,10116.85752,880.51,8.70,1251.99,12.37,-516.89,-5.11,1001.48,10.01
|
||||
9,매수 진입,2025-01-16 00:30,Long,3.0261,3343.2,10116.85752,880.51,8.70,1251.99,12.37,-516.89,-5.11,1001.48,10.01
|
||||
10,매도 청산,2025-01-18 16:30,Short Sell,3.1403,3439.9,10997.01631,185.98,1.69,416.99,3.79,-78.7,-0.72,1187.45,11.87
|
||||
10,매도 진입,2025-01-18 13:30,Short,3.1969,3439.9,10997.01631,185.98,1.69,416.99,3.79,-78.7,-0.72,1187.45,11.87
|
||||
11,매도 청산,2025-01-20 10:30,Short Sell,2.9745,3649.3,11182.91492,319.26,2.85,850.92,7.61,-258.1,-2.31,1506.71,15.07
|
||||
11,매도 진입,2025-01-20 07:00,Short,3.0644,3649.3,11182.91492,319.26,2.85,850.92,7.61,-258.1,-2.31,1506.71,15.07
|
||||
12,매수 청산,2025-01-23 02:00,Long Sell,3.1657,3548.1,11501.87577,-278.75,-2.42,23.78,0.21,-354.8,-3.08,1227.96,12.28
|
||||
12,매수 진입,2025-01-22 21:30,Long,3.2417,3548.1,11501.87577,-278.75,-2.42,23.78,0.21,-354.8,-3.08,1227.96,12.28
|
||||
13,매수 청산,2025-01-23 11:00,Long Sell,3.1515,3436.5,11223.26535,-401.96,-3.58,6.51,0.06,-424.43,-3.78,826.01,8.26
|
||||
13,매수 진입,2025-01-23 05:30,Long,3.2659,3436.5,11223.26535,-401.96,-3.58,6.51,0.06,-424.43,-3.78,826.01,8.26
|
||||
14,매도 청산,2025-01-23 13:00,Short Sell,3.1523,3435.1,10825.71765,-11.41,-0.11,113.84,1.05,-67.54,-0.62,814.6,8.15
|
||||
14,매도 진입,2025-01-23 11:00,Short,3.1515,3435.1,10825.71765,-11.41,-0.11,113.84,1.05,-67.54,-0.62,814.6,8.15
|
||||
15,매도 청산,2025-01-27 12:30,Short Sell,3.0109,3503.3,10810.13281,253.5,2.34,491.04,4.54,-37.61,-0.35,1068.1,10.68
|
||||
15,매도 진입,2025-01-27 07:30,Short,3.0857,3503.3,10810.13281,253.5,2.34,491.04,4.54,-37.61,-0.35,1068.1,10.68
|
||||
16,매도 청산,2025-01-30 00:30,Short Sell,3.0641,3611.3,11063.57868,-10.66,-0.10,135.33,1.22,-65.1,-0.59,1057.44,10.57
|
||||
16,매도 진입,2025-01-29 22:00,Short,3.0636,3611.3,11063.57868,-10.66,-0.10,135.33,1.22,-65.1,-0.59,1057.44,10.57
|
||||
17,매수 청산,2025-02-05 05:30,Long Sell,2.5677,4187.7,11053.01538,-308.98,-2.79,289.14,2.61,-453.34,-4.10,748.46,7.48
|
||||
17,매수 진입,2025-02-04 22:00,Long,2.6394,4187.7,11053.01538,-308.98,-2.79,289.14,2.61,-453.34,-4.10,748.46,7.48
|
||||
18,매도 청산,2025-02-05 08:30,Short Sell,2.5169,4326,10744.0536,-152.71,-1.42,76.6,0.71,-284.19,-2.64,595.75,5.96
|
||||
18,매도 진입,2025-02-05 07:30,Short,2.4836,4326,10744.0536,-152.71,-1.42,76.6,0.71,-284.19,-2.64,595.75,5.96
|
||||
19,매도 청산,2025-02-06 05:00,Short Sell,2.4374,4429.7,10591.4127,-214.09,-2.02,217.69,2.05,-231.48,-2.18,381.66,3.82
|
||||
19,매도 진입,2025-02-06 03:00,Short,2.391,4429.7,10591.4127,-214.09,-2.02,217.69,2.05,-231.48,-2.18,381.66,3.82
|
||||
20,매수 청산,2025-02-09 20:00,Long Sell,2.4533,4143.8,10377.31834,-219.55,-2.11,11.18,0.11,-230.4,-2.22,162.11,1.62
|
||||
20,매수 진입,2025-02-09 13:00,Long,2.5043,4143.8,10377.31834,-219.55,-2.11,11.18,0.11,-230.4,-2.22,162.11,1.62
|
||||
21,매수 청산,2025-02-11 22:30,Long Sell,2.4777,4101.2,10157.85216,-4.44,-0.04,207.15,2.04,-38.92,-0.38,157.67,1.58
|
||||
21,매수 진입,2025-02-11 14:00,Long,2.4768,4101.2,10157.85216,-4.44,-0.04,207.15,2.04,-38.92,-0.38,157.67,1.58
|
||||
22,매수 청산,2025-02-15 09:00,Long Sell,2.7388,4072.6,10153.39906,992.11,9.77,1388.77,13.67,-6.91,-0.07,1149.79,11.50
|
||||
22,매수 진입,2025-02-14 06:30,Long,2.4931,4072.6,10153.39906,992.11,9.77,1388.77,13.67,-6.91,-0.07,1149.79,11.50
|
||||
23,매수 청산,2025-02-15 17:00,Long Sell,2.7648,3946.9,11145.25622,-241.69,-2.17,12.91,0.12,-307.97,-2.76,908.1,9.08
|
||||
23,매수 진입,2025-02-15 12:00,Long,2.8238,3946.9,11145.25622,-241.69,-2.17,12.91,0.12,-307.97,-2.76,908.1,9.08
|
||||
24,매수 청산,2025-02-16 00:30,Long Sell,2.7813,3884.6,10903.68374,-108.13,-0.99,60.51,0.55,-191.6,-1.76,799.97,8.00
|
||||
24,매수 진입,2025-02-15 20:00,Long,2.8069,3884.6,10903.68374,-108.13,-0.99,60.51,0.55,-191.6,-1.76,799.97,8.00
|
||||
25,매도 청산,2025-02-17 18:30,Short Sell,2.6767,3978.3,10795.51488,138.22,1.28,305.59,2.83,-18.64,-0.17,938.19,9.38
|
||||
25,매도 진입,2025-02-17 13:00,Short,2.7136,3978.3,10795.51488,138.22,1.28,305.59,2.83,-18.64,-0.17,938.19,9.38
|
||||
26,매도 청산,2025-02-22 07:00,Short Sell,2.5363,4183.5,10933.57725,314.35,2.87,442.42,4.04,-141.17,-1.29,1252.54,12.53
|
||||
26,매도 진입,2025-02-22 01:00,Short,2.6135,4183.5,10933.57725,314.35,2.87,442.42,4.04,-141.17,-1.29,1252.54,12.53
|
||||
27,매도 청산,2025-02-24 15:00,Short Sell,2.4945,4430.8,11248.02888,186.48,1.66,415.54,3.69,-7.16,-0.06,1439.02,14.39
|
||||
27,매도 진입,2025-02-24 12:00,Short,2.5386,4430.8,11248.02888,186.48,1.66,415.54,3.69,-7.16,-0.06,1439.02,14.39
|
||||
28,매도 청산,2025-02-28 03:00,Short Sell,2.2029,5237.7,11434.42287,-112.9,-0.99,76.61,0.67,-167.99,-1.47,1326.12,13.26
|
||||
28,매도 진입,2025-02-28 00:00,Short,2.1831,5237.7,11434.42287,-112.9,-0.99,76.61,0.67,-167.99,-1.47,1326.12,13.26
|
||||
29,매수 청산,2025-03-01 20:30,Long Sell,2.1511,5142.4,11321.50784,-268.64,-2.37,88.55,0.78,-317.19,-2.80,1057.48,10.57
|
||||
29,매수 진입,2025-03-01 11:30,Long,2.2016,5142.4,11321.50784,-268.64,-2.37,88.55,0.78,-317.19,-2.80,1057.48,10.57
|
||||
30,매수 청산,2025-03-03 11:30,Long Sell,2.768,4635.7,11052.89951,1769.16,16.00,2848.39,25.76,-22.96,-0.21,2826.64,28.27
|
||||
30,매수 진입,2025-03-03 00:30,Long,2.3843,4635.7,11052.89951,1769.16,16.00,2848.39,25.76,-22.96,-0.21,2826.64,28.27
|
||||
31,매도 청산,2025-03-04 08:30,Short Sell,2.4107,5118.1,12821.35231,473.08,3.69,1052.27,8.20,-146.39,-1.14,3299.72,33.00
|
||||
31,매도 진입,2025-03-04 03:30,Short,2.5051,5118.1,12821.35231,473.08,3.69,1052.27,8.20,-146.39,-1.14,3299.72,33.00
|
||||
32,매도 청산,2025-03-07 11:30,Short Sell,2.4659,5348.1,13294.30698,95.83,0.72,143.89,1.08,-51.31,-0.39,3395.56,33.96
|
||||
32,매도 진입,2025-03-07 11:00,Short,2.4858,5348.1,13294.30698,95.83,0.72,143.89,1.08,-51.31,-0.39,3395.56,33.96
|
||||
33,매도 청산,2025-03-11 01:30,Short Sell,2.1018,6322.6,13390.00228,90.49,0.68,137.53,1.03,-67.95,-0.51,3486.05,34.86
|
||||
33,매도 진입,2025-03-11 01:00,Short,2.1178,6322.6,13390.00228,90.49,0.68,137.53,1.03,-67.95,-0.51,3486.05,34.86
|
||||
34,매수 청산,2025-03-12 14:00,Long Sell,2.1579,6180.1,13480.65213,-155.34,-1.15,394.46,2.92,-240.24,-1.78,3330.71,33.31
|
||||
34,매수 진입,2025-03-12 04:00,Long,2.1813,6180.1,13480.65213,-155.34,-1.15,394.46,2.92,-240.24,-1.78,3330.71,33.31
|
||||
35,매수 청산,2025-03-14 02:30,Long Sell,2.2465,5843.1,13325.18955,-209.25,-1.57,381.48,2.86,-234.38,-1.76,3121.46,31.21
|
||||
35,매수 진입,2025-03-13 19:00,Long,2.2805,5843.1,13325.18955,-209.25,-1.57,381.48,2.86,-234.38,-1.76,3121.46,31.21
|
||||
36,매수 청산,2025-03-18 08:30,Long Sell,2.3373,5526.1,13116.19835,-210.46,-1.60,17.41,0.13,-207.5,-1.58,2911,29.11
|
||||
36,매수 진입,2025-03-18 04:00,Long,2.3735,5526.1,13116.19835,-210.46,-1.60,17.41,0.13,-207.5,-1.58,2911,29.11
|
||||
37,매도 청산,2025-03-18 14:30,Short Sell,2.2937,5600.2,12905.6609,50.18,0.39,161.72,1.25,-23.64,-0.18,2961.19,29.61
|
||||
37,매도 진입,2025-03-18 11:30,Short,2.3045,5600.2,12905.6609,50.18,0.39,161.72,1.25,-23.64,-0.18,2961.19,29.61
|
||||
38,매도 청산,2025-03-21 03:30,Short Sell,2.4297,5339.3,12955.81145,-27.46,-0.21,1.76,0.01,-114.1,-0.88,2933.73,29.34
|
||||
38,매도 진입,2025-03-21 03:00,Short,2.4265,5339.3,12955.81145,-27.46,-0.21,1.76,0.01,-114.1,-0.88,2933.73,29.34
|
||||
39,매도 청산,2025-03-23 01:30,Short Sell,2.3854,5428.7,12928.44905,-31.52,-0.24,71.37,0.55,-43.17,-0.33,2902.21,29.02
|
||||
39,매도 진입,2025-03-22 22:30,Short,2.3815,5428.7,12928.44905,-31.52,-0.24,71.37,0.55,-43.17,-0.33,2902.21,29.02
|
||||
40,매수 청산,2025-03-24 23:00,Long Sell,2.4568,5288,12896.9032,84.3,0.65,193.67,1.50,-138.42,-1.07,2986.51,29.87
|
||||
40,매수 진입,2025-03-24 09:00,Long,2.4389,5288,12896.9032,84.3,0.65,193.67,1.50,-138.42,-1.07,2986.51,29.87
|
||||
41,매도 청산,2025-03-27 06:00,Short Sell,2.3821,5388.4,12981.19444,135.16,1.04,263.69,2.03,-45.61,-0.35,3121.67,31.22
|
||||
41,매도 진입,2025-03-27 00:00,Short,2.4091,5388.4,12981.19444,135.16,1.04,263.69,2.03,-45.61,-0.35,3121.67,31.22
|
||||
42,매도 청산,2025-03-31 10:30,Short Sell,2.1239,6215.1,13116.34704,-94.43,-0.72,180.58,1.38,-133.9,-1.02,3027.24,30.27
|
||||
42,매도 진입,2025-03-31 09:30,Short,2.1104,6215.1,13116.34704,-94.43,-0.72,180.58,1.38,-133.9,-1.02,3027.24,30.27
|
||||
43,매도 청산,2025-04-01 09:30,Short Sell,2.095,6242.5,13021.855,-66.62,-0.51,60.96,0.47,-133.18,-1.02,2960.61,29.61
|
||||
43,매도 진입,2025-04-01 06:00,Short,2.086,6242.5,13021.855,-66.62,-0.51,60.96,0.47,-133.18,-1.02,2960.61,29.61
|
||||
44,매도 청산,2025-04-03 09:30,Short Sell,2.0465,6417,12955.2813,-187.54,-1.45,110.32,0.85,-263.15,-2.03,2773.07,27.73
|
||||
44,매도 진입,2025-04-03 08:00,Short,2.0189,6417,12955.2813,-187.54,-1.45,110.32,0.85,-263.15,-2.03,2773.07,27.73
|
||||
45,매도 청산,2025-04-06 18:30,Short Sell,2.1016,6050.5,12767.7651,41.84,0.33,152.21,1.19,-17.21,-0.13,2814.91,28.15
|
||||
45,매도 진입,2025-04-06 16:00,Short,2.1102,6050.5,12767.7651,41.84,0.33,152.21,1.19,-17.21,-0.13,2814.91,28.15
|
||||
46,매수 청산,2025-04-09 02:00,Long Sell,1.8637,6540.6,12809.7651,-630.05,-4.92,101.49,0.79,-674.88,-5.27,2184.86,21.85
|
||||
46,매수 진입,2025-04-08 21:00,Long,1.9585,6540.6,12809.7651,-630.05,-4.92,101.49,0.79,-674.88,-5.27,2184.86,21.85
|
||||
47,매수 청산,2025-04-13 23:30,Long Sell,2.1455,5461.8,12179.814,-471.08,-3.87,86.89,0.71,-528.66,-4.34,1713.78,17.14
|
||||
47,매수 진입,2025-04-13 19:00,Long,2.23,5461.8,12179.814,-471.08,-3.87,86.89,0.71,-528.66,-4.34,1713.78,17.14
|
||||
48,매도 청산,2025-04-19 10:30,Short Sell,2.0702,5684.8,11708.98256,-69.08,-0.59,0,0.00,-71.2,-0.61,1644.7,16.45
|
||||
48,매도 진입,2025-04-19 09:30,Short,2.0597,5684.8,11708.98256,-69.08,-0.59,0,0.00,-71.2,-0.61,1644.7,16.45
|
||||
49,매수 청산,2025-04-20 01:30,Long Sell,2.0755,5565.1,11639.96316,-98.87,-0.85,25.4,0.22,-94.81,-0.81,1545.82,15.46
|
||||
49,매수 진입,2025-04-19 13:30,Long,2.0916,5565.1,11639.96316,-98.87,-0.85,25.4,0.22,-94.81,-0.81,1545.82,15.46
|
||||
50,매도 청산,2025-04-27 07:30,Short Sell,2.196,5275,11541.1725,-51.98,-0.45,0,0.00,-70.03,-0.61,1493.85,14.94
|
||||
50,매도 진입,2025-04-27 07:00,Short,2.1879,5275,11541.1725,-51.98,-0.45,0,0.00,-70.03,-0.61,1493.85,14.94
|
||||
51,매수 청산,2025-04-28 23:00,Long Sell,2.2803,5043.7,11489.04423,2.91,0.03,418.57,3.64,-52.01,-0.45,1496.75,14.97
|
||||
51,매수 진입,2025-04-28 14:30,Long,2.2779,5043.7,11489.04423,2.91,0.03,418.57,3.64,-52.01,-0.45,1496.75,14.97
|
||||
52,매도 청산,2025-04-29 16:00,Short Sell,2.2822,5068.8,11491.98336,-85.26,-0.74,8.08,0.07,-92.29,-0.80,1411.5,14.11
|
||||
52,매도 진입,2025-04-29 14:00,Short,2.2672,5068.8,11491.98336,-85.26,-0.74,8.08,0.07,-92.29,-0.80,1411.5,14.11
|
||||
53,매도 청산,2025-04-30 00:00,Short Sell,2.2878,5022.4,11406.87488,-92.53,-0.81,0.46,0.00,-104.01,-0.91,1318.97,13.19
|
||||
53,매도 진입,2025-04-29 22:30,Short,2.2712,5022.4,11406.87488,-92.53,-0.81,0.46,0.00,-104.01,-0.91,1318.97,13.19
|
||||
54,매도 청산,2025-04-30 09:30,Short Sell,2.2436,5014.1,11314.31665,55.66,0.49,167.96,1.48,-72.22,-0.64,1374.62,13.75
|
||||
54,매도 진입,2025-04-30 06:30,Short,2.2565,5014.1,11314.31665,55.66,0.49,167.96,1.48,-72.22,-0.64,1374.62,13.75
|
||||
55,매도 청산,2025-05-03 05:30,Short Sell,2.2145,5148.5,11369.9474,-40.51,-0.36,37.67,0.33,-35.95,-0.32,1334.11,13.34
|
||||
55,매도 진입,2025-05-03 04:30,Short,2.2084,5148.5,11369.9474,-40.51,-0.36,37.67,0.33,-35.95,-0.32,1334.11,13.34
|
||||
56,매도 청산,2025-05-03 12:00,Short Sell,2.2183,5143,11329.5147,-88.3,-0.78,36.1,0.32,-89.91,-0.79,1245.81,12.46
|
||||
56,매도 진입,2025-05-03 07:00,Short,2.2029,5143,11329.5147,-88.3,-0.78,36.1,0.32,-89.91,-0.79,1245.81,12.46
|
||||
57,매도 청산,2025-05-03 21:30,Short Sell,2.1982,5109.4,11241.19094,0.72,0.01,79.3,0.71,-8.07,-0.07,1246.53,12.47
|
||||
57,매도 진입,2025-05-03 18:30,Short,2.2001,5109.4,11241.19094,0.72,0.01,79.3,0.71,-8.07,-0.07,1246.53,12.47
|
||||
58,매도 청산,2025-05-04 04:30,Short Sell,2.1908,5151.4,11241.90022,-52.8,-0.47,9.93,0.09,-48.28,-0.43,1193.73,11.94
|
||||
58,매도 진입,2025-05-04 04:00,Short,2.1823,5151.4,11241.90022,-52.8,-0.47,9.93,0.09,-48.28,-0.43,1193.73,11.94
|
||||
59,매도 청산,2025-05-06 02:30,Short Sell,2.141,5206.2,11189.16504,33.76,0.30,214.71,1.92,-29.99,-0.27,1227.49,12.27
|
||||
59,매도 진입,2025-05-05 20:30,Short,2.1492,5206.2,11189.16504,33.76,0.30,214.71,1.92,-29.99,-0.27,1227.49,12.27
|
||||
60,매수 청산,2025-05-10 01:00,Long Sell,2.349,4783.4,11222.81308,4.41,0.04,389.66,3.47,-80.07,-0.71,1231.9,12.32
|
||||
60,매수 진입,2025-05-09 16:30,Long,2.3462,4783.4,11222.81308,4.41,0.04,389.66,3.47,-80.07,-0.71,1231.9,12.32
|
||||
61,매수 청산,2025-05-14 23:30,Long Sell,2.5613,4288,11227.2704,-253.3,-2.26,131.44,1.17,-304.22,-2.71,978.6,9.79
|
||||
61,매수 진입,2025-05-14 17:00,Long,2.6183,4288,11227.2704,-253.3,-2.26,131.44,1.17,-304.22,-2.71,978.6,9.79
|
||||
62,매도 청산,2025-05-19 18:00,Short Sell,2.3065,4673.2,10974.07556,186.64,1.70,297.97,2.71,-70.75,-0.64,1165.24,11.65
|
||||
62,매도 진입,2025-05-19 13:00,Short,2.3483,4673.2,10974.07556,186.64,1.70,297.97,2.71,-70.75,-0.64,1165.24,11.65
|
||||
63,매도 청산,2025-05-20 19:00,Short Sell,2.3504,4737.1,11160.6076,17.61,0.16,38.17,0.34,-16.31,-0.15,1182.85,11.83
|
||||
63,매도 진입,2025-05-20 18:00,Short,2.356,4737.1,11160.6076,17.61,0.16,38.17,0.34,-16.31,-0.15,1182.85,11.83
|
||||
64,매수 청산,2025-05-22 04:00,Long Sell,2.3586,4660.1,11178.18187,-195.74,-1.75,133.47,1.19,-297.59,-2.66,987.11,9.87
|
||||
64,매수 진입,2025-05-22 01:00,Long,2.3987,4660.1,11178.18187,-195.74,-1.75,133.47,1.19,-297.59,-2.66,987.11,9.87
|
||||
65,매수 청산,2025-05-22 16:30,Long Sell,2.3927,4570,10982.624,-56.75,-0.52,87.46,0.80,-71.12,-0.65,930.36,9.30
|
||||
65,매수 진입,2025-05-22 08:30,Long,2.4032,4570,10982.624,-56.75,-0.52,87.46,0.80,-71.12,-0.65,930.36,9.30
|
||||
66,매수 청산,2025-05-23 00:00,Long Sell,2.425,4459.9,10925.86302,-119.3,-1.09,0,0.00,-185.89,-1.70,811.06,8.11
|
||||
66,매수 진입,2025-05-22 18:30,Long,2.4498,4459.9,10925.86302,-119.3,-1.09,0,0.00,-185.89,-1.70,811.06,8.11
|
||||
67,매수 청산,2025-05-23 16:30,Long Sell,2.4309,4416.6,10806.53688,-78.84,-0.73,137.89,1.28,-164.2,-1.52,732.22,7.32
|
||||
67,매수 진입,2025-05-23 10:30,Long,2.4468,4416.6,10806.53688,-78.84,-0.73,137.89,1.28,-164.2,-1.52,732.22,7.32
|
||||
68,매수 청산,2025-06-04 07:00,Long Sell,2.2415,4839.8,10727.90068,111.88,1.04,314.17,2.93,-58.01,-0.54,844.1,8.44
|
||||
68,매수 진입,2025-06-03 21:30,Long,2.2166,4839.8,10727.90068,111.88,1.04,314.17,2.93,-58.01,-0.54,844.1,8.44
|
||||
69,매수 청산,2025-06-04 23:00,Long Sell,2.2339,4799.7,10839.64248,-126.22,-1.16,20.62,0.19,-129.61,-1.20,717.88,7.18
|
||||
69,매수 진입,2025-06-04 19:00,Long,2.2584,4799.7,10839.64248,-126.22,-1.16,20.62,0.19,-129.61,-1.20,717.88,7.18
|
||||
70,매도 청산,2025-06-06 08:00,Short Sell,2.0981,4934.6,10713.51006,351.8,3.28,546.91,5.10,-10.7,-0.10,1069.68,10.70
|
||||
70,매도 진입,2025-06-06 03:00,Short,2.1711,4934.6,10713.51006,351.8,3.28,546.91,5.10,-10.7,-0.10,1069.68,10.70
|
||||
71,매수 청산,2025-06-08 01:30,Long Sell,2.1825,5076.9,11065.10355,6.37,0.06,52.94,0.48,-49.61,-0.45,1076.05,10.76
|
||||
71,매수 진입,2025-06-07 13:00,Long,2.1795,5076.9,11065.10355,6.37,0.06,52.94,0.48,-49.61,-0.45,1076.05,10.76
|
||||
72,매수 청산,2025-06-10 12:30,Long Sell,2.3018,4880.8,11071.60672,154.1,1.39,292.81,2.64,-145.48,-1.31,1230.15,12.30
|
||||
72,매수 진입,2025-06-09 21:00,Long,2.2684,4880.8,11071.60672,154.1,1.39,292.81,2.64,-145.48,-1.31,1230.15,12.30
|
||||
73,매수 청산,2025-06-11 00:30,Long Sell,2.2827,4849.3,11225.64457,-165.07,-1.47,0,0.00,-220.77,-1.97,1065.08,10.65
|
||||
73,매수 진입,2025-06-10 22:00,Long,2.3149,4849.3,11225.64457,-165.07,-1.47,0,0.00,-220.77,-1.97,1065.08,10.65
|
||||
74,매수 청산,2025-06-17 08:00,Long Sell,2.2743,5079.9,11060.46627,483.7,4.37,802.26,7.25,-38.97,-0.35,1548.79,15.49
|
||||
74,매수 진입,2025-06-16 11:30,Long,2.1773,5079.9,11060.46627,483.7,4.37,802.26,7.25,-38.97,-0.35,1548.79,15.49
|
||||
75,매도 청산,2025-06-21 03:30,Short Sell,2.1296,5430.7,11544.03899,-30.42,-0.26,213.15,1.85,-43.18,-0.37,1518.36,15.18
|
||||
75,매도 진입,2025-06-21 01:30,Short,2.1257,5430.7,11544.03899,-30.42,-0.26,213.15,1.85,-43.18,-0.37,1518.36,15.18
|
||||
76,매수 청산,2025-06-24 21:30,Long Sell,2.1754,5626.3,11513.66032,716.29,6.22,913.61,7.93,-7.98,-0.07,2234.65,22.35
|
||||
76,매수 진입,2025-06-24 04:00,Long,2.0464,5626.3,11513.66032,716.29,6.22,913.61,7.93,-7.98,-0.07,2234.65,22.35
|
||||
77,매도 청산,2025-06-27 02:30,Short Sell,2.118,5620,12229.682,316.87,2.59,447.52,3.66,-32.99,-0.27,2551.52,25.52
|
||||
77,매도 진입,2025-06-26 19:00,Short,2.1761,5620,12229.682,316.87,2.59,447.52,3.66,-32.99,-0.27,2551.52,25.52
|
||||
78,매수 청산,2025-07-04 01:00,Long Sell,2.2438,5564.6,12546.50362,-70.67,-0.56,319.4,2.54,-90.71,-0.72,2480.86,24.81
|
||||
78,매수 진입,2025-07-03 14:00,Long,2.2547,5564.6,12546.50362,-70.67,-0.56,319.4,2.54,-90.71,-0.72,2480.86,24.81
|
||||
79,매도 청산,2025-07-06 05:00,Short Sell,2.215,5662.6,12475.84032,-76.83,-0.62,3.5,0.03,-71.81,-0.58,2404.03,24.04
|
||||
79,매도 진입,2025-07-06 03:30,Short,2.2032,5662.6,12475.84032,-76.83,-0.62,3.5,0.03,-71.81,-0.58,2404.03,24.04
|
||||
80,매수 청산,2025-07-12 07:00,Long Sell,2.7397,5022.9,12399.02865,1351.75,10.90,2535.62,20.44,-23.04,-0.19,3755.78,37.56
|
||||
80,매수 진입,2025-07-11 01:30,Long,2.4685,5022.9,12399.02865,1351.75,10.90,2535.62,20.44,-23.04,-0.19,3755.78,37.56
|
||||
81,매수 청산,2025-07-12 23:00,Long Sell,2.7753,4864.4,13750.19948,-260.93,-1.90,94.71,0.69,-384.44,-2.79,3494.85,34.95
|
||||
81,매수 진입,2025-07-12 19:30,Long,2.8267,4864.4,13750.19948,-260.93,-1.90,94.71,0.69,-384.44,-2.79,3494.85,34.95
|
||||
82,매도 청산,2025-07-13 02:30,Short Sell,2.7314,4985,13489.41,-137.46,-1.02,5.07,0.04,-172.39,-1.28,3357.38,33.57
|
||||
82,매도 진입,2025-07-13 02:00,Short,2.706,4985,13489.41,-137.46,-1.02,5.07,0.04,-172.39,-1.28,3357.38,33.57
|
||||
83,매수 청산,2025-07-15 02:30,Long Sell,2.9571,4632.4,13351.96652,335.68,2.51,701.56,5.25,-25.26,-0.19,3693.07,36.93
|
||||
83,매수 진입,2025-07-14 11:30,Long,2.8823,4632.4,13351.96652,335.68,2.51,701.56,5.25,-25.26,-0.19,3693.07,36.93
|
||||
84,매수 청산,2025-07-18 16:30,Long Sell,3.5301,4393.9,13687.43789,1811.79,13.23,2396.67,17.50,-42.82,-0.31,5504.86,55.05
|
||||
84,매수 진입,2025-07-17 14:30,Long,3.1151,4393.9,13687.43789,1811.79,13.23,2396.67,17.50,-42.82,-0.31,5504.86,55.05
|
||||
85,매수 청산,2025-07-21 22:00,Long Sell,3.5381,4346.5,15498.31505,-132.31,-0.85,55.96,0.36,-295.24,-1.90,5372.54,53.73
|
||||
85,매수 진입,2025-07-21 15:30,Long,3.5657,4346.5,15498.31505,-132.31,-0.85,55.96,0.36,-295.24,-1.90,5372.54,53.73
|
||||
86,매수 청산,2025-07-22 05:00,Long Sell,3.5474,4220.7,15366.30249,-405.93,-2.64,25.51,0.17,-478.86,-3.12,4966.62,49.67
|
||||
86,매수 진입,2025-07-22 00:00,Long,3.6407,4220.7,15366.30249,-405.93,-2.64,25.51,0.17,-478.86,-3.12,4966.62,49.67
|
||||
87,매도 청산,2025-07-31 08:30,Short Sell,3.0938,4872.8,14960.47056,-127.01,-0.85,9.12,0.06,-128.29,-0.86,4839.6,48.40
|
||||
87,매도 진입,2025-07-31 07:30,Short,3.0702,4872.8,14960.47056,-127.01,-0.85,9.12,0.06,-128.29,-0.86,4839.6,48.40
|
||||
88,매도 청산,2025-08-03 05:30,Short Sell,2.7926,5039.1,14833.59867,749.85,5.05,1008.94,6.80,-97.14,-0.65,5589.45,55.89
|
||||
88,매도 진입,2025-08-02 20:00,Short,2.9437,5039.1,14833.59867,749.85,5.05,1008.94,6.80,-97.14,-0.65,5589.45,55.89
|
||||
89,매수 청산,2025-08-06 00:00,Long Sell,2.9773,5076.7,15582.93065,-480.35,-3.08,48.09,0.31,-567.21,-3.64,5109.1,51.09
|
||||
89,매수 진입,2025-08-05 19:30,Long,3.0695,5076.7,15582.93065,-480.35,-3.08,48.09,0.31,-567.21,-3.64,5109.1,51.09
|
||||
90,매도 청산,2025-08-06 01:30,Short Sell,2.985,5074.7,15108.90431,-51.18,-0.34,98.5,0.65,-93.33,-0.62,5057.92,50.58
|
||||
90,매도 진입,2025-08-06 00:00,Short,2.9773,5074.7,15108.90431,-51.18,-0.34,98.5,0.65,-93.33,-0.62,5057.92,50.58
|
||||
91,매수 청산,2025-08-08 19:30,Long Sell,3.3099,4832.5,15051.78775,930.89,6.18,1287.16,8.55,-64.01,-0.43,5988.81,59.89
|
||||
91,매수 진입,2025-08-08 05:30,Long,3.1147,4832.5,15051.78775,930.89,6.18,1287.16,8.55,-64.01,-0.43,5988.81,59.89
|
||||
92,매수 청산,2025-08-14 00:30,Long Sell,3.2451,4862.3,15982.3801,-216.43,-1.35,227.97,1.43,-297.16,-1.86,5772.37,57.72
|
||||
92,매수 진입,2025-08-13 18:00,Long,3.287,4862.3,15982.3801,-216.43,-1.35,227.97,1.43,-297.16,-1.86,5772.37,57.72
|
||||
93,매도 청산,2025-08-15 00:00,Short Sell,3.1226,4871,15765.9657,543.39,3.45,1164.2,7.38,-51.61,-0.33,6315.76,63.16
|
||||
93,매도 진입,2025-08-14 18:30,Short,3.2367,4871,15765.9657,543.39,3.45,1164.2,7.38,-51.61,-0.33,6315.76,63.16
|
||||
94,매수 청산,2025-08-18 03:00,Long Sell,3.1078,5203.6,16309.12312,-150.37,-0.92,62.16,0.38,-160.03,-0.98,6165.39,61.65
|
||||
94,매수 진입,2025-08-17 14:00,Long,3.1342,5203.6,16309.12312,-150.37,-0.92,62.16,0.38,-160.03,-0.98,6165.39,61.65
|
||||
95,매도 청산,2025-08-18 07:00,Short Sell,3.1003,5206.3,16158.79331,4.78,0.03,128.38,0.79,-28.85,-0.18,6170.18,61.70
|
||||
95,매도 진입,2025-08-18 03:30,Short,3.1037,5206.3,16158.79331,4.78,0.03,128.38,0.79,-28.85,-0.18,6170.18,61.70
|
||||
96,매도 청산,2025-08-20 06:00,Short Sell,2.9038,5481.2,16163.51068,234.37,1.45,414.49,2.56,-108.96,-0.67,6404.55,64.05
|
||||
96,매도 진입,2025-08-19 23:30,Short,2.9489,5481.2,16163.51068,234.37,1.45,414.49,2.56,-108.96,-0.67,6404.55,64.05
|
||||
97,매수 청산,2025-08-23 09:30,Long Sell,3.059,5301.4,16397.76034,-193.82,-1.18,59.18,0.36,-263.15,-1.60,6210.72,62.11
|
||||
97,매수 진입,2025-08-23 02:30,Long,3.0931,5301.4,16397.76034,-193.82,-1.18,59.18,0.36,-263.15,-1.60,6210.72,62.11
|
||||
98,매수 청산,2025-08-23 21:00,Long Sell,3.0433,5287.8,16203.93432,-124.49,-0.77,49.57,0.31,-191.55,-1.18,6086.23,60.86
|
||||
98,매수 진입,2025-08-23 16:00,Long,3.0644,5287.8,16203.93432,-124.49,-0.77,49.57,0.31,-191.55,-1.18,6086.23,60.86
|
||||
99,매도 청산,2025-08-25 12:00,Short Sell,3.0443,5371.3,16079.52368,-285.3,-1.77,0,0.00,-322.8,-2.01,5800.93,58.01
|
||||
99,매도 진입,2025-08-25 10:00,Short,2.9936,5371.3,16079.52368,-285.3,-1.77,0,0.00,-322.8,-2.01,5800.93,58.01
|
||||
100,매수 청산,2025-08-27 10:30,Long Sell,2.9962,5340.1,15794.41377,192.88,1.22,655.85,4.15,-58.12,-0.37,5993.81,59.94
|
||||
100,매수 진입,2025-08-27 02:30,Long,2.9577,5340.1,15794.41377,192.88,1.22,655.85,4.15,-58.12,-0.37,5993.81,59.94
|
||||
101,매도 청산,2025-08-28 11:00,Short Sell,2.9853,5385.1,15987.28488,-101.68,-0.64,77.61,0.49,-118.4,-0.74,5892.13,58.92
|
||||
101,매도 진입,2025-08-28 09:00,Short,2.9688,5385.1,15987.28488,-101.68,-0.64,77.61,0.49,-118.4,-0.74,5892.13,58.92
|
||||
102,매도 청산,2025-08-29 04:00,Short Sell,2.9785,5325.9,15885.56193,9.67,0.06,23.47,0.15,-27.66,-0.17,5901.8,59.02
|
||||
102,매도 진입,2025-08-29 03:30,Short,2.9827,5325.9,15885.56193,9.67,0.06,23.47,0.15,-27.66,-0.17,5901.8,59.02
|
||||
103,매수 청산,2025-08-31 16:00,Long Sell,2.8235,5553.3,15895.21059,-228.1,-1.43,5.3,0.03,-224.6,-1.41,5673.7,56.74
|
||||
103,매수 진입,2025-08-31 11:00,Long,2.8623,5553.3,15895.21059,-228.1,-1.43,5.3,0.03,-224.6,-1.41,5673.7,56.74
|
||||
104,매도 청산,2025-09-02 01:00,Short Sell,2.7624,5675.9,15667.18677,-24.46,-0.16,41.41,0.26,-60.76,-0.39,5649.24,56.49
|
||||
104,매도 진입,2025-09-02 00:30,Short,2.7603,5675.9,15667.18677,-24.46,-0.16,41.41,0.26,-60.76,-0.39,5649.24,56.49
|
||||
105,매도 청산,2025-09-02 04:00,Short Sell,2.756,5668.1,15642.82238,9.03,0.06,218.2,1.39,-10.79,-0.07,5658.28,56.58
|
||||
105,매도 진입,2025-09-02 01:30,Short,2.7598,5668.1,15642.82238,9.03,0.06,218.2,1.39,-10.79,-0.07,5658.28,56.58
|
||||
106,매수 청산,2025-09-03 13:30,Long Sell,2.8334,5515.1,15651.8538,-37.88,-0.24,146.51,0.94,-316.21,-2.02,5620.4,56.20
|
||||
106,매수 진입,2025-09-03 00:00,Long,2.838,5515.1,15651.8538,-37.88,-0.24,146.51,0.94,-316.21,-2.02,5620.4,56.20
|
||||
107,매수 청산,2025-09-04 05:30,Long Sell,2.8563,5461,15614.0912,-28.32,-0.18,141.75,0.91,-98.54,-0.63,5592.07,55.92
|
||||
107,매수 진입,2025-09-03 19:30,Long,2.8592,5461,15614.0912,-28.32,-0.18,141.75,0.91,-98.54,-0.63,5592.07,55.92
|
||||
108,매도 청산,2025-09-05 02:30,Short Sell,2.8194,5540,15585.682,-46.28,-0.30,60.8,0.39,-42.8,-0.27,5545.8,55.46
|
||||
108,매도 진입,2025-09-05 00:30,Short,2.8133,5540,15585.682,-46.28,-0.30,60.8,0.39,-42.8,-0.27,5545.8,55.46
|
||||
109,매도 청산,2025-09-06 12:00,Short Sell,2.8232,5521.6,15539.43888,-61.59,-0.40,2.07,0.01,-70.27,-0.45,5484.2,54.84
|
||||
109,매도 진입,2025-09-06 11:30,Short,2.8143,5521.6,15539.43888,-61.59,-0.40,2.07,0.01,-70.27,-0.45,5484.2,54.84
|
||||
110,매수 청산,2025-09-09 23:30,Long Sell,2.9894,5175.2,15477.98816,-19.62,-0.13,231.35,1.49,-47.59,-0.31,5464.58,54.65
|
||||
110,매수 진입,2025-09-09 15:30,Long,2.9908,5175.2,15477.98816,-19.62,-0.13,231.35,1.49,-47.59,-0.31,5464.58,54.65
|
||||
111,매수 청산,2025-09-12 20:00,Long Sell,3.0433,5112.7,15458.24845,88.82,0.57,247.41,1.60,-70.6,-0.46,5553.4,55.53
|
||||
111,매수 진입,2025-09-12 04:00,Long,3.0235,5112.7,15458.24845,88.82,0.57,247.41,1.60,-70.6,-0.46,5553.4,55.53
|
||||
112,매수 청산,2025-09-14 00:30,Long Sell,3.142,5071.6,15546.9898,375.38,2.41,601.36,3.87,-34.62,-0.22,5928.79,59.29
|
||||
112,매수 진입,2025-09-13 03:00,Long,3.0655,5071.6,15546.9898,375.38,2.41,601.36,3.87,-34.62,-0.22,5928.79,59.29
|
||||
113,매도 청산,2025-09-15 01:30,Short Sell,3.0432,5137.4,15922.34382,275.59,1.73,450.86,2.83,-54.15,-0.34,6204.37,62.04
|
||||
113,매도 진입,2025-09-14 14:00,Short,3.0993,5137.4,15922.34382,275.59,1.73,450.86,2.83,-54.15,-0.34,6204.37,62.04
|
||||
114,매수 청산,2025-09-17 09:30,Long Sell,3.0398,5316.8,16197.6312,-48.57,-0.30,57.32,0.35,-74.53,-0.46,6155.81,61.56
|
||||
114,매수 진입,2025-09-17 02:00,Long,3.0465,5316.8,16197.6312,-48.57,-0.30,57.32,0.35,-74.53,-0.46,6155.81,61.56
|
||||
115,매수 청산,2025-09-19 02:00,Long Sell,3.1179,5171.5,16149.04305,-37.73,-0.23,80.42,0.50,-110.41,-0.68,6118.08,61.18
|
||||
115,매수 진입,2025-09-18 19:00,Long,3.1227,5171.5,16149.04305,-37.73,-0.23,80.42,0.50,-110.41,-0.68,6118.08,61.18
|
||||
116,매도 청산,2025-09-22 17:30,Short Sell,2.8113,5452.3,16111.5465,770.92,4.78,1395.89,8.66,-12.99,-0.08,6889,68.89
|
||||
116,매도 진입,2025-09-22 09:30,Short,2.955,5452.3,16111.5465,770.92,4.78,1395.89,8.66,-12.99,-0.08,6889,68.89
|
||||
117,매수 청산,2025-09-28 11:30,Long Sell,2.7909,6010.4,16882.01152,-121.05,-0.72,112.85,0.67,-147.4,-0.87,6767.95,67.68
|
||||
117,매수 진입,2025-09-28 07:30,Long,2.8088,6010.4,16882.01152,-121.05,-0.72,112.85,0.67,-147.4,-0.87,6767.95,67.68
|
||||
118,매수 청산,2025-09-29 22:00,Long Sell,2.8498,5796.7,16761.15805,-255.03,-1.52,71.55,0.43,-268.72,-1.60,6512.91,65.13
|
||||
118,매수 진입,2025-09-29 16:30,Long,2.8915,5796.7,16761.15805,-255.03,-1.52,71.55,0.43,-268.72,-1.60,6512.91,65.13
|
||||
119,매도 청산,2025-09-30 21:00,Short Sell,2.8472,5780.7,16506.21078,34.22,0.21,114.21,0.69,-34.35,-0.21,6547.13,65.47
|
||||
119,매도 진입,2025-09-30 18:00,Short,2.8554,5780.7,16506.21078,34.22,0.21,114.21,0.69,-34.35,-0.21,6547.13,65.47
|
||||
120,매수 청산,2025-10-03 00:00,Long Sell,2.9575,5586.8,16540.28008,-30.54,-0.18,190.04,1.15,-83.16,-0.50,6516.58,65.17
|
||||
120,매수 진입,2025-10-02 10:30,Long,2.9606,5586.8,16540.28008,-30.54,-0.18,190.04,1.15,-83.16,-0.50,6516.58,65.17
|
||||
121,매수 청산,2025-10-03 08:30,Long Sell,3.0311,5456.7,16509.79152,16.79,0.10,395.55,2.39,-65.54,-0.40,6533.38,65.33
|
||||
121,매수 진입,2025-10-03 01:30,Long,3.0256,5456.7,16509.79152,16.79,0.10,395.55,2.39,-65.54,-0.40,6533.38,65.33
|
||||
122,매수 청산,2025-10-04 06:30,Long Sell,3.0496,5386.2,16526.47746,-113.9,-0.69,136.66,0.83,-307.16,-1.86,6419.47,64.19
|
||||
122,매수 진입,2025-10-04 01:00,Long,3.0683,5386.2,16526.47746,-113.9,-0.69,136.66,0.83,-307.16,-1.86,6419.47,64.19
|
||||
123,매도 청산,2025-10-09 13:30,Short Sell,2.8471,5778.9,16412.65389,-53.6,-0.33,82.43,0.50,-56.84,-0.35,6365.87,63.66
|
||||
123,매도 진입,2025-10-09 11:30,Short,2.8401,5778.9,16412.65389,-53.6,-0.33,82.43,0.50,-56.84,-0.35,6365.87,63.66
|
||||
124,매도 청산,2025-10-11 08:30,Short Sell,2.3904,5950.7,16359.06937,2122.28,12.97,8890.94,54.33,-101.16,-0.62,8488.16,84.88
|
||||
124,매도 진입,2025-10-11 00:30,Short,2.7491,5950.7,16359.06937,2122.28,12.97,8890.94,54.33,-101.16,-0.62,8488.16,84.88
|
||||
125,매도 청산,2025-10-16 03:00,Short Sell,2.4346,7519.5,18480.67515,158.99,0.86,330.23,1.79,-48.75,-0.26,8647.14,86.47
|
||||
125,매도 진입,2025-10-16 00:30,Short,2.4577,7519.5,18480.67515,158.99,0.86,330.23,1.79,-48.75,-0.26,8647.14,86.47
|
||||
126,매도 청산,2025-10-17 03:00,Short Sell,2.3672,7913.5,18639.4579,-108.33,-0.58,71.68,0.38,-349.32,-1.87,8538.81,85.39
|
||||
126,매도 진입,2025-10-17 01:00,Short,2.3554,7913.5,18639.4579,-108.33,-0.58,71.68,0.38,-349.32,-1.87,8538.81,85.39
|
||||
127,매도 청산,2025-10-19 19:00,Short Sell,2.3626,7990.7,18531.23237,-362.56,-1.96,0,0.00,-515.62,-2.78,8176.25,81.76
|
||||
127,매도 진입,2025-10-19 18:30,Short,2.3191,7990.7,18531.23237,-362.56,-1.96,0,0.00,-515.62,-2.78,8176.25,81.76
|
||||
128,매수 청산,2025-10-20 08:00,Long Sell,2.402,7539.6,18168.92808,-73.32,-0.40,67.37,0.37,-179.17,-0.99,8102.93,81.03
|
||||
128,매수 진입,2025-10-19 23:30,Long,2.4098,7539.6,18168.92808,-73.32,-0.40,67.37,0.37,-179.17,-0.99,8102.93,81.03
|
||||
129,매수 청산,2025-10-20 22:00,Long Sell,2.453,7433.6,18095.61248,124.48,0.69,334.71,1.85,-7.98,-0.04,8227.41,82.27
|
||||
129,매수 진입,2025-10-20 13:30,Long,2.4343,7433.6,18095.61248,124.48,0.69,334.71,1.85,-7.98,-0.04,8227.41,82.27
|
||||
130,매수 청산,2025-10-21 11:00,Long Sell,2.4679,7345,18220.007,-107.82,-0.59,520.82,2.86,-138.76,-0.76,8119.59,81.20
|
||||
130,매수 진입,2025-10-21 04:30,Long,2.4806,7345,18220.007,-107.82,-0.59,520.82,2.86,-138.76,-0.76,8119.59,81.20
|
||||
131,매도 청산,2025-10-22 11:00,Short Sell,2.4248,7497.4,18112.21892,-81.99,-0.45,82.72,0.46,-112.21,-0.62,8037.6,80.38
|
||||
131,매도 진입,2025-10-22 09:30,Short,2.4158,7497.4,18112.21892,-81.99,-0.45,82.72,0.46,-112.21,-0.62,8037.6,80.38
|
||||
132,매수 청산,2025-10-24 06:00,Long Sell,2.3851,7446.5,18030.21045,-283.88,-1.57,2.47,0.01,-327.41,-1.82,7753.72,77.54
|
||||
132,매수 진입,2025-10-24 03:00,Long,2.4213,7446.5,18030.21045,-283.88,-1.57,2.47,0.01,-327.41,-1.82,7753.72,77.54
|
||||
133,매수 청산,2025-10-27 01:30,Long Sell,2.6253,6880.9,17746.52919,303.57,1.71,566.77,3.19,-61.46,-0.35,8057.29,80.57
|
||||
133,매수 진입,2025-10-25 22:00,Long,2.5791,6880.9,17746.52919,303.57,1.71,566.77,3.19,-61.46,-0.35,8057.29,80.57
|
||||
134,매수 청산,2025-11-01 09:30,Long Sell,2.4997,7095.7,18050.04166,-327.24,-1.81,0,0.00,-344.98,-1.91,7730.05,77.30
|
||||
134,매수 진입,2025-11-01 05:00,Long,2.5438,7095.7,18050.04166,-327.24,-1.81,0,0.00,-344.98,-1.91,7730.05,77.30
|
||||
135,매도 청산,2025-11-02 12:00,Short Sell,2.5109,7080.6,17722.7418,-70.14,-0.40,50.26,0.28,-78.6,-0.44,7659.92,76.60
|
||||
135,매도 진입,2025-11-02 08:30,Short,2.503,7080.6,17722.7418,-70.14,-0.40,50.26,0.28,-78.6,-0.44,7659.92,76.60
|
||||
136,매도 청산,2025-11-03 14:30,Short Sell,2.4462,7168.3,17652.65558,103.49,0.59,325.55,1.84,-34.3,-0.19,7763.4,77.63
|
||||
136,매도 진입,2025-11-03 12:00,Short,2.4626,7168.3,17652.65558,103.49,0.59,325.55,1.84,-34.3,-0.19,7763.4,77.63
|
||||
137,매수 청산,2025-11-06 11:30,Long Sell,2.3331,7769.4,17756.18676,356.25,2.01,699.91,3.94,-149.28,-0.84,8119.65,81.20
|
||||
137,매수 진입,2025-11-06 02:00,Long,2.2854,7769.4,17756.18676,356.25,2.01,699.91,3.94,-149.28,-0.84,8119.65,81.20
|
||||
138,매수 청산,2025-11-11 08:00,Long Sell,2.532,7064.6,18112.22148,-239.05,-1.32,3.35,0.02,-374.6,-2.07,7880.6,78.81
|
||||
138,매수 진입,2025-11-11 06:00,Long,2.5638,7064.6,18112.22148,-239.05,-1.32,3.35,0.02,-374.6,-2.07,7880.6,78.81
|
||||
139,매도 청산,2025-11-13 03:30,Short Sell,2.3444,7546.3,17873.41155,167.64,0.94,312.81,1.75,-48.65,-0.27,8048.24,80.48
|
||||
139,매도 진입,2025-11-13 01:00,Short,2.3685,7546.3,17873.41155,167.64,0.94,312.81,1.75,-48.65,-0.27,8048.24,80.48
|
||||
140,매도 청산,2025-11-15 20:00,Short Sell,2.2653,7970.4,18041.0004,-28.79,-0.16,158.57,0.88,-56.63,-0.31,8019.45,80.19
|
||||
140,매도 진입,2025-11-15 17:30,Short,2.2635,7970.4,18041.0004,-28.79,-0.16,158.57,0.88,-56.63,-0.31,8019.45,80.19
|
||||
141,매도 청산,2025-11-17 03:30,Short Sell,2.2028,8132.3,18012.23127,84.03,0.47,474.23,2.63,-144.64,-0.80,8103.48,81.03
|
||||
141,매도 진입,2025-11-16 23:30,Short,2.2149,8132.3,18012.23127,84.03,0.47,474.23,2.63,-144.64,-0.80,8103.48,81.03
|
||||
142,매수 청산,2025-11-19 07:30,Long Sell,2.2268,8093.1,18096.1716,-88.9,-0.49,36.46,0.20,-273.5,-1.51,8014.58,80.15
|
||||
142,매수 진입,2025-11-19 04:30,Long,2.236,8093.1,18096.1716,-88.9,-0.49,36.46,0.20,-273.5,-1.51,8014.58,80.15
|
||||
143,매수 청산,2025-11-25 16:00,Long Sell,2.2374,8677.8,18007.30278,1393.44,7.74,1819.47,10.10,-69.68,-0.39,9408.02,94.08
|
||||
143,매수 진입,2025-11-25 00:00,Long,2.0751,8677.8,18007.30278,1393.44,7.74,1819.47,10.10,-69.68,-0.39,9408.02,94.08
|
||||
144,매도 청산,2025-12-01 12:00,Short Sell,2.0637,8861.3,19400.04409,1097.9,5.66,1180.54,6.08,-75.99,-0.39,10505.92,105.06
|
||||
144,매도 진입,2025-12-01 06:30,Short,2.1893,8861.3,19400.04409,1097.9,5.66,1180.54,6.08,-75.99,-0.39,10505.92,105.06
|
||||
145,매수 청산,2025-12-03 20:00,Long Sell,2.1737,9320.5,20497.6436,-253.98,-1.24,176.35,0.86,-257.06,-1.25,10251.94,102.52
|
||||
145,매수 진입,2025-12-03 14:00,Long,2.1992,9320.5,20497.6436,-253.98,-1.24,176.35,0.86,-257.06,-1.25,10251.94,102.52
|
||||
146,매수 청산,2025-12-04 14:00,Long Sell,2.1874,9208.8,20243.70504,-116.53,-0.58,141.09,0.70,-134.26,-0.66,10135.41,101.35
|
||||
146,매수 진입,2025-12-04 06:00,Long,2.1983,9208.8,20243.70504,-116.53,-0.58,141.09,0.70,-134.26,-0.66,10135.41,101.35
|
||||
147,매수 청산,2025-12-08 08:00,Long Sell,2.0291,9568,20127.2448,-728.63,-3.62,66.58,0.33,-880.65,-4.37,9406.78,94.07
|
||||
147,매수 진입,2025-12-08 03:30,Long,2.1036,9568,20127.2448,-728.63,-3.62,66.58,0.33,-880.65,-4.37,9406.78,94.07
|
||||
148,매도 청산,2025-12-13 03:00,Short Sell,1.9961,9762.9,19398.8823,-104.4,-0.54,2,0.01,-145.42,-0.75,9302.38,93.02
|
||||
148,매도 진입,2025-12-13 02:30,Short,1.987,9762.9,19398.8823,-104.4,-0.54,2,0.01,-145.42,-0.75,9302.38,93.02
|
||||
149,매도 청산,2025-12-18 05:30,Short Sell,1.883,10226.6,19294.52622,22.42,0.12,393.16,2.04,-30.22,-0.16,9324.8,93.25
|
||||
149,매도 진입,2025-12-18 02:30,Short,1.8867,10226.6,19294.52622,22.42,0.12,393.16,2.04,-30.22,-0.16,9324.8,93.25
|
||||
150,매도 청산,2025-12-19 07:00,Short Sell,1.8199,10477.3,19316.99801,234.01,1.21,450.13,2.33,-33.92,-0.18,9558.81,95.59
|
||||
150,매도 진입,2025-12-19 04:00,Short,1.8437,10477.3,19316.99801,234.01,1.21,450.13,2.33,-33.92,-0.18,9558.81,95.59
|
||||
151,매수 청산,2025-12-21 00:00,Long Sell,1.9288,10138.9,19550.84087,-10.57,-0.05,302.43,1.55,-92.99,-0.48,9548.23,95.48
|
||||
151,매수 진입,2025-12-20 17:30,Long,1.9283,10138.9,19550.84087,-10.57,-0.05,302.43,1.55,-92.99,-0.48,9548.23,95.48
|
||||
152,매도 청산,2025-12-22 01:00,Short Sell,1.9049,10279.5,19540.30155,-56.77,-0.29,8.63,0.04,-95.19,-0.49,9491.47,94.91
|
||||
152,매도 진입,2025-12-22 00:30,Short,1.9009,10279.5,19540.30155,-56.77,-0.29,8.63,0.04,-95.19,-0.49,9491.47,94.91
|
||||
153,매도 청산,2025-12-23 07:30,Short Sell,1.8996,10262.6,19483.5461,-26.88,-0.14,53.78,0.28,-71.42,-0.37,9464.59,94.65
|
||||
153,매도 진입,2025-12-23 06:30,Short,1.8985,10262.6,19483.5461,-26.88,-0.14,53.78,0.28,-71.42,-0.37,9464.59,94.65
|
||||
154,매수 청산,2025-12-26 06:30,Long Sell,1.8691,10333.9,19456.66692,-157.08,-0.81,0.48,0.00,-193.79,-1.00,9307.5,93.08
|
||||
154,매수 진입,2025-12-26 01:00,Long,1.8828,10333.9,19456.66692,-157.08,-0.81,0.48,0.00,-193.79,-1.00,9307.5,93.08
|
||||
155,매도 청산,2025-12-26 11:00,Short Sell,1.837,10454.9,19299.7454,78.69,0.41,212.88,1.10,-65.22,-0.34,9386.2,93.86
|
||||
155,매도 진입,2025-12-26 08:00,Short,1.846,10454.9,19299.7454,78.69,0.41,212.88,1.10,-65.22,-0.34,9386.2,93.86
|
||||
156,매수 청산,2026-01-01 00:30,Long Sell,1.8581,10314.8,19378.41476,-227.9,-1.18,32.48,0.17,-233.65,-1.21,9158.29,91.58
|
||||
156,매수 진입,2025-12-31 21:00,Long,1.8787,10314.8,19378.41476,-227.9,-1.18,32.48,0.17,-233.65,-1.21,9158.29,91.58
|
||||
157,매도 청산,2026-01-01 06:30,Short Sell,1.8335,10321,19150.6155,211.83,1.11,438.21,2.29,-7.66,-0.04,9370.13,93.70
|
||||
157,매도 진입,2026-01-01 01:30,Short,1.8555,10321,19150.6155,211.83,1.11,438.21,2.29,-7.66,-0.04,9370.13,93.70
|
||||
158,매수 청산,2026-01-05 16:30,Long Sell,2.1313,9166,19362.2584,157.68,0.81,489.97,2.53,-37.08,-0.19,9527.8,95.28
|
||||
158,매수 진입,2026-01-05 10:00,Long,2.1124,9166,19362.2584,157.68,0.81,489.97,2.53,-37.08,-0.19,9527.8,95.28
|
||||
159,매수 청산,2026-01-06 17:00,Long Sell,2.3305,9014.9,19519.96297,1473.05,7.54,2260.34,11.58,-15.02,-0.08,11000.85,110.01
|
||||
159,매수 진입,2026-01-06 01:00,Long,2.1653,9014.9,19519.96297,1473.05,7.54,2260.34,11.58,-15.02,-0.08,11000.85,110.01
|
||||
160,매도 청산,2026-01-07 05:00,Short Sell,2.2277,9347,20992.4273,153.39,0.73,189.76,0.90,-120.56,-0.57,11154.24,111.54
|
||||
160,매도 진입,2026-01-07 04:00,Short,2.2459,9347,20992.4273,153.39,0.73,189.76,0.90,-120.56,-0.57,11154.24,111.54
|
||||
161,매도 청산,2026-01-09 20:00,Short Sell,2.1005,10143.3,21145.73751,-177.24,-0.84,6.76,0.03,-243.78,-1.15,10977,109.77
|
||||
161,매도 진입,2026-01-09 18:00,Short,2.0847,10143.3,21145.73751,-177.24,-0.84,6.76,0.03,-243.78,-1.15,10977,109.77
|
||||
162,매도 청산,2026-01-10 05:30,Short Sell,2.0872,10059.7,20968.43868,-44.95,-0.21,47.95,0.23,-90.88,-0.43,10932.04,109.32
|
||||
162,매도 진입,2026-01-10 04:30,Short,2.0844,10059.7,20968.43868,-44.95,-0.21,47.95,0.23,-90.88,-0.43,10932.04,109.32
|
||||
163,매도 청산,2026-01-10 06:30,Short Sell,2.0909,10036.7,20923.50849,-78.99,-0.38,77.95,0.37,-87.66,-0.42,10853.05,108.53
|
||||
163,매도 진입,2026-01-10 06:00,Short,2.0847,10036.7,20923.50849,-78.99,-0.38,77.95,0.37,-87.66,-0.42,10853.05,108.53
|
||||
164,매도 청산,2026-01-11 01:00,Short Sell,2.0932,9971.1,20844.58455,-43.61,-0.21,5.62,0.03,-59.19,-0.28,10809.44,108.09
|
||||
164,매도 진입,2026-01-10 23:30,Short,2.0905,9971.1,20844.58455,-43.61,-0.21,5.62,0.03,-59.19,-0.28,10809.44,108.09
|
||||
165,매도 청산,2026-01-12 07:30,Short Sell,2.065,10114.8,20801.0862,-102.65,-0.49,3.82,0.02,-202.52,-0.97,10706.79,107.07
|
||||
165,매도 진입,2026-01-12 05:30,Short,2.0565,10114.8,20801.0862,-102.65,-0.49,3.82,0.02,-202.52,-0.97,10706.79,107.07
|
||||
166,매도 청산,2026-01-12 21:00,Short Sell,2.0486,10078.6,20698.42082,34.86,0.17,180.19,0.87,-76.81,-0.37,10741.66,107.42
|
||||
166,매도 진입,2026-01-12 17:00,Short,2.0537,10078.6,20698.42082,34.86,0.17,180.19,0.87,-76.81,-0.37,10741.66,107.42
|
||||
167,매도 청산,2026-01-13 08:30,Short Sell,2.0555,10093.1,20733.24602,-29.71,-0.14,76.49,0.37,-48.67,-0.23,10711.94,107.12
|
||||
167,매도 진입,2026-01-13 08:00,Short,2.0542,10093.1,20733.24602,-29.71,-0.14,76.49,0.37,-48.67,-0.23,10711.94,107.12
|
||||
168,매수 청산,2026-01-15 08:00,Long Sell,2.136,9559.8,20703.65886,-300.38,-1.45,94.01,0.45,-399.28,-1.93,10411.57,104.12
|
||||
168,매수 진입,2026-01-15 01:30,Long,2.1657,9559.8,20703.65886,-300.38,-1.45,94.01,0.45,-399.28,-1.93,10411.57,104.12
|
||||
169,매도 청산,2026-01-17 14:00,Short Sell,2.0643,9899.2,20403.24112,-48.01,-0.24,68.06,0.33,-54.69,-0.27,10363.56,103.64
|
||||
169,매도 진입,2026-01-17 12:30,Short,2.0611,9899.2,20403.24112,-48.01,-0.24,68.06,0.33,-54.69,-0.27,10363.56,103.64
|
||||
170,매도 청산,2026-01-19 11:30,Short Sell,1.9577,10527.7,20355.30795,-271.16,-1.33,59.24,0.29,-321.87,-1.58,10092.4,100.92
|
||||
170,매도 진입,2026-01-19 09:30,Short,1.9335,10527.7,20355.30795,-271.16,-1.33,59.24,0.29,-321.87,-1.58,10092.4,100.92
|
||||
171,매수 청산,2026-01-22 12:30,Long Sell,1.9477,10152.3,20084.29509,-326.6,-1.63,4.15,0.02,-404.99,-2.02,9765.8,97.66
|
||||
171,매수 진입,2026-01-22 07:30,Long,1.9783,10152.3,20084.29509,-326.6,-1.63,4.15,0.02,-404.99,-2.02,9765.8,97.66
|
||||
172,매도 청산,2026-01-23 02:00,Short Sell,1.9231,10370.5,19757.8766,-201.51,-1.02,30.47,0.15,-255.76,-1.29,9564.28,95.64
|
||||
172,매도 진입,2026-01-23 00:30,Short,1.9052,10370.5,19757.8766,-201.51,-1.02,30.47,0.15,-255.76,-1.29,9564.28,95.64
|
||||
173,매수 청산,2026-01-27 15:30,Long Sell,1.9015,10213.3,19556.42684,-151.43,-0.77,23.84,0.12,-167.15,-0.85,9412.86,94.13
|
||||
173,매수 진입,2026-01-27 12:00,Long,1.9148,10213.3,19556.42684,-151.43,-0.77,23.84,0.12,-167.15,-0.85,9412.86,94.13
|
||||
174,매수 청산,2026-01-29 01:00,Long Sell,1.9076,10099.9,19404.93787,-153.84,-0.79,205.35,1.06,-207.74,-1.07,9259.02,92.59
|
||||
174,매수 진입,2026-01-28 16:00,Long,1.9213,10099.9,19404.93787,-153.84,-0.79,205.35,1.06,-207.74,-1.07,9259.02,92.59
|
||||
175,매도 청산,2026-01-29 14:00,Short Sell,1.8788,10143.4,19251.15886,178.42,0.93,323.99,1.68,-25.96,-0.13,9437.43,94.37
|
||||
175,매도 진입,2026-01-29 10:30,Short,1.8979,10143.4,19251.15886,178.42,0.93,323.99,1.68,-25.96,-0.13,9437.43,94.37
|
||||
176,매도 청산,2026-02-09 21:30,Short Sell,1.4041,13805.3,19429.57922,30.03,0.15,469.89,2.42,-79.56,-0.41,9467.47,94.67
|
||||
176,매도 진입,2026-02-09 18:00,Short,1.4074,13805.3,19429.57922,30.03,0.15,469.89,2.42,-79.56,-0.41,9467.47,94.67
|
||||
177,매도 청산,2026-02-28 03:00,Short Sell,1.3583,14074.7,19459.68022,326.58,1.68,612.91,3.15,-48.6,-0.25,9794.05,97.94
|
||||
177,매도 진입,2026-02-27 21:00,Short,1.3826,14074.7,19459.68022,326.58,1.68,612.91,3.15,-48.6,-0.25,9794.05,97.94
|
||||
178,매도 청산,2026-03-06 04:30,Short Sell,1.4159,14132.9,19786.06,-240.63,-1.22,13.28,0.07,-341.45,-1.73,9553.42,95.53
|
||||
178,매도 진입,2026-03-06 03:00,Short,1.4,14132.9,19786.06,-240.63,-1.22,13.28,0.07,-341.45,-1.73,9553.42,95.53
|
||||
179,매도 청산,2026-03-09 03:30,Short Sell,1.3445,14495.4,19545.59736,40.92,0.21,145.83,0.75,-74.5,-0.38,9594.34,95.94
|
||||
179,매도 진입,2026-03-08 22:30,Short,1.3484,14495.4,19545.59736,40.92,0.21,145.83,0.75,-74.5,-0.38,9594.34,95.94
|
||||
180,매도 청산,2026-03-13 02:00,Short Sell,1.382,14217.8,19586.44128,-78.25,-0.40,4.96,0.03,-124.42,-0.63,9516.08,95.16
|
||||
180,매도 진입,2026-03-13 01:30,Short,1.3776,14217.8,19586.44128,-78.25,-0.40,4.96,0.03,-124.42,-0.63,9516.08,95.16
|
||||
181,매수 청산,2026-03-16 02:00,Long Sell,1.4125,13700.5,19508.14195,-171.73,-0.88,78.51,0.40,-203.72,-1.04,9344.35,93.44
|
||||
181,매수 진입,2026-03-15 22:00,Long,1.4239,13700.5,19508.14195,-171.73,-0.88,78.51,0.40,-203.72,-1.04,9344.35,93.44
|
||||
182,매수 청산,2026-03-17 14:30,Long Sell,1.5294,13549.5,19336.49145,1370.09,7.08,2425.76,12.54,-67.35,-0.35,10714.44,107.14
|
||||
182,매수 진입,2026-03-16 07:00,Long,1.4271,13549.5,19336.49145,1370.09,7.08,2425.76,12.54,-67.35,-0.35,10714.44,107.14
|
||||
183,매도 청산,2026-03-18 11:30,Short Sell,1.5224,13684.5,20706.01695,-143.88,-0.69,46.46,0.22,-180.71,-0.87,10570.56,105.71
|
||||
183,매도 진입,2026-03-18 09:30,Short,1.5131,13684.5,20706.01695,-143.88,-0.69,46.46,0.22,-180.71,-0.87,10570.56,105.71
|
||||
184,매도 청산,2026-03-19 02:30,Short Sell,1.4492,13606.6,20562.29392,827.5,4.02,1017.71,4.95,-21.83,-0.11,11398.06,113.98
|
||||
184,매도 진입,2026-03-18 20:00,Short,1.5112,13606.6,20562.29392,827.5,4.02,1017.71,4.95,-21.83,-0.11,11398.06,113.98
|
||||
185,매도 청산,2026-03-21 20:30,Short Sell,1.44,14867.2,21389.44064,-36.45,-0.17,0,0.00,-42.75,-0.20,11361.61,113.62
|
||||
185,매도 진입,2026-03-21 19:30,Short,1.4387,14867.2,21389.44064,-36.45,-0.17,0,0.00,-42.75,-0.20,11361.61,113.62
|
||||
186,매도 청산,2026-03-22 11:00,Short Sell,1.4098,15135.4,21353.02232,-1.94,-0.01,68.65,0.32,-91.79,-0.43,11359.67,113.60
|
||||
186,매도 진입,2026-03-22 10:00,Short,1.4108,15135.4,21353.02232,-1.94,-0.01,68.65,0.32,-91.79,-0.43,11359.67,113.60
|
||||
187,매수 청산,2026-03-24 08:30,Long Sell,1.4303,14764.6,21351.08806,-250.27,-1.17,283.8,1.33,-336.31,-1.57,11109.4,111.09
|
||||
187,매수 진입,2026-03-23 23:30,Long,1.4461,14764.6,21351.08806,-250.27,-1.17,283.8,1.33,-336.31,-1.57,11109.4,111.09
|
||||
188,매도 청산,2026-03-25 04:30,Short Sell,1.3923,15045.2,21100.893,136.64,0.65,272.9,1.29,-106.23,-0.50,11246.05,112.46
|
||||
188,매도 진입,2026-03-24 23:00,Short,1.4025,15045.2,21100.893,136.64,0.65,272.9,1.29,-106.23,-0.50,11246.05,112.46
|
||||
189,매도 청산,2026-03-26 22:00,Short Sell,1.3744,15131.8,21237.4813,423.52,1.99,521.12,2.45,-41.78,-0.20,11669.57,116.70
|
||||
189,매도 진입,2026-03-26 12:00,Short,1.4035,15131.8,21237.4813,423.52,1.99,521.12,2.45,-41.78,-0.20,11669.57,116.70
|
||||
190,매도 청산,2026-03-31 15:30,Short Sell,1.3248,16381.2,21660.86076,-58.3,-0.27,112.56,0.52,-52.89,-0.24,11611.27,116.11
|
||||
190,매도 진입,2026-03-31 15:00,Short,1.3223,16381.2,21660.86076,-58.3,-0.27,112.56,0.52,-52.89,-0.24,11611.27,116.11
|
||||
191,매도 청산,2026-03-31 20:30,Short Sell,1.3148,16416.6,21602.60394,0.78,0.00,181.79,0.84,-146.54,-0.68,11612.05,116.12
|
||||
191,매도 진입,2026-03-31 16:30,Short,1.3159,16416.6,21602.60394,0.78,0.00,181.79,0.84,-146.54,-0.68,11612.05,116.12
|
||||
192,매수 청산,2026-04-02 04:00,Long Sell,1.3472,15929.3,21603.31666,-160.59,-0.74,179.32,0.83,-187.05,-0.87,11451.46,114.51
|
||||
192,매수 진입,2026-04-01 15:30,Long,1.3562,15929.3,21603.31666,-160.59,-0.74,179.32,0.83,-187.05,-0.87,11451.46,114.51
|
||||
193,매수 청산,2026-04-04 04:30,Long Sell,1.3147,16166.2,21442.84768,-206.22,-0.96,132.07,0.62,-218.74,-1.02,11245.24,112.45
|
||||
193,매수 진입,2026-04-03 21:30,Long,1.3264,16166.2,21442.84768,-206.22,-0.96,132.07,0.62,-218.74,-1.02,11245.24,112.45
|
||||
194,매도 청산,2026-04-04 20:00,Short Sell,1.3143,16179.1,21236.68666,-44.5,-0.21,85.34,0.40,-44.09,-0.21,11200.74,112.01
|
||||
194,매도 진입,2026-04-04 19:00,Short,1.3126,16179.1,21236.68666,-44.5,-0.21,85.34,0.40,-44.09,-0.21,11200.74,112.01
|
||||
195,매도 청산,2026-04-04 21:30,Short Sell,1.3103,16150.1,21192.16122,13.74,0.06,27.05,0.13,-24.63,-0.12,11214.48,112.14
|
||||
195,매도 진입,2026-04-04 20:30,Short,1.3122,16150.1,21192.16122,13.74,0.06,27.05,0.13,-24.63,-0.12,11214.48,112.14
|
||||
196,매수 청산,2026-04-12 09:30,Long Sell,1.3526,15467.5,21205.9425,-301.45,-1.42,17.81,0.08,-313.19,-1.48,10913.03,109.13
|
||||
196,매수 진입,2026-04-12 05:00,Long,1.371,15467.5,21205.9425,-301.45,-1.42,17.81,0.08,-313.19,-1.48,10913.03,109.13
|
||||
197,매도 청산,2026-04-13 22:00,Short Sell,1.3322,15767.5,20904.5515,-117.68,-0.56,31.06,0.15,-132.93,-0.64,10795.35,107.95
|
||||
197,매도 진입,2026-04-13 21:00,Short,1.3258,15767.5,20904.5515,-117.68,-0.56,31.06,0.15,-132.93,-0.64,10795.35,107.95
|
||||
198,매수 청산,2026-04-14 22:00,Long Sell,1.3672,15138.7,20786.94897,-105.91,-0.51,37.1,0.18,-129.42,-0.62,10689.44,106.89
|
||||
198,매수 진입,2026-04-14 16:30,Long,1.3731,15138.7,20786.94897,-105.91,-0.51,37.1,0.18,-129.42,-0.62,10689.44,106.89
|
||||
199,매수 청산,2026-04-15 03:30,Long Sell,1.3627,14851.8,20681.1315,-458.95,-2.22,34.8,0.17,-470.16,-2.27,10230.49,102.30
|
||||
199,매수 진입,2026-04-14 23:00,Long,1.3925,14851.8,20681.1315,-458.95,-2.22,34.8,0.17,-470.16,-2.27,10230.49,102.30
|
||||
200,매수 청산,2026-04-17 10:30,Long Sell,1.434,14111.9,20222.3527,-2.07,-0.01,461.84,2.28,-270.57,-1.34,10228.41,102.28
|
||||
200,매수 진입,2026-04-17 01:30,Long,1.433,14111.9,20222.3527,-2.07,-0.01,461.84,2.28,-270.57,-1.34,10228.41,102.28
|
||||
201,매수 청산,2026-04-18 09:00,Long Sell,1.4755,14044.8,20220.29856,486.43,2.40,977.86,4.83,-109.21,-0.54,10714.84,107.15
|
||||
201,매수 진입,2026-04-17 17:30,Long,1.4397,14044.8,20220.29856,486.43,2.40,977.86,4.83,-109.21,-0.54,10714.84,107.15
|
||||
202,매도 청산,2026-04-20 05:00,Short Sell,1.4177,14581,20706.4781,18.44,0.09,121.49,0.59,-44.74,-0.22,10733.28,107.33
|
||||
202,매도 진입,2026-04-20 02:30,Short,1.4201,14581,20706.4781,18.44,0.09,121.49,0.59,-44.74,-0.22,10733.28,107.33
|
||||
203,매수 청산,2026-04-21 22:30,Long Sell,1.4326,14423.4,20724.98346,-78.58,-0.38,169.12,0.82,-159.74,-0.77,10654.71,106.55
|
||||
203,매수 진입,2026-04-21 16:00,Long,1.4369,14423.4,20724.98346,-78.58,-0.38,169.12,0.82,-159.74,-0.77,10654.71,106.55
|
||||
|
104
PineScript/TradingSignal.cs
Normal file
104
PineScript/TradingSignal.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Serilog;
|
||||
|
||||
namespace PineScriptV6
|
||||
{
|
||||
/// <summary>plotshape의 text 값으로 감지된 매매 신호 종류</summary>
|
||||
public enum SignalType
|
||||
{
|
||||
Buy, // Long 진입
|
||||
Sell, // Long 청산
|
||||
Short, // Short 진입
|
||||
Cover // Short 청산
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 신호 발생 시점의 데이터를 담는 레코드.
|
||||
/// TradingView 결과와 비교하는 데 사용합니다.
|
||||
/// </summary>
|
||||
public record TradeSignalEvent
|
||||
{
|
||||
public DateTimeOffset Time { get; init; }
|
||||
public SignalType Signal { get; init; }
|
||||
public decimal Price { get; init; }
|
||||
|
||||
/// <summary>CSV 한 줄 포맷</summary>
|
||||
public string ToCsvLine() =>
|
||||
$"{Time:yyyy-MM-dd HH:mm},{Signal},{Price:F4}";
|
||||
|
||||
/// <summary>콘솔 출력용 포맷</summary>
|
||||
public override string ToString() =>
|
||||
$" {Time:yyyy-MM-dd HH:mm} | {Signal,-10} | {Price,10:F4}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pine Script의 plotshape → C# 매매 함수를 연결하는 신호 허브.
|
||||
/// 발생한 신호는 Results 컬렉션에 누적되어 TradingView 결과와 비교할 수 있습니다.
|
||||
/// </summary>
|
||||
public static class TradingSignal
|
||||
{
|
||||
// ── 매매 델리게이트 ──────────────────────────────────────
|
||||
public static Action<decimal>? OnBuy { get; set; }
|
||||
public static Action<decimal>? OnSell { get; set; }
|
||||
public static Action<decimal>? OnShort { get; set; }
|
||||
public static Action<decimal>? OnCover { get; set; }
|
||||
|
||||
// ── 결과 수집 ────────────────────────────────────────────
|
||||
/// <summary>발생한 모든 신호를 시간 순으로 누적합니다.</summary>
|
||||
public static List<TradeSignalEvent> Results { get; } = new();
|
||||
|
||||
/// <summary>Results를 초기화합니다. (재실행 시 사용)</summary>
|
||||
public static void ClearResults() => Results.Clear();
|
||||
|
||||
// ── text → SignalType 매핑 ───────────────────────────────
|
||||
private static readonly Dictionary<string, SignalType> TextMap =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "Long", SignalType.Buy },
|
||||
{ "Long Sell", SignalType.Sell },
|
||||
{ "Short", SignalType.Short },
|
||||
{ "Short Sell", SignalType.Cover },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// plotshape의 text 값으로 신호를 감지합니다.
|
||||
/// 감지된 신호는 Results에 기록되고 등록된 델리게이트를 호출합니다.
|
||||
/// </summary>
|
||||
public static void DetectAndRaise(string text, decimal price, DateTimeOffset time = default)
|
||||
{
|
||||
string key = text.Trim();
|
||||
if (!TextMap.TryGetValue(key, out SignalType signal)) return;
|
||||
|
||||
// ── 결과 수집 ──
|
||||
var evt = new TradeSignalEvent
|
||||
{
|
||||
Time = time == default ? DateTimeOffset.Now : time,
|
||||
Signal = signal,
|
||||
Price = price,
|
||||
};
|
||||
Results.Add(evt);
|
||||
|
||||
Log.Information("[Signal] {Signal,-10} | {Time:yyyy-MM-dd HH:mm} | {Price:F4}",
|
||||
signal, evt.Time, price);
|
||||
|
||||
// ── 델리게이트 호출 ──
|
||||
switch (signal)
|
||||
{
|
||||
case SignalType.Buy:
|
||||
OnBuy?.Invoke(price);
|
||||
break;
|
||||
case SignalType.Sell:
|
||||
OnSell?.Invoke(price);
|
||||
break;
|
||||
case SignalType.Short:
|
||||
OnShort?.Invoke(price);
|
||||
break;
|
||||
case SignalType.Cover:
|
||||
OnCover?.Invoke(price);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSignal(string text) =>
|
||||
TextMap.ContainsKey(text.Trim());
|
||||
}
|
||||
}
|
||||
211
PineScript/example.pine
Normal file
211
PineScript/example.pine
Normal file
@@ -0,0 +1,211 @@
|
||||
//@version=5
|
||||
indicator("LJH - 테스트 (Table 제거 버전)", overlay=true)
|
||||
|
||||
// === high close, low close ===
|
||||
highClose = ta.highest(high, 12)
|
||||
lowClose = ta.lowest(low, 4)
|
||||
|
||||
// === SMA4 Bands ===
|
||||
sma4 = (close + close[1] + close[2] + close[3]) / 4
|
||||
prevSma4 = (close[4] + close[1] + close[2] + close[3]) / 4
|
||||
prev2Sma4 = (close[4] + close[5] + close[2] + close[3]) / 4
|
||||
prev3Sma4 = (close[4] + close[5] + close[6] + close[3]) / 4
|
||||
|
||||
sma72 = ta.sma(close, 72)
|
||||
sma24 = ta.sma(close, 24)
|
||||
prevSma72 = ta.sma(close, 72)
|
||||
prev2Sma72 = ta.sma(close, 72)
|
||||
prevSma24 = ta.sma(close, 24)
|
||||
prev2Sma24 = ta.sma(close, 24)
|
||||
|
||||
sma4High = ta.highest(sma4, 12)
|
||||
sma4Middle = ta.sma(sma4, 12)
|
||||
sma4Low = ta.lowest(sma4, 12)
|
||||
|
||||
prevSma4High = ta.highest(prevSma4, 12)
|
||||
prevSma4Middle = ta.sma(prevSma4, 12)
|
||||
prevSma4Low = ta.lowest(prevSma4, 12)
|
||||
|
||||
prev2Sma4High = ta.highest(prev2Sma4, 12)
|
||||
prev2Sma4Middle = ta.sma(prev2Sma4, 12)
|
||||
prev2Sma4Low = ta.lowest(prev2Sma4, 12)
|
||||
|
||||
prev3Sma4High = ta.highest(prev3Sma4, 12)
|
||||
prev3Sma4Middle = ta.sma(prev3Sma4, 12)
|
||||
prev3Sma4Low = ta.lowest(prev3Sma4, 12)
|
||||
|
||||
// === 상태 추적 ===
|
||||
var bool inLong = false
|
||||
var bool inShort = false
|
||||
|
||||
// === 새 조건을 위한 상태 변수 ===
|
||||
var int sma4LongTouchBar = na
|
||||
var int sma4ShortTouchBar = na
|
||||
|
||||
// sma4가 sma4Low에 닿은 시점 저장
|
||||
if (sma4 == sma4Low and prevSma4 >= prevSma4Low)
|
||||
sma4LongTouchBar := bar_index
|
||||
|
||||
// sma4가 sma4High에 닿은 시점 저장
|
||||
if (sma4 == sma4High and prevSma4 <= prevSma4High)
|
||||
sma4ShortTouchBar := bar_index
|
||||
|
||||
//전저가 전고가 처리
|
||||
maLength = input.int(4, title="이동평균 기간")
|
||||
ma = ta.sma(close, maLength)
|
||||
|
||||
// 이동평균의 방향 비교
|
||||
maPrev = ma
|
||||
maPrev2 = ma
|
||||
|
||||
// 꼭지점 조건: 상승에서 하락으로 전환 (고점)
|
||||
isTop = maPrev2 < maPrev and maPrev > ma
|
||||
// 꼭지점 조건: 하락에서 상승으로 전환 (저점)
|
||||
isBottom = maPrev2 > maPrev and maPrev < ma
|
||||
|
||||
var float[] topCloses = array.new_float()
|
||||
var float[] bottomCloses = array.new_float()
|
||||
|
||||
if isTop
|
||||
array.push(topCloses, maPrev)
|
||||
if isBottom
|
||||
array.push(bottomCloses, maPrev)
|
||||
|
||||
// === 구름떼 (Ichimoku Cloud) 추가 ===
|
||||
conversionPeriods = 9
|
||||
basePeriods = 26
|
||||
laggingSpan2Periods = 52
|
||||
displacement = 26
|
||||
|
||||
conversionLine = (ta.highest(high, conversionPeriods) + ta.lowest(low, conversionPeriods)) / 2
|
||||
baseLine = (ta.highest(high, basePeriods) + ta.lowest(low, basePeriods)) / 2
|
||||
leadingSpan1 = (conversionLine + baseLine) / 2
|
||||
leadingSpan2 = (ta.highest(high, laggingSpan2Periods) + ta.lowest(low, laggingSpan2Periods)) / 2
|
||||
|
||||
cloudTopNow = math.max(leadingSpan1[displacement], leadingSpan2[displacement])
|
||||
cloudBottomNow = math.min(leadingSpan1[displacement], leadingSpan2[displacement])
|
||||
|
||||
cloudTopAfter = math.max(leadingSpan1, leadingSpan2)
|
||||
cloudBottomAfter = math.min(leadingSpan1, leadingSpan2)
|
||||
|
||||
isSma4BelowCloud = low < cloudBottomNow
|
||||
isSma4AboveCloud = high > cloudTopNow
|
||||
|
||||
var bool hasEnteredShortAfterCloudBreak = false
|
||||
|
||||
// 매 봉마다 상태 업데이트
|
||||
if not isSma4BelowCloud
|
||||
hasEnteredShortAfterCloudBreak := false
|
||||
|
||||
// 상태 변수
|
||||
var bool allowNewShortEntry = true
|
||||
var bool wasBelowCloud = false
|
||||
|
||||
// 구름 위/아래 상태 추적
|
||||
if sma4 < cloudTopNow and not wasBelowCloud
|
||||
allowNewShortEntry := true
|
||||
wasBelowCloud := true
|
||||
|
||||
if not (sma4 < cloudTopNow)
|
||||
allowNewShortEntry := false
|
||||
wasBelowCloud := false
|
||||
|
||||
// === 횡보 구간 감지 ===
|
||||
lengthWindow = 12
|
||||
rangePeriod = 240
|
||||
shortLengthWindow = 4
|
||||
shortRangePeriod = 40
|
||||
|
||||
// Long 기준 비율 계산식
|
||||
longRatio = (high - ta.lowest(low, lengthWindow)) / ta.lowest(low, lengthWindow) * 100
|
||||
// Short 기준 비율 계산식
|
||||
shortRatio = (ta.highest(high, shortLengthWindow) - low) / ta.highest(high, shortLengthWindow) * 100
|
||||
|
||||
// 평균 계산
|
||||
rangeThresholdLongPercent = ta.sma(longRatio, rangePeriod)
|
||||
rangeThresholdShortPercent = ta.sma(shortRatio, shortRangePeriod)
|
||||
|
||||
rangeLookback = 12
|
||||
highestHigh = ta.highest(high, rangeLookback)
|
||||
lowestLow = ta.lowest(low, rangeLookback)
|
||||
|
||||
priceRangeLong = high - lowestLow
|
||||
priceRangeShort = highestHigh - low
|
||||
|
||||
isSidewaysLong = priceRangeLong > (lowestLow * rangeThresholdLongPercent * 1 / 100) and sma4 > cloudTopNow
|
||||
isSidewaysShort = priceRangeShort > (highestHigh * (rangeThresholdShortPercent) / 100) and sma4 < cloudTopNow
|
||||
|
||||
isLongRecentTouch = not na(sma4LongTouchBar) and (bar_index - sma4LongTouchBar <= 10)
|
||||
isShortRecentTouch = not na(sma4ShortTouchBar) and (bar_index - sma4ShortTouchBar <= 5)
|
||||
|
||||
// === 진입/청산 조건 ===
|
||||
rawNewLong = highClose == high and open < close and isLongRecentTouch and isSidewaysLong and prevSma4 <= sma4 and sma4High == sma4 and (prev2Sma4Middle - prevSma4Middle) > (prevSma4Middle - sma4Middle) and sma4Middle > sma72 and sma4 > cloudTopNow
|
||||
rawLongSell = (sma4Middle < sma72 and prevSma4Middle > prevSma72) or (sma4 == sma4Low)
|
||||
|
||||
rawNewShort = (sma72 < cloudTopNow and prevSma4Middle > prevSma24 and sma4Middle > cloudBottomNow and sma4 < sma72 and sma4High > sma72 and sma24 > sma4Middle and sma4 == sma4Low and isSidewaysShort and prevSma4 > sma4 and sma4 < cloudBottomNow) or (sma4 < sma72 and sma4High > sma72 and sma24 < sma4Middle and sma4 == sma4Low and isSidewaysShort and open > close and sma4 < cloudBottomNow and sma4Middle < cloudTopNow)
|
||||
rawShortSell = (sma4High <= sma4 and sma4High < sma72) or sma4 > sma72 or (prevSma4Middle > prevSma4 and sma4Middle < sma4) or (isSidewaysShort and prevSma4Low == prevSma4 and sma4Low < sma4)
|
||||
|
||||
// === 시그널 변수 ===
|
||||
newLongSignal = false
|
||||
longSellSignal = false
|
||||
newShortSignal = false
|
||||
shortSellSignal = false
|
||||
|
||||
// === 롱 진입/청산 로직 ===
|
||||
if not inLong and rawNewLong
|
||||
newLongSignal := true
|
||||
inLong := true
|
||||
|
||||
if inLong and rawLongSell
|
||||
longSellSignal := true
|
||||
inLong := false
|
||||
|
||||
// === 쇼트 진입/청산 로직 ===
|
||||
if not inShort and rawNewShort
|
||||
newShortSignal := true
|
||||
inShort := true
|
||||
|
||||
if inShort and rawShortSell
|
||||
shortSellSignal := true
|
||||
inShort := false
|
||||
|
||||
// === 수익률 계산 및 라벨 표시용 변수 ===
|
||||
var float longEntryPrice = na
|
||||
var float shortEntryPrice = na
|
||||
|
||||
if newLongSignal
|
||||
longEntryPrice := close
|
||||
|
||||
if longSellSignal and not na(longEntryPrice)
|
||||
lastLongProfit = (close - longEntryPrice) / longEntryPrice * 100
|
||||
label.new(bar_index, high, text="롱: " + str.tostring(lastLongProfit, "#.##") + "%", color=color.green, textcolor=color.white, size=size.small)
|
||||
longEntryPrice := na
|
||||
|
||||
if newShortSignal
|
||||
shortEntryPrice := close
|
||||
|
||||
if shortSellSignal and not na(shortEntryPrice)
|
||||
lastShortProfit = (shortEntryPrice - close) / shortEntryPrice * 100
|
||||
label.new(bar_index, low, text="숏: " + str.tostring(lastShortProfit, "#.##") + "%", color=color.red, textcolor=color.white, size=size.small, style=label.style_label_up)
|
||||
shortEntryPrice := na
|
||||
|
||||
// === 시각화 (Plot) ===
|
||||
bgcolor(isSidewaysLong ? color.new(#458558, 85) : na, title="Sideways Long Background")
|
||||
bgcolor(isSidewaysShort ? color.new(#964c4c, 85) : na, title="Sideways Short Background")
|
||||
|
||||
plot(sma4High, color=color.green, linewidth=2, title="SMA(4) High")
|
||||
plot(sma4Low, color=color.red, linewidth=2, title="SMA(4) Low")
|
||||
plot(sma4Middle, color=color.rgb(130, 98, 247), linewidth=2, title="SMA(4) Middle")
|
||||
plot(sma72, color=color.rgb(0, 0, 0), linewidth=2, title="SMA(72)")
|
||||
plot(sma24, color=color.rgb(0, 4, 255), linewidth=2, title="SMA(24)")
|
||||
|
||||
plotshape(newLongSignal, title="Long Entry", location=location.belowbar, color=color.green, style=shape.labelup, text="Long")
|
||||
plotshape(longSellSignal, title="Long Exit", location=location.abovebar, color=color.red, style=shape.labeldown, text="Long Sell")
|
||||
|
||||
plotshape(newShortSignal, title="Short Entry", location=location.abovebar, color=color.rgb(153, 180, 91), style=shape.labeldown, text="Short")
|
||||
plotshape(shortSellSignal, title="Short Exit", location=location.belowbar, color=color.rgb(165, 62, 122), style=shape.labelup, text="Short Sell")
|
||||
|
||||
// 일목균형표 구름대 시각화
|
||||
p1 = plot(leadingSpan1, offset = displacement, title="Leading Span 1", color=color.new(color.green, 30))
|
||||
p2 = plot(leadingSpan2, offset = displacement, title="Leading Span 2", color=color.new(color.red, 30))
|
||||
fill(p1, p2, color = leadingSpan1 > leadingSpan2 ? color.new(color.green, 85) : color.new(color.red, 85), title="Ichimoku Cloud")
|
||||
Reference in New Issue
Block a user