feat: add orderbook shadow training pipeline
This commit is contained in:
@@ -20,12 +20,24 @@ from urllib.error import URLError
|
||||
from urllib.request import Request
|
||||
from urllib.request import urlopen
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from crypto_spot_bot.orderbook_features import load_orderbook_feature_map
|
||||
from tools.sync_orderbook_observations import sync_orderbook_observations
|
||||
|
||||
|
||||
ARTIFACT_NAMES = (
|
||||
"lstm_forecaster.json",
|
||||
"torch_retrain_guard.json",
|
||||
"torch_threshold_calibration.json",
|
||||
)
|
||||
SHADOW_ARTIFACT_NAMES = (
|
||||
"lstm_forecaster.shadow.json",
|
||||
"torch_shadow_guard.json",
|
||||
"torch_shadow_calibration.json",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -65,7 +77,35 @@ def poll_once(args: argparse.Namespace, repo_root: Path, runtime_dir: Path, log_
|
||||
message = ""
|
||||
summary: dict[str, Any] = {}
|
||||
try:
|
||||
run_retrain(args, job_id, lease_token, job, repo_root, log_path)
|
||||
parameters = job.get("parameters") if isinstance(job.get("parameters"), dict) else {}
|
||||
use_orderbook = parameters.get("use_orderbook", True) is not False
|
||||
orderbook_status: dict[str, Any] = {}
|
||||
if use_orderbook:
|
||||
report_progress(
|
||||
args,
|
||||
job_id,
|
||||
lease_token,
|
||||
"running",
|
||||
"orderbook_sync",
|
||||
4,
|
||||
"Синхронизирую forward-наблюдения стакана",
|
||||
)
|
||||
orderbook_status = prepare_orderbook_data(args, repo_root, parameters, log_path)
|
||||
if orderbook_status["state"] != "ready":
|
||||
summary = orderbook_status
|
||||
message = "forward orderbook coverage is still accumulating"
|
||||
success = True
|
||||
log(log_path, f"Job {job_id} remains in collecting state: {orderbook_status}")
|
||||
return
|
||||
run_retrain(
|
||||
args,
|
||||
job_id,
|
||||
lease_token,
|
||||
job,
|
||||
repo_root,
|
||||
log_path,
|
||||
orderbook_db=(repo_root / "runtime" / "orderbook_observations.sqlite3") if use_orderbook else None,
|
||||
)
|
||||
summary = read_json(runtime_dir / "torch_retrain_guard.json")
|
||||
accepted = summary.get("accepted") is True
|
||||
if accepted:
|
||||
@@ -78,12 +118,20 @@ def poll_once(args: argparse.Namespace, repo_root: Path, runtime_dir: Path, log_
|
||||
72,
|
||||
"Обучение завершено, загружаю артефакты",
|
||||
)
|
||||
for name in ARTIFACT_NAMES:
|
||||
artifact_names = SHADOW_ARTIFACT_NAMES if use_orderbook else ARTIFACT_NAMES
|
||||
if use_orderbook:
|
||||
summary["deployment"] = "shadow"
|
||||
summary["orderbook"] = orderbook_status
|
||||
for name in artifact_names:
|
||||
path = runtime_dir / name
|
||||
if path.is_file():
|
||||
upload_artifact(args, job_id, lease_token, path, log_path)
|
||||
message = "training completed; candidate accepted"
|
||||
log(log_path, f"Completed retrain job {job_id}; candidate accepted")
|
||||
message = (
|
||||
"training completed; candidate staged in shadow"
|
||||
if use_orderbook
|
||||
else "training completed; candidate accepted"
|
||||
)
|
||||
log(log_path, f"Completed retrain job {job_id}; {message}")
|
||||
else:
|
||||
reason = str(summary.get("reason") or "validation failed")
|
||||
message = f"training completed; candidate rejected by quality gate: {reason}"
|
||||
@@ -109,6 +157,7 @@ def run_retrain(
|
||||
job: dict[str, Any],
|
||||
repo_root: Path,
|
||||
log_path: Path,
|
||||
orderbook_db: Path | None = None,
|
||||
) -> None:
|
||||
script = repo_root / "tools" / "run_torch_retrain.ps1"
|
||||
if not script.is_file():
|
||||
@@ -153,6 +202,14 @@ def run_retrain(
|
||||
cmd.append("-Pooled")
|
||||
if parameters.get("resume_candidate") is True:
|
||||
cmd.append("-ResumeCandidate")
|
||||
if orderbook_db is not None:
|
||||
cmd.extend(["-OrderbookDb", str(orderbook_db)])
|
||||
for key, ps_arg, default in (
|
||||
("orderbook_min_samples_per_bucket", "-OrderbookMinSamplesPerBucket", 20),
|
||||
("orderbook_min_covered_buckets", "-OrderbookMinCoveredBuckets", 240),
|
||||
("orderbook_min_symbols", "-OrderbookMinSymbols", 2),
|
||||
):
|
||||
cmd.extend([ps_arg, str(int(parameters.get(key, default) or default))])
|
||||
log(log_path, "Running retrain: " + " ".join(quote_for_log(part) for part in cmd))
|
||||
report_progress(
|
||||
args,
|
||||
@@ -236,6 +293,65 @@ def run_retrain(
|
||||
)
|
||||
|
||||
|
||||
def prepare_orderbook_data(
|
||||
args: argparse.Namespace,
|
||||
repo_root: Path,
|
||||
parameters: dict[str, Any],
|
||||
log_path: Path,
|
||||
) -> dict[str, Any]:
|
||||
database_path = repo_root / "runtime" / "orderbook_observations.sqlite3"
|
||||
token = args.api_auth or os.environ.get("TRADEBOT_API_AUTH", "")
|
||||
sync_result = sync_orderbook_observations(
|
||||
api_base_url=args.api_base_url,
|
||||
token=token,
|
||||
database_path=database_path,
|
||||
)
|
||||
interval = str(parameters.get("interval") or os.environ.get("TORCH_RETRAIN_INTERVAL") or "60")
|
||||
minimum_samples = int(parameters.get("orderbook_min_samples_per_bucket", 20) or 20)
|
||||
minimum_buckets = int(parameters.get("orderbook_min_covered_buckets", 240) or 240)
|
||||
minimum_symbols = int(parameters.get("orderbook_min_symbols", 2) or 2)
|
||||
requested_symbols = {
|
||||
item.strip().upper()
|
||||
for item in str(parameters.get("symbols") or "").split(",")
|
||||
if item.strip()
|
||||
}
|
||||
_features, manifest = load_orderbook_feature_map(
|
||||
database_path,
|
||||
interval=interval,
|
||||
symbols=sorted(requested_symbols) if requested_symbols else None,
|
||||
min_samples_per_bucket=minimum_samples,
|
||||
)
|
||||
eligible = sorted(
|
||||
symbol
|
||||
for symbol, row in manifest.items()
|
||||
if int(row.get("covered_buckets", 0) or 0) >= minimum_buckets
|
||||
)
|
||||
state = "ready" if len(eligible) >= minimum_symbols else "collecting_orderbook"
|
||||
coverage = {
|
||||
symbol: int(row.get("covered_buckets", 0) or 0)
|
||||
for symbol, row in sorted(manifest.items())
|
||||
}
|
||||
result = {
|
||||
"accepted": False,
|
||||
"state": state,
|
||||
"reason": (
|
||||
"orderbook coverage ready for training"
|
||||
if state == "ready"
|
||||
else "forward orderbook coverage is below the configured minimum"
|
||||
),
|
||||
"eligible_symbols": eligible,
|
||||
"eligible_symbol_count": len(eligible),
|
||||
"minimum_symbols": minimum_symbols,
|
||||
"minimum_covered_buckets": minimum_buckets,
|
||||
"minimum_samples_per_bucket": minimum_samples,
|
||||
"covered_buckets_by_symbol": coverage,
|
||||
"local_samples": int(sync_result.get("local_samples", 0) or 0),
|
||||
"downloaded_samples": int(sync_result.get("downloaded", 0) or 0),
|
||||
}
|
||||
log(log_path, "Orderbook preparation: " + json.dumps(result, ensure_ascii=False, sort_keys=True))
|
||||
return result
|
||||
|
||||
|
||||
def friendly_training_message(message: str) -> str:
|
||||
cleaned = message.strip()
|
||||
if not cleaned:
|
||||
|
||||
Reference in New Issue
Block a user