Phase 0.2: Fee-Modell-Fundament (FeeModel + MarketData.TakerFeeBps + BUY-Fee-Log)
- FeeModel (pure, getestet): Kategorie-Fallback-Bps (Sports 75 / Politics-Finance 100 / Crypto 180 / Geopolitics 0 / Default 100) + FeeUsd(notional, bps). - MarketData.TakerFeeBps (Core) + Migration AddMarketTakerFeeBps (auf MySQL angewendet). Speichert den echten API-Satz, sobald verfuegbar; 0 = Fallback. - CopyTradingEngine loggt beim BUY die erwartete Taker-Fee (TakerFeeBps aus MarketCache, sonst Kategorie-Fallback) -> Akzeptanz "Fee im TradeReasoning". - FeeModelTests (Kategorie-Mapping case-insensitive, FeeUsd, 0-Faelle). 183 Tests gruen. Build/Smoke gruen. OFFEN (API-abhaengig, erst im Zielland verifizierbar): echtes fee_rate_bps-Feld aus der CLOB/Gamma-API lesen; Fee-basierter Edge-Discard; PnL-Fee-Korrektur. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
395caad11a
commit
cda5b055aa
@@ -17,5 +17,11 @@ namespace PolyTraderSharp.Models
|
|||||||
public string Outcomes { get; set; } = string.Empty; // e.g. "[\"Yes\", \"No\"]"
|
public string Outcomes { get; set; } = string.Empty; // e.g. "[\"Yes\", \"No\"]"
|
||||||
|
|
||||||
public bool NegRisk { get; set; }
|
public bool NegRisk { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Taker-Fee dieses Marktes in Basispunkten (100 = 1,0 %). 0 = keine/unbekannt.
|
||||||
|
/// Wird beim Markt-Sync gesetzt (API-Feld bevorzugt, sonst Kategorie-Fallback via FeeModel).
|
||||||
|
/// </summary>
|
||||||
|
public int TakerFeeBps { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+313
@@ -0,0 +1,313 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PolyTrader.Core.Persistence.Ef;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CoreDbContext))]
|
||||||
|
[Migration("20260707155613_AddMarketTakerFeeBps")]
|
||||||
|
partial class AddMarketTakerFeeBps
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.13")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTraderSharp.Models.AccountState", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ApiKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ApiPassphrase")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ApiSecret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<decimal>("AvailableBalance")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<bool>("CloseOnlyMode")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("HasOpenLimitOrders")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDemo")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("PayoutAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PayoutLimitUsd")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("PrivateKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TotalBalance")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("WalletAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.HasKey("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("core_accounts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTraderSharp.Models.MarketData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("ClobTokenIds")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("Closed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("ConditionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("NegRisk")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Outcomes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Question")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<int>("TakerFeeBps")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("core_markets", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTraderSharp.Models.Position", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDemo")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.Property<decimal>("AmountUsd")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("ConditionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentValueUsd")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("EntryPrice")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiryDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketQuestion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("OpenedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Outcome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Side")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("varchar(10)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("SourceTraderAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<int>("SourceTraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SourceTraderName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.HasKey("AccountId", "IsDemo", "TokenId");
|
||||||
|
|
||||||
|
b.ToTable("core_positions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTraderSharp.Models.TradeRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)");
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ClosedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("EntryPrice")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("ExitPrice")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("ExitReason")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDemo")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketQuestion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ModuleName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("OpenedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Outcome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnlPercent")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RealizedPnl")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("Side")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("varchar(10)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("ClosedAt");
|
||||||
|
|
||||||
|
b.HasIndex("ModuleName");
|
||||||
|
|
||||||
|
b.ToTable("core_trade_log", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMarketTakerFeeBps : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "TakerFeeBps",
|
||||||
|
table: "core_markets",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TakerFeeBps",
|
||||||
|
table: "core_markets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,6 +137,9 @@ namespace PolyTrader.Core.Persistence.Ef.Migrations
|
|||||||
.HasMaxLength(300)
|
.HasMaxLength(300)
|
||||||
.HasColumnType("varchar(300)");
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<int>("TakerFeeBps")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("core_markets", (string)null);
|
b.ToTable("core_markets", (string)null);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reine Fee-Logik (Phase 0.2). Polymarket erhebt seit März 2026 Taker-Fees (Maker zahlen 0).
|
||||||
|
/// Der reale Satz kommt idealerweise vom Markt-Objekt der CLOB-/Gamma-API (Feldname im Zielland
|
||||||
|
/// anhand https://docs.polymarket.com/trading/fees verifizieren); bis dahin greift die
|
||||||
|
/// Kategorie-Fallback-Tabelle. Werte in Basispunkten (bps): 100 bps = 1,0 %.
|
||||||
|
/// </summary>
|
||||||
|
public static class FeeModel
|
||||||
|
{
|
||||||
|
public const int MakerBps = 0;
|
||||||
|
|
||||||
|
// Fallback-Sätze je Kategorie (Taker, bps) – Näherung laut Plan, im Zielland verifizieren.
|
||||||
|
public const int SportsBps = 75; // 0,75 %
|
||||||
|
public const int PoliticsFinanceBps = 100; // 1,0 %
|
||||||
|
public const int CryptoBps = 180; // 1,8 %
|
||||||
|
public const int GeopoliticsBps = 0; // 0 %
|
||||||
|
public const int DefaultBps = 100; // konservativer Default
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback-Taker-Fee (bps) anhand der Markt-Kategorie, wenn die API keinen Satz liefert.
|
||||||
|
/// Robust gegen unbekannte/leere Kategorien (→ konservativer Default).
|
||||||
|
/// </summary>
|
||||||
|
public static int FallbackBps(string? category)
|
||||||
|
{
|
||||||
|
string c = (category ?? string.Empty).ToLowerInvariant();
|
||||||
|
if (c.Contains("sport")) return SportsBps;
|
||||||
|
if (c.Contains("crypto")) return CryptoBps;
|
||||||
|
if (c.Contains("geopolit")) return GeopoliticsBps;
|
||||||
|
if (c.Contains("politic") || c.Contains("finance") || c.Contains("econom")) return PoliticsFinanceBps;
|
||||||
|
return DefaultBps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Taker-Fee in USDC für ein Notional (Preis × Shares) bei gegebenem Satz.</summary>
|
||||||
|
public static decimal FeeUsd(decimal notionalUsd, int feeBps)
|
||||||
|
{
|
||||||
|
if (feeBps <= 0 || notionalUsd <= 0m) return 0m;
|
||||||
|
return notionalUsd * feeBps / 10000m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -396,6 +396,14 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
exactShares = exact.shares;
|
exactShares = exact.shares;
|
||||||
exactUsdc = exact.usdc;
|
exactUsdc = exact.usdc;
|
||||||
|
|
||||||
|
// Phase 0.2: erwartete Taker-Fee transparent loggen (API-Satz bevorzugt,
|
||||||
|
// sonst Kategorie-Fallback). Wird im Zielland gegen echte API-Fees verifiziert.
|
||||||
|
int feeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var feeMd)
|
||||||
|
? (feeMd.TakerFeeBps > 0 ? feeMd.TakerFeeBps : FeeModel.FallbackBps(feeMd.Category))
|
||||||
|
: FeeModel.FallbackBps(null);
|
||||||
|
decimal expectedFee = FeeModel.FeeUsd(exactUsdc, feeBps);
|
||||||
|
_logger.TradeReasoning($"💸 [FEE] {account.Name} | {signal.MarketQuestion}: erwartete Taker-Fee ~${expectedFee:F4} ({feeBps} bps auf ${exactUsdc:F2}).");
|
||||||
}
|
}
|
||||||
else if (signal.Side == "SELL")
|
else if (signal.Side == "SELL")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using PolyTrader.Modules.CopyTrading.Logic;
|
||||||
|
using Xunit;
|
||||||
|
using static PolyTrader.Modules.CopyTrading.Logic.FeeModel;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sicherheitsnetz für die Fee-Logik (Phase 0.2): Kategorie-Fallback + Fee-Berechnung.
|
||||||
|
/// </summary>
|
||||||
|
public class FeeModelTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Sports", 75)]
|
||||||
|
[InlineData("SPORTS", 75)] // case-insensitive
|
||||||
|
[InlineData("Crypto", 180)]
|
||||||
|
[InlineData("Geopolitics", 0)]
|
||||||
|
[InlineData("Politics", 100)]
|
||||||
|
[InlineData("Finance", 100)]
|
||||||
|
[InlineData("Economics", 100)]
|
||||||
|
[InlineData("Weird-Unknown", 100)] // konservativer Default
|
||||||
|
[InlineData("", 100)]
|
||||||
|
[InlineData(null, 100)]
|
||||||
|
public void FallbackBps_maps_category(string? category, int expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, FallbackBps(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FeeUsd_is_notional_times_bps()
|
||||||
|
{
|
||||||
|
Assert.Equal(1.0m, FeeUsd(100m, 100)); // 1,0 % von 100
|
||||||
|
Assert.Equal(0.9m, FeeUsd(50m, 180)); // 1,8 % von 50
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(100, 0)] // kein Satz
|
||||||
|
[InlineData(0, 100)] // kein Notional
|
||||||
|
[InlineData(-5, 100)] // negatives Notional
|
||||||
|
public void FeeUsd_zero_cases(double notional, int bps)
|
||||||
|
{
|
||||||
|
Assert.Equal(0m, FeeUsd((decimal)notional, bps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user