feat(trades): align TradeProposal and TradeAcceptance DTOs, update lifecycle and migrations
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using FinlyticCore.Entities.Settings;
|
||||||
using FinlyticTrades.Entities;
|
using FinlyticTrades.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ public class TradesDbContext : DbContext
|
|||||||
{
|
{
|
||||||
public TradesDbContext(DbContextOptions<TradesDbContext> options) : base(options) { }
|
public TradesDbContext(DbContextOptions<TradesDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
|
||||||
public DbSet<TradeEntity> Trades => Set<TradeEntity>();
|
public DbSet<TradeEntity> Trades => Set<TradeEntity>();
|
||||||
public DbSet<TradeHourlyUpdateEntity> TradeHourlyUpdates => Set<TradeHourlyUpdateEntity>();
|
public DbSet<TradeHourlyUpdateEntity> TradeHourlyUpdates => Set<TradeHourlyUpdateEntity>();
|
||||||
public DbSet<TradesSettingsEntity> Settings => Set<TradesSettingsEntity>();
|
public DbSet<TradesSettingsEntity> Settings => Set<TradesSettingsEntity>();
|
||||||
@@ -15,6 +17,12 @@ public class TradesDbContext : DbContext
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SettingEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.Key);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<TradeEntity>(entity =>
|
modelBuilder.Entity<TradeEntity>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasIndex(e => e.TradeId).IsUnique();
|
entity.HasIndex(e => e.TradeId).IsUnique();
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ public class TradeEntity
|
|||||||
[MaxLength(30)]
|
[MaxLength(30)]
|
||||||
public string InstrumentType { get; set; } = "Stock";
|
public string InstrumentType { get; set; } = "Stock";
|
||||||
|
|
||||||
|
[MaxLength(20)]
|
||||||
|
public string? DerivativeIsin { get; set; }
|
||||||
|
|
||||||
public double WinRate { get; set; }
|
public double WinRate { get; set; }
|
||||||
public VixMarketRegime VixRegime { get; set; }
|
public VixMarketRegime VixRegime { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticTrades.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 FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(TradesDbContext))]
|
||||||
|
[Migration("20260812190325_DerivativeIsin")]
|
||||||
|
partial class DerivativeIsin
|
||||||
|
{
|
||||||
|
/// <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("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ActualEntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("AnalysisId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("CloseReason")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClosedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DerivativeIsin")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("EntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMax")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMin")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("EventId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExecutionTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExitFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("FundamentalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InstrumentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGlobalProposal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRecurring")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsWin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KnockoutThreshold")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LeverageUsed")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("MaxLeverage")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlAbsolute")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PositionSize")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RiskRewardRatio")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskTolerance")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskWarning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sector")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("SignalType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("StopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("TakeProfitTargets")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TechnicalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Timeframe")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("TtlMinutes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UserExitPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UserExitTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("VixRegime")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<double>("WinRate")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AnalysisId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("EventId");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Sector");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("trades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FloatingPnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Recommendation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedStopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedTakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("TradeId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("trade_hourly_updates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("AtrStopLossMultiplier")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<int>("MaxOpenPositions")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<double>("RiskPerTradePercentage")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade")
|
||||||
|
.WithMany("HourlyUpdates")
|
||||||
|
.HasForeignKey("TradeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trade");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("HourlyUpdates");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DerivativeIsin : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DerivativeIsin",
|
||||||
|
table: "trades",
|
||||||
|
type: "character varying(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DerivativeIsin",
|
||||||
|
table: "trades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticTrades.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 FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(TradesDbContext))]
|
||||||
|
[Migration("20260813202535_CheckPendingTrades")]
|
||||||
|
partial class CheckPendingTrades
|
||||||
|
{
|
||||||
|
/// <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("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ActualEntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("AnalysisId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("CloseReason")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClosedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DerivativeIsin")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("EntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMax")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMin")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("EventId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExecutionTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExitFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("FundamentalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InstrumentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGlobalProposal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRecurring")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsWin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KnockoutThreshold")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LeverageUsed")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("MaxLeverage")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlAbsolute")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PositionSize")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RiskRewardRatio")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskTolerance")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskWarning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sector")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("SignalType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("StopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("TakeProfitTargets")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TechnicalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Timeframe")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("TtlMinutes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UserExitPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UserExitTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("VixRegime")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<double>("WinRate")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AnalysisId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("EventId");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Sector");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("trades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FloatingPnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Recommendation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedStopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedTakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("TradeId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("trade_hourly_updates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("AtrStopLossMultiplier")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<int>("MaxOpenPositions")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<double>("RiskPerTradePercentage")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade")
|
||||||
|
.WithMany("HourlyUpdates")
|
||||||
|
.HasForeignKey("TradeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trade");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("HourlyUpdates");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class CheckPendingTrades : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,10 @@ namespace FinlyticTrades.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DerivativeIsin")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
b.Property<decimal?>("EntryFee")
|
b.Property<decimal?>("EntryFee")
|
||||||
.HasColumnType("decimal(18,4)");
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
// Register DB Context
|
// 1. Standard DbContext (Scoped)
|
||||||
builder.Services.AddDbContext<TradesDbContext>(options =>
|
builder.Services.AddDbContext<TradesDbContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
// Register Domain Services
|
// 2. Domain Services (Scoped)
|
||||||
builder.Services.AddSingleton<ITradeLifecycleService, TradeLifecycleService>();
|
builder.Services.AddScoped<ITradeLifecycleService, TradeLifecycleService>();
|
||||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||||
|
|
||||||
// Register Hosted Services
|
// 3. Hosted Services / Singletons
|
||||||
builder.Services.AddSingleton<TradesMqttClient>();
|
builder.Services.AddSingleton<TradesMqttClient>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
|
||||||
builder.Services.AddHostedService<FeedbackExporterEngine>();
|
builder.Services.AddHostedService<FeedbackExporterEngine>();
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
// Run DB Migrations
|
// DB Migrations ausführen
|
||||||
using (var scope = host.Services.CreateScope())
|
using (var scope = host.Services.CreateScope())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -45,4 +45,4 @@ using (var scope = host.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
||||||
@@ -14,45 +14,15 @@ namespace FinlyticTrades.Services;
|
|||||||
|
|
||||||
public interface ITradeLifecycleService
|
public interface ITradeLifecycleService
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Processes a proposed trade.
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default);
|
Task<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes a manual analysis RPC response from FinlyticAnalyzer and ingests it if a trade was proposed.
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default);
|
Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Accepts a trade proposal and maps execution parameters.
|
|
||||||
/// </summary>
|
|
||||||
Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default);
|
Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an hourly update for a trade.
|
|
||||||
/// </summary>
|
|
||||||
Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default);
|
Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of active trades filtered by optional UserId.
|
|
||||||
/// </summary>
|
|
||||||
Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default);
|
Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of trades filtered by ISIN, status, and optional UserId.
|
|
||||||
/// </summary>
|
|
||||||
Task<List<TradeEntity>> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default);
|
Task<List<TradeEntity>> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes a trade manually.
|
|
||||||
/// </summary>
|
|
||||||
Task<TradeEntity?> CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default);
|
Task<TradeEntity?> CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rejects a trade proposal.
|
|
||||||
/// </summary>
|
|
||||||
Task<TradeEntity?> RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default);
|
Task<TradeEntity?> RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default);
|
||||||
|
void CalculatePnL(TradeEntity trade, decimal? overridePrice = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TradeLifecycleService : ITradeLifecycleService
|
public class TradeLifecycleService : ITradeLifecycleService
|
||||||
@@ -66,9 +36,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes a manual analysis RPC response from FinlyticAnalyzer and ingests it if a trade was proposed.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default)
|
public async Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (response == null || !response.IsTradeProposed)
|
if (response == null || !response.IsTradeProposed)
|
||||||
@@ -119,9 +86,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes a proposed trade.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default)
|
public async Task<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
||||||
@@ -177,9 +141,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Accepts a trade proposal and updates execution parameters.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default)
|
public async Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
string targetUserId = !string.IsNullOrWhiteSpace(request.UserId) ? request.UserId : "default_user";
|
string targetUserId = !string.IsNullOrWhiteSpace(request.UserId) ? request.UserId : "default_user";
|
||||||
@@ -212,6 +173,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
if (request.TakeProfit > 0) existingTrade.TakeProfit = request.TakeProfit.Value;
|
if (request.TakeProfit > 0) existingTrade.TakeProfit = request.TakeProfit.Value;
|
||||||
if (request.KnockoutThreshold > 0) existingTrade.KnockoutThreshold = request.KnockoutThreshold;
|
if (request.KnockoutThreshold > 0) existingTrade.KnockoutThreshold = request.KnockoutThreshold;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Timeframe)) existingTrade.Timeframe = request.Timeframe;
|
if (!string.IsNullOrWhiteSpace(request.Timeframe)) existingTrade.Timeframe = request.Timeframe;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.DerivativeIsin)) existingTrade.DerivativeIsin = request.DerivativeIsin;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Reasoning)) existingTrade.Reasoning = request.Reasoning;
|
if (!string.IsNullOrWhiteSpace(request.Reasoning)) existingTrade.Reasoning = request.Reasoning;
|
||||||
|
|
||||||
existingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
existingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
||||||
@@ -241,10 +203,10 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
TradeId = targetTradeId,
|
TradeId = targetTradeId,
|
||||||
AnalysisId = proposal?.AnalysisId ?? (string.IsNullOrWhiteSpace(request.AnalysisId) ? Guid.NewGuid().ToString("N") : request.AnalysisId),
|
AnalysisId = proposal?.AnalysisId ?? (string.IsNullOrWhiteSpace(request.AnalysisId) ? Guid.NewGuid().ToString("N") : request.AnalysisId),
|
||||||
EventId = proposal?.EventId ?? request.AnalysisId,
|
EventId = proposal?.EventId ?? request.AnalysisId,
|
||||||
Sector = proposal?.Sector ?? "General",
|
Sector = proposal?.Sector ?? request.Sector ?? "General",
|
||||||
Symbol = proposal?.Symbol ?? request.Symbol ?? request.Isin,
|
Symbol = proposal?.Symbol ?? request.Symbol ?? request.Isin,
|
||||||
Isin = proposal?.Isin ?? request.Isin,
|
Isin = proposal?.Isin ?? request.Isin,
|
||||||
CompanyName = proposal?.CompanyName ?? request.Symbol ?? request.Isin,
|
CompanyName = proposal?.CompanyName ?? request.CompanyName ?? request.Symbol ?? request.Isin,
|
||||||
Status = TradeStatus.Active,
|
Status = TradeStatus.Active,
|
||||||
IsGlobalProposal = false,
|
IsGlobalProposal = false,
|
||||||
UserId = targetUserId,
|
UserId = targetUserId,
|
||||||
@@ -256,6 +218,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
RiskTolerance = proposal?.RiskTolerance ?? "Moderate",
|
RiskTolerance = proposal?.RiskTolerance ?? "Moderate",
|
||||||
Timeframe = proposal?.Timeframe ?? request.Timeframe ?? "1D",
|
Timeframe = proposal?.Timeframe ?? request.Timeframe ?? "1D",
|
||||||
InstrumentType = proposal?.InstrumentType ?? request.InstrumentType ?? "Stock",
|
InstrumentType = proposal?.InstrumentType ?? request.InstrumentType ?? "Stock",
|
||||||
|
DerivativeIsin = request.DerivativeIsin ?? proposal?.DerivativeIsin,
|
||||||
WinRate = proposal?.WinRate ?? 50,
|
WinRate = proposal?.WinRate ?? 50,
|
||||||
VixRegime = proposal?.VixRegime ?? FinlyticCore.Models.Analyzer.VixMarketRegime.Normal,
|
VixRegime = proposal?.VixRegime ?? FinlyticCore.Models.Analyzer.VixMarketRegime.Normal,
|
||||||
VixValue = proposal?.VixValue ?? 15,
|
VixValue = proposal?.VixValue ?? 15,
|
||||||
@@ -294,9 +257,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
return newTrade;
|
return newTrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an hourly update for a trade.
|
|
||||||
/// </summary>
|
|
||||||
public async Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default)
|
public async Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var trade = await _dbContext.Trades
|
var trade = await _dbContext.Trades
|
||||||
@@ -352,9 +312,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
"TradesChannel", update.TradeId, update.Recommendation, update.CurrentPrice);
|
"TradesChannel", update.TradeId, update.Recommendation, update.CurrentPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of active trades filtered by optional UserId.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
public async Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
||||||
@@ -370,9 +327,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of trades filtered by ISIN, status, and optional UserId.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<List<TradeEntity>> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default)
|
public async Task<List<TradeEntity>> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
||||||
@@ -395,9 +349,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
return await query.OrderByDescending(t => t.CreatedAt).ToListAsync(cancellationToken);
|
return await query.OrderByDescending(t => t.CreatedAt).ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes a trade manually.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<TradeEntity?> CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default)
|
public async Task<TradeEntity?> CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var trade = await _dbContext.Trades
|
var trade = await _dbContext.Trades
|
||||||
@@ -420,9 +371,6 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
return trade;
|
return trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rejects a trade proposal.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<TradeEntity?> RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default)
|
public async Task<TradeEntity?> RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var trade = await _dbContext.Trades
|
var trade = await _dbContext.Trades
|
||||||
@@ -458,6 +406,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
entity.RiskTolerance = dto.RiskTolerance;
|
entity.RiskTolerance = dto.RiskTolerance;
|
||||||
entity.Timeframe = dto.Timeframe;
|
entity.Timeframe = dto.Timeframe;
|
||||||
entity.InstrumentType = dto.InstrumentType;
|
entity.InstrumentType = dto.InstrumentType;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dto.DerivativeIsin)) entity.DerivativeIsin = dto.DerivativeIsin;
|
||||||
entity.WinRate = dto.WinRate;
|
entity.WinRate = dto.WinRate;
|
||||||
entity.VixRegime = dto.VixRegime;
|
entity.VixRegime = dto.VixRegime;
|
||||||
entity.VixValue = dto.VixValue;
|
entity.VixValue = dto.VixValue;
|
||||||
@@ -484,11 +433,12 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
entity.IsRecurring = dto.IsRecurring;
|
entity.IsRecurring = dto.IsRecurring;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CalculatePnL(TradeEntity trade)
|
public void CalculatePnL(TradeEntity trade, decimal? overridePrice = null)
|
||||||
{
|
{
|
||||||
if (!trade.UserExitPrice.HasValue) return;
|
decimal? evalPrice = overridePrice ?? trade.UserExitPrice ?? trade.HourlyUpdates?.LastOrDefault()?.CurrentPrice;
|
||||||
|
if (!evalPrice.HasValue || evalPrice.Value <= 0m) return;
|
||||||
|
|
||||||
decimal exitPrice = trade.UserExitPrice.Value;
|
decimal exitPrice = evalPrice.Value;
|
||||||
decimal entryPrice = trade.ActualEntryPrice.HasValue && trade.ActualEntryPrice.Value > 0m
|
decimal entryPrice = trade.ActualEntryPrice.HasValue && trade.ActualEntryPrice.Value > 0m
|
||||||
? trade.ActualEntryPrice.Value
|
? trade.ActualEntryPrice.Value
|
||||||
: trade.EntryPrice;
|
: trade.EntryPrice;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticCore.Dtos;
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||||
using FinlyticCore.Models;
|
using FinlyticCore.Models;
|
||||||
using FinlyticCore.Models.Trades;
|
using FinlyticCore.Models.Trades;
|
||||||
using FinlyticCore.Util;
|
using FinlyticCore.Util;
|
||||||
@@ -21,18 +22,15 @@ namespace FinlyticTrades.Util;
|
|||||||
public class TradesMqttClient : ManagedMqttClient, IHostedService
|
public class TradesMqttClient : ManagedMqttClient, IHostedService
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ITradeLifecycleService _tradeLifecycleService;
|
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly ILogger<TradesMqttClient> _logger;
|
private readonly ILogger<TradesMqttClient> _logger;
|
||||||
|
|
||||||
public TradesMqttClient(
|
public TradesMqttClient(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ITradeLifecycleService tradeLifecycleService,
|
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<TradesMqttClient> logger) : base(logger)
|
ILogger<TradesMqttClient> logger) : base(logger)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_tradeLifecycleService = tradeLifecycleService;
|
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -61,7 +59,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
protected override async Task OnConnectedAsync()
|
protected override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] Trades MQTT Client connected. Subscribing to topics...", "TradesChannel");
|
_logger.LogInformation("[{Channel}] Trades MQTT Client connected. Subscribing to topics...", "TradesChannel");
|
||||||
|
|
||||||
await SubscribeAsync("finlytic/trades/proposed/#");
|
await SubscribeAsync("finlytic/trades/proposed/#");
|
||||||
await SubscribeAsync("finlytic/trades/updates/#");
|
await SubscribeAsync("finlytic/trades/updates/#");
|
||||||
await SubscribeAsync("finlytic/trades/accept/#");
|
await SubscribeAsync("finlytic/trades/accept/#");
|
||||||
@@ -71,6 +69,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
await SubscribeAsync("services/request/trades_Accept/#");
|
await SubscribeAsync("services/request/trades_Accept/#");
|
||||||
await SubscribeAsync("services/config/updated/#");
|
await SubscribeAsync("services/config/updated/#");
|
||||||
await SubscribeAsync("services/request/health_Ping/#");
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
|
await SubscribeAsync("services/response/tr_GetLivePrice/#");
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "TradesChannel");
|
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "TradesChannel");
|
||||||
}
|
}
|
||||||
@@ -114,12 +113,16 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Für Scoped-Services erzeugen wir pro eingehender Nachricht einen eigenen Scope
|
||||||
|
using var msgScope = _scopeFactory.CreateScope();
|
||||||
|
var tradeLifecycleService = msgScope.ServiceProvider.GetRequiredService<ITradeLifecycleService>();
|
||||||
|
|
||||||
if (topic.StartsWith("finlytic/trades/proposed/"))
|
if (topic.StartsWith("finlytic/trades/proposed/"))
|
||||||
{
|
{
|
||||||
var proposal = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeProposalDto);
|
var proposal = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeProposalDto);
|
||||||
if (proposal != null && (!string.IsNullOrWhiteSpace(proposal.Symbol) || !string.IsNullOrWhiteSpace(proposal.Isin)))
|
if (proposal != null && (!string.IsNullOrWhiteSpace(proposal.Symbol) || !string.IsNullOrWhiteSpace(proposal.Isin)))
|
||||||
{
|
{
|
||||||
await _tradeLifecycleService.ProcessProposedTradeAsync(proposal, CancellationToken.None);
|
await tradeLifecycleService.ProcessProposedTradeAsync(proposal, CancellationToken.None);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -131,7 +134,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
||||||
if (acceptDto != null)
|
if (acceptDto != null)
|
||||||
{
|
{
|
||||||
var newTrade = await _tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
var newTrade = await tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
||||||
if (newTrade != null)
|
if (newTrade != null)
|
||||||
{
|
{
|
||||||
var dto = MapToDto(newTrade);
|
var dto = MapToDto(newTrade);
|
||||||
@@ -145,7 +148,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
||||||
if (acceptDto != null)
|
if (acceptDto != null)
|
||||||
{
|
{
|
||||||
var acceptedTrade = await _tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
var acceptedTrade = await tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
||||||
if (acceptedTrade != null)
|
if (acceptedTrade != null)
|
||||||
{
|
{
|
||||||
var acceptedDto = MapToDto(acceptedTrade);
|
var acceptedDto = MapToDto(acceptedTrade);
|
||||||
@@ -159,21 +162,48 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
var update = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeHourlyUpdateDto);
|
var update = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeHourlyUpdateDto);
|
||||||
if (update != null)
|
if (update != null)
|
||||||
{
|
{
|
||||||
await _tradeLifecycleService.AddHourlyUpdateAsync(update, CancellationToken.None);
|
await tradeLifecycleService.AddHourlyUpdateAsync(update, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (topic.StartsWith("services/request/trades_Get/"))
|
else if (topic.StartsWith("services/request/trades_Get/"))
|
||||||
{
|
{
|
||||||
var correlationId = topic.Split('/').Last();
|
var correlationId = topic.Split('/').Last();
|
||||||
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.GetTradesRequest);
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.GetTradesRequest);
|
||||||
|
|
||||||
string? isin = request?.Isin;
|
string? isin = request?.Isin;
|
||||||
string? status = request?.Status;
|
string? status = request?.Status;
|
||||||
string? userId = request?.UserId;
|
string? userId = request?.UserId;
|
||||||
|
|
||||||
var trades = await _tradeLifecycleService.GetTradesAsync(isin, status, userId);
|
var trades = await tradeLifecycleService.GetTradesAsync(isin, status, userId);
|
||||||
|
|
||||||
|
var activeTrades = trades.Where(t => t.Status == TradeStatus.Active && !string.IsNullOrWhiteSpace(t.Isin)).ToList();
|
||||||
|
if (activeTrades.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var priceTasks = activeTrades.Select(t => FetchLivePriceAsync(t.Isin)).ToList();
|
||||||
|
var livePricesTask = Task.WhenAll(priceTasks);
|
||||||
|
if (await Task.WhenAny(livePricesTask, Task.Delay(1500)) == livePricesTask)
|
||||||
|
{
|
||||||
|
var livePrices = await livePricesTask;
|
||||||
|
for (int i = 0; i < activeTrades.Count; i++)
|
||||||
|
{
|
||||||
|
var lp = livePrices[i];
|
||||||
|
if (lp != null && lp.CurrentPrice > 0m)
|
||||||
|
{
|
||||||
|
var trade = activeTrades[i];
|
||||||
|
tradeLifecycleService.CalculatePnL(trade, lp.CurrentPrice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "[{Channel}] Live price fetch skipped or timed out during trades_Get", "TradesChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var dtos = trades.Select(MapToDto).ToList();
|
var dtos = trades.Select(MapToDto).ToList();
|
||||||
|
|
||||||
await PublishAsync($"services/response/trades_Get/{correlationId}", dtos);
|
await PublishAsync($"services/response/trades_Get/{correlationId}", dtos);
|
||||||
}
|
}
|
||||||
else if (topic.StartsWith("services/request/trades_Close/"))
|
else if (topic.StartsWith("services/request/trades_Close/"))
|
||||||
@@ -181,18 +211,17 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
var parts = topic.Split('/');
|
var parts = topic.Split('/');
|
||||||
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
||||||
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
||||||
|
|
||||||
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
||||||
|
|
||||||
if (request != null && !string.IsNullOrEmpty(tradeId))
|
if (request != null && !string.IsNullOrEmpty(tradeId))
|
||||||
{
|
{
|
||||||
var closedTrade = await _tradeLifecycleService.CloseTradeAsync(tradeId, request);
|
var closedTrade = await tradeLifecycleService.CloseTradeAsync(tradeId, request);
|
||||||
if (closedTrade != null)
|
if (closedTrade != null)
|
||||||
{
|
{
|
||||||
var closedDto = MapToDto(closedTrade);
|
var closedDto = MapToDto(closedTrade);
|
||||||
await PublishAsync($"services/response/trades_Close/{correlationId}", closedDto);
|
await PublishAsync($"services/response/trades_Close/{correlationId}", closedDto);
|
||||||
|
|
||||||
// Send event stream update specifically for closed trades (used by Feedback Engine & Analytics)
|
|
||||||
string sectorSafe = string.IsNullOrWhiteSpace(closedTrade.Sector) ? "general" : closedTrade.Sector.ToLowerInvariant();
|
string sectorSafe = string.IsNullOrWhiteSpace(closedTrade.Sector) ? "general" : closedTrade.Sector.ToLowerInvariant();
|
||||||
await PublishAsync($"finlytic/trades/closed/{sectorSafe}/{closedTrade.Symbol.ToLowerInvariant()}", closedDto);
|
await PublishAsync($"finlytic/trades/closed/{sectorSafe}/{closedTrade.Symbol.ToLowerInvariant()}", closedDto);
|
||||||
await PublishTradeUpdateAsync(closedDto);
|
await PublishTradeUpdateAsync(closedDto);
|
||||||
@@ -204,12 +233,12 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
var parts = topic.Split('/');
|
var parts = topic.Split('/');
|
||||||
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
||||||
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
||||||
|
|
||||||
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
||||||
|
|
||||||
if (request != null && !string.IsNullOrEmpty(tradeId))
|
if (request != null && !string.IsNullOrEmpty(tradeId))
|
||||||
{
|
{
|
||||||
var rejectedTrade = await _tradeLifecycleService.RejectTradeAsync(tradeId, request);
|
var rejectedTrade = await tradeLifecycleService.RejectTradeAsync(tradeId, request);
|
||||||
if (rejectedTrade != null)
|
if (rejectedTrade != null)
|
||||||
{
|
{
|
||||||
var rejectedDto = MapToDto(rejectedTrade);
|
var rejectedDto = MapToDto(rejectedTrade);
|
||||||
@@ -227,9 +256,26 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
|
|
||||||
public async Task PublishTradeUpdateAsync(TradeProposalDto trade)
|
public async Task PublishTradeUpdateAsync(TradeProposalDto trade)
|
||||||
{
|
{
|
||||||
|
await PublishAsync($"finlytic/trades/user/{trade.UserId ?? "all"}", trade);
|
||||||
await PublishAsync("finlytic/trades/update", trade);
|
await PublishAsync("finlytic/trades/update", trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LivePriceDto?> FetchLivePriceAsync(string isin)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin)) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await SendRpcRequestAsync<LivePriceDto, IsinRequest>(
|
||||||
|
"tr_GetLivePrice",
|
||||||
|
new IsinRequest(isin),
|
||||||
|
TimeSpan.FromMilliseconds(1200));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static TradeProposalDto MapToDto(TradeEntity t)
|
private static TradeProposalDto MapToDto(TradeEntity t)
|
||||||
{
|
{
|
||||||
List<decimal>? parseTakeProfitTargets()
|
List<decimal>? parseTakeProfitTargets()
|
||||||
@@ -279,7 +325,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
FundamentalRationale = t.FundamentalRationale,
|
FundamentalRationale = t.FundamentalRationale,
|
||||||
RiskWarning = t.RiskWarning,
|
RiskWarning = t.RiskWarning,
|
||||||
CreatedAt = t.CreatedAt,
|
CreatedAt = t.CreatedAt,
|
||||||
|
|
||||||
UserId = t.UserId,
|
UserId = t.UserId,
|
||||||
IsGlobalProposal = t.IsGlobalProposal,
|
IsGlobalProposal = t.IsGlobalProposal,
|
||||||
ActualEntryPrice = t.ActualEntryPrice,
|
ActualEntryPrice = t.ActualEntryPrice,
|
||||||
@@ -290,7 +336,10 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
ExecutionTimestamp = t.ExecutionTimestamp,
|
ExecutionTimestamp = t.ExecutionTimestamp,
|
||||||
Quantity = t.Quantity,
|
Quantity = t.Quantity,
|
||||||
KnockoutThreshold = t.KnockoutThreshold,
|
KnockoutThreshold = t.KnockoutThreshold,
|
||||||
IsRecurring = t.IsRecurring
|
IsRecurring = t.IsRecurring,
|
||||||
|
PnlAbsolute = t.PnlAbsolute,
|
||||||
|
PnlPercent = t.PnlPercent,
|
||||||
|
CurrentPrice = t.UserExitPrice ?? t.HourlyUpdates?.LastOrDefault()?.CurrentPrice
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user