단순 이관

This commit is contained in:
2026-04-20 20:27:42 +09:00
parent e6c530ff48
commit 83fb215eec
100 changed files with 4209 additions and 1467 deletions

View File

@@ -0,0 +1,47 @@
using Bitget_Server.Application.Services;
using Bitget_Server.Domain.Models;
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 기존 WinForms 코인 선택 그리드 데이터를 REST 화.
/// </summary>
[ApiController]
[Route("api/coins")]
[Produces("application/json")]
public class CoinsController : ControllerBase
{
private readonly SettingsService _settings;
private readonly ILogger<CoinsController> _logger;
public CoinsController(SettingsService settings, ILogger<CoinsController> logger)
{
_settings = settings;
_logger = logger;
}
/// <summary>
/// USDT 기준 Bitget 선물 마켓 정보 목록을 반환한다.
/// 내부적으로 BitgetClient 캐시(_state.marketInfoList)를 우선 사용하고 비어 있으면 호출.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<MarketInfo>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<ActionResult<IEnumerable<MarketInfo>>> GetCoins(CancellationToken ct)
{
try
{
var coins = await _settings.GetCoinsAsync(ct);
return Ok(coins);
}
catch (Exception ex)
{
_logger.LogError(ex, "코인 조회 실패");
return Problem(
detail: ex.Message,
statusCode: StatusCodes.Status502BadGateway,
title: "Failed to fetch coins from Bitget");
}
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 단순 헬스체크. Docker / 로드밸런서에서 사용.
/// </summary>
[ApiController]
[Route("api/health")]
[Produces("application/json")]
public class HealthController : ControllerBase
{
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Get() => Ok(new
{
status = "ok",
timestampUtc = DateTime.UtcNow,
version = typeof(HealthController).Assembly.GetName().Version?.ToString() ?? "unknown"
});
}

View File

@@ -0,0 +1,148 @@
using Bitget_Server.Api.Dtos;
using Bitget_Server.Application.Services;
using Bitget_Server.Infrastructure.Utils;
using Bitget_Server.Domain.Enums;
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 기존 WinForms MainForm 의 ComboBox / 텍스트박스 / API 선택 라디오버튼을 REST 화.
/// </summary>
[ApiController]
[Route("api/settings")]
[Produces("application/json")]
public class SettingsController : ControllerBase
{
private readonly SettingsService _settings;
public SettingsController(SettingsService settings)
{
_settings = settings;
}
// ===== 캔들 간격 =====
/// <summary>현재 선택된 캔들 간격(분)을 반환.</summary>
[HttpGet("candle-interval")]
[ProducesResponseType(typeof(CandleIntervalDto), StatusCodes.Status200OK)]
public ActionResult<CandleIntervalDto> GetCandleInterval()
{
var minutes = _settings.GetCandleIntervalMinutes();
var interval = (MCandleTimeType)minutes;
return Ok(new CandleIntervalDto(minutes, Convertor.GetEnumDescription(interval)));
}
/// <summary>캔들 간격을 변경.</summary>
[HttpPut("candle-interval")]
[ProducesResponseType(typeof(CandleIntervalDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<CandleIntervalDto> UpdateCandleInterval([FromBody] UpdateCandleIntervalRequest req)
{
try
{
_settings.SetCandleIntervalMinutes(req.Minutes);
var interval = (MCandleTimeType)req.Minutes;
return Ok(new CandleIntervalDto(req.Minutes, Convertor.GetEnumDescription(interval)));
}
catch (ArgumentException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest, title: "Invalid candle interval");
}
}
/// <summary>지원되는 캔들 간격 목록.</summary>
[HttpGet("candle-interval/options")]
[ProducesResponseType(typeof(IEnumerable<object>), StatusCodes.Status200OK)]
public ActionResult<IEnumerable<object>> ListCandleIntervals()
=> Ok(_settings.ListCandleIntervals());
// ===== 선택 코인 =====
[HttpGet("selected-coin")]
[ProducesResponseType(typeof(SelectedCoinDto), StatusCodes.Status200OK)]
public ActionResult<SelectedCoinDto> GetSelectedCoin()
=> Ok(new SelectedCoinDto(_settings.GetSelectedCoin()));
[HttpPut("selected-coin")]
[ProducesResponseType(typeof(SelectedCoinDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<SelectedCoinDto> UpdateSelectedCoin([FromBody] UpdateSelectedCoinRequest req)
{
try
{
_settings.SetSelectedCoin(req.MarketName);
return Ok(new SelectedCoinDto(req.MarketName));
}
catch (ArgumentException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest, title: "Invalid coin");
}
}
// ===== 레버리지 =====
[HttpGet("leverage")]
[ProducesResponseType(typeof(LeverageDto), StatusCodes.Status200OK)]
public ActionResult<LeverageDto> GetLeverage()
=> Ok(new LeverageDto(_settings.GetLeverage()));
[HttpPut("leverage")]
[ProducesResponseType(typeof(LeverageDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<LeverageDto> UpdateLeverage([FromBody] UpdateLeverageRequest req)
{
if (req.Leverage <= 0)
return Problem(detail: "Leverage must be > 0", statusCode: StatusCodes.Status400BadRequest, title: "Invalid leverage");
_settings.SetLeverage(req.Leverage);
return Ok(new LeverageDto(req.Leverage));
}
// ===== 주문 수량 =====
[HttpGet("order-amount")]
[ProducesResponseType(typeof(OrderAmountDto), StatusCodes.Status200OK)]
public ActionResult<OrderAmountDto> GetOrderAmount()
=> Ok(new OrderAmountDto(_settings.GetOrderAmount()));
[HttpPut("order-amount")]
[ProducesResponseType(typeof(OrderAmountDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<OrderAmountDto> UpdateOrderAmount([FromBody] UpdateOrderAmountRequest req)
{
if (req.OrderAmount <= 0)
return Problem(detail: "OrderAmount must be > 0", statusCode: StatusCodes.Status400BadRequest, title: "Invalid order amount");
_settings.SetOrderAmount(req.OrderAmount);
return Ok(new OrderAmountDto(req.OrderAmount));
}
// ===== Selected API 키 =====
[HttpGet("api-key")]
[ProducesResponseType(typeof(SelectedApiDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<SelectedApiDto> GetSelectedApi()
{
var id = _settings.GetSelectedApiId();
if (id == null) return NoContent();
return Ok(new SelectedApiDto(id.Value, string.Empty));
}
[HttpPut("api-key")]
[ProducesResponseType(typeof(SelectedApiDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<SelectedApiDto> UpdateSelectedApi([FromBody] UpdateSelectedApiRequest req)
{
try
{
_settings.SetSelectedApiId(req.Id);
return Ok(new SelectedApiDto(req.Id, string.Empty));
}
catch (KeyNotFoundException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound, title: "API key not found");
}
}
}

View File

@@ -0,0 +1,97 @@
using Bitget_Server.Api.Dtos;
using Bitget_Server.Application.Services;
using Bitget_Server.Application.State;
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 기존 WinForms 의 구름떼/영호 조건식 체크박스 및 신호검색 그리드를 REST 화.
/// </summary>
[ApiController]
[Route("api/signals")]
[Produces("application/json")]
public class SignalsController : ControllerBase
{
private readonly SignalProcessService _signalProcess;
private readonly MarketState _state;
public SignalsController(SignalProcessService signalProcess, MarketState state)
{
_signalProcess = signalProcess;
_state = state;
}
/// <summary>현재 활성화된 시그널 목록 + 카운트.</summary>
[HttpGet]
[ProducesResponseType(typeof(SignalListResponse), StatusCodes.Status200OK)]
public ActionResult<SignalListResponse> GetSignals()
{
var result = new SignalListResponse(
_signalProcess.SignalsLongUp.Select(Map),
_signalProcess.SignalsShortDown.Select(Map),
_signalProcess.SignalsLongDown.Select(Map),
_signalProcess.SignalsShortUp.Select(Map),
_signalProcess.LongCount,
_signalProcess.ShortCount);
return Ok(result);
static SignalSummaryDto Map(Domain.Models.SignalInfoData s)
=> new(s.MarketName, s.Description, s.SignalStart, s.Price, s.Rate,
s.IndicatorPrice, s.IndicatorRate, s.MCandleTimeType);
}
/// <summary>조건식 상태 조회.</summary>
[HttpGet("conditions")]
[ProducesResponseType(typeof(SignalConditionsDto), StatusCodes.Status200OK)]
public ActionResult<SignalConditionsDto> GetConditions()
=> Ok(new SignalConditionsDto(
_signalProcess.Checked구름떼상향돌파,
_signalProcess.Checked구름떼하향돌파,
_signalProcess.영호조건식_상향돌파,
_signalProcess.영호조건식_하향돌파));
/// <summary>조건식 상태 변경.</summary>
[HttpPut("conditions")]
[ProducesResponseType(typeof(SignalConditionsDto), StatusCodes.Status200OK)]
public ActionResult<SignalConditionsDto> UpdateConditions([FromBody] UpdateSignalConditionsRequest req)
{
_signalProcess.Checked구름떼상향돌파 = req.구름떼상향돌파;
_signalProcess.Checked구름떼하향돌파 = req.구름떼하향돌파;
_signalProcess.영호조건식_상향돌파 = req.영호조건식_상향돌파;
_signalProcess.영호조건식_하향돌파 = req.영호조건식_하향돌파;
return Ok(new SignalConditionsDto(
_signalProcess.Checked구름떼상향돌파,
_signalProcess.Checked구름떼하향돌파,
_signalProcess.영호조건식_상향돌파,
_signalProcess.영호조건식_하향돌파));
}
/// <summary>체크된 마켓/인터벌 목록 조회 (신호검색 대상).</summary>
[HttpGet("checked-markets")]
[ProducesResponseType(typeof(CheckedMarketsDto), StatusCodes.Status200OK)]
public ActionResult<CheckedMarketsDto> GetCheckedMarkets()
{
lock (_state.SyncRoot)
{
return Ok(new CheckedMarketsDto(
_state.CheckedMarkets.Select(m => m?.ToString() ?? string.Empty).ToList(),
_state.IntervalList.Select(i => i?.ToString() ?? string.Empty).ToList()));
}
}
/// <summary>체크된 마켓/인터벌 일괄 설정.</summary>
[HttpPut("checked-markets")]
[ProducesResponseType(typeof(CheckedMarketsDto), StatusCodes.Status200OK)]
public ActionResult<CheckedMarketsDto> UpdateCheckedMarkets([FromBody] UpdateCheckedMarketsRequest req)
{
lock (_state.SyncRoot)
{
_state.CheckedMarkets = req.Markets.Cast<object>().ToList();
_state.IntervalList = req.Intervals.Cast<object>().ToList();
}
return Ok(new CheckedMarketsDto(req.Markets, req.Intervals));
}
}

View File

@@ -0,0 +1,122 @@
using Bitget_Server.Api.Dtos;
using Bitget_Server.Application.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 기존 WinForms 의 주문/취소/잔고 버튼을 REST 화.
/// Bitget Private API 를 호출하므로 반드시 선택된 API 키가 설정되어 있어야 한다.
/// </summary>
[ApiController]
[Route("api/trading")]
[Produces("application/json")]
public class TradingController : ControllerBase
{
private readonly BitgetTradingService _trading;
private readonly ILogger<TradingController> _logger;
public TradingController(BitgetTradingService trading, ILogger<TradingController> logger)
{
_trading = trading;
_logger = logger;
}
/// <summary>Bitget 서버 타임스탬프 조회 (헬스체크 용도로도 활용).</summary>
[HttpGet("timestamp")]
[ProducesResponseType(typeof(long), StatusCodes.Status200OK)]
public async Task<ActionResult<long>> GetTimestamp(CancellationToken ct)
=> Ok(await _trading.GetTimeStampAsync(ct));
/// <summary>선물 계좌 잔고/포지션 조회.</summary>
[HttpGet("account")]
[ProducesResponseType(typeof(List<Dictionary<string, object>>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<IActionResult> GetAccount(CancellationToken ct)
{
var result = await _trading.GetFutureAccountListAsync(ct);
if (result == null)
return Problem(detail: "Bitget API 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Account fetch failed");
return Ok(result);
}
/// <summary>미체결 주문 조회.</summary>
[HttpGet("orders/pending")]
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<IActionResult> GetPendingOrders(CancellationToken ct)
{
var result = await _trading.GetPendingOrdersAsync(ct);
if (result == null)
return Problem(detail: "Bitget API 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Pending orders fetch failed");
return Ok(result);
}
/// <summary>주문 상세 조회.</summary>
[HttpGet("orders/{symbol}/{orderId}")]
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetOrderDetail(string symbol, string orderId, CancellationToken ct)
{
var result = await _trading.GetFutureOrderDetailAsync(symbol, orderId, ct);
if (result == null)
return Problem(detail: "주문 상세 응답 없음", statusCode: StatusCodes.Status404NotFound, title: "Order detail not found");
return Ok(result);
}
/// <summary>신규 주문.</summary>
[HttpPost("orders")]
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderRequest req, CancellationToken ct)
{
try
{
var result = await _trading.FutureOrderAsync(
req.Symbol,
req.MarginMode,
req.Size,
req.Price,
req.Side,
req.OrderType,
req.Leverage,
req.ClientOrderId ?? string.Empty,
ct);
if (result == null)
return Problem(detail: "주문 실패 (MarginMode/Leverage/Order 체인 중 단계 실패)",
statusCode: StatusCodes.Status502BadGateway, title: "Order failed");
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "주문 처리 실패: {Symbol}", req.Symbol);
return Problem(detail: ex.Message, statusCode: StatusCodes.Status500InternalServerError, title: "Order processing error");
}
}
/// <summary>주문 취소.</summary>
[HttpDelete("orders")]
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<IActionResult> CancelOrder([FromBody] CancelOrderRequest req, CancellationToken ct)
{
var result = await _trading.CancelFutureOrderAsync(req.Symbol, req.OrderId, ct);
if (result == null)
return Problem(detail: "취소 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Cancel failed");
return Ok(result);
}
/// <summary>포지션 모드 변경(hedge / one_way).</summary>
[HttpPut("position-mode")]
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
public async Task<IActionResult> ChangePositionMode([FromBody] ChangePositionModeRequest req, CancellationToken ct)
{
var result = await _trading.ChangeFuturePositionModeAsync(req.Mode, ct);
if (result == null)
return Problem(detail: "포지션 모드 변경 실패", statusCode: StatusCodes.Status502BadGateway, title: "Position mode change failed");
return Ok(result);
}
}

View File

@@ -0,0 +1,66 @@
using Bitget_Server.Domain.Enums;
using Bitget_Server.Infrastructure.Bitget;
using Microsoft.AspNetCore.Mvc;
namespace Bitget_Server.Api.Controllers;
/// <summary>
/// 기존 WinForms 의 BitGetWebSocektStart / closeWebSocket 버튼 REST 화.
/// </summary>
[ApiController]
[Route("api/websocket")]
[Produces("application/json")]
public class WebSocketController : ControllerBase
{
private readonly BitgetWebSocketClient _ws;
private readonly ILogger<WebSocketController> _logger;
public WebSocketController(BitgetWebSocketClient ws, ILogger<WebSocketController> logger)
{
_ws = ws;
_logger = logger;
}
/// <summary>현재 WebSocket 연결 상태.</summary>
[HttpGet("status")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
public IActionResult GetStatus() => Ok(new { connected = _ws.IsConnected });
/// <summary>
/// WebSocket 연결 시작. subscribeArgs 는 Bitget 공식 포맷의 args 리스트.
/// 예: [{"instType":"USDT-FUTURES","channel":"ticker","instId":"BTCUSDT"}]
/// </summary>
[HttpPost("start")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Start(
[FromQuery] BitgetWebSocketType type,
[FromBody] IEnumerable<object> subscribeArgs,
CancellationToken ct)
{
try
{
var dic = new Dictionary<string, object>
{
{ "op", "subscribe" },
{ "args", subscribeArgs ?? Array.Empty<object>() }
};
await _ws.StartAsync(type, dic, ct);
return Accepted();
}
catch (Exception ex)
{
_logger.LogError(ex, "WebSocket Start 실패");
return Problem(detail: ex.Message, statusCode: StatusCodes.Status500InternalServerError, title: "WebSocket start failed");
}
}
/// <summary>WebSocket 연결 종료.</summary>
[HttpPost("stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Stop(CancellationToken ct)
{
await _ws.StopAsync(ct);
return NoContent();
}
}