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,119 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using IBKRTrader.Core.Logging;
|
||||
|
||||
namespace IBKRTrader.Core.Database.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt alle core_xxx-Tabellen idempotent (IF NOT EXISTS).
|
||||
/// Wird einmalig beim App-Start ausgeführt.
|
||||
/// </summary>
|
||||
public class CoreMigrations
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public CoreMigrations(DatabaseService db, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.Info("Core", "Starte Core-Datenbankmigrationen...");
|
||||
await CreateCoreSettingsAsync();
|
||||
await CreateCoreWorkerLogAsync();
|
||||
await CreateCoreTradeHistoryAsync();
|
||||
await CreateCoreBudgetAsync();
|
||||
_logger.Info("Core", "Core-Migrationen abgeschlossen.");
|
||||
}
|
||||
|
||||
// ─── Tabellen ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Task CreateCoreSettingsAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_settings` (
|
||||
`key` VARCHAR(100) NOT NULL PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreWorkerLogAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_worker_log` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`worker_name` VARCHAR(100) NOT NULL,
|
||||
`module` VARCHAR(50) NOT NULL DEFAULT 'Core',
|
||||
`started_at` DATETIME,
|
||||
`finished_at` DATETIME,
|
||||
`status` ENUM('Running','Success','Error') DEFAULT 'Running',
|
||||
`message` TEXT,
|
||||
INDEX `idx_worker` (`worker_name`, `started_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreTradeHistoryAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_trade_history` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`module` VARCHAR(50) NOT NULL,
|
||||
`symbol` VARCHAR(20) NOT NULL,
|
||||
`action` ENUM('BUY','SELL') NOT NULL,
|
||||
`quantity` DECIMAL(18,4),
|
||||
`price` DECIMAL(18,4),
|
||||
`total_value` DECIMAL(18,4),
|
||||
`traded_at` DATETIME,
|
||||
`ibkr_order_id` VARCHAR(100),
|
||||
`status` VARCHAR(50),
|
||||
`notes` TEXT,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_symbol` (`symbol`),
|
||||
INDEX `idx_module` (`module`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreBudgetAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_budget` (
|
||||
`module` VARCHAR(50) NOT NULL PRIMARY KEY,
|
||||
`total_budget` DECIMAL(18,2) DEFAULT 0,
|
||||
`used_budget` DECIMAL(18,2) DEFAULT 0,
|
||||
`max_per_trade` DECIMAL(18,2) DEFAULT 0,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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;");
|
||||
}
|
||||
Reference in New Issue
Block a user