1
0
Files
homelab-public-context/runtime/latest.json
T

39 lines
318 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"schema_version": 1,
"channel": "homelab-runtime",
"command_id": "GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z",
"status": "FAIL",
"rc": 1,
"host": "pve01",
"mode": "verify",
"component": "gram1-site-usdc-v42-read-only-exact-scope",
"started_at_utc": "2026-08-02T14:04:48Z",
"finished_at_utc": "2026-08-02T14:04:48Z",
"reference_register_checked": true,
"reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e",
"error_register_checked": true,
"error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83",
"command_sha256": "54ae5729223f6a8183e916ce1cca935d919e30ea9be4fb136555519824fedb6b",
"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": "5f923ab9e75640a7e7fa95144ff2431810889f25caeb17a2acfaa27314ed848a",
"sanitized_output_sha256": "5f923ab9e75640a7e7fa95144ff2431810889f25caeb17a2acfaa27314ed848a",
"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-R3-20260802T140000Z\",\"command_rc\":0,\"control_plane_error\":null,\"mutation_outcome\":\"NO_MUTATION\",\"rc\":0,\"remote_rc\":0,\"remote_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"remote_stdout_sha256\":\"c7b4dca0bc02e3e303214ce7ff49f252a914a1fb0db74bb0496a9e140f28cec5\",\"report_sha256\":\"0791be77ab77aad39a24810e3f850c07f83f3635941b683de983e477af1ce037\",\"rollback_restored\":false,\"rollback_started\":false,\"source_count\":3,\"status\":\"OK\",\"version\":1}\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 = \\\"KCgpPT57CiAgInVzZSBzdHJpY3QiOwogIGNvbnN0IFZFUlNJT049IkdSQU0xX0NIQVJUU19FWFRFUk5BTF9WNCI7CiAgY29uc3Qgc3ltYm9scz1bIkhCQVIiLCJQT0wiLCJBVkFYIiwiTElOSyJdOwogIGNvbnN0IGNoYXJ0cz1uZXcgTWFwKCk7CiAgY29uc3QgY2FuZGxlQ2FjaGU9bmV3IE1hcCgpOwogIGNvbnN0IHNsZWVwPW1zPT5uZXcgUHJvbWlzZShyZXNvbHZlPT5zZXRUaW1lb3V0KHJlc29sdmUsbXMpKTsKICBjb25zdCBhZnRlckxheW91dD0oKT0+bmV3IFByb21pc2UocmVzb2x2ZT0+cmVxdWVzdEFuaW1hdGlvbkZyYW1lKCgpPT5yZXF1ZXN0QW5pbWF0aW9uRnJhbWUocmVzb2x2ZSkpKTsKICBjb25zdCB0aW1lS2V5PXZhbHVlPT57CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ibnVtYmVyIilyZXR1cm4gdmFsdWU7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIil7CiAgICAgIGNvbnN0IG51bWVyaWM9TnVtYmVyKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUobnVtZXJpYykpcmV0dXJuIG51bWVyaWM7CiAgICAgIGNvbnN0IHBhcnNlZD1EYXRlLnBhcnNlKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSlyZXR1cm4gTWF0aC5mbG9vcihwYXJzZWQvMTAwMCk7CiAgICB9CiAgICBpZih2YWx1ZSYmdHlwZW9mIHZhbHVlPT09Im9iamVjdCImJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUueWVhcikpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLm1vbnRoKSkmJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUuZGF5KSkpcmV0dXJuIERhdGUuVVRDKE51bWJlcih2YWx1ZS55ZWFyKSxOdW1iZXIodmFsdWUubW9udGgpLTEsTnVtYmVyKHZhbHVlLmRheSkpLzEwMDA7CiAgICByZXR1cm4gTmFOOwogIH07CiAgY29uc3Qgbm9ybWFsaXplVGltZT12YWx1ZT0+ewogICAgaWYodHlwZW9mIHZhbHVlPT09InN0cmluZyImJi9eXGQrJC8udGVzdCh2YWx1ZSkpdmFsdWU9TnVtYmVyKHZhbHVlKTsKICAgIGlmKHR5cGVvZiB2YWx1ZT09PSJudW1iZXIiJiZOdW1iZXIuaXNGaW5pdGUodmFsdWUpKXJldHVybiBNYXRoLmZsb29yKHZhbHVlPjFlMTE/dmFsdWUvMTAwMDp2YWx1ZSk7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIiYmL15cZHs0fS1cZHsyfS1cZHsyfS8udGVzdCh2YWx1ZSkpcmV0dXJuIHZhbHVlLnNsaWNlKDAsMTApOwogICAgaWYodmFsdWUmJnR5cGVvZiB2YWx1ZT09PSJvYmplY3QiJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLnllYXIpKSYmTnVtYmVyLmlzRmluaXRlKE51bWJlcih2YWx1ZS5tb250aCkpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLmRheSkpKXJldHVybiB7eWVhcjpOdW1iZXIodmFsdWUueWVhciksbW9udGg6TnVtYmVyKHZhbHVlLm1vbnRoKSxkYXk6TnVtYmVyKHZhbHVlLmRheSl9OwogICAgcmV0dXJuIG51bGw7CiAgfTsKICBjb25zdCBub3JtYWxpemVDYW5kbGVzPXJhdz0+ewogICAgY29uc3Qgcm93cz1bXTsKICAgIGZvcihjb25zdCBpdGVtIG9mIEFycmF5LmlzQXJyYXkocmF3KT9yYXc6W10pewogICAgICBjb25zdCBpc0FycmF5PUFycmF5LmlzQXJyYXkoaXRlbSk7CiAgICAgIGNvbnN0IHRpbWU9bm9ybWFsaXplVGltZShpc0FycmF5P2l0ZW1bMF06KGl0ZW0mJihpdGVtLnRpbWU/P2l0ZW0ub3Blbl90aW1lPz9pdGVtLm9wZW5UaW1lKSkpOwogICAgICBjb25zdCBvcGVuPU51bWJlcihpc0FycmF5P2l0ZW1bMV06aXRlbSYmaXRlbS5vcGVuKSxoaWdoPU51bWJlcihpc0FycmF5P2l0ZW1bMl06aXRlbSYmaXRlbS5oaWdoKSxsb3c9TnVtYmVyKGlzQXJyYXk/aXRlbVszXTppdGVtJiZpdGVtLmxvdyksY2xvc2U9TnVtYmVyKGlzQXJyYXk/aXRlbVs0XTppdGVtJiZpdGVtLmNsb3NlKTsKICAgICAgaWYodGltZT09PW51bGx8fCFbb3BlbixoaWdoLGxvdyxjbG9zZV0uZXZlcnkoTnVtYmVyLmlzRmluaXRlKSljb250aW51ZTsKICAgICAgcm93cy5wdXNoKHt0aW1lLG9wZW4saGlnaCxsb3csY2xvc2V9KTsKICAgIH0KICAgIHJvd3Muc29ydCgoYSxiKT0+dGltZUtleShhLnRpbWUpLXRpbWVLZXkoYi50aW1lKSk7CiAgICBjb25zdCBkZWR1cD1bXTsKICAgIGZvcihjb25zdCByb3cgb2Ygcm93cyl7Y29uc3Qga2V5PVN0cmluZyh0aW1lS2V5KHJvdy50aW1lKSk7aWYoZGVkdXAubGVuZ3RoJiZTdHJpbmcodGltZUtleShkZWR1cFtkZWR1cC5sZW5ndGgtMV0udGltZSkpPT09a2V5KWRlZHVwW2RlZHVwLmxlbmd0aC0xXT1yb3c7ZWxzZSBkZWR1cC5wdXNoKHJvdyl9CiAgICBpZighZGVkdXAubGVuZ3RoKXRocm93IG5ldyBFcnJvcigiQVBJINCy0LXRgNC90YPQuyDQtNCw0L3QvdGL0LUg0LHQtdC3INC60L7RgNGA0LXQutGC0L3Ri9GFIE9ITEMt0YHQstC10YfQtdC5Iik7CiAgICByZXR1cm4gZGVkdXA7CiAgfTsKICBjb25zdCBsb2FkQ2FuZGxlcz1hc3luYyBzeW1ib2w9PnsKICAgIGlmKGNhbmRsZUNhY2hlLmhhcyhzeW1ib2wpKXJldHVybiBjYW5kbGVDYWNoZS5nZXQoc3ltYm9sKTsKICAgIGNvbnN0IHRhc2s9KGFzeW5jKCk9PnsKICAgICAgY29uc3QgcmVzcG9uc2U9YXdhaXQgZmV0Y2goIi9hcGkva2xpbmVzLyIrZW5jb2RlVVJJQ29tcG9uZW50KHN5bWJvbCkrIj9saW1pdD0xMDAwJmludGVydmFsPTFNIix7Y2FjaGU6Im5vLXN0b3JlIixjcmVkZW50aWFsczoic2FtZS1vcmlnaW4iLGhlYWRlcnM6e0FjY2VwdDoiYXBwbGljYXRpb24vanNvbiJ9fSk7CiAgICAgIGlmKHJlc3BvbnNlLnN0YXR1cz09PTQwMXx8cmVzcG9uc2Uuc3RhdHVzPT09NDAzKXtsb2NhdGlvbj0iL2xvZ2luIjt0aHJvdyBuZXcgRXJyb3IoItCi0YDQtdCx0YPQtdGC0YHRjyDQv9C+0LLRgtC+0YDQvdGL0Lkg0LLRhdC+0LQiKX0KICAgICAgaWYoIXJlc3BvbnNlLm9rKXRocm93IG5ldyBFcnJvcigiSFRUUCAiK3Jlc3BvbnNlLnN0YXR1cyk7CiAgICAgIGNvbnN0IGRhdGE9YXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgICBpZihkYXRhJiZkYXRhLnN0YXR1cyYmZGF0YS5zdGF0dXMhPT0iT0siKXRocm93IG5ldyBFcnJvcihTdHJpbmcoZGF0YS5lcnJvcnx8ZGF0YS5tZXNzYWdlfHxkYXRhLnN0YXR1cykpOwogICAgICBjb25zdCByYXc9QXJyYXkuaXNBcnJheShkYXRhJiZkYXRhLmNhbmRsZXMpP2RhdGEuY2FuZGxlczooZGF0YSYmZGF0YS5yZXN1bHQmJkFycmF5LmlzQXJyYXkoZGF0YS5yZXN1bHQuY2FuZGxlcyk/ZGF0YS5yZXN1bHQuY2FuZGxlczpbXSk7CiAgICAgIHJldHVybiBub3JtYWxpemVDYW5kbGVzKHJhdyk7CiAgICB9KSgpOwogICAgY2FuZGxlQ2FjaGUuc2V0KHN5bWJvbCx0YXNrKTt0cnl7cmV0dXJuIGF3YWl0IHRhc2t9Y2F0Y2goZXJyb3Ipe2NhbmRsZUNhY2hlLmRlbGV0ZShzeW1ib2wpO3Rocm93IGVycm9yfQogIH07CiAgY29uc3Qgd2lkdGhPZj1ib3g9Pk1hdGgubWF4KDMyMCxNYXRoLmZsb29yKGJveC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKS53aWR0aHx8Ym94LmNsaWVudFdpZHRofHw5MDApKTsKICBjb25zdCB3YWl0TGlicmFyeT1hc3luYygpPT57Zm9yKGxldCBuPTA7bjwxMDA7bisrKXtpZih3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMmJnR5cGVvZiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMuY3JlYXRlQ2hhcnQ9PT0iZnVuY3Rpb24iKXJldHVybiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHM7YXdhaXQgc2xlZXAoMTAwKX10aHJvdyBuZXcgRXJyb3IoItCb0L7QutCw0LvRjNC90LDRjyDQsdC\"},{\"line\":407,\"snippet\":\"405:\\n406:_GRAM1_CHART_JS_B64 = \\\"KCgpPT57CiAgInVzZSBzdHJpY3QiOwogIGNvbnN0IFZFUlNJT049IkdSQU0xX0NIQVJUU19FWFRFUk5BTF9WNCI7CiAgY29uc3Qgc3ltYm9scz1bIkhCQVIiLCJQT0wiLCJBVkFYIiwiTElOSyJdOwogIGNvbnN0IGNoYXJ0cz1uZXcgTWFwKCk7CiAgY29uc3QgY2FuZGxlQ2FjaGU9bmV3IE1hcCgpOwogIGNvbnN0IHNsZWVwPW1zPT5uZXcgUHJvbWlzZShyZXNvbHZlPT5zZXRUaW1lb3V0KHJlc29sdmUsbXMpKTsKICBjb25zdCBhZnRlckxheW91dD0oKT0+bmV3IFByb21pc2UocmVzb2x2ZT0+cmVxdWVzdEFuaW1hdGlvbkZyYW1lKCgpPT5yZXF1ZXN0QW5pbWF0aW9uRnJhbWUocmVzb2x2ZSkpKTsKICBjb25zdCB0aW1lS2V5PXZhbHVlPT57CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ibnVtYmVyIilyZXR1cm4gdmFsdWU7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIil7CiAgICAgIGNvbnN0IG51bWVyaWM9TnVtYmVyKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUobnVtZXJpYykpcmV0dXJuIG51bWVyaWM7CiAgICAgIGNvbnN0IHBhcnNlZD1EYXRlLnBhcnNlKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSlyZXR1cm4gTWF0aC5mbG9vcihwYXJzZWQvMTAwMCk7CiAgICB9CiAgICBpZih2YWx1ZSYmdHlwZW9mIHZhbHVlPT09Im9iamVjdCImJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUueWVhcikpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLm1vbnRoKSkmJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUuZGF5KSkpcmV0dXJuIERhdGUuVVRDKE51bWJlcih2YWx1ZS55ZWFyKSxOdW1iZXIodmFsdWUubW9udGgpLTEsTnVtYmVyKHZhbHVlLmRheSkpLzEwMDA7CiAgICByZXR1cm4gTmFOOwogIH07CiAgY29uc3Qgbm9ybWFsaXplVGltZT12YWx1ZT0+ewogICAgaWYodHlwZW9mIHZhbHVlPT09InN0cmluZyImJi9eXGQrJC8udGVzdCh2YWx1ZSkpdmFsdWU9TnVtYmVyKHZhbHVlKTsKICAgIGlmKHR5cGVvZiB2YWx1ZT09PSJudW1iZXIiJiZOdW1iZXIuaXNGaW5pdGUodmFsdWUpKXJldHVybiBNYXRoLmZsb29yKHZhbHVlPjFlMTE/dmFsdWUvMTAwMDp2YWx1ZSk7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIiYmL15cZHs0fS1cZHsyfS1cZHsyfS8udGVzdCh2YWx1ZSkpcmV0dXJuIHZhbHVlLnNsaWNlKDAsMTApOwogICAgaWYodmFsdWUmJnR5cGVvZiB2YWx1ZT09PSJvYmplY3QiJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLnllYXIpKSYmTnVtYmVyLmlzRmluaXRlKE51bWJlcih2YWx1ZS5tb250aCkpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLmRheSkpKXJldHVybiB7eWVhcjpOdW1iZXIodmFsdWUueWVhciksbW9udGg6TnVtYmVyKHZhbHVlLm1vbnRoKSxkYXk6TnVtYmVyKHZhbHVlLmRheSl9OwogICAgcmV0dXJuIG51bGw7CiAgfTsKICBjb25zdCBub3JtYWxpemVDYW5kbGVzPXJhdz0+ewogICAgY29uc3Qgcm93cz1bXTsKICAgIGZvcihjb25zdCBpdGVtIG9mIEFycmF5LmlzQXJyYXkocmF3KT9yYXc6W10pewogICAgICBjb25zdCBpc0FycmF5PUFycmF5LmlzQXJyYXkoaXRlbSk7CiAgICAgIGNvbnN0IHRpbWU9bm9ybWFsaXplVGltZShpc0FycmF5P2l0ZW1bMF06KGl0ZW0mJihpdGVtLnRpbWU/P2l0ZW0ub3Blbl90aW1lPz9pdGVtLm9wZW5UaW1lKSkpOwogICAgICBjb25zdCBvcGVuPU51bWJlcihpc0FycmF5P2l0ZW1bMV06aXRlbSYmaXRlbS5vcGVuKSxoaWdoPU51bWJlcihpc0FycmF5P2l0ZW1bMl06aXRlbSYmaXRlbS5oaWdoKSxsb3c9TnVtYmVyKGlzQXJyYXk/aXRlbVszXTppdGVtJiZpdGVtLmxvdyksY2xvc2U9TnVtYmVyKGlzQXJyYXk/aXRlbVs0XTppdGVtJiZpdGVtLmNsb3NlKTsKICAgICAgaWYodGltZT09PW51bGx8fCFbb3BlbixoaWdoLGxvdyxjbG9zZV0uZXZlcnkoTnVtYmVyLmlzRmluaXRlKSljb250aW51ZTsKICAgICAgcm93cy5wdXNoKHt0aW1lLG9wZW4saGlnaCxsb3csY2xvc2V9KTsKICAgIH0KICAgIHJvd3Muc29ydCgoYSxiKT0+dGltZUtleShhLnRpbWUpLXRpbWVLZXkoYi50aW1lKSk7CiAgICBjb25zdCBkZWR1cD1bXTsKICAgIGZvcihjb25zdCByb3cgb2Ygcm93cyl7Y29uc3Qga2V5PVN0cmluZyh0aW1lS2V5KHJvdy50aW1lKSk7aWYoZGVkdXAubGVuZ3RoJiZTdHJpbmcodGltZUtleShkZWR1cFtkZWR1cC5sZW5ndGgtMV0udGltZSkpPT09a2V5KWRlZHVwW2RlZHVwLmxlbmd0aC0xXT1yb3c7ZWxzZSBkZWR1cC5wdXNoKHJvdyl9CiAgICBpZighZGVkdXAubGVuZ3RoKXRocm93IG5ldyBFcnJvcigiQVBJINCy0LXRgNC90YPQuyDQtNCw0L3QvdGL0LUg0LHQtdC3INC60L7RgNGA0LXQutGC0L3Ri9GFIE9ITEMt0YHQstC10YfQtdC5Iik7CiAgICByZXR1cm4gZGVkdXA7CiAgfTsKICBjb25zdCBsb2FkQ2FuZGxlcz1hc3luYyBzeW1ib2w9PnsKICAgIGlmKGNhbmRsZUNhY2hlLmhhcyhzeW1ib2wpKXJldHVybiBjYW5kbGVDYWNoZS5nZXQoc3ltYm9sKTsKICAgIGNvbnN0IHRhc2s9KGFzeW5jKCk9PnsKICAgICAgY29uc3QgcmVzcG9uc2U9YXdhaXQgZmV0Y2goIi9hcGkva2xpbmVzLyIrZW5jb2RlVVJJQ29tcG9uZW50KHN5bWJvbCkrIj9saW1pdD0xMDAwJmludGVydmFsPTFNIix7Y2FjaGU6Im5vLXN0b3JlIixjcmVkZW50aWFsczoic2FtZS1vcmlnaW4iLGhlYWRlcnM6e0FjY2VwdDoiYXBwbGljYXRpb24vanNvbiJ9fSk7CiAgICAgIGlmKHJlc3BvbnNlLnN0YXR1cz09PTQwMXx8cmVzcG9uc2Uuc3RhdHVzPT09NDAzKXtsb2NhdGlvbj0iL2xvZ2luIjt0aHJvdyBuZXcgRXJyb3IoItCi0YDQtdCx0YPQtdGC0YHRjyDQv9C+0LLRgtC+0YDQvdGL0Lkg0LLRhdC+0LQiKX0KICAgICAgaWYoIXJlc3BvbnNlLm9rKXRocm93IG5ldyBFcnJvcigiSFRUUCAiK3Jlc3BvbnNlLnN0YXR1cyk7CiAgICAgIGNvbnN0IGRhdGE9YXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgICBpZihkYXRhJiZkYXRhLnN0YXR1cyYmZGF0YS5zdGF0dXMhPT0iT0siKXRocm93IG5ldyBFcnJvcihTdHJpbmcoZGF0YS5lcnJvcnx8ZGF0YS5tZXNzYWdlfHxkYXRhLnN0YXR1cykpOwogICAgICBjb25zdCByYXc9QXJyYXkuaXNBcnJheShkYXRhJiZkYXRhLmNhbmRsZXMpP2RhdGEuY2FuZGxlczooZGF0YSYmZGF0YS5yZXN1bHQmJkFycmF5LmlzQXJyYXkoZGF0YS5yZXN1bHQuY2FuZGxlcyk/ZGF0YS5yZXN1bHQuY2FuZGxlczpbXSk7CiAgICAgIHJldHVybiBub3JtYWxpemVDYW5kbGVzKHJhdyk7CiAgICB9KSgpOwogICAgY2FuZGxlQ2FjaGUuc2V0KHN5bWJvbCx0YXNrKTt0cnl7cmV0dXJuIGF3YWl0IHRhc2t9Y2F0Y2goZXJyb3Ipe2NhbmRsZUNhY2hlLmRlbGV0ZShzeW1ib2wpO3Rocm93IGVycm9yfQogIH07CiAgY29uc3Qgd2lkdGhPZj1ib3g9Pk1hdGgubWF4KDMyMCxNYXRoLmZsb29yKGJveC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKS53aWR0aHx8Ym94LmNsaWVudFdpZHRofHw5MDApKTsKICBjb25zdCB3YWl0TGlicmFyeT1hc3luYygpPT57Zm9yKGxldCBuPTA7bjwxMDA7bisrKXtpZih3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMmJnR5cGVvZiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMuY3JlYXRlQ2hhcnQ9PT0iZnVuY3Rpb24iKXJldHVybiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHM7YXdhaXQgc2xlZXAoMTAwKX10aHJvdyBuZXcgRXJyb3IoItCb0L7QutCw0LvRjNC90LDRjyDQsdC40LHQu9C40L7RgtC10LrQsCDQs9GA0LDRhNC40LrQvtCyINC90LUg0LfQsNCz\"},{\"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 = \\\"KCgpPT57CiAgInVzZSBzdHJpY3QiOwogIGNvbnN0IFZFUlNJT049IkdSQU0xX0NIQVJUU19FWFRFUk5BTF9WNCI7CiAgY29uc3Qgc3ltYm9scz1bIkhCQVIiLCJQT0wiLCJBVkFYIiwiTElOSyJdOwogIGNvbnN0IGNoYXJ0cz1uZXcgTWFwKCk7CiAgY29uc3QgY2FuZGxlQ2FjaGU9bmV3IE1hcCgpOwogIGNvbnN0IHNsZWVwPW1zPT5uZXcgUHJvbWlzZShyZXNvbHZlPT5zZXRUaW1lb3V0KHJlc29sdmUsbXMpKTsKICBjb25zdCBhZnRlckxheW91dD0oKT0+bmV3IFByb21pc2UocmVzb2x2ZT0+cmVxdWVzdEFuaW1hdGlvbkZyYW1lKCgpPT5yZXF1ZXN0QW5pbWF0aW9uRnJhbWUocmVzb2x2ZSkpKTsKICBjb25zdCB0aW1lS2V5PXZhbHVlPT57CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ibnVtYmVyIilyZXR1cm4gdmFsdWU7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIil7CiAgICAgIGNvbnN0IG51bWVyaWM9TnVtYmVyKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUobnVtZXJpYykpcmV0dXJuIG51bWVyaWM7CiAgICAgIGNvbnN0IHBhcnNlZD1EYXRlLnBhcnNlKHZhbHVlKTtpZihOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSlyZXR1cm4gTWF0aC5mbG9vcihwYXJzZWQvMTAwMCk7CiAgICB9CiAgICBpZih2YWx1ZSYmdHlwZW9mIHZhbHVlPT09Im9iamVjdCImJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUueWVhcikpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLm1vbnRoKSkmJk51bWJlci5pc0Zpbml0ZShOdW1iZXIodmFsdWUuZGF5KSkpcmV0dXJuIERhdGUuVVRDKE51bWJlcih2YWx1ZS55ZWFyKSxOdW1iZXIodmFsdWUubW9udGgpLTEsTnVtYmVyKHZhbHVlLmRheSkpLzEwMDA7CiAgICByZXR1cm4gTmFOOwogIH07CiAgY29uc3Qgbm9ybWFsaXplVGltZT12YWx1ZT0+ewogICAgaWYodHlwZW9mIHZhbHVlPT09InN0cmluZyImJi9eXGQrJC8udGVzdCh2YWx1ZSkpdmFsdWU9TnVtYmVyKHZhbHVlKTsKICAgIGlmKHR5cGVvZiB2YWx1ZT09PSJudW1iZXIiJiZOdW1iZXIuaXNGaW5pdGUodmFsdWUpKXJldHVybiBNYXRoLmZsb29yKHZhbHVlPjFlMTE/dmFsdWUvMTAwMDp2YWx1ZSk7CiAgICBpZih0eXBlb2YgdmFsdWU9PT0ic3RyaW5nIiYmL15cZHs0fS1cZHsyfS1cZHsyfS8udGVzdCh2YWx1ZSkpcmV0dXJuIHZhbHVlLnNsaWNlKDAsMTApOwogICAgaWYodmFsdWUmJnR5cGVvZiB2YWx1ZT09PSJvYmplY3QiJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLnllYXIpKSYmTnVtYmVyLmlzRmluaXRlKE51bWJlcih2YWx1ZS5tb250aCkpJiZOdW1iZXIuaXNGaW5pdGUoTnVtYmVyKHZhbHVlLmRheSkpKXJldHVybiB7eWVhcjpOdW1iZXIodmFsdWUueWVhciksbW9udGg6TnVtYmVyKHZhbHVlLm1vbnRoKSxkYXk6TnVtYmVyKHZhbHVlLmRheSl9OwogICAgcmV0dXJuIG51bGw7CiAgfTsKICBjb25zdCBub3JtYWxpemVDYW5kbGVzPXJhdz0+ewogICAgY29uc3Qgcm93cz1bXTsKICAgIGZvcihjb25zdCBpdGVtIG9mIEFycmF5LmlzQXJyYXkocmF3KT9yYXc6W10pewogICAgICBjb25zdCBpc0FycmF5PUFycmF5LmlzQXJyYXkoaXRlbSk7CiAgICAgIGNvbnN0IHRpbWU9bm9ybWFsaXplVGltZShpc0FycmF5P2l0ZW1bMF06KGl0ZW0mJihpdGVtLnRpbWU/P2l0ZW0ub3Blbl90aW1lPz9pdGVtLm9wZW5UaW1lKSkpOwogICAgICBjb25zdCBvcGVuPU51bWJlcihpc0FycmF5P2l0ZW1bMV06aXRlbSYmaXRlbS5vcGVuKSxoaWdoPU51bWJlcihpc0FycmF5P2l0ZW1bMl06aXRlbSYmaXRlbS5oaWdoKSxsb3c9TnVtYmVyKGlzQXJyYXk/aXRlbVszXTppdGVtJiZpdGVtLmxvdyksY2xvc2U9TnVtYmVyKGlzQXJyYXk/aXRlbVs0XTppdGVtJiZpdGVtLmNsb3NlKTsKICAgICAgaWYodGltZT09PW51bGx8fCFbb3BlbixoaWdoLGxvdyxjbG9zZV0uZXZlcnkoTnVtYmVyLmlzRmluaXRlKSljb250aW51ZTsKICAgICAgcm93cy5wdXNoKHt0aW1lLG9wZW4saGlnaCxsb3csY2xvc2V9KTsKICAgIH0KICAgIHJvd3Muc29ydCgoYSxiKT0+dGltZUtleShhLnRpbWUpLXRpbWVLZXkoYi50aW1lKSk7CiAgICBjb25zdCBkZWR1cD1bXTsKICAgIGZvcihjb25zdCByb3cgb2Ygcm93cyl7Y29uc3Qga2V5PVN0cmluZyh0aW1lS2V5KHJvdy50aW1lKSk7aWYoZGVkdXAubGVuZ3RoJiZTdHJpbmcodGltZUtleShkZWR1cFtkZWR1cC5sZW5ndGgtMV0udGltZSkpPT09a2V5KWRlZHVwW2RlZHVwLmxlbmd0aC0xXT1yb3c7ZWxzZSBkZWR1cC5wdXNoKHJvdyl9CiAgICBpZighZGVkdXAubGVuZ3RoKXRocm93IG5ldyBFcnJvcigiQVBJINCy0LXRgNC90YPQuyDQtNCw0L3QvdGL0LUg0LHQtdC3INC60L7RgNGA0LXQutGC0L3Ri9GFIE9ITEMt0YHQstC10YfQtdC5Iik7CiAgICByZXR1cm4gZGVkdXA7CiAgfTsKICBjb25zdCBsb2FkQ2FuZGxlcz1hc3luYyBzeW1ib2w9PnsKICAgIGlmKGNhbmRsZUNhY2hlLmhhcyhzeW1ib2wpKXJldHVybiBjYW5kbGVDYWNoZS5nZXQoc3ltYm9sKTsKICAgIGNvbnN0IHRhc2s9KGFzeW5jKCk9PnsKICAgICAgY29uc3QgcmVzcG9uc2U9YXdhaXQgZmV0Y2goIi9hcGkva2xpbmVzLyIrZW5jb2RlVVJJQ29tcG9uZW50KHN5bWJvbCkrIj9saW1pdD0xMDAwJmludGVydmFsPTFNIix7Y2FjaGU6Im5vLXN0b3JlIixjcmVkZW50aWFsczoic2FtZS1vcmlnaW4iLGhlYWRlcnM6e0FjY2VwdDoiYXBwbGljYXRpb24vanNvbiJ9fSk7CiAgICAgIGlmKHJlc3BvbnNlLnN0YXR1cz09PTQwMXx8cmVzcG9uc2Uuc3RhdHVzPT09NDAzKXtsb2NhdGlvbj0iL2xvZ2luIjt0aHJvdyBuZXcgRXJyb3IoItCi0YDQtdCx0YPQtdGC0YHRjyDQv9C+0LLRgtC+0YDQvdGL0Lkg0LLRhdC+0LQiKX0KICAgICAgaWYoIXJlc3BvbnNlLm9rKXRocm93IG5ldyBFcnJvcigiSFRUUCAiK3Jlc3BvbnNlLnN0YXR1cyk7CiAgICAgIGNvbnN0IGRhdGE9YXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgICBpZihkYXRhJiZkYXRhLnN0YXR1cyYmZGF0YS5zdGF0dXMhPT0iT0siKXRocm93IG5ldyBFcnJvcihTdHJpbmcoZGF0YS5lcnJvcnx8ZGF0YS5tZXNzYWdlfHxkYXRhLnN0YXR1cykpOwogICAgICBjb25zdCByYXc9QXJyYXkuaXNBcnJheShkYXRhJiZkYXRhLmNhbmRsZXMpP2RhdGEuY2FuZGxlczooZGF0YSYmZGF0YS5yZXN1bHQmJkFycmF5LmlzQXJyYXkoZGF0YS5yZXN1bHQuY2FuZGxlcyk/ZGF0YS5yZXN1bHQuY2FuZGxlczpbXSk7CiAgICAgIHJldHVybiBub3JtYWxpemVDYW5kbGVzKHJhdyk7CiAgICB9KSgpOwogICAgY2FuZGxlQ2FjaGUuc2V0KHN5bWJvbCx0YXNrKTt0cnl7cmV0dXJuIGF3YWl0IHRhc2t9Y2F0Y2goZXJyb3Ipe2NhbmRsZUNhY2hlLmRlbGV0ZShzeW1ib2wpO3Rocm93IGVycm9yfQogIH07CiAgY29uc3Qgd2lkdGhPZj1ib3g9Pk1hdGgubWF4KDMyMCxNYXRoLmZsb29yKGJveC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKS53aWR0aHx8Ym94LmNsaWVudFdpZHRofHw5MDApKTsKICBjb25zdCB3YWl0TGlicmFyeT1hc3luYygpPT57Zm9yKGxldCBuPTA7bjwxMDA7bisrKXtpZih3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMmJnR5cGVvZiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHMuY3JlYXRlQ2hhcnQ9PT0iZnVuY3Rpb24iKXJldHVybiB3aW5kb3cuTGlnaHR3ZWlnaHRDaGFydHM7YXdhaXQgc2xlZXAoMTAwKX10aHJvdyBuZXcgRXJyb3IoItCb0L7QutCw0LvRjNC90LDRjyDQsdC40LHQu9C40L7RgtC10LrQsCDQs9GA0LDRhNC40LrQvtCyINC90LUg0LfQsNCz\"},{\"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"
}