feat: Darkmode, Core/Modul-Gliederung, OptionsWheel & DebugTrading Web-UI, Supervisor entfernt
This commit is contained in:
@@ -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<CongressRepository>();
|
||||
var portfolio = sp.GetService<IPortfolioService>();
|
||||
if (repo == null || portfolio == null)
|
||||
{
|
||||
return Results.Ok(new
|
||||
{
|
||||
trades = 0,
|
||||
members = 0,
|
||||
positions = Array.Empty<object>()
|
||||
});
|
||||
}
|
||||
|
||||
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<WorkerEngine>();
|
||||
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<ILedgerRepository>();
|
||||
return Results.Ok(ledger?.DistinctAccounts() ?? (IReadOnlyList<string>)Array.Empty<string>());
|
||||
});
|
||||
|
||||
group.MapGet("/overview", (
|
||||
AccountingReportService report,
|
||||
IServiceProvider sp,
|
||||
string? account,
|
||||
string? currency,
|
||||
DateTime? from,
|
||||
DateTime? to) =>
|
||||
{
|
||||
var report = sp.GetService<AccountingReportService>();
|
||||
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<object>()
|
||||
});
|
||||
}
|
||||
|
||||
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<ILedgerRepository>();
|
||||
if (ledger == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<IIngestRunRepository>();
|
||||
if (runs == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<AccountingIngestService>();
|
||||
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<ILedgerRepository>();
|
||||
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<AccountingReportService>();
|
||||
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<AccountingReportService>();
|
||||
var ledger = sp.GetService<ILedgerRepository>();
|
||||
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<OptionsWheelSettings>() ?? new OptionsWheelSettings();
|
||||
var module = sp.GetServices<IModule>().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<IDbContextFactory<OptionsWheelDbContext>>();
|
||||
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<IDbContextFactory<OptionsWheelDbContext>>();
|
||||
if (dbf == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<object>());
|
||||
}
|
||||
});
|
||||
|
||||
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<IDbContextFactory<OptionsWheelDbContext>>();
|
||||
if (dbf == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<object>());
|
||||
}
|
||||
});
|
||||
|
||||
group.MapGet("/legs", async (IServiceProvider requestSp) =>
|
||||
{
|
||||
var dbf = requestSp.GetService<IDbContextFactory<OptionsWheelDbContext>>();
|
||||
if (dbf == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<object>());
|
||||
}
|
||||
});
|
||||
|
||||
group.MapGet("/candidates", async (IServiceProvider requestSp) =>
|
||||
{
|
||||
var dbf = requestSp.GetService<IDbContextFactory<OptionsWheelDbContext>>();
|
||||
if (dbf == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<object>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 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<SettingsService>() ?? 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<IDbContextFactory<DebugTradingDbContext>>();
|
||||
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<IDbContextFactory<DebugTradingDbContext>>();
|
||||
if (dbf == null) return Results.Ok(Array.Empty<object>());
|
||||
|
||||
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<object>());
|
||||
}
|
||||
});
|
||||
|
||||
group.MapPost("/trigger", async (IServiceProvider requestSp) =>
|
||||
{
|
||||
var engine = requestSp.GetService<WorkerEngine>();
|
||||
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<SettingsService>();
|
||||
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);
|
||||
|
||||
@@ -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<IIngestRunRepository>(builder.Services);
|
||||
ForwardOptional<AccountingReportService>(builder.Services);
|
||||
ForwardOptional<AccountingIngestService>(builder.Services);
|
||||
ForwardOptional<SupervisorAgent>(builder.Services);
|
||||
ForwardOptional<DossierService>(builder.Services);
|
||||
ForwardOptional<ISupervisorReportRepository>(builder.Services);
|
||||
ForwardOptional<IDbContextFactory<OptionsWheelDbContext>>(builder.Services);
|
||||
ForwardOptional<IDbContextFactory<DebugTradingDbContext>>(builder.Services);
|
||||
|
||||
_app = builder.Build();
|
||||
|
||||
|
||||
@@ -23,10 +23,52 @@
|
||||
--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 {
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IBKRTrader</title>
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<script>
|
||||
// Sofortige Theme-Aktivierung ohne Flackern
|
||||
(function() {
|
||||
const saved = localStorage.getItem('ibkr_theme');
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = saved || (prefersDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -15,19 +24,57 @@
|
||||
<span id="app-mode-badge" class="brand-badge badge-paper">PAPER</span>
|
||||
</div>
|
||||
|
||||
<!-- Gruppierte Navigation: Core-Funktionen vs. Trading- & Spezial-Module -->
|
||||
<nav class="nav-tabs" id="main-nav">
|
||||
<button class="nav-tab active" data-view="dashboard">Dashboard</button>
|
||||
<button class="nav-tab" data-view="workers">Worker</button>
|
||||
<button class="nav-tab" data-view="positions">Positionen</button>
|
||||
<button class="nav-tab" data-view="orders">Orders</button>
|
||||
<button class="nav-tab" data-view="congress">Congress</button>
|
||||
<button class="nav-tab" data-view="accounting">Accounting</button>
|
||||
<button class="nav-tab" data-view="supervisor">Supervisor</button>
|
||||
<button class="nav-tab" data-view="logs">Logs</button>
|
||||
<button class="nav-tab" data-view="settings">Einstellungen</button>
|
||||
<!-- CORE BEREICH -->
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-label">CORE</span>
|
||||
<button class="nav-tab active" data-view="dashboard" title="Core: Dashboard & Übersicht">
|
||||
Dashboard
|
||||
</button>
|
||||
<button class="nav-tab" data-view="workers" title="Core: Worker & Hintergrunddienste">
|
||||
Worker
|
||||
</button>
|
||||
<button class="nav-tab" data-view="positions" title="Core: Bestands- und Positionsverwaltung">
|
||||
Positionen
|
||||
</button>
|
||||
<button class="nav-tab" data-view="orders" title="Core: Broker-Orders & Reservierungen">
|
||||
Orders
|
||||
</button>
|
||||
<button class="nav-tab" data-view="logs" title="Core: Echtzeit-Systemprotokoll">
|
||||
Logs
|
||||
</button>
|
||||
<button class="nav-tab" data-view="settings" title="Core: Systemkonfiguration">
|
||||
Einstellungen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<!-- MODULE BEREICH -->
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-label">MODULE</span>
|
||||
<button class="nav-tab" data-view="congress" title="Trading-Modul: Congress Trading (Buch: CT)">
|
||||
Congress Trading
|
||||
<span class="tab-badge badge-trading">TRADING</span>
|
||||
</button>
|
||||
<button class="nav-tab" data-view="optionswheel" title="Trading-Modul: OptionsWheel (Buch: OW)">
|
||||
OptionsWheel
|
||||
<span class="tab-badge badge-trading">TRADING</span>
|
||||
</button>
|
||||
<button class="nav-tab" data-view="debugtrading" title="Test-Tradingmodul: Debug Trading (Buch: DBG)">
|
||||
Debug Trading
|
||||
<span class="tab-badge badge-test">TEST</span>
|
||||
</button>
|
||||
<button class="nav-tab" data-view="accounting" title="Finanz-Modul: Accounting (Buchhaltung & BWA)">
|
||||
Accounting
|
||||
<span class="tab-badge badge-admin">FINANZEN</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div class="header-actions">
|
||||
<button id="theme-toggle-btn" class="btn theme-toggle-btn" title="Zwischen Hell- und Dunkelmodus wechseln">🌙</button>
|
||||
<button id="logout-btn" class="btn" style="padding: 4px 10px; font-size: 11px;">🔒 Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -17,7 +17,9 @@ export class AccountingView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar" style="flex-wrap: wrap; gap: 8px;">
|
||||
<div class="toolbar-group" style="flex-wrap: wrap; gap: 8px;">
|
||||
<label style="font-weight: 500; font-size: 12px; display: flex; align-items: center; gap: 4px;">
|
||||
<span class="view-tag tag-module">FINANZ-MODUL</span>
|
||||
<span style="font-weight: 600; font-size: 13px;">Accounting</span>
|
||||
<label style="font-weight: 500; font-size: 12px; display: flex; align-items: center; gap: 4px; margin-left: 6px;">
|
||||
Von: <input type="date" id="acc-from" class="input" value="${monthAgo}" style="width: 125px; padding: 3px 6px;" />
|
||||
</label>
|
||||
<label style="font-weight: 500; font-size: 12px; display: flex; align-items: center; gap: 4px;">
|
||||
|
||||
@@ -12,7 +12,9 @@ export class CongressTradingView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="ct-refresh-btn" class="btn">🔄 Aktualisieren</button>
|
||||
<span class="view-tag tag-trading">TRADING-MODUL · BUCH CT</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Congress Trading</span>
|
||||
<button id="ct-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
<button id="ct-scrape-btn" class="btn btn-primary" title="Löst den CT-Scrape-Worker sofort aus.">⚡ Scrape jetzt</button>
|
||||
</div>
|
||||
<div id="ct-status-text" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
|
||||
@@ -12,7 +12,9 @@ export class DashboardView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="dash-refresh-btn" class="btn">🔄 Aktualisieren</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Dashboard</span>
|
||||
<button id="dash-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
<span id="dash-mode-text" style="font-weight: 500; margin-left: 8px;"></span>
|
||||
</div>
|
||||
<div id="dash-status-text" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// ─── DebugTrading Test-Tradingmodul View ───
|
||||
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class DebugTradingView {
|
||||
constructor(container, setStatus) {
|
||||
this.container = container;
|
||||
this.setStatus = setStatus;
|
||||
this.statusData = null;
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<span class="view-tag tag-test">TEST-TRADINGMODUL · BUCH DBG</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Debug Trading</span>
|
||||
<button id="dbg-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
<button id="dbg-notaus-btn" class="btn btn-danger">🛑 Not-Aus umschalten</button>
|
||||
<button id="dbg-trigger-btn" class="btn btn-primary" title="Löst den DBG-TradeGenerator sofort aus">⚡ Trade-Generator auslösen</button>
|
||||
</div>
|
||||
<div id="dbg-status-text" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Sicherheits-Banner -->
|
||||
<div id="dbg-guard-banner" style="padding: 10px 14px; border-bottom: 1px solid var(--border-color); font-size: 12px; line-height: 1.5; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div id="dbg-guard-text">Prüfe Sicherheitsbedingungen …</div>
|
||||
<span style="font-size: 11px; color: var(--text-muted);">Zweck: Buchungsketten-Dauerlast & Neustart-Sicherheit</span>
|
||||
</div>
|
||||
|
||||
<!-- KPI Panel -->
|
||||
<div class="kpi-panel" id="dbg-kpis">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Modul-Status</div>
|
||||
<div class="kpi-value" id="kpi-dbg-enabled">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Not-Aus (Neueröffnungen)</div>
|
||||
<div class="kpi-value" id="kpi-dbg-notaus">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Offene Testpositionen</div>
|
||||
<div class="kpi-value" id="kpi-dbg-open">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Tagesbudget</div>
|
||||
<div class="kpi-value" id="kpi-dbg-budget">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Max. Nominal / Trade</div>
|
||||
<div class="kpi-value" id="kpi-dbg-max-trade">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Takt & Haltedauer</div>
|
||||
<div class="kpi-value" id="kpi-dbg-timing" style="font-size: 15px;">–</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testpositionen Tabelle -->
|
||||
<div class="table-container">
|
||||
<div style="padding: 8px 12px; font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Testpositionen (Tabelle: dbg_position – überleben Neustarts und Tagesgrenzen)</span>
|
||||
<span id="dbg-table-count" style="font-weight: normal; color: var(--text-light);">0 Einträge</span>
|
||||
</div>
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">ID</th>
|
||||
<th style="width: 130px;">Kontrakt</th>
|
||||
<th style="width: 80px;">Symbol</th>
|
||||
<th style="width: 70px;">Währung</th>
|
||||
<th class="text-right" style="width: 70px;">Stück</th>
|
||||
<th class="text-right" style="width: 90px;">Einstand</th>
|
||||
<th style="width: 130px;">Eröffnet am</th>
|
||||
<th style="width: 130px;">Fällig am (UTC)</th>
|
||||
<th style="width: 130px;">Geschlossen am</th>
|
||||
<th style="width: 90px; text-align: center;">Status</th>
|
||||
<th>Notiz / Signal-ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbg-positions-tbody">
|
||||
<tr><td colspan="11" style="text-align:center; color: var(--text-muted);">Lade Testpositionen …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('dbg-refresh-btn').addEventListener('click', () => this.refresh());
|
||||
document.getElementById('dbg-notaus-btn').addEventListener('click', () => this.toggleNotAus());
|
||||
document.getElementById('dbg-trigger-btn').addEventListener('click', () => this.triggerWorker());
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.setStatus('Lade DebugTrading-Status …');
|
||||
try {
|
||||
const data = await api.get('/api/modules/debugtrading/status');
|
||||
if (!data) return;
|
||||
this.statusData = data;
|
||||
|
||||
// Guard Banner
|
||||
const guardBanner = document.getElementById('dbg-guard-banner');
|
||||
const guardText = document.getElementById('dbg-guard-text');
|
||||
if (data.guardBlocked) {
|
||||
guardBanner.style.background = 'var(--color-danger-bg)';
|
||||
guardBanner.style.borderColor = 'var(--color-danger)';
|
||||
guardText.innerHTML = `<strong style="color: var(--color-danger);">⛔ SICHERHEITS-SPERRE:</strong> ${data.guardBlockedReason || 'Nicht im Paper-Modus'}`;
|
||||
} else {
|
||||
guardBanner.style.background = 'var(--color-success-bg)';
|
||||
guardBanner.style.borderColor = 'var(--color-success)';
|
||||
guardText.innerHTML = `<strong style="color: var(--color-success);">✓ SICHERHEITS-PRÜFUNG OK:</strong> Paper-Modus aktiv. Testbetrieb zulässig.`;
|
||||
}
|
||||
|
||||
// KPIs
|
||||
const enabledEl = document.getElementById('kpi-dbg-enabled');
|
||||
enabledEl.textContent = data.enabled ? 'AKTIV' : 'Inaktiv';
|
||||
enabledEl.style.color = data.enabled ? 'var(--color-success)' : 'var(--text-muted)';
|
||||
|
||||
const notausEl = document.getElementById('kpi-dbg-notaus');
|
||||
notausEl.textContent = data.stopOpening ? 'STOPP (Aktiv)' : 'Aus';
|
||||
notausEl.style.color = data.stopOpening ? 'var(--color-danger)' : 'var(--color-success)';
|
||||
|
||||
document.getElementById('kpi-dbg-open').textContent = `${data.openPositionsCount || 0} / ${data.maxOpenPositions || 5}`;
|
||||
document.getElementById('kpi-dbg-budget').textContent = `${Number(data.dailyBudget || 10000).toLocaleString('de-DE')} $`;
|
||||
document.getElementById('kpi-dbg-max-trade').textContent = `${Number(data.maxNotionalPerTrade || 1000).toLocaleString('de-DE')} $`;
|
||||
document.getElementById('kpi-dbg-timing').textContent = `alle ${data.intervalMinutes || 15}m | Halt: ${data.minHoldMinutes || 10}–${data.maxHoldMinutes || 4320}m`;
|
||||
|
||||
const notAusBtn = document.getElementById('dbg-notaus-btn');
|
||||
if (notAusBtn) {
|
||||
notAusBtn.textContent = data.stopOpening ? '🟢 Not-Aus aufheben' : '🛑 Not-Aus aktivieren';
|
||||
notAusBtn.className = data.stopOpening ? 'btn btn-primary' : 'btn btn-danger';
|
||||
}
|
||||
|
||||
const now = new Date().toLocaleTimeString('de-DE');
|
||||
document.getElementById('dbg-status-text').textContent = `Stand: ${now}`;
|
||||
this.setStatus(`DebugTrading aktualisiert um ${now}.`);
|
||||
|
||||
await this.loadPositions();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setStatus(`Fehler: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPositions() {
|
||||
try {
|
||||
const positions = await api.get('/api/modules/debugtrading/positions');
|
||||
const tbody = document.getElementById('dbg-positions-tbody');
|
||||
const countEl = document.getElementById('dbg-table-count');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!Array.isArray(positions) || positions.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="11" style="text-align:center; color: var(--text-muted);">Keine Testpositionen in dbg_position vorhanden.</td></tr>`;
|
||||
if (countEl) countEl.textContent = '0 Einträge';
|
||||
} else {
|
||||
if (countEl) countEl.textContent = `${positions.length} Einträge`;
|
||||
tbody.innerHTML = positions.map(p => `
|
||||
<tr>
|
||||
<td>${p.id}</td>
|
||||
<td style="font-family: var(--font-mono); font-size: 11px; font-weight: 500;">${p.contractKey}</td>
|
||||
<td style="font-weight: 600;">${p.symbol || '–'}</td>
|
||||
<td>${p.currency || '–'}</td>
|
||||
<td class="text-right">${p.quantity}</td>
|
||||
<td class="text-right">${Number(p.openPrice || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })}</td>
|
||||
<td>${p.openedAt}</td>
|
||||
<td style="font-family: var(--font-mono); font-size: 11px;">${p.closeAtUtc}</td>
|
||||
<td>${p.closedAt || '–'}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge ${p.isOpen ? 'badge-ok' : ''}">${p.isOpen ? 'OFFEN' : 'Geschlossen'}</span>
|
||||
</td>
|
||||
<td style="font-size: 11px; color: var(--text-muted);">${p.openSignalId || p.note || '–'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleNotAus() {
|
||||
const btn = document.getElementById('dbg-notaus-btn');
|
||||
btn.disabled = true;
|
||||
this.setStatus('Schalte Not-Aus um …');
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/modules/debugtrading/toggle-notaus');
|
||||
if (res && res.success) {
|
||||
alert(res.message);
|
||||
}
|
||||
await this.refresh();
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Umschalten des Not-Aus: ${e.message}`);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async triggerWorker() {
|
||||
const btn = document.getElementById('dbg-trigger-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Starte Worker …';
|
||||
this.setStatus('Löse DBG-TradeGenerator aus …');
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/modules/debugtrading/trigger');
|
||||
if (res && res.success) {
|
||||
alert(res.message || 'Worker erfolgreich ausgelöst.');
|
||||
} else {
|
||||
alert(`Fehler: ${res?.message || 'Unbekannter Fehler'}`);
|
||||
}
|
||||
await this.refresh();
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Starten des Workers: ${e.message}`);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Trade-Generator auslösen';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,10 @@ export class LogsView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<input type="text" id="log-filter-input" class="input" placeholder="Filter (z. B. CT, Core, Warn) …" style="width: 240px;" />
|
||||
<button id="log-clear-btn" class="btn">Geleert</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Logs</span>
|
||||
<input type="text" id="log-filter-input" class="input" placeholder="Filter (z. B. CT, Core, Warn) …" style="width: 240px; margin-left: 8px;" />
|
||||
<button id="log-clear-btn" class="btn">Leeren</button>
|
||||
<button id="log-copy-btn" class="btn">📋 Kopieren</button>
|
||||
<label class="checkbox-label" style="margin-left: 8px;">
|
||||
<input type="checkbox" id="log-autoscroll-chk" checked />
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
// ─── OptionsWheel Trading-Modul View ───
|
||||
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class OptionsWheelView {
|
||||
constructor(container, setStatus) {
|
||||
this.container = container;
|
||||
this.setStatus = setStatus;
|
||||
this.currentSubTab = 'watchlist';
|
||||
this.statusData = null;
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<span class="view-tag tag-trading">TRADING-MODUL · BUCH OW</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">OptionsWheel</span>
|
||||
<button id="ow-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
</div>
|
||||
<div id="ow-status-text" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Blocker-Hinweis -->
|
||||
<div id="ow-blocker-banner" style="display: none; padding: 10px 14px; background: var(--color-warning-bg); border-bottom: 1px solid var(--color-warning); color: var(--color-warning); font-size: 12px; line-height: 1.5;">
|
||||
<strong>⚠️ VORAUSSETZUNG IM CORE FEHLT:</strong> <span id="ow-blocker-msg"></span>
|
||||
</div>
|
||||
|
||||
<!-- KPI Panel -->
|
||||
<div class="kpi-panel" id="ow-kpis">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Modul-Status</div>
|
||||
<div class="kpi-value" id="kpi-ow-enabled">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Not-Aus (Neueröffnungen)</div>
|
||||
<div class="kpi-value" id="kpi-ow-notaus">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Basiswerte (Watchlist)</div>
|
||||
<div class="kpi-value" id="kpi-ow-underlyings">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Aktive Zyklen / Legs</div>
|
||||
<div class="kpi-value" id="kpi-ow-cycles-legs">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Max. Kapitalbindung</div>
|
||||
<div class="kpi-value" id="kpi-ow-capital">–</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Scan-Takt</div>
|
||||
<div class="kpi-value" id="kpi-ow-interval">–</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-Nav Tabs -->
|
||||
<div style="background: var(--bg-card); border-bottom: 1px solid var(--border-color); display: flex; gap: 4px; padding: 0 12px;">
|
||||
<button class="nav-tab active" data-subtab="watchlist" id="ow-tab-watchlist">Watchlist (Basiswerte)</button>
|
||||
<button class="nav-tab" data-subtab="cycles" id="ow-tab-cycles">Zyklen & Legs</button>
|
||||
<button class="nav-tab" data-subtab="candidates" id="ow-tab-candidates">Scan-Kandidaten</button>
|
||||
<button class="nav-tab" data-subtab="rules" id="ow-tab-rules">Regelwerk</button>
|
||||
</div>
|
||||
|
||||
<div id="ow-subcontent" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
|
||||
<!-- Subtab Content -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('ow-refresh-btn').addEventListener('click', () => this.refresh());
|
||||
|
||||
const subtabs = this.container.querySelectorAll('[data-subtab]');
|
||||
subtabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
subtabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
this.currentSubTab = tab.getAttribute('data-subtab');
|
||||
this.renderSubTab();
|
||||
});
|
||||
});
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.setStatus('Lade OptionsWheel-Status …');
|
||||
try {
|
||||
const data = await api.get('/api/modules/optionswheel/status');
|
||||
if (!data) return;
|
||||
this.statusData = data;
|
||||
|
||||
// Blocker Banner
|
||||
const blockerEl = document.getElementById('ow-blocker-banner');
|
||||
const blockerMsg = document.getElementById('ow-blocker-msg');
|
||||
if (data.blocker) {
|
||||
blockerEl.style.display = 'block';
|
||||
blockerMsg.textContent = data.blocker;
|
||||
} else {
|
||||
blockerEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// KPIs
|
||||
const enabledEl = document.getElementById('kpi-ow-enabled');
|
||||
enabledEl.textContent = data.enabled ? 'AKTIV' : 'Inaktiv';
|
||||
enabledEl.style.color = data.enabled ? 'var(--color-success)' : 'var(--text-muted)';
|
||||
|
||||
const notAusEl = document.getElementById('kpi-ow-notaus');
|
||||
notAusEl.textContent = data.noNewPositions ? 'STOPP (Aktiv)' : 'Normal';
|
||||
notAusEl.style.color = data.noNewPositions ? 'var(--color-warning)' : 'var(--color-success)';
|
||||
|
||||
document.getElementById('kpi-ow-underlyings').textContent = (data.underlyingsCount || 0).toLocaleString('de-DE');
|
||||
document.getElementById('kpi-ow-cycles-legs').textContent = `${data.activeCyclesCount || 0} / ${data.openLegsCount || 0}`;
|
||||
document.getElementById('kpi-ow-capital').textContent = `${data.maxBoundCapitalPercent || 25} %`;
|
||||
document.getElementById('kpi-ow-interval').textContent = `${data.scanIntervalMinutes || 30} min`;
|
||||
|
||||
const now = new Date().toLocaleTimeString('de-DE');
|
||||
document.getElementById('ow-status-text').textContent = `Stand: ${now}`;
|
||||
this.setStatus(`OptionsWheel aktualisiert um ${now}.`);
|
||||
|
||||
await this.renderSubTab();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setStatus(`Fehler: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async renderSubTab() {
|
||||
const sub = document.getElementById('ow-subcontent');
|
||||
if (!sub) return;
|
||||
|
||||
if (this.currentSubTab === 'watchlist') {
|
||||
sub.innerHTML = `
|
||||
<div class="table-container">
|
||||
<div style="padding: 8px 12px; font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase;">
|
||||
Definierte Basiswerte (Whitelist: Nur diese Ticker werden gehandelt)
|
||||
</div>
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 120px;">Symbol</th>
|
||||
<th style="width: 90px; text-align: center;">Status</th>
|
||||
<th style="width: 100px; text-align: center;">Gesperrt</th>
|
||||
<th>Sperrgrund</th>
|
||||
<th style="width: 130px;">Hinzugefügt</th>
|
||||
<th style="width: 130px;">Aktualisiert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ow-underlyings-tbody">
|
||||
<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Lade Watchlist …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
await this.loadUnderlyings();
|
||||
} else if (this.currentSubTab === 'cycles') {
|
||||
sub.innerHTML = `
|
||||
<div style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
|
||||
<div class="table-container" style="flex: 1; border-bottom: 1px solid var(--border-color);">
|
||||
<div style="padding: 6px 12px; font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase;">
|
||||
Wheel-Zyklen (ow_cycles)
|
||||
</div>
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">ID</th>
|
||||
<th style="width: 100px;">Symbol</th>
|
||||
<th style="width: 130px;">Start</th>
|
||||
<th style="width: 130px;">Ende</th>
|
||||
<th class="text-right">Prämie Brutto</th>
|
||||
<th class="text-right">Prämie Netto</th>
|
||||
<th class="text-right">Zuteilungskurs</th>
|
||||
<th class="text-right">Stück</th>
|
||||
<th class="text-right">Realisiert GuV</th>
|
||||
<th>Ergebnis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ow-cycles-tbody">
|
||||
<tr><td colspan="10" style="text-align:center; color: var(--text-muted);">Lade Zyklen …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container" style="flex: 1;">
|
||||
<div style="padding: 6px 12px; font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase;">
|
||||
Geschriebene Legs / Optionsbeine (ow_legs)
|
||||
</div>
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">ID</th>
|
||||
<th style="width: 70px;">Zyklus</th>
|
||||
<th style="width: 90px;">Symbol</th>
|
||||
<th style="width: 150px;">Kontrakt</th>
|
||||
<th style="width: 90px;">Verfall</th>
|
||||
<th class="text-right" style="width: 70px;">Strike</th>
|
||||
<th style="width: 50px;">Typ</th>
|
||||
<th class="text-right" style="width: 60px;">Menge</th>
|
||||
<th class="text-right" style="width: 80px;">Prämie</th>
|
||||
<th class="text-right" style="width: 60px;">Delta</th>
|
||||
<th class="text-right" style="width: 60px;">IV</th>
|
||||
<th class="text-right" style="width: 60px;">DTE</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ow-legs-tbody">
|
||||
<tr><td colspan="13" style="text-align:center; color: var(--text-muted);">Lade Legs …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
await this.loadCyclesAndLegs();
|
||||
} else if (this.currentSubTab === 'candidates') {
|
||||
sub.innerHTML = `
|
||||
<div class="table-container">
|
||||
<div style="padding: 8px 12px; font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase;">
|
||||
Strike-Kandidaten & Verwerfungsgründe (ow_candidates)
|
||||
</div>
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 120px;">Scan-Zeit</th>
|
||||
<th style="width: 90px;">Symbol</th>
|
||||
<th style="width: 160px;">Kontrakt</th>
|
||||
<th style="width: 90px;">Verfall</th>
|
||||
<th class="text-right" style="width: 70px;">Strike</th>
|
||||
<th style="width: 50px;">Typ</th>
|
||||
<th class="text-right" style="width: 60px;">Delta</th>
|
||||
<th class="text-right" style="width: 60px;">IV</th>
|
||||
<th class="text-right" style="width: 80px;">Bid / Ask</th>
|
||||
<th class="text-right" style="width: 80px;">Prämie</th>
|
||||
<th class="text-right" style="width: 90px;">Rendite p.a.</th>
|
||||
<th style="width: 70px; text-align: center;">Gewählt</th>
|
||||
<th>Verwerfungsgrund</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ow-candidates-tbody">
|
||||
<tr><td colspan="13" style="text-align:center; color: var(--text-muted);">Lade Kandidaten …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
await this.loadCandidates();
|
||||
} else if (this.currentSubTab === 'rules') {
|
||||
const r = this.statusData?.rules || {};
|
||||
sub.innerHTML = `
|
||||
<div style="padding: 24px; max-width: 800px; line-height: 1.6;">
|
||||
<h3 style="margin-bottom: 16px; font-size: 16px; color: var(--text-main);">Strategie-Regelwerk des OptionsWheel-Moduls</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 20px;">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Restlaufzeit (DTE)</div>
|
||||
<div class="kpi-value" style="font-size: 16px;">${r.minDte ?? 30} bis ${r.maxDte ?? 45} Tage</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Optimale Theta-Decay Spanne</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Delta-Band</div>
|
||||
<div class="kpi-value" style="font-size: 16px;">${r.minDelta ?? 0.15} bis ${r.maxDelta ?? 0.30}</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Konservatives OTM-Profil</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Mindestrendite p.a.</div>
|
||||
<div class="kpi-value" style="font-size: 16px;">${r.minAnnualizedReturnPercent ?? 12} %</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Schwellenwert für Eröffnung</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-caption">Roll-Gewinnziel</div>
|
||||
<div class="kpi-value" style="font-size: 16px;">${r.rollProfitTargetPercent ?? 80} %</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Vorzeitiges Schließen/Rollen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--bg-toolbar); border: 1px solid var(--border-color); border-radius: 4px; padding: 14px; font-size: 12px; color: var(--text-main);">
|
||||
<h4 style="margin-bottom: 6px; font-size: 13px;">Funktionsweise des Wheels:</h4>
|
||||
<ol style="margin-left: 18px; display: flex; flex-direction: column; gap: 6px;">
|
||||
<li><b>Schritt 1 (Cash-Secured Put):</b> Verkauf eines OTM-Puts auf einen Qualitäts-Basiswert der Watchlist gegen gesicherte Barreserve.</li>
|
||||
<li><b>Schritt 2 (Prämieneinzug oder Zuteilung):</b> Verfällt der Put wertlos, wird die Prämie verbucht. Bei Zuteilung werden die Aktien zum Strike-Kurs übernommen.</li>
|
||||
<li><b>Schritt 3 (Covered Call):</b> Auf den eingebuchten Aktienbestand werden OTM-Calls verkauft, bis die Position per Abruf gewinnbringend glattgestellt wird.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async loadUnderlyings() {
|
||||
try {
|
||||
const list = await api.get('/api/modules/optionswheel/underlyings');
|
||||
const tbody = document.getElementById('ow-underlyings-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Keine Basiswerte in der Watchlist definiert.</td></tr>`;
|
||||
} else {
|
||||
tbody.innerHTML = list.map(u => `
|
||||
<tr>
|
||||
<td style="font-weight: 600;">${u.symbol}</td>
|
||||
<td class="text-center">${u.active ? '<span class="badge badge-ok">Aktiv</span>' : '<span class="badge badge-warn">Inaktiv</span>'}</td>
|
||||
<td class="text-center">${u.halted ? '<span class="badge badge-error">Gesperrt</span>' : '–'}</td>
|
||||
<td>${u.haltReason || '–'}</td>
|
||||
<td>${u.createdAt || '–'}</td>
|
||||
<td>${u.updatedAt || '–'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCyclesAndLegs() {
|
||||
try {
|
||||
const [cycles, legs] = await Promise.all([
|
||||
api.get('/api/modules/optionswheel/cycles'),
|
||||
api.get('/api/modules/optionswheel/legs')
|
||||
]);
|
||||
|
||||
const cyclesTbody = document.getElementById('ow-cycles-tbody');
|
||||
if (cyclesTbody) {
|
||||
if (!Array.isArray(cycles) || cycles.length === 0) {
|
||||
cyclesTbody.innerHTML = `<tr><td colspan="10" style="text-align:center; color: var(--text-muted);">Keine Zyklen erfasst.</td></tr>`;
|
||||
} else {
|
||||
cyclesTbody.innerHTML = cycles.map(c => `
|
||||
<tr>
|
||||
<td>${c.id}</td>
|
||||
<td style="font-weight: 600;">${c.symbol}</td>
|
||||
<td>${c.startedAt}</td>
|
||||
<td>${c.endedAt || '–'}</td>
|
||||
<td class="text-right">${Number(c.premiumGross || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })} $</td>
|
||||
<td class="text-right">${Number(c.premiumNet || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })} $</td>
|
||||
<td class="text-right">${Number(c.assignedPrice || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })}</td>
|
||||
<td class="text-right">${c.assignedShares || 0}</td>
|
||||
<td class="text-right" style="font-weight: 600;">${c.realizedPnl !== null ? Number(c.realizedPnl).toLocaleString('de-DE', { minimumFractionDigits: 2 }) + ' $' : '–'}</td>
|
||||
<td>${c.outcome || 'Aktiv'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const legsTbody = document.getElementById('ow-legs-tbody');
|
||||
if (legsTbody) {
|
||||
if (!Array.isArray(legs) || legs.length === 0) {
|
||||
legsTbody.innerHTML = `<tr><td colspan="13" style="text-align:center; color: var(--text-muted);">Keine Legs erfasst.</td></tr>`;
|
||||
} else {
|
||||
legsTbody.innerHTML = legs.map(l => `
|
||||
<tr>
|
||||
<td>${l.id}</td>
|
||||
<td>${l.cycleId}</td>
|
||||
<td style="font-weight: 600;">${l.symbol}</td>
|
||||
<td style="font-family: var(--font-mono); font-size: 11px;">${l.contractKey}</td>
|
||||
<td>${l.expiry}</td>
|
||||
<td class="text-right">${Number(l.strike || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })}</td>
|
||||
<td><b>${l.right}</b></td>
|
||||
<td class="text-right">${l.contracts}</td>
|
||||
<td class="text-right">${Number(l.premiumOpen || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })} $</td>
|
||||
<td class="text-right">${l.deltaOpen !== null ? Number(l.deltaOpen).toLocaleString('de-DE', { minimumFractionDigits: 2 }) : '–'}</td>
|
||||
<td class="text-right">${l.ivOpen !== null ? Number(l.ivOpen).toLocaleString('de-DE', { minimumFractionDigits: 1 }) : '–'}</td>
|
||||
<td class="text-right">${l.dteOpen || '–'}</td>
|
||||
<td><span class="badge ${l.status === 'Open' ? 'badge-ok' : ''}">${l.status}</span></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCandidates() {
|
||||
try {
|
||||
const list = await api.get('/api/modules/optionswheel/candidates');
|
||||
const tbody = document.getElementById('ow-candidates-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="13" style="text-align:center; color: var(--text-muted);">Keine Scan-Kandidaten vorhanden.</td></tr>`;
|
||||
} else {
|
||||
tbody.innerHTML = list.map(c => `
|
||||
<tr>
|
||||
<td>${c.scannedAt}</td>
|
||||
<td style="font-weight: 600;">${c.symbol}</td>
|
||||
<td style="font-family: var(--font-mono); font-size: 11px;">${c.contractKey}</td>
|
||||
<td>${c.expiry}</td>
|
||||
<td class="text-right">${Number(c.strike || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })}</td>
|
||||
<td><b>${c.right}</b></td>
|
||||
<td class="text-right">${c.delta !== null ? Number(c.delta).toLocaleString('de-DE', { minimumFractionDigits: 2 }) : '–'}</td>
|
||||
<td class="text-right">${c.iv !== null ? Number(c.iv).toLocaleString('de-DE', { minimumFractionDigits: 1 }) : '–'}</td>
|
||||
<td class="text-right">${Number(c.bid || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })} / ${Number(c.ask || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })}</td>
|
||||
<td class="text-right">${Number(c.premium || 0).toLocaleString('de-DE', { minimumFractionDigits: 2 })} $</td>
|
||||
<td class="text-right" style="font-weight: 500;">${Number(c.annualizedReturnPercent || 0).toLocaleString('de-DE', { minimumFractionDigits: 1 })} %</td>
|
||||
<td class="text-center">${c.chosen ? '<span class="badge badge-ok">JA</span>' : '–'}</td>
|
||||
<td style="color: ${c.chosen ? 'var(--color-success)' : 'var(--text-muted)'};">${c.rejection || c.note || '–'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,11 @@ export class OrdersView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="ord-refresh-btn" class="btn">🔄 Aktualisieren</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Orders & Reservierungen</span>
|
||||
<button id="ord-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
<button id="ord-cancel-btn" class="btn btn-danger" disabled>❌ Order stornieren</button>
|
||||
<button id="ord-cancel-all-btn" class="btn btn-danger" style="margin-left: 12px;">⚠️ Alle stornieren (Paper)</button>
|
||||
<button id="ord-cancel-all-btn" class="btn btn-danger" style="margin-left: 8px;">⚠️ Alle stornieren (Paper)</button>
|
||||
</div>
|
||||
<div id="ord-status-summary" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,9 @@ export class PositionsView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="pos-refresh-btn" class="btn">🔄 Aktualisieren</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Positionen</span>
|
||||
<button id="pos-refresh-btn" class="btn" style="margin-left: 8px;">🔄 Aktualisieren</button>
|
||||
<label class="checkbox-label" style="margin-left: 8px;">
|
||||
<input type="checkbox" id="pos-broker-chk" checked />
|
||||
Broker abfragen (Bestandsvergleich)
|
||||
|
||||
@@ -14,7 +14,9 @@ export class SettingsView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="set-save-btn" class="btn btn-primary">💾 Speichern</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Einstellungen</span>
|
||||
<button id="set-save-btn" class="btn btn-primary" style="margin-left: 8px;">💾 Speichern</button>
|
||||
<button id="set-reload-btn" class="btn">🔄 Neu laden</button>
|
||||
</div>
|
||||
<div id="set-status-msg" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
// ─── Supervisor View ───
|
||||
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class SupervisorView {
|
||||
constructor(container, setStatus) {
|
||||
this.container = container;
|
||||
this.setStatus = setStatus;
|
||||
this.currentSubTab = 'analysis';
|
||||
this.profiles = [];
|
||||
this.signals = [];
|
||||
this.selectedSignal = null;
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.innerHTML = `
|
||||
<!-- Sub-Nav Tabs -->
|
||||
<div style="background: #ffffff; border-bottom: 1px solid var(--border-color); display: flex; gap: 4px; padding: 0 12px;">
|
||||
<button class="nav-tab active" data-subtab="analysis" id="sup-tab-analysis">Analyse</button>
|
||||
<button class="nav-tab" data-subtab="dossier" id="sup-tab-dossier">Dossier-Browser</button>
|
||||
<button class="nav-tab" data-subtab="reports" id="sup-tab-reports">Berichte</button>
|
||||
<button class="nav-tab" data-subtab="info" id="sup-tab-info">Hinweise</button>
|
||||
</div>
|
||||
|
||||
<div id="sup-subcontent" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
|
||||
<!-- Subtab content injected here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
const subtabs = this.container.querySelectorAll('[data-subtab]');
|
||||
subtabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
subtabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
this.currentSubTab = tab.getAttribute('data-subtab');
|
||||
this.renderSubTab();
|
||||
});
|
||||
});
|
||||
|
||||
await this.loadProfiles();
|
||||
await this.renderSubTab();
|
||||
}
|
||||
|
||||
async loadProfiles() {
|
||||
try {
|
||||
const data = await api.get('/api/modules/supervisor/profiles');
|
||||
if (Array.isArray(data)) {
|
||||
this.profiles = data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Konnte Supervisor-Profile nicht laden:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async renderSubTab() {
|
||||
const sub = document.getElementById('sup-subcontent');
|
||||
if (!sub) return;
|
||||
|
||||
if (this.currentSubTab === 'analysis') {
|
||||
sub.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<span style="font-weight: 500;">Profil:</span>
|
||||
<select id="sup-profile-select" class="select" style="min-width: 180px;">
|
||||
${this.profiles.map(p => `<option value="${p}">${p}</option>`).join('')}
|
||||
</select>
|
||||
<button id="sup-ask-btn" class="btn btn-primary">⚡ Fragen</button>
|
||||
</div>
|
||||
<div id="sup-ask-status" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px 14px; background: #ffffff; border-bottom: 1px solid var(--border-color);">
|
||||
<textarea id="sup-question-input" class="input" style="width: 100%; height: 72px; font-family: var(--font-family); resize: vertical;" placeholder="Frage an den Supervisor eingeben …"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="sup-answer-container" style="flex: 1; background: #14141e; color: #d2d2d2; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; padding: 14px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;">
|
||||
Wählen Sie ein Profil, geben Sie eine Frage ein und klicken Sie auf 'Fragen'.
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('sup-ask-btn').addEventListener('click', () => this.ask());
|
||||
document.getElementById('sup-question-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.ask();
|
||||
}
|
||||
});
|
||||
} else if (this.currentSubTab === 'dossier') {
|
||||
sub.innerHTML = `
|
||||
<div style="flex: 1; display: flex; overflow: hidden;">
|
||||
<!-- Left: Signals list -->
|
||||
<div style="width: 420px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; background: #ffffff;">
|
||||
<div class="toolbar" style="border-top: none;">
|
||||
<button id="sup-load-signals-btn" class="btn">🔄 Signale laden</button>
|
||||
</div>
|
||||
<div class="table-container" style="flex: 1;">
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 100px;">Zeit</th>
|
||||
<th style="width: 80px;">Symbol</th>
|
||||
<th style="width: 100px;">Modul</th>
|
||||
<th>Entscheidung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sup-signals-tbody">
|
||||
<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Signale laden …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Dossier detail markdown -->
|
||||
<div style="flex: 1; display: flex; flex-direction: column; background: #14141e;">
|
||||
<div style="padding: 8px 12px; background: #1e1e2d; border-bottom: 1px solid #2d2d3f; color: #a1a1aa; font-size: 11px; display: flex; justify-content: space-between;">
|
||||
<span id="sup-dossier-header">Dossier-Details</span>
|
||||
</div>
|
||||
<div id="sup-dossier-body" style="flex: 1; overflow-y: auto; padding: 14px; color: #d2d2d2; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; white-space: pre-wrap;">
|
||||
Signal in der linken Tabelle auswählen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('sup-load-signals-btn').addEventListener('click', () => this.loadSignals());
|
||||
await this.loadSignals();
|
||||
} else if (this.currentSubTab === 'reports') {
|
||||
sub.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<button id="sup-load-reports-btn" class="btn">🔄 Berichte laden</button>
|
||||
<div id="sup-reports-status" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="datagrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 140px;">Erstellt</th>
|
||||
<th style="width: 120px;">Profil</th>
|
||||
<th style="width: 160px;">Modell</th>
|
||||
<th>Frage</th>
|
||||
<th style="width: 60px; text-align: center;">Tools</th>
|
||||
<th style="width: 110px;">Tokens</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sup-reports-tbody">
|
||||
<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Lade Berichte …</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('sup-load-reports-btn').addEventListener('click', () => this.loadReports());
|
||||
await this.loadReports();
|
||||
} else if (this.currentSubTab === 'info') {
|
||||
sub.innerHTML = `
|
||||
<div style="padding: 24px; max-width: 800px; color: var(--text-main); line-height: 1.6;">
|
||||
<h3 style="margin-bottom: 12px; font-size: 16px;">Supervisor Modul-Hinweise</h3>
|
||||
<div id="sup-info-content">Lade Modul-Informationen …</div>
|
||||
</div>
|
||||
`;
|
||||
await this.loadInfo();
|
||||
}
|
||||
}
|
||||
|
||||
async ask() {
|
||||
const qEl = document.getElementById('sup-question-input');
|
||||
const question = qEl?.value?.trim();
|
||||
if (!question) {
|
||||
alert('Bitte eine Frage eingeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pEl = document.getElementById('sup-profile-select');
|
||||
const profile = pEl?.value;
|
||||
const btn = document.getElementById('sup-ask-btn');
|
||||
const ansEl = document.getElementById('sup-answer-container');
|
||||
const statusEl = document.getElementById('sup-ask-status');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Analysiere …';
|
||||
if (statusEl) statusEl.textContent = 'Agent denkt nach …';
|
||||
ansEl.textContent = 'Agent führt Tool-Aufrufe und Analysen durch … Bitte warten.';
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/modules/supervisor/ask', { question, profile });
|
||||
if (res && res.success) {
|
||||
let output = res.answer || '(Keine Antwort erhalten)';
|
||||
if (res.toolCalls && res.toolCalls.length > 0) {
|
||||
output += '\n\n── Ausgeführte Tool-Calls ──\n' +
|
||||
res.toolCalls.map(tc => `• ${tc.tool}: ${JSON.stringify(tc.arguments)}`).join('\n');
|
||||
}
|
||||
ansEl.textContent = output;
|
||||
if (statusEl) statusEl.textContent = `Fertig (${res.toolCallCount || 0} Tools genutzt).`;
|
||||
} else {
|
||||
ansEl.textContent = `Fehler: ${res?.message || 'Unbekannter Fehler'}`;
|
||||
}
|
||||
} catch (e) {
|
||||
ansEl.textContent = `Fehler bei der Abfrage: ${e.message}`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Fragen';
|
||||
}
|
||||
}
|
||||
|
||||
async loadSignals() {
|
||||
this.setStatus('Lade Signale …');
|
||||
try {
|
||||
const signals = await api.get('/api/modules/supervisor/signals?limit=100');
|
||||
this.signals = Array.isArray(signals) ? signals : [];
|
||||
const tbody = document.getElementById('sup-signals-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (this.signals.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Keine Signale gefunden.</td></tr>`;
|
||||
} else {
|
||||
tbody.innerHTML = this.signals.map((s, idx) => `
|
||||
<tr data-idx="${idx}" class="${this.selectedSignal && this.selectedSignal.signalId === s.signalId ? 'selected' : ''}">
|
||||
<td>${s.firstSeen ? s.firstSeen.substring(5, 16) : '–'}</td>
|
||||
<td style="font-weight: 600;">${s.symbol || '–'}</td>
|
||||
<td>${s.module || '–'}</td>
|
||||
<td>${s.lastDecision || '–'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
tbody.querySelectorAll('tr').forEach(tr => {
|
||||
tr.addEventListener('click', () => {
|
||||
const idx = parseInt(tr.getAttribute('data-idx'), 10);
|
||||
this.selectSignal(this.signals[idx]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.setStatus(`Signale geladen (${this.signals.length}).`);
|
||||
} catch (e) {
|
||||
this.setStatus(`Fehler: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async selectSignal(s) {
|
||||
this.selectedSignal = s;
|
||||
const headerEl = document.getElementById('sup-dossier-header');
|
||||
const bodyEl = document.getElementById('sup-dossier-body');
|
||||
if (!bodyEl) return;
|
||||
|
||||
if (headerEl) headerEl.textContent = `Dossier: ${s.symbol} (${s.signalId})`;
|
||||
bodyEl.textContent = 'Lade Dossier …';
|
||||
|
||||
try {
|
||||
const data = await api.get(`/api/modules/supervisor/dossier/${encodeURIComponent(s.signalId)}`);
|
||||
if (data && data.markdown) {
|
||||
bodyEl.textContent = data.markdown;
|
||||
} else {
|
||||
bodyEl.textContent = 'Kein Dossier für dieses Signal verfügbar.';
|
||||
}
|
||||
} catch (e) {
|
||||
bodyEl.textContent = `Fehler beim Laden des Dossiers: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async loadReports() {
|
||||
this.setStatus('Lade Berichte …');
|
||||
try {
|
||||
const reports = await api.get('/api/modules/supervisor/reports?limit=100');
|
||||
const tbody = document.getElementById('sup-reports-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!Array.isArray(reports) || reports.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Keine Berichte vorhanden.</td></tr>`;
|
||||
} else {
|
||||
tbody.innerHTML = reports.map(r => `
|
||||
<tr title="${(r.answer || '').replace(/"/g, '"')}">
|
||||
<td>${r.createdAt}</td>
|
||||
<td><b>${r.profile}</b></td>
|
||||
<td style="font-family: var(--font-mono); font-size: 11px;">${r.model}</td>
|
||||
<td>${r.question}</td>
|
||||
<td class="text-center">${r.toolCallCount}</td>
|
||||
<td style="font-size: 11px;">${r.tokens}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
this.setStatus(`Berichte geladen (${reports ? reports.length : 0}).`);
|
||||
} catch (e) {
|
||||
this.setStatus(`Fehler: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadInfo() {
|
||||
try {
|
||||
const info = await api.get('/api/modules/supervisor/info');
|
||||
const el = document.getElementById('sup-info-content');
|
||||
if (!el || !info) return;
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="margin-bottom: 16px; padding: 12px; border-radius: 4px; border: 1px solid ${info.openRouterKeyConfigured ? '#86efac' : '#fde68a'}; background: ${info.openRouterKeyConfigured ? '#f0fdf4' : '#fffbeb'}; color: ${info.openRouterKeyConfigured ? '#15803d' : '#b45309'};">
|
||||
<b>OpenRouter API-Key:</b> ${info.openRouterKeyConfigured ? '✅ Konfiguriert' : '⚠️ Nicht gesetzt (in settings.json oder OPENROUTER_API_KEY hinterlegen)'}
|
||||
</div>
|
||||
<p style="margin-bottom: 8px;"><b>Zeitzone:</b> ${info.timeZone || '–'}</p>
|
||||
<p style="margin-bottom: 8px;"><b>Beschreibung:</b> ${info.description || '–'}</p>
|
||||
`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ export class WorkersView {
|
||||
this.container.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="workers-trigger-btn" class="btn btn-primary" disabled>⚡ Jetzt ausführen</button>
|
||||
<span class="view-tag tag-core">CORE</span>
|
||||
<span style="font-weight: 600; font-size: 13px; margin-left: 4px;">Worker / Services</span>
|
||||
<button id="workers-trigger-btn" class="btn btn-primary" style="margin-left: 8px;" disabled>⚡ Jetzt ausführen</button>
|
||||
<button id="workers-refresh-btn" class="btn">🔄 Aktualisieren</button>
|
||||
</div>
|
||||
<div id="workers-status-text" style="color: var(--text-muted); font-size: 11px;"></div>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Hosting.Web.Endpoints;
|
||||
using IBKRTrader.Modules.CongressTrading;
|
||||
using IBKRTrader.Modules.DebugTrading;
|
||||
using IBKRTrader.Modules.OptionsWheel;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader.Tests.Web;
|
||||
|
||||
[Trait("cat", "unit")]
|
||||
public class ModuleEndpointsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_RegistersEndpoints_ForConfiguredModules()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IModule>(new CongressTradingModule());
|
||||
services.AddSingleton<IModule>(new OptionsWheelModule());
|
||||
services.AddSingleton<IModule>(new DebugTradingModule());
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
var app = builder.Build();
|
||||
var api = app.MapGroup("/api");
|
||||
|
||||
// Act
|
||||
ModuleEndpoints.Map(api, sp);
|
||||
|
||||
// Assert - Verify registered endpoints
|
||||
var routeBuilder = (IEndpointRouteBuilder)app;
|
||||
var patterns = routeBuilder.DataSources
|
||||
.SelectMany(ds => ds.Endpoints)
|
||||
.OfType<RouteEndpoint>()
|
||||
.Select(e => e.RoutePattern.RawText)
|
||||
.ToList();
|
||||
|
||||
patterns.Should().Contain(p => p != null && p.Contains("/modules/congresstrading"));
|
||||
patterns.Should().Contain(p => p != null && p.Contains("/modules/optionswheel/status"));
|
||||
patterns.Should().Contain(p => p != null && p.Contains("/modules/optionswheel/underlyings"));
|
||||
patterns.Should().Contain(p => p != null && p.Contains("/modules/debugtrading/status"));
|
||||
patterns.Should().Contain(p => p != null && p.Contains("/modules/debugtrading/positions"));
|
||||
|
||||
// Verify Supervisor is NOT registered
|
||||
patterns.Should().NotContain(p => p != null && p.Contains("/supervisor"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_DoesNotRegisterEndpoints_WhenModulesOmitted()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
// Empty modules list
|
||||
var sp = services.BuildServiceProvider();
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
var app = builder.Build();
|
||||
var api = app.MapGroup("/api");
|
||||
|
||||
// Act
|
||||
ModuleEndpoints.Map(api, sp);
|
||||
|
||||
// Assert - None of the module endpoints should be mapped
|
||||
var routeBuilder = (IEndpointRouteBuilder)app;
|
||||
var patterns = routeBuilder.DataSources
|
||||
.SelectMany(ds => ds.Endpoints)
|
||||
.OfType<RouteEndpoint>()
|
||||
.Select(e => e.RoutePattern.RawText)
|
||||
.ToList();
|
||||
|
||||
patterns.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user