File size: 2,722 Bytes
83bcbe3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""
persistence.py — Save and restore alert engine state to/from a JSON file.

Called from main.py on startup (load) and graceful shutdown (save),
so tracked asset ranges survive bot restarts.
"""

import json
import os
from typing import TYPE_CHECKING

import config
from logger import get_logger

if TYPE_CHECKING:
    from alert_engine import AlertEngine

log = get_logger("persistence")


def save_state(engine: "AlertEngine") -> None:
    """
    Serialise the alert engine's tracked assets to PERSISTENCE_FILE.
    Writes atomically via a temp file to avoid corruption on crash.
    """
    details = engine.get_asset_details()
    payload = {
        "version": 1,
        "assets": [
            {
                "name": d["name"],
                "min_price": d["min"],
                "max_price": d["max"],
            }
            for d in details
        ],
    }

    tmp_path = config.PERSISTENCE_FILE + ".tmp"
    try:
        with open(tmp_path, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2)
        os.replace(tmp_path, config.PERSISTENCE_FILE)
        log.info("State saved → %s (%d assets)", config.PERSISTENCE_FILE, len(details))
    except OSError as exc:
        log.error("Failed to save state: %s", exc)
    finally:
        # Clean up temp file if replace failed
        if os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
            except OSError:
                pass


def load_state(engine: "AlertEngine") -> int:
    """
    Load tracked assets from PERSISTENCE_FILE into the alert engine.
    Returns the number of assets restored.
    Silently skips the file if it doesn't exist or is malformed.
    """
    if not os.path.exists(config.PERSISTENCE_FILE):
        log.info("No persistence file found at %s — starting fresh", config.PERSISTENCE_FILE)
        return 0

    try:
        with open(config.PERSISTENCE_FILE, "r", encoding="utf-8") as f:
            payload = json.load(f)

        assets = payload.get("assets", [])
        restored = 0
        for entry in assets:
            name = entry.get("name", "").upper()
            min_p = float(entry["min_price"])
            max_p = float(entry["max_price"])
            if name and min_p < max_p:
                engine.add_asset(name, min_p, max_p)
                restored += 1
            else:
                log.warning("Skipping invalid entry in state file: %s", entry)

        log.info("Restored %d asset(s) from %s", restored, config.PERSISTENCE_FILE)
        return restored

    except (json.JSONDecodeError, KeyError, ValueError, OSError) as exc:
        log.error("Failed to load state from %s: %s", config.PERSISTENCE_FILE, exc)
        return 0