@
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.Logging;
|
||||||
using IBKRTrader.Core.Modularity;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
@@ -131,16 +130,8 @@ public sealed class LauncherForm : Form
|
|||||||
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
||||||
_logger.SetMinLevel(level);
|
_logger.SetMinLevel(level);
|
||||||
|
|
||||||
// core_-Schema läuft über EF-Migrationen (extern angewendet). Nur ibkr_ (Dapper) noch hier.
|
// Das gesamte Schema (core_ + ct_) läuft über EF-Migrationen, extern via
|
||||||
SetStatus("Migrationen...");
|
// `dotnet ef database update` angewendet – keine Laufzeit-Migration mehr.
|
||||||
try
|
|
||||||
{
|
|
||||||
await _services.GetRequiredService<IBKRMigrations>().RunAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error("Core", "IBKR-Datenbankfehler beim Start.", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var module in _modules)
|
foreach (var module in _modules)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using IBKRTrader.Core.AI;
|
using IBKRTrader.Core.AI;
|
||||||
using IBKRTrader.Core.Budget;
|
using IBKRTrader.Core.Budget;
|
||||||
using IBKRTrader.Core.Configuration;
|
using IBKRTrader.Core.Configuration;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Database.Migrations;
|
|
||||||
using IBKRTrader.Core.DependencyInjection;
|
using IBKRTrader.Core.DependencyInjection;
|
||||||
using IBKRTrader.Core.IBKR;
|
using IBKRTrader.Core.IBKR;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
@@ -94,8 +92,6 @@ internal static class Program
|
|||||||
|
|
||||||
services.AddSingleton<LoggingService>();
|
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<CoreSettingsService>(); // core_settings via EF
|
||||||
|
|
||||||
services.AddSingleton<IBKRGatewayService>();
|
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 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 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)
|
- [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`)
|
- Hinweis: nur build-verifizierbar (Unit-Tests ohne DB); Schema-Anwendung extern via `dotnet ef database update` (env `IBKRTRADER_MYSQL`)
|
||||||
|
|
||||||
### R4 – Trading-Kern einфügen
|
### 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.Logging;
|
||||||
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace IBKRTrader.Core.IBKR;
|
namespace IBKRTrader.Core.IBKR;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Datenzugriffsschicht für die core_ibkr_xxx-Tabellen.
|
/// Datenzugriffsschicht für die core_ibkr_-Tabellen (EF Core).
|
||||||
/// Alle IBKR-Marktdaten-Operationen laufen über diese Klasse.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class IBKRMarketDataRepository
|
public class IBKRMarketDataRepository
|
||||||
{
|
{
|
||||||
private readonly DatabaseService _db;
|
private readonly IDbContextFactory<CoreDbContext> _dbf;
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
public IBKRMarketDataRepository(DatabaseService db, LoggingService logger)
|
public IBKRMarketDataRepository(IDbContextFactory<CoreDbContext> dbf, LoggingService logger)
|
||||||
{
|
{
|
||||||
_db = db;
|
_dbf = dbf;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Instruments ─────────────────────────────────────────────────────────
|
// ─── Instruments ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Upsert eines Instruments (per ConID). Gibt die Instrument-ID zurück.</summary>
|
||||||
/// Legt ein neues Instrument an oder aktualisiert ein bestehendes (UPSERT via conid).
|
|
||||||
/// Gibt die Instrument-ID zurück.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<long> UpsertInstrumentAsync(IBKRInstrument instr)
|
public async Task<long> UpsertInstrumentAsync(IBKRInstrument instr)
|
||||||
{
|
{
|
||||||
const string sql = @"
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
INSERT INTO `core_ibkr_instruments`
|
var existing = await db.Instruments.FirstOrDefaultAsync(i => i.IbkrConid == instr.IbkrConid);
|
||||||
(`ibkr_conid`, `symbol`, `sec_type`, `exchange`, `primary_exchange`,
|
if (existing is null)
|
||||||
`currency`, `company_name`, `isin`, `sector`, `industry`,
|
{
|
||||||
`description`, `active`, `last_fetched`)
|
instr.CreatedAt = DateTime.UtcNow;
|
||||||
VALUES
|
instr.UpdatedAt = DateTime.UtcNow;
|
||||||
(@IbkrConid, @Symbol, @SecType, @Exchange, @PrimaryExchange,
|
db.Instruments.Add(instr);
|
||||||
@Currency, @CompanyName, @Isin, @Sector, @Industry,
|
await db.SaveChangesAsync();
|
||||||
@Description, @Active, @LastFetched)
|
return instr.Id;
|
||||||
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 conn = _db.CreateConnection();
|
existing.Symbol = instr.Symbol;
|
||||||
return await conn.ExecuteScalarAsync<long>(sql, instr);
|
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 async Task<IEnumerable<IBKRInstrument>> GetAllActiveInstrumentsAsync()
|
||||||
public Task<IEnumerable<IBKRInstrument>> GetAllActiveInstrumentsAsync()
|
{
|
||||||
=> _db.QueryAsync<IBKRInstrument>(
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
"SELECT * FROM `core_ibkr_instruments` WHERE `active` = 1 ORDER BY `symbol`");
|
return await db.Instruments.Where(i => i.Active).OrderBy(i => i.Symbol).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Findet ein Instrument anhand seiner IBKR ConID.</summary>
|
public async Task<IBKRInstrument?> GetInstrumentByConidAsync(long conid)
|
||||||
public Task<IBKRInstrument?> GetInstrumentByConidAsync(long conid)
|
{
|
||||||
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
"SELECT * FROM `core_ibkr_instruments` WHERE `ibkr_conid` = @conid",
|
return await db.Instruments.FirstOrDefaultAsync(i => i.IbkrConid == conid);
|
||||||
new { conid });
|
}
|
||||||
|
|
||||||
/// <summary>Gibt die Anzahl aktiver Instrumente zurück.</summary>
|
public async Task<int> GetActiveInstrumentCountAsync()
|
||||||
public Task<int> GetActiveInstrumentCountAsync()
|
{
|
||||||
=> _db.ExecuteScalarAsync<int>(
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
"SELECT COUNT(*) FROM `core_ibkr_instruments` WHERE `active` = 1");
|
return await db.Instruments.CountAsync(i => i.Active);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Market Data ─────────────────────────────────────────────────────────
|
// ─── Market Data ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Upsert einer Menge OHLCV-Balken (per PK InstrumentId+BarSize+Timestamp).</summary>
|
||||||
/// Fügt Marktdaten-Balken via UPSERT ein (ON DUPLICATE KEY UPDATE).
|
|
||||||
/// </summary>
|
|
||||||
public async Task UpsertMarketDataBatchAsync(IEnumerable<IBKRMarketBar> bars)
|
public async Task UpsertMarketDataBatchAsync(IEnumerable<IBKRMarketBar> bars)
|
||||||
{
|
{
|
||||||
const string sql = @"
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
INSERT INTO `core_ibkr_market_data`
|
foreach (var bar in bars)
|
||||||
(`instrument_id`, `bar_size`, `timestamp`,
|
{
|
||||||
`open`, `high`, `low`, `close`, `volume`, `wap`, `bar_count`)
|
var existing = await db.MarketBars.FindAsync(bar.InstrumentId, bar.BarSize, bar.Timestamp);
|
||||||
VALUES
|
if (existing is null)
|
||||||
(@InstrumentId, @BarSize, @Timestamp,
|
{
|
||||||
@Open, @High, @Low, @Close, @Volume, @Wap, @BarCount)
|
db.MarketBars.Add(bar);
|
||||||
ON DUPLICATE KEY UPDATE
|
}
|
||||||
`open` = VALUES(`open`),
|
else
|
||||||
`high` = VALUES(`high`),
|
{
|
||||||
`low` = VALUES(`low`),
|
existing.Open = bar.Open;
|
||||||
`close` = VALUES(`close`),
|
existing.High = bar.High;
|
||||||
`volume` = VALUES(`volume`),
|
existing.Low = bar.Low;
|
||||||
`wap` = VALUES(`wap`),
|
existing.Close = bar.Close;
|
||||||
`bar_count` = VALUES(`bar_count`)";
|
existing.Volume = bar.Volume;
|
||||||
|
existing.Wap = bar.Wap;
|
||||||
await using var conn = _db.CreateConnection();
|
existing.BarCount = bar.BarCount;
|
||||||
await conn.OpenAsync();
|
}
|
||||||
await conn.ExecuteAsync(sql, bars);
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Gibt den neuesten Timestamp für ein Instrument zurück.</summary>
|
public async Task<DateTime?> GetLatestBarTimestampAsync(long instrumentId, string barSize = "daily")
|
||||||
public Task<DateTime?> GetLatestBarTimestampAsync(long instrumentId, string barSize = "daily")
|
{
|
||||||
=> _db.QueryFirstOrDefaultAsync<DateTime?>(
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
@"SELECT MAX(`timestamp`) FROM `core_ibkr_market_data`
|
var bars = db.MarketBars.Where(m => m.InstrumentId == instrumentId && m.BarSize == barSize);
|
||||||
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
|
if (!await bars.AnyAsync()) return null;
|
||||||
new { instrumentId, barSize });
|
return await bars.MaxAsync(m => m.Timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Gibt die Anzahl Bars für ein Instrument zurück.</summary>
|
public async Task<int> GetBarCountAsync(long instrumentId, string barSize = "daily")
|
||||||
public Task<int> GetBarCountAsync(long instrumentId, string barSize = "daily")
|
{
|
||||||
=> _db.ExecuteScalarAsync<int>(
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
@"SELECT COUNT(*) FROM `core_ibkr_market_data`
|
return await db.MarketBars.CountAsync(m => m.InstrumentId == instrumentId && m.BarSize == barSize);
|
||||||
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
|
}
|
||||||
new { instrumentId, barSize });
|
|
||||||
|
|
||||||
// ─── External Identifiers ────────────────────────────────────────────────
|
// ─── External Identifiers ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Legt ein Ticker-Mapping an (ignoriert Duplikate).</summary>
|
||||||
/// Erstellt oder ignoriert ein External-Identifier-Mapping (IGNORE bei Duplikat).
|
public async Task UpsertExternalIdentifierAsync(long instrumentId, string source, string ticker)
|
||||||
/// </summary>
|
{
|
||||||
public Task UpsertExternalIdentifierAsync(long instrumentId, string source, string ticker)
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
=> _db.ExecuteAsync(@"
|
var exists = await db.ExternalIds.AnyAsync(
|
||||||
INSERT IGNORE INTO `core_ibkr_external_identifiers`
|
e => e.InstrumentId == instrumentId && e.Source == source && e.Ticker == ticker);
|
||||||
(`instrument_id`, `source`, `ticker`)
|
if (exists) return;
|
||||||
VALUES (@instrumentId, @source, @ticker)",
|
|
||||||
new { instrumentId, source, ticker });
|
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>
|
/// <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>
|
/// </summary>
|
||||||
public Task<IBKRInstrument?> FindInstrumentByExternalTickerAsync(string source, string ticker)
|
public async Task<IEnumerable<string>> GetUnmappedTickersFromCongressTradesAsync()
|
||||||
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(@"
|
{
|
||||||
SELECT i.* FROM `core_ibkr_instruments` i
|
await using var db = await _dbf.CreateDbContextAsync();
|
||||||
INNER JOIN `core_ibkr_external_identifiers` e ON e.`instrument_id` = i.`id`
|
const string sql = @"
|
||||||
WHERE e.`source` = @source AND e.`ticker` = @ticker
|
SELECT DISTINCT t.`Ticker` AS `Value`
|
||||||
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`
|
|
||||||
FROM `ct_trade` t
|
FROM `ct_trade` t
|
||||||
WHERE t.`ticker` IS NOT NULL
|
WHERE t.`Ticker` IS NOT NULL AND t.`Ticker` <> ''
|
||||||
AND t.`ticker` != ''
|
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM `core_ibkr_external_identifiers` e
|
SELECT 1 FROM `core_ibkr_external_identifiers` e
|
||||||
WHERE e.`source` = 'capitoltrades' AND e.`ticker` = t.`ticker`
|
WHERE e.`Source` = 'capitoltrades' AND e.`Ticker` = t.`Ticker`)
|
||||||
)
|
ORDER BY t.`Ticker`";
|
||||||
ORDER BY t.`ticker`");
|
return await db.Database.SqlQueryRaw<string>(sql).ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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.DependencyInjection.Abstractions" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.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. -->
|
<!-- 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 IBKRTrader.Core.Persistence.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -17,6 +18,11 @@ public class CoreDbContext : DbContext
|
|||||||
public DbSet<CoreWorkerLog> WorkerLog => Set<CoreWorkerLog>();
|
public DbSet<CoreWorkerLog> WorkerLog => Set<CoreWorkerLog>();
|
||||||
public DbSet<CoreSetting> Settings => Set<CoreSetting>();
|
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)
|
protected override void OnModelCreating(ModelBuilder b)
|
||||||
{
|
{
|
||||||
b.Entity<CorePosition>(e =>
|
b.Entity<CorePosition>(e =>
|
||||||
@@ -71,5 +77,47 @@ public class CoreDbContext : DbContext
|
|||||||
e.Property(x => x.Key).HasMaxLength(100);
|
e.Property(x => x.Key).HasMaxLength(100);
|
||||||
e.Property(x => x.Value).HasColumnType("text");
|
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);
|
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 =>
|
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Module")
|
b.Property<string>("Module")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using IBKRTrader.Core.Persistence.Ef;
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using IBKRTrader.Core.Persistence.Ef;
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.IBKR;
|
using IBKRTrader.Core.IBKR;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using IBKRTrader.Core.Persistence.Ef;
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.IBKR;
|
using IBKRTrader.Core.IBKR;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using IBKRTrader.Core.Persistence.Ef;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using IBKRTrader.Core.Persistence.Ef;
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using IBKRTrader.Core.Persistence.Ef;
|
using IBKRTrader.Core.Persistence.Ef;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IBKRTrader.Core.Database;
|
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
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