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:
@@ -0,0 +1,80 @@
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
|
||||
namespace IBKRTrader.Modules.CongressTrading.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Legt die ct_xxx-Tabellen idempotent an.
|
||||
/// Namensschema: ct_{tabellenname} gemäß Architekturregeln.
|
||||
/// </summary>
|
||||
public class CongressMigrations
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public CongressMigrations(DatabaseService db, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.Info("CT", "Starte CongressTrading-Datenbankmigrationen...");
|
||||
await CreateCongressMemberTableAsync();
|
||||
await CreateCongressTradeTableAsync();
|
||||
_logger.Info("CT", "CongressTrading-Migrationen abgeschlossen.");
|
||||
}
|
||||
|
||||
private Task CreateCongressMemberTableAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `ct_congressMember` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`bio_id` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`name` VARCHAR(200),
|
||||
`party` VARCHAR(50),
|
||||
`state` VARCHAR(50),
|
||||
`chamber` VARCHAR(10),
|
||||
`profile_url` VARCHAR(500),
|
||||
`first_seen` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_updated` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_bio_id` (`bio_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private async Task CreateCongressTradeTableAsync()
|
||||
{
|
||||
// Tabelle anlegen
|
||||
await _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `ct_trade` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`trade_id` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`member_bio_id` VARCHAR(20) NOT NULL,
|
||||
`issuer_name` VARCHAR(500),
|
||||
`issuer_id` VARCHAR(20),
|
||||
`ticker` VARCHAR(20),
|
||||
`trade_type` VARCHAR(50),
|
||||
`chamber` VARCHAR(20),
|
||||
`owner` VARCHAR(50),
|
||||
`value` DECIMAL(18,2),
|
||||
`size_range_low` BIGINT,
|
||||
`size_range_high` BIGINT,
|
||||
`trade_date` DATE,
|
||||
`published_date` DATE,
|
||||
`detail_url` VARCHAR(500),
|
||||
`details_fetched` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`scraped_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_member` (`member_bio_id`),
|
||||
INDEX `idx_trade_date` (`trade_date`),
|
||||
INDEX `idx_ticker` (`ticker`),
|
||||
INDEX `idx_details` (`details_fetched`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
// Idempotente Spalten-Migrationen (für bestehende DBs)
|
||||
await _db.ExecuteAsync("ALTER TABLE `ct_trade` ADD COLUMN IF NOT EXISTS `details_fetched` TINYINT(1) NOT NULL DEFAULT 1;");
|
||||
await _db.ExecuteAsync("ALTER TABLE `ct_trade` ADD COLUMN IF NOT EXISTS `chamber` VARCHAR(20);");
|
||||
await _db.ExecuteAsync("ALTER TABLE `ct_trade` ADD COLUMN IF NOT EXISTS `owner` VARCHAR(50);");
|
||||
await _db.ExecuteAsync("ALTER TABLE `ct_trade` ADD COLUMN IF NOT EXISTS `value` DECIMAL(18,2);");
|
||||
await _db.ExecuteAsync("ALTER TABLE `ct_trade` ADD INDEX IF NOT EXISTS `idx_details` (`details_fetched`);");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using Dapper;
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Modules.CongressTrading.Models;
|
||||
|
||||
namespace IBKRTrader.Modules.CongressTrading.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Datenbankzugriff für das CongressTrading-Modul.
|
||||
/// Kapselt alle INSERT/SELECT auf ct_congressMember und ct_trade.
|
||||
/// </summary>
|
||||
public class CongressRepository
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public CongressRepository(DatabaseService db, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ─── CongressMember ───────────────────────────────────────────────────────
|
||||
|
||||
public async Task<bool> MemberExistsAsync(string bioId)
|
||||
{
|
||||
var count = await _db.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM `ct_congressMember` WHERE bio_id = @bioId",
|
||||
new { bioId });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task UpsertMemberAsync(CongressMember member)
|
||||
{
|
||||
await _db.ExecuteAsync(@"
|
||||
INSERT INTO `ct_congressMember` (bio_id, name, party, state, chamber, profile_url)
|
||||
VALUES (@BioId, @Name, @Party, @State, @Chamber, @ProfileUrl)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
party = VALUES(party),
|
||||
state = VALUES(state),
|
||||
chamber = VALUES(chamber),
|
||||
profile_url = VALUES(profile_url),
|
||||
last_updated = CURRENT_TIMESTAMP", member);
|
||||
}
|
||||
|
||||
public async Task<HashSet<string>> GetAllMemberBioIdsAsync()
|
||||
{
|
||||
var ids = await _db.QueryAsync<string>(
|
||||
"SELECT bio_id FROM `ct_congressMember`");
|
||||
return ids.ToHashSet();
|
||||
}
|
||||
|
||||
// ─── CongressTrade ────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<bool> TradeExistsAsync(string tradeId)
|
||||
{
|
||||
var count = await _db.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM `ct_trade` WHERE trade_id = @tradeId",
|
||||
new { tradeId });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task<HashSet<string>> GetAllTradeIdsAsync()
|
||||
{
|
||||
var ids = await _db.QueryAsync<string>(
|
||||
"SELECT trade_id FROM `ct_trade`");
|
||||
return ids.ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>Fügt einen Trade ein. Ignoriert Duplikate (INSERT IGNORE).</summary>
|
||||
public async Task InsertTradeAsync(CongressTrade trade, bool detailsFetched = true)
|
||||
{
|
||||
await _db.ExecuteAsync(@"
|
||||
INSERT IGNORE INTO `ct_trade`
|
||||
(trade_id, member_bio_id, issuer_name, issuer_id, ticker,
|
||||
trade_type, chamber, owner, value,
|
||||
trade_date, published_date, detail_url, details_fetched)
|
||||
VALUES
|
||||
(@TradeId, @MemberBioId, @IssuerName, @IssuerId, @Ticker,
|
||||
@TradeType, @Chamber, @Owner, @Value,
|
||||
@TradeDate, @PublishedDate, @DetailUrl, @DetailsFetched)",
|
||||
new
|
||||
{
|
||||
trade.TradeId,
|
||||
trade.MemberBioId,
|
||||
trade.IssuerName,
|
||||
trade.IssuerId,
|
||||
trade.Ticker,
|
||||
trade.TradeType,
|
||||
trade.Chamber,
|
||||
trade.Owner,
|
||||
trade.Value,
|
||||
TradeDate = trade.TradeDate?.ToString("yyyy-MM-dd"),
|
||||
PublishedDate = trade.PublishedDate?.ToString("yyyy-MM-dd"),
|
||||
trade.DetailUrl,
|
||||
DetailsFetched = detailsFetched ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Aktualisiert die Detail-Felder eines bereits vorhandenen Trades.</summary>
|
||||
public async Task UpdateTradeDetailsAsync(CongressTrade trade)
|
||||
{
|
||||
await _db.ExecuteAsync(@"
|
||||
UPDATE `ct_trade`
|
||||
SET ticker = @Ticker,
|
||||
trade_type = @TradeType,
|
||||
trade_date = @TradeDate,
|
||||
published_date = @PublishedDate,
|
||||
details_fetched = 1
|
||||
WHERE trade_id = @TradeId
|
||||
AND details_fetched = 0",
|
||||
new
|
||||
{
|
||||
trade.TradeId,
|
||||
trade.Ticker,
|
||||
trade.TradeType,
|
||||
TradeDate = trade.TradeDate?.ToString("yyyy-MM-dd"),
|
||||
PublishedDate = trade.PublishedDate?.ToString("yyyy-MM-dd")
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Gibt Trade-IDs zurück für die noch keine Details geholt wurden.</summary>
|
||||
public async Task<IEnumerable<string>> GetTradesWithoutDetailsAsync(int limit = 500)
|
||||
=> await _db.QueryAsync<string>(
|
||||
"SELECT trade_id FROM `ct_trade` WHERE details_fetched = 0 LIMIT @limit",
|
||||
new { limit });
|
||||
|
||||
public async Task<int> GetTradeCountAsync()
|
||||
=> await _db.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM `ct_trade`");
|
||||
|
||||
public async Task<int> GetMemberCountAsync()
|
||||
=> await _db.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM `ct_congressMember`");
|
||||
|
||||
// ─── core_settings Flags ──────────────────────────────────────────────────
|
||||
|
||||
public async Task<string?> GetSettingAsync(string key)
|
||||
{
|
||||
return await _db.QueryFirstOrDefaultAsync<string>(
|
||||
"SELECT `value` FROM `core_settings` WHERE `key` = @key",
|
||||
new { key });
|
||||
}
|
||||
|
||||
public async Task SetSettingAsync(string key, string value)
|
||||
{
|
||||
await _db.ExecuteAsync(@"
|
||||
INSERT INTO `core_settings` (`key`, `value`)
|
||||
VALUES (@key, @value)
|
||||
ON DUPLICATE KEY UPDATE `value` = @value",
|
||||
new { key, value });
|
||||
}
|
||||
|
||||
public async Task DeleteSettingAsync(string key)
|
||||
{
|
||||
await _db.ExecuteAsync(
|
||||
"DELETE FROM `core_settings` WHERE `key` = @key", new { key });
|
||||
}
|
||||
|
||||
// ─── Reset ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Löscht alle CT-Fortschritts-Flags und alle CT-Daten aus der DB.
|
||||
/// Danach startet der History-Import beim nächsten Worker-Run von Seite 1.
|
||||
/// </summary>
|
||||
public async Task ResetHistoryImportAsync()
|
||||
{
|
||||
await DeleteSettingAsync("ct.history_import_done");
|
||||
await DeleteSettingAsync("ct.history_import_page");
|
||||
await DeleteSettingAsync("ct.history_import_total");
|
||||
await _db.ExecuteAsync("DELETE FROM `ct_trade`");
|
||||
await _db.ExecuteAsync("DELETE FROM `ct_congressMember`");
|
||||
_logger.Warn("CT", "History-Import-Reset durchgeführt – alle CT-Daten gelöscht.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user