217 lines
7.7 KiB
C#
217 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Alpaca.Markets;
|
|
using FinlyticBot.Util;
|
|
using FinlyticCore.Models.Trades;
|
|
using FinlyticCore.Services;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace FinlyticBot.Services;
|
|
|
|
public class AlpacaBrokerService : IAlpacaBrokerService
|
|
{
|
|
private readonly ISettingsService _settingsService;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IFinlyticLogger<AlpacaBrokerService> _finlyticLogger;
|
|
|
|
private IAlpacaTradingClient? _cachedClient;
|
|
private string _lastInitKey = string.Empty;
|
|
|
|
public AlpacaBrokerService(
|
|
ISettingsService settingsService,
|
|
IConfiguration configuration,
|
|
IFinlyticLogger<AlpacaBrokerService> finlyticLogger)
|
|
{
|
|
_settingsService = settingsService;
|
|
_configuration = configuration;
|
|
_finlyticLogger = finlyticLogger;
|
|
}
|
|
|
|
private async Task<IAlpacaTradingClient?> GetClientAsync(CancellationToken ct = default)
|
|
{
|
|
string keyId = await _settingsService.GetSettingAsync(SettingKeys.AlpacaKeyId, ct);
|
|
if (string.IsNullOrWhiteSpace(keyId))
|
|
{
|
|
keyId = _configuration["Alpaca:KeyId"] ?? _configuration["Alpaca__KeyId"] ?? string.Empty;
|
|
}
|
|
|
|
string secretKey = await _settingsService.GetSettingAsync(SettingKeys.AlpacaSecretKey, ct);
|
|
if (string.IsNullOrWhiteSpace(secretKey))
|
|
{
|
|
secretKey = _configuration["Alpaca:SecretKey"] ?? _configuration["Alpaca__SecretKey"] ?? string.Empty;
|
|
}
|
|
|
|
bool isPaper = await _settingsService.GetSettingAsync(SettingKeys.AlpacaIsPaper, ct);
|
|
if (string.IsNullOrWhiteSpace(keyId) || string.IsNullOrWhiteSpace(secretKey))
|
|
{
|
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Alpaca API credentials (KeyId/SecretKey) are missing or empty.");
|
|
return null;
|
|
}
|
|
|
|
string currentInitKey = $"{keyId}_{secretKey}_{isPaper}";
|
|
if (_cachedClient != null && _lastInitKey == currentInitKey)
|
|
{
|
|
return _cachedClient;
|
|
}
|
|
|
|
var environment = isPaper ? Alpaca.Markets.Environments.Paper : Alpaca.Markets.Environments.Live;
|
|
_cachedClient = environment.GetAlpacaTradingClient(new SecretKey(keyId, secretKey));
|
|
_lastInitKey = currentInitKey;
|
|
|
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Initialized Alpaca Trading Client (Environment: {Env})", isPaper ? "Paper" : "Live");
|
|
|
|
return _cachedClient;
|
|
}
|
|
|
|
public async Task<BrokerAccountInfo?> GetAccountInfoAsync(CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return null;
|
|
|
|
try
|
|
{
|
|
var account = await client.GetAccountAsync(ct);
|
|
return new BrokerAccountInfo(
|
|
Equity: account.Equity ?? 0m,
|
|
BuyingPower: account.BuyingPower ?? 0m,
|
|
Cash: account.TradableCash,
|
|
Currency: account.Currency ?? "USD",
|
|
IsBlocked: account.IsTradingBlocked
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogErrorAsync(SettingKeys.BotChannel, ex,
|
|
"[AlpacaBroker] Failed to fetch account information from Alpaca.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<IAsset?> GetAssetAsync(string symbol, CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return null;
|
|
|
|
try
|
|
{
|
|
return await client.GetAssetAsync(symbol, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Asset {Symbol} not found or query error: {Msg}", symbol, ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<IClock?> GetMarketClockAsync(CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return null;
|
|
|
|
try
|
|
{
|
|
return await client.GetClockAsync(ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Failed to query market clock: {Msg}", ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<IOrder?> PlaceBracketOrderAsync(
|
|
TradeProposalDto proposal,
|
|
decimal quantity,
|
|
string orderType = "Limit",
|
|
CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return null;
|
|
|
|
var side = string.Equals(proposal.SignalType, "SELL", StringComparison.OrdinalIgnoreCase)
|
|
? OrderSide.Sell
|
|
: OrderSide.Buy;
|
|
|
|
OrderQuantity orderQty = OrderQuantity.Fractional(quantity);
|
|
OrderBase baseOrder;
|
|
|
|
if (string.Equals(orderType, "Market", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
baseOrder = (side == OrderSide.Buy
|
|
? MarketOrder.Buy(proposal.Symbol, orderQty)
|
|
: MarketOrder.Sell(proposal.Symbol, orderQty))
|
|
.Bracket(proposal.TakeProfit, proposal.StopLoss);
|
|
}
|
|
else
|
|
{
|
|
baseOrder = (side == OrderSide.Buy
|
|
? LimitOrder.Buy(proposal.Symbol, orderQty, proposal.EntryPrice)
|
|
: LimitOrder.Sell(proposal.Symbol, orderQty, proposal.EntryPrice))
|
|
.Bracket(proposal.TakeProfit, proposal.StopLoss);
|
|
}
|
|
|
|
baseOrder.Duration = TimeInForce.Gtc;
|
|
|
|
try
|
|
{
|
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Placing Bracket Order for {Symbol}: Side={Side}, Qty={Qty}, Entry=${Entry:F2}, SL=${SL:F2}, TP=${TP:F2}",
|
|
proposal.Symbol, side, quantity, proposal.EntryPrice, proposal.StopLoss, proposal.TakeProfit);
|
|
|
|
var placedOrder = await client.PostOrderAsync(baseOrder, ct);
|
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
|
"[AlpacaBroker] Bracket Order successfully placed with Alpaca! OrderId: {OrderId}, Status: {Status}",
|
|
placedOrder.OrderId, placedOrder.OrderStatus);
|
|
|
|
return placedOrder;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogErrorAsync(SettingKeys.BotChannel, ex,
|
|
"[AlpacaBroker] Failed to place bracket order for {Symbol} on Alpaca.", proposal.Symbol);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> CancelOrderAsync(Guid orderId, CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return false;
|
|
|
|
try
|
|
{
|
|
return await client.CancelOrderAsync(orderId, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogErrorAsync(SettingKeys.BotChannel, ex,
|
|
"[AlpacaBroker] Failed to cancel order {OrderId} on Alpaca.", orderId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyList<IOrder>> GetOpenOrdersAsync(CancellationToken ct = default)
|
|
{
|
|
var client = await GetClientAsync(ct);
|
|
if (client == null) return Array.Empty<IOrder>();
|
|
|
|
try
|
|
{
|
|
var req = new ListOrdersRequest { OrderStatusFilter = OrderStatusFilter.Open };
|
|
return await client.ListOrdersAsync(req, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _finlyticLogger.LogErrorAsync(SettingKeys.BotChannel, ex,
|
|
"[AlpacaBroker] Failed to list open orders from Alpaca.");
|
|
return Array.Empty<IOrder>();
|
|
}
|
|
}
|
|
}
|