WebUI: Trading beenden, ohne die Anwendung zu beenden
Umsetzung von #563. Entscheidung Richard, 13.09.2026: Beenden soll ueber das WebUI gehen, aber beschraenkt auf das Trading. Ein Ende der ganzen Anwendung naehme den Webserver mit; danach waere das WebUI nicht mehr erreichbar. POST /api/dashboard/stop-trading setzt Live- und Demo-Modus auf Inaktiv und schaltet die Pause ein. Damit halten CopyTrading und ResolutionFarming (#569) an, wie beim Ende der Anwendung. Offene Orders an der Boerse werden bewusst nicht storniert: Das tut auch das Beenden der App nicht, und ein Eingriff in die CLOB-Integration gehoert nicht in einen Notschalter. Der Dialog sagt das ausdruecklich. Die Route verlangt {"confirmed": true}. Ein Aufruf ohne Absicht, etwa aus einem alten Skript, beendet nichts. Der Vorgang wird als Warnung protokolliert, mit den Modi vorher. Im Dashboard steht unter der Notfall-Pause jetzt "Trading beenden ...". Der Dialog ist wie im Desktop 10 Sekunden gesperrt, Abbrechen und Esc gehen jederzeit. Wieder aufgenommen wird nur ueber die Modus-Schalter. Die Pause aufzuheben genuegt nicht, das haelt ein Test fest. Offen bis #570: Laufende Verkaufsleitern und der REST-Pre-Redeem pruefen die Pause nicht. Der Inaktiv-Modus, den dieser Knopf setzt, sperrt den Pre-Redeem bereits; die Leitern friert erst #570 ein. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,45 @@ namespace PolyTrader.Web.Endpoints
|
|||||||
return Results.BadRequest(new { error = "Ungültiger Modus oder Parametertyp." });
|
return Results.BadRequest(new { error = "Ungültiger Modus oder Parametertyp." });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Trading beenden (#563) - das WebUI-Gegenstueck zu "Beenden" im Desktop, beschraenkt auf das
|
||||||
|
// Trading. Ein Ende der ganzen Anwendung naehme den Webserver mit; danach waere das WebUI nicht
|
||||||
|
// mehr erreichbar, und nur ein Zugang zur Maschine braechte es zurueck.
|
||||||
|
//
|
||||||
|
// Beide Modi auf Inaktiv und die Pause an: Damit halten CopyTrading (Engine, Monitor,
|
||||||
|
// Verkaufsleitern) und ResolutionFarming (#569) an, wie beim Ende der Anwendung. Offene Orders
|
||||||
|
// an der Boerse werden bewusst NICHT storniert. Das tut auch das Beenden der App nicht, und
|
||||||
|
// ein Eingriff in die CLOB-Integration gehoert nicht in einen Notschalter. Wieder
|
||||||
|
// aufgenommen wird nur ausdruecklich ueber die Modus-Schalter.
|
||||||
|
//
|
||||||
|
// "confirmed" ist Pflicht: Ein Aufruf ohne Absicht, etwa aus einem alten Skript oder einem
|
||||||
|
// doppelt abgeschickten Formular, soll das Trading nicht beenden.
|
||||||
|
group.MapPost("/stop-trading", (StopTradingRequest? req, TradingState state, TerminalLogger logger) =>
|
||||||
|
{
|
||||||
|
if (req?.Confirmed != true)
|
||||||
|
return Results.BadRequest(new { error = "Bestätigung fehlt – das Trading wurde nicht beendet." });
|
||||||
|
|
||||||
|
string liveBefore = state.LiveTradingMode.ToString();
|
||||||
|
string demoBefore = state.DemoTradingMode.ToString();
|
||||||
|
bool pausedBefore = state.GlobalTradingPaused;
|
||||||
|
|
||||||
|
state.LiveTradingMode = TradingMode.Inactive;
|
||||||
|
state.DemoTradingMode = TradingMode.Inactive;
|
||||||
|
state.GlobalTradingPaused = true;
|
||||||
|
|
||||||
|
logger.Warning($"🛑 WebUI: Trading beendet (Live {liveBefore} → Inactive, Demo {demoBefore} → Inactive, " +
|
||||||
|
$"Pause {(pausedBefore ? "war schon an" : "an")}). Anwendung und WebUI laufen weiter; " +
|
||||||
|
"offene Orders an der Börse bleiben stehen.");
|
||||||
|
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
liveTradingMode = state.LiveTradingMode.ToString(),
|
||||||
|
demoTradingMode = state.DemoTradingMode.ToString(),
|
||||||
|
globalTradingPaused = state.GlobalTradingPaused,
|
||||||
|
stoppedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group.MapGet("/overview-widgets", (IServiceProvider sp) =>
|
group.MapGet("/overview-widgets", (IServiceProvider sp) =>
|
||||||
{
|
{
|
||||||
var tradeLog = sp.GetService<ITradeLogRepository>();
|
var tradeLog = sp.GetService<ITradeLogRepository>();
|
||||||
@@ -251,5 +290,7 @@ namespace PolyTrader.Web.Endpoints
|
|||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record StopTradingRequest(bool Confirmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,11 @@
|
|||||||
<button class="btn btn-danger" id="btnPause" style="width: 100%; margin-top: 6px;" onclick="togglePause()">
|
<button class="btn btn-danger" id="btnPause" style="width: 100%; margin-top: 6px;" onclick="togglePause()">
|
||||||
Trading pausieren
|
Trading pausieren
|
||||||
</button>
|
</button>
|
||||||
|
<!-- #563: Gegenstueck zu "Beenden" im Desktop, beschraenkt auf das Trading. Die ganze
|
||||||
|
Anwendung zu beenden, naehme den Webserver mit. -->
|
||||||
|
<button class="btn btn-secondary" id="btnStopTrading" style="width: 100%; margin-top: 8px;" onclick="openStopTradingModal()">
|
||||||
|
🛑 Trading beenden …
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1176,6 +1181,38 @@ Wähle ein Profil und stelle mir eine Frage zu deinen Positionen, Risiken oder d
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- MODAL: TRADING BEENDEN (#563) -->
|
||||||
|
<!-- Dieselbe Sicherheitsabfrage wie beim Beenden im Desktop: Die Bestaetigung ist 10 Sekunden
|
||||||
|
gesperrt, Abbrechen geht jederzeit. -->
|
||||||
|
<div id="modalStopTrading" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="stopTradingTitle">
|
||||||
|
<div class="modal-dialog" style="max-width: 520px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="stopTradingTitle" style="margin: 0; font-size: 16px;">Trading beenden</h3>
|
||||||
|
<button class="modal-close" onclick="closeStopTradingModal()" aria-label="Schließen">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="display: flex; flex-direction: column; gap: 12px; font-size: 14px; line-height: 1.55;">
|
||||||
|
<p style="margin: 0;">Das beendet das Trading aller Module:</p>
|
||||||
|
<ul style="margin: 0; padding-left: 20px;">
|
||||||
|
<li>Live- und Demo-Modus werden auf <b>Inaktiv</b> gesetzt, die Pause wird eingeschaltet.</li>
|
||||||
|
<li>Es werden keine neuen Positionen eröffnet und keine Verkäufe mehr nachgepreist.</li>
|
||||||
|
<li><b>Anwendung und WebUI laufen weiter.</b></li>
|
||||||
|
</ul>
|
||||||
|
<div class="rate-warning" style="margin: 0;">
|
||||||
|
Offene Orders an der Börse bleiben stehen – genau wie beim Beenden der Anwendung.
|
||||||
|
Sie werden nicht storniert.
|
||||||
|
</div>
|
||||||
|
<p style="margin: 0; color: var(--muted); font-size: 13px;">
|
||||||
|
Wieder aufgenommen wird das Trading nur ausdrücklich über die Modus-Schalter.
|
||||||
|
</p>
|
||||||
|
<div id="stopTradingError" class="rate-warning" style="margin: 0;" hidden></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px; padding: 14px 20px 20px;">
|
||||||
|
<button class="btn btn-secondary" onclick="closeStopTradingModal()">Abbrechen</button>
|
||||||
|
<button class="btn btn-danger" id="btnConfirmStopTrading" onclick="confirmStopTrading()" disabled>Trading beenden (10 s)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- MODAL: MASTER TRADER EDIT/ADD -->
|
<!-- MODAL: MASTER TRADER EDIT/ADD -->
|
||||||
<div id="modalTrader" class="modal-overlay" style="display: none;">
|
<div id="modalTrader" class="modal-overlay" style="display: none;">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ let allLogs = [];
|
|||||||
|
|
||||||
// Init on DOM ready
|
// Init on DOM ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && document.getElementById('modalStopTrading')?.style.display === 'flex') closeStopTradingModal();
|
||||||
|
});
|
||||||
initNavMenus();
|
initNavMenus();
|
||||||
refreshNavModules();
|
refreshNavModules();
|
||||||
initSseLogs();
|
initSseLogs();
|
||||||
@@ -269,6 +272,64 @@ async function togglePause() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Trading beenden (#563) ====================
|
||||||
|
|
||||||
|
const STOP_TRADING_LOCK_SECONDS = 10;
|
||||||
|
let stopTradingTimer = null;
|
||||||
|
|
||||||
|
function openStopTradingModal() {
|
||||||
|
const modal = document.getElementById('modalStopTrading');
|
||||||
|
const btn = document.getElementById('btnConfirmStopTrading');
|
||||||
|
const err = document.getElementById('stopTradingError');
|
||||||
|
if (!modal || !btn) return;
|
||||||
|
|
||||||
|
err.hidden = true;
|
||||||
|
let remaining = STOP_TRADING_LOCK_SECONDS;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = `Trading beenden (${remaining} s)`;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
clearInterval(stopTradingTimer);
|
||||||
|
stopTradingTimer = setInterval(() => {
|
||||||
|
remaining--;
|
||||||
|
if (remaining > 0) {
|
||||||
|
btn.textContent = `Trading beenden (${remaining} s)`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(stopTradingTimer);
|
||||||
|
stopTradingTimer = null;
|
||||||
|
btn.textContent = 'Trading beenden';
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStopTradingModal() {
|
||||||
|
clearInterval(stopTradingTimer);
|
||||||
|
stopTradingTimer = null;
|
||||||
|
const modal = document.getElementById('modalStopTrading');
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStopTrading() {
|
||||||
|
const btn = document.getElementById('btnConfirmStopTrading');
|
||||||
|
const err = document.getElementById('stopTradingError');
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/dashboard/stop-trading', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ confirmed: true })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await errorText(res));
|
||||||
|
closeStopTradingModal();
|
||||||
|
refreshDashboard();
|
||||||
|
} catch (e) {
|
||||||
|
err.textContent = `Trading konnte nicht beendet werden: ${e.message}`;
|
||||||
|
err.hidden = false;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dashboard Account-Übersicht
|
// Dashboard Account-Übersicht
|
||||||
async function loadDashboardAccounts() {
|
async function loadDashboardAccounts() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PolyTraderSharp;
|
||||||
|
using PolyTraderSharp.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests.WebUi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trading beenden über das WebUI, ohne die Anwendung zu beenden (#563).
|
||||||
|
/// </summary>
|
||||||
|
public class StopTradingEndpointTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Beenden_setzt_beide_Modi_auf_Inaktiv_und_die_Pause_an()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
web.State.LiveTradingMode = TradingMode.Active;
|
||||||
|
web.State.DemoTradingMode = TradingMode.SellOnly;
|
||||||
|
|
||||||
|
var (status, body) = await web.PostJsonAsync("/api/dashboard/stop-trading", new { confirmed = true });
|
||||||
|
|
||||||
|
Assert.Equal(200, status);
|
||||||
|
Assert.Equal(TradingMode.Inactive, web.State.LiveTradingMode);
|
||||||
|
Assert.Equal(TradingMode.Inactive, web.State.DemoTradingMode);
|
||||||
|
Assert.True(web.State.GlobalTradingPaused);
|
||||||
|
Assert.True(body.GetProperty("globalTradingPaused").GetBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Ohne_Bestaetigung_bleibt_alles_wie_es_ist()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
web.State.LiveTradingMode = TradingMode.Active;
|
||||||
|
|
||||||
|
var (withoutBody, _) = await web.PostJsonAsync("/api/dashboard/stop-trading");
|
||||||
|
var (notConfirmed, _) = await web.PostJsonAsync("/api/dashboard/stop-trading", new { confirmed = false });
|
||||||
|
|
||||||
|
Assert.Equal(400, withoutBody);
|
||||||
|
Assert.Equal(400, notConfirmed);
|
||||||
|
Assert.Equal(TradingMode.Active, web.State.LiveTradingMode);
|
||||||
|
Assert.False(web.State.GlobalTradingPaused);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Anwendung_und_WebUI_bleiben_danach_erreichbar()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
await web.PostJsonAsync("/api/dashboard/stop-trading", new { confirmed = true });
|
||||||
|
var summary = await web.GetJsonAsync("/api/dashboard/summary");
|
||||||
|
|
||||||
|
Assert.True(summary.GetProperty("globalTradingPaused").GetBoolean());
|
||||||
|
Assert.Equal("Inactive", summary.GetProperty("liveTradingMode").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Beenden_wird_als_Warnung_protokolliert()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
await web.PostJsonAsync("/api/dashboard/stop-trading", new { confirmed = true });
|
||||||
|
|
||||||
|
var entry = web.Logger.GetHistory(TimeSpan.FromMinutes(1))
|
||||||
|
.LastOrDefault(e => e.Message.Contains("Trading beendet", StringComparison.Ordinal));
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
Assert.Equal(LogLevel.Warning, entry!.Level);
|
||||||
|
Assert.Contains("Orders an der Börse bleiben stehen", entry.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Wiederaufnahme_nur_ausdruecklich_ueber_die_Modus_Schalter()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
await web.PostJsonAsync("/api/dashboard/stop-trading", new { confirmed = true });
|
||||||
|
|
||||||
|
// Die Pause aufzuheben genuegt nicht: Die Modi stehen weiter auf Inaktiv.
|
||||||
|
await web.PostJsonAsync("/api/dashboard/trading-mode", new { modeType = "pause", targetMode = "" });
|
||||||
|
Assert.False(web.State.GlobalTradingPaused);
|
||||||
|
Assert.Equal(TradingMode.Inactive, web.State.LiveTradingMode);
|
||||||
|
Assert.Equal(TradingMode.Inactive, web.State.DemoTradingMode);
|
||||||
|
|
||||||
|
var (status, _) = await web.PostJsonAsync("/api/dashboard/trading-mode", new { modeType = "demo", targetMode = "Active" });
|
||||||
|
Assert.Equal(200, status);
|
||||||
|
Assert.Equal(TradingMode.Active, web.State.DemoTradingMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user