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

53 lines
383 KiB
Plaintext

CHAT_OUTPUT_BEGIN
COMMAND_ID=GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z
STATUS=FAIL
RC=1
HOST=pve01
MODE=verify
COMPONENT=gram1-site-usdc-v42-read-only-exact-scope
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83
COMMAND_SHA256=d45849dba99e656e8b65c0dc0834484e88107721f29aba54e114a3391a04ee3c
DUPLICATE_FAILED_COMMAND_BLOCKED=false
EXECUTION_STARTED=true
CHANGE_DECLARED=false
RESULT_CONTRACT_VALID=true
RESULT_CONTRACT_STATUS=NOT_APPLICABLE
RESULT_CONTRACT_ERROR=NONE
COMMAND_RC=1
CHANGES_MADE=false
ROLLBACK_STARTED=false
ROLLBACK_RESTORED=null
MUTATION_OUTCOME=NO_MUTATION
SANITIZED=yes
SECRETS_INCLUDED=no
PRIVATE_ADDRESSES_INCLUDED=no
RAW_EVIDENCE_SHA256=6cefc9b683a16aea837b45b3ef4ced41c011affea3a48284bceeb011d74a380c
SANITIZED_OUTPUT_SHA256=6cefc9b683a16aea837b45b3ef4ced41c011affea3a48284bceeb011d74a380c
OUTPUT_BEGIN
HOMELAB_RESULT_CONTRACT={"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z","command_rc":0,"control_plane_error":null,"mutation_outcome":"NO_MUTATION","rc":0,"remote_rc":0,"remote_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","remote_stdout_sha256":"5354dc705bf2fb5d001be3c31f3883698e4f0c13fe206990ec9836f983613cde","report_sha256":"9ff272c7633669033df260a4631e6678d84ec5e468e7db1348f731bfe30330d6","rollback_restored":false,"rollback_started":false,"schema":1,"source_count":3,"status":"OK"}
USDC_EXACT_SCOPE_REPORT_BEGIN
{"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":"/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"},{"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"}],"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 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":114,"snippet":"112: let loading=false;\n113: const kindNames={\n114: price_above:\"цена ≥\",\n115: price_below:\"цена ≤\",\n116: cap_above:\"капитализация ≥\","},{"line":115,"snippet":"113: const kindNames={\n114: price_above:\"цена ≥\",\n115: price_below:\"цена ≤\",\n116: cap_above:\"капитализация ≥\",\n117: cap_below:\"капитализация ≤\","},{"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":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();"}],"quantity":[{"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":153,"snippet":"151: 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);\n155: const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");"},{"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\";"}],"symbol":[{"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":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":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":235,"snippet":"233: \"use strict\";\n234: const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";\n235: const symbols=[\"HBAR\",\"POL\",\"AVAX\",\"LINK\"];\n236: const charts=new Map();\n237: const requests=new Map();"},{"line":280,"snippet":"278: return dedup;\n279: };\n280: const loadCandles=symbol=>{\n281: if(requests.has(symbol))return requests.get(symbol);\n282: const task=(async()=>{"},{"line":281,"snippet":"279: };\n280: const loadCandles=symbol=>{\n281: if(requests.has(symbol))return requests.get(symbol);\n282: const task=(async()=>{\n283: const response=await fetch(\"/api/klines/\"+encodeURIComponent(symbol)+\"?limit=1000&interval=1M\",{cache:\"no-store\",credentials:\"same-origin\",headers:{Accept:\"application/json\"}});"},{"line":283,"snippet":"281: if(requests.has(symbol))return requests.get(symbol);\n282: const task=(async()=>{\n283: const response=await fetch(\"/api/klines/\"+encodeURIComponent(symbol)+\"?limit=1000&interval=1M\",{cache:\"no-store\",credentials:\"same-origin\",headers:{Accept:\"application/json\"}});\n284: if(response.status===401||response.status===403){location.href=\"/login\";throw new Error(\"Требуется повторный вход\");}\n285: if(!response.ok)throw new Error(\"HTTP \"+response.status);"},{"line":291,"snippet":"289: return normalizeCandles(raw);\n290: })();\n291: requests.set(symbol,task);\n292: task.catch(()=>requests.delete(symbol));\n293: return task;"},{"line":292,"snippet":"290: })();\n291: requests.set(symbol,task);\n292: task.catch(()=>requests.delete(symbol));\n293: return task;\n294: };"},{"line":319,"snippet":"317: };\n318: const createChart=async item=>{\n319: const symbol=item.dataset.symbol;\n320: if(item.dataset.chartState===\"ready\"){\n321: const instance=charts.get(symbol);"},{"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":333,"snippet":"331: item._chartPromise=(async()=>{\n332: try{\n333: const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\n334: await afterLayout();\n335: if(!item.open){item.dataset.chartState=\"idle\";message.textContent=\"Нажмите, чтобы открыть график\";return null;}"},{"line":345,"snippet":"343: const observer=typeof ResizeObserver===\"function\"?new ResizeObserver(resize):null;\n344: 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\";"},{"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":348,"snippet":"346: charts.set(symbol,instance);\n347: item.dataset.chartState=\"ready\";\n348: message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\n349: return instance;\n350: }catch(error){"},{"line":352,"snippet":"350: }catch(error){\n351: item.dataset.chartState=\"error\";\n352: message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353: console.error(\"Gram1 chart error\",symbol,error);\n354: return null;"},{"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":366,"snippet":"364: root.dataset.chartUi=VERSION;\n365: const items=[...root.querySelectorAll(\"details.tv-accordion-item\")];\n366: if(items.length!==symbols.length)throw new Error(\"Некорректное количество секций графиков\");\n367: let syncing=false;\n368: const openOnly=item=>{"}],"total":[{"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ч\""}]},"line_count":397,"mode":"0o644","path":"templates/index.html","redactions":0,"sanitized_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","size":38036,"structure":{"api":["/api/alert","/api/alert/","/api/holding","/api/klines/","/api/state"],"functions":[{"line":9,"name":"load"},{"line":9,"name":"render"},{"line":9,"name":"bindInputs"},{"line":9,"name":"draw"},{"line":9,"name":"renderMoves"},{"line":9,"name":"label"},{"line":9,"name":"renderAlerts"},{"line":9,"name":"fx"},{"line":9,"name":"money"},{"line":9,"name":"coin"},{"line":11,"name":"install"},{"line":28,"name":"ensureOptions"},{"line":121,"name":"activeCurrency"},{"line":123,"name":"converted"},{"line":124,"name":"alertMoney"},{"line":125,"name":"make"},{"line":131,"name":"alertLabel"},{"line":176,"name":"refresh"},{"line":192,"name":"install"},{"line":213,"name":"redrawForCurrency"},{"line":239,"name":"afterLayout"},{"line":282,"name":"task"},{"line":295,"name":"waitLibrary"},{"line":303,"name":"makeSeries"},{"line":308,"name":"hideLegacy"},{"line":342,"name":"resize"},{"line":359,"name":"init"},{"line":397,"name":"move"}],"ids":["ALERT_CREATE_OVERRIDE_V3","ALERT_LIST_RENDERER_V5","GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7","GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7","MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1","SHARP_MOVES_LAYOUT_V2","aCoin","aEmail","aKind","aThreshold","aWindow","addAlert","alertCreateStatusV3","alertList","avgHeader","chart","chartTitle","coinCharts","coins","day","invested","marketStatus","moves","pnl","pnlp","rows","total","updated"]},"uid":0}],"sqlite":[{"path":"data/portfolio.db","quick_check":"ok","sha256":"742320dcfffa4523e846cb71c2a6af9602ca7ed2853d95ea23c1e23e64349d01","size":1716224,"tables":[{"columns":[{"name":"id","notnull":0,"pk":1,"type":"INTEGER"},{"name":"coin_id","notnull":1,"pk":0,"type":"TEXT"},{"name":"kind","notnull":1,"pk":0,"type":"TEXT"},{"name":"threshold","notnull":1,"pk":0,"type":"REAL"},{"name":"window_seconds","notnull":1,"pk":0,"type":"INTEGER"},{"name":"email","notnull":1,"pk":0,"type":"TEXT"},{"name":"enabled","notnull":1,"pk":0,"type":"INTEGER"},{"name":"latched","notnull":1,"pk":0,"type":"INTEGER"},{"name":"last_triggered","notnull":0,"pk":0,"type":"INTEGER"}],"row_count":4,"symbols":{},"table":"alerts","usdc_rows":{}},{"columns":[{"name":"coin_id","notnull":0,"pk":1,"type":"TEXT"},{"name":"quantity","notnull":1,"pk":0,"type":"REAL"},{"name":"avg_buy","notnull":1,"pk":0,"type":"REAL"}],"row_count":4,"symbols":{},"table":"holdings","usdc_rows":{}},{"columns":[{"name":"ts","notnull":1,"pk":1,"type":"INTEGER"},{"name":"coin_id","notnull":1,"pk":2,"type":"TEXT"},{"name":"price","notnull":1,"pk":0,"type":"REAL"},{"name":"market_cap","notnull":1,"pk":0,"type":"REAL"},{"name":"volume","notnull":1,"pk":0,"type":"REAL"}],"row_count":14378,"symbols":{},"table":"market_history","usdc_rows":{}}]}]}
USDC_EXACT_SCOPE_REPORT_END
SOURCE_GZIP_B64_BEGIN {"chunks":3,"gzip_sha256":"e40905e690835200b2ebda03fc54e6c43aca829b08d26fe5982dc703be8c4d51","original_sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","path":"app.py","redactions":1,"sanitized_sha256":"b6105195f16c4efb725a7629fd2d485d2c97cfab21fcb94344925d1a2dcf8bb2"}
SOURCE_GZIP_B64_CHUNK 0001/0003 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
SOURCE_GZIP_B64_CHUNK 0002/0003 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
SOURCE_GZIP_B64_CHUNK 0003/0003 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
SOURCE_GZIP_B64_END app.py
SOURCE_GZIP_B64_BEGIN {"chunks":1,"gzip_sha256":"49befa11ea71cd7a4c7d0a41e10d092232ecf6a2438c3311495ea36b7dcc1cda","original_sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","path":"worker.py","redactions":0,"sanitized_sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c"}
SOURCE_GZIP_B64_CHUNK 0001/0001 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
SOURCE_GZIP_B64_END worker.py
SOURCE_GZIP_B64_BEGIN {"chunks":3,"gzip_sha256":"ea0cc3667dce3284eb67783fdec57d3077fef09e400a37f0d11ed4b59aad6479","original_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","path":"templates/index.html","redactions":0,"sanitized_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2"}
SOURCE_GZIP_B64_CHUNK 0001/0003 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
SOURCE_GZIP_B64_CHUNK 0002/0003 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
SOURCE_GZIP_B64_CHUNK 0003/0003 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
SOURCE_GZIP_B64_END templates/index.html
{"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z","command_rc":0,"control_plane_error":null,"mutation_outcome":"NO_MUTATION","rc":0,"remote_rc":0,"remote_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","remote_stdout_sha256":"5354dc705bf2fb5d001be3c31f3883698e4f0c13fe206990ec9836f983613cde","report_sha256":"9ff272c7633669033df260a4631e6678d84ec5e468e7db1348f731bfe30330d6","rollback_restored":false,"rollback_started":false,"schema":1,"source_count":3,"status":"OK"}
HOMELAB_RESULT_CONTRACT={"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z","command_rc":1,"control_plane_error":null,"error":"0","error_type":"SystemExit","mutation_outcome":"NO_MUTATION","rc":1,"rollback_restored":false,"rollback_started":false,"schema":1,"status":"FAIL"}
{"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z","command_rc":1,"control_plane_error":null,"error":"0","error_type":"SystemExit","mutation_outcome":"NO_MUTATION","rc":1,"rollback_restored":false,"rollback_started":false,"schema":1,"status":"FAIL"}
OUTPUT_END
CHAT_OUTPUT_END