CHAT_OUTPUT_BEGIN
COMMAND_ID=GRAM1-USDC-V42-READ-ONLY-PLAN-20260801T145300Z
STATUS=OK
RC=0
HOST=pve01
MODE=verify
COMPONENT=gram1-usdc-v42-plan
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83
COMMAND_SHA256=4aa13a481b22ea81603d4ee7fc21459486180a7e1494918139d0491444affa0d
DUPLICATE_FAILED_COMMAND_BLOCKED=false
EXECUTION_STARTED=true
CHANGE_DECLARED=false
RESULT_CONTRACT_VALID=true
RESULT_CONTRACT_STATUS=NOT_APPLICABLE
RESULT_CONTRACT_ERROR=NONE
COMMAND_RC=0
CHANGES_MADE=false
ROLLBACK_STARTED=false
ROLLBACK_RESTORED=null
MUTATION_OUTCOME=NO_MUTATION
SANITIZED=yes
SECRETS_INCLUDED=no
PRIVATE_ADDRESSES_INCLUDED=no
RAW_EVIDENCE_SHA256=c16acd7ab954591945c4217fb1164a9d843e977e059f5813af0a186f0e2eef0e
SANITIZED_OUTPUT_SHA256=42e8083f48caed53cc9b54fb6257e82b09431702c60c32afbe615c365cfff23b
OUTPUT_BEGIN
HOMELAB_RESULT_CONTRACT={"changes_made":false,"command_id":"GRAM1-USDC-V42-READ-ONLY-PLAN-20260801T145300Z","mutation_outcome":"NO_MUTATION","rollback_restored":null,"rollback_started":false,"status":"OK","version":1}
{"changes_made":false,"command_id":"GRAM1-USDC-V42-READ-ONLY-PLAN-20260801T145300Z","control_plane_error":null,"gitea_changed":false,"mutation_outcome":"NO_MUTATION","pinned_host_key_aliases":["[PRIVATE_IP]"],"production_site_changed":false,"rc":0,"remote_result":{"active_container_hashes":{"web":{"rc":0,"stderr":"","stdout":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d  /app/app.py\n4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c  /app/worker.py\n3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2  /app/templates/index.html"},"worker":{"rc":0,"stderr":"","stdout":"ec226952cdb7fb58edbfc0173af47f4d901f3206e15169df12294983731fbc20  /app/app.py\n4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c  /app/worker.py\ne9e723c9b916383ce10dffe8f8bbdb4049b78d89c5b139e2de5ed82194a27be4  /app/templates/index.html"}},"app":{"assignments":[{"end_line":10,"line":10,"literal":null,"names":["MARKET"],"source":"MARKET = DATA / \"market.json\""},{"end_line":13,"line":13,"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":128,"line":128,"literal":{"avalanche-2":"AVAX","binancecoin":"BNB","bitcoin":"₿","cardano":"ADA","chainlink":"LINK","dogecoin":"Ð","ethereum":"Ξ","hedera":"HBAR","hedera-hashgraph":"HBAR","ripple":"XRP","solana":"S","toncoin":"TON"},"names":["symbols"],"source":"symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}"},{"end_line":139,"line":139,"literal":{"AVAX":"AVAXUSDT","HBAR":"HBARUSDT","LINK":"LINKUSDT","POL":"POLUSDT"},"names":["KLINE_SYMBOLS"],"source":"KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}"},{"end_line":147,"line":147,"literal":{},"names":["KLINE_CACHE"],"source":"KLINE_CACHE = {}"},{"end_line":148,"line":148,"literal":300,"names":["KLINE_CACHE_TTL"],"source":"KLINE_CACHE_TTL = 300"},{"end_line":149,"line":149,"literal":604800,"names":["KLINE_CACHE_MAX_STALE"],"source":"KLINE_CACHE_MAX_STALE = 604800"},{"end_line":150,"line":150,"literal":null,"names":["KLINE_CACHE_FILE"],"source":"KLINE_CACHE_FILE = DATA / \"klines-cache.json\""},{"end_line":151,"line":151,"literal":null,"names":["KLINE_CACHE_WRITE_LOCK"],"source":"KLINE_CACHE_WRITE_LOCK = threading.Lock()"},{"end_line":350,"line":350,"literal":null,"names":["holdings"],"source":"holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}"},{"end_line":351,"line":351,"literal":null,"names":["alerts"],"source":"alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]"},{"end_line":367,"line":367,"literal":null,"names":["coin"],"source":"coin = str(x.get(\"coin_id\",\"\"))"},{"end_line":370,"line":370,"literal":null,"names":["quantity"],"source":"quantity = max(0.0, float(x.get(\"quantity\",0)))"},{"end_line":371,"line":371,"literal":null,"names":["avg_buy"],"source":"avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))"},{"end_line":380,"line":380,"literal":null,"names":["coin"],"source":"coin = str(x.get(\"coin_id\",\"\"))"},{"end_line":406,"line":406,"literal":"<REDACTED_LONG_VALUE>","names":["_GRAM1_CHART_JS_B64"],"source":"_GRAM1_CHART_JS_B64 = \"<REDACTED_LONG_VALUE>\""},{"end_line":407,"line":407,"literal":["AVAX","POL","HBAR","LINK"],"names":["_GRAM1_CHART_SYMBOLS"],"source":"_GRAM1_CHART_SYMBOLS = {\"HBAR\", \"POL\", \"AVAX\", \"LINK\"}"},{"end_line":459,"line":459,"literal":null,"names":["price"],"source":"price=hi-span*n/4"},{"end_line":470,"line":470,"literal":null,"names":["symbol"],"source":"symbol=symbol.upper()"}],"relevant_functions":[{"end_line":23,"line":16,"name":"db","source":"def db():\n    c = sqlite3.connect(DB, timeout=10)\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.commit()\n    return c"},{"end_line":47,"line":42,"name":"market_data","source":"def market_data():\n    try:\n        obj = json.loads(MARKET.read_text())\n        return obj if isinstance(obj, dict) else {}\n    except Exception:\n        return {}"},{"end_line":56,"line":53,"name":"login_page","source":"def login_page(request: Request):\n    if request.session.get(\"auth\"):\n        return RedirectResponse(\"/\", 303)\n    return HTMLResponse(\"\"\"<!doctype html><html lang=\"ru\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title><style>body{margin:0;background:#071018;color:#f3f7fb;font:15px system-ui;min-height:100vh;display:grid;place-items:center}.c{width:min(390px,calc(100% - 40px));background:#101f2c;border:1px solid #203442;border-radius:20px;padding:28px;box-shadow:0 30px 90px #0008}h1{margin:0 0 6px}p{color:#91a7b8}.d{color:#63e6be}input,button{width:100%;box-sizing:border-box;border-radius:11px;padding:13px;margin-top:10px;font:inherit}input{background:#07131b;border:1px solid #294456;color:#fff}button{border:0;background:#63e6be;color:#071018;font-weight:800;cursor:pointer}.e{color:#ff6b81;min-height:20px}</style><div class=\"c\"><h1>Gram Crypto</h1><p class=\"d\">https://gram1.ru/</p><p>Введите пароль для доступа к криптопортфелю.</p><form method=\"post\"><input type=\"password\" name=\"password\" autocomplete=\"current-password\" autofocus required><div class=\"e\">\"\"\" + (\"Неверный пароль\" if request.query_params.get(\"bad\") else \"\") + \"\"\"</div><button>Войти</button></form></div></html>\"\"\")"},{"end_line":89,"line":85,"name":"health","source":"def health():\n    age = None\n    if MARKET.exists():\n        age = int(time.time() - MARKET.stat().st_mtime)\n    return {\"status\":\"OK\",\"type\":\"crypto-portfolio\",\"market_age_seconds\":age}"},{"end_line":134,"line":93,"name":"coin_icon","source":"def coin_icon(coin_id: str):\n    import html\n    import os\n    import re\n    from pathlib import Path\n    import httpx\n    from fastapi import HTTPException\n    from fastapi.responses import Response\n    if not re.fullmatch(r\"[a-z0-9-]{1,64}\", coin_id):\n        raise HTTPException(status_code=404)\n    cache_dir = Path(\"/data/coin-icons\")\n    cache_dir.mkdir(parents=True, exist_ok=True)\n    body_path = cache_dir / (coin_id + \".img\")\n    type_path = cache_dir / (coin_id + \".type\")\n    if body_path.exists() and body_path.stat().st_size > 128:\n        media_type = type_path.read_text().strip() if type_path.exists() else \"image/png\"\n        return Response(body_path.read_bytes(), media_type=media_type, headers={\"Cache-Control\":\"public, max-age=86400\"})\n    try:\n        with httpx.Client(timeout=6, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n            response = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params={\"vs_currency\":\"usd\",\"ids\":coin_id,\"sparkline\":\"false\"})\n            response.raise_for_status()\n            rows = response.json()\n            if not rows or not rows[0].get(\"image\"):\n                raise ValueError(\"image URL missing\")\n            image = client.get(rows[0][\"image\"])\n            image.raise_for_status()\n        media_type = (image.headers.get(\"content-type\") or \"image/png\").split(\";\", 1)[0]\n        if not media_type.startswith(\"image/\") or len(image.content) <= 128:\n            raise ValueError(\"invalid image\")\n        tmp_path = body_path.with_suffix(\".tmp\")\n        tmp_path.write_bytes(image.content)\n        os.replace(tmp_path, body_path)\n        type_path.write_text(media_type)\n        return Response(image.content, media_type=media_type, headers={\"Cache-Control\":\"public, max-age=86400\"})\n    except Exception:\n        symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}\n        label = symbols.get(coin_id, coin_id[:3].upper())\n        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]\n        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\n        safe_label = html.escape(label)\n        svg = f'<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\"><circle cx=\""},{"end_line":145,"line":142,"name":"lightweight_charts_js","source":"def lightweight_charts_js():\n    if not LIGHTWEIGHT_JS.is_file():\n        raise HTTPException(404, \"chart library missing\")\n    return Response(LIGHTWEIGHT_JS.read_bytes(), media_type=\"application/javascript\", headers={\"Cache-Control\": \"public, max-age=31536000, immutable\"})"},{"end_line":159,"line":153,"name":"_valid_kline_result","source":"def _valid_kline_result(result):\n    return (\n        isinstance(result, dict)\n        and result.get(\"status\") == \"OK\"\n        and isinstance(result.get(\"candles\"), list)\n        and len(result[\"candles\"]) >= 2\n    )"},{"end_line":183,"line":161,"name":"_load_kline_cache","source":"def _load_kline_cache():\n    if not KLINE_CACHE_FILE.is_file():\n        return\n    try:\n        payload = json.loads(KLINE_CACHE_FILE.read_text())\n        entries = payload.get(\"entries\", {})\n        now = time.time()\n        for name, item in entries.items():\n            parts = name.split(\":\")\n            if len(parts) == 3:\n                symbol, interval, limit_text = parts\n            elif len(parts) == 2:\n                symbol, limit_text = parts\n                interval = \"1d\"\n            else:\n                continue\n            limit = int(limit_text)\n            saved_at = float(item.get(\"saved_at\", 0))\n            result = item.get(\"result\")\n            if now - saved_at <= KLINE_CACHE_MAX_STALE and _valid_kline_result(result):\n                KLINE_CACHE[(symbol, interval, limit)] = (saved_at, result)\n    except Exception:\n        return"},{"end_line":201,"line":185,"name":"_save_kline_cache","source":"def _save_kline_cache():\n    with KLINE_CACHE_WRITE_LOCK:\n        entries = {}\n        for key, value in list(KLINE_CACHE.items()):\n            if len(key) == 3:\n                symbol, interval, limit = key\n            else:\n                symbol, limit = key\n                interval = \"1d\"\n            saved_at, result = value\n            if _valid_kline_result(result):\n                entries[symbol + \":\" + interval + \":\" + str(limit)] = {\"saved_at\": saved_at, \"result\": result}\n        payload = {\"version\": 1, \"saved_at\": int(time.time()), \"entries\": entries}\n        temporary = KLINE_CACHE_FILE.with_suffix(\".json.tmp\")\n        temporary.write_text(json.dumps(payload, ensure_ascii=False, separators=(\",\", \":\")))\n        os.replace(temporary, KLINE_CACHE_FILE)\n        os.chmod(KLINE_CACHE_FILE, 0o644)"},{"end_line":305,"line":205,"name":"fetch_klines","source":"def fetch_klines(symbol, limit=365, interval=\"1d\"):\n    key = str(symbol).upper()\n    pair = KLINE_SYMBOLS.get(key)\n    if not pair:\n        raise HTTPException(404, \"unknown symbol\")\n    interval = str(interval or \"1d\")\n    if interval not in {\"1d\", \"1M\"}:\n        raise HTTPException(400, \"unsupported interval\")\n    limit = max(1, min(int(limit), 1000))\n    if interval == \"1d\":\n        limit = max(30, limit)\n    cache_key = (key, interval, limit)\n    now = time.time()\n    cached = KLINE_CACHE.get(cache_key)\n    if cached and now - cached[0] < KLINE_CACHE_TTL:\n        result = dict(cached[1])\n        result[\"source\"] = \"cache\"\n        result[\"stale\"] = False\n        result[\"cache_age_seconds\"] = int(now - cached[0])\n        return result\n    stale = cached if cached and now - cached[0] <= KLINE_CACHE_MAX_STALE else None\n    remaining = limit\n    end_time = None\n    collected = []\n    try:\n        while remaining > 0:\n            page_limit = min(1000 if interval == \"1M\" else 100, remaining)\n            params = {\"symbol\": pair, \"interval\": interval, \"limit\": page_limit}\n            if end_time is not None:\n                params[\"endTime\"] = end_time\n            query = urllib.parse.urlencode(params)\n            request = urllib.request.Request(\n                \"https://data-api.binance.vision/api/v3/klines?\" + query,\n                headers={\"User-Agent\": \"crypto-portfolio/1.0\"},\n            )\n            rows = None\n            last_error = None\n            for attempt in range(3):\n                try:\n                    with urllib.request.urlopen(request, timeout=20) as response:\n                        rows = json.load(response)\n                    break\n                except Exception as exc:\n                    last_error = exc\n                    if attempt < 2:\n                        time.sleep(0.5 * (attempt + 1))\n            if rows is None:\n                raise last_error\n            if not isinstance(rows, list) or not rows:\n                break\n            collected.extend(rows)\n            remaining -= len(rows)\n            if len(rows) < page_limit:\n                break\n            end_time = int(rows[0][0]) - 1\n    except Exception as exc:\n        if stale:\n            result = dict(stale[1])\n            result[\"source\"] = \"stale-cache\"\n            result[\"stale\"] = True\n            result[\"cache_age_seconds\"] = int(now - stale[0])\n   "},{"end_line":310,"line":308,"name":"klines","source":"def klines(symbol: str, request: Request, limit: int = 365, interval: str = \"1d\"):\n    require_login(request)\n    return fetch_klines(symbol, limit, interval)"},{"end_line":344,"line":313,"name":"chart_health","source":"def chart_health():\n    now = time.time()\n    details = {}\n    bad = 0\n    for symbol in (\"HBAR\", \"POL\", \"AVAX\", \"LINK\"):\n        entry = KLINE_CACHE.get((symbol, \"1M\", 1000)) or KLINE_CACHE.get((symbol, \"1d\", 365))\n        if not entry or not _valid_kline_result(entry[1]):\n            details[symbol] = {\"status\": \"ERROR\", \"candles\": 0, \"cache_age_seconds\": None}\n            bad += 1\n            continue\n        age = int(now - entry[0])\n        expired = age > KLINE_CACHE_MAX_STALE\n        details[symbol] = {\n            \"status\": \"ERROR\" if expired else \"OK\",\n            \"candles\": len(entry[1][\"candles\"]),\n            \"cache_age_seconds\": age,\n            \"stale\": age > KLINE_CACHE_TTL,\n            \"source\": entry[1].get(\"source\", \"cache\"),\n        }\n        if expired:\n            bad += 1\n    library_ok = LIGHTWEIGHT_JS.is_file() and LIGHTWEIGHT_JS.stat().st_size > 100000\n    if not library_ok:\n        bad += 1\n    payload = {\n        \"status\": \"OK\" if bad == 0 else \"ERROR\",\n        \"library_ok\": library_ok,\n        \"disk_cache_present\": KLINE_CACHE_FILE.is_file(),\n        \"symbols\": details,\n        \"bad\": bad,\n    }\n    return JSONResponse(payload, status_code=200 if bad == 0 else 503)"},{"end_line":361,"line":347,"name":"state","source":"def state(request: Request):\n    require_login(request)\n    with db() as c:\n        holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}\n        alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]\n        history = {}\n        z = {r[\"name\"] for r in c.execute(\"PRAGMA table_info(market_history)\")}\n        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)\n        if cc and pc and tc:\n            q = f'SELECT \"{pc}\" p FROM market_history WHERE \"{cc}\"=? ORDER BY \"{tc}\"'\n            for cid in COINS:\n                v=[float(r[\"p\"]) for r in c.execute(q,(cid,)) if r[\"p\"] is not None]\n                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\n    # CRYPTO_HISTORY_STATE_V2\n    return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}"},{"end_line":374,"line":364,"name":"save_holding","source":"async def save_holding(request: Request):\n    require_login(request)\n    x = await request.json()\n    coin = str(x.get(\"coin_id\",\"\"))\n    if coin not in COINS:\n        raise HTTPException(400,\"bad coin\")\n    quantity = max(0.0, float(x.get(\"quantity\",0)))\n    avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))\n    with db() as c:\n        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()\n    return {\"ok\":True}"},{"end_line":388,"line":377,"name":"add_alert","source":"async def add_alert(request: Request):\n    require_login(request)\n    x = await request.json()\n    coin = str(x.get(\"coin_id\",\"\")); kind = str(x.get(\"kind\",\"\"))\n    if coin not in COINS or kind not in {\"price_above\",\"price_below\",\"cap_above\",\"cap_below\",\"pump\",\"dump\"}:\n        raise HTTPException(400,\"bad rule\")\n    threshold = float(x.get(\"threshold\",0)); window = int(x.get(\"window_seconds\",0)); email = str(x.get(\"email\",\"\")).strip()\n    if threshold <= 0 or \"@\" not in email or (kind in {\"pump\",\"dump\"} and window not in {300,900,1800,3600,14400,86400}):\n        raise HTTPException(400,\"bad values\")\n    with db() as c:\n        cur = c.execute(\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\",(coin,kind,threshold,window,email)); c.commit()\n    return {\"ok\":True,\"id\":cur.lastrowid}"},{"end_line":395,"line":391,"name":"delete_alert","source":"def delete_alert(alert_id: int, request: Request):\n    require_login(request)\n    with db() as c:\n        c.execute(\"DELETE FROM alerts WHERE id=?\",(alert_id,)); c.commit()\n    return {\"ok\":True}"},{"end_line":411,"line":410,"name":"_gram1_chart_javascript_v4","source":"def _gram1_chart_javascript_v4():\n    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})"},{"end_line":450,"line":413,"name":"_gram1_chart_data_v4","source":"async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\n    fn = globals().get(\"klines\")\n    if fn is None:\n        raise RuntimeError(\"klines route function not found\")\n    kwargs = {}\n    for name, parameter in _gram1_inspect.signature(fn).parameters.items():\n        lname = name.lower()\n        if lname in {\"symbol\", \"coin\", \"ticker\"}: kwargs[name] = symbol\n        elif lname == \"request\": kwargs[name] = request\n        elif lname == \"limit\": kwargs[name] = 1000\n        elif lname == \"interval\": kwargs[name] = \"1M\"\n        elif parameter.default is _gram1_inspect.Parameter.empty:\n            raise RuntimeError(\"unsupported required klines parameter: \" + name)\n    value = fn(**kwargs)\n    if _gram1_inspect.isawaitable(value): value = await value\n    if hasattr(value, \"body\"):\n        value = _gram1_json.loads(bytes(value.body).decode(\"utf-8\"))\n    if not isinstance(value, dict):\n        raise RuntimeError(\"klines result is not an object\")\n    if value.get(\"status\") not in (None, \"OK\"):\n        raise RuntimeError(str(value.get(\"error\") or value.get(\"message\") or value.get(\"status\")))\n    candles = value.get(\"candles\")\n    if not isinstance(candles, list) and isinstance(value.get(\"result\"), dict): candles = value[\"result\"].get(\"candles\")\n    if not isinstance(candles, list) or not candles:\n        raise RuntimeError(\"klines returned no candles\")\n    rows = []\n    for item in candles:\n        try:\n            if isinstance(item, dict):\n                t=item.get(\"time\",item.get(\"open_time\",item.get(\"openTime\")));o=float(item[\"open\"]);h=float(item[\"high\"]);l=float(item[\"low\"]);c=float(item[\"close\"])\n            else:\n                t=item[0];o=float(item[1]);h=float(item[2]);l=float(item[3]);c=float(item[4])\n            if h < l: continue\n            rows.append((str(t),o,h,l,c))\n        except Exception:\n            continue\n    if not rows: raise RuntimeError(\"no valid OHLC rows\")\n    return rows[-180:]"},{"end_line":464,"line":452,"name":"_gram1_chart_svg_v4","source":"def _gram1_chart_svg_v4(symbol: str, rows):\n    width,height,left,right,top,bottom=1200,430,72,28,24,42\n    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)\n    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))\n    def y(v): return top+(hi-v)/span*plot_h\n    parts=[f'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" role=\"img\" aria-label=\"{_gram1_html.escape(symbol)} candle chart\" data-symbol=\"{_gram1_html.escape(symbol)}\" data-candles=\"{len(rows)}\">', '<rect width=\"100%\" height=\"100%\" fill=\"#081823\"/>']\n    for n in range(5):\n        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\"{left}\" y1=\"{yy:.2f}\" x2=\"{width-right}\" y2=\"{yy:.2f}\" stroke=\"rgba(143,165,182,.12)\"/>');parts.append(f'<text x=\"6\" y=\"{yy+4:.2f}\" fill=\"#8fa5b6\" font-size=\"12\">{price:.8g}</text>')\n    for i,(_,o,h,l,c) in enumerate(rows):\n        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)\n        parts.append(f'<line x1=\"{x:.2f}\" y1=\"{yh:.2f}\" x2=\"{x:.2f}\" y2=\"{yl:.2f}\" stroke=\"{color}\" stroke-width=\"1.2\"/>');parts.append(f'<rect x=\"{x-body/2:.2f}\" y=\"{ry:.2f}\" width=\"{body:.2f}\" height=\"{rh:.2f}\" fill=\"{color}\"/>')\n    parts.append(f'<text x=\"{left}\" y=\"{height-12}\" fill=\"#8fa5b6\" font-size=\"13\">{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей</text></svg>')\n    return ''.join(parts)"},{"end_line":480,"line":467,"name":"_gram1_chart_svg_endpoint_v4","source":"async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\n    if not request.session.get(\"auth\"):\n        return _Gram1Response(content=\"Требуется вход\", status_code=401, media_type=\"text/plain; charset=utf-8\")\n    symbol=symbol.upper()\n    if symbol not in _GRAM1_CHART_SYMBOLS:\n        return _Gram1Response(content=\"Неизвестная монета\", status_code=404, media_type=\"text/plain; charset=utf-8\")\n    try:\n        rows=await _gram1_chart_data_v4(symbol,request)\n        svg=_gram1_chart_svg_v4(symbol,rows)\n        return _Gram1Response(content=svg,media_type=\"image/svg+xml\",headers={\"Cache-Control\":\"no-store\"})\n    except Exception as exc:\n        msg=_gram1_html.escape(str(exc))\n        svg=f'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1200 430\" data-symbol=\"{symbol}\" data-candles=\"0\"><rect width=\"100%\" height=\"100%\" fill=\"#081823\"/><text x=\"30\" y=\"60\" fill=\"#ff6b81\" font-size=\"18\">Ошибка серверного графика {symbol}: {msg}</text></svg>'\n        return _Gram1Response(content=svg,status_code=503,media_type=\"image/svg+xml\",headers={\"Cache-Control\":\"no-store\"})"}],"routes":[{"function":"login_page","line":53,"routes":[{"method":"GET","path":"/login"}]},{"function":"login","line":59,"routes":[{"method":"POST","path":"/login"}]},{"function":"logout","line":74,"routes":[{"method":"POST","path":"/logout"}]},{"function":"home","line":79,"routes":[{"method":"GET","path":"/"}]},{"function":"health","line":85,"routes":[{"method":"GET","path":"/health"}]},{"function":"coin_icon","line":93,"routes":[{"method":"GET","path":"/coin-icon/{coin_id}"}]},{"function":"lightweight_charts_js","line":142,"routes":[{"method":"GET","path":"/vendor/lightweight-charts.js"}]},{"function":"klines","line":308,"routes":[{"method":"GET","path":"/api/klines/{symbol}"}]},{"function":"chart_health","line":313,"routes":[{"method":"GET","path":"/api/chart-health"}]},{"function":"state","line":347,"routes":[{"method":"GET","path":"/api/state"}]},{"function":"save_holding","line":364,"routes":[{"method":"POST","path":"/api/holding"}]},{"function":"add_alert","line":377,"routes":[{"method":"POST","path":"/api/alert"}]},{"function":"delete_alert","line":391,"routes":[{"method":"DELETE","path":"/api/alert/{alert_id}"}]},{"function":"_gram1_chart_javascript_v4","line":410,"routes":[{"method":"GET","path":"/vendor/gram1-charts-v4.js"}]},{"function":"_gram1_chart_svg_endpoint_v4","line":467,"routes":[{"method":"GET","path":"/api/chart-svg/{symbol}"}]}]},"changes_made":false,"command_id":"GRAM1-USDC-V42-READ-ONLY-PLAN-20260801T145300Z","compose":{"compose_file":"compose.yaml","config_rc":0,"ps_excerpt":"{\"Command\":\"\\\"uvicorn app:app --h…\\\"\",\"CreatedAt\":\"2026-08-01 16:21:30 +0300 MSK\",\"ExitCode\":0,\"Health\":\"\",\"ID\":\"dc202c12ba81\",\"Image\":\"crypto-portfolio-web\",\"Labels\":\"com.docker.compose.depends_on=,com.docker.compose.image=sha256:2a9339d475b2f4a01f060162f48a23b9590eb3ba4b036b46aad72adad540c40d,com.docker.compose.project.config_files=/opt/stacks/crypto-portfolio/compose.yaml,com.docker.compose.project.working_dir=/opt/stacks/crypto-portfolio,com.docker.compose.<REDACTED_LONG_VALUE>,com.docker.compose.service=web,com.docker.compose.<REDACTED_LONG_VALUE>,com.docker.compose.oneoff=False,com.docker.compose.project=crypto-portfolio,com.docker.compose.version=2.26.1,com.docker.compose.container-number=1\",\"LocalVolumes\":\"0\",\"Mounts\":\"/opt/stacks/cr…,/opt/stacks/cr…,/opt/stacks/cr…,/opt/stacks/cr…\",\"Name\":\"crypto-portfolio\",\"Names\":\"crypto-portfolio\",\"Networks\":\"crypto-portfolio_default\",\"Ports\":\"<PRIVATE_ADDRESS>:8030-\\u003e8000/tcp\",\"Project\":\"crypto-portfolio\",\"Publishers\":[{\"URL\":\"<PRIVATE_ADDRESS>\",\"TargetPort\":8000,\"PublishedPort\":8030,\"Protocol\":\"tcp\"}],\"RunningFor\":\"4 hours ago\",\"Service\":\"web\",\"Size\":\"0B\",\"State\":\"running\",\"Status\":\"Up 4 hours\"}\n{\"Command\":\"\\\"python /app/worker.…\\\"\",\"CreatedAt\":\"2026-08-01 13:56:57 +0300 MSK\",\"ExitCode\":0,\"Health\":\"\",\"ID\":\"9a2ac999b94a\",\"Image\":\"crypto-portfolio-worker\",\"Labels\":\"com.docker.compose.project=crypto-portfolio,com.docker.compose.project.config_files=/opt/stacks/crypto-portfolio/compose.yaml,com.docker.compose.version=2.26.1,com.docker.compose.image=sha256:7ff5fdff7ab373e6b22dfa1a677b000d83ff9d021e4464915078e506781fd0ed,com.docker.compose.project.working_dir=/opt/stacks/crypto-portfolio,com.docker.compose.<REDACTED_LONG_VALUE>,com.docker.compose.service=worker,com.docker.compose.<REDACTED_LONG_VALUE>,com.docker.compose.container-number=1,com.docker.compose.depends_on=,com.docker.compose.oneoff=False\",\"LocalVolumes\":\"0\",\"Mounts\":\"/opt/stacks/cr…,/opt/stacks/cr…\",\"Name\":\"crypto-portfolio-worker\",\"Names\":\"crypto-portfolio-worker\",\"Networks\":\"crypto-portfolio_default\",\"Ports\":\"\",\"Project\":\"crypto-portfolio\",\"Publishers\":null,\"RunningFor\":\"6 hours ago\",\"Service\":\"worker\",\"Size\":\"0B\",\"State\":\"running\",\"Status\":\"Up 6 hours\"}","ps_rc":0,"services":["web","worker"],"services_rc":0,"stderr":""},"control_plane_error":null,"duration_seconds":0.043,"files":[{"bytes":32843,"exists":true,"gid":0,"mode":"0o640","path":"app.py","sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","uid":0},{"bytes":10300,"exists":true,"gid":0,"mode":"0o640","path":"worker.py","sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","uid":0},{"bytes":38036,"exists":true,"gid":0,"mode":"0o644","path":"templates/index.html","sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","uid":0},{"bytes":1233,"exists":true,"gid":0,"mode":"0o644","path":"compose.yaml","sha256":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8","uid":0}],"gitea_changed":false,"hostname":"core-apps","mutation_outcome":"NO_MUTATION","plan":{"current_usdc_present_anywhere":false,"market_sections_found":{"alerts":true,"charts":true,"sharp_moves":true,"top_coin_cards":true},"mutation_scope_candidate":["app.py","templates/index.html"],"next_step":"ONE_ATOMIC_REVERSIBLE_MUTATION_AFTER_REVIEW_OF_THIS_EXACT_REPORT","notes":["Do not add USDC to the shared market COINS/KLINE/alert lists.","Preserve HBAR/POL/AVAX/LINK market behavior unchanged.","Expose an editable USDC quantity and average purchase price only if the current positions UI already uses those inputs.","Portfolio totals must count USDC current value and cost basis; P/L for USDC follows the same row formula."],"positions_section_found":true,"quantity_or_cost_basis_storage_found":true,"requested_behavior":{"add_usdc_to_positions_only":true,"exclude_from_alerts":true,"exclude_from_charts":true,"exclude_from_sharp_moves":true,"exclude_from_top_coin_cards":true,"include_usdc_in_total_invested":true,"include_usdc_in_total_portfolio_value":true,"usdc_market_price_policy":"fixed_1_usd_unless_existing_source_is_explicitly_better"},"required_architecture":"separate portfolio-position asset set from market-tracked asset set; USDC must exist only in the former","worker_change_expected":false},"production_site_changed":false,"rc":0,"relevant_file_hits":[{"line":9,"path":"app.py","text":"DB = DATA / \"portfolio.db\""},{"line":20,"path":"app.py","text":"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)\")"},{"line":89,"path":"app.py","text":"return {\"status\":\"OK\",\"type\":\"crypto-portfolio\",\"market_age_seconds\":age}"},{"line":111,"path":"app.py","text":"with httpx.Client(timeout=6, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:"},{"line":238,"path":"app.py","text":"headers={\"User-Agent\": \"crypto-portfolio/1.0\"},"},{"line":350,"path":"app.py","text":"holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}"},{"line":361,"path":"app.py","text":"return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}"},{"line":363,"path":"app.py","text":"@app.post(\"/api/holding\")"},{"line":364,"path":"app.py","text":"async def save_holding(request: Request):"},{"line":370,"path":"app.py","text":"quantity = max(0.0, float(x.get(\"quantity\",0)))"},{"line":373,"path":"app.py","text":"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()"},{"line":4,"path":"compose.yaml","text":"container_name: crypto-portfolio"},{"line":29,"path":"compose.yaml","text":"container_name: crypto-portfolio-worker"},{"line":7,"path":"data/lightweight-charts.standalone.production.js","text":"!function(){\"use strict\";const t={title:\"\",visible:!0,hitTestTolerance:3,lastValueVisible:!0,priceLineVisible:!0,priceLineSource:0,priceLineWidth:1,priceLineColor:\"\",priceLineStyle:2,baseLineVisible:!0,baseLineWidth:1,baseLineColor:\"#B2B5BE\",baseLineStyle:0,priceFormat:{type:\"price\",precision:2,minMove:.01}};var i,n;function s(t,i){const n=function(t,i){switch(t){case 0:default:return[];case 1:return[i,i];case 2:return[2*i,2*i];case 3:return[6*i,6*i];case 4:return[i,4*i]}}(i,t.lineWidth);return t.setLineDash(n),n}function e(t,i,n,s){t.beginPath();const e=t.lineWidth%2?.5:0;t.moveTo(n,i+e),t.lineTo(s,i+e),t.stroke()}function r(t,i){if(!t)throw new Error(\"Assertion failed\"+(i?\": \"+i:\"\"))}function h(t){if(void 0===t)throw new Error(\"Value is undefined\");return t}function a(t){if(null===t)throw new Error(\"Value is null\");return t}function l(t){return a(h(t))}!function(t){t[t.Simple=0]=\"Simple\",t[t.WithSteps=1]=\"WithSteps\",t[t.Curved=2]=\"Curved\"}(i||(i={})),function(t){t[t.Solid=0]=\"Solid\","},{"line":3,"path":"templates/index.html","text":"<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);bo"},{"line":5,"path":"templates/index.html","text":"<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" class=\"muted\">—</span></div><div class=\"card\"><span class=\"muted\">Вложено</span><strong id=\"invested\">—</strong></div><div class=\"card\"><span class=\"muted\">Прибыль / убыток</span><strong id=\"pnl\">—</strong><span id=\"pnlp\" class=\"muted\">0%</span></div><div class=\"card\"><span class=\"muted\">Обновление рынка</span><strong id=\"updated\">—</strong><span class=\"muted\">каждые 5 минут</span></div></section>"},{"line":9,"path":"templates/index.html","text":"<script>let state=null;let selected=\"hedera-hashgraph\";let currency=localStorage.getItem(\"portfolio_currency\")===\"RUB\"?\"RUB\":\"USD\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\"RUB\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\"RUB\"?\"ru-RU\":\"en-US\",{style:\"currency\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\"RUB\"?\"₽\":\"$\")+new Intl.NumberFormat(currency===\"RUB\"?\"ru-RU\":\"en-US\",{notation:\"compact\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\"RUB\"?fx():1)).toFixed(currency===\"RUB\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\"/api/state\");if(r.status===401){location=\"/login\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id="},{"line":152,"path":"templates/index.html","text":"const holding=(data.holdings&&data.holdings[alert.coin_id])||{};"},{"line":153,"path":"templates/index.html","text":"const quantity=Number(holding.quantity||0);"},{"line":155,"path":"templates/index.html","text":"const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");"},{"line":7,"path":"worker.py","text":"DB = DATA / \"portfolio.db\""},{"line":18,"path":"worker.py","text":"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)\")"},{"line":43,"path":"worker.py","text":"with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:"},{"line":57,"path":"worker.py","text":"with httpx.Client(timeout=15, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as fx_client:"},{"line":107,"path":"worker.py","text":"def portfolio_text(c, coin_id, price):"},{"line":108,"path":"worker.py","text":"r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()"},{"line":111,"path":"worker.py","text":"quantity = float(r[\"quantity\"])"},{"line":113,"path":"worker.py","text":"value = quantity * price"},{"line":114,"path":"worker.py","text":"invested = quantity * avg"},{"line":115,"path":"worker.py","text":"pnl = value - invested"},{"line":116,"path":"worker.py","text":"pct = pnl / invested * 100 if invested else 0"},{"line":117,"path":"worker.py","text":"return \"\\nКоличество монет: {:.8f}\\nТекущая стоимость позиции: ${:,.2f}\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\n\".format(quantity,value,pnl,pct)"},{"line":167,"path":"worker.py","text":"body = description + \"\\nТекущая капитализация: ${:,.0f}\\n\".format(cap) + portfolio_text(c,a[\"coin_id\"],price) + \"\\nВремя события: \" + time.strftime(\"%Y-%m-%d %H:%M:%S %Z\")"}],"rollback_restored":null,"rollback_started":false,"root":"/opt/stacks/crypto-portfolio","safe_inventory":[{"bytes":32843,"path":"app.py","sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d"},{"bytes":1233,"path":"compose.yaml","sha256":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8"},{"bytes":20964,"path":"data/klines-cache.json","sha256":"536c86f5fe0de5da6f1cd060c8529aeac364ad8fd012c73f4fa477014bbe1aa9"},{"bytes":196203,"path":"data/lightweight-charts.standalone.production.js","sha256":"c0992580867c4912cc9385b3c2728315bcc1a76c7f1087dca908430fccdf31d7"},{"bytes":13864,"path":"data/market.json","sha256":"dd8bf136f8586a3f0a8d08bddda61ff4dd29c1eded3c48612cecc4f0be4e5b69"},{"bytes":1716224,"path":"data/portfolio.db","sha256":"742320dcfffa4523e846cb71c2a6af9602ca7ed2853d95ea23c1e23e64349d01"},{"bytes":96,"path":"data/worker-status.json","sha256":"f54b332a45a8cc8c2cef2bde5ea2d2bf4b1c6e5517752ce1555feca51d606f1c"},{"bytes":123,"path":"requirements.txt","sha256":"5dec3868a6a71a7dc8810b57acae9de25b99cae6978cd1750f4b0c01b27055a2"},{"bytes":38036,"path":"templates/index.html","sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2"},{"bytes":10300,"path":"worker.py","sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c"}],"schema":1,"sqlite":[{"bytes":1716224,"path":"data/portfolio.db","sha256":"742320dcfffa4523e846cb71c2a6af9602ca7ed2853d95ea23c1e23e64349d01","tables":[{"columns":["id","coin_id","kind","threshold","window_seconds","email","enabled","latched","last_triggered"],"name":"alerts","row_count":4,"schema":"CREATE TABLE 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)"},{"columns":["coin_id","quantity","avg_buy"],"name":"holdings","row_count":4,"safe_sample_columns":["quantity"],"safe_sample_rows":[[296172.0],[34492.0],[385.7],[298.1]],"schema":"CREATE TABLE holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)"},{"columns":["ts","coin_id","price","market_cap","volume"],"name":"market_history","row_count":14370,"schema":"CREATE TABLE 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))"}]}],"status":"OK","template":{"bytes":38036,"contains_dynamic_market_coins":true,"contains_usdc":false,"ids":["ALERT_CREATE_OVERRIDE_V3","ALERT_LIST_RENDERER_V5","GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7","GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7","MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1","SHARP_MOVES_LAYOUT_V2","aCoin","aEmail","aKind","aThreshold","aWindow","addAlert","alertCreateStatusV3","alertList","avgHeader","chart","chartTitle","coinCharts","coins","day","invested","marketStatus","moves","pnl","pnlp","rows","total","updated"],"jinja_loops":[],"relevant_context":[{"line":1,"text":"<!doctype html>"},{"line":2,"text":"<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>"},{"line":3,"text":"<style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;backgrou"},{"line":4,"text":"<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>"},{"line":5,"text":"<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" class=\"muted\">—</span></div><div class=\"card\"><span class=\"muted\">Вложено</span><strong id=\"invested\">—</strong></div><div class=\"card\"><span class=\"muted\">Прибыль / убыток</span><strong id=\"pnl\">—</strong><span id=\"pnlp\" class=\"muted\">0%</span></div><div class=\"card\"><span class=\"muted\">Обновление рынка</span><strong id=\"updated\">—</strong><span class=\"muted\">каждые 5 минут</span></div></section>"},{"line":6,"text":"<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message m"},{"line":7,"text":"<section class=\"panel\" style=\"margin-top:14px\"><div class=\"title\"><div><h2>Позиции портфеля</h2><span class=\"muted\">Количество и средняя цена покупки</span></div></div><div class=\"table\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\"avgHeader\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\"rows\"></tbody></table></div></section>"},{"line":8,"text":"<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</option><option value=\"dump\">Резкое падение</option></select><input id=\"aThreshold\" type=\"number\" step=\"any\" placeholder=\"Порог\"><select id=\"aWindow\" class=\"hidden\"><option value=\"300\">5 минут</option><option value=\"900\" selected>15 минут</option><option value=\"1800\">30 минут</option><option value=\"3600\">1 час</option><option value=\"14400\">4 часа</option><option value=\"86400\">24 часа</option></select><input id=\"aEmail\" class=\"full\" type=\"email\" value=\"<EMAIL_REDACTED>\" placeholder=\"Email\"><button id=\"addAl"},{"line":9,"text":"<script>let state=null;let selected=\"hedera-hashgraph\";let currency=localStorage.getItem(\"portfolio_currency\")===\"RUB\"?\"RUB\":\"USD\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\"RUB\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\"RUB\"?\"ru-RU\":\"en-US\",{style:\"currency\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));const compactMoney=n=>(currency===\"RUB\"?\"₽\":\"$\")+new Intl.NumberFormat(currency===\"RUB\"?\"ru-RU\":\"en-US\",{notation:\"compact\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\"RUB\"?fx():1)).toFixed(currency===\"RUB\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\"/api/state\");if(r.status===401){location=\"/login\";return}state=await r.json();render()}function render(){let m=state.market.coin"},{"line":10,"text":"(()=>{"},{"line":11,"text":"const install=()=>{"},{"line":12,"text":"const button=document.getElementById(\"addAlert\");"},{"line":13,"text":"const coinEl=document.getElementById(\"aCoin\");"},{"line":14,"text":"const kindEl=document.getElementById(\"aKind\");"},{"line":15,"text":"const thresholdEl=document.getElementById(\"aThreshold\");"},{"line":16,"text":"const windowEl=document.getElementById(\"aWindow\");"},{"line":17,"text":"const emailEl=document.getElementById(\"aEmail\");"},{"line":18,"text":"if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;}"},{"line":19,"text":"button.type=\"button\";"},{"line":20,"text":"let statusEl=document.getElementById(\"alertCreateStatusV3\");"},{"line":21,"text":"if(!statusEl){"},{"line":22,"text":"statusEl=document.createElement(\"div\");"},{"line":23,"text":"statusEl.id=\"alertCreateStatusV3\";"},{"line":24,"text":"statusEl.className=\"muted full\";"},{"line":25,"text":"statusEl.setAttribute(\"aria-live\",\"polite\");"},{"line":28,"text":"const ensureOptions=()=>{"},{"line":29,"text":"try{"},{"line":30,"text":"if(typeof state!==\"undefined\"&&state&&state.coins){"},{"line":31,"text":"const ids=Object.keys(state.coins);"},{"line":32,"text":"if(ids.length&&coinEl.options.length!==ids.length){"},{"line":33,"text":"coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");"},{"line":34,"text":"}"},{"line":35,"text":"}"},{"line":44,"text":"const threshold=Number(String(thresholdEl.value||\"\").trim().replace(\",\",\".\"));"},{"line":45,"text":"const payload={"},{"line":46,"text":"coin_id:String(coinEl.value||\"\"),"},{"line":47,"text":"kind:String(kindEl.value||\"\"),"},{"line":48,"text":"threshold:threshold,"},{"line":52,"text":"statusEl.className=\"muted full\";"},{"line":53,"text":"statusEl.textContent=\"\";"},{"line":54,"text":"if(!payload.coin_id){statusEl.className=\"bad full\";statusEl.textContent=\"Не выбрана монета\";return;}"},{"line":55,"text":"if(!Number.isFinite(payload.threshold)||payload.threshold<=0){statusEl.className=\"bad full\";statusEl.textContent=\"Укажи положительный порог\";return;}"},{"line":56,"text":"if(!payload.email||!payload.email.includes(\"@\")){statusEl.className=\"bad full\";statusEl.textContent=\"Укажи корректный email\";return;}"},{"line":63,"text":"if(!beforeResponse.ok){throw new Error(\"предварительная проверка HTTP \"+beforeResponse.status);}"},{"line":64,"text":"const before=await beforeResponse.json();"},{"line":65,"text":"const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));"},{"line":66,"text":"const response=await fetch(\"/api/alert\",{"},{"line":67,"text":"method:\"POST\","},{"line":68,"text":"credentials:\"same-origin\","},{"line":79,"text":"const fresh=await verifyResponse.json();"},{"line":80,"text":"const returnedId=Number(body.id||0);"},{"line":81,"text":"const created=(fresh.alerts||[]).find(a=>{"},{"line":82,"text":"const id=Number(a.id);"},{"line":83,"text":"const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);"},{"line":84,"text":"return idIsNew&&a.coin_id===payload.coin_id&&a.kind===payload.kind&&Number(a.threshold)===payload.threshold&&String(a.email)===payload.email;"},{"line":85,"text":"});"},{"line":86,"text":"if(!created){throw new Error(\"сервер не подтвердил новую запись оповещения\");}"},{"line":87,"text":"try{state=fresh;}catch(_){}"},{"line":88,"text":"try{render();}catch(error){console.error(error);}"},{"line":89,"text":"try{renderAlerts();}catch(_){}"},{"line":90,"text":"thresholdEl.value=\"\";"},{"line":91,"text":"statusEl.className=\"good full\";"},{"line":92,"text":"statusEl.textContent=\"Оповещение создано\";"},{"line":93,"text":"}catch(error){"},{"line":94,"text":"statusEl.className=\"bad full\";"},{"line":107,"text":"})();"},{"line":108,"text":"</script>"},{"line":109,"text":"<script id=\"ALERT_LIST_RENDERER_V5\">"},{"line":110,"text":"(()=>{"},{"line":111,"text":"let lastState=null;"},{"line":112,"text":"let loading=false;"},{"line":113,"text":"const kindNames={"},{"line":114,"text":"price_above:\"цена ≥\","},{"line":115,"text":"price_below:\"цена ≤\","},{"line":116,"text":"cap_above:\"капитализация ≥\","},{"line":117,"text":"cap_below:\"капитализация ≤\","},{"line":120,"text":"};"},{"line":121,"text":"const activeCurrency=()=>typeof currency===\"string\"&&currency===\"RUB\"?\"RUB\":\"USD\";"},{"line":122,"text":"const exchangeRate=data=>Number(data&&data.market&&data.market.usd_rub||0);"},{"line":123,"text":"const converted=(data,value)=>Number(value||0)*(activeCurrency()===\"RUB\"&&exchangeRate(data)>0?exchangeRate(data):1);"},{"line":124,"text":"const alertMoney=(data,value)=>new Intl.NumberFormat(activeCurrency()===\"RUB\"?\"ru-RU\":\"en-US\",{style:\"currency\",currency:activeCurrency(),minimumFractionDigits:2,maximumFractionDigits:2}).format(converted(data,value));"},{"line":125,"text":"const make=(tag,className,value)=>{"},{"line":126,"text":"const element=document.createElement(tag);"},{"line":129,"text":"return element;"},{"line":130,"text":"};"},{"line":131,"text":"const alertLabel=(data,alert)=>{"},{"line":132,"text":"const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;"},{"line":133,"text":"const kind=kindNames[alert.kind]||alert.kind;"},{"line":134,"text":"if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";"},{"line":135,"text":"return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);"},{"line":136,"text":"};"},{"line":137,"text":"const draw=data=>{"},{"line":138,"text":"const box=document.getElementById(\"alertList\");"},{"line":139,"text":"if(!box||!data||!Array.isArray(data.alerts))return;"},{"line":140,"text":"lastState=data;"},{"line":141,"text":"box.replaceChildren();"},{"line":142,"text":"if(data.alerts.length===0){"},{"line":143,"text":"box.append(make(\"span\",\"muted\",\"Оповещений пока нет\"));"},{"line":144,"text":"return;"},{"line":145,"text":"}"},{"line":146,"text":"data.alerts.forEach(alert=>{"},{"line":147,"text":"const row=make(\"div\",\"alert\");"},{"line":148,"text":"row.dataset.alertRowV4=\"1\";"},{"line":149,"text":"const left=make(\"div\");"},{"line":150,"text":"left.append(make(\"b\",\"\",alertLabel(data,alert)));"},{"line":151,"text":"left.append(make(\"div\",\"muted\",alert.email||\"\"));"},{"line":152,"text":"const holding=(data.holdings&&data.holdings[alert.coin_id])||{};"},{"line":153,"text":"const quantity=Number(holding.quantity||0);"},{"line":154,"text":"const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);"},{"line":155,"text":"const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");"},{"line":156,"text":"const button=make(\"button\",\"btn ghost del\",\"Удалить\");"},{"line":157,"text":"button.type=\"button\";"},{"line":158,"text":"button.dataset.alertDeleteV4=String(alert.id);"},{"line":159,"text":"button.onclick=async event=>{"},{"line":160,"text":"event.preventDefault();"},{"line":161,"text":"button.disabled=true;"},{"line":162,"text":"try{"},{"line":163,"text":"const response=await fetch(\"/api/alert/\"+encodeURIComponent(alert.id),{method:\"DELETE\",credentials:\"same-origin\",cache:\"no-store\"});"},{"line":164,"text":"if(response.status===401){location=\"/login\";return;}"},{"line":165,"text":"if(!response.ok)throw new Error(\"HTTP \"+response.status);"},{"line":177,"text":"if(loading)return;"},{"line":178,"text":"loading=true;"},{"line":179,"text":"const box=document.getElementById(\"alertList\");"},{"line":180,"text":"try{"},{"line":181,"text":"const response=await fetch(\"/api/state\",{credentials:\"same-origin\",cache:\"no-store\",headers:{\"Accept\":\"application/json\"}});"},{"line":185,"text":"draw(data);"},{"line":186,"text":"}catch(error){"},{"line":187,"text":"if(box&&!box.children.length)box.append(make(\"span\",\"bad\",\"Ошибка загрузки оповещений: \"+String(error.message||error)));"},{"line":188,"text":"}finally{"},{"line":189,"text":"loading=false;"},{"line":191,"text":"};"},{"line":192,"text":"const install=()=>{"},{"line":193,"text":"const box=document.getElementById(\"alertList\");"},{"line":194,"text":"if(box){"},{"line":195,"text":"const observer=new MutationObserver(()=>{"},{"line":196,"text":"if(lastState&&lastState.alerts&&lastState.alerts.length&&!box.querySelector(\"[data-alert-row-v4]\")){"},{"line":197,"text":"queueMicrotask(()=>draw(lastState));"},{"line":198,"text":"}"},{"line":200,"text":"observer.observe(box,{childList:true,subtree:true});"},{"line":201,"text":"}"},{"line":202,"text":"const add=document.getElementById(\"addAlert\");"},{"line":203,"text":"if(add){"},{"line":204,"text":"add.addEventListener(\"click\",()=>{"},{"line":215,"text":"})();"},{"line":216,"text":"</script>"},{"line":217,"text":"<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}"},{"line":218,"text":"#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}"},{"line":219,"text":".coin-chart-title{margin-bottom:12px}"},{"line":220,"text":".tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}"},{"line":221,"text":".tv-accordion-item:last-child{border-bottom:0}"},{"line":226,"text":".tv-accordion-body{display:block;border-top:1px solid var(--l)}"},{"line":227,"text":".tv-widget-slot{display:block;width:100%;height:430px;min-width:0;background:#081823}"},{"line":228,"text":".tv-chart-message{min-height:20px;padding:8px 14px 12px}"},{"line":229,"text":"@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}"},{"line":230,"text":"</style>"},{"line":231,"text":"<script src=\"/vendor/lightweight-charts.js\"></script>"},{"line":232,"text":"<script id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\">(()=>{"},{"line":233,"text":"\"use strict\";"},{"line":234,"text":"const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";"},{"line":235,"text":"const symbols=[\"HBAR\",\"POL\",\"AVAX\",\"LINK\"];"},{"line":236,"text":"const charts=new Map();"},{"line":237,"text":"const requests=new Map();"},{"line":238,"text":"const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));"},{"line":278,"text":"return dedup;"},{"line":279,"text":"};"},{"line":280,"text":"const loadCandles=symbol=>{"},{"line":281,"text":"if(requests.has(symbol))return requests.get(symbol);"},{"line":282,"text":"const task=(async()=>{"},{"line":283,"text":"const response=await fetch(\"/api/klines/\"+encodeURIComponent(symbol)+\"?limit=1000&interval=1M\",{cache:\"no-store\",credentials:\"same-origin\",headers:{Accept:\"application/json\"}});"},{"line":284,"text":"if(response.status===401||response.status===403){location.href=\"/login\";throw new Error(\"Требуется повторный вход\");}"},{"line":285,"text":"if(!response.ok)throw new Error(\"HTTP \"+response.status);"},{"line":289,"text":"return normalizeCandles(raw);"},{"line":290,"text":"})();"},{"line":291,"text":"requests.set(symbol,task);"},{"line":292,"text":"task.catch(()=>requests.delete(symbol));"},{"line":293,"text":"return task;"},{"line":294,"text":"};"},{"line":295,"text":"const waitLibrary=async()=>{"},{"line":296,"text":"for(let attempt=0;attempt<150;attempt++){"},{"line":297,"text":"if(window.LightweightCharts&&typeof window.LightweightCharts.createChart===\"function\")return window.LightweightCharts;"},{"line":298,"text":"await sleep(100);"},{"line":299,"text":"}"},{"line":300,"text":"throw new Error(\"Библиотека Lightweight Charts не загрузилась\");"},{"line":301,"text":"};"},{"line":302,"text":"const widthOf=box=>Math.max(320,Math.floor(box.getBoundingClientRect().width||box.clientWidth||900));"},{"line":304,"text":"if(typeof api.addCandlestickSeries===\"function\")return api.addCandlestickSeries(options);"},{"line":305,"text":"if(typeof api.addSeries===\"function\"&&library.CandlestickSeries)return api.addSeries(library.CandlestickSeries,options);"},{"line":306,"text":"throw new Error(\"Версия Lightweight Charts не поддерживается\");"},{"line":307,"text":"};"},{"line":308,"text":"const hideLegacy=()=>{"},{"line":309,"text":"for(const id of [\"chart\",\"chartTitle\",\"marketStatus\"]){"},{"line":310,"text":"const element=document.getElementById(id);"},{"line":311,"text":"if(!element)continue;"},{"line":316,"text":"}"},{"line":317,"text":"};"},{"line":318,"text":"const createChart=async item=>{"},{"line":319,"text":"const symbol=item.dataset.symbol;"},{"line":320,"text":"if(item.dataset.chartState===\"ready\"){"},{"line":321,"text":"const instance=charts.get(symbol);"},{"line":322,"text":"await afterLayout();"},{"line":323,"text":"if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}"},{"line":324,"text":"return instance;"},{"line":325,"text":"}"},{"line":326,"text":"if(item.dataset.chartState===\"loading\"&&item._chartPromise)return item._chartPromise;"},{"line":327,"text":"const box=item.querySelector(\".tv-widget-slot\");"},{"line":328,"text":"const message=item.querySelector(\".tv-chart-message\");"},{"line":329,"text":"item.dataset.chartState=\"loading\";"},{"line":330,"text":"message.textContent=\"Загрузка графика…\";"},{"line":331,"text":"item._chartPromise=(async()=>{"},{"line":332,"text":"try{"},{"line":333,"text":"const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);"},{"line":334,"text":"await afterLayout();"},{"line":335,"text":"if(!item.open){item.dataset.chartState=\"idle\";message.textContent=\"Нажмите, чтобы открыть график\";return null;}"},{"line":336,"text":"box.replaceChildren();"},{"line":337,"text":"const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};"},{"line":338,"text":"const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});"},{"line":339,"text":"const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});"},{"line":340,"text":"series.setData(candles);"},{"line":341,"text":"api.timeScale().fitContent();"},{"line":343,"text":"const observer=typeof ResizeObserver===\"function\"?new ResizeObserver(resize):null;"},{"line":344,"text":"if(observer)observer.observe(box);else window.addEventListener(\"resize\",resize,{passive:true});"},{"line":345,"text":"const instance={symbol,api,series,box,observer,resize,candles};"},{"line":346,"text":"charts.set(symbol,instance);"},{"line":347,"text":"item.dataset.chartState=\"ready\";"},{"line":348,"text":"message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;"},{"line":349,"text":"return instance;"},{"line":350,"text":"}catch(error){"},{"line":351,"text":"item.dataset.chartState=\"error\";"},{"line":352,"text":"message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);"},{"line":353,"text":"console.error(\"Gram1 chart error\",symbol,error);"},{"line":354,"text":"return null;"},{"line":355,"text":"}finally{item._chartPromise=null;}"},{"line":356,"text":"})();"},{"line":357,"text":"return item._chartPromise;"},{"line":358,"text":"};"},{"line":359,"text":"const init=async()=>{"},{"line":360,"text":"hideLegacy();"},{"line":361,"text":"const root=document.getElementById(\"coinCharts\");"},{"line":362,"text":"if(!root||root.dataset.interactiveReady===\"1\")return;"},{"line":363,"text":"root.dataset.interactiveReady=\"1\";"}],"section_headings":[{"line":6,"text":"<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message m"},{"line":7,"text":"<section class=\"panel\" style=\"margin-top:14px\"><div class=\"title\"><div><h2>Позиции портфеля</h2><span class=\"muted\">Количество и средняя цена покупки</span></div></div><div class=\"table\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\"avgHeader\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\"rows\"></tbody></table></div></section>"},{"line":8,"text":"<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</option><option value=\"dump\">Резкое падение</option></select><input id=\"aThreshold\" type=\"number\" step=\"any\" placeholder=\"Порог\"><select id=\"aWindow\" class=\"hidden\"><option value=\"300\">5 минут</option><option value=\"900\" selected>15 минут</option><option value=\"1800\">30 минут</option><option value=\"3600\">1 час</option><option value=\"14400\">4 часа</option><option value=\"86400\">24 часа</option></select><input id=\"aEmail\" class=\"full\" type=\"email\" value=\"<EMAIL_REDACTED>\" placeholder=\"Email\"><button id=\"addAl"},{"line":397,"text":"<script id=\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\">(()=>{const move=()=>{const chart=document.getElementById(\"chart\");const panel=chart&&chart.closest(\".panel\");const main=document.querySelector(\"main\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\"1 / -1\";const title=document.getElementById(\"chartTitle\");if(title)title.textContent=\"Графики монет\";const note=panel.querySelector(\".muted\");if(note)note.textContent=\"Свечи Binance · 1 месяц · весь доступный период\"};document.addEventListener(\"DOMContentLoaded\",move);window.addEventListener(\"load\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>"}],"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<?\",(now-370*86400,))\n        c.commit()\n        triggered = evaluate(c,market)\n        history_rows = c.execute(\"SELECT COUNT(*) AS n FROM market_history\").fetchone()[\"n\"]\n    atomic_json(MARKET,market)\n    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)"}],"routes":[]}},"rollback_restored":null,"rollback_started":false,"schema":1,"selected_method":"ALIAS_PINNED_AS_DEBIAN","ssh_method_probes":[{"duration_seconds":0.132,"identity_count":null,"identity_sudo_read_ok":true,"name":"ALIAS_PINNED_AS_DEBIAN","rc":0,"stderr":""},{"duration_seconds":0.13,"identity_count":2,"identity_sudo_read_ok":true,"name":"DIRECT_PINNED_AS_DEBIAN_NO_PROXY","rc":0,"stderr":""}],"status":"OK"}

OUTPUT_END
CHAT_OUTPUT_END
