Initial commit: IBKRTrader

.NET WinForms-Anwendung (Core, Modules/CongressTrading, UI).
Enthaelt .gitignore und settings.example.json als Konfigurationsvorlage.
Echte settings.json mit Zugangsdaten ist bewusst ausgeschlossen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-26 18:19:47 +02:00
co-authored by Claude Opus 4.8
commit ebeb035e92
47 changed files with 4527 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
using Dapper;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
namespace IBKRTrader.Core.IBKR;
/// <summary>
/// Datenzugriffsschicht für die core_ibkr_xxx-Tabellen.
/// Alle IBKR-Marktdaten-Operationen laufen über diese Klasse.
/// </summary>
public class IBKRMarketDataRepository
{
private readonly DatabaseService _db;
private readonly LoggingService _logger;
public IBKRMarketDataRepository(DatabaseService db, LoggingService logger)
{
_db = db;
_logger = logger;
}
// ─── Instruments ─────────────────────────────────────────────────────────
/// <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)
{
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 conn = _db.CreateConnection();
return await conn.ExecuteScalarAsync<long>(sql, instr);
}
/// <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`");
/// <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 });
/// <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");
// ─── Market Data ─────────────────────────────────────────────────────────
/// <summary>
/// Fügt Marktdaten-Balken via UPSERT ein (ON DUPLICATE KEY UPDATE).
/// </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);
}
/// <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 });
/// <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 });
// ─── 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>
/// Findet ein Instrument anhand eines externen Tickers (z.B. aus ct_trade).
/// </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`
FROM `ct_trade` t
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`");
}