{
  "schema_version": 1,
  "channel": "homelab-runtime",
  "command_id": "GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260802T134100Z",
  "status": "FAIL",
  "rc": 1,
  "host": "pve01",
  "mode": "verify",
  "component": "gram1-site-usdc-v42-read-only-exact-scope",
  "started_at_utc": "2026-08-02T13:42:05Z",
  "finished_at_utc": "2026-08-02T13:42:05Z",
  "reference_register_checked": true,
  "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e",
  "error_register_checked": true,
  "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83",
  "command_sha256": "e2078350b10ebe49f406309c6e057cf9a2be7e285a0022781467ab2439e156fe",
  "duplicate_failed_command_blocked": false,
  "block_reason": null,
  "execution_started": true,
  "change_declared": false,
  "result_contract_valid": true,
  "result_contract_status": null,
  "result_contract_error": null,
  "command_rc": 1,
  "changes_made": false,
  "rollback_started": false,
  "rollback_restored": null,
  "mutation_outcome": "NO_MUTATION",
  "sanitized": true,
  "secrets_included": false,
  "private_addresses_included": false,
  "raw_evidence_retained_locally": true,
  "raw_evidence_sha256": "9c92bb2c38e79ba79fb4e713821b30a4cbaa5aae4a54e6d52d6204cd4ea07500",
  "sanitized_output_sha256": "9c92bb2c38e79ba79fb4e713821b30a4cbaa5aae4a54e6d52d6204cd4ea07500",
  "output_truncated_in_json": true,
  "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt",
  "output": "HOMELAB_RESULT_CONTRACT={\"changes_made\":false,\"command_id\":\"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260802T134100Z\",\"command_rc\":0,\"control_plane_error\":null,\"mutation_outcome\":\"NO_MUTATION\",\"rc\":0,\"remote_rc\":0,\"remote_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"remote_stdout_sha256\":\"8d5d2dc41f486bd44dd9a197077743424079400c15e5280f3fe1f1a52eaea696\",\"report_sha256\":\"1f9cb1ebe693377e674b396b231e2376f2a037403d854d93281d82a5e579a8b3\",\"rollback_restored\":false,\"rollback_started\":false,\"schema\":1,\"source_count\":3,\"status\":\"OK\"}\nUSDC_EXACT_SCOPE_REPORT_BEGIN\n{\"baseline_comparison\":{\"app.py\":{\"current\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\",\"matches\":true,\"previous\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\"},\"compose\":{\"current\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"matches\":true,\"previous\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\"},\"worker.py\":{\"current\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"matches\":true,\"previous\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\"}},\"compose\":{\"environment_key_names\":[\"CRYPTO_DATA_DIR\",\"CRYPTO_EMAIL_CONFIG_FILE\",\"CRYPTO_PASSWORD_HASH_FILE\",\"CRYPTO_SESSION_SECRET_FILE\"],\"exists\":true,\"path\":\"compose.yaml\",\"redactions\":0,\"runtime\":{\"available\":true,\"config_services_rc\":0,\"containers\":[{\"env_names\":[\"CRYPTO_DATA_DIR\",\"CRYPTO_EMAIL_CONFIG_FILE\",\"CRYPTO_PASSWORD_HASH_FILE\",\"CRYPTO_SESSION_SECRET_FILE\",\"GPG_KEY\",\"LANG\",\"PATH\",\"PYTHONDONTWRITEBYTECODE\",\"PYTHONUNBUFFERED\",\"PYTHON_SHA256\",\"PYTHON_VERSION\",\"TZ\"],\"health\":null,\"id\":\"dc202c12ba81\",\"image\":\"crypto-portfolio-web\",\"image_id\":\"sha256:2a9339d475b2f4a01f060162f48a23b9590eb3ba4b036b46aad72adad540c40d\",\"mounts\":[{\"destination\":\"/data\",\"rw\":true,\"source\":\"/opt/stacks/crypto-portfolio/data\",\"type\":\"bind\"},{\"destination\":\"/run/secrets/session_secret\",\"rw\":false,\"source\":\"/opt/stacks/crypto-portfolio/secrets/session_secret\",\"type\":\"bind\"},{\"destination\":\"/run/secrets/email_config\",\"rw\":false,\"source\":\"/opt/stacks/crypto-portfolio/secrets/email_config\",\"type\":\"bind\"},{\"destination\":\"/run/secrets/password_hash\",\"rw\":false,\"source\":\"/opt/stacks/crypto-portfolio/secrets/password_hash\",\"type\":\"bind\"}],\"name\":\"crypto-portfolio\",\"restart_count\":0,\"running\":true,\"status\":\"running\"},{\"env_names\":[\"CRYPTO_DATA_DIR\",\"CRYPTO_EMAIL_CONFIG_FILE\",\"GPG_KEY\",\"LANG\",\"PATH\",\"PYTHONDONTWRITEBYTECODE\",\"PYTHONUNBUFFERED\",\"PYTHON_SHA256\",\"PYTHON_VERSION\",\"TZ\"],\"health\":null,\"id\":\"9a2ac999b94a\",\"image\":\"crypto-portfolio-worker\",\"image_id\":\"sha256:7ff5fdff7ab373e6b22dfa1a677b000d83ff9d021e4464915078e506781fd0ed\",\"mounts\":[{\"destination\":\"/data\",\"rw\":true,\"source\":\"/opt/stacks/crypto-portfolio/data\",\"type\":\"bind\"},{\"destination\":\"/run/secrets/email_config\",\"rw\":false,\"source\":\"/opt/stacks/crypto-portfolio/secrets/email_config\",\"type\":\"bind\"}],\"name\":\"crypto-portfolio-worker\",\"restart_count\":0,\"running\":true,\"status\":\"running\"}],\"deployed_source_hashes\":{\"web\":{\"files\":{\"/app/app.py\":{\"sha256\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\",\"size\":32843},\"/app/templates/index.html\":{\"sha256\":\"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2\",\"size\":38036},\"/app/worker.py\":{\"sha256\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"size\":10300}},\"rc\":0,\"stderr\":\"\",\"stdout_sha256\":\"bdf20f34c34ab85a74abc35edc7698d9e914c9b292eda221abf8e8ccb37b00f1\"},\"worker\":{\"files\":{\"/app/app.py\":{\"sha256\":\"ec226952cdb7fb58edbfc0173af47f4d901f3206e15169df12294983731fbc20\",\"size\":18076},\"/app/templates/index.html\":{\"sha256\":\"e9e723c9b916383ce10dffe8f8bbdb4049b78d89c5b139e2de5ed82194a27be4\",\"size\":36137},\"/app/worker.py\":{\"sha256\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"size\":10300}},\"rc\":0,\"stderr\":\"\",\"stdout_sha256\":\"333c26b471efe160d73c01a3d842334edfb59e2d7124f3a5c6142de9ca85f9bd\"}},\"ps_rc\":0,\"services\":[\"web\",\"worker\"]},\"sanitized_sha256\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"sha256\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"size\":1233},\"identity\":{\"euid\":0,\"hostname\":\"core-apps\",\"realpath\":\"/opt/stacks/crypto-portfolio\",\"root\":\"/opt/stacks/crypto-portfolio\"},\"previous_verified_baseline\":{\"app.py\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\",\"capsule_sha256\":\"ede6ac69de0b04481289b8138b3ec5a87a7dcef391a321f8624ad29d0f330956\",\"command_id\":\"GRAM1-SITE-CHARTS-V40-INTERACTIVE-ACCORDION-20260801T131500Z\",\"compose\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"worker.py\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\"},\"schema\":1,\"scope_assertion\":{\"exclude\":[\"sharp moves\",\"charts\",\"alerts\"],\"include\":[\"portfolio positions\",\"coin list\",\"portfolio current-value total\",\"portfolio invested total\"]},\"sources\":[{\"exists\":true,\"gid\":0,\"gzip_sha256\":\"e40905e690835200b2ebda03fc54e6c43aca829b08d26fe5982dc703be8c4d51\",\"hits\":{\"alert\":[{\"line\":21,\"snippet\":\"19:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n21:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n22:    c.commit()\\n23:    return c\"},{\"line\":351,\"snippet\":\"349:    with db() as c:\\n350:        holdings = {r[\\\"coin_id\\\"]: {\\\"quantity\\\":r[\\\"quantity\\\"],\\\"avg_buy\\\":r[\\\"avg_buy\\\"]} for r in c.execute(\\\"SELECT * FROM holdings\\\")}\\n351:        alerts = [dict(r) for r in c.execute(\\\"SELECT * FROM alerts ORDER BY id DESC\\\")]\\n352:        history = {}\\n353:        z = {r[\\\"name\\\"] for r in c.execute(\\\"PRAGMA table_info(market_history)\\\")}\"},{\"line\":361,\"snippet\":\"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\\n360:    # CRYPTO_HISTORY_STATE_V2\\n361:    return {\\\"coins\\\":COINS,\\\"market\\\":market_data(),\\\"holdings\\\":holdings,\\\"alerts\\\":alerts,\\\"history\\\":history}\\n362:\\n363:@app.post(\\\"/api/holding\\\")\"},{\"line\":376,\"snippet\":\"374:    return {\\\"ok\\\":True}\\n375:\\n376:@app.post(\\\"/api/alert\\\")\\n377:async def add_alert(request: Request):\\n378:    require_login(request)\"},{\"line\":377,\"snippet\":\"375:\\n376:@app.post(\\\"/api/alert\\\")\\n377:async def add_alert(request: Request):\\n378:    require_login(request)\\n379:    x = await request.json()\"},{\"line\":387,\"snippet\":\"385:        raise HTTPException(400,\\\"bad values\\\")\\n386:    with db() as c:\\n387:        cur = c.execute(\\\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\\\",(coin,kind,threshold,window,email)); c.commit()\\n388:    return {\\\"ok\\\":True,\\\"id\\\":cur.lastrowid}\\n389:\"},{\"line\":390,\"snippet\":\"388:    return {\\\"ok\\\":True,\\\"id\\\":cur.lastrowid}\\n389:\\n390:@app.delete(\\\"/api/alert/{alert_id}\\\")\\n391:def delete_alert(alert_id: int, request: Request):\\n392:    require_login(request)\"},{\"line\":391,\"snippet\":\"389:\\n390:@app.delete(\\\"/api/alert/{alert_id}\\\")\\n391:def delete_alert(alert_id: int, request: Request):\\n392:    require_login(request)\\n393:    with db() as c:\"},{\"line\":394,\"snippet\":\"392:    require_login(request)\\n393:    with db() as c:\\n394:        c.execute(\\\"DELETE FROM alerts WHERE id=?\\\",(alert_id,)); c.commit()\\n395:    return {\\\"ok\\\":True}\\n396:\"}],\"alerts\":[{\"line\":21,\"snippet\":\"19:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n21:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n22:    c.commit()\\n23:    return c\"},{\"line\":351,\"snippet\":\"349:    with db() as c:\\n350:        holdings = {r[\\\"coin_id\\\"]: {\\\"quantity\\\":r[\\\"quantity\\\"],\\\"avg_buy\\\":r[\\\"avg_buy\\\"]} for r in c.execute(\\\"SELECT * FROM holdings\\\")}\\n351:        alerts = [dict(r) for r in c.execute(\\\"SELECT * FROM alerts ORDER BY id DESC\\\")]\\n352:        history = {}\\n353:        z = {r[\\\"name\\\"] for r in c.execute(\\\"PRAGMA table_info(market_history)\\\")}\"},{\"line\":361,\"snippet\":\"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\\n360:    # CRYPTO_HISTORY_STATE_V2\\n361:    return {\\\"coins\\\":COINS,\\\"market\\\":market_data(),\\\"holdings\\\":holdings,\\\"alerts\\\":alerts,\\\"history\\\":history}\\n362:\\n363:@app.post(\\\"/api/holding\\\")\"},{\"line\":387,\"snippet\":\"385:        raise HTTPException(400,\\\"bad values\\\")\\n386:    with db() as c:\\n387:        cur = c.execute(\\\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\\\",(coin,kind,threshold,window,email)); c.commit()\\n388:    return {\\\"ok\\\":True,\\\"id\\\":cur.lastrowid}\\n389:\"},{\"line\":394,\"snippet\":\"392:    require_login(request)\\n393:    with db() as c:\\n394:        c.execute(\\\"DELETE FROM alerts WHERE id=?\\\",(alert_id,)); c.commit()\\n395:    return {\\\"ok\\\":True}\\n396:\"}],\"chart\":[{\"line\":138,\"snippet\":\"136:\\n137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\\n138:LIGHTWEIGHT_JS = DATA / \\\"lightweight-charts.standalone.production.js\\\"\\n139:KLINE_SYMBOLS = {\\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\", \\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\"}\\n140:\"},{\"line\":141,\"snippet\":\"139:KLINE_SYMBOLS = {\\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\", \\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\"}\\n140:\\n141:@app.get(\\\"/vendor/lightweight-charts.js\\\")\\n142:def lightweight_charts_js():\\n143:    if not LIGHTWEIGHT_JS.is_file():\"},{\"line\":142,\"snippet\":\"140:\\n141:@app.get(\\\"/vendor/lightweight-charts.js\\\")\\n142:def lightweight_charts_js():\\n143:    if not LIGHTWEIGHT_JS.is_file():\\n144:        raise HTTPException(404, \\\"chart library missing\\\")\"},{\"line\":144,\"snippet\":\"142:def lightweight_charts_js():\\n143:    if not LIGHTWEIGHT_JS.is_file():\\n144:        raise HTTPException(404, \\\"chart library missing\\\")\\n145:    return Response(LIGHTWEIGHT_JS.read_bytes(), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\": \\\"public, max-age=31536000, immutable\\\"})\\n146:\"},{\"line\":312,\"snippet\":\"310:    return fetch_klines(symbol, limit, interval)\\n311:\\n312:@app.get(\\\"/api/chart-health\\\")\\n313:def chart_health():\\n314:    now = time.time()\"},{\"line\":313,\"snippet\":\"311:\\n312:@app.get(\\\"/api/chart-health\\\")\\n313:def chart_health():\\n314:    now = time.time()\\n315:    details = {}\"},{\"line\":397,\"snippet\":\"395:    return {\\\"ok\\\":True}\\n396:\\n397:# GRAM1_SERVER_CHART_FALLBACK_V4\\n398:import asyncio as _gram1_asyncio\\n399:import base64 as _gram1_base64\"},{\"line\":406,\"snippet\":\"404:from fastapi.responses import Response as _Gram1Response\\n405:\\n406:_GRAM1_CHART_JS_B64 = \\\"[encoded data, 4907 characters]\"},{\"line\":407,\"snippet\":\"405:\\n406:_GRAM1_CHART_JS_B64 = \\\"[encoded data, 4968 characters]\"},{\"line\":409,\"snippet\":\"407:_GRAM1_CHART_SYMBOLS = {\\\"HBAR\\\", \\\"POL\\\", \\\"AVAX\\\", \\\"LINK\\\"}\\n408:\\n409:@app.get(\\\"/vendor/gram1-charts-v4.js\\\", include_in_schema=False)\\n410:def _gram1_chart_javascript_v4():\\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\"},{\"line\":410,\"snippet\":\"408:\\n409:@app.get(\\\"/vendor/gram1-charts-v4.js\\\", include_in_schema=False)\\n410:def _gram1_chart_javascript_v4():\\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\\n412:\"},{\"line\":411,\"snippet\":\"409:@app.get(\\\"/vendor/gram1-charts-v4.js\\\", include_in_schema=False)\\n410:def _gram1_chart_javascript_v4():\\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\\n412:\\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\"},{\"line\":413,\"snippet\":\"411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\\n412:\\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\\n414:    fn = globals().get(\\\"klines\\\")\\n415:    if fn is None:\"},{\"line\":452,\"snippet\":\"450:    return rows[-180:]\\n451:\\n452:def _gram1_chart_svg_v4(symbol: str, rows):\\n453:    width,height,left,right,top,bottom=1200,430,72,28,24,42\\n454:    lo=min(r[3] for r in rows);hi=max(r[2] for r in rows);span=max(hi-lo,abs(hi)*1e-9,1e-12)\"},{\"line\":457,\"snippet\":\"455:    plot_w=width-left-right;plot_h=height-top-bottom;step=plot_w/max(len(rows),1);body=max(2.0,min(10.0,step*0.58))\\n456:    def y(v): return top+(hi-v)/span*plot_h\\n457:    parts=[f'<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 {width} {height}\\\" role=\\\"img\\\" aria-label=\\\"{_gram1_html.escape(symbol)} candle chart\\\" data-symbol=\\\"{_gram1_html.escape(symbol)}\\\" data-candles=\\\"{len(rows)}\\\">', '<rect width=\\\"100%\\\" height=\\\"100%\\\" fill=\\\"#081823\\\"/>']\\n458:    for n in range(5):\\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\\\"{left}\\\" y1=\\\"{yy:.2f}\\\" x2=\\\"{width-right}\\\" y2=\\\"{yy:.2f}\\\" stroke=\\\"rgba(143,165,182,.12)\\\"/>');parts.append(f'<text x=\\\"6\\\" y=\\\"{yy+4:.2f}\\\" fill=\\\"#8fa5b6\\\" font-size=\\\"12\\\">{price:.8g}</text>')\"},{\"line\":466,\"snippet\":\"464:    return ''.join(parts)\\n465:\\n466:@app.get(\\\"/api/chart-svg/{symbol}\\\", include_in_schema=False)\\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\\n468:    if not request.session.get(\\\"auth\\\"):\"},{\"line\":467,\"snippet\":\"465:\\n466:@app.get(\\\"/api/chart-svg/{symbol}\\\", include_in_schema=False)\\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\\n468:    if not request.session.get(\\\"auth\\\"):\\n469:        return _Gram1Response(content=\\\"Требуется вход\\\", status_code=401, media_type=\\\"text/plain; charset=utf-8\\\")\"},{\"line\":471,\"snippet\":\"469:        return _Gram1Response(content=\\\"Требуется вход\\\", status_code=401, media_type=\\\"text/plain; charset=utf-8\\\")\\n470:    symbol=symbol.upper()\\n471:    if symbol not in _GRAM1_CHART_SYMBOLS:\\n472:        return _Gram1Response(content=\\\"Неизвестная монета\\\", status_code=404, media_type=\\\"text/plain; charset=utf-8\\\")\\n473:    try:\"},{\"line\":474,\"snippet\":\"472:        return _Gram1Response(content=\\\"Неизвестная монета\\\", status_code=404, media_type=\\\"text/plain; charset=utf-8\\\")\\n473:    try:\\n474:        rows=await _gram1_chart_data_v4(symbol,request)\\n475:        svg=_gram1_chart_svg_v4(symbol,rows)\\n476:        return _Gram1Response(content=svg,media_type=\\\"image/svg+xml\\\",headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\"},{\"line\":475,\"snippet\":\"473:    try:\\n474:        rows=await _gram1_chart_data_v4(symbol,request)\\n475:        svg=_gram1_chart_svg_v4(symbol,rows)\\n476:        return _Gram1Response(content=svg,media_type=\\\"image/svg+xml\\\",headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\\n477:    except Exception as exc:\"}],\"charts\":[{\"line\":138,\"snippet\":\"136:\\n137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\\n138:LIGHTWEIGHT_JS = DATA / \\\"lightweight-charts.standalone.production.js\\\"\\n139:KLINE_SYMBOLS = {\\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\", \\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\"}\\n140:\"},{\"line\":141,\"snippet\":\"139:KLINE_SYMBOLS = {\\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\", \\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\"}\\n140:\\n141:@app.get(\\\"/vendor/lightweight-charts.js\\\")\\n142:def lightweight_charts_js():\\n143:    if not LIGHTWEIGHT_JS.is_file():\"},{\"line\":142,\"snippet\":\"140:\\n141:@app.get(\\\"/vendor/lightweight-charts.js\\\")\\n142:def lightweight_charts_js():\\n143:    if not LIGHTWEIGHT_JS.is_file():\\n144:        raise HTTPException(404, \\\"chart library missing\\\")\"},{\"line\":409,\"snippet\":\"407:_GRAM1_CHART_SYMBOLS = {\\\"HBAR\\\", \\\"POL\\\", \\\"AVAX\\\", \\\"LINK\\\"}\\n408:\\n409:@app.get(\\\"/vendor/gram1-charts-v4.js\\\", include_in_schema=False)\\n410:def _gram1_chart_javascript_v4():\\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\"}],\"coin\":[{\"line\":13,\"snippet\":\"11:HASH_FILE = Path(os.getenv(\\\"CRYPTO_PASSWORD_HASH_FILE\\\", \\\"/run/secrets/password_hash\\\"))\\n12:SECRET_FILE = Path(os.getenv(\\\"CRYPTO_SESSION_SECRET_FILE\\\", \\\"/run/secrets/session_secret\\\"))\\n13:COINS = {\\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\", \\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\"}\\n14:FAILED = {}\\n15:\"},{\"line\":20,\"snippet\":\"18:    c.row_factory = sqlite3.Row\\n19:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n21:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n22:    c.commit()\"},{\"line\":21,\"snippet\":\"19:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n21:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n22:    c.commit()\\n23:    return c\"},{\"line\":91,\"snippet\":\"89:    return {\\\"status\\\":\\\"OK\\\",\\\"type\\\":\\\"crypto-portfolio\\\",\\\"market_age_seconds\\\":age}\\n90:\\n91:# COIN_ICON_ROUTE_V4\\n92:@app.get(\\\"/coin-icon/{coin_id}\\\")\\n93:def coin_icon(coin_id: str):\"},{\"line\":92,\"snippet\":\"90:\\n91:# COIN_ICON_ROUTE_V4\\n92:@app.get(\\\"/coin-icon/{coin_id}\\\")\\n93:def coin_icon(coin_id: str):\\n94:    import html\"},{\"line\":93,\"snippet\":\"91:# COIN_ICON_ROUTE_V4\\n92:@app.get(\\\"/coin-icon/{coin_id}\\\")\\n93:def coin_icon(coin_id: str):\\n94:    import html\\n95:    import os\"},{\"line\":101,\"snippet\":\"99:    from fastapi import HTTPException\\n100:    from fastapi.responses import Response\\n101:    if not re.fullmatch(r\\\"[a-z0-9-]{1,64}\\\", coin_id):\\n102:        raise HTTPException(status_code=404)\\n103:    cache_dir = Path(\\\"/data/coin-icons\\\")\"},{\"line\":103,\"snippet\":\"101:    if not re.fullmatch(r\\\"[a-z0-9-]{1,64}\\\", coin_id):\\n102:        raise HTTPException(status_code=404)\\n103:    cache_dir = Path(\\\"/data/coin-icons\\\")\\n104:    cache_dir.mkdir(parents=True, exist_ok=True)\\n105:    body_path = cache_dir / (coin_id + \\\".img\\\")\"},{\"line\":105,\"snippet\":\"103:    cache_dir = Path(\\\"/data/coin-icons\\\")\\n104:    cache_dir.mkdir(parents=True, exist_ok=True)\\n105:    body_path = cache_dir / (coin_id + \\\".img\\\")\\n106:    type_path = cache_dir / (coin_id + \\\".type\\\")\\n107:    if body_path.exists() and body_path.stat().st_size > 128:\"},{\"line\":106,\"snippet\":\"104:    cache_dir.mkdir(parents=True, exist_ok=True)\\n105:    body_path = cache_dir / (coin_id + \\\".img\\\")\\n106:    type_path = cache_dir / (coin_id + \\\".type\\\")\\n107:    if body_path.exists() and body_path.stat().st_size > 128:\\n108:        media_type = type_path.read_text().strip() if type_path.exists() else \\\"image/png\\\"\"},{\"line\":112,\"snippet\":\"110:    try:\\n111:        with httpx.Client(timeout=6, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n112:            response = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params={\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":coin_id,\\\"sparkline\\\":\\\"false\\\"})\\n113:            response.raise_for_status()\\n114:            rows = response.json()\"},{\"line\":128,\"snippet\":\"126:        return Response(image.content, media_type=media_type, headers={\\\"Cache-Control\\\":\\\"public, max-age=86400\\\"})\\n127:    except Exception:\\n128:        symbols = {\\\"bitcoin\\\":\\\"₿\\\",\\\"ethereum\\\":\\\"Ξ\\\",\\\"solana\\\":\\\"S\\\",\\\"binancecoin\\\":\\\"BNB\\\",\\\"ripple\\\":\\\"XRP\\\",\\\"cardano\\\":\\\"ADA\\\",\\\"hedera-hashgraph\\\":\\\"HBAR\\\",\\\"hedera\\\":\\\"HBAR\\\",\\\"avalanche-2\\\":\\\"AVAX\\\",\\\"chainlink\\\":\\\"LINK\\\",\\\"toncoin\\\":\\\"TON\\\",\\\"dogecoin\\\":\\\"Ð\\\"}\\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\\n130:        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\"},{\"line\":129,\"snippet\":\"127:    except Exception:\\n128:        symbols = {\\\"bitcoin\\\":\\\"₿\\\",\\\"ethereum\\\":\\\"Ξ\\\",\\\"solana\\\":\\\"S\\\",\\\"binancecoin\\\":\\\"BNB\\\",\\\"ripple\\\":\\\"XRP\\\",\\\"cardano\\\":\\\"ADA\\\",\\\"hedera-hashgraph\\\":\\\"HBAR\\\",\\\"hedera\\\":\\\"HBAR\\\",\\\"avalanche-2\\\":\\\"AVAX\\\",\\\"chainlink\\\":\\\"LINK\\\",\\\"toncoin\\\":\\\"TON\\\",\\\"dogecoin\\\":\\\"Ð\\\"}\\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\\n130:        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\"},{\"line\":131,\"snippet\":\"129:        label = symbols.get(coin_id, coin_id[:3].upper())\\n130:        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\\n132:        safe_label = html.escape(label)\\n133:        svg = f'<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"64\\\" height=\\\"64\\\" viewBox=\\\"0 0 64 64\\\"><circle cx=\\\"32\\\" cy=\\\"32\\\" r=\\\"31\\\" fill=\\\"{color}\\\"/><text x=\\\"32\\\" y=\\\"38\\\" text-anchor=\\\"middle\\\" font-family=\\\"Arial,sans-serif\\\" font-size=\\\"18\\\" font-weight=\\\"700\\\" fill=\\\"#fff\\\">{safe_label}</text></svg>'\"},{\"line\":350,\"snippet\":\"348:    require_login(request)\\n349:    with db() as c:\\n350:        holdings = {r[\\\"coin_id\\\"]: {\\\"quantity\\\":r[\\\"quantity\\\"],\\\"avg_buy\\\":r[\\\"avg_buy\\\"]} for r in c.execute(\\\"SELECT * FROM holdings\\\")}\\n351:        alerts = [dict(r) for r in c.execute(\\\"SELECT * FROM alerts ORDER BY id DESC\\\")]\\n352:        history = {}\"},{\"line\":354,\"snippet\":\"352:        history = {}\\n353:        z = {r[\\\"name\\\"] for r in c.execute(\\\"PRAGMA table_info(market_history)\\\")}\\n354:        cc = next((x for x in (\\\"coin_id\\\",\\\"coin\\\",\\\"asset_id\\\") if x in z),None); pc = next((x for x in (\\\"price\\\",\\\"current_price\\\",\\\"usd_price\\\",\\\"value\\\") if x in z),None); tc = next((x for x in (\\\"ts\\\",\\\"timestamp\\\",\\\"time\\\",\\\"created_at\\\",\\\"updated_at\\\") if x in z),None)\\n355:        if cc and pc and tc:\\n356:            q = f'SELECT \\\"{pc}\\\" p FROM market_history WHERE \\\"{cc}\\\"=? ORDER BY \\\"{tc}\\\"'\"},{\"line\":357,\"snippet\":\"355:        if cc and pc and tc:\\n356:            q = f'SELECT \\\"{pc}\\\" p FROM market_history WHERE \\\"{cc}\\\"=? ORDER BY \\\"{tc}\\\"'\\n357:            for cid in COINS:\\n358:                v=[float(r[\\\"p\\\"]) for r in c.execute(q,(cid,)) if r[\\\"p\\\"] is not None]\\n359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\"},{\"line\":361,\"snippet\":\"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\\n360:    # CRYPTO_HISTORY_STATE_V2\\n361:    return {\\\"coins\\\":COINS,\\\"market\\\":market_data(),\\\"holdings\\\":holdings,\\\"alerts\\\":alerts,\\\"history\\\":history}\\n362:\\n363:@app.post(\\\"/api/holding\\\")\"},{\"line\":367,\"snippet\":\"365:    require_login(request)\\n366:    x = await request.json()\\n367:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\"))\\n368:    if coin not in COINS:\\n369:        raise HTTPException(400,\\\"bad coin\\\")\"},{\"line\":368,\"snippet\":\"366:    x = await request.json()\\n367:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\"))\\n368:    if coin not in COINS:\\n369:        raise HTTPException(400,\\\"bad coin\\\")\\n370:    quantity = max(0.0, float(x.get(\\\"quantity\\\",0)))\"},{\"line\":369,\"snippet\":\"367:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\"))\\n368:    if coin not in COINS:\\n369:        raise HTTPException(400,\\\"bad coin\\\")\\n370:    quantity = max(0.0, float(x.get(\\\"quantity\\\",0)))\\n371:    avg_buy = max(0.0, float(x.get(\\\"avg_buy\\\",0)))\"},{\"line\":373,\"snippet\":\"371:    avg_buy = max(0.0, float(x.get(\\\"avg_buy\\\",0)))\\n372:    with db() as c:\\n373:        c.execute(\\\"INSERT INTO holdings(coin_id,quantity,avg_buy) VALUES(?,?,?) ON CONFLICT(coin_id) DO UPDATE SET quantity=excluded.quantity,avg_buy=excluded.avg_buy\\\",(coin,quantity,avg_buy)); c.commit()\\n374:    return {\\\"ok\\\":True}\\n375:\"},{\"line\":380,\"snippet\":\"378:    require_login(request)\\n379:    x = await request.json()\\n380:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\")); kind = str(x.get(\\\"kind\\\",\\\"\\\"))\\n381:    if coin not in COINS or kind not in {\\\"price_above\\\",\\\"price_below\\\",\\\"cap_above\\\",\\\"cap_below\\\",\\\"pump\\\",\\\"dump\\\"}:\\n382:        raise HTTPException(400,\\\"bad rule\\\")\"},{\"line\":381,\"snippet\":\"379:    x = await request.json()\\n380:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\")); kind = str(x.get(\\\"kind\\\",\\\"\\\"))\\n381:    if coin not in COINS or kind not in {\\\"price_above\\\",\\\"price_below\\\",\\\"cap_above\\\",\\\"cap_below\\\",\\\"pump\\\",\\\"dump\\\"}:\\n382:        raise HTTPException(400,\\\"bad rule\\\")\\n383:    threshold = float(x.get(\\\"threshold\\\",0)); window = int(x.get(\\\"window_seconds\\\",0)); email = str(x.get(\\\"email\\\",\\\"\\\")).strip()\"},{\"line\":387,\"snippet\":\"385:        raise HTTPException(400,\\\"bad values\\\")\\n386:    with db() as c:\\n387:        cur = c.execute(\\\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\\\",(coin,kind,threshold,window,email)); c.commit()\\n388:    return {\\\"ok\\\":True,\\\"id\\\":cur.lastrowid}\\n389:\"},{\"line\":420,\"snippet\":\"418:    for name, parameter in _gram1_inspect.signature(fn).parameters.items():\\n419:        lname = name.lower()\\n420:        if lname in {\\\"symbol\\\", \\\"coin\\\", \\\"ticker\\\"}: kwargs[name] = symbol\\n421:        elif lname == \\\"request\\\": kwargs[name] = request\\n422:        elif lname == \\\"limit\\\": kwargs[name] = 1000\"}],\"coins\":[{\"line\":13,\"snippet\":\"11:HASH_FILE = Path(os.getenv(\\\"CRYPTO_PASSWORD_HASH_FILE\\\", \\\"/run/secrets/password_hash\\\"))\\n12:SECRET_FILE = Path(os.getenv(\\\"CRYPTO_SESSION_SECRET_FILE\\\", \\\"/run/secrets/session_secret\\\"))\\n13:COINS = {\\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\", \\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\"}\\n14:FAILED = {}\\n15:\"},{\"line\":112,\"snippet\":\"110:    try:\\n111:        with httpx.Client(timeout=6, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n112:            response = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params={\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":coin_id,\\\"sparkline\\\":\\\"false\\\"})\\n113:            response.raise_for_status()\\n114:            rows = response.json()\"},{\"line\":357,\"snippet\":\"355:        if cc and pc and tc:\\n356:            q = f'SELECT \\\"{pc}\\\" p FROM market_history WHERE \\\"{cc}\\\"=? ORDER BY \\\"{tc}\\\"'\\n357:            for cid in COINS:\\n358:                v=[float(r[\\\"p\\\"]) for r in c.execute(q,(cid,)) if r[\\\"p\\\"] is not None]\\n359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\"},{\"line\":361,\"snippet\":\"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\\n360:    # CRYPTO_HISTORY_STATE_V2\\n361:    return {\\\"coins\\\":COINS,\\\"market\\\":market_data(),\\\"holdings\\\":holdings,\\\"alerts\\\":alerts,\\\"history\\\":history}\\n362:\\n363:@app.post(\\\"/api/holding\\\")\"},{\"line\":368,\"snippet\":\"366:    x = await request.json()\\n367:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\"))\\n368:    if coin not in COINS:\\n369:        raise HTTPException(400,\\\"bad coin\\\")\\n370:    quantity = max(0.0, float(x.get(\\\"quantity\\\",0)))\"},{\"line\":381,\"snippet\":\"379:    x = await request.json()\\n380:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\")); kind = str(x.get(\\\"kind\\\",\\\"\\\"))\\n381:    if coin not in COINS or kind not in {\\\"price_above\\\",\\\"price_below\\\",\\\"cap_above\\\",\\\"cap_below\\\",\\\"pump\\\",\\\"dump\\\"}:\\n382:        raise HTTPException(400,\\\"bad rule\\\")\\n383:    threshold = float(x.get(\\\"threshold\\\",0)); window = int(x.get(\\\"window_seconds\\\",0)); email = str(x.get(\\\"email\\\",\\\"\\\")).strip()\"}],\"portfolio\":[{\"line\":9,\"snippet\":\"7:\\n8:DATA = Path(os.getenv(\\\"CRYPTO_DATA_DIR\\\", \\\"/data\\\"))\\n9:DB = DATA / \\\"portfolio.db\\\"\\n10:MARKET = DATA / \\\"market.json\\\"\\n11:HASH_FILE = Path(os.getenv(\\\"CRYPTO_PASSWORD_HASH_FILE\\\", \\\"/run/secrets/password_hash\\\"))\"},{\"line\":89,\"snippet\":\"87:    if MARKET.exists():\\n88:        age = int(time.time() - MARKET.stat().st_mtime)\\n89:    return {\\\"status\\\":\\\"OK\\\",\\\"type\\\":\\\"crypto-portfolio\\\",\\\"market_age_seconds\\\":age}\\n90:\\n91:# COIN_ICON_ROUTE_V4\"},{\"line\":111,\"snippet\":\"109:        return Response(body_path.read_bytes(), media_type=media_type, headers={\\\"Cache-Control\\\":\\\"public, max-age=86400\\\"})\\n110:    try:\\n111:        with httpx.Client(timeout=6, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n112:            response = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params={\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":coin_id,\\\"sparkline\\\":\\\"false\\\"})\\n113:            response.raise_for_status()\"},{\"line\":238,\"snippet\":\"236:            request = urllib.request.Request(\\n237:                \\\"https://data-api.binance.vision/api/v3/klines?\\\" + query,\\n238:                headers={\\\"User-Agent\\\": \\\"crypto-portfolio/1.0\\\"},\\n239:            )\\n240:            rows = None\"}],\"price\":[{\"line\":354,\"snippet\":\"352:        history = {}\\n353:        z = {r[\\\"name\\\"] for r in c.execute(\\\"PRAGMA table_info(market_history)\\\")}\\n354:        cc = next((x for x in (\\\"coin_id\\\",\\\"coin\\\",\\\"asset_id\\\") if x in z),None); pc = next((x for x in (\\\"price\\\",\\\"current_price\\\",\\\"usd_price\\\",\\\"value\\\") if x in z),None); tc = next((x for x in (\\\"ts\\\",\\\"timestamp\\\",\\\"time\\\",\\\"created_at\\\",\\\"updated_at\\\") if x in z),None)\\n355:        if cc and pc and tc:\\n356:            q = f'SELECT \\\"{pc}\\\" p FROM market_history WHERE \\\"{cc}\\\"=? ORDER BY \\\"{tc}\\\"'\"},{\"line\":381,\"snippet\":\"379:    x = await request.json()\\n380:    coin = str(x.get(\\\"coin_id\\\",\\\"\\\")); kind = str(x.get(\\\"kind\\\",\\\"\\\"))\\n381:    if coin not in COINS or kind not in {\\\"price_above\\\",\\\"price_below\\\",\\\"cap_above\\\",\\\"cap_below\\\",\\\"pump\\\",\\\"dump\\\"}:\\n382:        raise HTTPException(400,\\\"bad rule\\\")\\n383:    threshold = float(x.get(\\\"threshold\\\",0)); window = int(x.get(\\\"window_seconds\\\",0)); email = str(x.get(\\\"email\\\",\\\"\\\")).strip()\"},{\"line\":459,\"snippet\":\"457:    parts=[f'<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 {width} {height}\\\" role=\\\"img\\\" aria-label=\\\"{_gram1_html.escape(symbol)} candle chart\\\" data-symbol=\\\"{_gram1_html.escape(symbol)}\\\" data-candles=\\\"{len(rows)}\\\">', '<rect width=\\\"100%\\\" height=\\\"100%\\\" fill=\\\"#081823\\\"/>']\\n458:    for n in range(5):\\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\\\"{left}\\\" y1=\\\"{yy:.2f}\\\" x2=\\\"{width-right}\\\" y2=\\\"{yy:.2f}\\\" stroke=\\\"rgba(143,165,182,.12)\\\"/>');parts.append(f'<text x=\\\"6\\\" y=\\\"{yy+4:.2f}\\\" fill=\\\"#8fa5b6\\\" font-size=\\\"12\\\">{price:.8g}</text>')\\n460:    for i,(_,o,h,l,c) in enumerate(rows):\\n461:        x=left+step*(i+.5);color=\\\"#63e6be\\\" if c>=o else \\\"#ff6b81\\\";yo=y(o);yc=y(c);yh=y(h);yl=y(l);rh=max(1.2,abs(yc-yo));ry=min(yo,yc)\"}],\"quantity\":[{\"line\":20,\"snippet\":\"18:    c.row_factory = sqlite3.Row\\n19:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n21:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n22:    c.commit()\"},{\"line\":350,\"snippet\":\"348:    require_login(request)\\n349:    with db() as c:\\n350:        holdings = {r[\\\"coin_id\\\"]: {\\\"quantity\\\":r[\\\"quantity\\\"],\\\"avg_buy\\\":r[\\\"avg_buy\\\"]} for r in c.execute(\\\"SELECT * FROM holdings\\\")}\\n351:        alerts = [dict(r) for r in c.execute(\\\"SELECT * FROM alerts ORDER BY id DESC\\\")]\\n352:        history = {}\"},{\"line\":370,\"snippet\":\"368:    if coin not in COINS:\\n369:        raise HTTPException(400,\\\"bad coin\\\")\\n370:    quantity = max(0.0, float(x.get(\\\"quantity\\\",0)))\\n371:    avg_buy = max(0.0, float(x.get(\\\"avg_buy\\\",0)))\\n372:    with db() as c:\"},{\"line\":373,\"snippet\":\"371:    avg_buy = max(0.0, float(x.get(\\\"avg_buy\\\",0)))\\n372:    with db() as c:\\n373:        c.execute(\\\"INSERT INTO holdings(coin_id,quantity,avg_buy) VALUES(?,?,?) ON CONFLICT(coin_id) DO UPDATE SET quantity=excluded.quantity,avg_buy=excluded.avg_buy\\\",(coin,quantity,avg_buy)); c.commit()\\n374:    return {\\\"ok\\\":True}\\n375:\"}],\"symbol\":[{\"line\":128,\"snippet\":\"126:        return Response(image.content, media_type=media_type, headers={\\\"Cache-Control\\\":\\\"public, max-age=86400\\\"})\\n127:    except Exception:\\n128:        symbols = {\\\"bitcoin\\\":\\\"₿\\\",\\\"ethereum\\\":\\\"Ξ\\\",\\\"solana\\\":\\\"S\\\",\\\"binancecoin\\\":\\\"BNB\\\",\\\"ripple\\\":\\\"XRP\\\",\\\"cardano\\\":\\\"ADA\\\",\\\"hedera-hashgraph\\\":\\\"HBAR\\\",\\\"hedera\\\":\\\"HBAR\\\",\\\"avalanche-2\\\":\\\"AVAX\\\",\\\"chainlink\\\":\\\"LINK\\\",\\\"toncoin\\\":\\\"TON\\\",\\\"dogecoin\\\":\\\"Ð\\\"}\\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\\n130:        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\"},{\"line\":129,\"snippet\":\"127:    except Exception:\\n128:        symbols = {\\\"bitcoin\\\":\\\"₿\\\",\\\"ethereum\\\":\\\"Ξ\\\",\\\"solana\\\":\\\"S\\\",\\\"binancecoin\\\":\\\"BNB\\\",\\\"ripple\\\":\\\"XRP\\\",\\\"cardano\\\":\\\"ADA\\\",\\\"hedera-hashgraph\\\":\\\"HBAR\\\",\\\"hedera\\\":\\\"HBAR\\\",\\\"avalanche-2\\\":\\\"AVAX\\\",\\\"chainlink\\\":\\\"LINK\\\",\\\"toncoin\\\":\\\"TON\\\",\\\"dogecoin\\\":\\\"Ð\\\"}\\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\\n130:        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\"},{\"line\":139,\"snippet\":\"137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\\n138:LIGHTWEIGHT_JS = DATA / \\\"lightweight-charts.standalone.production.js\\\"\\n139:KLINE_SYMBOLS = {\\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\", \\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\"}\\n140:\\n141:@app.get(\\\"/vendor/lightweight-charts.js\\\")\"},{\"line\":171,\"snippet\":\"169:            parts = name.split(\\\":\\\")\\n170:            if len(parts) == 3:\\n171:                symbol, interval, limit_text = parts\\n172:            elif len(parts) == 2:\\n173:                symbol, limit_text = parts\"},{\"line\":173,\"snippet\":\"171:                symbol, interval, limit_text = parts\\n172:            elif len(parts) == 2:\\n173:                symbol, limit_text = parts\\n174:                interval = \\\"1d\\\"\\n175:            else:\"},{\"line\":181,\"snippet\":\"179:            result = item.get(\\\"result\\\")\\n180:            if now - saved_at <= KLINE_CACHE_MAX_STALE and _valid_kline_result(result):\\n181:                KLINE_CACHE[(symbol, interval, limit)] = (saved_at, result)\\n182:    except Exception:\\n183:        return\"},{\"line\":190,\"snippet\":\"188:        for key, value in list(KLINE_CACHE.items()):\\n189:            if len(key) == 3:\\n190:                symbol, interval, limit = key\\n191:            else:\\n192:                symbol, limit = key\"},{\"line\":192,\"snippet\":\"190:                symbol, interval, limit = key\\n191:            else:\\n192:                symbol, limit = key\\n193:                interval = \\\"1d\\\"\\n194:            saved_at, result = value\"},{\"line\":196,\"snippet\":\"194:            saved_at, result = value\\n195:            if _valid_kline_result(result):\\n196:                entries[symbol + \\\":\\\" + interval + \\\":\\\" + str(limit)] = {\\\"saved_at\\\": saved_at, \\\"result\\\": result}\\n197:        payload = {\\\"version\\\": 1, \\\"saved_at\\\": int(time.time()), \\\"entries\\\": entries}\\n198:        temporary = KLINE_CACHE_FILE.with_suffix(\\\".json.tmp\\\")\"},{\"line\":205,\"snippet\":\"203:_load_kline_cache()\\n204:\\n205:def fetch_klines(symbol, limit=365, interval=\\\"1d\\\"):\\n206:    key = str(symbol).upper()\\n207:    pair = KLINE_SYMBOLS.get(key)\"},{\"line\":206,\"snippet\":\"204:\\n205:def fetch_klines(symbol, limit=365, interval=\\\"1d\\\"):\\n206:    key = str(symbol).upper()\\n207:    pair = KLINE_SYMBOLS.get(key)\\n208:    if not pair:\"},{\"line\":207,\"snippet\":\"205:def fetch_klines(symbol, limit=365, interval=\\\"1d\\\"):\\n206:    key = str(symbol).upper()\\n207:    pair = KLINE_SYMBOLS.get(key)\\n208:    if not pair:\\n209:        raise HTTPException(404, \\\"unknown symbol\\\")\"},{\"line\":209,\"snippet\":\"207:    pair = KLINE_SYMBOLS.get(key)\\n208:    if not pair:\\n209:        raise HTTPException(404, \\\"unknown symbol\\\")\\n210:    interval = str(interval or \\\"1d\\\")\\n211:    if interval not in {\\\"1d\\\", \\\"1M\\\"}:\"},{\"line\":232,\"snippet\":\"230:        while remaining > 0:\\n231:            page_limit = min(1000 if interval == \\\"1M\\\" else 100, remaining)\\n232:            params = {\\\"symbol\\\": pair, \\\"interval\\\": interval, \\\"limit\\\": page_limit}\\n233:            if end_time is not None:\\n234:                params[\\\"endTime\\\"] = end_time\"},{\"line\":295,\"snippet\":\"293:    result = {\\n294:        \\\"status\\\": \\\"OK\\\",\\n295:        \\\"symbol\\\": key,\\n296:        \\\"pair\\\": pair,\\n297:        \\\"candles\\\": candles,\"},{\"line\":307,\"snippet\":\"305:    return dict(result)\\n306:\\n307:@app.get(\\\"/api/klines/{symbol}\\\")\\n308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \\\"1d\\\"):\\n309:    require_login(request)\"},{\"line\":308,\"snippet\":\"306:\\n307:@app.get(\\\"/api/klines/{symbol}\\\")\\n308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \\\"1d\\\"):\\n309:    require_login(request)\\n310:    return fetch_klines(symbol, limit, interval)\"},{\"line\":310,\"snippet\":\"308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \\\"1d\\\"):\\n309:    require_login(request)\\n310:    return fetch_klines(symbol, limit, interval)\\n311:\\n312:@app.get(\\\"/api/chart-health\\\")\"},{\"line\":317,\"snippet\":\"315:    details = {}\\n316:    bad = 0\\n317:    for symbol in (\\\"HBAR\\\", \\\"POL\\\", \\\"AVAX\\\", \\\"LINK\\\"):\\n318:        entry = KLINE_CACHE.get((symbol, \\\"1M\\\", 1000)) or KLINE_CACHE.get((symbol, \\\"1d\\\", 365))\\n319:        if not entry or not _valid_kline_result(entry[1]):\"},{\"line\":318,\"snippet\":\"316:    bad = 0\\n317:    for symbol in (\\\"HBAR\\\", \\\"POL\\\", \\\"AVAX\\\", \\\"LINK\\\"):\\n318:        entry = KLINE_CACHE.get((symbol, \\\"1M\\\", 1000)) or KLINE_CACHE.get((symbol, \\\"1d\\\", 365))\\n319:        if not entry or not _valid_kline_result(entry[1]):\\n320:            details[symbol] = {\\\"status\\\": \\\"ERROR\\\", \\\"candles\\\": 0, \\\"cache_age_seconds\\\": None}\"},{\"line\":320,\"snippet\":\"318:        entry = KLINE_CACHE.get((symbol, \\\"1M\\\", 1000)) or KLINE_CACHE.get((symbol, \\\"1d\\\", 365))\\n319:        if not entry or not _valid_kline_result(entry[1]):\\n320:            details[symbol] = {\\\"status\\\": \\\"ERROR\\\", \\\"candles\\\": 0, \\\"cache_age_seconds\\\": None}\\n321:            bad += 1\\n322:            continue\"},{\"line\":325,\"snippet\":\"323:        age = int(now - entry[0])\\n324:        expired = age > KLINE_CACHE_MAX_STALE\\n325:        details[symbol] = {\\n326:            \\\"status\\\": \\\"ERROR\\\" if expired else \\\"OK\\\",\\n327:            \\\"candles\\\": len(entry[1][\\\"candles\\\"]),\"},{\"line\":341,\"snippet\":\"339:        \\\"library_ok\\\": library_ok,\\n340:        \\\"disk_cache_present\\\": KLINE_CACHE_FILE.is_file(),\\n341:        \\\"symbols\\\": details,\\n342:        \\\"bad\\\": bad,\\n343:    }\"},{\"line\":407,\"snippet\":\"405:\\n406:_GRAM1_CHART_JS_B64 = \\\"[encoded data, 4968 characters]\"},{\"line\":413,\"snippet\":\"411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\":\\\"no-store\\\"})\\n412:\\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\\n414:    fn = globals().get(\\\"klines\\\")\\n415:    if fn is None:\"},{\"line\":420,\"snippet\":\"418:    for name, parameter in _gram1_inspect.signature(fn).parameters.items():\\n419:        lname = name.lower()\\n420:        if lname in {\\\"symbol\\\", \\\"coin\\\", \\\"ticker\\\"}: kwargs[name] = symbol\\n421:        elif lname == \\\"request\\\": kwargs[name] = request\\n422:        elif lname == \\\"limit\\\": kwargs[name] = 1000\"},{\"line\":452,\"snippet\":\"450:    return rows[-180:]\\n451:\\n452:def _gram1_chart_svg_v4(symbol: str, rows):\\n453:    width,height,left,right,top,bottom=1200,430,72,28,24,42\\n454:    lo=min(r[3] for r in rows);hi=max(r[2] for r in rows);span=max(hi-lo,abs(hi)*1e-9,1e-12)\"},{\"line\":457,\"snippet\":\"455:    plot_w=width-left-right;plot_h=height-top-bottom;step=plot_w/max(len(rows),1);body=max(2.0,min(10.0,step*0.58))\\n456:    def y(v): return top+(hi-v)/span*plot_h\\n457:    parts=[f'<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 {width} {height}\\\" role=\\\"img\\\" aria-label=\\\"{_gram1_html.escape(symbol)} candle chart\\\" data-symbol=\\\"{_gram1_html.escape(symbol)}\\\" data-candles=\\\"{len(rows)}\\\">', '<rect width=\\\"100%\\\" height=\\\"100%\\\" fill=\\\"#081823\\\"/>']\\n458:    for n in range(5):\\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\\\"{left}\\\" y1=\\\"{yy:.2f}\\\" x2=\\\"{width-right}\\\" y2=\\\"{yy:.2f}\\\" stroke=\\\"rgba(143,165,182,.12)\\\"/>');parts.append(f'<text x=\\\"6\\\" y=\\\"{yy+4:.2f}\\\" fill=\\\"#8fa5b6\\\" font-size=\\\"12\\\">{price:.8g}</text>')\"},{\"line\":463,\"snippet\":\"461:        x=left+step*(i+.5);color=\\\"#63e6be\\\" if c>=o else \\\"#ff6b81\\\";yo=y(o);yc=y(c);yh=y(h);yl=y(l);rh=max(1.2,abs(yc-yo));ry=min(yo,yc)\\n462:        parts.append(f'<line x1=\\\"{x:.2f}\\\" y1=\\\"{yh:.2f}\\\" x2=\\\"{x:.2f}\\\" y2=\\\"{yl:.2f}\\\" stroke=\\\"{color}\\\" stroke-width=\\\"1.2\\\"/>');parts.append(f'<rect x=\\\"{x-body/2:.2f}\\\" y=\\\"{ry:.2f}\\\" width=\\\"{body:.2f}\\\" height=\\\"{rh:.2f}\\\" fill=\\\"{color}\\\"/>')\\n463:    parts.append(f'<text x=\\\"{left}\\\" y=\\\"{height-12}\\\" fill=\\\"#8fa5b6\\\" font-size=\\\"13\\\">{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей</text></svg>')\\n464:    return ''.join(parts)\\n465:\"},{\"line\":466,\"snippet\":\"464:    return ''.join(parts)\\n465:\\n466:@app.get(\\\"/api/chart-svg/{symbol}\\\", include_in_schema=False)\\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\\n468:    if not request.session.get(\\\"auth\\\"):\"}]},\"line_count\":481,\"mode\":\"0o640\",\"path\":\"app.py\",\"redactions\":1,\"sanitized_sha256\":\"b6105195f16c4efb725a7629fd2d485d2c97cfab21fcb94344925d1a2dcf8bb2\",\"sha256\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\",\"size\":32843,\"structure\":{\"defs\":[{\"end_line\":23,\"kind\":\"FunctionDef\",\"line\":16,\"name\":\"db\"},{\"end_line\":26,\"kind\":\"FunctionDef\",\"line\":25,\"name\":\"b64d\"},{\"end_line\":36,\"kind\":\"FunctionDef\",\"line\":28,\"name\":\"verify_password\"},{\"end_line\":40,\"kind\":\"FunctionDef\",\"line\":38,\"name\":\"require_login\"},{\"end_line\":47,\"kind\":\"FunctionDef\",\"line\":42,\"name\":\"market_data\"},{\"end_line\":56,\"kind\":\"FunctionDef\",\"line\":53,\"name\":\"login_page\"},{\"end_line\":71,\"kind\":\"FunctionDef\",\"line\":59,\"name\":\"login\"},{\"end_line\":76,\"kind\":\"FunctionDef\",\"line\":74,\"name\":\"logout\"},{\"end_line\":82,\"kind\":\"FunctionDef\",\"line\":79,\"name\":\"home\"},{\"end_line\":89,\"kind\":\"FunctionDef\",\"line\":85,\"name\":\"health\"},{\"end_line\":134,\"kind\":\"FunctionDef\",\"line\":93,\"name\":\"coin_icon\"},{\"end_line\":145,\"kind\":\"FunctionDef\",\"line\":142,\"name\":\"lightweight_charts_js\"},{\"end_line\":159,\"kind\":\"FunctionDef\",\"line\":153,\"name\":\"_valid_kline_result\"},{\"end_line\":183,\"kind\":\"FunctionDef\",\"line\":161,\"name\":\"_load_kline_cache\"},{\"end_line\":201,\"kind\":\"FunctionDef\",\"line\":185,\"name\":\"_save_kline_cache\"},{\"end_line\":305,\"kind\":\"FunctionDef\",\"line\":205,\"name\":\"fetch_klines\"},{\"end_line\":310,\"kind\":\"FunctionDef\",\"line\":308,\"name\":\"klines\"},{\"end_line\":344,\"kind\":\"FunctionDef\",\"line\":313,\"name\":\"chart_health\"},{\"end_line\":361,\"kind\":\"FunctionDef\",\"line\":347,\"name\":\"state\"},{\"end_line\":374,\"kind\":\"AsyncFunctionDef\",\"line\":364,\"name\":\"save_holding\"},{\"end_line\":388,\"kind\":\"AsyncFunctionDef\",\"line\":377,\"name\":\"add_alert\"},{\"end_line\":395,\"kind\":\"FunctionDef\",\"line\":391,\"name\":\"delete_alert\"},{\"end_line\":411,\"kind\":\"FunctionDef\",\"line\":410,\"name\":\"_gram1_chart_javascript_v4\"},{\"end_line\":450,\"kind\":\"AsyncFunctionDef\",\"line\":413,\"name\":\"_gram1_chart_data_v4\"},{\"end_line\":464,\"kind\":\"FunctionDef\",\"line\":452,\"name\":\"_gram1_chart_svg_v4\"},{\"end_line\":480,\"kind\":\"AsyncFunctionDef\",\"line\":467,\"name\":\"_gram1_chart_svg_endpoint_v4\"}],\"parse_ok\":true},\"uid\":0},{\"exists\":true,\"gid\":0,\"gzip_sha256\":\"49befa11ea71cd7a4c7d0a41e10d092232ecf6a2438c3311495ea36b7dcc1cda\",\"hits\":{\"alert\":[{\"line\":19,\"snippet\":\"17:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\"},{\"line\":122,\"snippet\":\"120:    now = int(time.time())\\n121:    triggered = 0\\n122:    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n123:        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n124:        if not coin:\"},{\"line\":163,\"snippet\":\"161:                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n162:        if reset and latched:\\n163:            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\\n164:            latched = 0\\n165:        if fired and not latched:\"},{\"line\":170,\"snippet\":\"168:            ok, result = send_email(a[\\\"email\\\"],subject,body)\\n169:            if ok:\\n170:                c.execute(\\\"UPDATE alerts SET latched=1,last_triggered=? WHERE id=?\\\",(now,a[\\\"id\\\"]))\\n171:                triggered += 1\\n172:            else:\"},{\"line\":173,\"snippet\":\"171:                triggered += 1\\n172:            else:\\n173:                print(\\\"ALERT_EMAIL_RESULT={} ALERT_ID={}\\\".format(result,a[\\\"id\\\"]),flush=True)\\n174:    c.commit()\\n175:    return triggered\"},{\"line\":189,\"snippet\":\"187:        history_rows = c.execute(\\\"SELECT COUNT(*) AS n FROM market_history\\\").fetchone()[\\\"n\\\"]\\n188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\"},{\"line\":190,\"snippet\":\"188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\\n192:def main():\"}],\"alerts\":[{\"line\":19,\"snippet\":\"17:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\"},{\"line\":122,\"snippet\":\"120:    now = int(time.time())\\n121:    triggered = 0\\n122:    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n123:        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n124:        if not coin:\"},{\"line\":163,\"snippet\":\"161:                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n162:        if reset and latched:\\n163:            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\\n164:            latched = 0\\n165:        if fired and not latched:\"},{\"line\":170,\"snippet\":\"168:            ok, result = send_email(a[\\\"email\\\"],subject,body)\\n169:            if ok:\\n170:                c.execute(\\\"UPDATE alerts SET latched=1,last_triggered=? WHERE id=?\\\",(now,a[\\\"id\\\"]))\\n171:                triggered += 1\\n172:            else:\"},{\"line\":189,\"snippet\":\"187:        history_rows = c.execute(\\\"SELECT COUNT(*) AS n FROM market_history\\\").fetchone()[\\\"n\\\"]\\n188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\"},{\"line\":190,\"snippet\":\"188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\\n192:def main():\"}],\"coin\":[{\"line\":11,\"snippet\":\"9:STATUS = DATA / \\\"worker-status.json\\\"\\n10:EMAIL_CONFIG = Path(os.getenv(\\\"CRYPTO_EMAIL_CONFIG_FILE\\\", \\\"/run/secrets/email_config\\\"))\\n11:COINS = {\\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\", \\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\"}\\n12:INTERVAL = 300\\n13:\"},{\"line\":18,\"snippet\":\"16:    c.row_factory = sqlite3.Row\\n17:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\"},{\"line\":19,\"snippet\":\"17:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\"},{\"line\":20,\"snippet\":\"18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\\n22:    c.commit()\"},{\"line\":21,\"snippet\":\"19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\\n22:    c.commit()\\n23:    return c\"},{\"line\":42,\"snippet\":\"40:\\n41:def fetch_market():\\n42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\"},{\"line\":44,\"snippet\":\"42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\\n45:        r.raise_for_status()\\n46:        rows = r.json()\"},{\"line\":58,\"snippet\":\"56:    try:\\n57:        with httpx.Client(timeout=15, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as fx_client:\\n58:            fx_data = fx_client.get(\\\"https://api.coingecko.com/api/v3/simple/price\\\", params={\\\"ids\\\":\\\"bitcoin\\\",\\\"vs_currencies\\\":\\\"usd,rub\\\"}).json()[\\\"bitcoin\\\"]\\n59:        candidate = float(fx_data[\\\"rub\\\"]) / float(fx_data[\\\"usd\\\"])\\n60:        if 40 <= candidate <= 200:\"},{\"line\":66,\"snippet\":\"64:    if not 40 <= usd_rub <= 200:\\n65:        usd_rub = 77.025093\\n66:    result = {\\\"updated_at\\\":now,\\\"currency\\\":\\\"usd\\\",\\\"usd_rub\\\":usd_rub,\\\"coins\\\":{}}\\n67:\\n68:    for x in rows:\"},{\"line\":70,\"snippet\":\"68:    for x in rows:\\n69:        cid = str(x.get(\\\"id\\\",\\\"\\\"))\\n70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\"},{\"line\":72,\"snippet\":\"70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\"},{\"line\":73,\"snippet\":\"71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\\n75:    return result\"},{\"line\":77,\"snippet\":\"75:    return result\\n76:\\n77:def reference_price(c, coin_id, target_ts):\\n78:    r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n79:    return float(r[\\\"price\\\"]) if r else None\"},{\"line\":78,\"snippet\":\"76:\\n77:def reference_price(c, coin_id, target_ts):\\n78:    r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n79:    return float(r[\\\"price\\\"]) if r else None\\n80:\"},{\"line\":107,\"snippet\":\"105:        return False, type(e).__name__\\n106:\\n107:def portfolio_text(c, coin_id, price):\\n108:    r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n109:    if not r:\"},{\"line\":108,\"snippet\":\"106:\\n107:def portfolio_text(c, coin_id, price):\\n108:    r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n109:    if not r:\\n110:        return \\\"\\\"\"},{\"line\":123,\"snippet\":\"121:    triggered = 0\\n122:    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n123:        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n124:        if not coin:\\n125:            continue\"},{\"line\":124,\"snippet\":\"122:    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n123:        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n124:        if not coin:\\n125:            continue\\n126:        kind = a[\\\"kind\\\"]\"},{\"line\":128,\"snippet\":\"126:        kind = a[\\\"kind\\\"]\\n127:        threshold = float(a[\\\"threshold\\\"])\\n128:        price = float(coin[\\\"price\\\"])\\n129:        cap = float(coin[\\\"market_cap\\\"])\\n130:        latched = int(a[\\\"latched\\\"])\"},{\"line\":129,\"snippet\":\"127:        threshold = float(a[\\\"threshold\\\"])\\n128:        price = float(coin[\\\"price\\\"])\\n129:        cap = float(coin[\\\"market_cap\\\"])\\n130:        latched = int(a[\\\"latched\\\"])\\n131:        fired = False\"},{\"line\":137,\"snippet\":\"135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\"},{\"line\":141,\"snippet\":\"139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\\n141:            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n142:        elif kind == \\\"cap_above\\\":\\n143:            fired = cap >= threshold\"},{\"line\":145,\"snippet\":\"143:            fired = cap >= threshold\\n144:            reset = cap < threshold * 0.99\\n145:            description = \\\"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n146:        elif kind == \\\"cap_below\\\":\\n147:            fired = cap <= threshold\"},{\"line\":149,\"snippet\":\"147:            fired = cap <= threshold\\n148:            reset = cap > threshold * 1.01\\n149:            description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n150:        elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n151:            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\"},{\"line\":151,\"snippet\":\"149:            description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n150:        elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n151:            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\\n152:            if old and old > 0:\\n153:                change = (price / old - 1) * 100\"},{\"line\":157,\"snippet\":\"155:                    fired = change >= threshold\\n156:                    reset = change < threshold * 0.5\\n157:                    description = \\\"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n158:                else:\\n159:                    fired = change <= -threshold\"},{\"line\":161,\"snippet\":\"159:                    fired = change <= -threshold\\n160:                    reset = change > -threshold * 0.5\\n161:                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n162:        if reset and latched:\\n163:            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\"},{\"line\":167,\"snippet\":\"165:        if fired and not latched:\\n166:            subject = \\\"Gram Crypto: {}\\\".format(description.split(\\\":\\\")[0])\\n167:            body = description + \\\"\\\\nТекущая капитализация: ${:,.0f}\\\\n\\\".format(cap) + portfolio_text(c,a[\\\"coin_id\\\"],price) + \\\"\\\\nВремя события: \\\" + time.strftime(\\\"%Y-%m-%d %H:%M:%S %Z\\\")\\n168:            ok, result = send_email(a[\\\"email\\\"],subject,body)\\n169:            if ok:\"},{\"line\":182,\"snippet\":\"180:    now = int(market[\\\"updated_at\\\"])\\n181:    with connect() as c:\\n182:        for cid, x in market[\\\"coins\\\"].items():\\n183:            c.execute(\\\"INSERT OR REPLACE INTO market_history(ts,coin_id,price,market_cap,volume) VALUES(?,?,?,?,?)\\\",(now,cid,x[\\\"price\\\"],x[\\\"market_cap\\\"],x[\\\"volume\\\"]))\\n184:        c.execute(\\\"DELETE FROM market_history WHERE ts<?\\\",(now-370*86400,))\"},{\"line\":183,\"snippet\":\"181:    with connect() as c:\\n182:        for cid, x in market[\\\"coins\\\"].items():\\n183:            c.execute(\\\"INSERT OR REPLACE INTO market_history(ts,coin_id,price,market_cap,volume) VALUES(?,?,?,?,?)\\\",(now,cid,x[\\\"price\\\"],x[\\\"market_cap\\\"],x[\\\"volume\\\"]))\\n184:        c.execute(\\\"DELETE FROM market_history WHERE ts<?\\\",(now-370*86400,))\\n185:        c.commit()\"}],\"coins\":[{\"line\":11,\"snippet\":\"9:STATUS = DATA / \\\"worker-status.json\\\"\\n10:EMAIL_CONFIG = Path(os.getenv(\\\"CRYPTO_EMAIL_CONFIG_FILE\\\", \\\"/run/secrets/email_config\\\"))\\n11:COINS = {\\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\", \\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\"}\\n12:INTERVAL = 300\\n13:\"},{\"line\":42,\"snippet\":\"40:\\n41:def fetch_market():\\n42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\"},{\"line\":44,\"snippet\":\"42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\\n45:        r.raise_for_status()\\n46:        rows = r.json()\"},{\"line\":66,\"snippet\":\"64:    if not 40 <= usd_rub <= 200:\\n65:        usd_rub = 77.025093\\n66:    result = {\\\"updated_at\\\":now,\\\"currency\\\":\\\"usd\\\",\\\"usd_rub\\\":usd_rub,\\\"coins\\\":{}}\\n67:\\n68:    for x in rows:\"},{\"line\":70,\"snippet\":\"68:    for x in rows:\\n69:        cid = str(x.get(\\\"id\\\",\\\"\\\"))\\n70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\"},{\"line\":72,\"snippet\":\"70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\"},{\"line\":73,\"snippet\":\"71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\\n75:    return result\"},{\"line\":123,\"snippet\":\"121:    triggered = 0\\n122:    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n123:        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n124:        if not coin:\\n125:            continue\"},{\"line\":182,\"snippet\":\"180:    now = int(market[\\\"updated_at\\\"])\\n181:    with connect() as c:\\n182:        for cid, x in market[\\\"coins\\\"].items():\\n183:            c.execute(\\\"INSERT OR REPLACE INTO market_history(ts,coin_id,price,market_cap,volume) VALUES(?,?,?,?,?)\\\",(now,cid,x[\\\"price\\\"],x[\\\"market_cap\\\"],x[\\\"volume\\\"]))\\n184:        c.execute(\\\"DELETE FROM market_history WHERE ts<?\\\",(now-370*86400,))\"},{\"line\":189,\"snippet\":\"187:        history_rows = c.execute(\\\"SELECT COUNT(*) AS n FROM market_history\\\").fetchone()[\\\"n\\\"]\\n188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\"},{\"line\":190,\"snippet\":\"188:    atomic_json(MARKET,market)\\n189:    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n190:    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\\n191:\\n192:def main():\"}],\"invested\":[{\"line\":114,\"snippet\":\"112:    avg = float(r[\\\"avg_buy\\\"])\\n113:    value = quantity * price\\n114:    invested = quantity * avg\\n115:    pnl = value - invested\\n116:    pct = pnl / invested * 100 if invested else 0\"},{\"line\":115,\"snippet\":\"113:    value = quantity * price\\n114:    invested = quantity * avg\\n115:    pnl = value - invested\\n116:    pct = pnl / invested * 100 if invested else 0\\n117:    return \\\"\\\\nКоличество монет: {:.8f}\\\\nТекущая стоимость позиции: ${:,.2f}\\\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\\\n\\\".format(quantity,value,pnl,pct)\"},{\"line\":116,\"snippet\":\"114:    invested = quantity * avg\\n115:    pnl = value - invested\\n116:    pct = pnl / invested * 100 if invested else 0\\n117:    return \\\"\\\\nКоличество монет: {:.8f}\\\\nТекущая стоимость позиции: ${:,.2f}\\\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\\\n\\\".format(quantity,value,pnl,pct)\\n118:\"}],\"portfolio\":[{\"line\":7,\"snippet\":\"5:\\n6:DATA = Path(os.getenv(\\\"CRYPTO_DATA_DIR\\\", \\\"/data\\\"))\\n7:DB = DATA / \\\"portfolio.db\\\"\\n8:MARKET = DATA / \\\"market.json\\\"\\n9:STATUS = DATA / \\\"worker-status.json\\\"\"},{\"line\":43,\"snippet\":\"41:def fetch_market():\\n42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\\n45:        r.raise_for_status()\"},{\"line\":57,\"snippet\":\"55:        pass\\n56:    try:\\n57:        with httpx.Client(timeout=15, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as fx_client:\\n58:            fx_data = fx_client.get(\\\"https://api.coingecko.com/api/v3/simple/price\\\", params={\\\"ids\\\":\\\"bitcoin\\\",\\\"vs_currencies\\\":\\\"usd,rub\\\"}).json()[\\\"bitcoin\\\"]\\n59:        candidate = float(fx_data[\\\"rub\\\"]) / float(fx_data[\\\"usd\\\"])\"},{\"line\":107,\"snippet\":\"105:        return False, type(e).__name__\\n106:\\n107:def portfolio_text(c, coin_id, price):\\n108:    r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n109:    if not r:\"},{\"line\":167,\"snippet\":\"165:        if fired and not latched:\\n166:            subject = \\\"Gram Crypto: {}\\\".format(description.split(\\\":\\\")[0])\\n167:            body = description + \\\"\\\\nТекущая капитализация: ${:,.0f}\\\\n\\\".format(cap) + portfolio_text(c,a[\\\"coin_id\\\"],price) + \\\"\\\\nВремя события: \\\" + time.strftime(\\\"%Y-%m-%d %H:%M:%S %Z\\\")\\n168:            ok, result = send_email(a[\\\"email\\\"],subject,body)\\n169:            if ok:\"}],\"price\":[{\"line\":20,\"snippet\":\"18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n21:    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\\n22:    c.commit()\"},{\"line\":42,\"snippet\":\"40:\\n41:def fetch_market():\\n42:    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n43:    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n44:        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\"},{\"line\":58,\"snippet\":\"56:    try:\\n57:        with httpx.Client(timeout=15, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as fx_client:\\n58:            fx_data = fx_client.get(\\\"https://api.coingecko.com/api/v3/simple/price\\\", params={\\\"ids\\\":\\\"bitcoin\\\",\\\"vs_currencies\\\":\\\"usd,rub\\\"}).json()[\\\"bitcoin\\\"]\\n59:        candidate = float(fx_data[\\\"rub\\\"]) / float(fx_data[\\\"usd\\\"])\\n60:        if 40 <= candidate <= 200:\"},{\"line\":72,\"snippet\":\"70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\"},{\"line\":77,\"snippet\":\"75:    return result\\n76:\\n77:def reference_price(c, coin_id, target_ts):\\n78:    r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n79:    return float(r[\\\"price\\\"]) if r else None\"},{\"line\":78,\"snippet\":\"76:\\n77:def reference_price(c, coin_id, target_ts):\\n78:    r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n79:    return float(r[\\\"price\\\"]) if r else None\\n80:\"},{\"line\":79,\"snippet\":\"77:def reference_price(c, coin_id, target_ts):\\n78:    r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n79:    return float(r[\\\"price\\\"]) if r else None\\n80:\\n81:def send_email(recipient, subject, body):\"},{\"line\":107,\"snippet\":\"105:        return False, type(e).__name__\\n106:\\n107:def portfolio_text(c, coin_id, price):\\n108:    r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n109:    if not r:\"},{\"line\":113,\"snippet\":\"111:    quantity = float(r[\\\"quantity\\\"])\\n112:    avg = float(r[\\\"avg_buy\\\"])\\n113:    value = quantity * price\\n114:    invested = quantity * avg\\n115:    pnl = value - invested\"},{\"line\":128,\"snippet\":\"126:        kind = a[\\\"kind\\\"]\\n127:        threshold = float(a[\\\"threshold\\\"])\\n128:        price = float(coin[\\\"price\\\"])\\n129:        cap = float(coin[\\\"market_cap\\\"])\\n130:        latched = int(a[\\\"latched\\\"])\"},{\"line\":134,\"snippet\":\"132:        reset = False\\n133:        description = \\\"\\\"\\n134:        if kind == \\\"price_above\\\":\\n135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\"},{\"line\":135,\"snippet\":\"133:        description = \\\"\\\"\\n134:        if kind == \\\"price_above\\\":\\n135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\"},{\"line\":136,\"snippet\":\"134:        if kind == \\\"price_above\\\":\\n135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\"},{\"line\":137,\"snippet\":\"135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\"},{\"line\":138,\"snippet\":\"136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\"},{\"line\":139,\"snippet\":\"137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\\n141:            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\"},{\"line\":140,\"snippet\":\"138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\\n141:            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n142:        elif kind == \\\"cap_above\\\":\"},{\"line\":141,\"snippet\":\"139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\\n141:            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n142:        elif kind == \\\"cap_above\\\":\\n143:            fired = cap >= threshold\"},{\"line\":151,\"snippet\":\"149:            description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n150:        elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n151:            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\\n152:            if old and old > 0:\\n153:                change = (price / old - 1) * 100\"},{\"line\":153,\"snippet\":\"151:            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\\n152:            if old and old > 0:\\n153:                change = (price / old - 1) * 100\\n154:                if kind == \\\"pump\\\":\\n155:                    fired = change >= threshold\"},{\"line\":157,\"snippet\":\"155:                    fired = change >= threshold\\n156:                    reset = change < threshold * 0.5\\n157:                    description = \\\"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n158:                else:\\n159:                    fired = change <= -threshold\"},{\"line\":161,\"snippet\":\"159:                    fired = change <= -threshold\\n160:                    reset = change > -threshold * 0.5\\n161:                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n162:        if reset and latched:\\n163:            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\"},{\"line\":167,\"snippet\":\"165:        if fired and not latched:\\n166:            subject = \\\"Gram Crypto: {}\\\".format(description.split(\\\":\\\")[0])\\n167:            body = description + \\\"\\\\nТекущая капитализация: ${:,.0f}\\\\n\\\".format(cap) + portfolio_text(c,a[\\\"coin_id\\\"],price) + \\\"\\\\nВремя события: \\\" + time.strftime(\\\"%Y-%m-%d %H:%M:%S %Z\\\")\\n168:            ok, result = send_email(a[\\\"email\\\"],subject,body)\\n169:            if ok:\"},{\"line\":183,\"snippet\":\"181:    with connect() as c:\\n182:        for cid, x in market[\\\"coins\\\"].items():\\n183:            c.execute(\\\"INSERT OR REPLACE INTO market_history(ts,coin_id,price,market_cap,volume) VALUES(?,?,?,?,?)\\\",(now,cid,x[\\\"price\\\"],x[\\\"market_cap\\\"],x[\\\"volume\\\"]))\\n184:        c.execute(\\\"DELETE FROM market_history WHERE ts<?\\\",(now-370*86400,))\\n185:        c.commit()\"}],\"quantity\":[{\"line\":18,\"snippet\":\"16:    c.row_factory = sqlite3.Row\\n17:    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n18:    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n19:    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n20:    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\"},{\"line\":108,\"snippet\":\"106:\\n107:def portfolio_text(c, coin_id, price):\\n108:    r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n109:    if not r:\\n110:        return \\\"\\\"\"},{\"line\":111,\"snippet\":\"109:    if not r:\\n110:        return \\\"\\\"\\n111:    quantity = float(r[\\\"quantity\\\"])\\n112:    avg = float(r[\\\"avg_buy\\\"])\\n113:    value = quantity * price\"},{\"line\":113,\"snippet\":\"111:    quantity = float(r[\\\"quantity\\\"])\\n112:    avg = float(r[\\\"avg_buy\\\"])\\n113:    value = quantity * price\\n114:    invested = quantity * avg\\n115:    pnl = value - invested\"},{\"line\":114,\"snippet\":\"112:    avg = float(r[\\\"avg_buy\\\"])\\n113:    value = quantity * price\\n114:    invested = quantity * avg\\n115:    pnl = value - invested\\n116:    pct = pnl / invested * 100 if invested else 0\"},{\"line\":117,\"snippet\":\"115:    pnl = value - invested\\n116:    pct = pnl / invested * 100 if invested else 0\\n117:    return \\\"\\\\nКоличество монет: {:.8f}\\\\nТекущая стоимость позиции: ${:,.2f}\\\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\\\n\\\".format(quantity,value,pnl,pct)\\n118:\\n119:def evaluate(c, market):\"}],\"symbol\":[{\"line\":72,\"snippet\":\"70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\"},{\"line\":137,\"snippet\":\"135:            fired = price >= threshold\\n136:            reset = price < threshold * 0.99\\n137:            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n138:        elif kind == \\\"price_below\\\":\\n139:            fired = price <= threshold\"},{\"line\":141,\"snippet\":\"139:            fired = price <= threshold\\n140:            reset = price > threshold * 1.01\\n141:            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n142:        elif kind == \\\"cap_above\\\":\\n143:            fired = cap >= threshold\"},{\"line\":145,\"snippet\":\"143:            fired = cap >= threshold\\n144:            reset = cap < threshold * 0.99\\n145:            description = \\\"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n146:        elif kind == \\\"cap_below\\\":\\n147:            fired = cap <= threshold\"},{\"line\":149,\"snippet\":\"147:            fired = cap <= threshold\\n148:            reset = cap > threshold * 1.01\\n149:            description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n150:        elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n151:            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\"},{\"line\":157,\"snippet\":\"155:                    fired = change >= threshold\\n156:                    reset = change < threshold * 0.5\\n157:                    description = \\\"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n158:                else:\\n159:                    fired = change <= -threshold\"},{\"line\":161,\"snippet\":\"159:                    fired = change <= -threshold\\n160:                    reset = change > -threshold * 0.5\\n161:                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n162:        if reset and latched:\\n163:            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\"}],\"total\":[{\"line\":72,\"snippet\":\"70:        if cid not in COINS:\\n71:            continue\\n72:        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n73:    if len(result[\\\"coins\\\"]) != len(COINS):\\n74:        raise RuntimeError(\\\"incomplete market response\\\")\"}]},\"line_count\":203,\"mode\":\"0o640\",\"path\":\"worker.py\",\"redactions\":0,\"sanitized_sha256\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"sha256\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"size\":10300,\"structure\":{\"defs\":[{\"end_line\":23,\"kind\":\"FunctionDef\",\"line\":14,\"name\":\"connect\"},{\"end_line\":28,\"kind\":\"FunctionDef\",\"line\":25,\"name\":\"atomic_json\"},{\"end_line\":39,\"kind\":\"FunctionDef\",\"line\":30,\"name\":\"config\"},{\"end_line\":75,\"kind\":\"FunctionDef\",\"line\":41,\"name\":\"fetch_market\"},{\"end_line\":79,\"kind\":\"FunctionDef\",\"line\":77,\"name\":\"reference_price\"},{\"end_line\":105,\"kind\":\"FunctionDef\",\"line\":81,\"name\":\"send_email\"},{\"end_line\":117,\"kind\":\"FunctionDef\",\"line\":107,\"name\":\"portfolio_text\"},{\"end_line\":175,\"kind\":\"FunctionDef\",\"line\":119,\"name\":\"evaluate\"},{\"end_line\":190,\"kind\":\"FunctionDef\",\"line\":177,\"name\":\"cycle\"},{\"end_line\":200,\"kind\":\"FunctionDef\",\"line\":192,\"name\":\"main\"}],\"parse_ok\":true},\"uid\":0},{\"exists\":true,\"gid\":0,\"gzip_sha256\":\"ea0cc3667dce3284eb67783fdec57d3077fef09e400a37f0d11ed4b59aad6479\",\"hits\":{\"alert\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":8,\"snippet\":\"6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":12,\"snippet\":\"10:(()=>{\\n11:  const install=()=>{\\n12:    const button=document.getElementById(\\\"addAlert\\\");\\n13:    const coinEl=document.getElementById(\\\"aCoin\\\");\\n14:    const kindEl=document.getElementById(\\\"aKind\\\");\"},{\"line\":20,\"snippet\":\"18:    if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;}\\n19:    button.type=\\\"button\\\";\\n20:    let statusEl=document.getElementById(\\\"alertCreateStatusV3\\\");\\n21:    if(!statusEl){\\n22:      statusEl=document.createElement(\\\"div\\\");\"},{\"line\":23,\"snippet\":\"21:    if(!statusEl){\\n22:      statusEl=document.createElement(\\\"div\\\");\\n23:      statusEl.id=\\\"alertCreateStatusV3\\\";\\n24:      statusEl.className=\\\"muted full\\\";\\n25:      statusEl.setAttribute(\\\"aria-live\\\",\\\"polite\\\");\"},{\"line\":65,\"snippet\":\"63:        if(!beforeResponse.ok){throw new Error(\\\"предварительная проверка HTTP \\\"+beforeResponse.status);}\\n64:        const before=await beforeResponse.json();\\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\\n66:        const response=await fetch(\\\"/api/alert\\\",{\\n67:          method:\\\"POST\\\",\"},{\"line\":66,\"snippet\":\"64:        const before=await beforeResponse.json();\\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\\n66:        const response=await fetch(\\\"/api/alert\\\",{\\n67:          method:\\\"POST\\\",\\n68:          credentials:\\\"same-origin\\\",\"},{\"line\":81,\"snippet\":\"79:        const fresh=await verifyResponse.json();\\n80:        const returnedId=Number(body.id||0);\\n81:        const created=(fresh.alerts||[]).find(a=>{\\n82:          const id=Number(a.id);\\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);\"},{\"line\":89,\"snippet\":\"87:        try{state=fresh;}catch(_){}\\n88:        try{render();}catch(error){console.error(error);}\\n89:        try{renderAlerts();}catch(_){}\\n90:        thresholdEl.value=\\\"\\\";\\n91:        statusEl.className=\\\"good full\\\";\"},{\"line\":109,\"snippet\":\"107:})();\\n108:</script>\\n109:<script id=\\\"ALERT_LIST_RENDERER_V5\\\">\\n110:(()=>{\\n111:  let lastState=null;\"},{\"line\":124,\"snippet\":\"122:  const exchangeRate=data=>Number(data&&data.market&&data.market.usd_rub||0);\\n123:  const converted=(data,value)=>Number(value||0)*(activeCurrency()===\\\"RUB\\\"&&exchangeRate(data)>0?exchangeRate(data):1);\\n124:  const alertMoney=(data,value)=>new Intl.NumberFormat(activeCurrency()===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:activeCurrency(),minimumFractionDigits:2,maximumFractionDigits:2}).format(converted(data,value));\\n125:  const make=(tag,className,value)=>{\\n126:    const element=document.createElement(tag);\"},{\"line\":131,\"snippet\":\"129:    return element;\\n130:  };\\n131:  const alertLabel=(data,alert)=>{\\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\"},{\"line\":132,\"snippet\":\"130:  };\\n131:  const alertLabel=(data,alert)=>{\\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\"},{\"line\":133,\"snippet\":\"131:  const alertLabel=(data,alert)=>{\\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\\n135:    return symbol+\\\": \\\"+kind+\\\" \\\"+alertMoney(data,alert.threshold);\"},{\"line\":134,\"snippet\":\"132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\\n135:    return symbol+\\\": \\\"+kind+\\\" \\\"+alertMoney(data,alert.threshold);\\n136:  };\"},{\"line\":135,\"snippet\":\"133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\\n135:    return symbol+\\\": \\\"+kind+\\\" \\\"+alertMoney(data,alert.threshold);\\n136:  };\\n137:  const draw=data=>{\"},{\"line\":138,\"snippet\":\"136:  };\\n137:  const draw=data=>{\\n138:    const box=document.getElementById(\\\"alertList\\\");\\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\\n140:    lastState=data;\"},{\"line\":139,\"snippet\":\"137:  const draw=data=>{\\n138:    const box=document.getElementById(\\\"alertList\\\");\\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\\n140:    lastState=data;\\n141:    box.replaceChildren();\"},{\"line\":142,\"snippet\":\"140:    lastState=data;\\n141:    box.replaceChildren();\\n142:    if(data.alerts.length===0){\\n143:      box.append(make(\\\"span\\\",\\\"muted\\\",\\\"Оповещений пока нет\\\"));\\n144:      return;\"},{\"line\":146,\"snippet\":\"144:      return;\\n145:    }\\n146:    data.alerts.forEach(alert=>{\\n147:      const row=make(\\\"div\\\",\\\"alert\\\");\\n148:      row.dataset.alertRowV4=\\\"1\\\";\"},{\"line\":147,\"snippet\":\"145:    }\\n146:    data.alerts.forEach(alert=>{\\n147:      const row=make(\\\"div\\\",\\\"alert\\\");\\n148:      row.dataset.alertRowV4=\\\"1\\\";\\n149:      const left=make(\\\"div\\\");\"},{\"line\":148,\"snippet\":\"146:    data.alerts.forEach(alert=>{\\n147:      const row=make(\\\"div\\\",\\\"alert\\\");\\n148:      row.dataset.alertRowV4=\\\"1\\\";\\n149:      const left=make(\\\"div\\\");\\n150:      left.append(make(\\\"b\\\",\\\"\\\",alertLabel(data,alert)));\"},{\"line\":150,\"snippet\":\"148:      row.dataset.alertRowV4=\\\"1\\\";\\n149:      const left=make(\\\"div\\\");\\n150:      left.append(make(\\\"b\\\",\\\"\\\",alertLabel(data,alert)));\\n151:      left.append(make(\\\"div\\\",\\\"muted\\\",alert.email||\\\"\\\"));\\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\"},{\"line\":151,\"snippet\":\"149:      const left=make(\\\"div\\\");\\n150:      left.append(make(\\\"b\\\",\\\"\\\",alertLabel(data,alert)));\\n151:      left.append(make(\\\"div\\\",\\\"muted\\\",alert.email||\\\"\\\"));\\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\\n153:      const quantity=Number(holding.quantity||0);\"},{\"line\":152,\"snippet\":\"150:      left.append(make(\\\"b\\\",\\\"\\\",alertLabel(data,alert)));\\n151:      left.append(make(\\\"div\\\",\\\"muted\\\",alert.email||\\\"\\\"));\\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\\n153:      const quantity=Number(holding.quantity||0);\\n154:      const isPrice=[\\\"price_above\\\",\\\"price_below\\\"].includes(alert.kind);\"},{\"line\":154,\"snippet\":\"152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\\n153:      const quantity=Number(holding.quantity||0);\\n154:      const isPrice=[\\\"price_above\\\",\\\"price_below\\\"].includes(alert.kind);\\n155:      const sale=make(\\\"div\\\",\\\"sale\\\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\\\"—\\\");\\n156:      const button=make(\\\"button\\\",\\\"btn ghost del\\\",\\\"Удалить\\\");\"},{\"line\":155,\"snippet\":\"153:      const quantity=Number(holding.quantity||0);\\n154:      const isPrice=[\\\"price_above\\\",\\\"price_below\\\"].includes(alert.kind);\\n155:      const sale=make(\\\"div\\\",\\\"sale\\\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\\\"—\\\");\\n156:      const button=make(\\\"button\\\",\\\"btn ghost del\\\",\\\"Удалить\\\");\\n157:      button.type=\\\"button\\\";\"},{\"line\":158,\"snippet\":\"156:      const button=make(\\\"button\\\",\\\"btn ghost del\\\",\\\"Удалить\\\");\\n157:      button.type=\\\"button\\\";\\n158:      button.dataset.alertDeleteV4=String(alert.id);\\n159:      button.onclick=async event=>{\\n160:        event.preventDefault();\"},{\"line\":163,\"snippet\":\"161:        button.disabled=true;\\n162:        try{\\n163:          const response=await fetch(\\\"/api/alert/\\\"+encodeURIComponent(alert.id),{method:\\\"DELETE\\\",credentials:\\\"same-origin\\\",cache:\\\"no-store\\\"});\\n164:          if(response.status===401){location=\\\"/login\\\";return;}\\n165:          if(!response.ok)throw new Error(\\\"HTTP \\\"+response.status);\"}],\"alerts\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":8,\"snippet\":\"6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":65,\"snippet\":\"63:        if(!beforeResponse.ok){throw new Error(\\\"предварительная проверка HTTP \\\"+beforeResponse.status);}\\n64:        const before=await beforeResponse.json();\\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\\n66:        const response=await fetch(\\\"/api/alert\\\",{\\n67:          method:\\\"POST\\\",\"},{\"line\":81,\"snippet\":\"79:        const fresh=await verifyResponse.json();\\n80:        const returnedId=Number(body.id||0);\\n81:        const created=(fresh.alerts||[]).find(a=>{\\n82:          const id=Number(a.id);\\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);\"},{\"line\":89,\"snippet\":\"87:        try{state=fresh;}catch(_){}\\n88:        try{render();}catch(error){console.error(error);}\\n89:        try{renderAlerts();}catch(_){}\\n90:        thresholdEl.value=\\\"\\\";\\n91:        statusEl.className=\\\"good full\\\";\"},{\"line\":139,\"snippet\":\"137:  const draw=data=>{\\n138:    const box=document.getElementById(\\\"alertList\\\");\\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\\n140:    lastState=data;\\n141:    box.replaceChildren();\"},{\"line\":142,\"snippet\":\"140:    lastState=data;\\n141:    box.replaceChildren();\\n142:    if(data.alerts.length===0){\\n143:      box.append(make(\\\"span\\\",\\\"muted\\\",\\\"Оповещений пока нет\\\"));\\n144:      return;\"},{\"line\":146,\"snippet\":\"144:      return;\\n145:    }\\n146:    data.alerts.forEach(alert=>{\\n147:      const row=make(\\\"div\\\",\\\"alert\\\");\\n148:      row.dataset.alertRowV4=\\\"1\\\";\"},{\"line\":196,\"snippet\":\"194:    if(box){\\n195:      const observer=new MutationObserver(()=>{\\n196:        if(lastState&&lastState.alerts&&lastState.alerts.length&&!box.querySelector(\\\"[data-alert-row-v4]\\\")){\\n197:          queueMicrotask(()=>draw(lastState));\\n198:        }\"}],\"chart\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":217,\"snippet\":\"215:})();\\n216:</script>\\n217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\"},{\"line\":218,\"snippet\":\"216:</script>\\n217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\"},{\"line\":219,\"snippet\":\"217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\\n221:.tv-accordion-item:last-child{border-bottom:0}\"},{\"line\":228,\"snippet\":\"226:.tv-accordion-body{display:block;border-top:1px solid var(--l)}\\n227:.tv-widget-slot{display:block;width:100%;height:430px;min-width:0;background:#081823}\\n228:.tv-chart-message{min-height:20px;padding:8px 14px 12px}\\n229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\\n230:</style>\"},{\"line\":231,\"snippet\":\"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\\n230:</style>\\n231:<script src=\\\"/vendor/lightweight-charts.js\\\"></script>\\n232:<script id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\\\">(()=>{\\n233:  \\\"use strict\\\";\"},{\"line\":232,\"snippet\":\"230:</style>\\n231:<script src=\\\"/vendor/lightweight-charts.js\\\"></script>\\n232:<script id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\\\">(()=>{\\n233:  \\\"use strict\\\";\\n234:  const VERSION=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\";\"},{\"line\":236,\"snippet\":\"234:  const VERSION=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\";\\n235:  const symbols=[\\\"HBAR\\\",\\\"POL\\\",\\\"AVAX\\\",\\\"LINK\\\"];\\n236:  const charts=new Map();\\n237:  const requests=new Map();\\n238:  const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));\"},{\"line\":297,\"snippet\":\"295:  const waitLibrary=async()=>{\\n296:    for(let attempt=0;attempt<150;attempt++){\\n297:      if(window.LightweightCharts&&typeof window.LightweightCharts.createChart===\\\"function\\\")return window.LightweightCharts;\\n298:      await sleep(100);\\n299:    }\"},{\"line\":300,\"snippet\":\"298:      await sleep(100);\\n299:    }\\n300:    throw new Error(\\\"Библиотека Lightweight Charts не загрузилась\\\");\\n301:  };\\n302:  const widthOf=box=>Math.max(320,Math.floor(box.getBoundingClientRect().width||box.clientWidth||900));\"},{\"line\":306,\"snippet\":\"304:    if(typeof api.addCandlestickSeries===\\\"function\\\")return api.addCandlestickSeries(options);\\n305:    if(typeof api.addSeries===\\\"function\\\"&&library.CandlestickSeries)return api.addSeries(library.CandlestickSeries,options);\\n306:    throw new Error(\\\"Версия Lightweight Charts не поддерживается\\\");\\n307:  };\\n308:  const hideLegacy=()=>{\"},{\"line\":309,\"snippet\":\"307:  };\\n308:  const hideLegacy=()=>{\\n309:    for(const id of [\\\"chart\\\",\\\"chartTitle\\\",\\\"marketStatus\\\"]){\\n310:      const element=document.getElementById(id);\\n311:      if(!element)continue;\"},{\"line\":318,\"snippet\":\"316:    }\\n317:  };\\n318:  const createChart=async item=>{\\n319:    const symbol=item.dataset.symbol;\\n320:    if(item.dataset.chartState===\\\"ready\\\"){\"},{\"line\":320,\"snippet\":\"318:  const createChart=async item=>{\\n319:    const symbol=item.dataset.symbol;\\n320:    if(item.dataset.chartState===\\\"ready\\\"){\\n321:      const instance=charts.get(symbol);\\n322:      await afterLayout();\"},{\"line\":321,\"snippet\":\"319:    const symbol=item.dataset.symbol;\\n320:    if(item.dataset.chartState===\\\"ready\\\"){\\n321:      const instance=charts.get(symbol);\\n322:      await afterLayout();\\n323:      if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}\"},{\"line\":326,\"snippet\":\"324:      return instance;\\n325:    }\\n326:    if(item.dataset.chartState===\\\"loading\\\"&&item._chartPromise)return item._chartPromise;\\n327:    const box=item.querySelector(\\\".tv-widget-slot\\\");\\n328:    const message=item.querySelector(\\\".tv-chart-message\\\");\"},{\"line\":328,\"snippet\":\"326:    if(item.dataset.chartState===\\\"loading\\\"&&item._chartPromise)return item._chartPromise;\\n327:    const box=item.querySelector(\\\".tv-widget-slot\\\");\\n328:    const message=item.querySelector(\\\".tv-chart-message\\\");\\n329:    item.dataset.chartState=\\\"loading\\\";\\n330:    message.textContent=\\\"Загрузка графика…\\\";\"},{\"line\":329,\"snippet\":\"327:    const box=item.querySelector(\\\".tv-widget-slot\\\");\\n328:    const message=item.querySelector(\\\".tv-chart-message\\\");\\n329:    item.dataset.chartState=\\\"loading\\\";\\n330:    message.textContent=\\\"Загрузка графика…\\\";\\n331:    item._chartPromise=(async()=>{\"},{\"line\":331,\"snippet\":\"329:    item.dataset.chartState=\\\"loading\\\";\\n330:    message.textContent=\\\"Загрузка графика…\\\";\\n331:    item._chartPromise=(async()=>{\\n332:      try{\\n333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\"},{\"line\":335,\"snippet\":\"333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\\n334:        await afterLayout();\\n335:        if(!item.open){item.dataset.chartState=\\\"idle\\\";message.textContent=\\\"Нажмите, чтобы открыть график\\\";return null;}\\n336:        box.replaceChildren();\\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\\\"#081823\\\"}:{color:\\\"#081823\\\"};\"},{\"line\":338,\"snippet\":\"336:        box.replaceChildren();\\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\\\"#081823\\\"}:{color:\\\"#081823\\\"};\\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\\\"#8fa5b6\\\"},grid:{vertLines:{color:\\\"rgba(143,165,182,.08)\\\"},horzLines:{color:\\\"rgba(143,165,182,.08)\\\"}},rightPriceScale:{borderColor:\\\"rgba(143,165,182,.20)\\\"},timeScale:{borderColor:\\\"rgba(143,165,182,.20)\\\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\\n339:        const series=makeSeries(api,library,{upColor:\\\"#63e6be\\\",downColor:\\\"#ff6b81\\\",wickUpColor:\\\"#63e6be\\\",wickDownColor:\\\"#ff6b81\\\",borderVisible:false,priceFormat:{type:\\\"price\\\",precision:8,minMove:0.00000001}});\\n340:        series.setData(candles);\"},{\"line\":346,\"snippet\":\"344:        if(observer)observer.observe(box);else window.addEventListener(\\\"resize\\\",resize,{passive:true});\\n345:        const instance={symbol,api,series,box,observer,resize,candles};\\n346:        charts.set(symbol,instance);\\n347:        item.dataset.chartState=\\\"ready\\\";\\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\"},{\"line\":347,\"snippet\":\"345:        const instance={symbol,api,series,box,observer,resize,candles};\\n346:        charts.set(symbol,instance);\\n347:        item.dataset.chartState=\\\"ready\\\";\\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\\n349:        return instance;\"},{\"line\":351,\"snippet\":\"349:        return instance;\\n350:      }catch(error){\\n351:        item.dataset.chartState=\\\"error\\\";\\n352:        message.textContent=\\\"Ошибка графика \\\"+symbol+\\\": \\\"+String(error&&error.message||error);\\n353:        console.error(\\\"Gram1 chart error\\\",symbol,error);\"},{\"line\":353,\"snippet\":\"351:        item.dataset.chartState=\\\"error\\\";\\n352:        message.textContent=\\\"Ошибка графика \\\"+symbol+\\\": \\\"+String(error&&error.message||error);\\n353:        console.error(\\\"Gram1 chart error\\\",symbol,error);\\n354:        return null;\\n355:      }finally{item._chartPromise=null;}\"},{\"line\":355,\"snippet\":\"353:        console.error(\\\"Gram1 chart error\\\",symbol,error);\\n354:        return null;\\n355:      }finally{item._chartPromise=null;}\\n356:    })();\\n357:    return item._chartPromise;\"},{\"line\":357,\"snippet\":\"355:      }finally{item._chartPromise=null;}\\n356:    })();\\n357:    return item._chartPromise;\\n358:  };\\n359:  const init=async()=>{\"},{\"line\":361,\"snippet\":\"359:  const init=async()=>{\\n360:    hideLegacy();\\n361:    const root=document.getElementById(\\\"coinCharts\\\");\\n362:    if(!root||root.dataset.interactiveReady===\\\"1\\\")return;\\n363:    root.dataset.interactiveReady=\\\"1\\\";\"}],\"charts\":[{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":217,\"snippet\":\"215:})();\\n216:</script>\\n217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\"},{\"line\":218,\"snippet\":\"216:</script>\\n217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\"},{\"line\":231,\"snippet\":\"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\\n230:</style>\\n231:<script src=\\\"/vendor/lightweight-charts.js\\\"></script>\\n232:<script id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\\\">(()=>{\\n233:  \\\"use strict\\\";\"},{\"line\":232,\"snippet\":\"230:</style>\\n231:<script src=\\\"/vendor/lightweight-charts.js\\\"></script>\\n232:<script id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\\\">(()=>{\\n233:  \\\"use strict\\\";\\n234:  const VERSION=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\";\"},{\"line\":236,\"snippet\":\"234:  const VERSION=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\";\\n235:  const symbols=[\\\"HBAR\\\",\\\"POL\\\",\\\"AVAX\\\",\\\"LINK\\\"];\\n236:  const charts=new Map();\\n237:  const requests=new Map();\\n238:  const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));\"},{\"line\":297,\"snippet\":\"295:  const waitLibrary=async()=>{\\n296:    for(let attempt=0;attempt<150;attempt++){\\n297:      if(window.LightweightCharts&&typeof window.LightweightCharts.createChart===\\\"function\\\")return window.LightweightCharts;\\n298:      await sleep(100);\\n299:    }\"},{\"line\":300,\"snippet\":\"298:      await sleep(100);\\n299:    }\\n300:    throw new Error(\\\"Библиотека Lightweight Charts не загрузилась\\\");\\n301:  };\\n302:  const widthOf=box=>Math.max(320,Math.floor(box.getBoundingClientRect().width||box.clientWidth||900));\"},{\"line\":306,\"snippet\":\"304:    if(typeof api.addCandlestickSeries===\\\"function\\\")return api.addCandlestickSeries(options);\\n305:    if(typeof api.addSeries===\\\"function\\\"&&library.CandlestickSeries)return api.addSeries(library.CandlestickSeries,options);\\n306:    throw new Error(\\\"Версия Lightweight Charts не поддерживается\\\");\\n307:  };\\n308:  const hideLegacy=()=>{\"},{\"line\":320,\"snippet\":\"318:  const createChart=async item=>{\\n319:    const symbol=item.dataset.symbol;\\n320:    if(item.dataset.chartState===\\\"ready\\\"){\\n321:      const instance=charts.get(symbol);\\n322:      await afterLayout();\"},{\"line\":321,\"snippet\":\"319:    const symbol=item.dataset.symbol;\\n320:    if(item.dataset.chartState===\\\"ready\\\"){\\n321:      const instance=charts.get(symbol);\\n322:      await afterLayout();\\n323:      if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}\"},{\"line\":326,\"snippet\":\"324:      return instance;\\n325:    }\\n326:    if(item.dataset.chartState===\\\"loading\\\"&&item._chartPromise)return item._chartPromise;\\n327:    const box=item.querySelector(\\\".tv-widget-slot\\\");\\n328:    const message=item.querySelector(\\\".tv-chart-message\\\");\"},{\"line\":329,\"snippet\":\"327:    const box=item.querySelector(\\\".tv-widget-slot\\\");\\n328:    const message=item.querySelector(\\\".tv-chart-message\\\");\\n329:    item.dataset.chartState=\\\"loading\\\";\\n330:    message.textContent=\\\"Загрузка графика…\\\";\\n331:    item._chartPromise=(async()=>{\"},{\"line\":335,\"snippet\":\"333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\\n334:        await afterLayout();\\n335:        if(!item.open){item.dataset.chartState=\\\"idle\\\";message.textContent=\\\"Нажмите, чтобы открыть график\\\";return null;}\\n336:        box.replaceChildren();\\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\\\"#081823\\\"}:{color:\\\"#081823\\\"};\"},{\"line\":346,\"snippet\":\"344:        if(observer)observer.observe(box);else window.addEventListener(\\\"resize\\\",resize,{passive:true});\\n345:        const instance={symbol,api,series,box,observer,resize,candles};\\n346:        charts.set(symbol,instance);\\n347:        item.dataset.chartState=\\\"ready\\\";\\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\"},{\"line\":347,\"snippet\":\"345:        const instance={symbol,api,series,box,observer,resize,candles};\\n346:        charts.set(symbol,instance);\\n347:        item.dataset.chartState=\\\"ready\\\";\\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\\n349:        return instance;\"},{\"line\":351,\"snippet\":\"349:        return instance;\\n350:      }catch(error){\\n351:        item.dataset.chartState=\\\"error\\\";\\n352:        message.textContent=\\\"Ошибка графика \\\"+symbol+\\\": \\\"+String(error&&error.message||error);\\n353:        console.error(\\\"Gram1 chart error\\\",symbol,error);\"},{\"line\":361,\"snippet\":\"359:  const init=async()=>{\\n360:    hideLegacy();\\n361:    const root=document.getElementById(\\\"coinCharts\\\");\\n362:    if(!root||root.dataset.interactiveReady===\\\"1\\\")return;\\n363:    root.dataset.interactiveReady=\\\"1\\\";\"},{\"line\":375,\"snippet\":\"373:    };\\n374:    items.forEach((item,index)=>{\\n375:      item.dataset.chartState=\\\"idle\\\";\\n376:      item.open=index===0;\\n377:      item.addEventListener(\\\"toggle\\\",async()=>{\"},{\"line\":397,\"snippet\":\"395:\\n396:<style id=\\\"SHARP_MOVES_LAYOUT_V2\\\">.sharp-moves-panel{grid-column:1 / -1;justify-self:center;width:min(100%,860px);margin-inline:auto}.sharp-moves-panel .title{justify-content:center}.sharp-moves-panel .title h2{width:100%;text-align:center}#moves{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:12px!important}@media(max-width:750px){.sharp-moves-panel{width:100%}#moves{grid-template-columns:1fr!important}}</style>\\n397:<script id=\\\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\\\">(()=>{const move=()=>{const chart=document.getElementById(\\\"chart\\\");const panel=chart&&chart.closest(\\\".panel\\\");const main=document.querySelector(\\\"main\\\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\\\"1 / -1\\\";const title=document.getElementById(\\\"chartTitle\\\");if(title)title.textContent=\\\"Графики монет\\\";const note=panel.querySelector(\\\".muted\\\");if(note)note.textContent=\\\"Свечи Binance · 1 месяц · весь доступный период\\\"};document.addEventListener(\\\"DOMContentLoaded\\\",move);window.addEventListener(\\\"load\\\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>\"}],\"coin\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":8,\"snippet\":\"6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":13,\"snippet\":\"11:  const install=()=>{\\n12:    const button=document.getElementById(\\\"addAlert\\\");\\n13:    const coinEl=document.getElementById(\\\"aCoin\\\");\\n14:    const kindEl=document.getElementById(\\\"aKind\\\");\\n15:    const thresholdEl=document.getElementById(\\\"aThreshold\\\");\"},{\"line\":18,\"snippet\":\"16:    const windowEl=document.getElementById(\\\"aWindow\\\");\\n17:    const emailEl=document.getElementById(\\\"aEmail\\\");\\n18:    if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;}\\n19:    button.type=\\\"button\\\";\\n20:    let statusEl=document.getElementById(\\\"alertCreateStatusV3\\\");\"},{\"line\":30,\"snippet\":\"28:    const ensureOptions=()=>{\\n29:      try{\\n30:        if(typeof state!==\\\"undefined\\\"&&state&&state.coins){\\n31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\"},{\"line\":31,\"snippet\":\"29:      try{\\n30:        if(typeof state!==\\\"undefined\\\"&&state&&state.coins){\\n31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\");\"},{\"line\":32,\"snippet\":\"30:        if(typeof state!==\\\"undefined\\\"&&state&&state.coins){\\n31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\");\\n34:          }\"},{\"line\":33,\"snippet\":\"31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\");\\n34:          }\\n35:        }\"},{\"line\":46,\"snippet\":\"44:      const threshold=Number(String(thresholdEl.value||\\\"\\\").trim().replace(\\\",\\\",\\\".\\\"));\\n45:      const payload={\\n46:        coin_id:String(coinEl.value||\\\"\\\"),\\n47:        kind:String(kindEl.value||\\\"\\\"),\\n48:        threshold:threshold,\"},{\"line\":54,\"snippet\":\"52:      statusEl.className=\\\"muted full\\\";\\n53:      statusEl.textContent=\\\"\\\";\\n54:      if(!payload.coin_id){statusEl.className=\\\"bad full\\\";statusEl.textContent=\\\"Не выбрана монета\\\";return;}\\n55:      if(!Number.isFinite(payload.threshold)||payload.threshold<=0){statusEl.className=\\\"bad full\\\";statusEl.textContent=\\\"Укажи положительный порог\\\";return;}\\n56:      if(!payload.email||!payload.email.includes(\\\"@\\\")){statusEl.className=\\\"bad full\\\";statusEl.textContent=\\\"Укажи корректный email\\\";return;}\"},{\"line\":84,\"snippet\":\"82:          const id=Number(a.id);\\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);\\n84:          return idIsNew&&a.coin_id===payload.coin_id&&a.kind===payload.kind&&Number(a.threshold)===payload.threshold&&String(a.email)===payload.email;\\n85:        });\\n86:        if(!created){throw new Error(\\\"сервер не подтвердил новую запись оповещения\\\");}\"},{\"line\":132,\"snippet\":\"130:  };\\n131:  const alertLabel=(data,alert)=>{\\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\"},{\"line\":152,\"snippet\":\"150:      left.append(make(\\\"b\\\",\\\"\\\",alertLabel(data,alert)));\\n151:      left.append(make(\\\"div\\\",\\\"muted\\\",alert.email||\\\"\\\"));\\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\\n153:      const quantity=Number(holding.quantity||0);\\n154:      const isPrice=[\\\"price_above\\\",\\\"price_below\\\"].includes(alert.kind);\"},{\"line\":218,\"snippet\":\"216:</script>\\n217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\"},{\"line\":219,\"snippet\":\"217:<style id=\\\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\\\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\\n219:.coin-chart-title{margin-bottom:12px}\\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\\n221:.tv-accordion-item:last-child{border-bottom:0}\"},{\"line\":361,\"snippet\":\"359:  const init=async()=>{\\n360:    hideLegacy();\\n361:    const root=document.getElementById(\\\"coinCharts\\\");\\n362:    if(!root||root.dataset.interactiveReady===\\\"1\\\")return;\\n363:    root.dataset.interactiveReady=\\\"1\\\";\"}],\"coins\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":30,\"snippet\":\"28:    const ensureOptions=()=>{\\n29:      try{\\n30:        if(typeof state!==\\\"undefined\\\"&&state&&state.coins){\\n31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\"},{\"line\":31,\"snippet\":\"29:      try{\\n30:        if(typeof state!==\\\"undefined\\\"&&state&&state.coins){\\n31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\");\"},{\"line\":33,\"snippet\":\"31:          const ids=Object.keys(state.coins);\\n32:          if(ids.length&&coinEl.options.length!==ids.length){\\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\");\\n34:          }\\n35:        }\"},{\"line\":132,\"snippet\":\"130:  };\\n131:  const alertLabel=(data,alert)=>{\\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\\n133:    const kind=kindNames[alert.kind]||alert.kind;\\n134:    if([\\\"pump\\\",\\\"dump\\\"].includes(alert.kind))return symbol+\\\": \\\"+kind+\\\" \\\"+Number(alert.threshold).toLocaleString(\\\"ru-RU\\\",{maximumFractionDigits:12})+\\\"%\\\";\"}],\"invested\":[{\"line\":5,\"snippet\":\"3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-ch\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"}],\"move\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":338,\"snippet\":\"336:        box.replaceChildren();\\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\\\"#081823\\\"}:{color:\\\"#081823\\\"};\\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\\\"#8fa5b6\\\"},grid:{vertLines:{color:\\\"rgba(143,165,182,.08)\\\"},horzLines:{color:\\\"rgba(143,165,182,.08)\\\"}},rightPriceScale:{borderColor:\\\"rgba(143,165,182,.20)\\\"},timeScale:{borderColor:\\\"rgba(143,165,182,.20)\\\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\\n339:        const series=makeSeries(api,library,{upColor:\\\"#63e6be\\\",downColor:\\\"#ff6b81\\\",wickUpColor:\\\"#63e6be\\\",wickDownColor:\\\"#ff6b81\\\",borderVisible:false,priceFormat:{type:\\\"price\\\",precision:8,minMove:0.00000001}});\\n340:        series.setData(candles);\"},{\"line\":339,\"snippet\":\"337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\\\"#081823\\\"}:{color:\\\"#081823\\\"};\\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\\\"#8fa5b6\\\"},grid:{vertLines:{color:\\\"rgba(143,165,182,.08)\\\"},horzLines:{color:\\\"rgba(143,165,182,.08)\\\"}},rightPriceScale:{borderColor:\\\"rgba(143,165,182,.20)\\\"},timeScale:{borderColor:\\\"rgba(143,165,182,.20)\\\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\\n339:        const series=makeSeries(api,library,{upColor:\\\"#63e6be\\\",downColor:\\\"#ff6b81\\\",wickUpColor:\\\"#63e6be\\\",wickDownColor:\\\"#ff6b81\\\",borderVisible:false,priceFormat:{type:\\\"price\\\",precision:8,minMove:0.00000001}});\\n340:        series.setData(candles);\\n341:        api.timeScale().fitContent();\"},{\"line\":396,\"snippet\":\"394:</script>\\n395:\\n396:<style id=\\\"SHARP_MOVES_LAYOUT_V2\\\">.sharp-moves-panel{grid-column:1 / -1;justify-self:center;width:min(100%,860px);margin-inline:auto}.sharp-moves-panel .title{justify-content:center}.sharp-moves-panel .title h2{width:100%;text-align:center}#moves{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:12px!important}@media(max-width:750px){.sharp-moves-panel{width:100%}#moves{grid-template-columns:1fr!important}}</style>\\n397:<script id=\\\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\\\">(()=>{const move=()=>{const chart=document.getElementById(\\\"chart\\\");const panel=chart&&chart.closest(\\\".panel\\\");const main=document.querySelector(\\\"main\\\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\\\"1 / -1\\\";const title=document.getElementById(\\\"chartTitle\\\");if(title)title.textContent=\\\"Графики монет\\\";const note=panel.querySelector(\\\".muted\\\");if(note)note.textContent=\\\"Свечи Binance · 1 месяц · весь доступный период\\\"};document.addEventListener(\\\"DOMContentLoaded\\\",move);window.addEventListener(\\\"load\\\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>\"},{\"line\":397,\"snippet\":\"395:\\n396:<style id=\\\"SHARP_MOVES_LAYOUT_V2\\\">.sharp-moves-panel{grid-column:1 / -1;justify-self:center;width:min(100%,860px);margin-inline:auto}.sharp-moves-panel .title{justify-content:center}.sharp-moves-panel .title h2{width:100%;text-align:center}#moves{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:12px!important}@media(max-width:750px){.sharp-moves-panel{width:100%}#moves{grid-template-columns:1fr!important}}</style>\\n397:<script id=\\\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\\\">(()=>{const move=()=>{const chart=document.getElementById(\\\"chart\\\");const panel=chart&&chart.closest(\\\".panel\\\");const main=document.querySelector(\\\"main\\\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\\\"1 / -1\\\";const title=document.getElementById(\\\"chartTitle\\\");if(title)title.textContent=\\\"Графики монет\\\";const note=panel.querySelector(\\\".muted\\\");if(note)note.textContent=\\\"Свечи Binance · 1 месяц · весь доступный период\\\"};document.addEventListener(\\\"DOMContentLoaded\\\",move);window.addEventListener(\\\"load\\\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>\"}],\"moves\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":6,\"snippet\":\"4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\\n6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\"},{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"},{\"line\":396,\"snippet\":\"394:</script>\\n395:\\n396:<style id=\\\"SHARP_MOVES_LAYOUT_V2\\\">.sharp-moves-panel{grid-column:1 / -1;justify-self:center;width:min(100%,860px);margin-inline:auto}.sharp-moves-panel .title{justify-content:center}.sharp-moves-panel .title h2{width:100%;text-align:center}#moves{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:12px!important}@media(max-width:750px){.sharp-moves-panel{width:100%}#moves{grid-template-columns:1fr!important}}</style>\\n397:<script id=\\\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\\\">(()=>{const move=()=>{const chart=document.getElementById(\\\"chart\\\");const panel=chart&&chart.closest(\\\".panel\\\");const main=document.querySelector(\\\"main\\\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\\\"1 / -1\\\";const title=document.getElementById(\\\"chartTitle\\\");if(title)title.textContent=\\\"Графики монет\\\";const note=panel.querySelector(\\\".muted\\\");if(note)note.textContent=\\\"Свечи Binance · 1 месяц · весь доступный период\\\"};document.addEventListener(\\\"DOMContentLoaded\\\",move);window.addEventListener(\\\"load\\\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>\"}],\"portfolio\":[{\"line\":9,\"snippet\":\"7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"><span class=\\\\\\\"coin-symbol\\\\\\\">\\\"+state.coins[id]+\\\"</span><b class=\\\\\\\"coin-price\\\\\\\">\\\"+money(c.price,2)+\\\"</b></div><div class=\\\\\\\"\\\"+(ch>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+(ch>=0?\\\"+\\\":\\\"\\\")+ch.toFixed(2)+\\\"%</div><small class=\\\\\\\"muted\\\\\\\">Cap \\\"+compactMoney(c.market_cap)+\\\"</small></div>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\".coin\\\").forEach(x=>{let id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");document.querySelectorAll(\\\"#rows tr\\\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\\\"asset-cell\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon table-coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";td.prepend(img);let symbol=td.querySelector(\\\"b\\\");if(symbol)symbol.classList.add(\\\"asset-symbol\\\")});totalEl=total;document.getElementById(\\\"total\\\").textContent=money(total,0);invested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\"\"}],\"position\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"}],\"price\":[{\"line\":3,\"snippet\":\"1:<!doctype html>\\n2:<html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title>\\n3:<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head>\\n4:<body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\\\"top-actions\\\"><div class=\\\"currency-switch\\\"><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"USD\\\">USD</button><button type=\\\"button\\\" class=\\\"currency-btn\\\" data-currency=\\\"RUB\\\">RUB</button></div><form method=\\\"post\\\" action=\\\"/logout\\\"><button class=\\\"btn ghost\\\">Выйти</button></form></div></div>\\n5:<section class=\\\"grid4\\\"><div class=\\\"card\\\"><span class=\\\"muted\\\">Стоимость портфеля</span><strong id=\\\"total\\\">—</strong><span id=\\\"day\\\" class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section>\"},{\"line\":8,\"snippet\":\"6:<section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title coin-chart-title\\\"><div><h2>Графики монет</h2><span class=\\\"muted\\\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\\\"chartTitle\\\" class=\\\"legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></h2><span id=\\\"marketStatus\\\" class=\\\"status legacy-chart-sink\\\" aria-hidden=\\\"true\\\"></span><svg id=\\\"chart\\\" class=\\\"chart legacy-chart-sink\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\" aria-hidden=\\\"true\\\" focusable=\\\"false\\\"></svg><div id=\\\"coinCharts\\\" data-chart-ui=\\\"GRAM1_INTERACTIVE_ACCORDION_V7\\\"><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"HBAR\\\" open><summary class=\\\"tv-accordion-head\\\"><span>HBAR / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"POL\\\"><summary class=\\\"tv-accordion-head\\\"><span>POL / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"AVAX\\\"><summary class=\\\"tv-accordion-head\\\"><span>AVAX / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details><details name=\\\"coin-chart\\\" class=\\\"tv-accordion-item\\\" data-symbol=\\\"LINK\\\"><summary class=\\\"tv-accordion-head\\\"><span>LINK / USDT</span><span class=\\\"tv-accordion-chevron\\\" aria-hidden=\\\"true\\\">⌄</span></summary><div class=\\\"tv-accordion-body\\\"><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-message muted\\\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section>\\n7:<section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></table></div></section>\\n8:<section class=\\\"alerts\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Новое оповещение</h2></div><div class=\\\"form\\\"><select id=\\\"aCoin\\\"></select><select id=\\\"aKind\\\"><option value=\\\"price_above\\\">Цена выше</option><option value=\\\"price_below\\\">Цена ниже</option><option value=\\\"cap_above\\\">Капитализация выше</option><option value=\\\"cap_below\\\">Капитализация ниже</option><option value=\\\"pump\\\">Резкий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main>\\n9:<script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function loa"
}
