From 61987482e120c8b88f64ae4dedec4ce0fcc62eb5 Mon Sep 17 00:00:00 2001 From: jerick Date: Mon, 4 May 2026 11:09:17 -0400 Subject: [PATCH] added coin cooldown to stop coin chase --- .env.example | 2 +- bot.py | 6 +++++- config.py | 4 ++++ portfolio.py | 52 +++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 6c7f095..46e71fb 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,4 @@ KRAKEN_API_SECRET=your_api_secret_here # Starting balance for paper trading simulation # Needs to be at least (max_positions * min_order_usd) to fill all slots. # Default config: 5 positions * $15 min = $75 minimum, $500+ recommended. -PAPER_BALANCE_USD=1000 +PAPER_BALANCE_USD=1000 \ No newline at end of file diff --git a/bot.py b/bot.py index 3822cf8..031a995 100644 --- a/bot.py +++ b/bot.py @@ -139,7 +139,11 @@ def run_cycle(config: Config) -> bool: return bool(closed) try: - opportunities = scanner.scan(exclude_pairs=portfolio.open_pairs()) + cooled_down = { + pair for pair in portfolio._cooldowns + if portfolio.on_cooldown(pair, config.sold_cooldown_hours) + } + opportunities = scanner.scan(exclude_pairs=portfolio.open_pairs() | cooled_down) except KrakenError as exc: log.error("Market scan failed: %s", exc) return bool(closed) diff --git a/config.py b/config.py index 1d2b851..e542359 100644 --- a/config.py +++ b/config.py @@ -41,6 +41,10 @@ class Config: # Prevents dead money from tying up capital indefinitely. max_hold_hours: int = 72 + # Cooldown: hours to wait before re-buying a pair that was just sold. + # Prevents immediately re-entering a coin whose momentum has stalled or reversed. + sold_cooldown_hours: float = 4.0 + # ── Execution ──────────────────────────────────────────────────────────── # ALWAYS start with paper_trading=True and verify behaviour before going live. # Set to False only after you understand the bot's decisions. diff --git a/portfolio.py b/portfolio.py index 8eeef10..695da6b 100644 --- a/portfolio.py +++ b/portfolio.py @@ -42,21 +42,29 @@ class Position: class Portfolio: def __init__(self, filepath: str): self._path = Path(filepath) + self._cooldown_path = self._path.with_suffix(".cooldown.json") self.positions: dict[str, Position] = {} + self._cooldowns: dict[str, str] = {} # pair → ISO sell time self._load() def _load(self) -> None: - if not self._path.exists(): - return - try: - content = self._path.read_text().strip() - if not content: - return - data = json.loads(content) - self.positions = {pair: Position(**fields) for pair, fields in data.items()} - log.info("Loaded %d position(s) from %s", len(self.positions), self._path) - except Exception as exc: - log.error("Could not load positions file: %s", exc) + if self._path.exists(): + try: + content = self._path.read_text().strip() + if content: + data = json.loads(content) + self.positions = {pair: Position(**fields) for pair, fields in data.items()} + log.info("Loaded %d position(s) from %s", len(self.positions), self._path) + except Exception as exc: + log.error("Could not load positions file: %s", exc) + + if self._cooldown_path.exists(): + try: + content = self._cooldown_path.read_text().strip() + if content: + self._cooldowns = json.loads(content) + except Exception as exc: + log.error("Could not load cooldown file: %s", exc) def _save(self) -> None: try: @@ -69,6 +77,12 @@ class Portfolio: except Exception as exc: log.error("Could not save positions file: %s", exc) + def _save_cooldowns(self) -> None: + try: + self._cooldown_path.write_text(json.dumps(self._cooldowns, indent=2)) + except Exception as exc: + log.error("Could not save cooldown file: %s", exc) + def add(self, position: Position) -> None: self.positions[position.pair] = position self._save() @@ -81,8 +95,24 @@ class Portfolio: pos = self.positions.pop(pair, None) if pos: self._save() + self._cooldowns[pair] = datetime.now(tz=timezone.utc).isoformat() + self._save_cooldowns() return pos + def on_cooldown(self, pair: str, cooldown_hours: float) -> bool: + sell_time_str = self._cooldowns.get(pair) + if not sell_time_str: + return False + sell_time = datetime.fromisoformat(sell_time_str) + elapsed = (datetime.now(tz=timezone.utc) - sell_time).total_seconds() / 3600 + if elapsed < cooldown_hours: + log.debug("%s on cooldown: %.1fh of %.1fh elapsed", pair, elapsed, cooldown_hours) + return True + # Cooldown expired — clean it up + del self._cooldowns[pair] + self._save_cooldowns() + return False + def get(self, pair: str) -> Position | None: return self.positions.get(pair)