From 01365218af84aedb17b84f328d7495da2157e3ff Mon Sep 17 00:00:00 2001 From: "DESKTOP-ACOOR7P\\lee" Date: Tue, 21 Apr 2026 01:19:08 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + BitgetApi/BitgetApi.csproj | 1 + .../Api/Controllers/DiagnosticsController.cs | 212 ++++++ Bitget_Server/Api/Dtos/DiagnosticsDtos.cs | 70 ++ Bitget_Server/Bitget_Server.csproj.user | 5 +- Bitget_Server/Program.cs | 8 +- Bitget_Server/data/GlobalOptions.json | 14 + Bitget_Server/wwwroot/chart.html | 604 ++++++++++++++++++ Bitget_Server/wwwroot/index.html | 409 ++++++++++++ 9 files changed, 1321 insertions(+), 3 deletions(-) create mode 100644 Bitget_Server/Api/Controllers/DiagnosticsController.cs create mode 100644 Bitget_Server/Api/Dtos/DiagnosticsDtos.cs create mode 100644 Bitget_Server/data/GlobalOptions.json create mode 100644 Bitget_Server/wwwroot/chart.html create mode 100644 Bitget_Server/wwwroot/index.html diff --git a/.gitignore b/.gitignore index aa850be..862197f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /packages /.cr /.idea +/Bitget_Server/logs diff --git a/BitgetApi/BitgetApi.csproj b/BitgetApi/BitgetApi.csproj index 573d09d..2bcdc97 100644 --- a/BitgetApi/BitgetApi.csproj +++ b/BitgetApi/BitgetApi.csproj @@ -130,6 +130,7 @@ MainForm.cs + Designer ResXFileCodeGenerator diff --git a/Bitget_Server/Api/Controllers/DiagnosticsController.cs b/Bitget_Server/Api/Controllers/DiagnosticsController.cs new file mode 100644 index 0000000..1f29238 --- /dev/null +++ b/Bitget_Server/Api/Controllers/DiagnosticsController.cs @@ -0,0 +1,212 @@ +using Bitget_Server.Api.Dtos; +using Bitget_Server.Application.Services; +using Bitget_Server.Application.State; +using Bitget_Server.Domain.Enums; +using Bitget_Server.Domain.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Bitget_Server.Api.Controllers; + +/// +/// 런타임 캐시/지표 계산 결과를 외부에서 점검하기 위한 진단용 API. +/// wwwroot/chart.html 화면이 주로 소비한다. +/// +[ApiController] +[Route("api/diag")] +[Produces("application/json")] +public class DiagnosticsController : ControllerBase +{ + private readonly MarketState _state; + private readonly PriceLevelTracker _priceLevels; + private readonly SignalProcessService _signals; + + public DiagnosticsController(MarketState state, PriceLevelTracker priceLevels, SignalProcessService signals) + { + _state = state; + _priceLevels = priceLevels; + _signals = signals; + } + + /// + /// 현재 캐시에 존재하는 심볼 + 서버가 지원하는 인터벌 목록. + /// + [HttpGet("symbols")] + [ProducesResponseType(typeof(DiagSymbolsResponse), StatusCodes.Status200OK)] + public ActionResult GetSymbols() + { + lock (_state.SyncRoot) + { + var symbols = _state.candleDataLists + .Select(x => x.ConGoodsData?.MarketName) + .Where(n => !string.IsNullOrEmpty(n)) + .Distinct() + .OrderBy(n => n) + .ToList(); + + var intervals = Enum.GetNames(typeof(MCandleTimeType)); + return Ok(new DiagSymbolsResponse(symbols, intervals)); + } + } + + /// + /// 특정 심볼/인터벌의 캔들 데이터 스냅샷 + 인디케이터 동봉. + /// Chart.js financial 플러그인 호환 포맷 (t,o,h,l,c). + /// + [HttpGet("candles")] + [ProducesResponseType(typeof(DiagCandleResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public ActionResult GetCandles( + [FromQuery] string symbol, + [FromQuery] string interval = "M1", + [FromQuery] int limit = 200) + { + if (string.IsNullOrWhiteSpace(symbol)) + return Problem("symbol 파라미터 필수", statusCode: StatusCodes.Status400BadRequest); + + if (!Enum.TryParse(interval, ignoreCase: true, out var t)) + return Problem($"지원하지 않는 interval: {interval}", statusCode: StatusCodes.Status400BadRequest); + + List snapshot; + lock (_state.SyncRoot) + { + var entry = _state.candleDataLists + .FirstOrDefault(x => string.Equals(x.ConGoodsData?.MarketName, symbol, StringComparison.OrdinalIgnoreCase)); + + if (entry == null || entry.candleDataList == null || !entry.candleDataList.TryGetValue(t, out var list) || list == null) + return Problem( + detail: $"캐시에 {symbol}/{interval} 데이터가 없습니다. WebSocketCalcBackgroundService 가 동작 중이며 대상 마켓이 checked 되었는지 확인.", + statusCode: StatusCodes.Status404NotFound, + title: "Candle data not cached"); + + // 얕은 복사 (원본 컬렉션 수정과 응답 직렬화 동시 진행 방지) + snapshot = list.ToList(); + } + + if (limit > 0 && snapshot.Count > limit) + snapshot = snapshot.Skip(snapshot.Count - limit).ToList(); + + var dtos = snapshot.Select(ToDto).OrderBy(d => d.t).ToList(); + return Ok(new DiagCandleResponse(symbol.ToUpperInvariant(), t.ToString(), dtos.Count, dtos)); + } + + /// + /// 특정 심볼/인터벌에 대한 PriceLevelTracker 의 전체 내부 상태. + /// (전고가/전저가/전전고가/전전저가/tempHighVal/tempLowVal 등) + /// + [HttpGet("price-levels")] + [ProducesResponseType(typeof(DiagPriceLevelsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public ActionResult GetPriceLevels( + [FromQuery] string symbol, + [FromQuery] string interval = "M1") + { + if (string.IsNullOrWhiteSpace(symbol)) + return Problem("symbol 파라미터 필수", statusCode: StatusCodes.Status400BadRequest); + + var key = symbol + interval; + if (!_priceLevels.ContainsKey(key)) + return Problem( + detail: $"PriceLevelTracker 에 키가 없습니다 (key={key}). SignalProcessBackgroundService 가 해당 마켓을 처리한 적이 있는지 확인.", + statusCode: StatusCodes.Status404NotFound, + title: "Price levels not tracked"); + + var levels = _priceLevels.GetOrCreate(key); + // 값 복사 + var copy = new Dictionary(levels); + return Ok(new DiagPriceLevelsDto(symbol.ToUpperInvariant(), interval, key, copy)); + } + + /// + /// 현재 활성 신호를 심볼/인터벌로 필터링해 차트 마커용 포맷으로 반환. + /// + [HttpGet("signal-markers")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public ActionResult> GetSignalMarkers( + [FromQuery] string symbol, + [FromQuery] string interval = null) + { + IEnumerable Map(IEnumerable src, string dir) => + src.Select(s => new DiagSignalMarkerDto( + direction: dir, + symbol: s.MarketName, + interval: s.MCandleTimeType.ToString(), + t: new DateTimeOffset(s.SignalStart == default ? DateTime.UtcNow : s.SignalStart.ToUniversalTime()) + .ToUnixTimeMilliseconds(), + price: s.Price, + rate: s.Rate, + indicatorPrice: s.IndicatorPrice, + indicatorRate: s.IndicatorRate, + description: s.Description ?? string.Empty)); + + bool SymbolMatch(SignalInfoData s) => + string.IsNullOrWhiteSpace(symbol) || + string.Equals(s.MarketName, symbol, StringComparison.OrdinalIgnoreCase); + + bool IntervalMatch(SignalInfoData s) => + string.IsNullOrWhiteSpace(interval) || + string.Equals(s.MCandleTimeType.ToString(), interval, StringComparison.OrdinalIgnoreCase); + + var all = new List(); + all.AddRange(Map(_signals.SignalsLongUp.Where(s => SymbolMatch(s) && IntervalMatch(s)), "LongUp")); + all.AddRange(Map(_signals.SignalsShortDown.Where(s => SymbolMatch(s) && IntervalMatch(s)), "ShortDown")); + all.AddRange(Map(_signals.SignalsLongDown.Where(s => SymbolMatch(s) && IntervalMatch(s)), "LongDown")); + all.AddRange(Map(_signals.SignalsShortUp.Where(s => SymbolMatch(s) && IntervalMatch(s)), "ShortUp")); + + return Ok(all); + } + + // ===================================================================== + // private helpers + // ===================================================================== + + private static DiagCandleDto ToDto(CandleData c) + { + long t = c.Argument == default + ? 0L + : new DateTimeOffset(DateTime.SpecifyKind(c.Argument, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); + + DiagIchimokuDto ichi = null; + if (c.IchimokData != null) + { + ichi = new DiagIchimokuDto( + c.IchimokData.기준선, + c.IchimokData.전환선, + c.IchimokData.후행스팬, + c.IchimokData.선행스팬1, + c.IchimokData.선행스팬2, + c.IchimokData.상한선, + c.IchimokData.하한선, + c.IchimokData.IsLongUp, + c.IchimokData.IsLongDown, + c.IchimokData.IsShortUp, + c.IchimokData.IsShortDown); + } + + Dictionary ma = null; + if (c.movingAverageDataList != null) + { + ma = c.movingAverageDataList.ToDictionary( + kv => kv.Key, + kv => new DiagMovingAverageDto( + kv.Value.Value, + kv.Value.Average, + kv.Value.BolangerValue_Up, + kv.Value.BolangerValue_Down)); + } + + Dictionary macd = null; + if (c.impulseMacdDataList != null) + { + macd = c.impulseMacdDataList.ToDictionary( + kv => kv.Key, + kv => new DiagImpulseMacdDto(kv.Value.MD, kv.Value.SH, kv.Value.SB)); + } + + return new DiagCandleDto( + t, + c.Open, c.High, c.Low, c.Close, + c.Vol_C, c.Vol_USDT, + c.WebSocketUpdate, c.WebServiceUpdate, + ichi, ma, macd); + } +} diff --git a/Bitget_Server/Api/Dtos/DiagnosticsDtos.cs b/Bitget_Server/Api/Dtos/DiagnosticsDtos.cs new file mode 100644 index 0000000..f174303 --- /dev/null +++ b/Bitget_Server/Api/Dtos/DiagnosticsDtos.cs @@ -0,0 +1,70 @@ +namespace Bitget_Server.Api.Dtos; + +/// +/// /api/diag 엔드포인트 전용 응답 모델. +/// Chart.js financial 플러그인 포맷에 맞춰 {t, o, h, l, c} 가 기본이며, +/// 인디케이터는 선택 필드로 포함된다. +/// +public record DiagSymbolsResponse( + IEnumerable Symbols, + IEnumerable Intervals); + +public record DiagCandleDto( + long t, // epoch millis (Chart.js x-axis) + double o, + double h, + double l, + double c, + double volC, + double volUsdt, + bool wsUpdated, + bool restUpdated, + DiagIchimokuDto ichimoku, + Dictionary ma, + Dictionary impulseMacd); + +public record DiagIchimokuDto( + double 기준선, + double 전환선, + double 후행스팬, + double 선행스팬1, + double 선행스팬2, + double 상한선, + double 하한선, + bool IsLongUp, + bool IsLongDown, + bool IsShortUp, + bool IsShortDown); + +public record DiagMovingAverageDto( + double Value, + double Average, + double BolUp, + double BolDown); + +public record DiagImpulseMacdDto(double MD, double SH, double SB); + +public record DiagCandleResponse( + string Symbol, + string Interval, + int Count, + IEnumerable Candles); + +/// PriceLevelTracker 의 전고점/전저점 상태 전체 키·값. +public record DiagPriceLevelsDto( + string Symbol, + string Interval, + string Key, + IReadOnlyDictionary Levels); + +/// SignalProcessService 의 현재 신호 요약 (차트 마커용). +public record DiagSignalMarkerDto( + string direction, // "LongUp" / "ShortDown" / "LongDown" / "ShortUp" + string symbol, + string interval, + long t, + double price, + double rate, + double indicatorPrice, + double indicatorRate, + string description); diff --git a/Bitget_Server/Bitget_Server.csproj.user b/Bitget_Server/Bitget_Server.csproj.user index dd2d54c..983ecfc 100644 --- a/Bitget_Server/Bitget_Server.csproj.user +++ b/Bitget_Server/Bitget_Server.csproj.user @@ -1,6 +1,9 @@  - Container (Dockerfile) + http + + + ProjectDebugger \ No newline at end of file diff --git a/Bitget_Server/Program.cs b/Bitget_Server/Program.cs index a976c20..7598ced 100644 --- a/Bitget_Server/Program.cs +++ b/Bitget_Server/Program.cs @@ -131,6 +131,10 @@ var app = builder.Build(); app.UseExceptionHandler(); app.UseStatusCodePages(); +// 정적 파일 (wwwroot/index.html 등). UseStaticFiles 앞에 UseDefaultFiles 필수. +app.UseDefaultFiles(); +app.UseStaticFiles(); + // Swagger 는 환경 불문 항상 활성화 app.UseSwagger(); app.UseSwaggerUI(options => @@ -143,8 +147,8 @@ app.UseSerilogRequestLogging(); app.MapControllers(); -// 루트 접속 시 Swagger 로 리다이렉트 -app.MapGet("/", () => Results.Redirect("/swagger")); +// 루트에 wwwroot/index.html 이 없을 경우에만 Swagger 로 폴백 리다이렉트 +app.MapFallback(() => Results.Redirect("/swagger")); Log.Information("Bitget_Server 시작 — URL http://*:5000, DataDir={DataDir}, LogDir={LogDir}", dataDir, logDir); diff --git a/Bitget_Server/data/GlobalOptions.json b/Bitget_Server/data/GlobalOptions.json new file mode 100644 index 0000000..bfc1f27 --- /dev/null +++ b/Bitget_Server/data/GlobalOptions.json @@ -0,0 +1,14 @@ +{ + "LogDebugWrite": true, + "SelectAPI": { + "PassphraseKey": "dudgh0505", + "AccessKey": "bg_f93aa530288b6ec9d62095994c51972a", + "SecretKey": "2fce18bcd12ca23a7920c48295a58da031443ea383af8114ed599c1b14ac678e", + "Name": "김영호", + "Id": 0 + }, + "Leverage": 10.0, + "OrderAmount": 50.0, + "CandleIntervalMinutes": 5, + "SelectedCoin": "" +} \ No newline at end of file diff --git a/Bitget_Server/wwwroot/chart.html b/Bitget_Server/wwwroot/chart.html new file mode 100644 index 0000000..08eab63 --- /dev/null +++ b/Bitget_Server/wwwroot/chart.html @@ -0,0 +1,604 @@ + + + + + +Bitget Server — Candle + Indicator + + + + + + + + + + + +
+

Candle + Indicator

+ + + + + + idle + + ← API Console +
+ +
+
+ +
+
+
+ Candlestick + Indicators + + + + + + + +
+ +
+
+ +
+
+ + +
+
Volume (USDT)
+
+ +
+
+ + +
+
Impulse MACD (MD/SH/SB)
+
+ +
+
+ + +
+
PriceLevelTracker state
+
+
+
+
+ + +
+
Active Signals
+
+
+
+
+
+
+ + + + diff --git a/Bitget_Server/wwwroot/index.html b/Bitget_Server/wwwroot/index.html new file mode 100644 index 0000000..628c217 --- /dev/null +++ b/Bitget_Server/wwwroot/index.html @@ -0,0 +1,409 @@ + + + + + +Bitget Server — API Console + + + +
+

Bitget Server — API Console

+ health: — + [Candle Chart →] + [Swagger →] +
+ +
+
+

Endpoints

+ +
+
+ +
+
+ GET + + +
+
+ + + +
+ — + idle +
+ + +
// 엔드포인트를 선택하거나 URL 을 편집 후 Send 를 누르세요.
+
+
+
+ + + +