Spaces:
Paused
Paused
Your Name commited on
Commit ·
24d8dad
1
Parent(s): 3ff79ee
Complete Futures luxury dashboard data integration
Browse files
hermes_overlay/tools/futures_dashboard_api.py
CHANGED
|
@@ -41,6 +41,7 @@ from trading.futures_execution import get_futures_positions as _get_futures_posi
|
|
| 41 |
from trading.futures_execution import get_futures_account as _get_futures_account
|
| 42 |
from trading.futures_execution import get_trading_mode as _get_trading_mode
|
| 43 |
from trading.trade_cycle import run_futures_cycle as _run_futures_cycle
|
|
|
|
| 44 |
from trading.symbols import normalize_symbol
|
| 45 |
|
| 46 |
router = APIRouter()
|
|
@@ -52,7 +53,6 @@ _SYMBOL_CACHE_PATH = Path(os.environ.get("HERMES_SYMBOL_CACHE_PATH", "/opt/data/
|
|
| 52 |
_TEMPLATE_PATH = Path(__file__).with_name("templates") / "hermes_futures_desk_luxury.html"
|
| 53 |
# UI discovery only. These are deliberately unverified; they never authorize execution.
|
| 54 |
_EMERGENCY_BASES = tuple("""BTC ETH USDT BNB SOL XRP USDC ADA AVAX DOGE DOT TRX LINK MATIC LTC BCH UNI ATOM ETC XLM NEAR FIL APT ARB OP ICP SUI INJ MKR AAVE ALGO VET QNT EGLD FTM THETA XTZ EOS FLOW GRT SAND MANA AXS CHZ SNX CRV LDO RUNE KAS PEPE WIF BONK FLOKI SHIB HBAR IMX SEI TIA JUP JTO PYTH WLD FET RNDR TAO KAVA COMP DAI GALA ENJ BAT ZEC DASH XMR NEO IOTA KSM WAVES ZIL ONE CELO ROSE MINA LUNC LUNA GMT GST APE YGG ILV MAGIC DYDX GMX PERP ENS BLUR LPT ARKM STRK DYM MANTA ALT ZK JASMY SFP CAKE BSV BS BCH SV CFX AGIX OCEAN OGN IOST IOTX ICX QTUM ONT ZRX KNC OMG SKL ANKR STORJ SC HNT AUDIO API3 BAND UMA NMR REQ RLC CTSI CELR COTI DENT HOT WIN JST SUN TWT BNT KDA XVS ALPHA BAKE BEL BETA C98 DAR DODO LINA LIT MASK MDT MLN NKN NULS OXT PHA QI QKC RAD RARE RAY REEF REN RIF RPL RVN SFP SPELL STG SUSHI TLM TOMO TRB TROY UFT UNFI VTHO WOO WRX XEC XNO XRD XVS YFI ZEN ASTR AURORA BICO CKB CSPR DUSK FLUX GLMR MOVR OSMO SCRT STX EGLD JOE KLAY KMD LQTY LRC LPT METIS NTRN ORDI PENDLE PIXEL PORTAL RON SKL STRAX SUPER SXP TNSR TON TURBO VANA WAXP XAI XCH XCN XEM XMR XRD YGG ZETA ZRO ACE AEVO AGLD AI ALICE AMB AMP ANKR APE API3 ASTR ATA AXS BADGER BAL BICO BLZ BOME BOND BSW C98 CANTO CELO CFX COMBO COS COTI CYBER DATA DCR DEGO DENT DEXE DGB DIA DODO DOGS DUSK EDU ELF ELF ELA FIDA FIO FLM FORT FLOKI FRONT GTC HIFI HIGH HIVE HOOK HOT ICP ID IDIA ILV IMX IRIS JOE KAVA KDA KEY KLAY LEVER LINA LOOKS LQTY LSK LTO MBOX MDT MEME MERL METER MINA MOB MOG MOCA MOVR NFP NKN NMR NTRN OAX OCEAN OGN OM ONG ONDO ORBS OSMO OXT PAXG PEOPLE PEPE PERP PHB PHA PIVX PONKE POND POWR PROM PSG PUNDIX QI QUICK RAD RARE RATS RAY RDNT REI REQ RLC RON ROSE RSR RSS3 RUNE SAGA SAND SC SCRT SFP SHIB SKL SLERF SNT SNX SOLV SONIC SPELL SPX SRM STEEM STG STMX STORJ STRK STX SSV SWEAT TAIKO TKO TLM TNSR TOKEN TON TRB TROY TURBO TUT USTC VANA VELO VET VOXEL WAXP WIF WIN WLD WOO XAI XEC XEL XLM XNO XRD XTZ YFI ZEC ZEN ZK ZRO ZRX""".split())
|
| 55 |
-
_TELEGRAM_PAGE_SUFFIX = """<script>(async()=>{const c=document.createElement('div');c.className='card full';c.innerHTML='<h2 class=section-title>Telegram analysis bot</h2><div id=telegramStatus>Loading...</div>';document.querySelector('.grid')?.appendChild(c);try{const r=await fetch('/api/telegram/status');const s=await r.json();document.getElementById('telegramStatus').textContent=(s.enabled?'Enabled':'Disabled')+' · '+s.mode+' · webhook '+(s.webhookEndpointReady?'ready':'not configured')+' · proxy '+(s.proxyConfigured?'yes':'no')+' · relay '+(s.relayConfigured?'yes':'no')+' · authorized users '+s.authorizedUserCount}catch(e){document.getElementById('telegramStatus').textContent='Status unavailable'}})();</script>"""
|
| 56 |
|
| 57 |
|
| 58 |
class _CycleRequest(BaseModel):
|
|
@@ -110,13 +110,20 @@ def _is_future_iso(value: object) -> bool:
|
|
| 110 |
return False
|
| 111 |
|
| 112 |
|
| 113 |
-
async def _probe(url: str, timeout: float = 3.0) ->
|
|
|
|
| 114 |
try:
|
| 115 |
async with httpx.AsyncClient() as client:
|
| 116 |
resp = await client.get(url, timeout=timeout)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
|
| 122 |
async def _mark_price(ccxt_perp_symbol: str) -> float | None:
|
|
@@ -264,13 +271,22 @@ async def _load_symbols() -> tuple[list[dict], str]:
|
|
| 264 |
@router.get("/api/futures/status")
|
| 265 |
async def futures_status() -> JSONResponse:
|
| 266 |
state = _dash_state.snapshot()
|
| 267 |
-
ds4_health,
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
account = await _get_futures_account()
|
| 270 |
positions = await _get_futures_positions()
|
| 271 |
return JSONResponse({
|
| 272 |
-
"primaryDatasource": {
|
| 273 |
-
"secondaryDatasource": {
|
|
|
|
| 274 |
"fieldSources": state.get("field_sources", {}),
|
| 275 |
"currentSignal": state.get("current_signal"),
|
| 276 |
"signalReasons": state.get("signal_reasons", []),
|
|
@@ -292,6 +308,7 @@ async def futures_status() -> JSONResponse:
|
|
| 292 |
"openPositionCount": len(positions.get("positions", [])),
|
| 293 |
"updatedAt": state.get("updated_at"),
|
| 294 |
"serverTime": time.time(),
|
|
|
|
| 295 |
})
|
| 296 |
|
| 297 |
|
|
@@ -302,23 +319,54 @@ async def futures_symbols() -> JSONResponse:
|
|
| 302 |
for item in items:
|
| 303 |
item.setdefault("source", source)
|
| 304 |
item["updatedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(float(updated)))
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
@router.get("/api/futures/positions")
|
| 309 |
async def futures_positions() -> JSONResponse:
|
| 310 |
positions = await _get_futures_positions()
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
unrealized = None
|
| 315 |
if mark:
|
| 316 |
direction = 1 if p["side"] == "long" else -1
|
| 317 |
unrealized = (mark - p["entryPrice"]) * p["size"] * direction
|
| 318 |
-
|
|
|
|
| 319 |
return JSONResponse({"mode": positions.get("mode"), "positions": enriched})
|
| 320 |
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
@router.post("/api/futures/analyze", dependencies=[Depends(_authenticated_dashboard_request)])
|
| 323 |
async def futures_analyze(request: _CycleRequest) -> JSONResponse:
|
| 324 |
"""Run analysis only and return a server-referenced deterministic plan."""
|
|
@@ -456,13 +504,7 @@ async def futures_page() -> HTMLResponse:
|
|
| 456 |
try:
|
| 457 |
page = _TEMPLATE_PATH.read_text(encoding="utf-8")
|
| 458 |
except OSError:
|
| 459 |
-
|
| 460 |
-
# legacy installation has not yet copied the template directory.
|
| 461 |
-
page = _PAGE
|
| 462 |
-
if "</body>" in page:
|
| 463 |
-
page = page.replace("</body>", _TELEGRAM_PAGE_SUFFIX + "</body>", 1)
|
| 464 |
-
else:
|
| 465 |
-
page += _TELEGRAM_PAGE_SUFFIX
|
| 466 |
return HTMLResponse(page)
|
| 467 |
|
| 468 |
|
|
|
|
| 41 |
from trading.futures_execution import get_futures_account as _get_futures_account
|
| 42 |
from trading.futures_execution import get_trading_mode as _get_trading_mode
|
| 43 |
from trading.trade_cycle import run_futures_cycle as _run_futures_cycle
|
| 44 |
+
from trading.dual_datasource_client import get_market_context as _get_market_context
|
| 45 |
from trading.symbols import normalize_symbol
|
| 46 |
|
| 47 |
router = APIRouter()
|
|
|
|
| 53 |
_TEMPLATE_PATH = Path(__file__).with_name("templates") / "hermes_futures_desk_luxury.html"
|
| 54 |
# UI discovery only. These are deliberately unverified; they never authorize execution.
|
| 55 |
_EMERGENCY_BASES = tuple("""BTC ETH USDT BNB SOL XRP USDC ADA AVAX DOGE DOT TRX LINK MATIC LTC BCH UNI ATOM ETC XLM NEAR FIL APT ARB OP ICP SUI INJ MKR AAVE ALGO VET QNT EGLD FTM THETA XTZ EOS FLOW GRT SAND MANA AXS CHZ SNX CRV LDO RUNE KAS PEPE WIF BONK FLOKI SHIB HBAR IMX SEI TIA JUP JTO PYTH WLD FET RNDR TAO KAVA COMP DAI GALA ENJ BAT ZEC DASH XMR NEO IOTA KSM WAVES ZIL ONE CELO ROSE MINA LUNC LUNA GMT GST APE YGG ILV MAGIC DYDX GMX PERP ENS BLUR LPT ARKM STRK DYM MANTA ALT ZK JASMY SFP CAKE BSV BS BCH SV CFX AGIX OCEAN OGN IOST IOTX ICX QTUM ONT ZRX KNC OMG SKL ANKR STORJ SC HNT AUDIO API3 BAND UMA NMR REQ RLC CTSI CELR COTI DENT HOT WIN JST SUN TWT BNT KDA XVS ALPHA BAKE BEL BETA C98 DAR DODO LINA LIT MASK MDT MLN NKN NULS OXT PHA QI QKC RAD RARE RAY REEF REN RIF RPL RVN SFP SPELL STG SUSHI TLM TOMO TRB TROY UFT UNFI VTHO WOO WRX XEC XNO XRD XVS YFI ZEN ASTR AURORA BICO CKB CSPR DUSK FLUX GLMR MOVR OSMO SCRT STX EGLD JOE KLAY KMD LQTY LRC LPT METIS NTRN ORDI PENDLE PIXEL PORTAL RON SKL STRAX SUPER SXP TNSR TON TURBO VANA WAXP XAI XCH XCN XEM XMR XRD YGG ZETA ZRO ACE AEVO AGLD AI ALICE AMB AMP ANKR APE API3 ASTR ATA AXS BADGER BAL BICO BLZ BOME BOND BSW C98 CANTO CELO CFX COMBO COS COTI CYBER DATA DCR DEGO DENT DEXE DGB DIA DODO DOGS DUSK EDU ELF ELF ELA FIDA FIO FLM FORT FLOKI FRONT GTC HIFI HIGH HIVE HOOK HOT ICP ID IDIA ILV IMX IRIS JOE KAVA KDA KEY KLAY LEVER LINA LOOKS LQTY LSK LTO MBOX MDT MEME MERL METER MINA MOB MOG MOCA MOVR NFP NKN NMR NTRN OAX OCEAN OGN OM ONG ONDO ORBS OSMO OXT PAXG PEOPLE PEPE PERP PHB PHA PIVX PONKE POND POWR PROM PSG PUNDIX QI QUICK RAD RARE RATS RAY RDNT REI REQ RLC RON ROSE RSR RSS3 RUNE SAGA SAND SC SCRT SFP SHIB SKL SLERF SNT SNX SOLV SONIC SPELL SPX SRM STEEM STG STMX STORJ STRK STX SSV SWEAT TAIKO TKO TLM TNSR TOKEN TON TRB TROY TURBO TUT USTC VANA VELO VET VOXEL WAXP WIF WIN WLD WOO XAI XEC XEL XLM XNO XRD XTZ YFI ZEC ZEN ZK ZRO ZRX""".split())
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
class _CycleRequest(BaseModel):
|
|
|
|
| 110 |
return False
|
| 111 |
|
| 112 |
|
| 113 |
+
async def _probe(url: str, timeout: float = 3.0) -> dict:
|
| 114 |
+
started = time.perf_counter()
|
| 115 |
try:
|
| 116 |
async with httpx.AsyncClient() as client:
|
| 117 |
resp = await client.get(url, timeout=timeout)
|
| 118 |
+
status = "healthy" if resp.status_code < 400 else "degraded"
|
| 119 |
+
return {"status": status, "endpoint": url, "httpStatus": resp.status_code,
|
| 120 |
+
"latencyMs": round((time.perf_counter()-started)*1000, 1),
|
| 121 |
+
"lastSuccessful": time.time() if resp.status_code < 400 else None,
|
| 122 |
+
"failureReason": None if resp.status_code < 400 else f"HTTP {resp.status_code}"}
|
| 123 |
+
except Exception as exc:
|
| 124 |
+
return {"status": "unavailable", "endpoint": url,
|
| 125 |
+
"latencyMs": round((time.perf_counter()-started)*1000, 1),
|
| 126 |
+
"lastSuccessful": None, "failureReason": type(exc).__name__}
|
| 127 |
|
| 128 |
|
| 129 |
async def _mark_price(ccxt_perp_symbol: str) -> float | None:
|
|
|
|
| 271 |
@router.get("/api/futures/status")
|
| 272 |
async def futures_status() -> JSONResponse:
|
| 273 |
state = _dash_state.snapshot()
|
| 274 |
+
ds4_health, ds2_primary, ds2_fallback = await asyncio.gather(
|
| 275 |
+
_probe(f"{DS4_BASE}/api/short-hunter/health"),
|
| 276 |
+
_probe(f"{DS2_BASE}/real/api/market/tickers"),
|
| 277 |
+
_probe(f"{DS2_BASE}/api/coins/top"),
|
| 278 |
+
)
|
| 279 |
+
def health_obj(value):
|
| 280 |
+
return value if isinstance(value, dict) else {"status": value}
|
| 281 |
+
ds4_health, ds2_primary, ds2_fallback = map(health_obj, (ds4_health, ds2_primary, ds2_fallback))
|
| 282 |
+
ds2_health = ds2_primary if ds2_primary.get("status") in {"healthy", "ok"} else ds2_fallback
|
| 283 |
+
ds2_health["fallbackEndpoint"] = ds2_fallback.get("endpoint")
|
| 284 |
account = await _get_futures_account()
|
| 285 |
positions = await _get_futures_positions()
|
| 286 |
return JSONResponse({
|
| 287 |
+
"primaryDatasource": {**ds4_health, "url": DS4_BASE},
|
| 288 |
+
"secondaryDatasource": {**ds2_health, "url": DS2_BASE,
|
| 289 |
+
"primaryEndpoint": ds2_primary.get("endpoint")},
|
| 290 |
"fieldSources": state.get("field_sources", {}),
|
| 291 |
"currentSignal": state.get("current_signal"),
|
| 292 |
"signalReasons": state.get("signal_reasons", []),
|
|
|
|
| 308 |
"openPositionCount": len(positions.get("positions", [])),
|
| 309 |
"updatedAt": state.get("updated_at"),
|
| 310 |
"serverTime": time.time(),
|
| 311 |
+
"marketDataStatus": "healthy" if ds4_health.get("status") == "healthy" else "degraded",
|
| 312 |
})
|
| 313 |
|
| 314 |
|
|
|
|
| 319 |
for item in items:
|
| 320 |
item.setdefault("source", source)
|
| 321 |
item["updatedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(float(updated)))
|
| 322 |
+
verified = sum(1 for item in items if item.get("futuresVerified") is True)
|
| 323 |
+
return JSONResponse({"symbols": items, "source": source,
|
| 324 |
+
"catalogCount": len(items), "verifiedFuturesCount": verified,
|
| 325 |
+
"marketOnlyCount": max(0, len(items)-verified),
|
| 326 |
+
"updatedAt": items[0]["updatedAt"] if items else None})
|
| 327 |
|
| 328 |
|
| 329 |
@router.get("/api/futures/positions")
|
| 330 |
async def futures_positions() -> JSONResponse:
|
| 331 |
positions = await _get_futures_positions()
|
| 332 |
+
sem = asyncio.Semaphore(4)
|
| 333 |
+
async def enrich(p):
|
| 334 |
+
async with sem:
|
| 335 |
+
try:
|
| 336 |
+
mark = await asyncio.wait_for(_mark_price(p["symbol"]), timeout=4.0)
|
| 337 |
+
except Exception:
|
| 338 |
+
mark = None
|
| 339 |
unrealized = None
|
| 340 |
if mark:
|
| 341 |
direction = 1 if p["side"] == "long" else -1
|
| 342 |
unrealized = (mark - p["entryPrice"]) * p["size"] * direction
|
| 343 |
+
return {**p, "markPrice": mark, "unrealizedPnl": unrealized}
|
| 344 |
+
enriched = await asyncio.gather(*(enrich(p) for p in positions.get("positions", [])))
|
| 345 |
return JSONResponse({"mode": positions.get("mode"), "positions": enriched})
|
| 346 |
|
| 347 |
|
| 348 |
+
@router.get("/api/futures/market", dependencies=[Depends(_authenticated_dashboard_request)])
|
| 349 |
+
async def futures_market(symbol: str = "BTCUSDT", interval: str = "5m", limit: int = 120) -> JSONResponse:
|
| 350 |
+
"""Return real merged OHLCV and market metrics for the chart."""
|
| 351 |
+
if interval not in {"1m", "5m", "15m", "1h"}:
|
| 352 |
+
raise HTTPException(status_code=400, detail="Unsupported interval")
|
| 353 |
+
context = await _get_market_context(symbol, timeframe=interval)
|
| 354 |
+
merged = context.get("merged") or {}
|
| 355 |
+
candles = merged.get("ohlcv") or []
|
| 356 |
+
if limit > 0:
|
| 357 |
+
candles = candles[-min(limit, 500):]
|
| 358 |
+
ticker = merged.get("ticker") or {}
|
| 359 |
+
return JSONResponse({"symbol": context.get("symbol"), "interval": interval,
|
| 360 |
+
"candles": candles, "ticker": ticker,
|
| 361 |
+
"funding": merged.get("funding"),
|
| 362 |
+
"openInterest": merged.get("openInterest"),
|
| 363 |
+
"source": context.get("sources", {}).get("ohlcv", "unavailable"),
|
| 364 |
+
"sources": context.get("sources", {}),
|
| 365 |
+
"fetchedAt": context.get("fetchedAt"),
|
| 366 |
+
"warnings": context.get("warnings", []),
|
| 367 |
+
"noTradeGuard": context.get("noTradeGuard", True)})
|
| 368 |
+
|
| 369 |
+
|
| 370 |
@router.post("/api/futures/analyze", dependencies=[Depends(_authenticated_dashboard_request)])
|
| 371 |
async def futures_analyze(request: _CycleRequest) -> JSONResponse:
|
| 372 |
"""Run analysis only and return a server-referenced deterministic plan."""
|
|
|
|
| 504 |
try:
|
| 505 |
page = _TEMPLATE_PATH.read_text(encoding="utf-8")
|
| 506 |
except OSError:
|
| 507 |
+
page = "<!doctype html><title>Futures Desk unavailable</title><p>The Futures dashboard template is not installed.</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
return HTMLResponse(page)
|
| 509 |
|
| 510 |
|
hermes_overlay/tools/templates/hermes_futures_desk_luxury.html
CHANGED
|
@@ -440,6 +440,10 @@ details.tech-shell[open] summary svg{transform:rotate(90deg)}
|
|
| 440 |
}
|
| 441 |
@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
| 442 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
</head>
|
| 444 |
<body>
|
| 445 |
<div class="app-shell">
|
|
@@ -530,18 +534,23 @@ details.tech-shell[open] summary svg{transform:rotate(90deg)}
|
|
| 530 |
</div>
|
| 531 |
<div class="action-cluster">
|
| 532 |
<button id="analyze" class="btn primary"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg>Analyze market</button>
|
| 533 |
-
<button id="execute" class="btn secondary" disabled><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>Execute paper trade</button>
|
| 534 |
</div>
|
| 535 |
</div>
|
| 536 |
</section>
|
| 537 |
|
| 538 |
<section class="grid">
|
| 539 |
-
<div id="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
<div class="decision-icon"><span class="medallion amber"><svg id="decisionIconSvg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg></span></div>
|
| 541 |
<div class="decision-body">
|
| 542 |
<div class="section-title">Decision</div>
|
| 543 |
-
<strong id="decision">
|
| 544 |
-
<div id="decisionHint" class="hint">
|
| 545 |
</div>
|
| 546 |
</div>
|
| 547 |
|
|
@@ -617,7 +626,7 @@ details.tech-shell[open] summary svg{transform:rotate(90deg)}
|
|
| 617 |
<div id="signalsCard" class="card span-5 accS"><h2 class="section-title"><span class="medallion sapphire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/></svg></span>Signal analysis</h2><div id="signals" class="rows"><div class="empty">No valid signal components were available.</div></div></div>
|
| 618 |
|
| 619 |
<div id="advisoryCard" class="card span-7 accL"><h2 class="section-title"><span class="medallion violet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 0 1 7 7c0 3-2 4-2 7H7c0-3-2-4-2-7a7 7 0 0 1 7-7z"/><path d="M9 21h6"/><path d="M12 6v6l3 2"/></svg></span>Remote advisory</h2><div id="advisoryPanel" class="empty">Disabled until Analyze is run with Remote advisory enabled.</div></div>
|
| 620 |
-
<div id="accountCard" class="card span-5 accP"><div class="account-head"><h2 class="section-title"><span class="medallion amber"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="14" rx="2.5"/><path d="M3 10h18"/><path d="M7 3.5h6"/></svg></span>Account & positions</h2></div><div id="account" class="rows"><div class="empty">Waiting for account status…</div></div><div id="positionsTable" class="positions"
|
| 621 |
|
| 622 |
<div id="sourcesCard" class="card source-wrap">
|
| 623 |
<div class="source-grid">
|
|
@@ -626,6 +635,7 @@ details.tech-shell[open] summary svg{transform:rotate(90deg)}
|
|
| 626 |
<div id="ds2Card" class="card source"><h2 class="section-title"><span class="medallion sapphire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12h8M12 8v8"/></svg></span>Datasource 2 · Complementary</h2><div id="ds2" class="empty">Loading…</div></div>
|
| 627 |
</div>
|
| 628 |
</div>
|
|
|
|
| 629 |
|
| 630 |
<div class="card full accP"><h2 class="section-title"><span class="medallion amber"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>Trade rejection reasons</h2><div id="reasons" class="empty">No plan analyzed yet.</div></div>
|
| 631 |
|
|
@@ -651,6 +661,8 @@ details.tech-shell[open] summary svg{transform:rotate(90deg)}
|
|
| 651 |
const $=id=>document.getElementById(id);
|
| 652 |
const esc=x=>String(x??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 653 |
const fmt=x=>x===null||x===undefined||x===''?'—':esc(x);
|
|
|
|
|
|
|
| 654 |
let symbols=[],activeIndex=-1,analysisBusy=false,latestPlan=null,latestStatus=null,statusBusy=false,positionsBusy=false,confirmResolver=null,hydratedFromStatus=false,selectedSymbolMeta=null;
|
| 655 |
|
| 656 |
async function apiFetch(url,options={},timeoutMs=15000){
|
|
@@ -801,13 +813,16 @@ function formatLocalTime(iso){
|
|
| 801 |
const d=new Date(iso);return isNaN(d.getTime())?String(iso):'Valid until '+d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
| 802 |
}
|
| 803 |
function updateExecutionAvailability(){
|
| 804 |
-
if(!latestPlan)
|
| 805 |
const d=latestPlan.decision||'NO_TRADE';
|
| 806 |
const validExpiry=latestPlan.expires_at&&Date.parse(latestPlan.expires_at)>Date.now();
|
| 807 |
const currentSymbol=normalizedSymbol(),planSymbol=String(latestPlan.symbol||'').toUpperCase();
|
| 808 |
const currentRisk=riskValue(),planRisk=latestPlan.risk_profile||'';
|
| 809 |
const verified=latestPlan.futuresVerified===true || selectedSymbolMeta?.symbol===planSymbol&&selectedSymbolMeta?.futuresVerified===true;
|
| 810 |
-
|
|
|
|
|
|
|
|
|
|
| 811 |
const track=$('expiryTrack').children;
|
| 812 |
if(validExpiry&&latestPlan.created_at){
|
| 813 |
const total=Date.parse(latestPlan.expires_at)-Date.parse(latestPlan.created_at),left=Date.parse(latestPlan.expires_at)-Date.now();
|
|
@@ -836,9 +851,9 @@ function renderPlan(p){
|
|
| 836 |
$('expiry').title=p.expires_at||'';
|
| 837 |
$('expirySub').textContent=formatLocalTime(p.expires_at);
|
| 838 |
$('planRows').innerHTML=[
|
| 839 |
-
rowCopy('Entry',p.entry),rowCopy('Stop Loss',p.stop_loss),rowCopy('Take Profit',p.take_profit),
|
| 840 |
row('Reward-to-risk',p.reward_to_risk),row('Leverage',p.effective_leverage??p.requested_leverage),
|
| 841 |
-
row('Quantity',p.quantity),row('Slippage',p.estimated_slippage_percent),row('Created',p.created_at),
|
| 842 |
rowCopy('Plan ID',p.planId)
|
| 843 |
].join('');
|
| 844 |
const comps=Object.entries(p.components||{});
|
|
@@ -853,12 +868,12 @@ function renderPlan(p){
|
|
| 853 |
|
| 854 |
function renderPositions(list,mode){
|
| 855 |
const positions=Array.isArray(list)?list:[];
|
| 856 |
-
if(!positions.length){$('positionsTable').hidden=
|
| 857 |
$('positionsTable').hidden=false;
|
| 858 |
$('positionsTable').innerHTML='<table class="acct"><thead><tr><th>Symbol</th><th>Side</th><th>Quantity</th><th>Entry</th><th>Mark</th><th>PnL</th></tr></thead><tbody>'+positions.map(p=>{
|
| 859 |
const side=String(p.side||p.positionSide||'—').toUpperCase(),sideClass=side.includes('LONG')?'long':side.includes('SHORT')?'short':'';
|
| 860 |
const pnl=p.unrealizedPnl??p.pnl??p.unrealized_pnl,pnlNum=parseFloat(pnl),pnlClass=isFinite(pnlNum)?(pnlNum>=0?'pnl-pos':'pnl-neg'):'';
|
| 861 |
-
return '<tr><td>'+fmt(p.symbol)+'</td><td><span class="pos-side '+sideClass+'">'+fmt(side)+'</span></td><td>'+
|
| 862 |
}).join('')+'</tbody></table>';
|
| 863 |
}
|
| 864 |
async function refreshPositions(){
|
|
@@ -874,10 +889,10 @@ function renderStatus(s){
|
|
| 874 |
latestStatus=s;
|
| 875 |
$('mode').textContent=fmt(s.tradingMode);
|
| 876 |
$('account').innerHTML=[
|
| 877 |
-
row('Paper equity',s.equity),row('Daily PnL',s.realizedPnlToday),
|
| 878 |
row('Open positions',s.openPositionCount??0),row('Latest execution',s.latestPaperExecutionResult?.status||'None')
|
| 879 |
].join('');
|
| 880 |
-
const sw=(s.warnings||[]).join(' '),bs=/HTTP 451/i.test(sw)?'unavailable':Object.values(s.fieldSources||{}).some(x=>x==='binance_public')?'degraded':'
|
| 881 |
const badges=[['DS4',s.primaryDatasource?.status],['Binance',bs],['DS2',s.secondaryDatasource?.status],['Remote Advisory',s.latestTradePlan?.external_advisory?(s.latestTradePlan.external_advisory.status||'available'):'info']];
|
| 882 |
$('badges').innerHTML=badges.map(x=>{
|
| 883 |
const cls=x[1]==='info'?'info':statusClass(x[1]);
|
|
@@ -886,6 +901,10 @@ function renderStatus(s){
|
|
| 886 |
const now=new Date().toLocaleTimeString();
|
| 887 |
$('refresh').textContent='Last refresh '+now;$('footerTime').textContent=now;
|
| 888 |
renderSources(s);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
|
| 890 |
/* hydrate the workspace with the server's last known plan on first load only */
|
| 891 |
if(!hydratedFromStatus&&!latestPlan&&s.latestTradePlan){
|
|
@@ -915,11 +934,11 @@ function setComboOpen(open){
|
|
| 915 |
$('symbolSearch').setAttribute('aria-expanded',String(open));
|
| 916 |
if(!open)activeIndex=-1;
|
| 917 |
}
|
| 918 |
-
function chooseSymbol(symbol){$('symbolSearch').value=String(symbol||'').toUpperCase();selectedSymbolMeta=symbols.find(x=>x.symbol===$('symbolSearch').value)||null;setComboOpen(false);updateExecutionAvailability()}
|
| 919 |
function showList(){
|
| 920 |
const q=$('symbolSearch').value.trim().toUpperCase();
|
| 921 |
const found=symbols.filter(x=>(String(x.symbol||'')+' '+String(x.name||'')+' '+String(x.baseAsset||'')+' '+String(x.quoteAsset||'')+' '+String(x.rank||'')).toUpperCase().includes(q)).slice(0,80);
|
| 922 |
-
$('symbolList').innerHTML=found.length?found.map(x=>'<div class="combo-item" role="option" tabindex="-1" data-symbol="'+esc(x.symbol)+'"><span><b>'+esc(x.symbol)+'</b><small>'+esc(x.name||'')+' · '+esc(x.baseAsset)+' / '+esc(x.quoteAsset)+'</small></span><small class="'+(x.futuresVerified?'verified':'unverified')+'">'+(x.futuresVerified?'✓ Verified Futures':'Market-only')+'</small></div>').join(''):'<div class="empty">No active pair found. Validated manual entry is allowed.</div>';
|
| 923 |
setComboOpen(true);
|
| 924 |
document.querySelectorAll('.combo-item').forEach(el=>el.onclick=()=>chooseSymbol(el.dataset.symbol));
|
| 925 |
}
|
|
@@ -949,11 +968,23 @@ async function loadSymbols(){
|
|
| 949 |
try{
|
| 950 |
const p=await apiFetch('/api/futures/symbols');symbols=Array.isArray(p.symbols)?p.symbols.filter(x=>x&&x.symbol):[];
|
| 951 |
if(latestPlan){latestPlan.futuresVerified=symbols.some(x=>x.symbol===String(latestPlan.symbol||'').toUpperCase()&&x.futuresVerified===true);updateExecutionAvailability()}
|
| 952 |
-
$('symbolState').textContent=symbols.length?symbols.length
|
| 953 |
renderChips();
|
| 954 |
}catch(e){$('symbolState').textContent='Symbol service unavailable; validated manual entry enabled'}
|
| 955 |
}
|
| 956 |
function normalizedSymbol(){return $('symbolSearch').value.trim().toUpperCase()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
function setAnalysisBusy(busy){
|
| 958 |
analysisBusy=busy;$('analyze').disabled=busy;$('loading').classList.toggle('show',busy);$('analyze').setAttribute('aria-busy',String(busy));
|
| 959 |
if(busy)$('execute').disabled=true;else updateExecutionAvailability();
|
|
@@ -967,7 +998,7 @@ $('analyze').onclick=async()=>{
|
|
| 967 |
const p=await apiFetch('/api/futures/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({symbol,risk_profile:riskValue(),include_external_context:$('advisory').checked})});
|
| 968 |
renderPlan(p);toast(p.decision==='NO_TRADE'?'Analysis complete — no trade':'Analysis complete — '+p.decision+' plan ready','ok');
|
| 969 |
}catch(e){
|
| 970 |
-
latestPlan=null;$('execute').disabled=true;$('reasons').innerHTML='<div class="empty">'+esc(e.message)+'</div>';$('decision').textContent='
|
| 971 |
toast(e.message||'Analysis failed','err');
|
| 972 |
}finally{clearInterval(timer);$('elapsed').textContent='0.0s';setAnalysisBusy(false)}
|
| 973 |
};
|
|
@@ -986,7 +1017,7 @@ $('execute').onclick=async()=>{
|
|
| 986 |
function tickClock(){$('clock').textContent=new Date().toLocaleTimeString()}
|
| 987 |
setInterval(tickClock,1000);tickClock();
|
| 988 |
setInterval(()=>{if(latestPlan?.expires_at){$('expiry').textContent=expiryCountdown(latestPlan.expires_at);updateExecutionAvailability()}},1000);
|
| 989 |
-
loadSymbols();refresh();
|
| 990 |
setInterval(()=>{if(!document.hidden)refresh()},5000);
|
| 991 |
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh()});
|
| 992 |
</script>
|
|
|
|
| 440 |
}
|
| 441 |
@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
| 442 |
</style>
|
| 443 |
+
<style>
|
| 444 |
+
.market-card{min-height:280px}.market-header,.chart-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px}.market-header strong{font-size:24px}.market-header .muted{display:block;font-size:12px;margin-top:4px}.market-chart{width:100%;height:220px;margin-top:12px;background:linear-gradient(180deg,#111d29,#0a121a);border:1px solid var(--line);border-radius:12px}.chart-empty{fill:var(--muted);font-size:14px}.source-badge{color:var(--blue);font-size:10px;border:1px solid #315a80;border-radius:999px;padding:2px 5px}.state-NOT_ANALYZED{border-color:var(--line)}
|
| 445 |
+
@media(max-width:720px){.market-card{grid-column:span 12}.market-header,.chart-toolbar{align-items:flex-start;flex-direction:column}.market-chart{height:180px}.action-cluster{align-items:stretch}.action-cluster .mobile-note{max-width:none}}
|
| 446 |
+
</style>
|
| 447 |
</head>
|
| 448 |
<body>
|
| 449 |
<div class="app-shell">
|
|
|
|
| 534 |
</div>
|
| 535 |
<div class="action-cluster">
|
| 536 |
<button id="analyze" class="btn primary"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg>Analyze market</button>
|
| 537 |
+
<button id="execute" class="btn secondary" disabled><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>Execute paper trade</button><div id="executeReason" class="mobile-note">Run analysis first</div>
|
| 538 |
</div>
|
| 539 |
</div>
|
| 540 |
</section>
|
| 541 |
|
| 542 |
<section class="grid">
|
| 543 |
+
<div id="marketCard" class="card span-7 market-card">
|
| 544 |
+
<div class="market-header"><div><div class="section-title">Selected market</div><strong id="marketSymbol">BTCUSDT</strong><span id="marketMeta" class="muted">Waiting for market data</span></div><strong id="marketPrice" class="num">—</strong></div>
|
| 545 |
+
<div class="chart-toolbar"><span id="chartSource" class="muted">Chart unavailable</span><div id="chartIntervals" class="segmented"><button type="button" data-interval="1m">1m</button><button type="button" data-interval="5m" class="active">5m</button><button type="button" data-interval="15m">15m</button><button type="button" data-interval="1h">1h</button></div></div>
|
| 546 |
+
<svg id="marketChart" class="market-chart" viewBox="0 0 900 260" preserveAspectRatio="none" role="img" aria-label="OHLCV market chart"><text x="450" y="130" text-anchor="middle" class="chart-empty">No real OHLCV data available</text></svg>
|
| 547 |
+
</div>
|
| 548 |
+
<div id="decisionCard" class="card decision state-NOT_ANALYZED">
|
| 549 |
<div class="decision-icon"><span class="medallion amber"><svg id="decisionIconSvg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg></span></div>
|
| 550 |
<div class="decision-body">
|
| 551 |
<div class="section-title">Decision</div>
|
| 552 |
+
<strong id="decision">Waiting for analysis</strong>
|
| 553 |
+
<div id="decisionHint" class="hint">Run an analysis to evaluate the current market.</div>
|
| 554 |
</div>
|
| 555 |
</div>
|
| 556 |
|
|
|
|
| 626 |
<div id="signalsCard" class="card span-5 accS"><h2 class="section-title"><span class="medallion sapphire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/></svg></span>Signal analysis</h2><div id="signals" class="rows"><div class="empty">No valid signal components were available.</div></div></div>
|
| 627 |
|
| 628 |
<div id="advisoryCard" class="card span-7 accL"><h2 class="section-title"><span class="medallion violet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 0 1 7 7c0 3-2 4-2 7H7c0-3-2-4-2-7a7 7 0 0 1 7-7z"/><path d="M9 21h6"/><path d="M12 6v6l3 2"/></svg></span>Remote advisory</h2><div id="advisoryPanel" class="empty">Disabled until Analyze is run with Remote advisory enabled.</div></div>
|
| 629 |
+
<div id="accountCard" class="card span-5 accP"><div class="account-head"><h2 class="section-title"><span class="medallion amber"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="14" rx="2.5"/><path d="M3 10h18"/><path d="M7 3.5h6"/></svg></span>Account & positions</h2></div><div id="account" class="rows"><div class="empty">Waiting for account status…</div></div><div id="positionsTable" class="positions"><div class="empty">No open Paper positions</div></div></div>
|
| 630 |
|
| 631 |
<div id="sourcesCard" class="card source-wrap">
|
| 632 |
<div class="source-grid">
|
|
|
|
| 635 |
<div id="ds2Card" class="card source"><h2 class="section-title"><span class="medallion sapphire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12h8M12 8v8"/></svg></span>Datasource 2 · Complementary</h2><div id="ds2" class="empty">Loading…</div></div>
|
| 636 |
</div>
|
| 637 |
</div>
|
| 638 |
+
<div id="telegramCard" class="card full"><h2 class="section-title">System health · Telegram</h2><div id="telegramStatus" class="empty">Status unavailable</div></div>
|
| 639 |
|
| 640 |
<div class="card full accP"><h2 class="section-title"><span class="medallion amber"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>Trade rejection reasons</h2><div id="reasons" class="empty">No plan analyzed yet.</div></div>
|
| 641 |
|
|
|
|
| 661 |
const $=id=>document.getElementById(id);
|
| 662 |
const esc=x=>String(x??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 663 |
const fmt=x=>x===null||x===undefined||x===''?'—':esc(x);
|
| 664 |
+
const numFmt=x=>{const n=Number(x);return Number.isFinite(n)?n.toLocaleString(undefined,{maximumFractionDigits:8}):'—'};
|
| 665 |
+
const money=x=>{const n=Number(x);return Number.isFinite(n)?n.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})+' USDT':'—'};
|
| 666 |
let symbols=[],activeIndex=-1,analysisBusy=false,latestPlan=null,latestStatus=null,statusBusy=false,positionsBusy=false,confirmResolver=null,hydratedFromStatus=false,selectedSymbolMeta=null;
|
| 667 |
|
| 668 |
async function apiFetch(url,options={},timeoutMs=15000){
|
|
|
|
| 813 |
const d=new Date(iso);return isNaN(d.getTime())?String(iso):'Valid until '+d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
| 814 |
}
|
| 815 |
function updateExecutionAvailability(){
|
| 816 |
+
if(!latestPlan){$('execute').disabled=true;$('executeReason').textContent='Run analysis first';return}
|
| 817 |
const d=latestPlan.decision||'NO_TRADE';
|
| 818 |
const validExpiry=latestPlan.expires_at&&Date.parse(latestPlan.expires_at)>Date.now();
|
| 819 |
const currentSymbol=normalizedSymbol(),planSymbol=String(latestPlan.symbol||'').toUpperCase();
|
| 820 |
const currentRisk=riskValue(),planRisk=latestPlan.risk_profile||'';
|
| 821 |
const verified=latestPlan.futuresVerified===true || selectedSymbolMeta?.symbol===planSymbol&&selectedSymbolMeta?.futuresVerified===true;
|
| 822 |
+
const disabled=analysisBusy||!latestPlan.planId||!(d==='LONG'||d==='SHORT')||!!latestPlan.noTradeGuard||!latestPlan.risk_approved||!verified||!validExpiry||!!latestPlan.executed||currentSymbol!==planSymbol||currentRisk!==planRisk;
|
| 823 |
+
$('execute').disabled=disabled;
|
| 824 |
+
const reason=analysisBusy?'Analysis in progress':!latestPlan.planId?'Run analysis first':d!=='LONG'&&d!=='SHORT'?'No directional plan':latestPlan.noTradeGuard?'noTradeGuard active':!latestPlan.risk_approved?'Risk approval failed':!verified?'Market-only symbol':!validExpiry?'Plan expired':latestPlan.executed?'Plan already executed':currentSymbol!==planSymbol?'Symbol changed':currentRisk!==planRisk?'Risk profile changed':'Ready';
|
| 825 |
+
$('executeReason').textContent=reason;
|
| 826 |
const track=$('expiryTrack').children;
|
| 827 |
if(validExpiry&&latestPlan.created_at){
|
| 828 |
const total=Date.parse(latestPlan.expires_at)-Date.parse(latestPlan.created_at),left=Date.parse(latestPlan.expires_at)-Date.now();
|
|
|
|
| 851 |
$('expiry').title=p.expires_at||'';
|
| 852 |
$('expirySub').textContent=formatLocalTime(p.expires_at);
|
| 853 |
$('planRows').innerHTML=[
|
| 854 |
+
rowCopy('Entry',money(p.entry)),rowCopy('Stop Loss',money(p.stop_loss)),rowCopy('Take Profit',money(p.take_profit)),
|
| 855 |
row('Reward-to-risk',p.reward_to_risk),row('Leverage',p.effective_leverage??p.requested_leverage),
|
| 856 |
+
row('Quantity',numFmt(p.quantity)),row('Slippage',p.estimated_slippage_percent==null?'—':p.estimated_slippage_percent+'%'),row('Created',p.created_at),
|
| 857 |
rowCopy('Plan ID',p.planId)
|
| 858 |
].join('');
|
| 859 |
const comps=Object.entries(p.components||{});
|
|
|
|
| 868 |
|
| 869 |
function renderPositions(list,mode){
|
| 870 |
const positions=Array.isArray(list)?list:[];
|
| 871 |
+
if(!positions.length){$('positionsTable').hidden=false;$('positionsTable').innerHTML='<div class="empty">No open Paper positions</div>';return}
|
| 872 |
$('positionsTable').hidden=false;
|
| 873 |
$('positionsTable').innerHTML='<table class="acct"><thead><tr><th>Symbol</th><th>Side</th><th>Quantity</th><th>Entry</th><th>Mark</th><th>PnL</th></tr></thead><tbody>'+positions.map(p=>{
|
| 874 |
const side=String(p.side||p.positionSide||'—').toUpperCase(),sideClass=side.includes('LONG')?'long':side.includes('SHORT')?'short':'';
|
| 875 |
const pnl=p.unrealizedPnl??p.pnl??p.unrealized_pnl,pnlNum=parseFloat(pnl),pnlClass=isFinite(pnlNum)?(pnlNum>=0?'pnl-pos':'pnl-neg'):'';
|
| 876 |
+
return '<tr><td>'+fmt(p.symbol)+'</td><td><span class="pos-side '+sideClass+'">'+fmt(side)+'</span></td><td>'+numFmt(p.quantity??p.size??p.qty??p.positionAmt)+'</td><td>'+money(p.entryPrice??p.entry_price)+'</td><td>'+money(p.markPrice??p.mark_price)+'</td><td class="'+pnlClass+'">'+money(pnl)+'</td></tr>';
|
| 877 |
}).join('')+'</tbody></table>';
|
| 878 |
}
|
| 879 |
async function refreshPositions(){
|
|
|
|
| 889 |
latestStatus=s;
|
| 890 |
$('mode').textContent=fmt(s.tradingMode);
|
| 891 |
$('account').innerHTML=[
|
| 892 |
+
row('Paper equity',money(s.equity)),row('Daily PnL',money(s.realizedPnlToday)),
|
| 893 |
row('Open positions',s.openPositionCount??0),row('Latest execution',s.latestPaperExecutionResult?.status||'None')
|
| 894 |
].join('');
|
| 895 |
+
const sw=(s.warnings||[]).join(' '),bs=/HTTP 451/i.test(sw)?'unavailable':Object.values(s.fieldSources||{}).some(x=>x==='binance_public')?'degraded':'standby';
|
| 896 |
const badges=[['DS4',s.primaryDatasource?.status],['Binance',bs],['DS2',s.secondaryDatasource?.status],['Remote Advisory',s.latestTradePlan?.external_advisory?(s.latestTradePlan.external_advisory.status||'available'):'info']];
|
| 897 |
$('badges').innerHTML=badges.map(x=>{
|
| 898 |
const cls=x[1]==='info'?'info':statusClass(x[1]);
|
|
|
|
| 901 |
const now=new Date().toLocaleTimeString();
|
| 902 |
$('refresh').textContent='Last refresh '+now;$('footerTime').textContent=now;
|
| 903 |
renderSources(s);
|
| 904 |
+
apiFetch('/api/telegram/status',{},5000).then(t=>{
|
| 905 |
+
const state=!t.enabled?'Disabled':!t.authorizedUserCount?'Owner setup required':(t.proxyConfigured||t.relayConfigured?'Ready':'Relay/proxy required for proactive alerts');
|
| 906 |
+
$('telegramStatus').innerHTML='<span class="badge '+(state==='Ready'?'ok':state==='Disabled'?'info':'degraded')+'">'+esc(state)+'</span><div class="muted">Webhook '+(t.webhookEndpointReady?'ready':'not configured')+' · mode '+esc(t.mode||'webhook')+'</div>';
|
| 907 |
+
}).catch(()=>{$('telegramStatus').textContent='Status unavailable'});
|
| 908 |
|
| 909 |
/* hydrate the workspace with the server's last known plan on first load only */
|
| 910 |
if(!hydratedFromStatus&&!latestPlan&&s.latestTradePlan){
|
|
|
|
| 934 |
$('symbolSearch').setAttribute('aria-expanded',String(open));
|
| 935 |
if(!open)activeIndex=-1;
|
| 936 |
}
|
| 937 |
+
function chooseSymbol(symbol){$('symbolSearch').value=String(symbol||'').toUpperCase();selectedSymbolMeta=symbols.find(x=>x.symbol===$('symbolSearch').value)||null;setComboOpen(false);updateExecutionAvailability();loadChart()}
|
| 938 |
function showList(){
|
| 939 |
const q=$('symbolSearch').value.trim().toUpperCase();
|
| 940 |
const found=symbols.filter(x=>(String(x.symbol||'')+' '+String(x.name||'')+' '+String(x.baseAsset||'')+' '+String(x.quoteAsset||'')+' '+String(x.rank||'')).toUpperCase().includes(q)).slice(0,80);
|
| 941 |
+
$('symbolList').innerHTML=found.length?found.map(x=>'<div class="combo-item" role="option" tabindex="-1" data-symbol="'+esc(x.symbol)+'"><span><b>'+esc(x.symbol)+'</b><small>'+esc(x.name||'')+' · '+esc(x.baseAsset)+' / '+esc(x.quoteAsset)+'</small></span><small><span class="source-badge">'+esc(x.source||'unknown')+'</span> <span class="'+(x.futuresVerified?'verified':'unverified')+'">'+(x.futuresVerified?'✓ Verified Futures':'Market-only')+'</span></small></div>').join(''):'<div class="empty">No active pair found. Validated manual entry is allowed.</div>';
|
| 942 |
setComboOpen(true);
|
| 943 |
document.querySelectorAll('.combo-item').forEach(el=>el.onclick=()=>chooseSymbol(el.dataset.symbol));
|
| 944 |
}
|
|
|
|
| 968 |
try{
|
| 969 |
const p=await apiFetch('/api/futures/symbols');symbols=Array.isArray(p.symbols)?p.symbols.filter(x=>x&&x.symbol):[];
|
| 970 |
if(latestPlan){latestPlan.futuresVerified=symbols.some(x=>x.symbol===String(latestPlan.symbol||'').toUpperCase()&&x.futuresVerified===true);updateExecutionAvailability()}
|
| 971 |
+
$('symbolState').textContent=symbols.length?`${p.catalogCount??symbols.length} catalog entries · ${p.verifiedFuturesCount??symbols.filter(x=>x.futuresVerified).length} verified Futures · ${p.marketOnlyCount??symbols.filter(x=>!x.futuresVerified).length} market-only`:'Symbol service unavailable; validated manual entry enabled';
|
| 972 |
renderChips();
|
| 973 |
}catch(e){$('symbolState').textContent='Symbol service unavailable; validated manual entry enabled'}
|
| 974 |
}
|
| 975 |
function normalizedSymbol(){return $('symbolSearch').value.trim().toUpperCase()}
|
| 976 |
+
let chartInterval='5m',chartBusy=false;
|
| 977 |
+
function renderChart(data){
|
| 978 |
+
const candles=Array.isArray(data?.candles)?data.candles:[],svg=$('marketChart');
|
| 979 |
+
$('chartSource').textContent=candles.length?(data.source||'unavailable')+' · '+(data.fetchedAt?new Date(data.fetchedAt*1000).toLocaleTimeString():'fresh'):'Chart unavailable — no real candles returned';
|
| 980 |
+
if(!candles.length){svg.innerHTML='<text x="450" y="130" text-anchor="middle" class="chart-empty">No real OHLCV data available</text>';return}
|
| 981 |
+
const vals=candles.map(c=>Number(c.close??c[4])).filter(Number.isFinite),lo=Math.min(...vals),hi=Math.max(...vals),span=hi-lo||1;
|
| 982 |
+
const points=vals.map((v,i)=>`${(i/(vals.length-1||1))*880+10},${240-((v-lo)/span)*220}`).join(' ');
|
| 983 |
+
svg.innerHTML='<polyline fill="none" stroke="var(--gold)" stroke-width="2" points="'+points+'"/><line x1="10" y1="240" x2="890" y2="240" stroke="var(--line)"/>';
|
| 984 |
+
const last=vals.at(-1);$('marketPrice').textContent=money(last);$('marketSymbol').textContent=data.symbol||normalizedSymbol();
|
| 985 |
+
}
|
| 986 |
+
async function loadChart(){if(chartBusy)return;chartBusy=true;try{renderChart(await apiFetch('/api/futures/market?symbol='+encodeURIComponent(normalizedSymbol()||'BTCUSDT')+'&interval='+chartInterval+'&limit=120',{},12000))}catch(e){renderChart({candles:[]});$('chartSource').textContent='Chart unavailable'}finally{chartBusy=false}}
|
| 987 |
+
document.querySelectorAll('#chartIntervals button').forEach(btn=>btn.onclick=()=>{document.querySelectorAll('#chartIntervals button').forEach(b=>b.classList.remove('active'));btn.classList.add('active');chartInterval=btn.dataset.interval;loadChart()});
|
| 988 |
function setAnalysisBusy(busy){
|
| 989 |
analysisBusy=busy;$('analyze').disabled=busy;$('loading').classList.toggle('show',busy);$('analyze').setAttribute('aria-busy',String(busy));
|
| 990 |
if(busy)$('execute').disabled=true;else updateExecutionAvailability();
|
|
|
|
| 998 |
const p=await apiFetch('/api/futures/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({symbol,risk_profile:riskValue(),include_external_context:$('advisory').checked})});
|
| 999 |
renderPlan(p);toast(p.decision==='NO_TRADE'?'Analysis complete — no trade':'Analysis complete — '+p.decision+' plan ready','ok');
|
| 1000 |
}catch(e){
|
| 1001 |
+
latestPlan=null;$('execute').disabled=true;$('reasons').innerHTML='<div class="empty">'+esc(e.message)+'</div>';$('decision').textContent='Analysis failed';$('decisionHint').textContent='No executable plan was produced.';$('decisionCard').className='card decision state-ANALYSIS_FAILED';$('executeReason').textContent='Analysis failed';
|
| 1002 |
toast(e.message||'Analysis failed','err');
|
| 1003 |
}finally{clearInterval(timer);$('elapsed').textContent='0.0s';setAnalysisBusy(false)}
|
| 1004 |
};
|
|
|
|
| 1017 |
function tickClock(){$('clock').textContent=new Date().toLocaleTimeString()}
|
| 1018 |
setInterval(tickClock,1000);tickClock();
|
| 1019 |
setInterval(()=>{if(latestPlan?.expires_at){$('expiry').textContent=expiryCountdown(latestPlan.expires_at);updateExecutionAvailability()}},1000);
|
| 1020 |
+
loadSymbols();refresh();loadChart();
|
| 1021 |
setInterval(()=>{if(!document.hidden)refresh()},5000);
|
| 1022 |
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh()});
|
| 1023 |
</script>
|