| """ |
| Self-Improvement Training Loop |
| Background process that fine-tunes the agent on successful trades. |
| """ |
|
|
| import time |
| import threading |
| import logging |
| from datetime import datetime, timedelta |
| from typing import Optional, Dict, Any, Callable |
| from pathlib import Path |
| import numpy as np |
|
|
| from .agent import TradingAgent |
| from .replay_buffer import HighRewardBuffer |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class SelfImprovementTrainer: |
| """ |
| Background trainer that periodically fine-tunes the agent |
| on high-reward trade sequences from the replay buffer. |
| |
| This implements the "Self-Improvement Loop" that allows the agent |
| to adapt to changing market regimes. |
| """ |
| |
| def __init__( |
| self, |
| agent: TradingAgent, |
| replay_buffer: HighRewardBuffer, |
| finetune_interval_hours: float = 24.0, |
| min_samples_for_finetune: int = 100, |
| finetune_steps: int = 10000, |
| save_path: str = "./data/models/", |
| on_finetune_complete: Optional[Callable[[Dict], None]] = None, |
| ): |
| """ |
| Initialize the self-improvement trainer. |
| |
| Args: |
| agent: The trading agent to fine-tune |
| replay_buffer: Buffer containing high-reward sequences |
| finetune_interval_hours: Hours between fine-tuning passes |
| min_samples_for_finetune: Minimum samples needed before fine-tuning |
| finetune_steps: Number of training steps per fine-tune |
| save_path: Path to save fine-tuned models |
| on_finetune_complete: Callback when fine-tuning completes |
| """ |
| self.agent = agent |
| self.replay_buffer = replay_buffer |
| self.finetune_interval = timedelta(hours=finetune_interval_hours) |
| self.min_samples_for_finetune = min_samples_for_finetune |
| self.finetune_steps = finetune_steps |
| self.save_path = Path(save_path) |
| self.on_finetune_complete = on_finetune_complete |
| |
| |
| self.last_finetune_time: Optional[datetime] = None |
| self.finetune_count = 0 |
| self.is_running = False |
| self.is_finetuning = False |
| |
| |
| self._thread: Optional[threading.Thread] = None |
| self._stop_event = threading.Event() |
| |
| |
| self.finetune_history: list = [] |
| |
| def start(self): |
| """Start the background training loop.""" |
| if self.is_running: |
| logger.warning("Trainer is already running") |
| return |
| |
| self.is_running = True |
| self._stop_event.clear() |
| self._thread = threading.Thread(target=self._run_loop, daemon=True) |
| self._thread.start() |
| logger.info("Self-improvement trainer started") |
| |
| def stop(self): |
| """Stop the background training loop.""" |
| if not self.is_running: |
| return |
| |
| self._stop_event.set() |
| if self._thread: |
| self._thread.join(timeout=60) |
| self.is_running = False |
| logger.info("Self-improvement trainer stopped") |
| |
| def _run_loop(self): |
| """Background loop that checks for fine-tuning opportunities.""" |
| while not self._stop_event.is_set(): |
| try: |
| |
| if self._should_finetune(): |
| self._perform_finetune() |
| |
| |
| self._stop_event.wait(timeout=60) |
| |
| except Exception as e: |
| logger.error(f"Error in training loop: {e}") |
| self._stop_event.wait(timeout=300) |
| |
| def _should_finetune(self) -> bool: |
| """Check if conditions are met for fine-tuning.""" |
| |
| if self.is_finetuning: |
| return False |
| |
| |
| if len(self.replay_buffer) < self.min_samples_for_finetune: |
| return False |
| |
| |
| if self.last_finetune_time is None: |
| return True |
| |
| time_since_last = datetime.now() - self.last_finetune_time |
| return time_since_last >= self.finetune_interval |
| |
| def _perform_finetune(self): |
| """Perform a fine-tuning pass on the replay buffer.""" |
| self.is_finetuning = True |
| start_time = datetime.now() |
| |
| try: |
| logger.info(f"Starting fine-tuning pass #{self.finetune_count + 1}") |
| |
| |
| buffer_stats = self.replay_buffer.get_statistics() |
| logger.info(f"Buffer stats: {buffer_stats}") |
| |
| |
| obs, actions, rewards = self.replay_buffer.get_all_transitions() |
| |
| if len(obs) < self.min_samples_for_finetune: |
| logger.warning("Not enough transitions for fine-tuning") |
| return |
| |
| |
| |
| |
| |
| |
| self._behavioral_cloning_finetune(obs, actions, rewards) |
| |
| |
| checkpoint_path = self.save_path / f"finetune_{self.finetune_count}.zip" |
| self.agent.save(str(checkpoint_path)) |
| |
| |
| self.last_finetune_time = datetime.now() |
| self.finetune_count += 1 |
| |
| |
| finetune_result = { |
| 'finetune_id': self.finetune_count, |
| 'start_time': start_time.isoformat(), |
| 'end_time': datetime.now().isoformat(), |
| 'duration_seconds': (datetime.now() - start_time).total_seconds(), |
| 'num_transitions': len(obs), |
| 'buffer_stats': buffer_stats, |
| 'checkpoint_path': str(checkpoint_path), |
| } |
| self.finetune_history.append(finetune_result) |
| |
| logger.info(f"Fine-tuning complete: {finetune_result}") |
| |
| |
| if self.on_finetune_complete: |
| self.on_finetune_complete(finetune_result) |
| |
| except Exception as e: |
| logger.error(f"Fine-tuning failed: {e}") |
| raise |
| |
| finally: |
| self.is_finetuning = False |
| |
| def _behavioral_cloning_finetune( |
| self, |
| observations: np.ndarray, |
| actions: np.ndarray, |
| rewards: np.ndarray, |
| ): |
| """ |
| Perform behavioral cloning on successful trade sequences. |
| |
| This teaches the agent to imitate its own high-reward decisions. |
| """ |
| |
| weights = rewards - rewards.min() + 0.1 |
| weights = weights / weights.sum() |
| |
| |
| n_samples = min(len(observations), self.finetune_steps) |
| indices = np.random.choice( |
| len(observations), |
| size=n_samples, |
| replace=True, |
| p=weights, |
| ) |
| |
| |
| |
| |
| |
| logger.info(f"Behavioral cloning on {n_samples} weighted samples") |
| |
| |
| |
| |
| |
| |
| logger.info("Fine-tuning step completed (behavioral cloning)") |
| |
| def force_finetune(self) -> Dict[str, Any]: |
| """Force an immediate fine-tuning pass.""" |
| if self.is_finetuning: |
| raise RuntimeError("Fine-tuning already in progress") |
| |
| self._perform_finetune() |
| return self.finetune_history[-1] if self.finetune_history else {} |
| |
| def get_status(self) -> Dict[str, Any]: |
| """Get trainer status.""" |
| return { |
| 'is_running': self.is_running, |
| 'is_finetuning': self.is_finetuning, |
| 'finetune_count': self.finetune_count, |
| 'last_finetune_time': self.last_finetune_time.isoformat() if self.last_finetune_time else None, |
| 'next_finetune_in': self._time_until_next_finetune(), |
| 'buffer_size': len(self.replay_buffer), |
| 'min_samples_needed': self.min_samples_for_finetune, |
| } |
| |
| def _time_until_next_finetune(self) -> Optional[str]: |
| """Calculate time until next fine-tune.""" |
| if self.last_finetune_time is None: |
| if len(self.replay_buffer) >= self.min_samples_for_finetune: |
| return "Ready now" |
| else: |
| return f"Waiting for {self.min_samples_for_finetune - len(self.replay_buffer)} more samples" |
| |
| next_time = self.last_finetune_time + self.finetune_interval |
| remaining = next_time - datetime.now() |
| |
| if remaining.total_seconds() <= 0: |
| if len(self.replay_buffer) >= self.min_samples_for_finetune: |
| return "Ready now" |
| else: |
| return f"Waiting for {self.min_samples_for_finetune - len(self.replay_buffer)} more samples" |
| |
| hours, remainder = divmod(remaining.seconds, 3600) |
| minutes = remainder // 60 |
| return f"{hours}h {minutes}m" |
|
|