From 2fc70af7ad1fca3b4878261bcfefaee13539fd11 Mon Sep 17 00:00:00 2001 From: homelab-runtime-publisher Date: Sat, 1 Aug 2026 17:02:09 +0000 Subject: [PATCH] runtime: publish GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z --- ...EAD-ONLY-EXACT-SCOPE-20260801T165000Z.json | 38 ++++++++++++++ ...READ-ONLY-EXACT-SCOPE-20260801T165000Z.txt | 52 +++++++++++++++++++ runtime/latest.json | 24 ++++----- runtime/latest.txt | 38 ++++++++++---- 4 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 runtime/history/GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z.json create mode 100644 runtime/history/GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z.txt diff --git a/runtime/history/GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z.json b/runtime/history/GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z.json new file mode 100644 index 0000000..6640fd9 --- /dev/null +++ b/runtime/history/GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-20260801T165000Z.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "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", + "started_at_utc": "2026-08-01T17:02:08Z", + "finished_at_utc": "2026-08-01T17:02:09Z", + "reference_register_checked": true, + "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", + "error_register_checked": true, + "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", + "command_sha256": "d45849dba99e656e8b65c0dc0834484e88107721f29aba54e114a3391a04ee3c", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 1, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "6cefc9b683a16aea837b45b3ef4ced41c011affea3a48284bceeb011d74a380c", + "sanitized_output_sha256": "6cefc9b683a16aea837b45b3ef4ced41c011affea3a48284bceeb011d74a380c", + "output_truncated_in_json": true, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "HOMELAB_RESULT_CONTRACT={\"changes_made\":false,\"command_id\":\"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-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\"}\nUSDC_EXACT_SCOPE_REPORT_BEGIN\n{\"baseline_comparison\":{\"app.py\":{\"current\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\",\"matches\":true,\"previous\":\"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d\"},\"compose\":{\"current\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"matches\":true,\"previous\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\"},\"worker.py\":{\"current\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\",\"matches\":true,\"previous\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\"}},\"compose\":{\"environment_key_names\":[\"CRYPTO_DATA_DIR\",\"CRYPTO_EMAIL_CONFIG_FILE\",\"CRYPTO_PASSWORD_HASH_FILE\",\"CRYPTO_SESSION_SECRET_FILE\"],\"exists\":true,\"path\":\"compose.yaml\",\"redactions\":0,\"runtime\":{\"available\":true,\"config_services_rc\":0,\"containers\":[{\"env_names\":[\"CRYPTO_DATA_DIR\",\"CRYPTO_EMAIL_CONFIG_FILE\",\"CRYPTO_PASSWORD_HASH_FILE\",\"CRYPTO_SESSION_SECRET_FILE\",\"GPG_KEY\",\"LANG\",\"PATH\",\"PYTHONDONTWRITEBYTECODE\",\"PYTHONUNBUFFERED\",\"PYTHON_SHA256\",\"PYTHON_VERSION\",\"TZ\"],\"health\":null,\"id\":\"dc202c12ba81\",\"image\":\"crypto-portfolio-web\",\"image_id\":\"sha256:2a9339d475b2f4a01f060162f48a23b9590eb3ba4b036b46aad72adad540c40d\",\"mounts\":[{\"destination\":\"/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'', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\"},{\"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'{safe_label}'\"},{\"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'', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\\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'', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\"},{\"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'');parts.append(f'')\\n463: parts.append(f'{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей')\\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= 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= 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:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":8,\"snippet\":\"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n109:\\n217:\"},{\"line\":231,\"snippet\":\"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\\n230:\\n231:\\n232:\\n232:\\n217:\\n231:\\n232:\\n232:\"}],\"coin\":[{\"line\":3,\"snippet\":\"1:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":8,\"snippet\":\"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n217:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n395:\\n396:\\n397:\"},{\"line\":397,\"snippet\":\"395:\\n396:\\n397:\"}],\"moves\":[{\"line\":3,\"snippet\":\"1:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n395:\\n396:\\n397:\"}],\"portfolio\":[{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\n109:\n217:"},{"line":231,"snippet":"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\n230:\n231:\n232:\n232:\n217:\n231:\n232:\n232:"}],"coin":[{"line":3,"snippet":"1:\n2:Gram Crypto\n3:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":8,"snippet":"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n217:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n395:\n396:\n397:"},{"line":397,"snippet":"395:\n396:\n397:"}],"moves":[{"line":3,"snippet":"1:\n2:Gram Crypto\n3:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n395:\n396:\n397:"}],"portfolio":[{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\"},{\"line\":109,\"text\":\"\"},{\"line\":217,\"text\":\"\"},{\"line\":231,\"text\":\"\"},{\"line\":232,\"text\":\"\"}],\"sha256\":\"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2\"},\"worker\":{\"assignments\":[{\"end_line\":8,\"line\":8,\"literal\":null,\"names\":[\"MARKET\"],\"source\":\"MARKET = DATA / \\\"market.json\\\"\"},{\"end_line\":11,\"line\":11,\"literal\":{\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"hedera-hashgraph\":\"HBAR\",\"polygon-ecosystem-token\":\"POL\"},\"names\":[\"COINS\"],\"source\":\"COINS = {\\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\", \\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\"}\"},{\"end_line\":111,\"line\":111,\"literal\":null,\"names\":[\"quantity\"],\"source\":\"quantity = float(r[\\\"quantity\\\"])\"},{\"end_line\":112,\"line\":112,\"literal\":null,\"names\":[\"avg\"],\"source\":\"avg = float(r[\\\"avg_buy\\\"])\"},{\"end_line\":114,\"line\":114,\"literal\":null,\"names\":[\"invested\"],\"source\":\"invested = quantity * avg\"},{\"end_line\":123,\"line\":123,\"literal\":null,\"names\":[\"coin\"],\"source\":\"coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\"},{\"end_line\":128,\"line\":128,\"literal\":null,\"names\":[\"price\"],\"source\":\"price = float(coin[\\\"price\\\"])\"},{\"end_line\":179,\"line\":179,\"literal\":null,\"names\":[\"market\"],\"source\":\"market = fetch_market()\"}],\"relevant_functions\":[{\"end_line\":23,\"line\":14,\"name\":\"connect\",\"source\":\"def connect():\\n c = sqlite3.connect(DB, timeout=15)\\n c.row_factory = sqlite3.Row\\n c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n 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)\\\")\\n 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)\\\")\\n 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))\\\")\\n c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\\n c.commit()\\n return c\"},{\"end_line\":75,\"line\":41,\"name\":\"fetch_market\",\"source\":\"def fetch_market():\\n params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\\n r.raise_for_status()\\n rows = r.json()\\n now = int(time.time())\\n usd_rub = 0.0\\n try:\\n with open(MARKET, \\\"r\\\", encoding=\\\"utf-8\\\") as previous_file:\\n previous_rate = float((json.load(previous_file) or {}).get(\\\"usd_rub\\\") or 0)\\n if 40 <= previous_rate <= 200:\\n usd_rub = previous_rate\\n except Exception:\\n pass\\n try:\\n with httpx.Client(timeout=15, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as fx_client:\\n fx_data = fx_client.get(\\\"https://api.coingecko.com/api/v3/simple/price\\\", params={\\\"ids\\\":\\\"bitcoin\\\",\\\"vs_currencies\\\":\\\"usd,rub\\\"}).json()[\\\"bitcoin\\\"]\\n candidate = float(fx_data[\\\"rub\\\"]) / float(fx_data[\\\"usd\\\"])\\n if 40 <= candidate <= 200:\\n usd_rub = candidate\\n except Exception:\\n pass\\n if not 40 <= usd_rub <= 200:\\n usd_rub = 77.025093\\n result = {\\\"updated_at\\\":now,\\\"currency\\\":\\\"usd\\\",\\\"usd_rub\\\":usd_rub,\\\"coins\\\":{}}\\n\\n for x in rows:\\n cid = str(x.get(\\\"id\\\",\\\"\\\"))\\n if cid not in COINS:\\n continue\\n 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\\\")}\\n if len(result[\\\"coins\\\"]) != len(COINS):\\n raise RuntimeError(\\\"incomplete market response\\\")\\n return result\"},{\"end_line\":79,\"line\":77,\"name\":\"reference_price\",\"source\":\"def reference_price(c, coin_id, target_ts):\\n r = c.execute(\\\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\\\",(coin_id,target_ts)).fetchone()\\n return float(r[\\\"price\\\"]) if r else None\"},{\"end_line\":117,\"line\":107,\"name\":\"portfolio_text\",\"source\":\"def portfolio_text(c, coin_id, price):\\n r = c.execute(\\\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\\\",(coin_id,)).fetchone()\\n if not r:\\n return \\\"\\\"\\n quantity = float(r[\\\"quantity\\\"])\\n avg = float(r[\\\"avg_buy\\\"])\\n value = quantity * price\\n invested = quantity * avg\\n pnl = value - invested\\n pct = pnl / invested * 100 if invested else 0\\n return \\\"\\\\nКоличество монет: {:.8f}\\\\nТекущая стоимость позиции: ${:,.2f}\\\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\\\n\\\".format(quantity,value,pnl,pct)\"},{\"end_line\":175,\"line\":119,\"name\":\"evaluate\",\"source\":\"def evaluate(c, market):\\n now = int(time.time())\\n triggered = 0\\n for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n if not coin:\\n continue\\n kind = a[\\\"kind\\\"]\\n threshold = float(a[\\\"threshold\\\"])\\n price = float(coin[\\\"price\\\"])\\n cap = float(coin[\\\"market_cap\\\"])\\n latched = int(a[\\\"latched\\\"])\\n fired = False\\n reset = False\\n description = \\\"\\\"\\n if kind == \\\"price_above\\\":\\n fired = price >= threshold\\n reset = price < threshold * 0.99\\n description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n elif kind == \\\"price_below\\\":\\n fired = price <= threshold\\n reset = price > threshold * 1.01\\n description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n elif kind == \\\"cap_above\\\":\\n fired = cap >= threshold\\n reset = cap < threshold * 0.99\\n description = \\\"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n elif kind == \\\"cap_below\\\":\\n fired = cap <= threshold\\n reset = cap > threshold * 1.01\\n description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\\n if old and old > 0:\\n change = (price / old - 1) * 100\\n if kind == \\\"pump\\\":\\n fired = change >= threshold\\n reset = change < threshold * 0.5\\n description = \\\"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n else:\\n fired = change <= -threshold\\n reset = change > -threshold * 0.5\\n description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n if reset and latched:\\n c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",\"},{\"end_line\":190,\"line\":177,\"name\":\"cycle\",\"source\":\"def cycle():\\n DATA.mkdir(parents=True, exist_ok=True)\\n market = fetch_market()\\n now = int(market[\\\"updated_at\\\"])\\n with connect() as c:\\n for cid, x in market[\\\"coins\\\"].items():\\n 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\\\"]))\\n c.execute(\\\"DELETE FROM market_history WHERE ts', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\"},{\"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'{safe_label}'\"},{\"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'', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\\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'', '']\\n458: for n in range(5):\\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\"},{\"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'');parts.append(f'')\\n463: parts.append(f'{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей')\\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= 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= 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:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":8,\"snippet\":\"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n109:\\n217:\"},{\"line\":231,\"snippet\":\"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\\n230:\\n231:\\n232:\\n232:\\n217:\\n231:\\n232:\\n232:\"}],\"coin\":[{\"line\":3,\"snippet\":\"1:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":8,\"snippet\":\"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n217:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n395:\\n396:\\n397:\"},{\"line\":397,\"snippet\":\"395:\\n396:\\n397:\"}],\"moves\":[{\"line\":3,\"snippet\":\"1:\\n2:Gram Crypto\\n3:\\n4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\"},{\"line\":6,\"snippet\":\"4:
https://gram1.ru/

Мой криптопортфель

\\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\"},{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:\\n395:\\n396:\\n397:\"}],\"portfolio\":[{\"line\":9,\"snippet\":\"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\\n8:

Новое оповещение

Мои оповещения

\\n9:"},{"line":109,"text":""},{"line":217,"text":""},{"line":231,"text":""},{"line":232,"text":""}],"sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2"},"worker":{"assignments":[{"end_line":8,"line":8,"literal":null,"names":["MARKET"],"source":"MARKET = DATA / \"market.json\""},{"end_line":11,"line":11,"literal":{"avalanche-2":"AVAX","chainlink":"LINK","hedera-hashgraph":"HBAR","polygon-ecosystem-token":"POL"},"names":["COINS"],"source":"COINS = {\"hedera-hashgraph\": \"HBAR\", \"polygon-ecosystem-token\": \"POL\", \"avalanche-2\": \"AVAX\", \"chainlink\": \"LINK\"}"},{"end_line":111,"line":111,"literal":null,"names":["quantity"],"source":"quantity = float(r[\"quantity\"])"},{"end_line":112,"line":112,"literal":null,"names":["avg"],"source":"avg = float(r[\"avg_buy\"])"},{"end_line":114,"line":114,"literal":null,"names":["invested"],"source":"invested = quantity * avg"},{"end_line":123,"line":123,"literal":null,"names":["coin"],"source":"coin = market[\"coins\"].get(a[\"coin_id\"])"},{"end_line":128,"line":128,"literal":null,"names":["price"],"source":"price = float(coin[\"price\"])"},{"end_line":179,"line":179,"literal":null,"names":["market"],"source":"market = fetch_market()"}],"relevant_functions":[{"end_line":23,"line":14,"name":"connect","source":"def connect():\n c = sqlite3.connect(DB, timeout=15)\n c.row_factory = sqlite3.Row\n c.execute(\"PRAGMA journal_mode=WAL\")\n 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)\")\n 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)\")\n 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))\")\n c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")\n c.commit()\n return c"},{"end_line":75,"line":41,"name":"fetch_market","source":"def fetch_market():\n params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)\n r.raise_for_status()\n rows = r.json()\n now = int(time.time())\n usd_rub = 0.0\n try:\n with open(MARKET, \"r\", encoding=\"utf-8\") as previous_file:\n previous_rate = float((json.load(previous_file) or {}).get(\"usd_rub\") or 0)\n if 40 <= previous_rate <= 200:\n usd_rub = previous_rate\n except Exception:\n pass\n try:\n with httpx.Client(timeout=15, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as fx_client:\n fx_data = fx_client.get(\"https://api.coingecko.com/api/v3/simple/price\", params={\"ids\":\"bitcoin\",\"vs_currencies\":\"usd,rub\"}).json()[\"bitcoin\"]\n candidate = float(fx_data[\"rub\"]) / float(fx_data[\"usd\"])\n if 40 <= candidate <= 200:\n usd_rub = candidate\n except Exception:\n pass\n if not 40 <= usd_rub <= 200:\n usd_rub = 77.025093\n result = {\"updated_at\":now,\"currency\":\"usd\",\"usd_rub\":usd_rub,\"coins\":{}}\n\n for x in rows:\n cid = str(x.get(\"id\",\"\"))\n if cid not in COINS:\n continue\n 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\")}\n if len(result[\"coins\"]) != len(COINS):\n raise RuntimeError(\"incomplete market response\")\n return result"},{"end_line":79,"line":77,"name":"reference_price","source":"def reference_price(c, coin_id, target_ts):\n r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n return float(r[\"price\"]) if r else None"},{"end_line":117,"line":107,"name":"portfolio_text","source":"def portfolio_text(c, coin_id, price):\n r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n if not r:\n return \"\"\n quantity = float(r[\"quantity\"])\n avg = float(r[\"avg_buy\"])\n value = quantity * price\n invested = quantity * avg\n pnl = value - invested\n pct = pnl / invested * 100 if invested else 0\n return \"\\nКоличество монет: {:.8f}\\nТекущая стоимость позиции: ${:,.2f}\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\n\".format(quantity,value,pnl,pct)"},{"end_line":175,"line":119,"name":"evaluate","source":"def evaluate(c, market):\n now = int(time.time())\n triggered = 0\n for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n coin = market[\"coins\"].get(a[\"coin_id\"])\n if not coin:\n continue\n kind = a[\"kind\"]\n threshold = float(a[\"threshold\"])\n price = float(coin[\"price\"])\n cap = float(coin[\"market_cap\"])\n latched = int(a[\"latched\"])\n fired = False\n reset = False\n description = \"\"\n if kind == \"price_above\":\n fired = price >= threshold\n reset = price < threshold * 0.99\n description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n elif kind == \"price_below\":\n fired = price <= threshold\n reset = price > threshold * 1.01\n description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n elif kind == \"cap_above\":\n fired = cap >= threshold\n reset = cap < threshold * 0.99\n description = \"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n elif kind == \"cap_below\":\n fired = cap <= threshold\n reset = cap > threshold * 1.01\n description = \"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n elif kind in {\"pump\",\"dump\"}:\n old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))\n if old and old > 0:\n change = (price / old - 1) * 100\n if kind == \"pump\":\n fired = change >= threshold\n reset = change < threshold * 0.5\n description = \"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n else:\n fired = change <= -threshold\n reset = change > -threshold * 0.5\n description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n if reset and latched:\n c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\","},{"end_line":190,"line":177,"name":"cycle","source":"def cycle():\n DATA.mkdir(parents=True, exist_ok=True)\n market = fetch_market()\n now = int(market[\"updated_at\"])\n with connect() as c:\n for cid, x in market[\"coins\"].items():\n 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\"]))\n c.execute(\"DELETE FROM market_history WHERE ts', '']\n458: for n in range(5):\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')"},{"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'{safe_label}'"},{"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'', '']\n458: for n in range(5):\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')\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'', '']\n458: for n in range(5):\n459: yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'');parts.append(f'{price:.8g}')"},{"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'');parts.append(f'')\n463: parts.append(f'{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей')\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= 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= 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:\n2:Gram Crypto\n3:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":8,"snippet":"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n109:\n217:"},{"line":231,"snippet":"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\n230:\n231:\n232:\n232:\n217:\n231:\n232:\n232:"}],"coin":[{"line":3,"snippet":"1:\n2:Gram Crypto\n3:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":8,"snippet":"6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n217:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n395:\n396:\n397:"},{"line":397,"snippet":"395:\n396:\n397:"}],"moves":[{"line":3,"snippet":"1:\n2:Gram Crypto\n3:\n4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
"},{"line":6,"snippet":"4:
https://gram1.ru/

Мой криптопортфель

\n5:
Стоимость портфеля
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут
\n6:

Графики монет

Свечи Binance · 1 месяц · весь доступный период

HBAR / USDT
Нажмите, чтобы открыть график
POL / USDT
Нажмите, чтобы открыть график
AVAX / USDT
Нажмите, чтобы открыть график
LINK / USDT
Нажмите, чтобы открыть график

Резкие движения

\n7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

"},{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9:\n395:\n396:\n397:"}],"portfolio":[{"line":9,"snippet":"7:

Позиции портфеля

Количество и средняя цена покупки
АктивЦенаКапитализацияКоличествоСредняя покупка (USD)ВложеноСейчасP/L
\n8:

Новое оповещение

Мои оповещения

\n9: