From 5082be2e5a09566d405d44d62e5779a88b1e0c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Wed, 15 Jul 2026 09:50:57 +0300 Subject: [PATCH] feat: auto-queue orderbook retrain at coverage gate --- .env.example | 1 + README.md | 1 + crypto_spot_bot/__init__.py | 2 +- crypto_spot_bot/dashboard.py | 11 +++++++++ tools/windows_training_agent.py | 41 +++++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 41cc4ff..fff7231 100644 --- a/.env.example +++ b/.env.example @@ -126,6 +126,7 @@ TORCH_ORDERBOOK_DB=runtime/orderbook_observations.sqlite3 TORCH_ORDERBOOK_MIN_SAMPLES_PER_BUCKET=20 TORCH_ORDERBOOK_MIN_COVERED_BUCKETS=240 TORCH_ORDERBOOK_MIN_SYMBOLS=2 +TORCH_ORDERBOOK_AUTO_CHECK_SECONDS=3600 # Forward-only gate for an offline-approved shadow model. Promotion remains an # explicit authenticated API action after every check has passed. diff --git a/README.md b/README.md index 4d3663f..c552214 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ Live-исполнение ведет журнал order intent до отправ - `GET /api/training/market-observations/manifest` — training-token manifest для инкрементальной синхронизации forward L1-данных. - `GET /api/training/shadow` — состояние изолированной shadow-модели и повторного forward-gate. - `POST /api/training/shadow/promote` — атомарное продвижение shadow-модели; возвращает `409`, пока forward-gate не пройден. +- `POST /api/training/retrain/auto` — ограниченная training-token команда Windows-agent; ставит только orderbook-retrain без произвольных параметров. - `GET /api/trades` — последние сделки. - `GET /api/signals` — последние сигналы стратегии. - `GET /api/events` — события. diff --git a/crypto_spot_bot/__init__.py b/crypto_spot_bot/__init__.py index 8c8e097..1fb70e9 100644 --- a/crypto_spot_bot/__init__.py +++ b/crypto_spot_bot/__init__.py @@ -1,3 +1,3 @@ """Crypto spot trading bot package.""" -__version__ = "1.1.0" +__version__ = "1.1.1" diff --git a/crypto_spot_bot/dashboard.py b/crypto_spot_bot/dashboard.py index a4ce191..fd351ce 100644 --- a/crypto_spot_bot/dashboard.py +++ b/crypto_spot_bot/dashboard.py @@ -223,6 +223,17 @@ def create_app(settings: Settings | None = None) -> FastAPI: ) -> dict[str, Any]: return training.request_retrain(payload) + @app.post("/api/training/retrain/auto") + async def training_retrain_auto( + _: None = Depends(authorizer.require_training), + ) -> dict[str, Any]: + return training.request_retrain( + { + "source": "windows-agent-auto", + "parameters": {"use_orderbook": True}, + } + ) + @app.post("/api/training/heartbeat") async def training_heartbeat( payload: dict[str, Any] | None = None, diff --git a/tools/windows_training_agent.py b/tools/windows_training_agent.py index 3c244b1..910e06e 100644 --- a/tools/windows_training_agent.py +++ b/tools/windows_training_agent.py @@ -38,6 +38,7 @@ SHADOW_ARTIFACT_NAMES = ( "torch_shadow_guard.json", "torch_shadow_calibration.json", ) +_LAST_ORDERBOOK_AUTO_CHECK = 0.0 def main() -> None: @@ -63,6 +64,7 @@ def poll_once(args: argparse.Namespace, repo_root: Path, runtime_dir: Path, log_ api_json(args, "/api/training/heartbeat", worker) claim = api_json(args, "/api/training/claim", worker) if not claim.get("claimed"): + maybe_auto_queue_orderbook(args, repo_root, runtime_dir, log_path) return job = claim.get("job") if isinstance(claim.get("job"), dict) else {} job_id = str(job.get("id") or "") @@ -352,6 +354,45 @@ def prepare_orderbook_data( return result +def maybe_auto_queue_orderbook( + args: argparse.Namespace, + repo_root: Path, + runtime_dir: Path, + log_path: Path, +) -> None: + global _LAST_ORDERBOOK_AUTO_CHECK + try: + interval_seconds = max( + 300, + int(os.environ.get("TORCH_ORDERBOOK_AUTO_CHECK_SECONDS", "3600") or 3600), + ) + except ValueError: + interval_seconds = 3600 + now = time.monotonic() + if _LAST_ORDERBOOK_AUTO_CHECK and now - _LAST_ORDERBOOK_AUTO_CHECK < interval_seconds: + return + _LAST_ORDERBOOK_AUTO_CHECK = now + marker_path = runtime_dir / "orderbook_auto_queue.json" + if marker_path.is_file() or (runtime_dir / "lstm_forecaster.shadow.json").is_file(): + return + status = prepare_orderbook_data(args, repo_root, {}, log_path) + if status.get("state") != "ready": + return + response = api_json(args, "/api/training/retrain/auto", {}) + if not response.get("queued"): + log(log_path, f"Automatic orderbook retrain was not queued: {response.get('reason', 'unknown')}") + return + marker = { + "queued_at": datetime.now().astimezone().isoformat(timespec="seconds"), + "job_id": (response.get("job") or {}).get("id"), + "coverage": status, + } + marker_tmp = marker_path.with_suffix(".tmp") + marker_tmp.write_text(json.dumps(marker, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + marker_tmp.replace(marker_path) + log(log_path, f"Automatically queued orderbook retrain job {marker['job_id']}") + + def friendly_training_message(message: str) -> str: cleaned = message.strip() if not cleaned: