From 6189d1bf8c67187ed7cc1ea174b2f23d758c04f3 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 8 Sep 2026 12:21:26 +0200 Subject: [PATCH] feat: Darkmode, Core/Modul-Gliederung, OptionsWheel & DebugTrading Web-UI, Supervisor entfernt --- .../Web/Endpoints/ModuleEndpoints.cs | 465 ++++++++++++++---- src/IBKRTrader.Hosting/Web/WebserverWorker.cs | 10 +- src/IBKRTrader.Hosting/wwwroot/css/app.css | 259 ++++++++-- src/IBKRTrader.Hosting/wwwroot/index.html | 67 ++- src/IBKRTrader.Hosting/wwwroot/js/app.js | 39 +- .../wwwroot/js/views/accounting.js | 4 +- .../wwwroot/js/views/congress.js | 4 +- .../wwwroot/js/views/dashboard.js | 4 +- .../wwwroot/js/views/debugtrading.js | 220 +++++++++ .../wwwroot/js/views/logs.js | 6 +- .../wwwroot/js/views/optionswheel.js | 400 +++++++++++++++ .../wwwroot/js/views/orders.js | 6 +- .../wwwroot/js/views/positions.js | 4 +- .../wwwroot/js/views/settings.js | 4 +- .../wwwroot/js/views/supervisor.js | 304 ------------ .../wwwroot/js/views/workers.js | 4 +- .../Web/ModuleEndpointsTests.cs | 75 +++ 17 files changed, 1403 insertions(+), 472 deletions(-) create mode 100644 src/IBKRTrader.Hosting/wwwroot/js/views/debugtrading.js create mode 100644 src/IBKRTrader.Hosting/wwwroot/js/views/optionswheel.js delete mode 100644 src/IBKRTrader.Hosting/wwwroot/js/views/supervisor.js create mode 100644 tests/IBKRTrader.Tests/Web/ModuleEndpointsTests.cs diff --git a/src/IBKRTrader.Hosting/Web/Endpoints/ModuleEndpoints.cs b/src/IBKRTrader.Hosting/Web/Endpoints/ModuleEndpoints.cs index 11d4717..b312187 100644 --- a/src/IBKRTrader.Hosting/Web/Endpoints/ModuleEndpoints.cs +++ b/src/IBKRTrader.Hosting/Web/Endpoints/ModuleEndpoints.cs @@ -1,8 +1,6 @@ using System.Globalization; -using System.Text.Json; -using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Modularity; -using IBKRTrader.Core.Time; +using IBKRTrader.Core.Settings; using IBKRTrader.Core.Trading; using IBKRTrader.Core.Workers; using IBKRTrader.Modules.Accounting.Logic; @@ -10,12 +8,15 @@ using IBKRTrader.Modules.Accounting.Persistence; using IBKRTrader.Modules.Accounting.Services; using IBKRTrader.Modules.CongressTrading; using IBKRTrader.Modules.CongressTrading.Database; -using IBKRTrader.Modules.Supervisor.Agent; -using IBKRTrader.Modules.Supervisor.Persistence; -using IBKRTrader.Modules.Supervisor.Services; +using IBKRTrader.Modules.DebugTrading.Logic; +using IBKRTrader.Modules.DebugTrading.Persistence.Ef; +using IBKRTrader.Modules.OptionsWheel; +using IBKRTrader.Modules.OptionsWheel.Persistence.Ef; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace IBKRTrader.Hosting.Web.Endpoints; @@ -38,8 +39,11 @@ public static class ModuleEndpoints if (modules.Any(m => string.Equals(m.Name, "Accounting", StringComparison.OrdinalIgnoreCase))) MapAccounting(api); - if (modules.Any(m => string.Equals(m.Name, "Supervisor", StringComparison.OrdinalIgnoreCase))) - MapSupervisor(api); + if (modules.Any(m => string.Equals(m.Name, "OptionsWheel", StringComparison.OrdinalIgnoreCase))) + MapOptionsWheel(api, sp); + + if (modules.Any(m => string.Equals(m.Name, "DebugTrading", StringComparison.OrdinalIgnoreCase))) + MapDebugTrading(api, sp); } // ─── CongressTrading ────────────────────────────────────────────────────────── @@ -47,13 +51,25 @@ public static class ModuleEndpoints { var group = api.MapGroup("/modules/congresstrading"); - group.MapGet("/summary", async (CongressRepository repo, IPortfolioService portfolio) => + group.MapGet("/summary", async (IServiceProvider sp) => { + var repo = sp.GetService(); + var portfolio = sp.GetService(); + if (repo == null || portfolio == null) + { + return Results.Ok(new + { + trades = 0, + members = 0, + positions = Array.Empty() + }); + } + var trades = await repo.GetTradeCountAsync().ConfigureAwait(false); var members = await repo.GetMemberCountAsync().ConfigureAwait(false); var positions = await portfolio.GetPositionsAsync(CongressTradingModule.LogTag).ConfigureAwait(false); - return new + return Results.Ok(new { trades, members, @@ -64,11 +80,15 @@ public static class ModuleEndpoints avgPrice = p.AvgPrice, notional = p.Notional }) - }; + }); }); - group.MapPost("/scrape", async (WorkerEngine engine) => + group.MapPost("/scrape", async (IServiceProvider sp) => { + var engine = sp.GetService(); + if (engine == null) + return Results.BadRequest(new { success = false, message = "Worker-Engine nicht verfügbar." }); + try { await engine.TriggerWorkerAsync("CT-ScrapeWorker").ConfigureAwait(false); @@ -86,18 +106,40 @@ public static class ModuleEndpoints { var group = api.MapGroup("/modules/accounting"); - group.MapGet("/accounts", (ILedgerRepository ledger) => + group.MapGet("/accounts", (IServiceProvider sp) => { - return ledger.DistinctAccounts(); + var ledger = sp.GetService(); + return Results.Ok(ledger?.DistinctAccounts() ?? (IReadOnlyList)Array.Empty()); }); group.MapGet("/overview", ( - AccountingReportService report, + IServiceProvider sp, string? account, string? currency, DateTime? from, DateTime? to) => { + var report = sp.GetService(); + if (report == null) + { + return Results.Ok(new + { + currencyCode = "USD", + currencyNote = "", + statement = new + { + netTradingResult = 0m, + tradeVolume = 0m, + dividends = 0m, + fees = 0m, + closingBalance = 0m, + tradeCount = 0, + entryCount = 0 + }, + monthly = Array.Empty() + }); + } + var fromDate = from ?? DateTime.Today.AddMonths(-1); var toDate = (to ?? DateTime.Today).AddDays(1).AddTicks(-1); var curr = string.IsNullOrWhiteSpace(currency) ? "USD" : currency; @@ -109,7 +151,7 @@ public static class ModuleEndpoints decimal M(decimal v) => Math.Round(v * view.Factor, 2); - return new + return Results.Ok(new { currencyCode = view.Code, currencyNote = view.Note, @@ -134,21 +176,24 @@ public static class ModuleEndpoints netTradingResult = M(m.NetTradingResult), closingBalance = M(m.ClosingBalance) }) - }; + }); }); group.MapGet("/ledger", ( - ILedgerRepository ledger, + IServiceProvider sp, string? account, DateTime? from, DateTime? to, int? limit) => { + var ledger = sp.GetService(); + if (ledger == null) return Results.Ok(Array.Empty()); + var fromDate = from ?? DateTime.Today.AddMonths(-1); var toDate = (to ?? DateTime.Today).AddDays(1).AddTicks(-1); var acc = string.IsNullOrWhiteSpace(account) || account == "(alle)" ? null : account; - return ledger.Query(acc, fromDate, toDate, limit ?? 2000).Select(e => new + return Results.Ok(ledger.Query(acc, fromDate, toDate, limit ?? 2000).Select(e => new { timestamp = e.Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), accountId = e.AccountId, @@ -162,13 +207,16 @@ public static class ModuleEndpoints feeBase = e.FeeBase, netBase = e.NetBase, transactionId = e.TransactionId - }); + })); }); - group.MapGet("/runs", (IIngestRunRepository runs, string? account, int? limit) => + group.MapGet("/runs", (IServiceProvider sp, string? account, int? limit) => { + var runs = sp.GetService(); + if (runs == null) return Results.Ok(Array.Empty()); + var acc = string.IsNullOrWhiteSpace(account) || account == "(alle)" ? null : account; - return runs.GetRecent(acc, limit ?? 100).Select(r => new + return Results.Ok(runs.GetRecent(acc, limit ?? 100).Select(r => new { accountId = r.AccountId, startedAt = r.StartedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), @@ -179,11 +227,15 @@ public static class ModuleEndpoints success = r.Success, balanceDeltaBase = r.BalanceDeltaBase, message = r.Message - }); + })); }); - group.MapPost("/ingest", async (AccountingIngestService ingest, bool backfill = false) => + group.MapPost("/ingest", async (IServiceProvider sp, bool backfill = false) => { + var ingest = sp.GetService(); + if (ingest == null) + return Results.BadRequest(new { success = false, message = "Accounting-Dienst nicht verfügbar." }); + try { await ingest.IngestAllAsync(backfill, CancellationToken.None).ConfigureAwait(false); @@ -195,8 +247,11 @@ public static class ModuleEndpoints } }); - group.MapGet("/export/ledger.csv", (ILedgerRepository ledger, string? account, DateTime? from, DateTime? to) => + group.MapGet("/export/ledger.csv", (IServiceProvider sp, string? account, DateTime? from, DateTime? to) => { + var ledger = sp.GetService(); + if (ledger == null) return Results.NotFound(); + var fromDate = from ?? DateTime.Today.AddMonths(-1); var toDate = (to ?? DateTime.Today).AddDays(1).AddTicks(-1); var acc = string.IsNullOrWhiteSpace(account) || account == "(alle)" ? null : account; @@ -206,8 +261,11 @@ public static class ModuleEndpoints return Results.File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", "ledger.csv"); }); - group.MapGet("/export/statement.csv", (AccountingReportService report, string? account, DateTime? from, DateTime? to) => + group.MapGet("/export/statement.csv", (IServiceProvider sp, string? account, DateTime? from, DateTime? to) => { + var report = sp.GetService(); + if (report == null) return Results.NotFound(); + var fromDate = from ?? DateTime.Today.AddMonths(-1); var toDate = (to ?? DateTime.Today).AddDays(1).AddTicks(-1); var acc = string.IsNullOrWhiteSpace(account) || account == "(alle)" ? null : account; @@ -218,13 +276,16 @@ public static class ModuleEndpoints }); group.MapGet("/export/statement.pdf", ( - AccountingReportService report, - ILedgerRepository ledger, + IServiceProvider sp, string? account, string? currency, DateTime? from, DateTime? to) => { + var report = sp.GetService(); + var ledger = sp.GetService(); + if (report == null || ledger == null) return Results.NotFound(); + var fromDate = from ?? DateTime.Today.AddMonths(-1); var toDate = (to ?? DateTime.Today).AddDays(1).AddTicks(-1); var acc = string.IsNullOrWhiteSpace(account) || account == "(alle)" ? null : account; @@ -240,90 +301,312 @@ public static class ModuleEndpoints }); } - // ─── Supervisor ─────────────────────────────────────────────────────────────── - private static void MapSupervisor(RouteGroupBuilder api) + // ─── OptionsWheel ───────────────────────────────────────────────────────────── + private static void MapOptionsWheel(RouteGroupBuilder api, IServiceProvider sp) { - var group = api.MapGroup("/modules/supervisor"); + var group = api.MapGroup("/modules/optionswheel"); - group.MapGet("/profiles", () => SupervisorProfiles.All.Select(p => p.Name)); - - group.MapGet("/signals", (DossierService dossiers, int? limit) => + group.MapGet("/status", async ( + IConfiguration config, + IServiceProvider requestSp) => { - return dossiers.RecentSignals(limit ?? 200); - }); + var settings = config.GetSection("OptionsWheel").Get() ?? new OptionsWheelSettings(); + var module = sp.GetServices().FirstOrDefault(m => string.Equals(m.Name, "OptionsWheel", StringComparison.OrdinalIgnoreCase)); + var blocker = module?.GetActivationBlocker(config); - group.MapGet("/dossier/{signalId}", (DossierService dossiers, string signalId) => - { - var dossier = dossiers.BuildForSignal(signalId); - var markdown = DossierBuilder.ToMarkdown(dossier); - return new { signalId, markdown }; - }); + int underlyingsCount = 0; + int activeCyclesCount = 0; + int openLegsCount = 0; - group.MapGet("/reports", (ISupervisorReportRepository reports, int? limit) => - { - return reports.GetRecent(limit ?? 100).Select(r => new + var dbf = requestSp.GetService>(); + if (dbf != null) { - createdAt = r.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), - profile = r.Profile, - model = r.Model, - question = r.Question, - answer = r.Answer, - toolCallCount = r.ToolCallCount, - tokens = $"{r.PromptTokens} / {r.CompletionTokens}" + try + { + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + underlyingsCount = await db.Underlyings.CountAsync().ConfigureAwait(false); + activeCyclesCount = await db.Cycles.CountAsync(c => c.EndedAt == null).ConfigureAwait(false); + openLegsCount = await db.Legs.CountAsync(l => l.Status == "Open").ConfigureAwait(false); + } + catch { /* DB-Zugriff optional falls Tabellen noch nicht angelegt */ } + } + + return Results.Ok(new + { + enabled = settings.Enabled, + noNewPositions = settings.NoNewPositions, + scanIntervalMinutes = settings.ScanIntervalMinutes, + maxBoundCapitalPercent = settings.MaxBoundCapitalPercent, + blocker, + underlyingsCount, + activeCyclesCount, + openLegsCount, + rules = new + { + minDte = settings.Rules.MinDaysToExpiry, + maxDte = settings.Rules.MaxDaysToExpiry, + minDelta = settings.Rules.MinDelta, + maxDelta = settings.Rules.MaxDelta, + minAnnualizedReturnPercent = settings.Rules.MinAnnualizedReturnPercent, + rollProfitTargetPercent = settings.Rules.ProfitTargetPercent, + rollDteThreshold = settings.Rules.RollAtDaysToExpiry + } }); }); - group.MapGet("/info", () => + group.MapGet("/underlyings", async (IServiceProvider requestSp) => { - var keySet = !string.IsNullOrEmpty(OpenRouterClient.DefaultApiKeyProvider()); - return new - { - openRouterKeyConfigured = keySet, - timeZone = AppTimeZone.CurrentId, - description = "Supervisor – read-only Analyse und Forensik über alle Module." - }; - }); - - group.MapPost("/ask", async ( - SupervisorAskRequest req, - SupervisorAgent agent, - ISupervisorReportRepository reports) => - { - if (string.IsNullOrWhiteSpace(req.Question)) - return Results.BadRequest(new { success = false, message = "Keine Frage angegeben." }); - - var profile = SupervisorProfiles.ByName(req.Profile ?? ""); + var dbf = requestSp.GetService>(); + if (dbf == null) return Results.Ok(Array.Empty()); try { - var result = await agent.AskAsync(req.Question, profile: profile).ConfigureAwait(false); + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + var list = await db.Underlyings + .OrderBy(u => u.Symbol) + .Select(u => new + { + u.Id, + u.Symbol, + u.Active, + u.Halted, + u.HaltReason, + createdAt = u.CreatedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + updatedAt = u.UpdatedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) + }) + .ToListAsync().ConfigureAwait(false); + return Results.Ok(list); + } + catch + { + return Results.Ok(Array.Empty()); + } + }); - reports.Insert(new SupervisorReport - { - Profile = profile.Name, - Model = SupervisorAgent.DefaultModel, - Question = req.Question, - Answer = result.Answer, - ToolCallsJson = JsonSerializer.Serialize(result.ToolInvocations.Select(i => new { i.Tool, i.Arguments })), - ToolCallCount = result.ToolInvocations.Count, - PromptTokens = result.PromptTokens, - CompletionTokens = result.CompletionTokens - }); + group.MapGet("/cycles", async (IServiceProvider requestSp) => + { + var dbf = requestSp.GetService>(); + if (dbf == null) return Results.Ok(Array.Empty()); - return Results.Ok(new + try + { + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + var list = await db.Cycles + .OrderByDescending(c => c.StartedAt) + .Take(100) + .Select(c => new + { + c.Id, + c.Symbol, + startedAt = c.StartedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + endedAt = c.EndedAt.HasValue ? c.EndedAt.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : null, + c.PremiumGross, + c.PremiumNet, + c.AssignedPrice, + c.AssignedShares, + c.RealizedPnl, + c.Outcome + }) + .ToListAsync().ConfigureAwait(false); + return Results.Ok(list); + } + catch + { + return Results.Ok(Array.Empty()); + } + }); + + group.MapGet("/legs", async (IServiceProvider requestSp) => + { + var dbf = requestSp.GetService>(); + if (dbf == null) return Results.Ok(Array.Empty()); + + try + { + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + var list = await db.Legs + .OrderByDescending(l => l.OpenedAt) + .Take(100) + .Select(l => new + { + l.Id, + l.CycleId, + l.Symbol, + l.ContractKey, + expiry = l.Expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + l.Strike, + l.Right, + l.Contracts, + l.PremiumOpen, + l.PriceClose, + l.DeltaOpen, + l.IvOpen, + l.DteOpen, + openedAt = l.OpenedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + closedAt = l.ClosedAt.HasValue ? l.ClosedAt.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : null, + l.Status + }) + .ToListAsync().ConfigureAwait(false); + return Results.Ok(list); + } + catch + { + return Results.Ok(Array.Empty()); + } + }); + + group.MapGet("/candidates", async (IServiceProvider requestSp) => + { + var dbf = requestSp.GetService>(); + if (dbf == null) return Results.Ok(Array.Empty()); + + try + { + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + var list = await db.Candidates + .OrderByDescending(c => c.ScannedAt) + .Take(100) + .Select(c => new + { + c.Id, + scannedAt = c.ScannedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + c.Symbol, + c.ContractKey, + expiry = c.Expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + c.Strike, + c.Right, + c.Delta, + c.Iv, + c.Bid, + c.Ask, + c.Premium, + c.AnnualizedReturnPercent, + c.Chosen, + rejection = c.Rejection.ToString(), + c.Note + }) + .ToListAsync().ConfigureAwait(false); + return Results.Ok(list); + } + catch + { + return Results.Ok(Array.Empty()); + } + }); + } + + // ─── DebugTrading ───────────────────────────────────────────────────────────── + private static void MapDebugTrading(RouteGroupBuilder api, IServiceProvider sp) + { + var group = api.MapGroup("/modules/debugtrading"); + + group.MapGet("/status", async (IServiceProvider requestSp) => + { + var settingsService = requestSp.GetService() ?? new SettingsService(); + var settings = settingsService.Settings.DebugTrading; + var trading = settingsService.Settings.Trading; + var blockedReason = DebugTradingGuard.CheckPaperMode(trading.ParsedMode, trading.Mode); + + int openPositionsCount = 0; + var dbf = requestSp.GetService>(); + if (dbf != null) + { + try { - success = true, - answer = result.Answer, - toolCalls = result.ToolInvocations.Select(i => new { tool = i.Tool, arguments = i.Arguments }), - toolCallCount = result.ToolInvocations.Count - }); + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + openPositionsCount = await db.Positions.CountAsync(p => p.ClosedAt == null).ConfigureAwait(false); + } + catch { /* DB noch nicht initialisiert */ } + } + + return Results.Ok(new + { + enabled = settings.Enabled, + stopOpening = settings.StopOpening, + onlyDuringMarketHours = settings.OnlyDuringMarketHours, + intervalMinutes = settings.IntervalMinutes, + maxOpenPositions = settings.MaxOpenPositions, + maxNotionalPerTrade = settings.MaxNotionalPerTrade, + dailyBudget = settings.DailyBudget, + minHoldMinutes = settings.MinHoldMinutes, + maxHoldMinutes = settings.MaxHoldMinutes, + tradingMode = trading.Mode, + isPaper = string.Equals(trading.Mode, "Paper", StringComparison.OrdinalIgnoreCase), + guardBlocked = blockedReason != null, + guardBlockedReason = blockedReason, + openPositionsCount + }); + }); + + group.MapGet("/positions", async (IServiceProvider requestSp) => + { + var dbf = requestSp.GetService>(); + if (dbf == null) return Results.Ok(Array.Empty()); + + try + { + await using var db = await dbf.CreateDbContextAsync().ConfigureAwait(false); + var list = await db.Positions + .OrderByDescending(p => p.OpenedAt) + .Take(100) + .Select(p => new + { + p.Id, + p.ContractKey, + p.Symbol, + p.Currency, + p.Quantity, + p.OpenPrice, + openedAt = p.OpenedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + closeAtUtc = p.CloseAtUtc.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture), + closedAt = p.ClosedAt.HasValue ? p.ClosedAt.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : null, + isOpen = p.ClosedAt == null, + p.OpenSignalId, + p.Note + }) + .ToListAsync().ConfigureAwait(false); + return Results.Ok(list); + } + catch + { + return Results.Ok(Array.Empty()); + } + }); + + group.MapPost("/trigger", async (IServiceProvider requestSp) => + { + var engine = requestSp.GetService(); + if (engine == null) + return Results.BadRequest(new { success = false, message = "WorkerEngine nicht verfügbar." }); + + try + { + await engine.TriggerWorkerAsync("DBG-TradeGenerator").ConfigureAwait(false); + return Results.Ok(new { success = true, message = "DBG-TradeGenerator ausgelöst." }); } catch (Exception ex) { return Results.BadRequest(new { success = false, message = ex.Message }); } }); + + group.MapPost("/toggle-notaus", (IServiceProvider requestSp) => + { + var settingsService = requestSp.GetService(); + if (settingsService == null) + return Results.BadRequest(new { success = false, message = "SettingsService nicht verfügbar." }); + + var current = settingsService.Settings.DebugTrading.StopOpening; + settingsService.Settings.DebugTrading.StopOpening = !current; + settingsService.Save(); + return Results.Ok(new + { + success = true, + stopOpening = settingsService.Settings.DebugTrading.StopOpening, + message = settingsService.Settings.DebugTrading.StopOpening + ? "Not-Aus AKTIV: Keine neuen Testpositionen werden eröffnet." + : "Not-Aus AUS: Neue Testpositionen wieder erlaubt." + }); + }); } } - -public sealed record SupervisorAskRequest(string Question, string? Profile); diff --git a/src/IBKRTrader.Hosting/Web/WebserverWorker.cs b/src/IBKRTrader.Hosting/Web/WebserverWorker.cs index f5cf871..d197fb0 100644 --- a/src/IBKRTrader.Hosting/Web/WebserverWorker.cs +++ b/src/IBKRTrader.Hosting/Web/WebserverWorker.cs @@ -10,9 +10,8 @@ using IBKRTrader.Hosting.Web.Endpoints; using IBKRTrader.Modules.Accounting.Persistence; using IBKRTrader.Modules.Accounting.Services; using IBKRTrader.Modules.CongressTrading.Database; -using IBKRTrader.Modules.Supervisor.Agent; -using IBKRTrader.Modules.Supervisor.Persistence; -using IBKRTrader.Modules.Supervisor.Services; +using IBKRTrader.Modules.DebugTrading.Persistence.Ef; +using IBKRTrader.Modules.OptionsWheel.Persistence.Ef; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -117,9 +116,8 @@ public sealed class WebserverWorker : WorkerBase, IDisposable ForwardOptional(builder.Services); ForwardOptional(builder.Services); ForwardOptional(builder.Services); - ForwardOptional(builder.Services); - ForwardOptional(builder.Services); - ForwardOptional(builder.Services); + ForwardOptional>(builder.Services); + ForwardOptional>(builder.Services); _app = builder.Build(); diff --git a/src/IBKRTrader.Hosting/wwwroot/css/app.css b/src/IBKRTrader.Hosting/wwwroot/css/app.css index fbdeaa7..8292ab9 100644 --- a/src/IBKRTrader.Hosting/wwwroot/css/app.css +++ b/src/IBKRTrader.Hosting/wwwroot/css/app.css @@ -22,11 +22,53 @@ --color-warning-bg: #ffedd5; --color-danger: #dc2626; --color-danger-bg: #fee2e2; + + --badge-trading-bg: #e0e7ff; + --badge-trading-text: #3730a3; + --badge-test-bg: #fef3c7; + --badge-test-text: #92400e; + --badge-admin-bg: #d1fae5; + --badge-admin-text: #065f46; + --badge-core-bg: #f1f5f9; + --badge-core-text: #475569; --font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } +[data-theme="dark"] { + --bg-app: #111115; + --bg-card: #18181f; + --bg-toolbar: #1e1e26; + --bg-statusbar: #15151c; + --bg-hover: #262633; + --bg-selected: #223456; + --border-color: #2e2e3d; + --border-subtle: #242430; + + --text-main: #e2e4ea; + --text-muted: #959aa8; + --text-light: #646979; + + --color-primary: #3b82f6; + --color-primary-hover: #60a5fa; + --color-success: #22c55e; + --color-success-bg: #143322; + --color-warning: #f97316; + --color-warning-bg: #3d2311; + --color-danger: #ef4444; + --color-danger-bg: #3c1618; + + --badge-trading-bg: #272652; + --badge-trading-text: #c7d2fe; + --badge-test-bg: #3c2912; + --badge-test-text: #fde68a; + --badge-admin-bg: #133327; + --badge-admin-text: #a7f3d0; + --badge-core-bg: #22222d; + --badge-core-text: #94a3b8; +} + * { box-sizing: border-box; margin: 0; @@ -42,12 +84,13 @@ body { display: flex; flex-direction: column; overflow: hidden; + transition: background-color 0.15s ease, color 0.15s ease; } /* ─── App Header & Navigation ─────────────────────────────────────────── */ .app-header { - background: #ffffff; + background: var(--bg-card); border-bottom: 1px solid var(--border-color); display: flex; align-items: center; @@ -56,12 +99,14 @@ body { height: 48px; user-select: none; flex-shrink: 0; + gap: 16px; } .brand { display: flex; align-items: center; gap: 10px; + flex-shrink: 0; } .brand-title { @@ -93,24 +138,62 @@ body { font-weight: 600; } +/* ─── Navigation Tabs & Grouping ─────────────────────────────────────── */ + .nav-tabs { display: flex; - gap: 4px; + align-items: center; + gap: 8px; overflow-x: auto; scrollbar-width: none; + flex: 1; +} + +.nav-tabs::-webkit-scrollbar { + display: none; +} + +.nav-group { + display: flex; + align-items: center; + gap: 2px; + background: var(--bg-app); + padding: 2px 4px; + border-radius: 6px; + border: 1px solid var(--border-subtle); +} + +.nav-group-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.6px; + color: var(--text-light); + padding: 0 6px; + text-transform: uppercase; + user-select: none; +} + +.nav-divider { + width: 1px; + height: 22px; + background: var(--border-color); + margin: 0 4px; } .nav-tab { background: none; border: none; - padding: 8px 14px; + padding: 6px 10px; font-family: inherit; - font-size: 13px; + font-size: 12px; color: var(--text-muted); cursor: pointer; - border-bottom: 2px solid transparent; + border-radius: 4px; transition: all 0.15s ease; white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 6px; } .nav-tab:hover { @@ -121,14 +204,81 @@ body { .nav-tab.active { color: var(--color-primary); font-weight: 600; - border-bottom-color: var(--color-primary); - background: transparent; + background: var(--bg-card); + box-shadow: 0 1px 2px rgba(0,0,0,0.06); +} + +/* Tab Badges */ +.tab-badge { + font-size: 9px; + font-weight: 700; + padding: 1px 5px; + border-radius: 3px; + letter-spacing: 0.4px; + text-transform: uppercase; +} + +.badge-trading { + background: var(--badge-trading-bg); + color: var(--badge-trading-text); +} + +.badge-test { + background: var(--badge-test-bg); + color: var(--badge-test-text); +} + +.badge-admin { + background: var(--badge-admin-bg); + color: var(--badge-admin-text); +} + +.badge-core { + background: var(--badge-core-bg); + color: var(--badge-core-text); +} + +/* Category Tags in View Headers */ +.view-tag { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 4px; + letter-spacing: 0.3px; + text-transform: uppercase; +} + +.tag-trading { + background: var(--badge-trading-bg); + color: var(--badge-trading-text); + border: 1px solid var(--border-subtle); +} + +.tag-test { + background: var(--badge-test-bg); + color: var(--badge-test-text); + border: 1px solid var(--border-subtle); +} + +.tag-module { + background: var(--badge-admin-bg); + color: var(--badge-admin-text); + border: 1px solid var(--border-subtle); +} + +.tag-core { + background: var(--badge-core-bg); + color: var(--badge-core-text); + border: 1px solid var(--border-subtle); } .header-actions { display: flex; align-items: center; - gap: 12px; + gap: 8px; + flex-shrink: 0; } /* ─── App Body Layout ─────────────────────────────────────────────────── */ @@ -177,7 +327,7 @@ body { /* ─── Buttons & Controls ──────────────────────────────────────────────── */ .btn { - background: #ffffff; + background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 4px; padding: 5px 12px; @@ -194,13 +344,18 @@ body { .btn:hover { background: var(--bg-hover); - border-color: #bbb; + border-color: var(--text-muted); } .btn:active { background: var(--bg-selected); } +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .btn-primary { background: var(--color-primary); border-color: var(--color-primary-hover); @@ -209,7 +364,7 @@ body { .btn-primary:hover { background: var(--color-primary-hover); - border-color: #1e40af; + border-color: var(--color-primary); } .btn-danger { @@ -222,20 +377,26 @@ body { background: #b91c1c; } -.input, select { - background: #ffffff; +.theme-toggle-btn { + padding: 4px 8px; + font-size: 13px; + line-height: 1; +} + +.input, select, textarea { + background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 4px; - padding: 4px 8px; + padding: 5px 8px; font-family: inherit; font-size: 12px; color: var(--text-main); outline: none; } -.input:focus, select:focus { +.input:focus, select:focus, textarea:focus { border-color: var(--color-primary); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } .checkbox-label { @@ -255,17 +416,17 @@ body { flex-wrap: wrap; padding: 12px; gap: 8px; - background: #ffffff; + background: var(--bg-card); border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; } .kpi-card { - background: #f5f5f5; + background: var(--bg-toolbar); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px 14px; - min-width: 160px; + min-width: 150px; flex: 1; display: flex; flex-direction: column; @@ -291,7 +452,7 @@ body { .table-container { flex: 1; overflow: auto; - background: #ffffff; + background: var(--bg-card); position: relative; } @@ -303,10 +464,10 @@ body { } .datagrid th { - background: #fafafa; + background: var(--bg-toolbar); color: var(--text-muted); font-weight: 600; - padding: 6px 10px; + padding: 7px 10px; border-bottom: 1px solid var(--border-color); position: sticky; top: 0; @@ -316,8 +477,8 @@ body { } .datagrid td { - padding: 5px 10px; - border-bottom: 1px solid #eeeeee; + padding: 6px 10px; + border-bottom: 1px solid var(--border-subtle); color: var(--text-main); white-space: nowrap; font-feature-settings: "tnum"; @@ -325,7 +486,7 @@ body { .datagrid tbody tr { cursor: pointer; - height: 26px; + height: 28px; } .datagrid tbody tr:hover { @@ -333,8 +494,8 @@ body { } .datagrid tbody tr.selected { - background: #e0e7ff !important; - color: #1e3a8a; + background: var(--bg-selected) !important; + color: var(--text-main); } .datagrid .text-right { @@ -354,16 +515,16 @@ body { font-weight: 600; text-transform: uppercase; } -.badge-ok { background: #dcfce7; color: #15803d; } -.badge-warn { background: #fef3c7; color: #b45309; } -.badge-error { background: #fee2e2; color: #b91c1c; } +.badge-ok { background: var(--color-success-bg); color: var(--color-success); } +.badge-warn { background: var(--color-warning-bg); color: var(--color-warning); } +.badge-error { background: var(--color-danger-bg); color: var(--color-danger); } /* ─── Log Viewer ──────────────────────────────────────────────────────── */ .log-scroller { flex: 1; - background: #1e1e1e; - color: #d4d4d4; + background: #101014; + color: #d4d4d8; font-family: var(--font-mono); font-size: 11px; line-height: 1.5; @@ -376,9 +537,9 @@ body { .log-line { padding: 1px 0; } -.log-info { color: #96d296; } -.log-warn { color: #ffbe3c; } -.log-error { color: #ff5050; font-weight: 600; } +.log-info { color: #86efac; } +.log-warn { color: #fde047; } +.log-error { color: #f87171; font-weight: 600; } /* ─── Settings Tree Form ──────────────────────────────────────────────── */ @@ -386,10 +547,11 @@ body { flex: 1; overflow-y: auto; padding: 16px 20px; + background: var(--bg-app); } .settings-section { - background: #ffffff; + background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 4px; margin-bottom: 12px; @@ -397,9 +559,10 @@ body { } .settings-section-header { - background: #fafafa; + background: var(--bg-toolbar); padding: 8px 14px; font-weight: 600; + color: var(--text-main); border-bottom: 1px solid var(--border-color); cursor: pointer; display: flex; @@ -408,10 +571,10 @@ body { } .settings-section-body { - padding: 10px 14px; + padding: 12px 16px; display: grid; - grid-template-columns: 240px 1fr; - row-gap: 8px; + grid-template-columns: 260px 1fr; + row-gap: 10px; column-gap: 16px; align-items: center; } @@ -423,7 +586,7 @@ body { .settings-desc { font-size: 11px; color: var(--text-muted); - margin-top: 1px; + margin-top: 2px; } /* ─── Statusbar ───────────────────────────────────────────────────────── */ @@ -447,18 +610,18 @@ body { .modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.45); + background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; - backdrop-filter: blur(2px); + backdrop-filter: blur(3px); } .login-card { - background: #ffffff; + background: var(--bg-card); border-radius: 8px; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.25), 0 10px 10px -5px rgba(0, 0, 0, 0.1); width: 100%; max-width: 380px; padding: 28px; @@ -470,6 +633,7 @@ body { font-weight: 600; margin-bottom: 6px; text-align: center; + color: var(--text-main); } .login-subtitle { @@ -488,6 +652,7 @@ body { font-size: 12px; font-weight: 500; margin-bottom: 6px; + color: var(--text-main); } .form-input { @@ -496,12 +661,14 @@ body { border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px; + background: var(--bg-toolbar); + color: var(--text-main); outline: none; } .form-input:focus { border-color: var(--color-primary); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } .login-error { diff --git a/src/IBKRTrader.Hosting/wwwroot/index.html b/src/IBKRTrader.Hosting/wwwroot/index.html index 84bd1ae..f81f563 100644 --- a/src/IBKRTrader.Hosting/wwwroot/index.html +++ b/src/IBKRTrader.Hosting/wwwroot/index.html @@ -5,6 +5,15 @@ IBKRTrader + @@ -15,19 +24,57 @@ PAPER + -
+
+
diff --git a/src/IBKRTrader.Hosting/wwwroot/js/app.js b/src/IBKRTrader.Hosting/wwwroot/js/app.js index 26535b0..54c86b7 100644 --- a/src/IBKRTrader.Hosting/wwwroot/js/app.js +++ b/src/IBKRTrader.Hosting/wwwroot/js/app.js @@ -7,8 +7,9 @@ import { WorkersView } from './views/workers.js'; import { PositionsView } from './views/positions.js'; import { OrdersView } from './views/orders.js'; import { CongressTradingView } from './views/congress.js'; +import { OptionsWheelView } from './views/optionswheel.js'; +import { DebugTradingView } from './views/debugtrading.js'; import { AccountingView } from './views/accounting.js'; -import { SupervisorView } from './views/supervisor.js'; import { LogsView } from './views/logs.js'; import { SettingsView } from './views/settings.js'; @@ -18,25 +19,31 @@ class App { this.statusMsgEl = document.getElementById('status-bar-message'); this.clockEl = document.getElementById('status-bar-clock'); this.modeBadgeEl = document.getElementById('app-mode-badge'); + this.themeToggleBtn = document.getElementById('theme-toggle-btn'); this.currentView = null; this.currentViewName = null; this.views = { + // Core-Funktionen dashboard: DashboardView, workers: WorkersView, positions: PositionsView, orders: OrdersView, - congress: CongressTradingView, - accounting: AccountingView, - supervisor: SupervisorView, logs: LogsView, - settings: SettingsView + settings: SettingsView, + + // Module + congress: CongressTradingView, + optionswheel: OptionsWheelView, + debugtrading: DebugTradingView, + accounting: AccountingView }; this.setStatus = this.setStatus.bind(this); } init() { + this.initTheme(); this.startClock(); this.wireNav(); @@ -63,6 +70,28 @@ class App { }); } + initTheme() { + const updateButton = (theme) => { + if (this.themeToggleBtn) { + this.themeToggleBtn.textContent = theme === 'dark' ? '☀️' : '🌙'; + this.themeToggleBtn.title = theme === 'dark' ? 'Zu hellem Modus wechseln' : 'Zu dunklem Modus wechseln'; + } + }; + + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + updateButton(currentTheme); + + if (this.themeToggleBtn) { + this.themeToggleBtn.addEventListener('click', () => { + const active = document.documentElement.getAttribute('data-theme') || 'light'; + const next = active === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('ibkr_theme', next); + updateButton(next); + }); + } + } + startClock() { const updateClock = () => { const now = new Date(); diff --git a/src/IBKRTrader.Hosting/wwwroot/js/views/accounting.js b/src/IBKRTrader.Hosting/wwwroot/js/views/accounting.js index 2acd23d..f620905 100644 --- a/src/IBKRTrader.Hosting/wwwroot/js/views/accounting.js +++ b/src/IBKRTrader.Hosting/wwwroot/js/views/accounting.js @@ -17,7 +17,9 @@ export class AccountingView { this.container.innerHTML = `
-