feat(Trades): refactor trades MQTT client and DTOs
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
using FinlyticTrades.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Database;
|
||||||
|
|
||||||
|
public class TradesDbContext : DbContext
|
||||||
|
{
|
||||||
|
public TradesDbContext(DbContextOptions<TradesDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<TradeEntity> Trades => Set<TradeEntity>();
|
||||||
|
public DbSet<TradeHourlyUpdateEntity> TradeHourlyUpdates => Set<TradeHourlyUpdateEntity>();
|
||||||
|
public DbSet<TradesSettingsEntity> Settings => Set<TradesSettingsEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<TradeEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(e => e.TradeId).IsUnique();
|
||||||
|
entity.HasIndex(e => e.AnalysisId);
|
||||||
|
entity.HasIndex(e => e.EventId);
|
||||||
|
entity.HasIndex(e => e.Status);
|
||||||
|
entity.HasIndex(e => e.Sector);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
entity.HasIndex(e => e.CreatedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TradeHourlyUpdateEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(e => e.TradeId);
|
||||||
|
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 ["FinlyticTrades/FinlyticTrades.csproj", "FinlyticTrades/"]
|
||||||
|
RUN dotnet restore "FinlyticTrades/FinlyticTrades.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/FinlyticTrades"
|
||||||
|
RUN dotnet build "FinlyticTrades.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "FinlyticTrades.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", "FinlyticTrades.dll"]
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using FinlyticCore.Models.Analyzer;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Entities;
|
||||||
|
|
||||||
|
[Table("trades")]
|
||||||
|
public class TradeEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string TradeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string AnalysisId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string EventId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Sector { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(150)]
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public TradeStatus Status { get; set; } = TradeStatus.Proposed;
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
public bool IsGlobalProposal { get; set; } = true;
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal EntryPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal StopLoss { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal TakeProfit { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(10)]
|
||||||
|
public string SignalType { get; set; } = "BUY";
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string RiskTolerance { get; set; } = "Moderate";
|
||||||
|
|
||||||
|
[MaxLength(20)]
|
||||||
|
public string Timeframe { get; set; } = "1D";
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string InstrumentType { get; set; } = "Stock";
|
||||||
|
|
||||||
|
public double WinRate { get; set; }
|
||||||
|
public VixMarketRegime VixRegime { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal VixValue { get; set; }
|
||||||
|
|
||||||
|
public int TtlMinutes { get; set; } = 60;
|
||||||
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// --- New Fields for Detailed Execution & Rationale ---
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? EntryZoneMin { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? EntryZoneMax { get; set; }
|
||||||
|
|
||||||
|
public string? TakeProfitTargets { get; set; } // Stored as comma separated values
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? RiskRewardRatio { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? MaxLeverage { get; set; }
|
||||||
|
|
||||||
|
public string TechnicalRationale { get; set; } = string.Empty;
|
||||||
|
public string FundamentalRationale { get; set; } = string.Empty;
|
||||||
|
public string RiskWarning { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// --- User Exit Data ---
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? UserExitPrice { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UserExitTimestamp { get; set; }
|
||||||
|
|
||||||
|
// --- Real Trade Execution Data ---
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? ActualEntryPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? PositionSize { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? LeverageUsed { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? EntryFee { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? ExitFee { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ExecutionTimestamp { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? Quantity { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? KnockoutThreshold { get; set; }
|
||||||
|
|
||||||
|
public bool IsRecurring { get; set; } = false;
|
||||||
|
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string? CloseReason { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? PnlAbsolute { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? PnlPercent { get; set; }
|
||||||
|
|
||||||
|
public bool? IsWin { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? ClosedAt { get; set; }
|
||||||
|
|
||||||
|
public List<TradeHourlyUpdateEntity> HourlyUpdates { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Entities;
|
||||||
|
|
||||||
|
[Table("trade_hourly_updates")]
|
||||||
|
[Index(nameof(TradeId), nameof(Timestamp))]
|
||||||
|
public class TradeHourlyUpdateEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Guid TradeId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(TradeId))]
|
||||||
|
public TradeEntity? Trade { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Recommendation { get; set; } = "Hold"; // "Hold", "AdjustSL", "AdjustTP", "Close"
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal CurrentPrice { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? SuggestedStopLoss { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? SuggestedTakeProfit { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal VixValue { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "decimal(18,4)")]
|
||||||
|
public decimal? FloatingPnlPercent { get; set; }
|
||||||
|
|
||||||
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Entities;
|
||||||
|
|
||||||
|
public class TradesSettingsEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public double AtrStopLossMultiplier { get; set; } = 1.5;
|
||||||
|
public double RiskPerTradePercentage { get; set; } = 1.0;
|
||||||
|
public int MaxOpenPositions { get; set; } = 5;
|
||||||
|
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="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" />
|
||||||
|
<PackageReference Include="Parquet.Net" Version="5.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
// <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("20260801073417_Init")]
|
||||||
|
partial class Init
|
||||||
|
{
|
||||||
|
/// <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<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<decimal>("EntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("EventId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("InstrumentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsWin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlAbsolute")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RiskTolerance")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
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>("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<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<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.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,153 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Init : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
AtrStopLossMultiplier = table.Column<double>(type: "double precision", nullable: false),
|
||||||
|
RiskPerTradePercentage = table.Column<double>(type: "double precision", nullable: false),
|
||||||
|
MaxOpenPositions = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Settings", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "trades",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TradeId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
AnalysisId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
EventId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Sector = table.Column<string>(type: "character varying(50)", maxLength: 50, 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(150)", maxLength: 150, nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
EntryPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
StopLoss = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
TakeProfit = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
SignalType = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||||
|
RiskTolerance = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Timeframe = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
InstrumentType = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
WinRate = table.Column<double>(type: "double precision", nullable: false),
|
||||||
|
VixRegime = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
VixValue = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
TtlMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Reasoning = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UserExitPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
UserExitTimestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
CloseReason = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||||
|
PnlAbsolute = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
PnlPercent = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
IsWin = table.Column<bool>(type: "boolean", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
ClosedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_trades", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "trade_hourly_updates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TradeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Recommendation = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
CurrentPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
SuggestedStopLoss = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
SuggestedTakeProfit = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
|
||||||
|
VixValue = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
|
||||||
|
Reasoning = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_trade_hourly_updates", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_trade_hourly_updates_trades_TradeId",
|
||||||
|
column: x => x.TradeId,
|
||||||
|
principalTable: "trades",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trade_hourly_updates_Timestamp",
|
||||||
|
table: "trade_hourly_updates",
|
||||||
|
column: "Timestamp");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trade_hourly_updates_TradeId",
|
||||||
|
table: "trade_hourly_updates",
|
||||||
|
column: "TradeId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_AnalysisId",
|
||||||
|
table: "trades",
|
||||||
|
column: "AnalysisId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_CreatedAt",
|
||||||
|
table: "trades",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_EventId",
|
||||||
|
table: "trades",
|
||||||
|
column: "EventId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_Isin",
|
||||||
|
table: "trades",
|
||||||
|
column: "Isin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_Sector",
|
||||||
|
table: "trades",
|
||||||
|
column: "Sector");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_Status",
|
||||||
|
table: "trades",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trades_TradeId",
|
||||||
|
table: "trades",
|
||||||
|
column: "TradeId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "trade_hourly_updates");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "trades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
// <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("20260802205654_ExpandTradeEntity")]
|
||||||
|
partial class ExpandTradeEntity
|
||||||
|
{
|
||||||
|
/// <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<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<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<string>("FundamentalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InstrumentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsWin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
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<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<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<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.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,101 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ExpandTradeEntity : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "EntryZoneMax",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "EntryZoneMin",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FundamentalRationale",
|
||||||
|
table: "trades",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "MaxLeverage",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "RiskRewardRatio",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "RiskWarning",
|
||||||
|
table: "trades",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "TakeProfitTargets",
|
||||||
|
table: "trades",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "TechnicalRationale",
|
||||||
|
table: "trades",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EntryZoneMax",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EntryZoneMin",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FundamentalRationale",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MaxLeverage",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RiskRewardRatio",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RiskWarning",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TakeProfitTargets",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TechnicalRationale",
|
||||||
|
table: "trades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+306
@@ -0,0 +1,306 @@
|
|||||||
|
// <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("20260803170643_AddMultiUserTradeExecution")]
|
||||||
|
partial class AddMultiUserTradeExecution
|
||||||
|
{
|
||||||
|
/// <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<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<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.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,132 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMultiUserTradeExecution : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "ActualEntryPrice",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "EntryFee",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ExecutionTimestamp",
|
||||||
|
table: "trades",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "ExitFee",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsGlobalProposal",
|
||||||
|
table: "trades",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsRecurring",
|
||||||
|
table: "trades",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "KnockoutThreshold",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "LeverageUsed",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "PositionSize",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Quantity",
|
||||||
|
table: "trades",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "UserId",
|
||||||
|
table: "trades",
|
||||||
|
type: "character varying(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ActualEntryPrice",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EntryFee",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExecutionTimestamp",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExitFee",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsGlobalProposal",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsRecurring",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "KnockoutThreshold",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LeverageUsed",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PositionSize",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Quantity",
|
||||||
|
table: "trades");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserId",
|
||||||
|
table: "trades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+311
@@ -0,0 +1,311 @@
|
|||||||
|
// <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("20260807210605_AddIndexToTradeHourlyUpdate")]
|
||||||
|
partial class AddIndexToTradeHourlyUpdate
|
||||||
|
{
|
||||||
|
/// <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<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,37 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIndexToTradeHourlyUpdate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "FloatingPnlPercent",
|
||||||
|
table: "trade_hourly_updates",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_trade_hourly_updates_TradeId_Timestamp",
|
||||||
|
table: "trade_hourly_updates",
|
||||||
|
columns: new[] { "TradeId", "Timestamp" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_trade_hourly_updates_TradeId_Timestamp",
|
||||||
|
table: "trade_hourly_updates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FloatingPnlPercent",
|
||||||
|
table: "trade_hourly_updates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticTrades.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(TradesDbContext))]
|
||||||
|
partial class TradesDbContextModelSnapshot : 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("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<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,48 @@
|
|||||||
|
using System;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticTrades.Database;
|
||||||
|
using FinlyticTrades.Services;
|
||||||
|
using FinlyticTrades.Util;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
// Register DB Context
|
||||||
|
builder.Services.AddDbContext<TradesDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
// Register Domain Services
|
||||||
|
builder.Services.AddSingleton<ITradeLifecycleService, TradeLifecycleService>();
|
||||||
|
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||||
|
|
||||||
|
// Register Hosted Services
|
||||||
|
builder.Services.AddSingleton<TradesMqttClient>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
|
||||||
|
builder.Services.AddHostedService<FeedbackExporterEngine>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
// Run DB Migrations
|
||||||
|
using (var scope = host.Services.CreateScope())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
|
||||||
|
await context.Database.MigrateAsync();
|
||||||
|
Console.WriteLine("Database migrations successfully executed for FinlyticTrades.");
|
||||||
|
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||||
|
await settingsService.GetSettingsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogError(ex, "An error occurred during database migration for FinlyticTrades on startup.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Finlytic Trades Service
|
||||||
|
|
||||||
|
Finlytic Trades is a C# microservice managing the full lifecycle of automated trade signals and positions. It handles proposed trade validation, position tracking, TTL expiration, hourly performance updates, and trade closure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Modules & Architecture
|
||||||
|
|
||||||
|
1. **Trade Lifecycle Engine (`ITradeLifecycleService`)**:
|
||||||
|
- Ingests trade proposals (`TradeProposalDto`), validates parameters (Entry, Stop Loss, Take Profit, Win Rate, Risk Tolerance), and tracks positions through `Active`, `Closed`, `Expired`, or `Cancelled` states.
|
||||||
|
|
||||||
|
2. **TTL Worker Service (`TtlWorkerService`)**:
|
||||||
|
- Periodically checks active trades against Time-To-Live (`TtlMinutes`) constraints and automatically expires stale trades.
|
||||||
|
|
||||||
|
3. **Feedback Exporter Engine (`FeedbackExporterEngine`)**:
|
||||||
|
- Exports trade outcome data (`TradeFeedbackRecord`) for AI model retraining and win-rate calibration.
|
||||||
|
|
||||||
|
4. **MQTT RPC & Event Communication**:
|
||||||
|
- Subscribes to `finlytic/trades/proposed/#` and `finlytic/trades/updates/#`.
|
||||||
|
- Handles RPC requests on `finlytic/trades/get_active/request` and `finlytic/trades/close/request/#`.
|
||||||
|
- Publishes position updates to `finlytic/trades/update` and `finlytic/trades/get_active/response`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Status
|
||||||
|
|
||||||
|
### Implemented Features
|
||||||
|
- [x] Full Trade Lifecycle Management (`TradesDbContext` with PostgreSQL indexes).
|
||||||
|
- [x] Automated TTL Expiration Worker (`TtlWorkerService`).
|
||||||
|
- [x] AI Feedback Record Exporter (`FeedbackExporterEngine`).
|
||||||
|
- [x] Pure Worker Service Architecture (`Host.CreateApplicationBuilder`, Kestrel HTTP server removed).
|
||||||
|
- [x] Zero-Allocation MQTT RPC handlers for active trades & trade closure.
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
- [ ] Automated Trailing Stop Loss adjustment engine based on ATR (Average True Range).
|
||||||
|
- [ ] Direct Broker API Execution integration (Trade Republic / Interactive Brokers automated order placement).
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticTrades.Database;
|
||||||
|
using FinlyticTrades.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Parquet.Serialization;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Services;
|
||||||
|
|
||||||
|
public interface IFeedbackExporterEngine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exports feedback data for closed trades.
|
||||||
|
/// </summary>
|
||||||
|
Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<FeedbackExporterEngine> _logger;
|
||||||
|
private readonly string _feedbackDir;
|
||||||
|
|
||||||
|
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, ILogger<FeedbackExporterEngine> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
||||||
|
|
||||||
|
if (!Directory.Exists(_feedbackDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_feedbackDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service started.", "TradesChannel");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ExportFeedbackDataAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] Error executing feedback exporter job.", "TradesChannel");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromHours(6), stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service stopped.", "TradesChannel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports feedback data for closed trades into sector-based JSON and Parquet formats.
|
||||||
|
/// Uses atomic file-writes to avoid thread-lock conflicts with reader processes.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
|
||||||
|
|
||||||
|
var closedTrades = await dbContext.Trades
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Status == TradeStatus.Closed && t.UserExitPrice.HasValue)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (closedTrades.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] No closed trades available for export.", "TradesChannel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = closedTrades.GroupBy(t => SanitizeSectorName(t.Sector));
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
var sectorName = group.Key;
|
||||||
|
var sectorDir = Path.Combine(_feedbackDir, sectorName);
|
||||||
|
|
||||||
|
if (!Directory.Exists(sectorDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(sectorDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
var feedbackRecords = new List<TradeFeedbackRecord>();
|
||||||
|
|
||||||
|
foreach (var t in group)
|
||||||
|
{
|
||||||
|
var startTime = t.ExecutionTimestamp ?? t.CreatedAt;
|
||||||
|
var endTime = t.UserExitTimestamp ?? t.ClosedAt ?? DateTime.UtcNow;
|
||||||
|
double reactionDelay = Math.Max(0, (endTime - startTime).TotalMinutes);
|
||||||
|
|
||||||
|
decimal exitPrice = t.UserExitPrice ?? t.EntryPrice;
|
||||||
|
|
||||||
|
decimal entryPrice = t.ActualEntryPrice.HasValue && t.ActualEntryPrice.Value > 0
|
||||||
|
? t.ActualEntryPrice.Value
|
||||||
|
: t.EntryPrice;
|
||||||
|
|
||||||
|
decimal slippagePct = t.EntryPrice > 0
|
||||||
|
? Math.Abs((entryPrice - t.EntryPrice) / t.EntryPrice) * 100.0m
|
||||||
|
: 0m;
|
||||||
|
|
||||||
|
var rec = new TradeFeedbackRecord
|
||||||
|
{
|
||||||
|
TradeId = t.TradeId,
|
||||||
|
AnalysisId = t.AnalysisId,
|
||||||
|
Sector = t.Sector,
|
||||||
|
Symbol = t.Symbol,
|
||||||
|
Isin = t.Isin,
|
||||||
|
EntryPrice = entryPrice,
|
||||||
|
StopLoss = t.StopLoss,
|
||||||
|
TakeProfit = t.TakeProfit,
|
||||||
|
UserExitPrice = exitPrice,
|
||||||
|
PnlAbsolute = t.PnlAbsolute ?? 0m,
|
||||||
|
PnlPercent = t.PnlPercent ?? 0m,
|
||||||
|
IsWin = t.IsWin ?? false,
|
||||||
|
CloseReason = t.CloseReason ?? "Unknown",
|
||||||
|
VixRegime = t.VixRegime,
|
||||||
|
VixValue = t.VixValue,
|
||||||
|
ReactionDelayMinutes = Math.Round(reactionDelay, 2),
|
||||||
|
SlippagePercent = Math.Round(slippagePct, 2),
|
||||||
|
CreatedAt = t.CreatedAt,
|
||||||
|
ClosedAt = endTime
|
||||||
|
};
|
||||||
|
|
||||||
|
feedbackRecords.Add(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Atomic JSON Export (.tmp -> move)
|
||||||
|
string jsonPath = Path.Combine(sectorDir, $"{sectorName}_feedback.json");
|
||||||
|
string jsonTmpPath = Path.Combine(sectorDir, $"{sectorName}_feedback.json.tmp");
|
||||||
|
string jsonContent = JsonSerializer.Serialize(feedbackRecords, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(jsonTmpPath, jsonContent, cancellationToken);
|
||||||
|
File.Move(jsonTmpPath, jsonPath, overwrite: true);
|
||||||
|
|
||||||
|
// 2. Atomic Parquet Export (.tmp -> move)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string parquetPath = Path.Combine(sectorDir, $"{sectorName}_feedback.parquet");
|
||||||
|
string parquetTmpPath = Path.Combine(sectorDir, $"{sectorName}_feedback.parquet.tmp");
|
||||||
|
|
||||||
|
await using (var fileStream = new FileStream(parquetTmpPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true))
|
||||||
|
{
|
||||||
|
await ParquetSerializer.SerializeAsync(feedbackRecords, fileStream, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(parquetTmpPath, parquetPath, overwrite: true);
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", "TradesChannel", sectorName, parquetPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "[{Channel}] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", "TradesChannel", sectorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully exported feedback data for {Count} closed trades across {Sectors} sectors.",
|
||||||
|
"TradesChannel", closedTrades.Count, groups.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeSectorName(string? sector)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sector)) return "general";
|
||||||
|
|
||||||
|
var clean = Regex.Replace(sector.Trim().ToLowerInvariant(), @"[^a-z0-9_\-]", "_");
|
||||||
|
return string.IsNullOrWhiteSpace(clean) ? "general" : clean;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using FinlyticTrades.Database;
|
||||||
|
using FinlyticTrades.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Services;
|
||||||
|
|
||||||
|
public interface ISettingsDbService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current settings.
|
||||||
|
/// </summary>
|
||||||
|
Task<TradesSettingsEntity> GetSettingsAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the provided settings.
|
||||||
|
/// </summary>
|
||||||
|
Task<TradesSettingsEntity> SaveSettingsAsync(TradesSettingsEntity settings);
|
||||||
|
/// <summary>
|
||||||
|
/// Updates settings from a dictionary of key-value pairs.
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SettingsDbService : ISettingsDbService
|
||||||
|
{
|
||||||
|
private readonly TradesDbContext _context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the SettingsDbService class.
|
||||||
|
/// </summary>
|
||||||
|
public SettingsDbService(TradesDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current settings.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TradesSettingsEntity> GetSettingsAsync()
|
||||||
|
{
|
||||||
|
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
||||||
|
if (settings == null)
|
||||||
|
{
|
||||||
|
settings = new TradesSettingsEntity { Id = Guid.NewGuid() };
|
||||||
|
_context.Settings.Add(settings);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_context.ChangeTracker.Clear();
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the provided settings.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TradesSettingsEntity> SaveSettingsAsync(TradesSettingsEntity settings)
|
||||||
|
{
|
||||||
|
var existing = await _context.Settings.FirstOrDefaultAsync();
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
|
||||||
|
_context.Settings.Add(settings);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.AtrStopLossMultiplier = settings.AtrStopLossMultiplier;
|
||||||
|
existing.RiskPerTradePercentage = settings.RiskPerTradePercentage;
|
||||||
|
existing.MaxOpenPositions = settings.MaxOpenPositions;
|
||||||
|
existing.UpdatedAt = settings.UpdatedAt;
|
||||||
|
_context.Settings.Update(existing);
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates settings from a dictionary of key-value pairs.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
var settings = await GetSettingsAsync();
|
||||||
|
|
||||||
|
foreach (var (key, value) in dictionary)
|
||||||
|
{
|
||||||
|
if (string.Equals(key, "AtrStopLossMultiplier", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var atr))
|
||||||
|
settings.AtrStopLossMultiplier = atr;
|
||||||
|
else if (string.Equals(key, "RiskPerTradePercentage", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var risk))
|
||||||
|
settings.RiskPerTradePercentage = risk;
|
||||||
|
else if (string.Equals(key, "MaxOpenPositions", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var maxPos))
|
||||||
|
settings.MaxOpenPositions = maxPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await SaveSettingsAsync(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Models.Analyzer;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticTrades.Database;
|
||||||
|
using FinlyticTrades.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Services;
|
||||||
|
|
||||||
|
public interface ITradeLifecycleService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a proposed trade.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accepts a trade proposal and maps execution parameters.
|
||||||
|
/// </summary>
|
||||||
|
Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an hourly update for a trade.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes a trade manually.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TradeLifecycleService : ITradeLifecycleService
|
||||||
|
{
|
||||||
|
private readonly TradesDbContext _dbContext;
|
||||||
|
private readonly ILogger<TradeLifecycleService> _logger;
|
||||||
|
|
||||||
|
public TradeLifecycleService(TradesDbContext dbContext, ILogger<TradeLifecycleService> logger)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
if (response == null || !response.IsTradeProposed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", "TradesChannel", response?.AnalysisId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Proposal != null)
|
||||||
|
{
|
||||||
|
response.Proposal.UserId = userId;
|
||||||
|
return await ProcessProposedTradeAsync(response.Proposal, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.N8nResponse != null)
|
||||||
|
{
|
||||||
|
var n8n = response.N8nResponse;
|
||||||
|
var exec = n8n.ExecutionPlan;
|
||||||
|
|
||||||
|
var generatedProposal = new TradeProposalDto
|
||||||
|
{
|
||||||
|
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
|
||||||
|
AnalysisId = response.AnalysisId,
|
||||||
|
EventId = response.AnalysisId,
|
||||||
|
UserId = userId,
|
||||||
|
IsGlobalProposal = false,
|
||||||
|
Status = "Proposed",
|
||||||
|
SignalType = string.Equals(n8n.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
||||||
|
RiskTolerance = n8n.SuggestedRisk,
|
||||||
|
Timeframe = n8n.SuggestedTimeframe,
|
||||||
|
Reasoning = n8n.AiReasoning,
|
||||||
|
StopLoss = exec?.StopLoss ?? 0m,
|
||||||
|
TakeProfit = exec?.TakeProfitTargets?.FirstOrDefault() ?? 0m,
|
||||||
|
EntryZoneMin = exec?.EntryZone?.Min,
|
||||||
|
EntryZoneMax = exec?.EntryZone?.Max,
|
||||||
|
TakeProfitTargets = exec?.TakeProfitTargets,
|
||||||
|
RiskRewardRatio = exec?.RiskRewardRatio,
|
||||||
|
MaxLeverage = exec?.MaxLeverage,
|
||||||
|
TechnicalRationale = n8n.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
|
||||||
|
FundamentalRationale = n8n.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
|
||||||
|
RiskWarning = n8n.DetailedAnalysis?.RiskWarning ?? string.Empty,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
return await ProcessProposedTradeAsync(generatedProposal, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a proposed trade.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping.", "TradesChannel");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetStatus = string.Equals(proposal.Status, "Rejected", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TradeStatus.Rejected
|
||||||
|
: TradeStatus.Proposed;
|
||||||
|
|
||||||
|
var existingTrade = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t =>
|
||||||
|
(!string.IsNullOrWhiteSpace(proposal.TradeId) && t.TradeId == proposal.TradeId) ||
|
||||||
|
(!string.IsNullOrWhiteSpace(proposal.AnalysisId) && t.AnalysisId == proposal.AnalysisId),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (existingTrade != null)
|
||||||
|
{
|
||||||
|
if (existingTrade.Status != TradeStatus.Active && existingTrade.Status != TradeStatus.Closed)
|
||||||
|
{
|
||||||
|
existingTrade.Status = targetStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
MapProposalToEntity(proposal, existingTrade);
|
||||||
|
_dbContext.Trades.Update(existingTrade);
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully UPDATED trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||||
|
"TradesChannel", existingTrade.TradeId, proposal.Symbol, proposal.Isin, existingTrade.Status);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string tradeId = !string.IsNullOrWhiteSpace(proposal.TradeId) ? proposal.TradeId : ("TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant());
|
||||||
|
|
||||||
|
var tradeEntity = new TradeEntity
|
||||||
|
{
|
||||||
|
TradeId = tradeId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
MapProposalToEntity(proposal, tradeEntity);
|
||||||
|
tradeEntity.Status = targetStatus;
|
||||||
|
|
||||||
|
_dbContext.Trades.Add(tradeEntity);
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully ingested NEW trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||||
|
"TradesChannel", tradeId, proposal.Symbol, proposal.Isin, targetStatus);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accepts a trade proposal and updates execution parameters.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string targetUserId = !string.IsNullOrWhiteSpace(request.UserId) ? request.UserId : "default_user";
|
||||||
|
|
||||||
|
var existingTrade = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t =>
|
||||||
|
(!string.IsNullOrEmpty(request.TradeId) && t.TradeId == request.TradeId) ||
|
||||||
|
(!string.IsNullOrEmpty(request.AnalysisId) && t.AnalysisId == request.AnalysisId), cancellationToken);
|
||||||
|
|
||||||
|
if (existingTrade != null)
|
||||||
|
{
|
||||||
|
if (existingTrade.Status == TradeStatus.Closed)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] Refused to accept trade {TradeId} because its status is CLOSED", "TradesChannel", existingTrade.TradeId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingTrade.Status = TradeStatus.Active;
|
||||||
|
existingTrade.IsGlobalProposal = false;
|
||||||
|
existingTrade.UserId = targetUserId;
|
||||||
|
|
||||||
|
if (request.ActualEntryPrice > 0) existingTrade.ActualEntryPrice = request.ActualEntryPrice;
|
||||||
|
if (request.EntryPrice > 0) existingTrade.EntryPrice = request.EntryPrice.Value;
|
||||||
|
if (request.PositionSize > 0) existingTrade.PositionSize = request.PositionSize;
|
||||||
|
if (request.LeverageUsed > 0) existingTrade.LeverageUsed = request.LeverageUsed;
|
||||||
|
if (request.Quantity > 0) existingTrade.Quantity = request.Quantity;
|
||||||
|
if (request.EntryFee.HasValue) existingTrade.EntryFee = request.EntryFee;
|
||||||
|
if (request.ExitFee.HasValue) existingTrade.ExitFee = request.ExitFee;
|
||||||
|
if (request.StopLoss > 0) existingTrade.StopLoss = request.StopLoss.Value;
|
||||||
|
if (request.TakeProfit > 0) existingTrade.TakeProfit = request.TakeProfit.Value;
|
||||||
|
if (request.KnockoutThreshold > 0) existingTrade.KnockoutThreshold = request.KnockoutThreshold;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Timeframe)) existingTrade.Timeframe = request.Timeframe;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Reasoning)) existingTrade.Reasoning = request.Reasoning;
|
||||||
|
|
||||||
|
existingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
existingTrade.PnlAbsolute = -(existingTrade.EntryFee ?? 0m) - (existingTrade.ExitFee ?? 0m);
|
||||||
|
if (existingTrade.PositionSize > 0)
|
||||||
|
{
|
||||||
|
existingTrade.PnlPercent = (existingTrade.PnlAbsolute / existingTrade.PositionSize) * 100m;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbContext.Trades.Update(existingTrade);
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully ACCEPTED and UPDATED trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", existingTrade.TradeId, existingTrade.Isin, existingTrade.UserId);
|
||||||
|
return existingTrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
var proposal = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t => t.IsGlobalProposal &&
|
||||||
|
(!string.IsNullOrEmpty(request.AnalysisId) ? t.AnalysisId == request.AnalysisId : t.Isin == request.Isin),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var targetTradeId = !string.IsNullOrWhiteSpace(request.TradeId) ? request.TradeId : ("TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant());
|
||||||
|
|
||||||
|
var newTrade = new TradeEntity
|
||||||
|
{
|
||||||
|
TradeId = targetTradeId,
|
||||||
|
AnalysisId = proposal?.AnalysisId ?? (string.IsNullOrWhiteSpace(request.AnalysisId) ? Guid.NewGuid().ToString("N") : request.AnalysisId),
|
||||||
|
EventId = proposal?.EventId ?? request.AnalysisId,
|
||||||
|
Sector = proposal?.Sector ?? "General",
|
||||||
|
Symbol = proposal?.Symbol ?? request.Symbol ?? request.Isin,
|
||||||
|
Isin = proposal?.Isin ?? request.Isin,
|
||||||
|
CompanyName = proposal?.CompanyName ?? request.Symbol ?? request.Isin,
|
||||||
|
Status = TradeStatus.Active,
|
||||||
|
IsGlobalProposal = false,
|
||||||
|
UserId = targetUserId,
|
||||||
|
|
||||||
|
EntryPrice = proposal?.EntryPrice ?? request.EntryPrice ?? request.ActualEntryPrice ?? 0m,
|
||||||
|
StopLoss = request.StopLoss > 0 ? request.StopLoss.Value : (proposal?.StopLoss ?? 0m),
|
||||||
|
TakeProfit = request.TakeProfit > 0 ? request.TakeProfit.Value : (proposal?.TakeProfit ?? 0m),
|
||||||
|
SignalType = proposal?.SignalType ?? request.SignalType ?? "BUY",
|
||||||
|
RiskTolerance = proposal?.RiskTolerance ?? "Moderate",
|
||||||
|
Timeframe = proposal?.Timeframe ?? request.Timeframe ?? "1D",
|
||||||
|
InstrumentType = proposal?.InstrumentType ?? request.InstrumentType ?? "Stock",
|
||||||
|
WinRate = proposal?.WinRate ?? 50,
|
||||||
|
VixRegime = proposal?.VixRegime ?? FinlyticCore.Models.Analyzer.VixMarketRegime.Normal,
|
||||||
|
VixValue = proposal?.VixValue ?? 15,
|
||||||
|
Reasoning = proposal?.Reasoning ?? request.Reasoning ?? "User Accepted Trade",
|
||||||
|
EntryZoneMin = proposal?.EntryZoneMin,
|
||||||
|
EntryZoneMax = proposal?.EntryZoneMax,
|
||||||
|
TakeProfitTargets = proposal?.TakeProfitTargets,
|
||||||
|
RiskRewardRatio = proposal?.RiskRewardRatio,
|
||||||
|
MaxLeverage = proposal?.MaxLeverage,
|
||||||
|
TechnicalRationale = proposal?.TechnicalRationale ?? string.Empty,
|
||||||
|
FundamentalRationale = proposal?.FundamentalRationale ?? string.Empty,
|
||||||
|
RiskWarning = proposal?.RiskWarning ?? string.Empty,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
|
||||||
|
ActualEntryPrice = request.ActualEntryPrice > 0 ? request.ActualEntryPrice : (proposal?.EntryPrice ?? request.EntryPrice ?? 0m),
|
||||||
|
PositionSize = request.PositionSize,
|
||||||
|
LeverageUsed = request.LeverageUsed > 0 ? request.LeverageUsed : 1m,
|
||||||
|
EntryFee = request.EntryFee,
|
||||||
|
ExitFee = request.ExitFee,
|
||||||
|
ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||||
|
Quantity = request.Quantity,
|
||||||
|
KnockoutThreshold = request.KnockoutThreshold,
|
||||||
|
IsRecurring = request.IsRecurring
|
||||||
|
};
|
||||||
|
|
||||||
|
newTrade.PnlAbsolute = -(newTrade.EntryFee ?? 0m) - (newTrade.ExitFee ?? 0m);
|
||||||
|
if (newTrade.PositionSize > 0)
|
||||||
|
{
|
||||||
|
newTrade.PnlPercent = (newTrade.PnlAbsolute / newTrade.PositionSize) * 100m;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbContext.Trades.Add(newTrade);
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", newTrade.TradeId, request.Isin, newTrade.UserId);
|
||||||
|
return newTrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an hourly update for a trade.
|
||||||
|
/// </summary>
|
||||||
|
public async Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var trade = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t => t.TradeId == update.TradeId || t.Id.ToString() == update.TradeId, cancellationToken);
|
||||||
|
|
||||||
|
if (trade == null || (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", "TradesChannel", update.TradeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateEntity = new TradeHourlyUpdateEntity
|
||||||
|
{
|
||||||
|
TradeId = trade.Id,
|
||||||
|
Recommendation = update.Recommendation,
|
||||||
|
CurrentPrice = update.CurrentPrice,
|
||||||
|
SuggestedStopLoss = update.SuggestedStopLoss,
|
||||||
|
SuggestedTakeProfit = update.SuggestedTakeProfit,
|
||||||
|
VixValue = update.VixValue,
|
||||||
|
Reasoning = update.Reasoning,
|
||||||
|
Timestamp = update.Timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
_dbContext.TradeHourlyUpdates.Add(updateEntity);
|
||||||
|
|
||||||
|
if (update.SuggestedStopLoss.HasValue && update.SuggestedStopLoss > 0)
|
||||||
|
trade.StopLoss = update.SuggestedStopLoss.Value;
|
||||||
|
if (update.SuggestedTakeProfit.HasValue && update.SuggestedTakeProfit > 0)
|
||||||
|
trade.TakeProfit = update.SuggestedTakeProfit.Value;
|
||||||
|
|
||||||
|
if (string.Equals(update.Recommendation, "Close", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (trade.IsGlobalProposal || trade.Status == TradeStatus.Proposed)
|
||||||
|
{
|
||||||
|
trade.Status = TradeStatus.Invalidated;
|
||||||
|
trade.CloseReason = "ProposalInvalidated";
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trade.Status = TradeStatus.Closed;
|
||||||
|
trade.UserExitPrice = update.CurrentPrice;
|
||||||
|
trade.UserExitTimestamp = DateTime.UtcNow;
|
||||||
|
trade.CloseReason = "AiRecommendationClose";
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
CalculatePnL(trade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("[{Channel}] Added hourly update for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
||||||
|
"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)
|
||||||
|
{
|
||||||
|
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.UserId == userId || t.IsGlobalProposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.Where(t => t.Status == TradeStatus.Active || t.Status == TradeStatus.Proposed)
|
||||||
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.UserId == userId || t.IsGlobalProposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(isin))
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.Isin == isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<TradeStatus>(status, true, out var parsedStatus))
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.Status == parsedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var trade = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t => t.TradeId == tradeId || t.Id.ToString() == tradeId, cancellationToken);
|
||||||
|
|
||||||
|
if (trade == null) return null;
|
||||||
|
|
||||||
|
trade.Status = TradeStatus.Closed;
|
||||||
|
trade.UserExitPrice = request.UserExitPrice;
|
||||||
|
trade.UserExitTimestamp = request.UserExitTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
||||||
|
trade.CloseReason = request.CloseReason;
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
CalculatePnL(trade);
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("[{Channel}] Trade {TradeId} manually closed at price {ExitPrice}. PnL: {PnlAbs} ({PnlPct:F2}%)",
|
||||||
|
"TradesChannel", trade.TradeId, trade.UserExitPrice, trade.PnlAbsolute, trade.PnlPercent);
|
||||||
|
|
||||||
|
return trade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rejects a trade proposal.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TradeEntity?> RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var trade = await _dbContext.Trades
|
||||||
|
.FirstOrDefaultAsync(t => t.TradeId == tradeId || t.Id.ToString() == tradeId, cancellationToken);
|
||||||
|
|
||||||
|
if (trade == null) return null;
|
||||||
|
|
||||||
|
trade.Status = TradeStatus.Rejected;
|
||||||
|
trade.CloseReason = request.CloseReason ?? "UserRejected";
|
||||||
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("[{Channel}] Trade {TradeId} rejected by user.", "TradesChannel", trade.TradeId);
|
||||||
|
|
||||||
|
return trade;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapProposalToEntity(TradeProposalDto dto, TradeEntity entity)
|
||||||
|
{
|
||||||
|
entity.AnalysisId = dto.AnalysisId;
|
||||||
|
entity.EventId = dto.EventId;
|
||||||
|
entity.UserId = !string.IsNullOrWhiteSpace(dto.UserId) ? dto.UserId : (entity.UserId ?? "default_user");
|
||||||
|
entity.IsGlobalProposal = dto.IsGlobalProposal;
|
||||||
|
entity.Sector = dto.Sector;
|
||||||
|
entity.Symbol = dto.Symbol;
|
||||||
|
entity.Isin = dto.Isin;
|
||||||
|
entity.CompanyName = dto.CompanyName;
|
||||||
|
|
||||||
|
entity.EntryPrice = dto.EntryPrice;
|
||||||
|
entity.StopLoss = dto.StopLoss;
|
||||||
|
entity.TakeProfit = dto.TakeProfit;
|
||||||
|
entity.SignalType = dto.SignalType;
|
||||||
|
entity.RiskTolerance = dto.RiskTolerance;
|
||||||
|
entity.Timeframe = dto.Timeframe;
|
||||||
|
entity.InstrumentType = dto.InstrumentType;
|
||||||
|
entity.WinRate = dto.WinRate;
|
||||||
|
entity.VixRegime = dto.VixRegime;
|
||||||
|
entity.VixValue = dto.VixValue;
|
||||||
|
entity.TtlMinutes = dto.TtlMinutes;
|
||||||
|
entity.Reasoning = dto.Reasoning;
|
||||||
|
|
||||||
|
entity.EntryZoneMin = dto.EntryZoneMin;
|
||||||
|
entity.EntryZoneMax = dto.EntryZoneMax;
|
||||||
|
entity.TakeProfitTargets = dto.TakeProfitTargets != null ? string.Join(",", dto.TakeProfitTargets) : entity.TakeProfitTargets;
|
||||||
|
entity.RiskRewardRatio = dto.RiskRewardRatio;
|
||||||
|
entity.MaxLeverage = dto.MaxLeverage;
|
||||||
|
entity.TechnicalRationale = dto.TechnicalRationale;
|
||||||
|
entity.FundamentalRationale = dto.FundamentalRationale;
|
||||||
|
entity.RiskWarning = dto.RiskWarning;
|
||||||
|
|
||||||
|
if (dto.ActualEntryPrice.HasValue) entity.ActualEntryPrice = dto.ActualEntryPrice;
|
||||||
|
if (dto.PositionSize.HasValue) entity.PositionSize = dto.PositionSize;
|
||||||
|
if (dto.LeverageUsed.HasValue) entity.LeverageUsed = dto.LeverageUsed;
|
||||||
|
if (dto.EntryFee.HasValue) entity.EntryFee = dto.EntryFee;
|
||||||
|
if (dto.ExitFee.HasValue) entity.ExitFee = dto.ExitFee;
|
||||||
|
if (dto.ExecutionTimestamp.HasValue) entity.ExecutionTimestamp = dto.ExecutionTimestamp;
|
||||||
|
if (dto.Quantity.HasValue) entity.Quantity = dto.Quantity;
|
||||||
|
if (dto.KnockoutThreshold.HasValue) entity.KnockoutThreshold = dto.KnockoutThreshold;
|
||||||
|
entity.IsRecurring = dto.IsRecurring;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CalculatePnL(TradeEntity trade)
|
||||||
|
{
|
||||||
|
if (!trade.UserExitPrice.HasValue) return;
|
||||||
|
|
||||||
|
decimal exitPrice = trade.UserExitPrice.Value;
|
||||||
|
decimal entryPrice = trade.ActualEntryPrice.HasValue && trade.ActualEntryPrice.Value > 0m
|
||||||
|
? trade.ActualEntryPrice.Value
|
||||||
|
: trade.EntryPrice;
|
||||||
|
|
||||||
|
if (entryPrice <= 0m) return;
|
||||||
|
|
||||||
|
decimal positionSize = trade.PositionSize.HasValue && trade.PositionSize.Value > 0m
|
||||||
|
? trade.PositionSize.Value
|
||||||
|
: ((trade.Quantity ?? 1m) * entryPrice);
|
||||||
|
|
||||||
|
decimal entryFee = trade.EntryFee ?? 0m;
|
||||||
|
decimal exitFee = trade.ExitFee ?? 0m;
|
||||||
|
decimal totalFees = entryFee + exitFee;
|
||||||
|
|
||||||
|
decimal rawMoveRatio;
|
||||||
|
bool isShort = string.Equals(trade.SignalType, "SELL", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(trade.SignalType, "SHORT", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isShort)
|
||||||
|
{
|
||||||
|
rawMoveRatio = (entryPrice - exitPrice) / entryPrice;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rawMoveRatio = (exitPrice - entryPrice) / entryPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal pnlAbs;
|
||||||
|
if (string.Equals(trade.InstrumentType, "KnockOut", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(trade.InstrumentType, "Certificate", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(trade.InstrumentType, "Option", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
pnlAbs = (rawMoveRatio * positionSize) - totalFees;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
decimal leverage = trade.LeverageUsed > 0m ? trade.LeverageUsed.Value : 1m;
|
||||||
|
pnlAbs = (rawMoveRatio * positionSize * leverage) - totalFees;
|
||||||
|
}
|
||||||
|
|
||||||
|
trade.PnlAbsolute = Math.Round(pnlAbs, 4);
|
||||||
|
trade.PnlPercent = positionSize > 0m
|
||||||
|
? Math.Round((pnlAbs / positionSize) * 100.0m, 2)
|
||||||
|
: 0m;
|
||||||
|
|
||||||
|
trade.IsWin = pnlAbs > 0m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Models;
|
||||||
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Util;
|
||||||
|
using FinlyticTrades.Entities;
|
||||||
|
using FinlyticTrades.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Util;
|
||||||
|
|
||||||
|
public class TradesMqttClient : ManagedMqttClient, IHostedService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ITradeLifecycleService _tradeLifecycleService;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<TradesMqttClient> _logger;
|
||||||
|
|
||||||
|
public TradesMqttClient(
|
||||||
|
IConfiguration configuration,
|
||||||
|
ITradeLifecycleService tradeLifecycleService,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<TradesMqttClient> logger) : base(logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_tradeLifecycleService = tradeLifecycleService;
|
||||||
|
_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_trades")}_{Guid.NewGuid():N}"
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Starting Unified Trades MQTT Client. Host: {Host}, ClientId: {ClientId}", "TradesChannel", config.Host, config.ClientId);
|
||||||
|
await ConnectAsync(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Stopping Unified Trades MQTT Client.", "TradesChannel");
|
||||||
|
await DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Trades MQTT Client connected. Subscribing to topics...", "TradesChannel");
|
||||||
|
|
||||||
|
await SubscribeAsync("finlytic/trades/proposed/#");
|
||||||
|
await SubscribeAsync("finlytic/trades/updates/#");
|
||||||
|
await SubscribeAsync("finlytic/trades/accept/#");
|
||||||
|
await SubscribeAsync("services/request/trades_Get/#");
|
||||||
|
await SubscribeAsync("services/request/trades_Close/#");
|
||||||
|
await SubscribeAsync("services/request/trades_Reject/#");
|
||||||
|
await SubscribeAsync("services/request/trades_Accept/#");
|
||||||
|
await SubscribeAsync("services/config/updated/#");
|
||||||
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "TradesChannel");
|
||||||
|
}
|
||||||
|
|
||||||
|
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("FinlyticTrades", StringComparison.OrdinalIgnoreCase)
|
||||||
|
: topic.Contains("FinlyticTrades", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isForMe)
|
||||||
|
{
|
||||||
|
var correlationId = segments[^1];
|
||||||
|
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||||
|
var healthResp = new ServiceHealthResponse("FinlyticTrades", "Online", DateTime.UtcNow, "Connected");
|
||||||
|
await PublishAsync(respTopic, healthResp);
|
||||||
|
_logger.LogInformation("[{Channel}] [TradesMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "TradesChannel", correlationId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (topic.EndsWith("FinlyticTrades", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] [TradesMqttClient] Received config update event for FinlyticTrades.", "TradesChannel");
|
||||||
|
var payload = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
|
||||||
|
if (payload?.Settings != null && payload.Settings.Count > 0)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var settingsDb = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||||
|
await settingsDb.UpdateSettingsFromDictionaryAsync(payload.Settings);
|
||||||
|
_logger.LogInformation("[{Channel}] [TradesMqttClient] Persisted {Count} updated settings to FinlyticTrades database.", "TradesChannel", payload.Settings.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.StartsWith("finlytic/trades/proposed/"))
|
||||||
|
{
|
||||||
|
var proposal = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeProposalDto);
|
||||||
|
if (proposal != null && (!string.IsNullOrWhiteSpace(proposal.Symbol) || !string.IsNullOrWhiteSpace(proposal.Isin)))
|
||||||
|
{
|
||||||
|
await _tradeLifecycleService.ProcessProposedTradeAsync(proposal, CancellationToken.None);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] [TradesMqttClient] Received proposed trade payload but Symbol/ISIN is empty. Skipping ingestion.", "TradesChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("finlytic/trades/accept/"))
|
||||||
|
{
|
||||||
|
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
||||||
|
if (acceptDto != null)
|
||||||
|
{
|
||||||
|
var newTrade = await _tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
||||||
|
if (newTrade != null)
|
||||||
|
{
|
||||||
|
var dto = MapToDto(newTrade);
|
||||||
|
await PublishTradeUpdateAsync(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("services/request/trades_Accept/"))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/').Last();
|
||||||
|
var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto);
|
||||||
|
if (acceptDto != null)
|
||||||
|
{
|
||||||
|
var acceptedTrade = await _tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None);
|
||||||
|
if (acceptedTrade != null)
|
||||||
|
{
|
||||||
|
var acceptedDto = MapToDto(acceptedTrade);
|
||||||
|
await PublishAsync($"services/response/trades_Accept/{correlationId}", acceptedDto);
|
||||||
|
await PublishTradeUpdateAsync(acceptedDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("finlytic/trades/updates/"))
|
||||||
|
{
|
||||||
|
var update = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeHourlyUpdateDto);
|
||||||
|
if (update != null)
|
||||||
|
{
|
||||||
|
await _tradeLifecycleService.AddHourlyUpdateAsync(update, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("services/request/trades_Get/"))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/').Last();
|
||||||
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.GetTradesRequest);
|
||||||
|
|
||||||
|
string? isin = request?.Isin;
|
||||||
|
string? status = request?.Status;
|
||||||
|
string? userId = request?.UserId;
|
||||||
|
|
||||||
|
var trades = await _tradeLifecycleService.GetTradesAsync(isin, status, userId);
|
||||||
|
var dtos = trades.Select(MapToDto).ToList();
|
||||||
|
|
||||||
|
await PublishAsync($"services/response/trades_Get/{correlationId}", dtos);
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("services/request/trades_Close/"))
|
||||||
|
{
|
||||||
|
var parts = topic.Split('/');
|
||||||
|
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
||||||
|
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
||||||
|
|
||||||
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
||||||
|
|
||||||
|
if (request != null && !string.IsNullOrEmpty(tradeId))
|
||||||
|
{
|
||||||
|
var closedTrade = await _tradeLifecycleService.CloseTradeAsync(tradeId, request);
|
||||||
|
if (closedTrade != null)
|
||||||
|
{
|
||||||
|
var closedDto = MapToDto(closedTrade);
|
||||||
|
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();
|
||||||
|
await PublishAsync($"finlytic/trades/closed/{sectorSafe}/{closedTrade.Symbol.ToLowerInvariant()}", closedDto);
|
||||||
|
await PublishTradeUpdateAsync(closedDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("services/request/trades_Reject/"))
|
||||||
|
{
|
||||||
|
var parts = topic.Split('/');
|
||||||
|
var tradeId = parts.Length > 3 ? parts[3] : string.Empty;
|
||||||
|
var correlationId = parts.Length > 4 ? parts[4] : string.Empty;
|
||||||
|
|
||||||
|
var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest);
|
||||||
|
|
||||||
|
if (request != null && !string.IsNullOrEmpty(tradeId))
|
||||||
|
{
|
||||||
|
var rejectedTrade = await _tradeLifecycleService.RejectTradeAsync(tradeId, request);
|
||||||
|
if (rejectedTrade != null)
|
||||||
|
{
|
||||||
|
var rejectedDto = MapToDto(rejectedTrade);
|
||||||
|
await PublishAsync($"services/response/trades_Reject/{correlationId}", rejectedDto);
|
||||||
|
await PublishTradeUpdateAsync(rejectedDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] Error processing incoming MQTT message on topic {Topic}", "TradesChannel", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishTradeUpdateAsync(TradeProposalDto trade)
|
||||||
|
{
|
||||||
|
await PublishAsync("finlytic/trades/update", trade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TradeProposalDto MapToDto(TradeEntity t)
|
||||||
|
{
|
||||||
|
List<decimal>? parseTakeProfitTargets()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(t.TakeProfitTargets)) return null;
|
||||||
|
|
||||||
|
var list = new List<decimal>();
|
||||||
|
var parts = t.TakeProfitTargets.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
if (decimal.TryParse(part, NumberStyles.Number, CultureInfo.InvariantCulture, out var val))
|
||||||
|
{
|
||||||
|
list.Add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list.Count > 0 ? list : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TradeProposalDto
|
||||||
|
{
|
||||||
|
TradeId = t.TradeId,
|
||||||
|
Status = t.Status.ToString(),
|
||||||
|
AnalysisId = t.AnalysisId,
|
||||||
|
EventId = t.EventId,
|
||||||
|
Sector = t.Sector,
|
||||||
|
Symbol = t.Symbol,
|
||||||
|
Isin = t.Isin,
|
||||||
|
CompanyName = t.CompanyName,
|
||||||
|
EntryPrice = t.EntryPrice,
|
||||||
|
StopLoss = t.StopLoss,
|
||||||
|
TakeProfit = t.TakeProfit,
|
||||||
|
SignalType = t.SignalType,
|
||||||
|
RiskTolerance = t.RiskTolerance,
|
||||||
|
Timeframe = t.Timeframe,
|
||||||
|
InstrumentType = t.InstrumentType,
|
||||||
|
WinRate = t.WinRate,
|
||||||
|
VixRegime = t.VixRegime,
|
||||||
|
VixValue = t.VixValue,
|
||||||
|
TtlMinutes = t.TtlMinutes,
|
||||||
|
Reasoning = t.Reasoning,
|
||||||
|
EntryZoneMin = t.EntryZoneMin,
|
||||||
|
EntryZoneMax = t.EntryZoneMax,
|
||||||
|
TakeProfitTargets = parseTakeProfitTargets(),
|
||||||
|
RiskRewardRatio = t.RiskRewardRatio,
|
||||||
|
MaxLeverage = t.MaxLeverage,
|
||||||
|
TechnicalRationale = t.TechnicalRationale,
|
||||||
|
FundamentalRationale = t.FundamentalRationale,
|
||||||
|
RiskWarning = t.RiskWarning,
|
||||||
|
CreatedAt = t.CreatedAt,
|
||||||
|
|
||||||
|
UserId = t.UserId,
|
||||||
|
IsGlobalProposal = t.IsGlobalProposal,
|
||||||
|
ActualEntryPrice = t.ActualEntryPrice,
|
||||||
|
PositionSize = t.PositionSize,
|
||||||
|
LeverageUsed = t.LeverageUsed,
|
||||||
|
EntryFee = t.EntryFee,
|
||||||
|
ExitFee = t.ExitFee,
|
||||||
|
ExecutionTimestamp = t.ExecutionTimestamp,
|
||||||
|
Quantity = t.Quantity,
|
||||||
|
KnockoutThreshold = t.KnockoutThreshold,
|
||||||
|
IsRecurring = t.IsRecurring
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user