Files
Finlytic/FinlyticBot/Services/AlpacaBrokerService.cs
T

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>();
}
}
}