|
@@ -6,10 +6,11 @@ from uuid import uuid4
|
|
|
from fastapi import APIRouter, Form
|
|
from fastapi import APIRouter, Form
|
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
|
|
|
|
-from .exec_client import list_accounts
|
|
|
|
|
|
|
+from .exec_client import list_accounts, list_markets
|
|
|
from .strategy_engine import get_running_strategy, pause_strategy, reconcile_instance, render_strategy, resume_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_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_config, 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
|
|
|
|
|
+from .crypto_client import call_crypto_tool
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
|
|
|
|
@@ -19,6 +20,33 @@ def dashboard_home():
|
|
|
accounts = list_accounts()
|
|
accounts = list_accounts()
|
|
|
strategies = list_strategy_instances()
|
|
strategies = list_strategy_instances()
|
|
|
available_modules = list_available_strategy_modules()
|
|
available_modules = list_available_strategy_modules()
|
|
|
|
|
+ markets = list_markets()
|
|
|
|
|
+
|
|
|
|
|
+ # human label: "<name> (<description capped>)" when description exists
|
|
|
|
|
+ def market_label(m: dict) -> str:
|
|
|
|
|
+ name = m.get("name") or m.get("market_symbol") or ""
|
|
|
|
|
+ desc = m.get("description") or ""
|
|
|
|
|
+ desc = str(desc)
|
|
|
|
|
+ if desc:
|
|
|
|
|
+ desc = desc[:32]
|
|
|
|
|
+ return f"{name} ({desc})"
|
|
|
|
|
+ return str(name)
|
|
|
|
|
+
|
|
|
|
|
+ market_by_symbol = {
|
|
|
|
|
+ (m.get("market_symbol") or ""): m
|
|
|
|
|
+ for m in (markets or [])
|
|
|
|
|
+ if isinstance(m, dict) and m.get("market_symbol")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ market_options = "".join(
|
|
|
|
|
+ f'<option value="{m.get("market_symbol")}">{market_label(m)}</option>'
|
|
|
|
|
+ for m in (markets or [])
|
|
|
|
|
+ if isinstance(m, dict) and m.get("market_symbol")
|
|
|
|
|
+ )
|
|
|
|
|
+ if market_options:
|
|
|
|
|
+ market_options = '<option value="" disabled selected>Select market</option>' + market_options
|
|
|
|
|
+ else:
|
|
|
|
|
+ market_options = '<option value="xrpusd" selected>xrpusd (default)</option>'
|
|
|
|
|
|
|
|
account_options = "".join(
|
|
account_options = "".join(
|
|
|
f'<option value="{a.get("id")}">{a.get("display_name") or a.get("venue_account_ref") or a.get("id")}</option>'
|
|
f'<option value="{a.get("id")}">{a.get("display_name") or a.get("venue_account_ref") or a.get("id")}</option>'
|
|
@@ -32,21 +60,19 @@ def dashboard_home():
|
|
|
if isinstance(a, dict)
|
|
if isinstance(a, dict)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # Initial snapshot (balances are refreshed client-side via an endpoint).
|
|
|
account_rows = "".join(
|
|
account_rows = "".join(
|
|
|
"""
|
|
"""
|
|
|
- <tr>
|
|
|
|
|
|
|
+ <tr data-account-id="{id}">
|
|
|
<td>{display_name}</td>
|
|
<td>{display_name}</td>
|
|
|
- <td>{venue}</td>
|
|
|
|
|
- <td>{venue_account_ref}</td>
|
|
|
|
|
- <td>{description}</td>
|
|
|
|
|
- <td>{enabled}</td>
|
|
|
|
|
|
|
+ <td class="balances">(loading)</td>
|
|
|
|
|
+ <td class="total-usd">(loading)</td>
|
|
|
</tr>
|
|
</tr>
|
|
|
""".strip().format(
|
|
""".strip().format(
|
|
|
- display_name=(a.get("display_name") or "") if isinstance(a, dict) else "",
|
|
|
|
|
- venue=(a.get("venue") or "") if isinstance(a, dict) else "",
|
|
|
|
|
- venue_account_ref=(a.get("venue_account_ref") or "") if isinstance(a, dict) else "",
|
|
|
|
|
- description=(a.get("description") or "") if isinstance(a, dict) else "",
|
|
|
|
|
- enabled=("yes" if (a.get("enabled") if isinstance(a, dict) else False) else "no"),
|
|
|
|
|
|
|
+ id=(a.get("id") or "") if isinstance(a, dict) else "",
|
|
|
|
|
+ display_name=(a.get("display_name") or a.get("venue") or a.get("id") or "")
|
|
|
|
|
+ if isinstance(a, dict)
|
|
|
|
|
+ else "",
|
|
|
)
|
|
)
|
|
|
for a in (accounts or [])
|
|
for a in (accounts or [])
|
|
|
)
|
|
)
|
|
@@ -58,8 +84,7 @@ def dashboard_home():
|
|
|
<td>{name}</td>
|
|
<td>{name}</td>
|
|
|
<td>{strategy_type}</td>
|
|
<td>{strategy_type}</td>
|
|
|
<td>{account_name}</td>
|
|
<td>{account_name}</td>
|
|
|
- <td>{mode}</td>
|
|
|
|
|
- <td>{config}</td>
|
|
|
|
|
|
|
+ <td>{market_label}</td>
|
|
|
<td class="actions">
|
|
<td class="actions">
|
|
|
<button type="button" class="ghost" onclick="toggleDetails('{id}')">Details</button>
|
|
<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}/power"><button type="submit" class="ghost">{power_label}</button></form>
|
|
@@ -69,7 +94,7 @@ def dashboard_home():
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
|
<tr id="details-{id}" class="detail-row" style="display:none;">
|
|
<tr id="details-{id}" class="detail-row" style="display:none;">
|
|
|
- <td colspan="7">
|
|
|
|
|
|
|
+ <td colspan="5">
|
|
|
<div style="margin-top:10px; display:grid; gap:12px;">
|
|
<div style="margin-top:10px; display:grid; gap:12px;">
|
|
|
<div>
|
|
<div>
|
|
|
<strong>Render</strong>
|
|
<strong>Render</strong>
|
|
@@ -97,11 +122,18 @@ def dashboard_home():
|
|
|
activation_disabled="disabled" if s.mode == "off" or (get_running_strategy(s.id).paused if get_running_strategy(s.id) else False) else "",
|
|
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_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 "",
|
|
pause_disabled="disabled" if s.mode == "off" else "",
|
|
|
|
|
+ market_label=market_label(
|
|
|
|
|
+ market_by_symbol.get(s.market_symbol or "")
|
|
|
|
|
+ or {"name": s.market_symbol or "", "description": ""}
|
|
|
|
|
+ ),
|
|
|
)
|
|
)
|
|
|
for s in strategies
|
|
for s in strategies
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
module_options = "".join(f'<option value="{m.module_name}">{m.module_name}</option>' for m in available_modules)
|
|
module_options = "".join(f'<option value="{m.module_name}">{m.module_name}</option>' for m in available_modules)
|
|
|
|
|
+ module_options = '<option value="" disabled selected>Select strategy blueprint</option>' + (module_options or "")
|
|
|
|
|
+
|
|
|
|
|
+ account_options = '<option value="" disabled selected>Select account</option>' + (account_options or "")
|
|
|
|
|
|
|
|
return f"""<!doctype html>
|
|
return f"""<!doctype html>
|
|
|
<html>
|
|
<html>
|
|
@@ -131,8 +163,8 @@ def dashboard_home():
|
|
|
<h1>Trader MCP Dashboard</h1>
|
|
<h1>Trader MCP Dashboard</h1>
|
|
|
<p class="muted">Strategies and exec-mcp accounts</p>
|
|
<p class="muted">Strategies and exec-mcp accounts</p>
|
|
|
|
|
|
|
|
- <section>
|
|
|
|
|
- <h2>Strategies</h2>
|
|
|
|
|
|
|
+ <details id="strategy-form-section">
|
|
|
|
|
+ <summary>Add strategy</summary>
|
|
|
<form method="post" action="/dashboard/strategies/add" style="display:grid; gap:10px; max-width: 720px; margin-top: 12px;">
|
|
<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 />
|
|
<input name="name" placeholder="strategy name, e.g. My super Grid 0.5" required />
|
|
|
<select name="strategy_type" required>
|
|
<select name="strategy_type" required>
|
|
@@ -141,17 +173,22 @@ def dashboard_home():
|
|
|
<select name="account_id" required>
|
|
<select name="account_id" required>
|
|
|
{account_options}
|
|
{account_options}
|
|
|
</select>
|
|
</select>
|
|
|
|
|
+ <select name="market_symbol" required>
|
|
|
|
|
+ {market_options}
|
|
|
|
|
+ </select>
|
|
|
<button type="submit">Add strategy</button>
|
|
<button type="submit">Add strategy</button>
|
|
|
</form>
|
|
</form>
|
|
|
|
|
+ </details>
|
|
|
|
|
|
|
|
|
|
+ <section>
|
|
|
|
|
+ <h2>Strategies</h2>
|
|
|
<table>
|
|
<table>
|
|
|
<tr>
|
|
<tr>
|
|
|
<th>state</th>
|
|
<th>state</th>
|
|
|
<th>name</th>
|
|
<th>name</th>
|
|
|
<th>type</th>
|
|
<th>type</th>
|
|
|
<th>account</th>
|
|
<th>account</th>
|
|
|
- <th>mode</th>
|
|
|
|
|
- <th>config</th>
|
|
|
|
|
|
|
+ <th>market</th>
|
|
|
<th>actions</th>
|
|
<th>actions</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
{strategy_rows}
|
|
{strategy_rows}
|
|
@@ -164,11 +201,9 @@ def dashboard_home():
|
|
|
|
|
|
|
|
<table>
|
|
<table>
|
|
|
<tr>
|
|
<tr>
|
|
|
- <th>name</th>
|
|
|
|
|
- <th>venue</th>
|
|
|
|
|
- <th>exchange account ref</th>
|
|
|
|
|
- <th>description</th>
|
|
|
|
|
- <th>enabled</th>
|
|
|
|
|
|
|
+ <th>account</th>
|
|
|
|
|
+ <th>balances</th>
|
|
|
|
|
+ <th>total value (USD)</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
{account_rows}
|
|
{account_rows}
|
|
|
</table>
|
|
</table>
|
|
@@ -244,6 +279,36 @@ def dashboard_home():
|
|
|
const open = localStorage.getItem('trader-mcp:accounts-section') === 'open';
|
|
const open = localStorage.getItem('trader-mcp:accounts-section') === 'open';
|
|
|
accounts.open = open;
|
|
accounts.open = open;
|
|
|
}}
|
|
}}
|
|
|
|
|
+
|
|
|
|
|
+ const strategyForm = document.getElementById('strategy-form-section');
|
|
|
|
|
+ if (strategyForm) {{
|
|
|
|
|
+ const open = localStorage.getItem('trader-mcp:strategy-form-section') === 'open';
|
|
|
|
|
+ strategyForm.open = open;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ async function refreshAccounts() {{
|
|
|
|
|
+ try {{
|
|
|
|
|
+ const res = await fetch('/dashboard/accounts/overview');
|
|
|
|
|
+ const payload = await res.json();
|
|
|
|
|
+ const byId = payload && payload.by_account_id ? payload.by_account_id : {{}};
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('tr[data-account-id]').forEach((row) => {{
|
|
|
|
|
+ const accId = row.dataset.accountId;
|
|
|
|
|
+ const info = byId[accId];
|
|
|
|
|
+ const balancesCell = row.querySelector('td.balances');
|
|
|
|
|
+ const totalCell = row.querySelector('td.total-usd');
|
|
|
|
|
+ if (!info) {{
|
|
|
|
|
+ if (balancesCell) balancesCell.textContent = '';
|
|
|
|
|
+ if (totalCell) totalCell.textContent = '';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+ if (balancesCell) balancesCell.textContent = info.balances_text || '';
|
|
|
|
|
+ if (totalCell) totalCell.textContent = info.total_value_usd_formatted || '';
|
|
|
|
|
+ }});
|
|
|
|
|
+ }} catch (err) {{
|
|
|
|
|
+ // best-effort: don’t break the whole dashboard
|
|
|
|
|
+ }}
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
const accountsSection = document.getElementById('accounts-section');
|
|
const accountsSection = document.getElementById('accounts-section');
|
|
@@ -253,9 +318,17 @@ def dashboard_home():
|
|
|
}});
|
|
}});
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
|
|
+ const strategyFormSection = document.getElementById('strategy-form-section');
|
|
|
|
|
+ if (strategyFormSection) {{
|
|
|
|
|
+ strategyFormSection.addEventListener('toggle', () => {{
|
|
|
|
|
+ localStorage.setItem('trader-mcp:strategy-form-section', strategyFormSection.open ? 'open' : 'closed');
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
restoreDetails();
|
|
restoreDetails();
|
|
|
refreshAll();
|
|
refreshAll();
|
|
|
- setInterval(refreshAll, 6000);
|
|
|
|
|
|
|
+ refreshAccounts();
|
|
|
|
|
+ setInterval(() => {{ refreshAll(); refreshAccounts(); }}, 6000);
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
</html>"""
|
|
</html>"""
|
|
@@ -266,16 +339,32 @@ def dashboard_strategies_add(
|
|
|
name: str = Form(...),
|
|
name: str = Form(...),
|
|
|
strategy_type: str = Form(...),
|
|
strategy_type: str = Form(...),
|
|
|
account_id: str = Form(...),
|
|
account_id: str = Form(...),
|
|
|
|
|
+ market_symbol: str = Form(...),
|
|
|
):
|
|
):
|
|
|
strategy_id = str(uuid4())
|
|
strategy_id = str(uuid4())
|
|
|
default_config = get_strategy_default_config(strategy_type.strip())
|
|
default_config = get_strategy_default_config(strategy_type.strip())
|
|
|
client_id = synthesize_client_id(strategy_type.strip(), strategy_id, name.strip())
|
|
client_id = synthesize_client_id(strategy_type.strip(), strategy_id, name.strip())
|
|
|
|
|
+
|
|
|
|
|
+ markets = list_markets()
|
|
|
|
|
+ chosen = next((m for m in markets or [] if isinstance(m, dict) and m.get("market_symbol") == market_symbol), None)
|
|
|
|
|
+ base_currency = chosen.get("base_currency") if isinstance(chosen, dict) else None
|
|
|
|
|
+ counter_currency = chosen.get("counter_currency") if isinstance(chosen, dict) else None
|
|
|
|
|
+ # fallback backfill
|
|
|
|
|
+ if not base_currency:
|
|
|
|
|
+ base_currency = "XRP"
|
|
|
|
|
+ if not counter_currency:
|
|
|
|
|
+ counter_currency = "USD"
|
|
|
|
|
+ market_symbol = market_symbol.strip() if market_symbol else "xrpusd"
|
|
|
|
|
+
|
|
|
add_strategy_instance(
|
|
add_strategy_instance(
|
|
|
id=strategy_id,
|
|
id=strategy_id,
|
|
|
strategy_type=strategy_type.strip(),
|
|
strategy_type=strategy_type.strip(),
|
|
|
account_id=account_id.strip(),
|
|
account_id=account_id.strip(),
|
|
|
client_id=client_id,
|
|
client_id=client_id,
|
|
|
mode="off",
|
|
mode="off",
|
|
|
|
|
+ market_symbol=market_symbol,
|
|
|
|
|
+ base_currency=base_currency,
|
|
|
|
|
+ counter_currency=counter_currency,
|
|
|
config=default_config,
|
|
config=default_config,
|
|
|
)
|
|
)
|
|
|
update_strategy_name(strategy_id, name.strip())
|
|
update_strategy_name(strategy_id, name.strip())
|
|
@@ -337,3 +426,93 @@ def dashboard_strategies_config(strategy_id: str, config_json: str = Form(...)):
|
|
|
@router.get("/strategies/{strategy_id}/render")
|
|
@router.get("/strategies/{strategy_id}/render")
|
|
|
def dashboard_strategies_render(strategy_id: str):
|
|
def dashboard_strategies_render(strategy_id: str):
|
|
|
return render_strategy(strategy_id)
|
|
return render_strategy(strategy_id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.get("/accounts/overview")
|
|
|
|
|
+def dashboard_accounts_overview():
|
|
|
|
|
+ accounts = list_accounts()
|
|
|
|
|
+ by_account_id: dict[str, dict[str, str | float]] = {}
|
|
|
|
|
+
|
|
|
|
|
+ for a in accounts or []:
|
|
|
|
|
+ if not isinstance(a, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ account_id = a.get("id")
|
|
|
|
|
+ if not account_id:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ info = None
|
|
|
|
|
+ try:
|
|
|
|
|
+ # exec-mcp should return: balances[{asset_code, available/total, value_usd, ...}], total_value_usd.
|
|
|
|
|
+ info = _safe_get_account_info(account_id)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ info = None
|
|
|
|
|
+
|
|
|
|
|
+ if not info or not isinstance(info, dict):
|
|
|
|
|
+ by_account_id[str(account_id)] = {
|
|
|
|
|
+ "balances_text": "",
|
|
|
|
|
+ "total_value_usd_formatted": "",
|
|
|
|
|
+ }
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ balances = info.get("balances")
|
|
|
|
|
+ balances_parts: list[str] = []
|
|
|
|
|
+ if isinstance(balances, list):
|
|
|
|
|
+ for b in balances:
|
|
|
|
|
+ if not isinstance(b, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ asset = b.get("asset_code")
|
|
|
|
|
+ # use available if present, else total.
|
|
|
|
|
+ qty = b.get("available", b.get("total"))
|
|
|
|
|
+ try:
|
|
|
|
|
+ qty_str = f"{float(qty):.8g}" if qty is not None else ""
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ qty_str = str(qty) if qty is not None else ""
|
|
|
|
|
+ value_usd = b.get("value_usd")
|
|
|
|
|
+ if asset and qty_str != "":
|
|
|
|
|
+ balances_parts.append(f"{asset} {qty_str}")
|
|
|
|
|
+
|
|
|
|
|
+ total_value_usd = 0.0
|
|
|
|
|
+ if isinstance(balances, list):
|
|
|
|
|
+ for b in balances:
|
|
|
|
|
+ if not isinstance(b, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ asset = str(b.get("asset_code") or "").upper()
|
|
|
|
|
+ if not asset:
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ qty = float(b.get("total") if b.get("total") is not None else b.get("available") or 0)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ qty = 0.0
|
|
|
|
|
+
|
|
|
|
|
+ if asset in {"EUR", "USD"}:
|
|
|
|
|
+ try:
|
|
|
|
|
+ total_value_usd += float(b.get("value_usd") or 0)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ price_payload = call_crypto_tool("get_price", {"symbol": asset})
|
|
|
|
|
+ price = float(price_payload.get("price")) if isinstance(price_payload, dict) and price_payload.get("price") is not None else 0.0
|
|
|
|
|
+ total_value_usd += qty * price
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ try:
|
|
|
|
|
+ total_value_usd += float(b.get("value_usd") or 0)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ total_value_usd_formatted = f"{total_value_usd:.2f} $" if total_value_usd else ""
|
|
|
|
|
+
|
|
|
|
|
+ by_account_id[str(account_id)] = {
|
|
|
|
|
+ "balances_text": ", ".join(balances_parts),
|
|
|
|
|
+ "total_value_usd_formatted": total_value_usd_formatted,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {"by_account_id": by_account_id}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _safe_get_account_info(account_id: str):
|
|
|
|
|
+ # isolated helper so we can keep the loop above readable
|
|
|
|
|
+ from .exec_client import get_account_info
|
|
|
|
|
+
|
|
|
|
|
+ return get_account_info(account_id)
|