|
@@ -24,7 +24,7 @@ from .narrative_engine import build_narrative
|
|
|
from .replay import build_replay_input
|
|
from .replay import build_replay_input
|
|
|
from .state_engine import synthesize_state
|
|
from .state_engine import synthesize_state
|
|
|
from .store import delete_concern, get_decision_profile, get_state, init_db, list_concerns, list_strategy_assignments, list_strategy_groups, latest_cycle, latest_cycles, latest_decisions, latest_narratives, latest_observations, latest_regime_samples, prune_older_than, recent_regime_samples, recent_states_for_concern, sync_concerns_from_strategies, upsert_concern, upsert_cycle, upsert_decision, upsert_decision_profile, upsert_narrative, upsert_observation, upsert_regime_sample, upsert_state, latest_states, upsert_strategy_assignment, upsert_strategy_group
|
|
from .store import delete_concern, get_decision_profile, get_state, init_db, list_concerns, list_strategy_assignments, list_strategy_groups, latest_cycle, latest_cycles, latest_decisions, latest_narratives, latest_observations, latest_regime_samples, prune_older_than, recent_regime_samples, recent_states_for_concern, sync_concerns_from_strategies, upsert_concern, upsert_cycle, upsert_decision, upsert_decision_profile, upsert_narrative, upsert_observation, upsert_regime_sample, upsert_state, latest_states, upsert_strategy_assignment, upsert_strategy_group
|
|
|
-from .trader_client import apply_control_decision as trader_apply_control_decision, cancel_all_orders as trader_cancel_all_orders, get_strategy as trader_get_strategy, list_strategies
|
|
|
|
|
|
|
+from .trader_client import apply_control_decision as trader_apply_control_decision, cancel_all_orders as trader_cancel_all_orders, control_strategy as trader_control_strategy, get_strategy as trader_get_strategy, list_strategies
|
|
|
|
|
|
|
|
mcp = FastMCP(
|
|
mcp = FastMCP(
|
|
|
"hermes-mcp",
|
|
"hermes-mcp",
|
|
@@ -123,7 +123,7 @@ async def _maybe_dispatch_trader_action(*, cfg: object, decision_id: str, concer
|
|
|
|
|
|
|
|
|
|
|
|
|
@mcp.tool(description="Return Hermes current state, narrative, uncertainty, and a short self-assessment report.")
|
|
@mcp.tool(description="Return Hermes current state, narrative, uncertainty, and a short self-assessment report.")
|
|
|
-def report() -> dict:
|
|
|
|
|
|
|
+async def report() -> dict:
|
|
|
state = get_state()
|
|
state = get_state()
|
|
|
cfg = load_config()
|
|
cfg = load_config()
|
|
|
concerns = list_concerns()
|
|
concerns = list_concerns()
|
|
@@ -132,10 +132,16 @@ def report() -> dict:
|
|
|
groups_by_concern.setdefault(str(group.get("concern_id") or ""), []).append(group)
|
|
groups_by_concern.setdefault(str(group.get("concern_id") or ""), []).append(group)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- accounts_by_id, markets_by_symbol, total_values = anyio.run(_load_exec_enrichment, cfg.exec_url, cfg.crypto_url, concerns)
|
|
|
|
|
|
|
+ accounts_by_id, markets_by_symbol, total_values = await _load_exec_enrichment(cfg.exec_url, cfg.crypto_url, concerns)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
accounts_by_id, markets_by_symbol, total_values = {}, {}, {}
|
|
accounts_by_id, markets_by_symbol, total_values = {}, {}, {}
|
|
|
|
|
+ try:
|
|
|
|
|
+ strategy_inventory = await list_strategies(cfg.trader_url)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ strategy_inventory = []
|
|
|
|
|
+ strategies_by_id = {str(s.get("id") or "").strip(): s for s in strategy_inventory if str(s.get("id") or "").strip()}
|
|
|
|
|
|
|
|
|
|
+ latest_decision_by_concern = {str(d.get("concern_id") or ""): d for d in latest_decisions(200)}
|
|
|
concern_summaries = []
|
|
concern_summaries = []
|
|
|
for concern in concerns:
|
|
for concern in concerns:
|
|
|
concern_id = str(concern.get("id") or "")
|
|
concern_id = str(concern.get("id") or "")
|
|
@@ -146,37 +152,63 @@ def report() -> dict:
|
|
|
groups = groups_by_concern.get(concern_id, [])
|
|
groups = groups_by_concern.get(concern_id, [])
|
|
|
active_playbook = next((g for g in groups if str(g.get("status") or "").lower() == "active"), None)
|
|
active_playbook = next((g for g in groups if str(g.get("status") or "").lower() == "active"), None)
|
|
|
assignments = list_strategy_assignments(strategy_group_id=str(active_playbook.get("id") or "")) if active_playbook else []
|
|
assignments = list_strategy_assignments(strategy_group_id=str(active_playbook.get("id") or "")) if active_playbook else []
|
|
|
|
|
+ latest_decision = latest_decision_by_concern.get(concern_id, {})
|
|
|
|
|
+ active_strategy_id = str(latest_decision.get("target_strategy") or "").strip()
|
|
|
|
|
+ active_strategy = next((a for a in assignments if str(a.get("strategy_id") or "").strip() == active_strategy_id), None)
|
|
|
|
|
+ if not active_strategy and assignments:
|
|
|
|
|
+ active_strategy = next((a for a in assignments if str(a.get("role") or "").strip().lower() in {"primary", "active", "buy", "sell", "trend_buy", "trend_sell"}), assignments[0])
|
|
|
|
|
+ active_strategy_id = str(active_strategy.get("strategy_id") or "").strip()
|
|
|
|
|
+ active_strategy_meta = strategies_by_id.get(active_strategy_id, {}) if active_strategy_id else {}
|
|
|
concern_summaries.append({
|
|
concern_summaries.append({
|
|
|
"concern_id": concern_id,
|
|
"concern_id": concern_id,
|
|
|
"account_id": account_id or None,
|
|
"account_id": account_id or None,
|
|
|
"account": account_info.get("display_name") or account_id or None,
|
|
"account": account_info.get("display_name") or account_id or None,
|
|
|
"market_symbol": str(concern.get("market_symbol") or "") or None,
|
|
"market_symbol": str(concern.get("market_symbol") or "") or None,
|
|
|
"market": market_info.get("name") or str(concern.get("market_symbol") or "") or None,
|
|
"market": market_info.get("name") or str(concern.get("market_symbol") or "") or None,
|
|
|
|
|
+ "label": f"{account_info.get('display_name') or account_id or 'account'} · {market_info.get('name') or str(concern.get('market_symbol') or '') or 'market'}",
|
|
|
"status": str(concern.get("status") or "active"),
|
|
"status": str(concern.get("status") or "active"),
|
|
|
"active_playbook": {
|
|
"active_playbook": {
|
|
|
"id": str(active_playbook.get("id") or "") or None,
|
|
"id": str(active_playbook.get("id") or "") or None,
|
|
|
"name": str(active_playbook.get("name") or "") or None,
|
|
"name": str(active_playbook.get("name") or "") or None,
|
|
|
"family": str(active_playbook.get("strategy_family") or "") or None,
|
|
"family": str(active_playbook.get("strategy_family") or "") or None,
|
|
|
} if active_playbook else None,
|
|
} if active_playbook else None,
|
|
|
- "active_strategies": [
|
|
|
|
|
|
|
+ "active_strategy": {
|
|
|
|
|
+ "strategy_id": active_strategy_id or None,
|
|
|
|
|
+ "label": _strategy_display_label(active_strategy_meta) if active_strategy_meta else active_strategy_id or None,
|
|
|
|
|
+ "role": str(active_strategy.get("role") or "active") if active_strategy else None,
|
|
|
|
|
+ "strategy_type": str(active_strategy.get("strategy_type") or "") if active_strategy else None,
|
|
|
|
|
+ } if active_strategy_id else None,
|
|
|
|
|
+ "assigned_strategies": [
|
|
|
{
|
|
{
|
|
|
"strategy_id": str(a.get("strategy_id") or "") or None,
|
|
"strategy_id": str(a.get("strategy_id") or "") or None,
|
|
|
|
|
+ "label": _strategy_display_label(strategies_by_id.get(str(a.get("strategy_id") or "").strip(), {})) if str(a.get("strategy_id") or "").strip() in strategies_by_id else str(a.get("strategy_id") or "") or None,
|
|
|
"role": str(a.get("role") or "member") or "member",
|
|
"role": str(a.get("role") or "member") or "member",
|
|
|
|
|
+ "status": str(a.get("status") or "active") or "active",
|
|
|
"strategy_type": str(a.get("strategy_type") or "") or None,
|
|
"strategy_type": str(a.get("strategy_type") or "") or None,
|
|
|
}
|
|
}
|
|
|
for a in assignments
|
|
for a in assignments
|
|
|
],
|
|
],
|
|
|
- "balances": _compact_balances(account_info.get("balances") or account_info.get("balance") or account_info.get("wallets") or []),
|
|
|
|
|
|
|
+ "balances": _structured_balances(account_info.get("balances") or account_info.get("balance") or account_info.get("wallets") or []),
|
|
|
"total_value_usd": total_values.get(account_id) if total_values.get(account_id) is not None else account_info.get("total_value_usd"),
|
|
"total_value_usd": total_values.get(account_id) if total_values.get(account_id) is not None else account_info.get("total_value_usd"),
|
|
|
})
|
|
})
|
|
|
- return {
|
|
|
|
|
- "status": state.get("status", "stub"),
|
|
|
|
|
- "thinking": state.get("thinking", "Hermes scaffold is ready."),
|
|
|
|
|
|
|
+ status = str(state.get("status") or "").strip().lower()
|
|
|
|
|
+ if status in {"", "stub", "scaffold"}:
|
|
|
|
|
+ status = "ok"
|
|
|
|
|
+ thinking = str(state.get("thinking") or "").strip()
|
|
|
|
|
+ thinking_l = thinking.lower()
|
|
|
|
|
+ if not thinking or "scaffold" in thinking_l or "waiting for integrations" in thinking_l:
|
|
|
|
|
+ thinking = "Hermes report is ready."
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "status": status,
|
|
|
|
|
+ "thinking": thinking,
|
|
|
"confidence": state.get("confidence", 0.0),
|
|
"confidence": state.get("confidence", 0.0),
|
|
|
- "uncertainty": state.get("uncertainty", ["no live adapters wired yet"]),
|
|
|
|
|
"layers": state.get("layers", []),
|
|
"layers": state.get("layers", []),
|
|
|
"concerns": concern_summaries,
|
|
"concerns": concern_summaries,
|
|
|
}
|
|
}
|
|
|
|
|
+ uncertainty = state.get("uncertainty")
|
|
|
|
|
+ if isinstance(uncertainty, list) and uncertainty:
|
|
|
|
|
+ payload["uncertainty"] = uncertainty
|
|
|
|
|
+ return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
@asynccontextmanager
|
|
@@ -556,6 +588,30 @@ def _compact_balances(payload: object) -> str:
|
|
|
return " | ".join(parts[:5]) or "-"
|
|
return " | ".join(parts[:5]) or "-"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _structured_balances(payload: object) -> list[dict[str, Any]]:
|
|
|
|
|
+ if not isinstance(payload, list):
|
|
|
|
|
+ return []
|
|
|
|
|
+ balances: list[dict[str, Any]] = []
|
|
|
|
|
+ for item in payload:
|
|
|
|
|
+ if not isinstance(item, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ asset = str(item.get("asset_code") or item.get("asset") or "").upper().strip()
|
|
|
|
|
+ if not asset:
|
|
|
|
|
+ continue
|
|
|
|
|
+ entry: dict[str, Any] = {"asset": asset}
|
|
|
|
|
+ for key in ("total", "available", "value_usd"):
|
|
|
|
|
+ value = item.get(key)
|
|
|
|
|
+ if isinstance(value, (int, float)):
|
|
|
|
|
+ entry[key] = float(value)
|
|
|
|
|
+ else:
|
|
|
|
|
+ try:
|
|
|
|
|
+ entry[key] = float(value)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ entry[key] = None
|
|
|
|
|
+ balances.append(entry)
|
|
|
|
|
+ return balances
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _resolve_regime_symbol(concern: dict) -> str | None:
|
|
def _resolve_regime_symbol(concern: dict) -> str | None:
|
|
|
base = str(concern.get("base_currency") or "").strip().upper()
|
|
base = str(concern.get("base_currency") or "").strip().upper()
|
|
|
if base:
|
|
if base:
|
|
@@ -691,6 +747,7 @@ def dashboard_data() -> JSONResponse:
|
|
|
for strategy in strategy_inventory
|
|
for strategy in strategy_inventory
|
|
|
if str(strategy.get("account_id") or "").strip() and str(strategy.get("market_symbol") or "").strip()
|
|
if str(strategy.get("account_id") or "").strip() and str(strategy.get("market_symbol") or "").strip()
|
|
|
}
|
|
}
|
|
|
|
|
+ strategies_by_id = {str(strategy.get("id") or "").strip(): strategy for strategy in strategy_inventory if str(strategy.get("id") or "").strip()}
|
|
|
enriched = []
|
|
enriched = []
|
|
|
concern_lookup: dict[str, dict] = {}
|
|
concern_lookup: dict[str, dict] = {}
|
|
|
for concern in concerns:
|
|
for concern in concerns:
|
|
@@ -725,6 +782,13 @@ def dashboard_data() -> JSONResponse:
|
|
|
"market_display": concern_meta.get("market_display"),
|
|
"market_display": concern_meta.get("market_display"),
|
|
|
"market_symbol": concern_meta.get("market_symbol"),
|
|
"market_symbol": concern_meta.get("market_symbol"),
|
|
|
}})
|
|
}})
|
|
|
|
|
+ def _decorate_decision(row: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
+ target_strategy_id = str(row.get("target_strategy") or "").strip()
|
|
|
|
|
+ strategy = strategies_by_id.get(target_strategy_id, {})
|
|
|
|
|
+ return {
|
|
|
|
|
+ **row,
|
|
|
|
|
+ "target_strategy_label": _strategy_display_label(strategy) if strategy else target_strategy_id or None,
|
|
|
|
|
+ }
|
|
|
return JSONResponse({
|
|
return JSONResponse({
|
|
|
"latest_cycle": latest_cycle(),
|
|
"latest_cycle": latest_cycle(),
|
|
|
"cycles": latest_cycles(10),
|
|
"cycles": latest_cycles(10),
|
|
@@ -735,8 +799,8 @@ def dashboard_data() -> JSONResponse:
|
|
|
"state_samples": latest_states(20),
|
|
"state_samples": latest_states(20),
|
|
|
"state_history": latest_states(100),
|
|
"state_history": latest_states(100),
|
|
|
"narrative_samples": latest_narratives(20),
|
|
"narrative_samples": latest_narratives(20),
|
|
|
- "decision_samples": latest_decisions(20),
|
|
|
|
|
- "decision_history": latest_decisions(100),
|
|
|
|
|
|
|
+ "decision_samples": [_decorate_decision(d) for d in latest_decisions(20)],
|
|
|
|
|
+ "decision_history": [_decorate_decision(d) for d in latest_decisions(100)],
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1258,6 +1322,7 @@ async def dashboard_playbook_assignment_upsert(playbook_id: str, request: Reques
|
|
|
|
|
|
|
|
@app.post("/dashboard/concerns/{concern_id}/status")
|
|
@app.post("/dashboard/concerns/{concern_id}/status")
|
|
|
async def dashboard_set_concern_status(concern_id: str, request: Request) -> JSONResponse:
|
|
async def dashboard_set_concern_status(concern_id: str, request: Request) -> JSONResponse:
|
|
|
|
|
+ cfg = load_config()
|
|
|
concern_id = str(concern_id or "").strip()
|
|
concern_id = str(concern_id or "").strip()
|
|
|
concern = next((c for c in list_concerns() if str(c.get("id") or "") == concern_id), None)
|
|
concern = next((c for c in list_concerns() if str(c.get("id") or "") == concern_id), None)
|
|
|
if not concern:
|
|
if not concern:
|
|
@@ -1269,11 +1334,28 @@ async def dashboard_set_concern_status(concern_id: str, request: Request) -> JSO
|
|
|
return JSONResponse({"ok": False, "error": "status must be active or inactive"}, status_code=400)
|
|
return JSONResponse({"ok": False, "error": "status must be active or inactive"}, status_code=400)
|
|
|
|
|
|
|
|
account_id = str(concern.get("account_id") or "").strip()
|
|
account_id = str(concern.get("account_id") or "").strip()
|
|
|
|
|
+ market_symbol = str(concern.get("market_symbol") or "").strip().lower()
|
|
|
if status == "inactive" and account_id:
|
|
if status == "inactive" and account_id:
|
|
|
try:
|
|
try:
|
|
|
- await trader_cancel_all_orders(cfg.trader_url if (cfg := load_config()) else "", account_id)
|
|
|
|
|
|
|
+ await trader_cancel_all_orders(cfg.trader_url, account_id)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
pass
|
|
pass
|
|
|
|
|
+ try:
|
|
|
|
|
+ strategies = [
|
|
|
|
|
+ s for s in await list_strategies(cfg.trader_url)
|
|
|
|
|
+ if str(s.get("account_id") or "").strip() == account_id
|
|
|
|
|
+ and str(s.get("market_symbol") or "").strip().lower() == market_symbol
|
|
|
|
|
+ ]
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ strategies = []
|
|
|
|
|
+ for strategy in strategies:
|
|
|
|
|
+ instance_id = str(strategy.get("id") or "").strip()
|
|
|
|
|
+ if not instance_id:
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ await trader_control_strategy(cfg.trader_url, instance_id, "stop")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
|
|
|
upsert_concern(
|
|
upsert_concern(
|
|
|
id=str(concern.get("id") or ""),
|
|
id=str(concern.get("id") or ""),
|