feat: auto-queue orderbook retrain at coverage gate
This commit is contained in:
@@ -126,6 +126,7 @@ TORCH_ORDERBOOK_DB=runtime/orderbook_observations.sqlite3
|
|||||||
TORCH_ORDERBOOK_MIN_SAMPLES_PER_BUCKET=20
|
TORCH_ORDERBOOK_MIN_SAMPLES_PER_BUCKET=20
|
||||||
TORCH_ORDERBOOK_MIN_COVERED_BUCKETS=240
|
TORCH_ORDERBOOK_MIN_COVERED_BUCKETS=240
|
||||||
TORCH_ORDERBOOK_MIN_SYMBOLS=2
|
TORCH_ORDERBOOK_MIN_SYMBOLS=2
|
||||||
|
TORCH_ORDERBOOK_AUTO_CHECK_SECONDS=3600
|
||||||
|
|
||||||
# Forward-only gate for an offline-approved shadow model. Promotion remains an
|
# Forward-only gate for an offline-approved shadow model. Promotion remains an
|
||||||
# explicit authenticated API action after every check has passed.
|
# explicit authenticated API action after every check has passed.
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ Live-исполнение ведет журнал order intent до отправ
|
|||||||
- `GET /api/training/market-observations/manifest` — training-token manifest для инкрементальной синхронизации forward L1-данных.
|
- `GET /api/training/market-observations/manifest` — training-token manifest для инкрементальной синхронизации forward L1-данных.
|
||||||
- `GET /api/training/shadow` — состояние изолированной shadow-модели и повторного forward-gate.
|
- `GET /api/training/shadow` — состояние изолированной shadow-модели и повторного forward-gate.
|
||||||
- `POST /api/training/shadow/promote` — атомарное продвижение shadow-модели; возвращает `409`, пока 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/trades` — последние сделки.
|
||||||
- `GET /api/signals` — последние сигналы стратегии.
|
- `GET /api/signals` — последние сигналы стратегии.
|
||||||
- `GET /api/events` — события.
|
- `GET /api/events` — события.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Crypto spot trading bot package."""
|
"""Crypto spot trading bot package."""
|
||||||
|
|
||||||
__version__ = "1.1.0"
|
__version__ = "1.1.1"
|
||||||
|
|||||||
@@ -223,6 +223,17 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return training.request_retrain(payload)
|
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")
|
@app.post("/api/training/heartbeat")
|
||||||
async def training_heartbeat(
|
async def training_heartbeat(
|
||||||
payload: dict[str, Any] | None = None,
|
payload: dict[str, Any] | None = None,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ SHADOW_ARTIFACT_NAMES = (
|
|||||||
"torch_shadow_guard.json",
|
"torch_shadow_guard.json",
|
||||||
"torch_shadow_calibration.json",
|
"torch_shadow_calibration.json",
|
||||||
)
|
)
|
||||||
|
_LAST_ORDERBOOK_AUTO_CHECK = 0.0
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
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)
|
api_json(args, "/api/training/heartbeat", worker)
|
||||||
claim = api_json(args, "/api/training/claim", worker)
|
claim = api_json(args, "/api/training/claim", worker)
|
||||||
if not claim.get("claimed"):
|
if not claim.get("claimed"):
|
||||||
|
maybe_auto_queue_orderbook(args, repo_root, runtime_dir, log_path)
|
||||||
return
|
return
|
||||||
job = claim.get("job") if isinstance(claim.get("job"), dict) else {}
|
job = claim.get("job") if isinstance(claim.get("job"), dict) else {}
|
||||||
job_id = str(job.get("id") or "")
|
job_id = str(job.get("id") or "")
|
||||||
@@ -352,6 +354,45 @@ def prepare_orderbook_data(
|
|||||||
return result
|
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:
|
def friendly_training_message(message: str) -> str:
|
||||||
cleaned = message.strip()
|
cleaned = message.strip()
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
|
|||||||
Reference in New Issue
Block a user