|
@@ -1,7 +1,15 @@
|
|
|
-from fastapi import APIRouter
|
|
|
|
|
-from fastapi.responses import HTMLResponse
|
|
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+from uuid import uuid4
|
|
|
|
|
+
|
|
|
|
|
+from fastapi import APIRouter, Form
|
|
|
|
|
+from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
|
|
|
|
from .exec_client import list_accounts
|
|
from .exec_client import list_accounts
|
|
|
|
|
+from .strategy_engine import pause_strategy, reconcile_instance, resume_strategy, get_running_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
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
|
|
|
|
@@ -9,8 +17,22 @@ router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
@router.get("/", response_class=HTMLResponse)
|
|
|
def dashboard_home():
|
|
def dashboard_home():
|
|
|
accounts = list_accounts()
|
|
accounts = list_accounts()
|
|
|
|
|
+ strategies = list_strategy_instances()
|
|
|
|
|
+ available_modules = list_available_strategy_modules()
|
|
|
|
|
+
|
|
|
|
|
+ account_options = "".join(
|
|
|
|
|
+ f'<option value="{a.get("id")}">{a.get("display_name") or a.get("venue_account_ref") or a.get("id")}</option>'
|
|
|
|
|
+ for a in (accounts or [])
|
|
|
|
|
+ if isinstance(a, dict)
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- rows = "".join(
|
|
|
|
|
|
|
+ account_lookup = {
|
|
|
|
|
+ (a.get("id") if isinstance(a, dict) else None): (a.get("display_name") or a.get("venue_account_ref") or a.get("id") or "")
|
|
|
|
|
+ for a in (accounts or [])
|
|
|
|
|
+ if isinstance(a, dict)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ account_rows = "".join(
|
|
|
"""
|
|
"""
|
|
|
<tr>
|
|
<tr>
|
|
|
<td>{display_name}</td>
|
|
<td>{display_name}</td>
|
|
@@ -29,39 +51,179 @@ def dashboard_home():
|
|
|
for a in (accounts or [])
|
|
for a in (accounts or [])
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ strategy_rows = "".join(
|
|
|
|
|
+ """
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td>{indicator}</td>
|
|
|
|
|
+ <td>{name}</td>
|
|
|
|
|
+ <td>{strategy_type}</td>
|
|
|
|
|
+ <td>{account_name}</td>
|
|
|
|
|
+ <td>{mode}</td>
|
|
|
|
|
+ <td>{config}</td>
|
|
|
|
|
+ <td class="actions">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ """.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,
|
|
|
|
|
+ name=s.name or s.id,
|
|
|
|
|
+ strategy_type=s.strategy_type,
|
|
|
|
|
+ account_name=account_lookup.get(s.account_id, s.account_id),
|
|
|
|
|
+ mode=s.mode,
|
|
|
|
|
+ config=s.config,
|
|
|
|
|
+ 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 "",
|
|
|
|
|
+ pause_label=("Resume" if (get_running_strategy(s.id).paused if get_running_strategy(s.id) else False) else "Pause"),
|
|
|
|
|
+ pause_disabled="disabled" if s.mode == "off" else "",
|
|
|
|
|
+ )
|
|
|
|
|
+ for s in strategies
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ module_options = "".join(f'<option value="{m.module_name}">{m.module_name}</option>' for m in available_modules)
|
|
|
|
|
+
|
|
|
return f"""<!doctype html>
|
|
return f"""<!doctype html>
|
|
|
<html>
|
|
<html>
|
|
|
<head>
|
|
<head>
|
|
|
- <meta charset=\"utf-8\" />
|
|
|
|
|
- <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
|
|
|
|
|
|
|
+ <meta charset="utf-8" />
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
<title>Trader MCP Dashboard</title>
|
|
<title>Trader MCP Dashboard</title>
|
|
|
<style>
|
|
<style>
|
|
|
body {{ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 2rem; color: #111827; }}
|
|
body {{ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 2rem; color: #111827; }}
|
|
|
- .card {{ max-width: 980px; padding: 1.25rem; border: 1px solid #e5e7eb; border-radius: 12px; }}
|
|
|
|
|
|
|
+ .card {{ max-width: 1100px; padding: 1.25rem; border: 1px solid #e5e7eb; border-radius: 12px; }}
|
|
|
.muted {{ color: #6b7280; }}
|
|
.muted {{ color: #6b7280; }}
|
|
|
table {{ width: 100%; border-collapse: collapse; margin-top: 14px; }}
|
|
table {{ width: 100%; border-collapse: collapse; margin-top: 14px; }}
|
|
|
th, td {{ border-bottom: 1px solid #e5e7eb; padding: 10px 8px; text-align: left; vertical-align: top; }}
|
|
th, td {{ border-bottom: 1px solid #e5e7eb; padding: 10px 8px; text-align: left; vertical-align: top; }}
|
|
|
th {{ background: #f9fafb; }}
|
|
th {{ background: #f9fafb; }}
|
|
|
.pill {{ display:inline-block; padding:2px 10px; border-radius:999px; background:#f3f4f6; font-size: 0.9em; }}
|
|
.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 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; }}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
- <div class=\"card\">
|
|
|
|
|
|
|
+ <div class="card">
|
|
|
<h1>Trader MCP Dashboard</h1>
|
|
<h1>Trader MCP Dashboard</h1>
|
|
|
- <p class=\"muted\">exec-mcp accounts</p>
|
|
|
|
|
|
|
+ <p class="muted">Strategies and exec-mcp accounts</p>
|
|
|
|
|
|
|
|
- <table>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <th>name</th>
|
|
|
|
|
- <th>venue</th>
|
|
|
|
|
- <th>exchange account ref</th>
|
|
|
|
|
- <th>description</th>
|
|
|
|
|
- <th>enabled</th>
|
|
|
|
|
- </tr>
|
|
|
|
|
- {rows}
|
|
|
|
|
- </table>
|
|
|
|
|
|
|
+ <section>
|
|
|
|
|
+ <h2>Strategies</h2>
|
|
|
|
|
+ <form method="post" action="/dashboard/strategies/add" style="display:grid; gap:10px; max-width: 720px; margin-top: 12px;">
|
|
|
|
|
+ <input name="name" placeholder="strategy name, e.g. My super Grid 0.5" required />
|
|
|
|
|
+ <select name="strategy_type" required>
|
|
|
|
|
+ {module_options}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ <select name="account_id" required>
|
|
|
|
|
+ {account_options}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ <button type="submit">Add strategy</button>
|
|
|
|
|
+ </form>
|
|
|
|
|
|
|
|
- <p class=\"muted\" style=\"margin-top: 12px;\">Source: <span class=\"pill\">exec-mcp.list_accounts</span></p>
|
|
|
|
|
|
|
+ <table>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>state</th>
|
|
|
|
|
+ <th>name</th>
|
|
|
|
|
+ <th>type</th>
|
|
|
|
|
+ <th>account</th>
|
|
|
|
|
+ <th>mode</th>
|
|
|
|
|
+ <th>config</th>
|
|
|
|
|
+ <th>actions</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ {strategy_rows}
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <details>
|
|
|
|
|
+ <summary>Accounts</summary>
|
|
|
|
|
+ <p class="muted">exec-mcp accounts</p>
|
|
|
|
|
+
|
|
|
|
|
+ <table>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>name</th>
|
|
|
|
|
+ <th>venue</th>
|
|
|
|
|
+ <th>exchange account ref</th>
|
|
|
|
|
+ <th>description</th>
|
|
|
|
|
+ <th>enabled</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ {account_rows}
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </details>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="muted" style="margin-top: 12px;">Source: <span class="pill">exec-mcp.list_accounts</span></p>
|
|
|
</div>
|
|
</div>
|
|
|
</body>
|
|
</body>
|
|
|
</html>"""
|
|
</html>"""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/strategies/add")
|
|
|
|
|
+def dashboard_strategies_add(
|
|
|
|
|
+ name: str = Form(...),
|
|
|
|
|
+ strategy_type: str = Form(...),
|
|
|
|
|
+ account_id: str = Form(...),
|
|
|
|
|
+):
|
|
|
|
|
+ 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(
|
|
|
|
|
+ id=strategy_id,
|
|
|
|
|
+ strategy_type=strategy_type.strip(),
|
|
|
|
|
+ account_id=account_id.strip(),
|
|
|
|
|
+ client_id=client_id,
|
|
|
|
|
+ mode="off",
|
|
|
|
|
+ config=default_config,
|
|
|
|
|
+ )
|
|
|
|
|
+ update_strategy_name(strategy_id, name.strip())
|
|
|
|
|
+ reconcile_instance(strategy_id)
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/strategies/{strategy_id}/delete")
|
|
|
|
|
+def dashboard_strategies_delete(strategy_id: str):
|
|
|
|
|
+ delete_strategy_instance(strategy_id)
|
|
|
|
|
+ reconcile_instance(strategy_id)
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/strategies/{strategy_id}/power")
|
|
|
|
|
+def dashboard_strategies_power(strategy_id: str):
|
|
|
|
|
+ from .strategy_store import get_strategy_instance
|
|
|
|
|
+
|
|
|
|
|
+ record = get_strategy_instance(strategy_id)
|
|
|
|
|
+ if record is None:
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+ update_strategy_mode(strategy_id, "observe" if record.mode == "off" else "off")
|
|
|
|
|
+ reconcile_instance(strategy_id)
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/strategies/{strategy_id}/activation")
|
|
|
|
|
+def dashboard_strategies_activation(strategy_id: str):
|
|
|
|
|
+ from .strategy_store import get_strategy_instance
|
|
|
|
|
+
|
|
|
|
|
+ record = get_strategy_instance(strategy_id)
|
|
|
|
|
+ if record is None or record.mode == "off":
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+ update_strategy_mode(strategy_id, "active" if record.mode != "active" else "observe")
|
|
|
|
|
+ reconcile_instance(strategy_id)
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/strategies/{strategy_id}/pause")
|
|
|
|
|
+def dashboard_strategies_pause(strategy_id: str):
|
|
|
|
|
+ runtime = get_running_strategy(strategy_id)
|
|
|
|
|
+ if runtime is None:
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|
|
|
|
|
+ if runtime.paused:
|
|
|
|
|
+ resume_strategy(strategy_id)
|
|
|
|
|
+ else:
|
|
|
|
|
+ pause_strategy(strategy_id)
|
|
|
|
|
+ return RedirectResponse(url="/dashboard", status_code=303)
|