@
R3 Slice 4: IBKR-Marktdaten auf EF; Dapper vollstaendig entfernt - 3 IBKR-Entities (IBKRInstrument/IBKRMarketBar/IBKRExternalIdentifier) im CoreDbContext (core_ibkr_instruments/_market_data/_external_identifiers); Migration AddIbkr - IBKRMarketDataRepository von Dapper auf EF (IDbContextFactory<CoreDbContext>); Cross-Modul-Query (ct_trade) via Database.SqlQueryRaw - DatabaseService, IBKRMigrations, Dapper-Package entfernt; Laufzeit-Migrationen komplett weg (Schema extern via dotnet ef database update) - Tests: +4 IBKRMarketDataRepository (EF-InMemory) -> 43/43 gruen; Build + smoke-ui + App-Start ok R3 abgeschlossen: Persistenz vollstaendig auf EF Core. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
+2
-11
@@ -1,4 +1,3 @@
|
||||
using IBKRTrader.Core.Database.Migrations;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Core.Settings;
|
||||
@@ -131,16 +130,8 @@ public sealed class LauncherForm : Form
|
||||
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
||||
_logger.SetMinLevel(level);
|
||||
|
||||
// core_-Schema läuft über EF-Migrationen (extern angewendet). Nur ibkr_ (Dapper) noch hier.
|
||||
SetStatus("Migrationen...");
|
||||
try
|
||||
{
|
||||
await _services.GetRequiredService<IBKRMigrations>().RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Core", "IBKR-Datenbankfehler beim Start.", ex);
|
||||
}
|
||||
// Das gesamte Schema (core_ + ct_) läuft über EF-Migrationen, extern via
|
||||
// `dotnet ef database update` angewendet – keine Laufzeit-Migration mehr.
|
||||
|
||||
foreach (var module in _modules)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using IBKRTrader.Core.AI;
|
||||
using IBKRTrader.Core.Budget;
|
||||
using IBKRTrader.Core.Configuration;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Database.Migrations;
|
||||
using IBKRTrader.Core.DependencyInjection;
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Logging;
|
||||
@@ -94,8 +92,6 @@ internal static class Program
|
||||
|
||||
services.AddSingleton<LoggingService>();
|
||||
|
||||
services.AddSingleton<DatabaseService>(); // noch für IBKR-Marktdaten (ibkr_, Dapper)
|
||||
services.AddSingleton<IBKRMigrations>(); // ibkr_-Tabellen (Dapper); core_ läuft über EF
|
||||
services.AddSingleton<CoreSettingsService>(); // core_settings via EF
|
||||
|
||||
services.AddSingleton<IBKRGatewayService>();
|
||||
|
||||
@@ -95,7 +95,9 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin
|
||||
- [x] **Slice 1:** `Configuration/DatabaseOptions` + `DatabaseServerVersion`-Pin (MariaDB 11.8.6); `AddCorePersistence` (`AddDbContextFactory`) + `CoreDbContext` + Entities (core_position, core_trade_history, core_budget, core_worker_log, core_settings) + Design-Time-Factory; **EF-Migration `InitialCore` erzeugt**
|
||||
- [x] **Slice 2:** `PortfolioService`, `BudgetService`, `TradeHistoryService` auf EF (`IDbContextFactory<CoreDbContext>`) umgestellt; **5 EF-InMemory-Unit-Tests** (Buchführung real verifiziert). WorkerBase-Log folgt in Slice 4.
|
||||
- [x] **Slice 3:** Modul auf EF (`CongressTradingDbContext` ct_ + `CongressRepository`); `WorkerBase`-Log auf EF (core_worker_log); `CoreSettingsService` (core_settings); `CoreMigrations`/`CongressMigrations` (Dapper) entfernt; EF-Migration `InitialCongressTrading`; `appsettings.Local.json` (gitignored) als Connection-Quelle; Fallback für leeren Connection-String. **+4 EF-InMemory-Tests** (CongressRepository)
|
||||
- [ ] **Slice 4:** IBKR-Marktdaten (`ibkr_`) + `IBKRMigrations` auf EF; dann restliches Dapper + `DatabaseService` entfernen
|
||||
- [x] **Slice 4:** IBKR-Marktdaten (`core_ibkr_*`) auf EF (3 Entities im `CoreDbContext`, Cross-Modul-Query via Raw-SQL); Migration `AddIbkr`; **Dapper + `DatabaseService` + `IBKRMigrations` vollständig entfernt** – Persistenz ist jetzt **komplett EF Core**. +4 EF-InMemory-Tests.
|
||||
|
||||
**R3 abgeschlossen** ✅ – gesamte Persistenz auf EF Core (Migrationen: `InitialCore`, `AddIbkr`, `InitialCongressTrading`), extern via `dotnet ef database update` anzuwenden.
|
||||
- Hinweis: nur build-verifizierbar (Unit-Tests ohne DB); Schema-Anwendung extern via `dotnet ef database update` (env `IBKRTRADER_MYSQL`)
|
||||
|
||||
### R4 – Trading-Kern einфügen
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
using Dapper;
|
||||
using MySqlConnector;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
namespace IBKRTrader.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Zentraler Datenbankzugriff via MySqlConnector + Dapper.
|
||||
/// Jede Methode öffnet eine eigene Connection (Connection-Pooling via MySqlConnector).
|
||||
/// </summary>
|
||||
public class DatabaseService
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public DatabaseService(SettingsService settings, LoggingService logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
|
||||
// Dapper support for DateOnly
|
||||
SqlMapper.AddTypeHandler(new DateOnlyTypeHandler());
|
||||
}
|
||||
|
||||
private class DateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly>
|
||||
{
|
||||
public override void SetValue(System.Data.IDbDataParameter parameter, DateOnly value)
|
||||
{
|
||||
parameter.Value = value.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
public override DateOnly Parse(object value)
|
||||
{
|
||||
if (value is DateTime dt) return DateOnly.FromDateTime(dt);
|
||||
if (value is string s && DateOnly.TryParse(s, out var d)) return d;
|
||||
return DateOnly.FromDateTime(Convert.ToDateTime(value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Connection ───────────────────────────────────────────────────────────
|
||||
|
||||
public MySqlConnection CreateConnection()
|
||||
=> new(_settings.Settings.Database.BuildConnectionString());
|
||||
|
||||
public async Task<bool> TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var conn = CreateConnection();
|
||||
await conn.OpenAsync();
|
||||
_logger.Info("Core", "Datenbankverbindung erfolgreich hergestellt.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Core", "Datenbankverbindung fehlgeschlagen.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Abfragen (Dapper) ────────────────────────────────────────────────────
|
||||
|
||||
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object? param = null)
|
||||
{
|
||||
await using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
public async Task<T?> QueryFirstOrDefaultAsync<T>(string sql, object? param = null)
|
||||
{
|
||||
await using var conn = CreateConnection();
|
||||
return await conn.QueryFirstOrDefaultAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
public async Task<int> ExecuteAsync(string sql, object? param = null)
|
||||
{
|
||||
await using var conn = CreateConnection();
|
||||
return await conn.ExecuteAsync(sql, param);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteScalarAsync<T>(string sql, object? param = null)
|
||||
{
|
||||
await using var conn = CreateConnection();
|
||||
#pragma warning disable CS8603
|
||||
return await conn.ExecuteScalarAsync<T>(sql, param);
|
||||
#pragma warning restore CS8603
|
||||
}
|
||||
|
||||
// ─── Worker-Log Hilfsmethoden ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>Legt einen neuen Worker-Log-Eintrag an und gibt die neue ID zurück.</summary>
|
||||
public async Task<long> BeginWorkerLogAsync(string workerName, string module)
|
||||
{
|
||||
const string sql =
|
||||
@"INSERT INTO `core_worker_log` (worker_name, module, started_at, status)
|
||||
VALUES (@workerName, @module, @now, 'Running');
|
||||
SELECT LAST_INSERT_ID();";
|
||||
return await ExecuteScalarAsync<long>(sql,
|
||||
new { workerName, module, now = DateTime.UtcNow });
|
||||
}
|
||||
|
||||
/// <summary>Schließt einen Worker-Log-Eintrag ab (Success oder Error).</summary>
|
||||
public async Task EndWorkerLogAsync(long logId, bool success, string? message = null)
|
||||
{
|
||||
const string sql =
|
||||
@"UPDATE `core_worker_log`
|
||||
SET finished_at = @now, status = @status, message = @msg
|
||||
WHERE id = @id";
|
||||
await ExecuteAsync(sql, new
|
||||
{
|
||||
id = logId,
|
||||
now = DateTime.UtcNow,
|
||||
status = success ? "Success" : "Error",
|
||||
msg = message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using IBKRTrader.Core.Logging;
|
||||
|
||||
namespace IBKRTrader.Core.Database.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt die core_ibkr_xxx-Tabellen idempotent (IF NOT EXISTS).
|
||||
/// Wird beim App-Start nach CoreMigrations ausgeführt.
|
||||
/// </summary>
|
||||
public class IBKRMigrations
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public IBKRMigrations(DatabaseService db, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.Info("Core", "Starte IBKR-Datenbankmigrationen...");
|
||||
await CreateInstrumentsTableAsync();
|
||||
await CreateMarketDataTableAsync();
|
||||
await CreateExternalIdentifiersTableAsync();
|
||||
_logger.Info("Core", "IBKR-Migrationen abgeschlossen.");
|
||||
}
|
||||
|
||||
// ─── Tabellen ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Task CreateInstrumentsTableAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_ibkr_instruments` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`ibkr_conid` BIGINT NOT NULL UNIQUE,
|
||||
`symbol` VARCHAR(20) NOT NULL,
|
||||
`sec_type` VARCHAR(10) NOT NULL DEFAULT 'STK',
|
||||
`exchange` VARCHAR(20) NOT NULL DEFAULT 'SMART',
|
||||
`primary_exchange` VARCHAR(20),
|
||||
`currency` VARCHAR(5) NOT NULL DEFAULT 'USD',
|
||||
`company_name` TEXT,
|
||||
`isin` VARCHAR(12),
|
||||
`sector` VARCHAR(100),
|
||||
`industry` VARCHAR(100),
|
||||
`description` TEXT,
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`last_fetched` DATETIME,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_symbol` (`symbol`),
|
||||
INDEX `idx_conid` (`ibkr_conid`),
|
||||
INDEX `idx_active` (`active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateMarketDataTableAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_ibkr_market_data` (
|
||||
`instrument_id` BIGINT NOT NULL,
|
||||
`bar_size` VARCHAR(10) NOT NULL DEFAULT 'daily',
|
||||
`timestamp` DATETIME NOT NULL,
|
||||
`open` DECIMAL(12,6),
|
||||
`high` DECIMAL(12,6),
|
||||
`low` DECIMAL(12,6),
|
||||
`close` DECIMAL(12,6),
|
||||
`volume` BIGINT,
|
||||
`wap` DECIMAL(12,6),
|
||||
`bar_count` INT,
|
||||
PRIMARY KEY (`instrument_id`, `bar_size`, `timestamp`),
|
||||
INDEX `idx_timestamp` (`timestamp`),
|
||||
CONSTRAINT `fk_md_instrument` FOREIGN KEY (`instrument_id`)
|
||||
REFERENCES `core_ibkr_instruments` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateExternalIdentifiersTableAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_ibkr_external_identifiers` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`instrument_id` BIGINT NOT NULL,
|
||||
`source` VARCHAR(50) NOT NULL,
|
||||
`ticker` VARCHAR(50) NOT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_mapping` (`instrument_id`, `source`, `ticker`),
|
||||
INDEX `idx_source_ticker` (`source`, `ticker`),
|
||||
CONSTRAINT `fk_ext_instrument` FOREIGN KEY (`instrument_id`)
|
||||
REFERENCES `core_ibkr_instruments` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
}
|
||||
@@ -1,152 +1,154 @@
|
||||
using Dapper;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IBKRTrader.Core.IBKR;
|
||||
|
||||
/// <summary>
|
||||
/// Datenzugriffsschicht für die core_ibkr_xxx-Tabellen.
|
||||
/// Alle IBKR-Marktdaten-Operationen laufen über diese Klasse.
|
||||
/// Datenzugriffsschicht für die core_ibkr_-Tabellen (EF Core).
|
||||
/// </summary>
|
||||
public class IBKRMarketDataRepository
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
private readonly IDbContextFactory<CoreDbContext> _dbf;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public IBKRMarketDataRepository(DatabaseService db, LoggingService logger)
|
||||
public IBKRMarketDataRepository(IDbContextFactory<CoreDbContext> dbf, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_dbf = dbf;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ─── Instruments ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Legt ein neues Instrument an oder aktualisiert ein bestehendes (UPSERT via conid).
|
||||
/// Gibt die Instrument-ID zurück.
|
||||
/// </summary>
|
||||
/// <summary>Upsert eines Instruments (per ConID). Gibt die Instrument-ID zurück.</summary>
|
||||
public async Task<long> UpsertInstrumentAsync(IBKRInstrument instr)
|
||||
{
|
||||
const string sql = @"
|
||||
INSERT INTO `core_ibkr_instruments`
|
||||
(`ibkr_conid`, `symbol`, `sec_type`, `exchange`, `primary_exchange`,
|
||||
`currency`, `company_name`, `isin`, `sector`, `industry`,
|
||||
`description`, `active`, `last_fetched`)
|
||||
VALUES
|
||||
(@IbkrConid, @Symbol, @SecType, @Exchange, @PrimaryExchange,
|
||||
@Currency, @CompanyName, @Isin, @Sector, @Industry,
|
||||
@Description, @Active, @LastFetched)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`symbol` = VALUES(`symbol`),
|
||||
`sec_type` = VALUES(`sec_type`),
|
||||
`exchange` = VALUES(`exchange`),
|
||||
`primary_exchange` = VALUES(`primary_exchange`),
|
||||
`currency` = VALUES(`currency`),
|
||||
`company_name` = VALUES(`company_name`),
|
||||
`isin` = VALUES(`isin`),
|
||||
`sector` = VALUES(`sector`),
|
||||
`industry` = VALUES(`industry`),
|
||||
`description` = VALUES(`description`),
|
||||
`last_fetched` = VALUES(`last_fetched`);
|
||||
SELECT `id` FROM `core_ibkr_instruments` WHERE `ibkr_conid` = @IbkrConid;";
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var existing = await db.Instruments.FirstOrDefaultAsync(i => i.IbkrConid == instr.IbkrConid);
|
||||
if (existing is null)
|
||||
{
|
||||
instr.CreatedAt = DateTime.UtcNow;
|
||||
instr.UpdatedAt = DateTime.UtcNow;
|
||||
db.Instruments.Add(instr);
|
||||
await db.SaveChangesAsync();
|
||||
return instr.Id;
|
||||
}
|
||||
|
||||
await using var conn = _db.CreateConnection();
|
||||
return await conn.ExecuteScalarAsync<long>(sql, instr);
|
||||
existing.Symbol = instr.Symbol;
|
||||
existing.SecType = instr.SecType;
|
||||
existing.Exchange = instr.Exchange;
|
||||
existing.PrimaryExchange = instr.PrimaryExchange;
|
||||
existing.Currency = instr.Currency;
|
||||
existing.CompanyName = instr.CompanyName;
|
||||
existing.Isin = instr.Isin;
|
||||
existing.Sector = instr.Sector;
|
||||
existing.Industry = instr.Industry;
|
||||
existing.Description = instr.Description;
|
||||
existing.LastFetched = instr.LastFetched;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return existing.Id;
|
||||
}
|
||||
|
||||
/// <summary>Gibt alle aktiven Instrumente zurück.</summary>
|
||||
public Task<IEnumerable<IBKRInstrument>> GetAllActiveInstrumentsAsync()
|
||||
=> _db.QueryAsync<IBKRInstrument>(
|
||||
"SELECT * FROM `core_ibkr_instruments` WHERE `active` = 1 ORDER BY `symbol`");
|
||||
public async Task<IEnumerable<IBKRInstrument>> GetAllActiveInstrumentsAsync()
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
return await db.Instruments.Where(i => i.Active).OrderBy(i => i.Symbol).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>Findet ein Instrument anhand seiner IBKR ConID.</summary>
|
||||
public Task<IBKRInstrument?> GetInstrumentByConidAsync(long conid)
|
||||
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(
|
||||
"SELECT * FROM `core_ibkr_instruments` WHERE `ibkr_conid` = @conid",
|
||||
new { conid });
|
||||
public async Task<IBKRInstrument?> GetInstrumentByConidAsync(long conid)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
return await db.Instruments.FirstOrDefaultAsync(i => i.IbkrConid == conid);
|
||||
}
|
||||
|
||||
/// <summary>Gibt die Anzahl aktiver Instrumente zurück.</summary>
|
||||
public Task<int> GetActiveInstrumentCountAsync()
|
||||
=> _db.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM `core_ibkr_instruments` WHERE `active` = 1");
|
||||
public async Task<int> GetActiveInstrumentCountAsync()
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
return await db.Instruments.CountAsync(i => i.Active);
|
||||
}
|
||||
|
||||
// ─── Market Data ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Fügt Marktdaten-Balken via UPSERT ein (ON DUPLICATE KEY UPDATE).
|
||||
/// </summary>
|
||||
/// <summary>Upsert einer Menge OHLCV-Balken (per PK InstrumentId+BarSize+Timestamp).</summary>
|
||||
public async Task UpsertMarketDataBatchAsync(IEnumerable<IBKRMarketBar> bars)
|
||||
{
|
||||
const string sql = @"
|
||||
INSERT INTO `core_ibkr_market_data`
|
||||
(`instrument_id`, `bar_size`, `timestamp`,
|
||||
`open`, `high`, `low`, `close`, `volume`, `wap`, `bar_count`)
|
||||
VALUES
|
||||
(@InstrumentId, @BarSize, @Timestamp,
|
||||
@Open, @High, @Low, @Close, @Volume, @Wap, @BarCount)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`open` = VALUES(`open`),
|
||||
`high` = VALUES(`high`),
|
||||
`low` = VALUES(`low`),
|
||||
`close` = VALUES(`close`),
|
||||
`volume` = VALUES(`volume`),
|
||||
`wap` = VALUES(`wap`),
|
||||
`bar_count` = VALUES(`bar_count`)";
|
||||
|
||||
await using var conn = _db.CreateConnection();
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync(sql, bars);
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var existing = await db.MarketBars.FindAsync(bar.InstrumentId, bar.BarSize, bar.Timestamp);
|
||||
if (existing is null)
|
||||
{
|
||||
db.MarketBars.Add(bar);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Open = bar.Open;
|
||||
existing.High = bar.High;
|
||||
existing.Low = bar.Low;
|
||||
existing.Close = bar.Close;
|
||||
existing.Volume = bar.Volume;
|
||||
existing.Wap = bar.Wap;
|
||||
existing.BarCount = bar.BarCount;
|
||||
}
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>Gibt den neuesten Timestamp für ein Instrument zurück.</summary>
|
||||
public Task<DateTime?> GetLatestBarTimestampAsync(long instrumentId, string barSize = "daily")
|
||||
=> _db.QueryFirstOrDefaultAsync<DateTime?>(
|
||||
@"SELECT MAX(`timestamp`) FROM `core_ibkr_market_data`
|
||||
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
|
||||
new { instrumentId, barSize });
|
||||
public async Task<DateTime?> GetLatestBarTimestampAsync(long instrumentId, string barSize = "daily")
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var bars = db.MarketBars.Where(m => m.InstrumentId == instrumentId && m.BarSize == barSize);
|
||||
if (!await bars.AnyAsync()) return null;
|
||||
return await bars.MaxAsync(m => m.Timestamp);
|
||||
}
|
||||
|
||||
/// <summary>Gibt die Anzahl Bars für ein Instrument zurück.</summary>
|
||||
public Task<int> GetBarCountAsync(long instrumentId, string barSize = "daily")
|
||||
=> _db.ExecuteScalarAsync<int>(
|
||||
@"SELECT COUNT(*) FROM `core_ibkr_market_data`
|
||||
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
|
||||
new { instrumentId, barSize });
|
||||
public async Task<int> GetBarCountAsync(long instrumentId, string barSize = "daily")
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
return await db.MarketBars.CountAsync(m => m.InstrumentId == instrumentId && m.BarSize == barSize);
|
||||
}
|
||||
|
||||
// ─── External Identifiers ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt oder ignoriert ein External-Identifier-Mapping (IGNORE bei Duplikat).
|
||||
/// </summary>
|
||||
public Task UpsertExternalIdentifierAsync(long instrumentId, string source, string ticker)
|
||||
=> _db.ExecuteAsync(@"
|
||||
INSERT IGNORE INTO `core_ibkr_external_identifiers`
|
||||
(`instrument_id`, `source`, `ticker`)
|
||||
VALUES (@instrumentId, @source, @ticker)",
|
||||
new { instrumentId, source, ticker });
|
||||
/// <summary>Legt ein Ticker-Mapping an (ignoriert Duplikate).</summary>
|
||||
public async Task UpsertExternalIdentifierAsync(long instrumentId, string source, string ticker)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var exists = await db.ExternalIds.AnyAsync(
|
||||
e => e.InstrumentId == instrumentId && e.Source == source && e.Ticker == ticker);
|
||||
if (exists) return;
|
||||
|
||||
db.ExternalIds.Add(new IBKRExternalIdentifier { InstrumentId = instrumentId, Source = source, Ticker = ticker });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IBKRInstrument?> FindInstrumentByExternalTickerAsync(string source, string ticker)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var query =
|
||||
from e in db.ExternalIds
|
||||
join i in db.Instruments on e.InstrumentId equals i.Id
|
||||
where e.Source == source && e.Ticker == ticker
|
||||
select i;
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Findet ein Instrument anhand eines externen Tickers (z.B. aus ct_trade).
|
||||
/// Findet alle Ticker aus ct_trade (Modul-Tabelle) ohne IBKR-Mapping. Cross-Tabellen-Query via
|
||||
/// Raw-SQL, damit der Core den Modul-DbContext nicht referenzieren muss.
|
||||
/// </summary>
|
||||
public Task<IBKRInstrument?> FindInstrumentByExternalTickerAsync(string source, string ticker)
|
||||
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(@"
|
||||
SELECT i.* FROM `core_ibkr_instruments` i
|
||||
INNER JOIN `core_ibkr_external_identifiers` e ON e.`instrument_id` = i.`id`
|
||||
WHERE e.`source` = @source AND e.`ticker` = @ticker
|
||||
LIMIT 1",
|
||||
new { source, ticker });
|
||||
|
||||
/// <summary>
|
||||
/// Findet alle einzigartigen Ticker aus ct_trade, die noch kein IBKR-Mapping haben.
|
||||
/// </summary>
|
||||
public Task<IEnumerable<string>> GetUnmappedTickersFromCongressTradesAsync()
|
||||
=> _db.QueryAsync<string>(@"
|
||||
SELECT DISTINCT t.`ticker`
|
||||
public async Task<IEnumerable<string>> GetUnmappedTickersFromCongressTradesAsync()
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
const string sql = @"
|
||||
SELECT DISTINCT t.`Ticker` AS `Value`
|
||||
FROM `ct_trade` t
|
||||
WHERE t.`ticker` IS NOT NULL
|
||||
AND t.`ticker` != ''
|
||||
WHERE t.`Ticker` IS NOT NULL AND t.`Ticker` <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `core_ibkr_external_identifiers` e
|
||||
WHERE e.`source` = 'capitoltrades' AND e.`ticker` = t.`ticker`
|
||||
)
|
||||
ORDER BY t.`ticker`");
|
||||
WHERE e.`Source` = 'capitoltrades' AND e.`Ticker` = t.`Ticker`)
|
||||
ORDER BY t.`Ticker`";
|
||||
return await db.Database.SqlQueryRaw<string>(sql).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
<!-- EF Core / Pomelo (MariaDB 11.8.6). EF 8 laeuft auf net10. -->
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Persistence.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -17,6 +18,11 @@ public class CoreDbContext : DbContext
|
||||
public DbSet<CoreWorkerLog> WorkerLog => Set<CoreWorkerLog>();
|
||||
public DbSet<CoreSetting> Settings => Set<CoreSetting>();
|
||||
|
||||
// IBKR-Marktdaten
|
||||
public DbSet<IBKRInstrument> Instruments => Set<IBKRInstrument>();
|
||||
public DbSet<IBKRMarketBar> MarketBars => Set<IBKRMarketBar>();
|
||||
public DbSet<IBKRExternalIdentifier> ExternalIds => Set<IBKRExternalIdentifier>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
b.Entity<CorePosition>(e =>
|
||||
@@ -71,5 +77,47 @@ public class CoreDbContext : DbContext
|
||||
e.Property(x => x.Key).HasMaxLength(100);
|
||||
e.Property(x => x.Value).HasColumnType("text");
|
||||
});
|
||||
|
||||
b.Entity<IBKRInstrument>(e =>
|
||||
{
|
||||
e.ToTable("core_ibkr_instruments");
|
||||
e.HasKey(x => x.Id);
|
||||
e.HasIndex(x => x.IbkrConid).IsUnique();
|
||||
e.HasIndex(x => x.Symbol);
|
||||
e.HasIndex(x => x.Active);
|
||||
e.Property(x => x.Symbol).HasMaxLength(20);
|
||||
e.Property(x => x.SecType).HasMaxLength(10);
|
||||
e.Property(x => x.Exchange).HasMaxLength(20);
|
||||
e.Property(x => x.PrimaryExchange).HasMaxLength(20);
|
||||
e.Property(x => x.Currency).HasMaxLength(5);
|
||||
e.Property(x => x.CompanyName).HasColumnType("text");
|
||||
e.Property(x => x.Isin).HasMaxLength(12);
|
||||
e.Property(x => x.Sector).HasMaxLength(100);
|
||||
e.Property(x => x.Industry).HasMaxLength(100);
|
||||
e.Property(x => x.Description).HasColumnType("text");
|
||||
});
|
||||
|
||||
b.Entity<IBKRMarketBar>(e =>
|
||||
{
|
||||
e.ToTable("core_ibkr_market_data");
|
||||
e.HasKey(x => new { x.InstrumentId, x.BarSize, x.Timestamp });
|
||||
e.HasIndex(x => x.Timestamp);
|
||||
e.Property(x => x.BarSize).HasMaxLength(10);
|
||||
e.Property(x => x.Open).HasPrecision(12, 6);
|
||||
e.Property(x => x.High).HasPrecision(12, 6);
|
||||
e.Property(x => x.Low).HasPrecision(12, 6);
|
||||
e.Property(x => x.Close).HasPrecision(12, 6);
|
||||
e.Property(x => x.Wap).HasPrecision(12, 6);
|
||||
});
|
||||
|
||||
b.Entity<IBKRExternalIdentifier>(e =>
|
||||
{
|
||||
e.ToTable("core_ibkr_external_identifiers");
|
||||
e.HasKey(x => x.Id);
|
||||
e.HasIndex(x => new { x.InstrumentId, x.Source, x.Ticker }).IsUnique();
|
||||
e.HasIndex(x => new { x.Source, x.Ticker });
|
||||
e.Property(x => x.Source).HasMaxLength(50);
|
||||
e.Property(x => x.Ticker).HasMaxLength(50);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
[DbContext(typeof(CoreDbContext))]
|
||||
[Migration("20260728162301_AddIbkr")]
|
||||
partial class AddIbkr
|
||||
{
|
||||
/// <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("IBKRTrader.Core.IBKR.IBKRExternalIdentifier", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Ticker")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Source", "Ticker");
|
||||
|
||||
b.HasIndex("InstrumentId", "Source", "Ticker")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("core_ibkr_external_identifiers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRInstrument", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Exchange")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<long>("IbkrConid")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Industry")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("varchar(12)");
|
||||
|
||||
b.Property<DateTime?>("LastFetched")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("PrimaryExchange")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("SecType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Active");
|
||||
|
||||
b.HasIndex("IbkrConid")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Symbol");
|
||||
|
||||
b.ToTable("core_ibkr_instruments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRMarketBar", b =>
|
||||
{
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("BarSize")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("BarCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Close")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("High")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Low")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Open")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<long>("Volume")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("Wap")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.HasKey("InstrumentId", "BarSize", "Timestamp");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("core_ibkr_market_data", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b =>
|
||||
{
|
||||
b.Property<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<decimal>("MaxPerTrade")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TotalBudget")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("UsedBudget")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("Module");
|
||||
|
||||
b.ToTable("core_budget", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CorePosition", b =>
|
||||
{
|
||||
b.Property<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<decimal>("AvgPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Module", "Symbol");
|
||||
|
||||
b.ToTable("core_position", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("core_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("IbkrOrderId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<decimal>("TotalValue")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<DateTime>("TradedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Module");
|
||||
|
||||
b.HasIndex("Symbol");
|
||||
|
||||
b.ToTable("core_trade_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreWorkerLog", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("WorkerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkerName", "StartedAt");
|
||||
|
||||
b.ToTable("core_worker_log", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIbkr : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "core_ibkr_external_identifiers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
InstrumentId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Source = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Ticker = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_core_ibkr_external_identifiers", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "core_ibkr_instruments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
IbkrConid = table.Column<long>(type: "bigint", nullable: false),
|
||||
Symbol = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SecType = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Exchange = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PrimaryExchange = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Currency = table.Column<string>(type: "varchar(5)", maxLength: 5, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CompanyName = table.Column<string>(type: "text", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Isin = table.Column<string>(type: "varchar(12)", maxLength: 12, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Sector = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Industry = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Description = table.Column<string>(type: "text", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Active = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
LastFetched = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_core_ibkr_instruments", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "core_ibkr_market_data",
|
||||
columns: table => new
|
||||
{
|
||||
InstrumentId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BarSize = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Open = table.Column<decimal>(type: "decimal(12,6)", precision: 12, scale: 6, nullable: false),
|
||||
High = table.Column<decimal>(type: "decimal(12,6)", precision: 12, scale: 6, nullable: false),
|
||||
Low = table.Column<decimal>(type: "decimal(12,6)", precision: 12, scale: 6, nullable: false),
|
||||
Close = table.Column<decimal>(type: "decimal(12,6)", precision: 12, scale: 6, nullable: false),
|
||||
Volume = table.Column<long>(type: "bigint", nullable: false),
|
||||
Wap = table.Column<decimal>(type: "decimal(12,6)", precision: 12, scale: 6, nullable: true),
|
||||
BarCount = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_core_ibkr_market_data", x => new { x.InstrumentId, x.BarSize, x.Timestamp });
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_external_identifiers_InstrumentId_Source_Ticker",
|
||||
table: "core_ibkr_external_identifiers",
|
||||
columns: new[] { "InstrumentId", "Source", "Ticker" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_external_identifiers_Source_Ticker",
|
||||
table: "core_ibkr_external_identifiers",
|
||||
columns: new[] { "Source", "Ticker" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_instruments_Active",
|
||||
table: "core_ibkr_instruments",
|
||||
column: "Active");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_instruments_IbkrConid",
|
||||
table: "core_ibkr_instruments",
|
||||
column: "IbkrConid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_instruments_Symbol",
|
||||
table: "core_ibkr_instruments",
|
||||
column: "Symbol");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_ibkr_market_data_Timestamp",
|
||||
table: "core_ibkr_market_data",
|
||||
column: "Timestamp");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "core_ibkr_external_identifiers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "core_ibkr_instruments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "core_ibkr_market_data");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,159 @@ namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRExternalIdentifier", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Ticker")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Source", "Ticker");
|
||||
|
||||
b.HasIndex("InstrumentId", "Source", "Ticker")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("core_ibkr_external_identifiers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRInstrument", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Exchange")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<long>("IbkrConid")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Industry")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("varchar(12)");
|
||||
|
||||
b.Property<DateTime?>("LastFetched")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("PrimaryExchange")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("SecType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Active");
|
||||
|
||||
b.HasIndex("IbkrConid")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Symbol");
|
||||
|
||||
b.ToTable("core_ibkr_instruments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRMarketBar", b =>
|
||||
{
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("BarSize")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("BarCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Close")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("High")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Low")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Open")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<long>("Volume")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("Wap")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.HasKey("InstrumentId", "BarSize", "Timestamp");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("core_ibkr_market_data", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b =>
|
||||
{
|
||||
b.Property<string>("Module")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Modules.CongressTrading.Database;
|
||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Modules.CongressTrading.Database;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IBKRTrader.Tests.IBKR;
|
||||
|
||||
/// <summary>IBKR-Repo gegen EF-InMemory (Raw-SQL-Cross-Modul-Query wird hier nicht getestet).</summary>
|
||||
[Trait("cat", "unit")]
|
||||
public class IBKRMarketDataRepositoryTests
|
||||
{
|
||||
private sealed class Factory(DbContextOptions<CoreDbContext> o) : IDbContextFactory<CoreDbContext>
|
||||
{
|
||||
public CoreDbContext CreateDbContext() => new(o);
|
||||
}
|
||||
|
||||
private static IBKRMarketDataRepository CreateSut()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<CoreDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
|
||||
return new IBKRMarketDataRepository(new Factory(opts), new LoggingService());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertInstrument_IsIdempotent_ByConid_AndReturnsId()
|
||||
{
|
||||
var repo = CreateSut();
|
||||
|
||||
var id1 = await repo.UpsertInstrumentAsync(new IBKRInstrument { IbkrConid = 111, Symbol = "AAPL" });
|
||||
var id2 = await repo.UpsertInstrumentAsync(new IBKRInstrument { IbkrConid = 111, Symbol = "AAPL2" });
|
||||
|
||||
id2.Should().Be(id1);
|
||||
(await repo.GetActiveInstrumentCountAsync()).Should().Be(1);
|
||||
(await repo.GetInstrumentByConidAsync(111))!.Symbol.Should().Be("AAPL2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExternalIdentifier_Dedup_AndLookup()
|
||||
{
|
||||
var repo = CreateSut();
|
||||
var id = await repo.UpsertInstrumentAsync(new IBKRInstrument { IbkrConid = 222, Symbol = "MSFT" });
|
||||
|
||||
await repo.UpsertExternalIdentifierAsync(id, "capitoltrades", "MSFT");
|
||||
await repo.UpsertExternalIdentifierAsync(id, "capitoltrades", "MSFT"); // Duplikat
|
||||
|
||||
var found = await repo.FindInstrumentByExternalTickerAsync("capitoltrades", "MSFT");
|
||||
found.Should().NotBeNull();
|
||||
found!.IbkrConid.Should().Be(222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarketData_Upsert_Count_AndLatest()
|
||||
{
|
||||
var repo = CreateSut();
|
||||
|
||||
await repo.UpsertMarketDataBatchAsync(new[]
|
||||
{
|
||||
new IBKRMarketBar { InstrumentId = 1, BarSize = "daily", Timestamp = new DateTime(2026, 1, 1), Close = 10m },
|
||||
new IBKRMarketBar { InstrumentId = 1, BarSize = "daily", Timestamp = new DateTime(2026, 1, 2), Close = 11m },
|
||||
});
|
||||
// Upsert desselben Keys aktualisiert (kein zweiter Eintrag).
|
||||
await repo.UpsertMarketDataBatchAsync(new[]
|
||||
{
|
||||
new IBKRMarketBar { InstrumentId = 1, BarSize = "daily", Timestamp = new DateTime(2026, 1, 2), Close = 12m },
|
||||
});
|
||||
|
||||
(await repo.GetBarCountAsync(1)).Should().Be(2);
|
||||
(await repo.GetLatestBarTimestampAsync(1)).Should().Be(new DateTime(2026, 1, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LatestBarTimestamp_IsNull_WhenNoData()
|
||||
{
|
||||
var repo = CreateSut();
|
||||
(await repo.GetLatestBarTimestampAsync(999)).Should().BeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user