2026-04-20 20:27:42 +09:00
|
|
|
using Bitget_Server.Application.BackgroundServices;
|
|
|
|
|
using Bitget_Server.Application.Options;
|
|
|
|
|
using Bitget_Server.Application.Services;
|
|
|
|
|
using Bitget_Server.Application.State;
|
|
|
|
|
using Bitget_Server.Infrastructure.Bitget;
|
|
|
|
|
using Bitget_Server.Infrastructure.Utils;
|
|
|
|
|
using Microsoft.OpenApi.Models;
|
|
|
|
|
using Serilog;
|
2024-06-02 19:32:08 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 1. Kestrel: HTTP only / port 5000
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.WebHost.ConfigureKestrel(options =>
|
|
|
|
|
{
|
|
|
|
|
options.ListenAnyIP(5000); // HTTP 전용, HTTPS 사용하지 않음
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 2. Options 바인딩
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.Configure<BitgetOptions>(builder.Configuration.GetSection(BitgetOptions.SectionName));
|
|
|
|
|
var bitgetOptions = builder.Configuration.GetSection(BitgetOptions.SectionName).Get<BitgetOptions>() ?? new BitgetOptions();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 3. 영속화 경로 설정 (BaseSettings<GlobalOptions>.StorageDirectory)
|
|
|
|
|
// 컨테이너에서는 /data, 로컬에서는 ./data 로 마운트됨
|
|
|
|
|
// =========================================================================
|
|
|
|
|
var dataDir = Path.GetFullPath(bitgetOptions.DataDirectory);
|
|
|
|
|
var logDir = Path.GetFullPath(bitgetOptions.LogDirectory);
|
|
|
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
|
Directory.CreateDirectory(logDir);
|
|
|
|
|
BaseSettings<GlobalOptions>.StorageDirectory = dataDir;
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 4. Serilog 구성 (Console + 일자별 File Sink)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
Log.Logger = new LoggerConfiguration()
|
|
|
|
|
.MinimumLevel.Is(bitgetOptions.LogDebugWrite
|
|
|
|
|
? Serilog.Events.LogEventLevel.Debug
|
|
|
|
|
: Serilog.Events.LogEventLevel.Information)
|
|
|
|
|
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
|
|
|
|
|
.Enrich.FromLogContext()
|
|
|
|
|
.WriteTo.Console()
|
|
|
|
|
.WriteTo.Map(
|
|
|
|
|
keyPropertyName: "SourceContext",
|
|
|
|
|
defaultKey: "general",
|
|
|
|
|
configure: (key, wt) => wt.File(
|
|
|
|
|
path: Path.Combine(logDir, "bitget-.log"),
|
|
|
|
|
rollingInterval: RollingInterval.Day,
|
|
|
|
|
retainedFileCountLimit: 30,
|
|
|
|
|
outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"))
|
|
|
|
|
.CreateLogger();
|
|
|
|
|
|
|
|
|
|
builder.Host.UseSerilog();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 5. DI 등록 - 상태 / 설정 싱글톤
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.AddSingleton<MarketState>();
|
|
|
|
|
builder.Services.AddSingleton(sp => GlobalOptions.Instance); // BaseSettings 기반 파일 영속화 싱글톤
|
|
|
|
|
builder.Services.AddSingleton<PriceLevelTracker>();
|
|
|
|
|
|
|
|
|
|
// 기본 API 키 자동 선택 (GlobalOptions.SelectAPI 가 비어 있으면 appsettings.json 의 DefaultApiId 로 초기화)
|
|
|
|
|
// 반드시 다른 HostedService 보다 먼저 등록되어야 함 (HostedServices 는 등록 순서대로 StartAsync).
|
|
|
|
|
builder.Services.AddHostedService<ApiKeyBootstrapper>();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 6. DI 등록 - Bitget Infrastructure
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.AddSingleton(sp => new BitgetRateLimiter(
|
|
|
|
|
bitgetOptions.RateLimitPerSecond,
|
|
|
|
|
sp.GetRequiredService<ILogger<BitgetRateLimiter>>()));
|
|
|
|
|
|
|
|
|
|
// Bitget 단일 엔드포인트만 호출하므로 HttpClient 를 Singleton 으로 공유해도 안전.
|
|
|
|
|
// (BackgroundService 가 Singleton 이므로 BitgetClient 도 Singleton 이어야 캡처 문제가 없음)
|
|
|
|
|
builder.Services.AddSingleton(sp =>
|
2024-06-02 19:32:08 +09:00
|
|
|
{
|
2026-04-20 20:27:42 +09:00
|
|
|
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
|
|
|
|
http.DefaultRequestHeaders.Add("User-Agent", "Bitget_Server/1.0");
|
|
|
|
|
return http;
|
|
|
|
|
});
|
|
|
|
|
builder.Services.AddSingleton<BitgetClient>();
|
|
|
|
|
builder.Services.AddSingleton<BitgetWebSocketClient>();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 7. DI 등록 - Application Services (모두 Singleton: 상태 보관 + BackgroundService 에서 참조)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.AddSingleton<SettingsService>();
|
|
|
|
|
builder.Services.AddSingleton<BitgetTradingService>();
|
|
|
|
|
builder.Services.AddSingleton<InitialCandleLoader>();
|
|
|
|
|
builder.Services.AddSingleton<SignalProcessService>();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 8. DI 등록 - BackgroundServices (HostedServices)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.AddHostedService<MarketInfoBackgroundService>();
|
|
|
|
|
builder.Services.AddHostedService<WebSocketCalcBackgroundService>();
|
|
|
|
|
builder.Services.AddHostedService<SignalProcessBackgroundService>();
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 9. MVC / Swagger / ProblemDetails
|
|
|
|
|
// =========================================================================
|
|
|
|
|
builder.Services.AddControllers()
|
|
|
|
|
.AddNewtonsoftJson(opts =>
|
2024-06-02 19:32:08 +09:00
|
|
|
{
|
2026-04-20 20:27:42 +09:00
|
|
|
opts.SerializerSettings.ReferenceLoopHandling =
|
|
|
|
|
Newtonsoft.Json.ReferenceLoopHandling.Ignore;
|
|
|
|
|
opts.SerializerSettings.NullValueHandling =
|
|
|
|
|
Newtonsoft.Json.NullValueHandling.Ignore;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
builder.Services.AddProblemDetails();
|
|
|
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
|
|
|
builder.Services.AddSwaggerGen(options =>
|
|
|
|
|
{
|
|
|
|
|
options.SwaggerDoc("v1", new OpenApiInfo
|
|
|
|
|
{
|
|
|
|
|
Title = "Bitget Server",
|
|
|
|
|
Version = "v1",
|
|
|
|
|
Description = "기존 Bitget WinForms 애플리케이션을 ASP.NET Core 8 REST API 로 이식한 서버."
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-06-02 20:48:47 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
// =========================================================================
|
|
|
|
|
// 10. 미들웨어 파이프라인 - HTTPS 리디렉션 없음
|
|
|
|
|
// =========================================================================
|
|
|
|
|
var app = builder.Build();
|
2024-06-02 19:32:08 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
app.UseExceptionHandler();
|
|
|
|
|
app.UseStatusCodePages();
|
2024-06-02 19:32:08 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
// Swagger 는 환경 불문 항상 활성화
|
|
|
|
|
app.UseSwagger();
|
|
|
|
|
app.UseSwaggerUI(options =>
|
|
|
|
|
{
|
|
|
|
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Bitget Server v1");
|
|
|
|
|
options.RoutePrefix = "swagger";
|
|
|
|
|
});
|
2024-06-02 19:32:08 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
app.UseSerilogRequestLogging();
|
2024-06-02 19:32:08 +09:00
|
|
|
|
2026-04-20 20:27:42 +09:00
|
|
|
app.MapControllers();
|
|
|
|
|
|
|
|
|
|
// 루트 접속 시 Swagger 로 리다이렉트
|
|
|
|
|
app.MapGet("/", () => Results.Redirect("/swagger"));
|
|
|
|
|
|
|
|
|
|
Log.Information("Bitget_Server 시작 — URL http://*:5000, DataDir={DataDir}, LogDir={LogDir}", dataDir, logDir);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
app.Run();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
Log.CloseAndFlush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 애플리케이션 시작 시 appsettings 의 DefaultApiId 로 GlobalOptions.SelectAPI 를 초기화한다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal sealed class ApiKeyBootstrapper : IHostedService
|
|
|
|
|
{
|
|
|
|
|
private readonly IServiceProvider _sp;
|
|
|
|
|
public ApiKeyBootstrapper(IServiceProvider sp) => _sp = sp;
|
|
|
|
|
|
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var global = _sp.GetRequiredService<GlobalOptions>();
|
|
|
|
|
var opts = _sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<BitgetOptions>>().Value;
|
|
|
|
|
|
|
|
|
|
if (global.SelectAPI == null && opts.ApiKeys?.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
global.SelectAPI = opts.ApiKeys.FirstOrDefault(k => k.Id == opts.DefaultApiId)
|
|
|
|
|
?? opts.ApiKeys.First();
|
|
|
|
|
global.Save();
|
|
|
|
|
}
|
2024-06-02 19:32:08 +09:00
|
|
|
}
|
2026-04-20 20:27:42 +09:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Warning(ex, "ApiKey 부트스트랩 실패 — 이후 /api/settings/api-key 로 수동 설정 필요");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
2024-06-02 19:32:08 +09:00
|
|
|
}
|
2026-04-20 20:27:42 +09:00
|
|
|
|
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
2024-06-02 19:32:08 +09:00
|
|
|
}
|