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 _finlyticLogger; private IAlpacaTradingClient? _cachedClient; private string _lastInitKey = string.Empty; public AlpacaBrokerService( ISettingsService settingsService, IConfiguration configuration, IFinlyticLogger finlyticLogger) { _settingsService = settingsService; _configuration = configuration; _finlyticLogger = finlyticLogger; } private async Task 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 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 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 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 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 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> GetOpenOrdersAsync(CancellationToken ct = default) { var client = await GetClientAsync(ct); if (client == null) return Array.Empty(); 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(); } } }