181 lines
7.4 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|