File size: 9,270 Bytes
fc115d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""
High-Reward Replay Buffer
Stores successful trade sequences for self-improvement fine-tuning.
"""

import numpy as np
import pickle
from pathlib import Path
from typing import Optional, Dict, List, Tuple, Any
from collections import deque
from dataclasses import dataclass, field
import threading
import time


@dataclass
class TradeSequence:
    """Represents a high-reward trade sequence."""
    observations: np.ndarray
    actions: np.ndarray
    rewards: np.ndarray
    total_reward: float
    sharpe_ratio: float
    trade_pnl: float
    timestamp: float = field(default_factory=time.time)
    
    def __len__(self) -> int:
        return len(self.observations)


class HighRewardBuffer:
    """
    Buffer that stores high-reward trade sequences for fine-tuning.
    
    Only sequences that exceed the reward threshold are stored.
    Implements a priority queue based on total reward.
    """
    
    def __init__(
        self,
        max_size: int = 10000,
        reward_threshold: float = 0.5,
        min_sequence_length: int = 10,
        save_path: Optional[str] = None,
    ):
        """
        Initialize the replay buffer.
        
        Args:
            max_size: Maximum number of sequences to store
            reward_threshold: Minimum total reward to store a sequence
            min_sequence_length: Minimum sequence length to consider
            save_path: Path to save/load buffer
        """
        self.max_size = max_size
        self.reward_threshold = reward_threshold
        self.min_sequence_length = min_sequence_length
        self.save_path = save_path
        
        self.sequences: List[TradeSequence] = []
        self.lock = threading.Lock()
        
        # Statistics
        self.total_sequences_seen = 0
        self.total_sequences_stored = 0
        
        # Load existing buffer if available
        if save_path and Path(save_path).exists():
            self.load()
            
    def add_sequence(
        self,
        observations: np.ndarray,
        actions: np.ndarray,
        rewards: np.ndarray,
        episode_metrics: Dict[str, Any],
    ) -> bool:
        """
        Add a trade sequence to the buffer if it exceeds threshold.
        
        Args:
            observations: Array of observations
            actions: Array of actions taken
            rewards: Array of rewards received
            episode_metrics: Dictionary with sharpe_ratio, total_pnl, etc.
            
        Returns:
            True if sequence was added, False otherwise
        """
        self.total_sequences_seen += 1
        
        total_reward = np.sum(rewards)
        sequence_length = len(observations)
        
        # Check if sequence meets criteria
        if sequence_length < self.min_sequence_length:
            return False
            
        if total_reward < self.reward_threshold:
            return False
            
        # Create sequence object
        sequence = TradeSequence(
            observations=observations,
            actions=actions,
            rewards=rewards,
            total_reward=total_reward,
            sharpe_ratio=episode_metrics.get('sharpe_ratio', 0.0),
            trade_pnl=episode_metrics.get('total_pnl', 0.0),
        )
        
        with self.lock:
            # Add to buffer
            self.sequences.append(sequence)
            self.total_sequences_stored += 1
            
            # Keep only top-k sequences by reward
            if len(self.sequences) > self.max_size:
                self.sequences.sort(key=lambda x: x.total_reward, reverse=True)
                self.sequences = self.sequences[:self.max_size]
                
        return True
        
    def sample_batch(
        self,
        batch_size: int = 32,
        prioritized: bool = True,
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Sample a batch of transitions from the buffer.
        
        Args:
            batch_size: Number of transitions to sample
            prioritized: Whether to use priority sampling
            
        Returns:
            Tuple of (observations, actions, rewards)
        """
        if len(self.sequences) == 0:
            raise ValueError("Buffer is empty")
            
        with self.lock:
            if prioritized:
                # Priority sampling based on total reward
                rewards = np.array([s.total_reward for s in self.sequences])
                probs = rewards / rewards.sum()
                indices = np.random.choice(
                    len(self.sequences),
                    size=min(batch_size, len(self.sequences)),
                    replace=False,
                    p=probs,
                )
            else:
                indices = np.random.choice(
                    len(self.sequences),
                    size=min(batch_size, len(self.sequences)),
                    replace=False,
                )
                
            # Collect transitions from sampled sequences
            obs_list = []
            action_list = []
            reward_list = []
            
            for idx in indices:
                seq = self.sequences[idx]
                # Sample a random window from the sequence
                if len(seq) > batch_size:
                    start = np.random.randint(0, len(seq) - batch_size)
                    obs_list.extend(seq.observations[start:start+batch_size])
                    action_list.extend(seq.actions[start:start+batch_size])
                    reward_list.extend(seq.rewards[start:start+batch_size])
                else:
                    obs_list.extend(seq.observations)
                    action_list.extend(seq.actions)
                    reward_list.extend(seq.rewards)
                    
        return (
            np.array(obs_list),
            np.array(action_list),
            np.array(reward_list),
        )
        
    def get_all_transitions(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Get all transitions in the buffer.
        
        Returns:
            Tuple of (all_observations, all_actions, all_rewards)
        """
        if len(self.sequences) == 0:
            return np.array([]), np.array([]), np.array([])
            
        with self.lock:
            obs_list = []
            action_list = []
            reward_list = []
            
            for seq in self.sequences:
                obs_list.extend(seq.observations)
                action_list.extend(seq.actions)
                reward_list.extend(seq.rewards)
                
        return (
            np.array(obs_list),
            np.array(action_list),
            np.array(reward_list),
        )
        
    def save(self, path: Optional[str] = None):
        """Save buffer to disk."""
        save_path = path or self.save_path
        if not save_path:
            raise ValueError("No save path specified")
            
        Path(save_path).parent.mkdir(parents=True, exist_ok=True)
        
        with self.lock:
            data = {
                'sequences': self.sequences,
                'total_sequences_seen': self.total_sequences_seen,
                'total_sequences_stored': self.total_sequences_stored,
            }
            with open(save_path, 'wb') as f:
                pickle.dump(data, f)
                
        print(f"Buffer saved to {save_path} ({len(self.sequences)} sequences)")
        
    def load(self, path: Optional[str] = None):
        """Load buffer from disk."""
        load_path = path or self.save_path
        if not load_path or not Path(load_path).exists():
            return
            
        with open(load_path, 'rb') as f:
            data = pickle.load(f)
            
        with self.lock:
            self.sequences = data['sequences']
            self.total_sequences_seen = data['total_sequences_seen']
            self.total_sequences_stored = data['total_sequences_stored']
            
        print(f"Buffer loaded from {load_path} ({len(self.sequences)} sequences)")
        
    def clear(self):
        """Clear the buffer."""
        with self.lock:
            self.sequences = []
            
    def __len__(self) -> int:
        return len(self.sequences)
        
    def get_statistics(self) -> Dict[str, Any]:
        """Get buffer statistics."""
        if len(self.sequences) == 0:
            return {
                'size': 0,
                'total_seen': self.total_sequences_seen,
                'total_stored': self.total_sequences_stored,
                'storage_rate': 0.0,
            }
            
        with self.lock:
            rewards = [s.total_reward for s in self.sequences]
            sharpes = [s.sharpe_ratio for s in self.sequences]
            
        return {
            'size': len(self.sequences),
            'total_seen': self.total_sequences_seen,
            'total_stored': self.total_sequences_stored,
            'storage_rate': self.total_sequences_stored / max(1, self.total_sequences_seen),
            'avg_reward': np.mean(rewards),
            'max_reward': np.max(rewards),
            'avg_sharpe': np.mean(sharpes),
            'total_transitions': sum(len(s) for s in self.sequences),
        }