feat(bot): add FinlyticBot autonomous paper trading microservice with Alpaca Markets API integration
This commit is contained in:
+15
-13
@@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAssets", "FinlyticAssets\FinlyticAssets.csproj", "{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAssets", "FinlyticAssets\FinlyticAssets.csproj", "{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTrades", "FinlyticT
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBot", "FinlyticBot\FinlyticBot.csproj", "{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -69,18 +71,6 @@ Global
|
|||||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.Build.0 = Release|Any CPU
|
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.ActiveCfg = Release|Any CPU
|
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.Build.0 = Release|Any CPU
|
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -119,6 +109,18 @@ Global
|
|||||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ public class AdminSettingsController : ControllerBase
|
|||||||
["FinlyticSentiment"] = "sentiment",
|
["FinlyticSentiment"] = "sentiment",
|
||||||
["FinlyticAnalyzer"] = "analyzer",
|
["FinlyticAnalyzer"] = "analyzer",
|
||||||
["FinlyticTrades"] = "trades",
|
["FinlyticTrades"] = "trades",
|
||||||
["FinlyticAssets"] = "assets"
|
["FinlyticAssets"] = "assets",
|
||||||
|
["FinlyticBot"] = "bot"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<string, List<ServiceConfigItem>> _inMemorySettings = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly ConcurrentDictionary<string, List<ServiceConfigItem>> _inMemorySettings = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using FinlyticBot.Entities;
|
||||||
|
using FinlyticCore.Database;
|
||||||
|
using FinlyticCore.Entities.Settings;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Database;
|
||||||
|
|
||||||
|
public class BotDbContext : DbContext, ISettingsDbContext
|
||||||
|
{
|
||||||
|
public BotDbContext(DbContextOptions<BotDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
|
||||||
|
public DbSet<ExecutedPaperTradeEntity> ExecutedPaperTrades => Set<ExecutedPaperTradeEntity>();
|
||||||
|
public DbSet<BotAuditLogEntity> BotAuditLogs => Set<BotAuditLogEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SettingEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.Key).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ExecutedPaperTradeEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(e => e.TradeId).IsUnique();
|
||||||
|
entity.HasIndex(e => e.Symbol);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
entity.HasIndex(e => e.AlpacaOrderId);
|
||||||
|
entity.HasIndex(e => e.Status);
|
||||||
|
entity.HasIndex(e => e.PlacedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<BotAuditLogEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(e => e.TradeId);
|
||||||
|
entity.HasIndex(e => e.Symbol);
|
||||||
|
entity.HasIndex(e => e.Action);
|
||||||
|
entity.HasIndex(e => e.Timestamp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
|
||||||
|
COPY ["FinlyticBot/FinlyticBot.csproj", "FinlyticBot/"]
|
||||||
|
RUN dotnet restore "FinlyticBot/FinlyticBot.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/FinlyticBot"
|
||||||
|
RUN dotnet build "FinlyticBot.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "FinlyticBot.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "FinlyticBot.dll"]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Entities;
|
||||||
|
|
||||||
|
[Table("BotAuditLogs")]
|
||||||
|
public class BotAuditLogEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string TradeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Action { get; set; } = string.Empty; // "Evaluated", "Rejected", "OrderPlaced", "OrderFilled", "OrderClosed", "EmergencyStopped"
|
||||||
|
|
||||||
|
public bool IsAccepted { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? CalculatedSize { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? AccountEquity { get; set; }
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Entities;
|
||||||
|
|
||||||
|
[Table("ExecutedPaperTrades")]
|
||||||
|
public class ExecutedPaperTradeEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string TradeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Guid? AlpacaOrderId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(20)]
|
||||||
|
public string Side { get; set; } = "BUY"; // "BUY", "SELL"
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal SignalEntryPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? ActualFillPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal StopLossPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal TakeProfitPrice1 { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? TakeProfitPrice2 { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal CalculatedCrv { get; set; }
|
||||||
|
|
||||||
|
public double WinRate { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? SlippagePercent { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal RealizedPnl { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? RealizedPnlPercent { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Status { get; set; } = "Proposed"; // "Proposed", "Submitted", "Accepted", "PartiallyFilled", "Filled", "Closed", "Rejected", "Canceled"
|
||||||
|
|
||||||
|
[MaxLength(1000)]
|
||||||
|
public string? RejectReason { get; set; }
|
||||||
|
|
||||||
|
public DateTime PlacedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public DateTime? FilledAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ClosedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Alpaca.Markets" Version="7.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticBot.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticBot.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDbContext))]
|
||||||
|
[Migration("20260817142432_InitBotDatabase")]
|
||||||
|
partial class InitBotDatabase
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticBot.Entities.BotAuditLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("AccountEquity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CalculatedSize")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAccepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Action");
|
||||||
|
|
||||||
|
b.HasIndex("Symbol");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId");
|
||||||
|
|
||||||
|
b.ToTable("BotAuditLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticBot.Entities.ExecutedPaperTradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ActualFillPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AlpacaOrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("CalculatedCrv")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClosedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FilledAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PlacedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RealizedPnl")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RealizedPnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("RejectReason")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Side")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<decimal>("SignalEntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SlippagePercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("StopLossPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TakeProfitPrice1")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TakeProfitPrice2")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<double>("WinRate")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlpacaOrderId");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("PlacedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("Symbol");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ExecutedPaperTrades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceIdentifier")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DynamicSettings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitBotDatabase : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BotAuditLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TradeId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Action = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
IsAccepted = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Reason = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
CalculatedSize = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
AccountEquity = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BotAuditLogs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DynamicSettings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||||
|
ValueJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
LastUpdatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DynamicSettings", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ExecutedPaperTrades",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TradeId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Isin = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
CompanyName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
AlpacaOrderId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
Side = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
Quantity = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
SignalEntryPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
ActualFillPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
StopLossPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
TakeProfitPrice1 = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
TakeProfitPrice2 = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
CalculatedCrv = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
WinRate = table.Column<double>(type: "double precision", nullable: false),
|
||||||
|
SlippagePercent = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
RealizedPnl = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
RealizedPnlPercent = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
Status = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
RejectReason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||||
|
PlacedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
FilledAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ClosedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ExecutedPaperTrades", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BotAuditLogs_Action",
|
||||||
|
table: "BotAuditLogs",
|
||||||
|
column: "Action");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BotAuditLogs_Symbol",
|
||||||
|
table: "BotAuditLogs",
|
||||||
|
column: "Symbol");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BotAuditLogs_Timestamp",
|
||||||
|
table: "BotAuditLogs",
|
||||||
|
column: "Timestamp");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BotAuditLogs_TradeId",
|
||||||
|
table: "BotAuditLogs",
|
||||||
|
column: "TradeId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_AlpacaOrderId",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "AlpacaOrderId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_Isin",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "Isin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_PlacedAt",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "PlacedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_Status",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_Symbol",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "Symbol");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExecutedPaperTrades_TradeId",
|
||||||
|
table: "ExecutedPaperTrades",
|
||||||
|
column: "TradeId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BotAuditLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DynamicSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ExecutedPaperTrades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticBot.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticBot.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BotDbContext))]
|
||||||
|
partial class BotDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticBot.Entities.BotAuditLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("AccountEquity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CalculatedSize")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAccepted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Action");
|
||||||
|
|
||||||
|
b.HasIndex("Symbol");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId");
|
||||||
|
|
||||||
|
b.ToTable("BotAuditLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticBot.Entities.ExecutedPaperTradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ActualFillPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AlpacaOrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("CalculatedCrv")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClosedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FilledAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PlacedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RealizedPnl")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RealizedPnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("RejectReason")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Side")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<decimal>("SignalEntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SlippagePercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("StopLossPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TakeProfitPrice1")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TakeProfitPrice2")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<double>("WinRate")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlpacaOrderId");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("PlacedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("Symbol");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ExecutedPaperTrades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceIdentifier")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DynamicSettings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using FinlyticBot.Database;
|
||||||
|
using FinlyticBot.Services;
|
||||||
|
using FinlyticBot.Util;
|
||||||
|
using FinlyticCore.Database;
|
||||||
|
using FinlyticCore.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
// 1. DbContext (Scoped)
|
||||||
|
builder.Services.AddDbContext<BotDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<BotDbContext>());
|
||||||
|
|
||||||
|
// 2. Core Services
|
||||||
|
builder.Services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
|
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
|
||||||
|
|
||||||
|
// 3. Domain Services
|
||||||
|
builder.Services.AddSingleton<IBotRiskSizingService, BotRiskSizingService>();
|
||||||
|
builder.Services.AddSingleton<IAlpacaBrokerService, AlpacaBrokerService>();
|
||||||
|
builder.Services.AddSingleton<IBotOrderExecutionService, BotOrderExecutionService>();
|
||||||
|
|
||||||
|
// 4. Hosted Services
|
||||||
|
builder.Services.AddSingleton<BotMqttClient>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<BotMqttClient>());
|
||||||
|
builder.Services.AddHostedService<AlpacaWebSocketMonitorWorker>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
// Run DB Migrations
|
||||||
|
using (var scope = host.Services.CreateScope())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<BotDbContext>();
|
||||||
|
await context.Database.MigrateAsync();
|
||||||
|
Console.WriteLine("Database migrations successfully executed for FinlyticBot.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Critical error during database migration for FinlyticBot: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Alpaca.Markets;
|
||||||
|
using FinlyticBot.Database;
|
||||||
|
using FinlyticBot.Util;
|
||||||
|
using FinlyticCore.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Services;
|
||||||
|
|
||||||
|
public class AlpacaWebSocketMonitorWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly BotMqttClient _mqttClient;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IFinlyticLogger<AlpacaWebSocketMonitorWorker> _finlyticLogger;
|
||||||
|
|
||||||
|
private IAlpacaStreamingClient? _streamingClient;
|
||||||
|
|
||||||
|
public AlpacaWebSocketMonitorWorker(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ISettingsService settingsService,
|
||||||
|
BotMqttClient mqttClient,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IFinlyticLogger<AlpacaWebSocketMonitorWorker> finlyticLogger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_settingsService = settingsService;
|
||||||
|
_mqttClient = mqttClient;
|
||||||
|
_configuration = configuration;
|
||||||
|
_finlyticLogger = finlyticLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[AlpacaWebSocketMonitor] Starting Alpaca Trade Update Stream Monitor...");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string keyId = await _settingsService.GetSettingAsync(SettingKeys.AlpacaKeyId, stoppingToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(keyId))
|
||||||
|
{
|
||||||
|
keyId = _configuration["Alpaca:KeyId"] ?? _configuration["Alpaca__KeyId"] ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
string secretKey = await _settingsService.GetSettingAsync(SettingKeys.AlpacaSecretKey, stoppingToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(secretKey))
|
||||||
|
{
|
||||||
|
secretKey = _configuration["Alpaca:SecretKey"] ?? _configuration["Alpaca__SecretKey"] ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPaper = await _settingsService.GetSettingAsync(SettingKeys.AlpacaIsPaper, stoppingToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(keyId) || string.IsNullOrWhiteSpace(secretKey))
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var environment = isPaper ? Alpaca.Markets.Environments.Paper : Alpaca.Markets.Environments.Live;
|
||||||
|
_streamingClient = environment.GetAlpacaStreamingClient(new SecretKey(keyId, secretKey));
|
||||||
|
|
||||||
|
_streamingClient.OnTradeUpdate += HandleTradeUpdate;
|
||||||
|
|
||||||
|
var authStatus = await _streamingClient.ConnectAndAuthenticateAsync(stoppingToken);
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[AlpacaWebSocketMonitor] Connected & Authenticated to Alpaca Streaming WS. Status: {Status}", authStatus.ToString());
|
||||||
|
|
||||||
|
// Keep connection alive until cancellation
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
using (stoppingToken.Register(() => tcs.TrySetResult(true)))
|
||||||
|
{
|
||||||
|
await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _streamingClient.DisconnectAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel, ex,
|
||||||
|
"[AlpacaWebSocketMonitor] Streaming WebSocket disconnected. Retrying in 10s...");
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTradeUpdate(ITradeUpdate update)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BotDbContext>();
|
||||||
|
|
||||||
|
var order = update.Order;
|
||||||
|
if (order == null) return;
|
||||||
|
|
||||||
|
var trade = await dbContext.ExecutedPaperTrades
|
||||||
|
.FirstOrDefaultAsync(t => t.AlpacaOrderId == order.OrderId);
|
||||||
|
|
||||||
|
if (trade == null)
|
||||||
|
{
|
||||||
|
// Check if it's a child order (SL / TP) of an existing trade
|
||||||
|
trade = await dbContext.ExecutedPaperTrades
|
||||||
|
.Where(t => t.Symbol == order.Symbol && t.Status == "Filled")
|
||||||
|
.OrderByDescending(t => t.PlacedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trade == null) return;
|
||||||
|
|
||||||
|
if (update.Event == TradeEvent.Fill)
|
||||||
|
{
|
||||||
|
decimal fillPrice = update.Price ?? order.AverageFillPrice ?? trade.SignalEntryPrice;
|
||||||
|
trade.ActualFillPrice = fillPrice;
|
||||||
|
trade.Status = "Filled";
|
||||||
|
trade.FilledAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (trade.SignalEntryPrice > 0)
|
||||||
|
{
|
||||||
|
trade.SlippagePercent = Math.Round(((fillPrice - trade.SignalEntryPrice) / trade.SignalEntryPrice) * 100m, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[AlpacaTradeUpdate] Order FILLED for {Symbol}: FillPrice=${Price:F2} (Signal: ${SigPrice:F2}, Slippage: {Slip:F3}%)",
|
||||||
|
trade.Symbol, fillPrice, trade.SignalEntryPrice, trade.SlippagePercent ?? 0m);
|
||||||
|
}
|
||||||
|
else if (update.Event == TradeEvent.PartialFill)
|
||||||
|
{
|
||||||
|
trade.Status = "PartiallyFilled";
|
||||||
|
}
|
||||||
|
else if (update.Event == TradeEvent.Canceled || update.Event == TradeEvent.Expired || update.Event == TradeEvent.Rejected)
|
||||||
|
{
|
||||||
|
trade.Status = update.Event.ToString();
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else if (update.Event == TradeEvent.Stopped || update.Event == TradeEvent.Calculated)
|
||||||
|
{
|
||||||
|
// Position closed by Stop Loss or Take Profit
|
||||||
|
trade.Status = "Closed";
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
decimal exitPrice = update.Price ?? trade.ActualFillPrice ?? trade.SignalEntryPrice;
|
||||||
|
if (trade.ActualFillPrice.HasValue && update.Price.HasValue)
|
||||||
|
{
|
||||||
|
exitPrice = update.Price.Value;
|
||||||
|
decimal diff = trade.Side == "BUY" ? (exitPrice - trade.ActualFillPrice.Value) : (trade.ActualFillPrice.Value - exitPrice);
|
||||||
|
trade.RealizedPnl = diff * trade.Quantity;
|
||||||
|
if (trade.ActualFillPrice.Value > 0)
|
||||||
|
{
|
||||||
|
trade.RealizedPnlPercent = Math.Round((diff / trade.ActualFillPrice.Value) * 100m, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[AlpacaTradeUpdate] Position CLOSED for {Symbol}: Realized PnL: ${Pnl:F2} ({Pct:F2}%)",
|
||||||
|
trade.Symbol, trade.RealizedPnl, trade.RealizedPnlPercent ?? 0m);
|
||||||
|
|
||||||
|
// Publish Closed Trade to MQTT for WinRate calibration & AI feedback loop
|
||||||
|
bool isWin = trade.RealizedPnl > 0;
|
||||||
|
var feedbackDto = new FinlyticCore.Models.Trades.TradeProposalDto
|
||||||
|
{
|
||||||
|
TradeId = trade.TradeId,
|
||||||
|
Symbol = trade.Symbol,
|
||||||
|
Isin = trade.Isin,
|
||||||
|
CompanyName = trade.CompanyName,
|
||||||
|
EntryPrice = trade.SignalEntryPrice,
|
||||||
|
ActualEntryPrice = trade.ActualFillPrice,
|
||||||
|
CurrentPrice = exitPrice,
|
||||||
|
StopLoss = trade.StopLossPrice,
|
||||||
|
TakeProfit = trade.TakeProfitPrice1,
|
||||||
|
Status = isWin ? "Closed_Profit" : "Closed_Loss",
|
||||||
|
SignalType = trade.Side,
|
||||||
|
WinRate = trade.WinRate,
|
||||||
|
PnlAbsolute = trade.RealizedPnl,
|
||||||
|
PnlPercent = trade.RealizedPnlPercent,
|
||||||
|
CloseReason = isWin ? "TakeProfit_Hit" : "StopLoss_Hit",
|
||||||
|
UserExitTimestamp = trade.ClosedAt,
|
||||||
|
CreatedAt = trade.PlacedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
await _mqttClient.PublishAsync($"finlytic/trades/closed/{trade.TradeId}", feedbackDto);
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[AlpacaTradeUpdate] Dispatched closed trade feedback event to MQTT for {TradeId} (Win: {IsWin})",
|
||||||
|
trade.TradeId, isWin);
|
||||||
|
}
|
||||||
|
|
||||||
|
trade.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_ = _finlyticLogger.LogErrorAsync(SettingKeys.BotChannel, ex,
|
||||||
|
"[AlpacaWebSocketMonitor] Error processing TradeUpdate event.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Alpaca.Markets;
|
||||||
|
using FinlyticBot.Database;
|
||||||
|
using FinlyticBot.Entities;
|
||||||
|
using FinlyticBot.Util;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Services;
|
||||||
|
|
||||||
|
public interface IBotOrderExecutionService
|
||||||
|
{
|
||||||
|
Task ProcessTradeProposalAsync(TradeProposalDto proposal, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BotOrderExecutionService : IBotOrderExecutionService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IAlpacaBrokerService _brokerService;
|
||||||
|
private readonly IBotRiskSizingService _sizingService;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IFinlyticLogger<BotOrderExecutionService> _finlyticLogger;
|
||||||
|
|
||||||
|
public BotOrderExecutionService(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IAlpacaBrokerService brokerService,
|
||||||
|
IBotRiskSizingService sizingService,
|
||||||
|
ISettingsService settingsService,
|
||||||
|
IFinlyticLogger<BotOrderExecutionService> finlyticLogger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_brokerService = brokerService;
|
||||||
|
_sizingService = sizingService;
|
||||||
|
_settingsService = settingsService;
|
||||||
|
_finlyticLogger = finlyticLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ProcessTradeProposalAsync(TradeProposalDto proposal, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(proposal);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(proposal.Symbol))
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
||||||
|
"[BotExecution] Trade proposal has no valid Symbol. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BotDbContext>();
|
||||||
|
|
||||||
|
// 1. Check if trade was already processed
|
||||||
|
bool alreadyExists = await dbContext.ExecutedPaperTrades
|
||||||
|
.AnyAsync(t => t.TradeId == proposal.TradeId, ct);
|
||||||
|
if (alreadyExists)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[BotExecution] Trade proposal {TradeId} ({Symbol}) already processed. Skipping duplicate.",
|
||||||
|
proposal.TradeId, proposal.Symbol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate asset on Alpaca
|
||||||
|
var asset = await _brokerService.GetAssetAsync(proposal.Symbol, ct);
|
||||||
|
if (asset == null || !asset.IsTradable)
|
||||||
|
{
|
||||||
|
string reason = $"Asset '{proposal.Symbol}' is not tradeable on Alpaca.";
|
||||||
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
||||||
|
"[BotExecution] Trade {TradeId} ({Symbol}) rejected: {Reason}", proposal.TradeId, proposal.Symbol, reason);
|
||||||
|
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "Rejected", false, reason, 0, 0, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Query Account Equity from Alpaca
|
||||||
|
var account = await _brokerService.GetAccountInfoAsync(ct);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
string reason = "Failed to query account information from Alpaca API.";
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "Rejected", false, reason, 0, 0, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.IsBlocked)
|
||||||
|
{
|
||||||
|
string reason = "Alpaca trading account is currently blocked.";
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "Rejected", false, reason, 0, account.Equity, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Calculate stats (current open trades and today's loss)
|
||||||
|
int openTradesCount = await dbContext.ExecutedPaperTrades
|
||||||
|
.CountAsync(t => t.Status == "Submitted" || t.Status == "Filled" || t.Status == "PartiallyFilled", ct);
|
||||||
|
|
||||||
|
var todayUtc = DateTime.UtcNow.Date;
|
||||||
|
var todayClosedTrades = await dbContext.ExecutedPaperTrades
|
||||||
|
.Where(t => t.ClosedAt >= todayUtc && t.Status == "Closed")
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
decimal todayRealizedLoss = todayClosedTrades.Where(t => t.RealizedPnl < 0).Sum(t => Math.Abs(t.RealizedPnl));
|
||||||
|
decimal todayLossPercent = account.Equity > 0 ? (todayRealizedLoss / account.Equity) * 100m : 0m;
|
||||||
|
|
||||||
|
// 5. Evaluate through Risk & Sizing Engine
|
||||||
|
var sizing = await _sizingService.EvaluateAndSizeTradeAsync(
|
||||||
|
proposal,
|
||||||
|
account.Equity,
|
||||||
|
openTradesCount,
|
||||||
|
todayLossPercent,
|
||||||
|
ct
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sizing.IsApproved)
|
||||||
|
{
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "Rejected", false, sizing.RejectReason, 0, account.Equity, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Submit Order to Alpaca
|
||||||
|
string orderType = await _settingsService.GetSettingAsync(SettingKeys.ExecutionOrderType, ct);
|
||||||
|
var placedOrder = await _brokerService.PlaceBracketOrderAsync(proposal, sizing.Quantity, orderType, ct);
|
||||||
|
|
||||||
|
if (placedOrder == null)
|
||||||
|
{
|
||||||
|
string reason = "Alpaca API rejected bracket order placement.";
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "OrderFailed", false, reason, sizing.Quantity, account.Equity, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Persist Executed Paper Trade
|
||||||
|
var executedTrade = new ExecutedPaperTradeEntity
|
||||||
|
{
|
||||||
|
TradeId = proposal.TradeId,
|
||||||
|
Symbol = proposal.Symbol,
|
||||||
|
Isin = proposal.Isin,
|
||||||
|
CompanyName = proposal.CompanyName,
|
||||||
|
AlpacaOrderId = placedOrder.OrderId,
|
||||||
|
Side = string.Equals(proposal.SignalType, "SELL", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
||||||
|
Quantity = sizing.Quantity,
|
||||||
|
SignalEntryPrice = proposal.EntryPrice,
|
||||||
|
StopLossPrice = proposal.StopLoss,
|
||||||
|
TakeProfitPrice1 = proposal.TakeProfit,
|
||||||
|
TakeProfitPrice2 = proposal.TakeProfitTargets != null && proposal.TakeProfitTargets.Count > 1 ? proposal.TakeProfitTargets[1] : null,
|
||||||
|
CalculatedCrv = sizing.CalculatedCrv,
|
||||||
|
WinRate = proposal.WinRate,
|
||||||
|
Status = placedOrder.OrderStatus == OrderStatus.Filled ? "Filled" : "Submitted",
|
||||||
|
PlacedAt = DateTime.UtcNow,
|
||||||
|
FilledAt = placedOrder.OrderStatus == OrderStatus.Filled ? DateTime.UtcNow : null,
|
||||||
|
ActualFillPrice = placedOrder.AverageFillPrice ?? (placedOrder.OrderStatus == OrderStatus.Filled ? proposal.EntryPrice : null)
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.ExecutedPaperTrades.Add(executedTrade);
|
||||||
|
await RecordAuditLogAsync(dbContext, proposal, "OrderPlaced", true, $"Bracket Order placed with Alpaca. OrderId: {placedOrder.OrderId}", sizing.Quantity, account.Equity, ct);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[BotExecution] Trade {Symbol} successfully recorded in database with Alpaca OrderId {OrderId}.",
|
||||||
|
proposal.Symbol, placedOrder.OrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RecordAuditLogAsync(
|
||||||
|
BotDbContext dbContext,
|
||||||
|
TradeProposalDto proposal,
|
||||||
|
string action,
|
||||||
|
bool isAccepted,
|
||||||
|
string? reason,
|
||||||
|
decimal calculatedSize,
|
||||||
|
decimal accountEquity,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var audit = new BotAuditLogEntity
|
||||||
|
{
|
||||||
|
TradeId = proposal.TradeId,
|
||||||
|
Symbol = proposal.Symbol,
|
||||||
|
Action = action,
|
||||||
|
IsAccepted = isAccepted,
|
||||||
|
Reason = reason,
|
||||||
|
CalculatedSize = calculatedSize,
|
||||||
|
AccountEquity = accountEquity,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.BotAuditLogs.Add(audit);
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticBot.Util;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Services;
|
||||||
|
|
||||||
|
public class BotRiskSizingService : IBotRiskSizingService
|
||||||
|
{
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IFinlyticLogger<BotRiskSizingService> _finlyticLogger;
|
||||||
|
|
||||||
|
public BotRiskSizingService(
|
||||||
|
ISettingsService settingsService,
|
||||||
|
IFinlyticLogger<BotRiskSizingService> finlyticLogger)
|
||||||
|
{
|
||||||
|
_settingsService = settingsService;
|
||||||
|
_finlyticLogger = finlyticLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SizingResult> EvaluateAndSizeTradeAsync(
|
||||||
|
TradeProposalDto proposal,
|
||||||
|
decimal accountEquity,
|
||||||
|
int currentOpenTradesCount,
|
||||||
|
decimal todayRealizedLossPercent,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(proposal);
|
||||||
|
|
||||||
|
// 1. Check Master Bot Switch
|
||||||
|
bool isEnabled = await _settingsService.GetSettingAsync(SettingKeys.IsEnabled, ct);
|
||||||
|
if (!isEnabled)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, "FinlyticBot is currently disabled in settings.", 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Daily Drawdown Circuit Breaker
|
||||||
|
double dailyLossLimit = await _settingsService.GetSettingAsync(SettingKeys.DailyLossLimitPercent, ct);
|
||||||
|
if (todayRealizedLossPercent >= (decimal)dailyLossLimit)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogWarningAsync(SettingKeys.BotChannel,
|
||||||
|
"[RiskEngine] Circuit breaker triggered! Today's realized loss {Loss:F2}% >= limit {Limit:F2}%. Rejecting trade {TradeId}.",
|
||||||
|
todayRealizedLossPercent, dailyLossLimit, proposal.TradeId);
|
||||||
|
return new SizingResult(false, $"Daily loss limit reached ({todayRealizedLossPercent:F2}% >= {dailyLossLimit:F2}%).", 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Max Open Trades Limit
|
||||||
|
int maxOpenTrades = await _settingsService.GetSettingAsync(SettingKeys.MaxOpenTrades, ct);
|
||||||
|
if (currentOpenTradesCount >= maxOpenTrades)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, $"Max concurrent open positions reached ({currentOpenTradesCount}/{maxOpenTrades}).", 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate CRV (Chance-Risiko-Verhältnis)
|
||||||
|
double minCrv = await _settingsService.GetSettingAsync(SettingKeys.MinCrv, ct);
|
||||||
|
decimal calculatedCrv = proposal.RiskRewardRatio ?? 0;
|
||||||
|
if (calculatedCrv <= 0 && proposal.EntryPrice > 0 && proposal.StopLoss > 0 && proposal.TakeProfit > 0)
|
||||||
|
{
|
||||||
|
decimal slDist = Math.Abs(proposal.EntryPrice - proposal.StopLoss);
|
||||||
|
decimal tpDist = Math.Abs(proposal.TakeProfit - proposal.EntryPrice);
|
||||||
|
if (slDist > 0)
|
||||||
|
{
|
||||||
|
calculatedCrv = Math.Round(tpDist / slDist, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calculatedCrv < (decimal)minCrv)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[RiskEngine] Trade {Symbol} rejected: CRV {Crv:F2} below threshold {MinCrv:F2}",
|
||||||
|
proposal.Symbol, calculatedCrv, minCrv);
|
||||||
|
return new SizingResult(false, $"CRV {calculatedCrv:F2} below minimum threshold {minCrv:F2}.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Validate Win-Rate
|
||||||
|
double minWinRate = await _settingsService.GetSettingAsync(SettingKeys.MinWinRate, ct);
|
||||||
|
if (proposal.WinRate < minWinRate)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[RiskEngine] Trade {Symbol} rejected: Win-Rate {WinRate:F1}% below threshold {MinWinRate:F1}%",
|
||||||
|
proposal.Symbol, proposal.WinRate, minWinRate);
|
||||||
|
return new SizingResult(false, $"Win-Rate {proposal.WinRate:F1}% below minimum threshold {minWinRate:F1}%.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Validate VIX Threshold
|
||||||
|
double maxVix = await _settingsService.GetSettingAsync(SettingKeys.MaxVixThreshold, ct);
|
||||||
|
if (proposal.VixValue > (decimal)maxVix)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, $"VIX {proposal.VixValue:F1} exceeds maximum volatility threshold {maxVix:F1}.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Calculate Position Sizing (Fixed Fractional Sizing)
|
||||||
|
if (accountEquity <= 0)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, "Account equity is zero or negative.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
double riskPercent = await _settingsService.GetSettingAsync(SettingKeys.RiskPerTradePercent, ct);
|
||||||
|
decimal maxRiskAmount = accountEquity * ((decimal)riskPercent / 100m);
|
||||||
|
decimal priceRiskPerUnit = Math.Abs(proposal.EntryPrice - proposal.StopLoss);
|
||||||
|
|
||||||
|
if (priceRiskPerUnit <= 0)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, "Stop loss cannot be identical to entry price.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal calculatedQty = Math.Floor(maxRiskAmount / priceRiskPerUnit);
|
||||||
|
if (calculatedQty <= 0)
|
||||||
|
{
|
||||||
|
// Allow fractional share if total position is at least 10$
|
||||||
|
calculatedQty = Math.Round(maxRiskAmount / priceRiskPerUnit, 2);
|
||||||
|
if (calculatedQty <= 0)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, "Calculated order quantity is 0 (account equity too small for Stop Loss distance).", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal totalPositionValue = calculatedQty * proposal.EntryPrice;
|
||||||
|
|
||||||
|
// 8. Cap against Max Single Position Cap
|
||||||
|
double maxCap = await _settingsService.GetSettingAsync(SettingKeys.MaxSinglePositionCap, ct);
|
||||||
|
if (totalPositionValue > (decimal)maxCap && proposal.EntryPrice > 0)
|
||||||
|
{
|
||||||
|
calculatedQty = Math.Floor((decimal)maxCap / proposal.EntryPrice);
|
||||||
|
totalPositionValue = calculatedQty * proposal.EntryPrice;
|
||||||
|
if (calculatedQty <= 0)
|
||||||
|
{
|
||||||
|
return new SizingResult(false, "Position size exceeds maximum position cap.", 0, 0, 0, calculatedCrv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal actualRiskAmount = calculatedQty * priceRiskPerUnit;
|
||||||
|
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.BotChannel,
|
||||||
|
"[RiskEngine] Sizing APPROVED for {Symbol}: Qty={Qty}, PositionVal=${PosVal:F2}, Risk=${Risk:F2} ({RiskPct:F1}%), CRV={Crv:F2}",
|
||||||
|
proposal.Symbol, calculatedQty, totalPositionValue, actualRiskAmount, riskPercent, calculatedCrv);
|
||||||
|
|
||||||
|
return new SizingResult(true, null, calculatedQty, totalPositionValue, actualRiskAmount, calculatedCrv);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Alpaca.Markets;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Services;
|
||||||
|
|
||||||
|
public record BrokerAccountInfo(
|
||||||
|
decimal Equity,
|
||||||
|
decimal BuyingPower,
|
||||||
|
decimal Cash,
|
||||||
|
string Currency,
|
||||||
|
bool IsBlocked
|
||||||
|
);
|
||||||
|
|
||||||
|
public interface IAlpacaBrokerService
|
||||||
|
{
|
||||||
|
Task<BrokerAccountInfo?> GetAccountInfoAsync(CancellationToken ct = default);
|
||||||
|
Task<IAsset?> GetAssetAsync(string symbol, CancellationToken ct = default);
|
||||||
|
Task<IClock?> GetMarketClockAsync(CancellationToken ct = default);
|
||||||
|
Task<IOrder?> PlaceBracketOrderAsync(TradeProposalDto proposal, decimal quantity, string orderType = "Limit", CancellationToken ct = default);
|
||||||
|
Task<bool> CancelOrderAsync(Guid orderId, CancellationToken ct = default);
|
||||||
|
Task<IReadOnlyList<IOrder>> GetOpenOrdersAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Services;
|
||||||
|
|
||||||
|
public record SizingResult(
|
||||||
|
bool IsApproved,
|
||||||
|
string? RejectReason,
|
||||||
|
decimal Quantity,
|
||||||
|
decimal TotalPositionValue,
|
||||||
|
decimal RiskAmount,
|
||||||
|
decimal CalculatedCrv
|
||||||
|
);
|
||||||
|
|
||||||
|
public interface IBotRiskSizingService
|
||||||
|
{
|
||||||
|
Task<SizingResult> EvaluateAndSizeTradeAsync(
|
||||||
|
TradeProposalDto proposal,
|
||||||
|
decimal accountEquity,
|
||||||
|
int currentOpenTradesCount,
|
||||||
|
decimal todayRealizedLossPercent,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticBot.Services;
|
||||||
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Dtos.Settings;
|
||||||
|
using FinlyticCore.Models;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
|
using FinlyticCore.Util;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Util;
|
||||||
|
|
||||||
|
public class BotMqttClient : ManagedMqttClient, IHostedService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<BotMqttClient> _logger;
|
||||||
|
|
||||||
|
public BotMqttClient(
|
||||||
|
IConfiguration configuration,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<BotMqttClient> logger) : base(logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var config = new MqttConfiguration
|
||||||
|
{
|
||||||
|
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
|
||||||
|
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
|
||||||
|
Username = _configuration["MQTT:Username"] ?? _configuration["MQTT__Username"],
|
||||||
|
Password = _configuration["MQTT:Password"] ?? _configuration["MQTT__Password"],
|
||||||
|
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_bot")}_{Guid.NewGuid():N}"
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting FinlyticBot MQTT Client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
||||||
|
await ConnectAsync(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Stopping FinlyticBot MQTT Client.");
|
||||||
|
await DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("FinlyticBot MQTT Client connected. Subscribing to topics...");
|
||||||
|
|
||||||
|
await SubscribeAsync("services/events/trades/proposal");
|
||||||
|
await SubscribeAsync("finlytic/trades/proposed/#");
|
||||||
|
await SubscribeAsync("services/events/analyzer/trade_proposed");
|
||||||
|
await SubscribeAsync("services/request/bot_Settings_GetAll/#");
|
||||||
|
await SubscribeAsync("services/request/bot_Settings_Update/#");
|
||||||
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
|
|
||||||
|
FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||||
|
{
|
||||||
|
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticBot", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await PublishAsync("finlytic/logs/FinlyticBot", logDto);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully subscribed to FinlyticBot event and RPC channels.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnMessageReceivedAsync(string topic, string payloadStr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var segments = topic.Split('/');
|
||||||
|
bool isForMe = segments.Length >= 5
|
||||||
|
? segments[3].Equals("FinlyticBot", StringComparison.OrdinalIgnoreCase)
|
||||||
|
: topic.Contains("FinlyticBot", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isForMe)
|
||||||
|
{
|
||||||
|
string correlationId = segments[^1];
|
||||||
|
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||||
|
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticBot", "Online", DateTime.UtcNow, "Connected"));
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<BotMqttClient>>();
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel,
|
||||||
|
"[FinlyticBot] Responded to live health_Ping RPC [CorrelationId: {CorrelationId}].", correlationId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.StartsWith("services/request/bot_Settings_GetAll/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/')[^1];
|
||||||
|
await HandleSettingsGetAllAsync(correlationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.StartsWith("services/request/bot_Settings_Update/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/')[^1];
|
||||||
|
await HandleSettingsUpdateAsync(correlationId, payloadStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.Equals("services/events/trades/proposal", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
topic.StartsWith("finlytic/trades/proposed/", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
topic.Equals("services/events/analyzer/trade_proposed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await HandleTradeProposalEventAsync(payloadStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing incoming MQTT message on topic {Topic}", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleTradeProposalEventAsync(string payloadStr)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payloadStr)) return;
|
||||||
|
|
||||||
|
TradeProposalDto? proposal = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
proposal = JsonSerializer.Deserialize<TradeProposalDto>(payloadStr, FinlyticJsonSerializerContext.Default.TradeProposalDto);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
proposal = JsonSerializer.Deserialize<TradeProposalDto>(payloadStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal == null) return;
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var executionService = scope.ServiceProvider.GetRequiredService<IBotOrderExecutionService>();
|
||||||
|
await executionService.ProcessTradeProposalAsync(proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSettingsGetAllAsync(string correlationId)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
|
||||||
|
var responseTopic = $"services/response/bot_Settings_GetAll/{correlationId}";
|
||||||
|
await PublishAsync(responseTopic, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSettingsUpdateAsync(string correlationId, string payload)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
Dictionary<string, object?>? updates = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
updates = new Dictionary<string, object?>();
|
||||||
|
foreach (var item in list) updates[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates != null && updates.Count > 0)
|
||||||
|
{
|
||||||
|
await settingsService.UpdateSettingsAsync(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
var responseTopic = $"services/response/bot_Settings_Update/{correlationId}";
|
||||||
|
await PublishAsync(responseTopic, currentSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using FinlyticCore.Models.Settings;
|
||||||
|
|
||||||
|
namespace FinlyticBot.Util;
|
||||||
|
|
||||||
|
public static class SettingKeys
|
||||||
|
{
|
||||||
|
// --- Logging-Kanäle ---
|
||||||
|
public static readonly SettingKey<bool> BotChannel = new("Logging.Channel.Bot", true);
|
||||||
|
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
|
||||||
|
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
|
||||||
|
|
||||||
|
// --- Master Bot Control ---
|
||||||
|
public static readonly SettingKey<bool> IsEnabled = new("Bot.IsEnabled", false);
|
||||||
|
|
||||||
|
// --- Filter & Zulassungskriterien ---
|
||||||
|
public static readonly SettingKey<double> MinCrv = new("Bot.MinCrv", 1.50);
|
||||||
|
public static readonly SettingKey<double> MinWinRate = new("Bot.MinWinRate", 65.0);
|
||||||
|
public static readonly SettingKey<double> MaxVixThreshold = new("Bot.MaxVixThreshold", 25.0);
|
||||||
|
public static readonly SettingKey<double> MaxEntryDeviationPercent = new("Bot.MaxEntryDeviationPercent", 0.75);
|
||||||
|
|
||||||
|
// --- Risiko-Management & Positionsgrößen ---
|
||||||
|
public static readonly SettingKey<double> RiskPerTradePercent = new("Bot.RiskPerTradePercent", 1.0);
|
||||||
|
public static readonly SettingKey<double> MaxSinglePositionCap = new("Bot.MaxSinglePositionCap", 5000.0);
|
||||||
|
public static readonly SettingKey<int> MaxOpenTrades = new("Bot.MaxOpenTrades", 5);
|
||||||
|
public static readonly SettingKey<double> DailyLossLimitPercent = new("Bot.DailyLossLimitPercent", 3.0);
|
||||||
|
public static readonly SettingKey<int> MaxConsecutiveLosses = new("Bot.MaxConsecutiveLosses", 3);
|
||||||
|
|
||||||
|
// --- Order Execution Strategie ---
|
||||||
|
public static readonly SettingKey<string> TakeProfitMode = new("Bot.TakeProfitMode", "Split50_50"); // "TP1_Only", "TP2_Only", "Split50_50"
|
||||||
|
public static readonly SettingKey<string> ExecutionOrderType = new("Bot.ExecutionOrderType", "Limit"); // "Limit", "Market"
|
||||||
|
|
||||||
|
// --- Alpaca API Konfiguration (Optional live im UI überschreibbar) ---
|
||||||
|
public static readonly SettingKey<string> AlpacaKeyId = new("Alpaca.KeyId", "");
|
||||||
|
public static readonly SettingKey<string> AlpacaSecretKey = new("Alpaca.SecretKey", "");
|
||||||
|
public static readonly SettingKey<bool> AlpacaIsPaper = new("Alpaca.IsPaper", true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=OmniDB;Database=finlytic_bot;Username=admin;Password=YourPasswordHere"
|
||||||
|
},
|
||||||
|
"MQTT": {
|
||||||
|
"Host": "localhost",
|
||||||
|
"Port": 1883,
|
||||||
|
"ClientId": "finlytic_bot"
|
||||||
|
},
|
||||||
|
"Alpaca": {
|
||||||
|
"KeyId": "",
|
||||||
|
"SecretKey": "",
|
||||||
|
"IsPaper": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,22 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback
|
- C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback
|
||||||
|
|
||||||
|
finlyticbot:
|
||||||
|
image: finlyticbot
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: FinlyticBot/Dockerfile
|
||||||
|
networks:
|
||||||
|
- postgres-network
|
||||||
|
environment:
|
||||||
|
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_bot;Username=admin;Password=${DB_PASSWORD}
|
||||||
|
- MQTT__Host=host.docker.internal
|
||||||
|
- MQTT__Port=4545
|
||||||
|
- MQTT__ClientId=finlytic_bot
|
||||||
|
- Alpaca__KeyId=${ALPACA_KEY_ID:-}
|
||||||
|
- Alpaca__SecretKey=${ALPACA_SECRET_KEY:-}
|
||||||
|
- Alpaca__IsPaper=true
|
||||||
|
|
||||||
finlyticbackend:
|
finlyticbackend:
|
||||||
image: finlyticbackend
|
image: finlyticbackend
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user