Files
PineSript/Saver/Core/SaverService.cs

181 lines
7.4 KiB
C#

using System.Text.Json.Nodes;
using Saver.Adapters;
using Saver.Config;
using Saver.Models;
using Serilog;
namespace Saver.Core
{
/// <summary>
/// 전체 수집 서비스 오케스트레이터.
///
/// 실행 순서 (인터벌별):
/// 0) 복구 모드 (Recovery.Mode != 0 일 때)
/// 1) 과거 데이터 채우기 (최초 데이터 ~ 현재)
/// 2) 실시간 수집 — 모든 인터벌 동시 실행
/// </summary>
public class SaverService : IDisposable
{
private readonly ExchangeConfig _cfg;
private readonly IExchangeAdapter _adapter;
private readonly CandleRepository _repo;
private readonly RateLimiter _rateLimiter;
private readonly RecoveryConfig _recovery;
public SaverService(
ExchangeConfig cfg,
IExchangeAdapter adapter,
string rootPath,
RecoveryConfig recovery)
{
_cfg = cfg;
_adapter = adapter;
_repo = new CandleRepository(rootPath);
_rateLimiter = new RateLimiter(cfg.MaxRequestsPerSec);
_recovery = recovery;
}
public async Task RunAsync(CancellationToken ct = default)
{
Log.Information("[Saver] ══════════════════════════════════════");
Log.Information("[Saver] 거래소: {Name}", _adapter.Name);
Log.Information("[Saver] Rate Limit: {Rps}req/s", _cfg.MaxRequestsPerSec);
if (_recovery.Mode != 0)
Log.Information("[Saver] 복구 모드: {Mode}", _recovery.Mode);
Log.Information("[Saver] ══════════════════════════════════════");
foreach (var product in _cfg.Products.Where(p => p.Enabled))
{
if (ct.IsCancellationRequested) break;
// 유효한 인터벌만 필터링 (미설정 시 "1m" 기본값)
var intervals = product.Intervals
.Where(CandleInterval.IsValid)
.Select(CandleInterval.Normalize)
.Distinct()
.ToList();
if (intervals.Count == 0)
intervals = new List<string> { "1m" };
Log.Information("[Saver] 상품 처리 시작: {Type} / 인터벌: [{Intervals}]",
product.Type, string.Join(", ", intervals));
// ── 심볼 목록 갱신 ─────────────────────────────────
var allSymbols = await _adapter.GetSymbolsAsync(product.Type, ct);
var blacklist = _cfg.Blacklist.Select(s => s.ToUpperInvariant()).ToHashSet();
var whitelist = _cfg.Whitelist.Select(s => s.ToUpperInvariant()).ToHashSet();
var symbols = allSymbols
.Where(s => !blacklist.Contains(s.ToUpperInvariant()))
.Where(s => whitelist.Count == 0 || whitelist.Contains(s.ToUpperInvariant()))
.OrderBy(s => s)
.ToList();
if (whitelist.Count > 0)
Log.Information("[Saver] {Type}: 화이트리스트 적용 → {Active}개 심볼",
product.Type, symbols.Count);
else
Log.Information("[Saver] {Type}: 전체 {All}개 → 블랙리스트 제외 {Active}개",
product.Type, allSymbols.Count, symbols.Count);
if (symbols.Count == 0)
{
Log.Warning("[Saver] {Type}: 수집할 심볼 없음", product.Type);
continue;
}
// ── 인터벌별 복구 + 과거 데이터 채우기 ────────────
bool recoveryRan = false;
foreach (var interval in intervals)
{
if (ct.IsCancellationRequested) break;
var filler = new HistoricalFiller(
_adapter, _repo, _rateLimiter, _cfg.MaxCandlesPerRequest, interval);
// 복구 모드
if (_recovery.Mode != 0)
{
var recovery = new RecoveryService(
_cfg, _adapter, _repo, _rateLimiter, filler, _recovery);
await recovery.RunAsync(product.Type, symbols, interval, ct);
recoveryRan = true;
}
// 과거 데이터 채우기
await filler.FillAllAsync(product.Type, symbols, ct);
}
// 복구 완료 후 Mode 초기화
if (recoveryRan)
{
ResetRecoveryMode();
Log.Information("[Saver] 복구 완료 → 정상 수집으로 전환");
}
// ── 실시간 수집 — 인터벌별 병렬 실행 ───────────────
Log.Information("[Saver] 실시간 수집 시작: {Count}개 인터벌 동시 실행",
intervals.Count);
var pollerTasks = intervals.Select(interval =>
{
var filler = new HistoricalFiller(
_adapter, _repo, _rateLimiter, _cfg.MaxCandlesPerRequest, interval);
var poller = new LivePoller(
_adapter, _repo, _rateLimiter, filler, interval);
return poller.RunAsync(product.Type, symbols, ct);
}).ToList();
await Task.WhenAll(pollerTasks);
}
}
/// <summary>
/// appsettings.json의 Recovery.Mode를 0으로 초기화합니다.
/// </summary>
private static void ResetRecoveryMode()
{
try
{
string path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (!File.Exists(path))
{
Log.Warning("[Saver] appsettings.json을 찾을 수 없어 Mode 초기화를 건너뜁니다.");
return;
}
var json = File.ReadAllText(path);
var node = JsonNode.Parse(json,
nodeOptions: new JsonNodeOptions { PropertyNameCaseInsensitive = true });
if (node is JsonObject root)
{
if (root["Recovery"] is not JsonObject recoveryNode)
{
recoveryNode = new JsonObject();
root["Recovery"] = recoveryNode;
}
recoveryNode["Mode"] = 0;
}
var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true };
File.WriteAllText(path, node!.ToJsonString(options));
Log.Information("[Saver] appsettings.json Recovery.Mode → 0 으로 초기화 완료");
}
catch (Exception ex)
{
Log.Warning(ex, "[Saver] appsettings.json Mode 초기화 실패 — 수동으로 0으로 변경해주세요.");
}
}
public void Dispose()
{
_adapter.Dispose();
_repo.Dispose();
}
}
}