|
|
@@ -7,9 +7,9 @@ from fastapi import APIRouter, Form
|
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
|
|
from .exec_client import list_accounts
|
|
|
-from .strategy_engine import pause_strategy, reconcile_instance, resume_strategy, get_running_strategy
|
|
|
+from .strategy_engine import get_running_strategy, pause_strategy, reconcile_instance, render_strategy, resume_strategy
|
|
|
from .strategy_registry import get_strategy_default_config, list_available_strategy_modules
|
|
|
-from .strategy_store import add_strategy_instance, delete_strategy_instance, list_strategy_instances, synthesize_client_id, update_strategy_mode, update_strategy_name
|
|
|
+from .strategy_store import add_strategy_instance, delete_strategy_instance, list_strategy_instances, synthesize_client_id, update_strategy_config, update_strategy_mode, update_strategy_name
|
|
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
|
|
|
@@ -61,12 +61,28 @@ def dashboard_home():
|
|
|
<td>{mode}</td>
|
|
|
<td>{config}</td>
|
|
|
<td class="actions">
|
|
|
+ <button type="button" class="ghost" onclick="toggleDetails('{id}')">Details</button>
|
|
|
<form method="post" action="/dashboard/strategies/{id}/power"><button type="submit" class="ghost">{power_label}</button></form>
|
|
|
<form method="post" action="/dashboard/strategies/{id}/activation"><button type="submit" {activation_disabled}>{activation_label}</button></form>
|
|
|
<form method="post" action="/dashboard/strategies/{id}/pause"><button type="submit" {pause_disabled}>{pause_label}</button></form>
|
|
|
<form method="post" action="/dashboard/strategies/{id}/delete"><button type="submit" class="danger">Delete</button></form>
|
|
|
</td>
|
|
|
</tr>
|
|
|
+ <tr id="details-{id}" class="detail-row" style="display:none;">
|
|
|
+ <td colspan="7">
|
|
|
+ <div style="margin-top:10px; display:grid; gap:12px;">
|
|
|
+ <div>
|
|
|
+ <strong>Render</strong>
|
|
|
+ <div class="render-panel" data-strategy-id="{id}" style="background:#f9fafb; padding:10px; border-radius:8px; border:1px solid #e5e7eb;">(loading)</div>
|
|
|
+ </div>
|
|
|
+ <form method="post" action="/dashboard/strategies/{id}/config" style="display:grid; gap:8px;">
|
|
|
+ <strong>Edit config</strong>
|
|
|
+ <textarea name="config_json" rows="6" style="width:100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;">{config_json}</textarea>
|
|
|
+ <button type="submit">Save config</button>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
""".strip().format(
|
|
|
indicator=("🔵 paused" if (get_running_strategy(s.id).paused if get_running_strategy(s.id) else False) else ("" if (s.mode or "off") == "off" else ("🟡 observe" if s.mode == "observe" else "✅ active"))),
|
|
|
id=s.id,
|
|
|
@@ -75,6 +91,7 @@ def dashboard_home():
|
|
|
account_name=account_lookup.get(s.account_id, s.account_id),
|
|
|
mode=s.mode,
|
|
|
config=s.config,
|
|
|
+ config_json=json.dumps(s.config, indent=2, sort_keys=True),
|
|
|
power_label="Turn on" if s.mode == "off" else "Turn off",
|
|
|
activation_label="Activate" if s.mode != "active" else "Deactivate",
|
|
|
activation_disabled="disabled" if s.mode == "off" or (get_running_strategy(s.id).paused if get_running_strategy(s.id) else False) else "",
|
|
|
@@ -100,14 +117,13 @@ def dashboard_home():
|
|
|
th, td {{ border-bottom: 1px solid #e5e7eb; padding: 10px 8px; text-align: left; vertical-align: top; }}
|
|
|
th {{ background: #f9fafb; }}
|
|
|
.pill {{ display:inline-block; padding:2px 10px; border-radius:999px; background:#f3f4f6; font-size: 0.9em; }}
|
|
|
- details {{ margin: 14px 0; }}
|
|
|
- summary {{ cursor: pointer; font-weight: 600; }}
|
|
|
- .actions {{ display: flex; gap: 8px; flex-wrap: wrap; }}
|
|
|
+ .actions {{ display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }}
|
|
|
.actions form {{ display: inline; }}
|
|
|
button {{ border: 1px solid #d1d5db; background: white; border-radius: 8px; padding: 8px 10px; cursor: pointer; }}
|
|
|
button.danger {{ background: #fee2e2; border-color: #fecaca; }}
|
|
|
button.ghost {{ background: #f9fafb; }}
|
|
|
- input, select {{ padding: 8px 10px; border-radius: 8px; border: 1px solid #d1d5db; }}
|
|
|
+ input, select, textarea {{ padding: 8px 10px; border-radius: 8px; border: 1px solid #d1d5db; }}
|
|
|
+ .render-panel {{ min-height: 24px; }}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -142,7 +158,7 @@ def dashboard_home():
|
|
|
</table>
|
|
|
</section>
|
|
|
|
|
|
- <details>
|
|
|
+ <details id="accounts-section">
|
|
|
<summary>Accounts</summary>
|
|
|
<p class="muted">exec-mcp accounts</p>
|
|
|
|
|
|
@@ -160,6 +176,87 @@ def dashboard_home():
|
|
|
|
|
|
<p class="muted" style="margin-top: 12px;">Source: <span class="pill">exec-mcp.list_accounts</span></p>
|
|
|
</div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ function renderWidget(widget) {{
|
|
|
+ if (!widget || !widget.type) return '';
|
|
|
+ if (widget.type === 'metric') {{
|
|
|
+ return `<div style="display:inline-block; min-width:140px; padding:10px 12px; margin:6px 8px 6px 0; background:#fff; border:1px solid #dbe1e8; border-radius:10px;"><div class="muted" style="font-size:12px; margin-bottom:4px;">${{widget.label || 'metric'}}</div><div style="font-size:20px; font-weight:700;">${{widget.value ?? ''}}</div></div>`;
|
|
|
+ }}
|
|
|
+ if (widget.type === 'text') {{
|
|
|
+ return `<div style="padding:8px 0;">${{widget.label ? `<strong>${{widget.label}}:</strong> ` : ''}}${{widget.value ?? ''}}</div>`;
|
|
|
+ }}
|
|
|
+ if (widget.type === 'line_chart') {{
|
|
|
+ const data = Array.isArray(widget.data) ? widget.data : [];
|
|
|
+ return `<div style="padding:8px 0;"><strong>${{widget.label || 'chart'}}</strong><pre style="white-space:pre-wrap; margin:6px 0 0;">${{JSON.stringify(data, null, 2)}}</pre></div>`;
|
|
|
+ }}
|
|
|
+ return `<pre style="white-space:pre-wrap; margin:0;">${{JSON.stringify(widget, null, 2)}}</pre>`;
|
|
|
+ }}
|
|
|
+
|
|
|
+ async function refreshRender(panel) {{
|
|
|
+ const id = panel.dataset.strategyId;
|
|
|
+ try {{
|
|
|
+ const res = await fetch(`/dashboard/strategies/${{encodeURIComponent(id)}}/render`);
|
|
|
+ const data = await res.json();
|
|
|
+ if (data.paused) {{
|
|
|
+ panel.innerHTML = '<div class="muted">paused</div>';
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+ const render = data.render;
|
|
|
+ if (!render || !render.widgets || !render.widgets.length) {{
|
|
|
+ panel.innerHTML = '<div class="muted">(no render)</div>';
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+ panel.innerHTML = render.widgets.map(renderWidget).join('');
|
|
|
+ }} catch (err) {{
|
|
|
+ panel.innerHTML = '<div class="muted">(render unavailable)</div>';
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ function refreshAll() {{
|
|
|
+ document.querySelectorAll('.render-panel').forEach(refreshRender);
|
|
|
+ }}
|
|
|
+
|
|
|
+ function toggleDetails(id) {{
|
|
|
+ const row = document.getElementById(`details-${{id}}`);
|
|
|
+ if (!row) return;
|
|
|
+ row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
|
|
|
+ localStorage.setItem(`trader-mcp:detail:${{id}}`, row.style.display === 'table-row' ? 'open' : 'closed');
|
|
|
+ if (row.style.display === 'table-row') {{
|
|
|
+ const panel = row.querySelector('.render-panel');
|
|
|
+ if (panel) refreshRender(panel);
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ function restoreDetails() {{
|
|
|
+ document.querySelectorAll('.detail-row').forEach((row) => {{
|
|
|
+ const id = row.id.replace(/^details-/, '');
|
|
|
+ const open = localStorage.getItem(`trader-mcp:detail:${{id}}`) === 'open';
|
|
|
+ row.style.display = open ? 'table-row' : 'none';
|
|
|
+ if (open) {{
|
|
|
+ const panel = row.querySelector('.render-panel');
|
|
|
+ if (panel) refreshRender(panel);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+
|
|
|
+ const accounts = document.getElementById('accounts-section');
|
|
|
+ if (accounts) {{
|
|
|
+ const open = localStorage.getItem('trader-mcp:accounts-section') === 'open';
|
|
|
+ accounts.open = open;
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ const accountsSection = document.getElementById('accounts-section');
|
|
|
+ if (accountsSection) {{
|
|
|
+ accountsSection.addEventListener('toggle', () => {{
|
|
|
+ localStorage.setItem('trader-mcp:accounts-section', accountsSection.open ? 'open' : 'closed');
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
+ restoreDetails();
|
|
|
+ refreshAll();
|
|
|
+ setInterval(refreshAll, 6000);
|
|
|
+ </script>
|
|
|
</body>
|
|
|
</html>"""
|
|
|
|
|
|
@@ -173,7 +270,7 @@ def dashboard_strategies_add(
|
|
|
strategy_id = str(uuid4())
|
|
|
default_config = get_strategy_default_config(strategy_type.strip())
|
|
|
client_id = synthesize_client_id(strategy_type.strip(), strategy_id, name.strip())
|
|
|
- record = add_strategy_instance(
|
|
|
+ add_strategy_instance(
|
|
|
id=strategy_id,
|
|
|
strategy_type=strategy_type.strip(),
|
|
|
account_id=account_id.strip(),
|
|
|
@@ -227,3 +324,16 @@ def dashboard_strategies_pause(strategy_id: str):
|
|
|
else:
|
|
|
pause_strategy(strategy_id)
|
|
|
return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/strategies/{strategy_id}/config")
|
|
|
+def dashboard_strategies_config(strategy_id: str, config_json: str = Form(...)):
|
|
|
+ config = json.loads(config_json) if config_json.strip() else {}
|
|
|
+ update_strategy_config(strategy_id, config)
|
|
|
+ reconcile_instance(strategy_id)
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/strategies/{strategy_id}/render")
|
|
|
+def dashboard_strategies_render(strategy_id: str):
|
|
|
+ return render_strategy(strategy_id)
|