diff --git a/README.md b/README.md index 08871cc..ee3529b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ python tools\train_lstm_forecaster.py --symbols BTCUSDT,ETHUSDT,SOLUSDT,XRPUSDT, Файл из `TIME_SERIES_LSTM_MODEL_PATH` читается ботом автоматически. Даже если LSTM-параметры сохранены, сделка меняется только тогда, когда текущая walk-forward проверка в `crypto_spot_bot/time_series.py` показывает качество лучше baseline. +Автопереобучение запускает тот же train-скрипт, пишет лог в `runtime/lstm_retrain.log` и защищается от параллельных запусков: + +```powershell +powershell -ExecutionPolicy Bypass -File tools\run_lstm_retrain.ps1 +powershell -ExecutionPolicy Bypass -File tools\install_windows_lstm_retrainer.ps1 +``` + +На Linux/Raspberry Pi можно включить user systemd timer: + +```bash +bash tools/run_lstm_retrain.sh +bash tools/install_lstm_retrainer_systemd.sh +``` + +По умолчанию расписание переобучает каждые 6 часов с `--limit 1000`; Windows-установщик фиксирует пары `BTCUSDT,ETHUSDT,SOLUSDT,XRPUSDT,LTCUSDT`, чтобы первый scheduled run был предсказуемым. Параметры можно переопределить через env: `LSTM_RETRAIN_SYMBOLS`, `LSTM_RETRAIN_LIMIT`, `LSTM_RETRAIN_LOOKBACKS`, `LSTM_RETRAIN_UNITS`, `LSTM_RETRAIN_RIDGES`, `LSTM_RETRAIN_INTERVAL`, `LSTM_RETRAIN_ENV`. + ## Docker ```bash diff --git a/tools/install_lstm_retrainer_systemd.sh b/tools/install_lstm_retrainer_systemd.sh new file mode 100755 index 0000000..5ba96ee --- /dev/null +++ b/tools/install_lstm_retrainer_systemd.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SYSTEMD_DIR="$HOME/.config/systemd/user" +SERVICE_NAME="tradebot-lstm-retrainer.service" +TIMER_NAME="tradebot-lstm-retrainer.timer" + +mkdir -p "$SYSTEMD_DIR" + +cat > "$SYSTEMD_DIR/$SERVICE_NAME" < "$SYSTEMD_DIR/$TIMER_NAME" <&1 | Tee-Object -FilePath $LogFile -Append + if ($LASTEXITCODE -ne 0) { + throw "Trainer failed with exit code $LASTEXITCODE." + } + Write-RetrainLog "Finished LSTM retrain." +} +catch { + Write-RetrainLog "ERROR: $($_.Exception.Message)" + exit 1 +} +finally { + if ($pushedLocation) { + Pop-Location -ErrorAction SilentlyContinue + } + if ($hasLock) { + $mutex.ReleaseMutex() + } + $mutex.Dispose() +} diff --git a/tools/run_lstm_retrain.sh b/tools/run_lstm_retrain.sh new file mode 100755 index 0000000..962b93e --- /dev/null +++ b/tools/run_lstm_retrain.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RUNTIME_DIR="$REPO_ROOT/runtime" +LOG_FILE="$RUNTIME_DIR/lstm_retrain.log" +LOCK_FILE="$RUNTIME_DIR/lstm_retrain.lock" + +mkdir -p "$RUNTIME_DIR" + +log() { + printf '[%s] %s\n' "$(date -Is)" "$*" | tee -a "$LOG_FILE" +} + +if command -v flock >/dev/null 2>&1; then + exec 9>"$LOCK_FILE" + if ! flock -n 9; then + log "Another LSTM retrain is already running; skipping." + exit 0 + fi +else + LOCK_DIR="$LOCK_FILE.d" + if ! mkdir "$LOCK_DIR" 2>/dev/null; then + log "Another LSTM retrain is already running; skipping." + exit 0 + fi + trap 'rmdir "$LOCK_DIR"' EXIT +fi + +if [[ -x "$REPO_ROOT/.venv/bin/python" ]]; then + PYTHON="$REPO_ROOT/.venv/bin/python" +elif command -v python3 >/dev/null 2>&1; then + PYTHON="$(command -v python3)" +elif command -v python >/dev/null 2>&1; then + PYTHON="$(command -v python)" +else + log "ERROR: Python was not found." + exit 1 +fi + +SYMBOLS="${LSTM_RETRAIN_SYMBOLS:-}" +LIMIT="${LSTM_RETRAIN_LIMIT:-1000}" +LOOKBACKS="${LSTM_RETRAIN_LOOKBACKS:-16,32}" +UNITS="${LSTM_RETRAIN_UNITS:-4,6}" +RIDGES="${LSTM_RETRAIN_RIDGES:-0.001}" +INTERVAL="${LSTM_RETRAIN_INTERVAL:-}" +ENV_FILE="${LSTM_RETRAIN_ENV:-}" + +if [[ -z "$ENV_FILE" && -f "$REPO_ROOT/.env" ]]; then + ENV_FILE="$REPO_ROOT/.env" +fi + +args=( + "tools/train_lstm_forecaster.py" + "--limit" "$LIMIT" + "--lookbacks" "$LOOKBACKS" + "--units" "$UNITS" + "--ridges" "$RIDGES" +) + +if [[ -n "$SYMBOLS" ]]; then args+=("--symbols" "$SYMBOLS"); fi +if [[ -n "$INTERVAL" ]]; then args+=("--interval" "$INTERVAL"); fi +if [[ -n "$ENV_FILE" ]]; then args+=("--env" "$ENV_FILE"); fi + +cd "$REPO_ROOT" +log "Starting LSTM retrain: $PYTHON -u ${args[*]}" +"$PYTHON" -u "${args[@]}" 2>&1 | tee -a "$LOG_FILE" +log "Finished LSTM retrain." diff --git a/tools/train_lstm_forecaster.py b/tools/train_lstm_forecaster.py index 13595ca..fd86a2e 100644 --- a/tools/train_lstm_forecaster.py +++ b/tools/train_lstm_forecaster.py @@ -56,7 +56,9 @@ def main() -> None: ) output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(artifact, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + tmp_output = output.with_name(f"{output.name}.tmp") + tmp_output.write_text(json.dumps(artifact, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + tmp_output.replace(output) print(f"saved {output}")