Project structure:

o1_made_2/
├── models/
│   ├── best_model.pth
│   ├── loss_plot.png
│   ├── model.pth
│   ├── scaler.gz
│   └── test_prediction.png
├── data_loader.py
├── model.py
├── test.py
└── train.py


Project files:

data_loader.py
```py

import requests
import pandas as pd
import numpy as np
import time
from datetime import datetime, timedelta
from tqdm import tqdm

def get_data(start_time, end_time, interval):
    url = "https://api.binance.com/api/v3/klines"
    params = {
        'symbol': 'BTCUSDT',
        'interval': interval,
        'startTime': start_time,
        'endTime': end_time,
        'limit': 1000
    }
    response = requests.get(url, params=params)
    data = response.json()
    return data

def add_indicators(df):
    # Simple Moving Averages
    df['SMA_10'] = df['Close'].rolling(window=10).mean()
    df['SMA_30'] = df['Close'].rolling(window=30).mean()

    # Exponential Moving Averages
    df['EMA_10'] = df['Close'].ewm(span=10, adjust=False).mean()
    df['EMA_30'] = df['Close'].ewm(span=30, adjust=False).mean()

    # Relative Strength Index (RSI)
    delta = df['Close'].diff()
    up = delta.clip(lower=0)
    down = -delta.clip(upper=0)
    ema_up = up.ewm(com=13, adjust=False).mean()
    ema_down = down.ewm(com=13, adjust=False).mean()
    rs = ema_up / ema_down
    df['RSI'] = 100 - (100 / (1 + rs))

    # Bollinger Bands
    df['BB_Middle'] = df['Close'].rolling(window=20).mean()
    df['BB_Std'] = df['Close'].rolling(window=20).std()
    df['BB_Upper'] = df['BB_Middle'] + (df['BB_Std'] * 2)
    df['BB_Lower'] = df['BB_Middle'] - (df['BB_Std'] * 2)

    # MACD
    exp1 = df['Close'].ewm(span=12, adjust=False).mean()
    exp2 = df['Close'].ewm(span=26, adjust=False).mean()
    df['MACD'] = exp1 - exp2
    df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()

    # Stochastic Oscillator
    df['Low_14'] = df['Low'].rolling(window=14).min()
    df['High_14'] = df['High'].rolling(window=14).max()
    df['%K'] = 100 * ((df['Close'] - df['Low_14']) / (df['High_14'] - df['Low_14']))
    df['%D'] = df['%K'].rolling(window=3).mean()

    return df

def collect_data(start_date_str, end_date_str):
    interval = '5m'
    start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
    end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
    start_time = int(start_date.timestamp() * 1000)
    end_time = int(end_date.timestamp() * 1000)
    total_requests = (end_time - start_time) // (1000 * 60 * 5 * 1000) + 1

    all_data = []
    with tqdm(total=total_requests, desc="Collecting data") as pbar:
        while start_time < end_time:
            data = get_data(start_time, end_time, interval)
            if not data:
                break
            all_data.extend(data)
            start_time = data[-1][6] + 1
            time.sleep(0.5)
            pbar.update(1)

    columns = ['Open Time', 'Open', 'High', 'Low', 'Close', 'Volume',
               'Close Time', 'Quote Asset Volume', 'Number of Trades',
               'Taker Buy Base Asset Volume', 'Taker Buy Quote Asset Volume', 'Ignore']
    df = pd.DataFrame(all_data, columns=columns)
    df['Open Time'] = pd.to_datetime(df['Open Time'], unit='ms')
    df['Close Time'] = pd.to_datetime(df['Close Time'], unit='ms')
    numeric_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
    df[numeric_columns] = df[numeric_columns].astype(float)
    df.reset_index(drop=True, inplace=True)
    df.to_csv('data/raw_data.csv', index=False)
    print("Data successfully saved to data/raw_data.csv")
    df = add_indicators(df)
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)
    df.to_csv('data/processed_data.csv', index=False)
    print("Data with indicators saved to data/processed_data.csv")

def preprocess_data():
    df = pd.read_csv('data/processed_data.csv')

    # Date-Time Features
    df['Hour'] = pd.to_datetime(df['Open Time']).dt.hour
    df['DayOfWeek'] = pd.to_datetime(df['Open Time']).dt.dayofweek

    # Selecting Features
    features = ['Open', 'High', 'Low', 'Close', 'Volume', 'SMA_10', 'SMA_30',
                'EMA_10', 'EMA_30', 'RSI', 'BB_Middle', 'BB_Upper', 'BB_Lower',
                'MACD', 'MACD_Signal', '%K', '%D', 'Hour', 'DayOfWeek']

    # Scaling Features
    from sklearn.preprocessing import MinMaxScaler
    scaler = MinMaxScaler()
    scaled_features = scaler.fit_transform(df[features])

    # Saving the scaler
    import joblib
    joblib.dump(scaler, 'models/scaler.gz')

    # Creating DataFrame with scaled features
    scaled_df = pd.DataFrame(scaled_features, columns=features)

    # Adding the target variable (Close price shifted by -1 to predict the next time step)
    scaled_df['Target'] = scaled_df['Close'].shift(-1)
    scaled_df.dropna(inplace=True)
    scaled_df.reset_index(drop=True, inplace=True)

    # Splitting the data
    train_size = int(len(scaled_df) * 0.8)
    train_data = scaled_df.iloc[:train_size]
    test_data = scaled_df.iloc[train_size:]

    # Saving the data
    train_data.to_csv('data/train_data.csv', index=False)
    test_data.to_csv('data/test_data.csv', index=False)
    print("Data has been split into train_data.csv and test_data.csv")

if __name__ == "__main__":
    start_date_str = '2022-09-01'
    end_date_str = '2023-09-30'
    print(f"Collecting data from {start_date_str} to {end_date_str}")
    collect_data(start_date_str, end_date_str)
    print("Preprocessing data")
    preprocess_data()

```


model.py
```py

import torch
import torch.nn as nn
import torch.nn.functional as F

class AttentionModel(nn.Module):
    def __init__(self, input_size, hidden_size=64, num_layers=2):
        super(AttentionModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers

        # Bidirectional LSTM
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
                            batch_first=True, bidirectional=True)
        self.dropout = nn.Dropout(0.3)

        # Attention Layer
        self.attention = nn.Linear(hidden_size * 2, 1)

        # Fully connected layer
        self.fc = nn.Linear(hidden_size * 2, 1)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers * 2, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers * 2, x.size(0), self.hidden_size).to(x.device)

        out, _ = self.lstm(x, (h0, c0))
        out = self.dropout(out)

        # Attention Mechanism
        attn_weights = F.softmax(self.attention(out), dim=1)
        attn_out = torch.bmm(attn_weights.permute(0, 2, 1), out).squeeze(1)

        # Output layer
        out = self.fc(attn_out)
        return out

```


test.py
```py

import torch
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from model import AttentionModel
import joblib
from sklearn.metrics import mean_squared_error, mean_absolute_error

class BitcoinTestDataset:
    def __init__(self, data, sequence_length=100):
        self.sequence_length = sequence_length
        self.data = data.values.astype(np.float32)
        self.features = data.columns.tolist()
        self.feature_size = data.shape[1] - 1  # Exclude target

    def __len__(self):
        return len(self.data) - self.sequence_length

    def __getitem__(self, idx):
        x = self.data[idx:idx + self.sequence_length, :-1]
        y = self.data[idx + self.sequence_length, -1]
        return torch.tensor(x), torch.tensor(y)

def inverse_transform(scaled_values, scaler, feature_name):
    idx = scaler.feature_names_in_.tolist().index(feature_name)
    min_ = scaler.data_min_[idx]
    max_ = scaler.data_max_[idx]
    return scaled_values * (max_ - min_) + min_

def test():
    # Load data
    test_df = pd.read_csv('data/test_data.csv')
    test_dataset = BitcoinTestDataset(test_df, sequence_length=100)
    test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=False)

    # Load scaler
    scaler = joblib.load('models/scaler.gz')

    # Initialize model
    input_size = test_df.shape[1] - 1  # Exclude target
    model = AttentionModel(input_size=input_size)
    model.load_state_dict(torch.load('models/best_model.pth'))
    model.eval()

    # Device configuration
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    predictions = []
    actuals = []

    with torch.no_grad():
        for sequences, targets in test_loader:
            sequences = sequences.to(device)
            outputs = model(sequences)
            predictions.append(outputs.cpu().numpy().flatten()[0])
            actuals.append(targets.numpy().flatten()[0])

    # Inverse transform
    predictions_inverse = inverse_transform(np.array(predictions), scaler, 'Close')
    actuals_inverse = inverse_transform(np.array(actuals), scaler, 'Close')

    # Evaluation metrics
    rmse = np.sqrt(mean_squared_error(actuals_inverse, predictions_inverse))
    mae = mean_absolute_error(actuals_inverse, predictions_inverse)
    mape = np.mean(np.abs((actuals_inverse - predictions_inverse) / actuals_inverse)) * 100

    print(f'Test RMSE: {rmse:.2f}')
    print(f'Test MAE: {mae:.2f}')
    print(f'Test MAPE: {mape:.2f}%')

    # Plotting actual vs predicted prices
    plt.figure(figsize=(15,5))
    plt.plot(actuals_inverse, label='Actual Prices')
    plt.plot(predictions_inverse, label='Predicted Prices')
    plt.legend()
    plt.title('Bitcoin Price Prediction')
    plt.xlabel('Time')
    plt.ylabel('Price (USD)')
    plt.savefig('models/test_prediction.png')
    plt.show()

if __name__ == "__main__":
    test()

```


train.py
```py

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from model import AttentionModel
import numpy as np
from sklearn.model_selection import train_test_split
import joblib
import matplotlib.pyplot as plt
import os

# Dataset Definition
class BitcoinDataset(Dataset):
    def __init__(self, data, sequence_length=100):
        self.sequence_length = sequence_length
        self.data = data.values.astype(np.float32)
        self.features = data.columns.tolist()
        self.feature_size = data.shape[1] - 1  # Exclude target

    def __len__(self):
        return len(self.data) - self.sequence_length

    def __getitem__(self, idx):
        x = self.data[idx:idx + self.sequence_length, :-1]
        y = self.data[idx + self.sequence_length, -1]
        return torch.tensor(x), torch.tensor(y)

def train():
    # Load and split data
    train_df = pd.read_csv('data/train_data.csv')
    val_size = 0.1
    train_data, val_data = train_test_split(train_df, test_size=val_size, shuffle=False)

    # Parameters
    sequence_length = 100
    batch_size = 64
    num_epochs = 8#50
    learning_rate = 0.001
    input_size = train_data.shape[1] - 1  # Exclude target

    # Create datasets and loaders
    train_dataset = BitcoinDataset(train_data, sequence_length)
    val_dataset = BitcoinDataset(val_data, sequence_length)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, drop_last=True)

    # Initialize model, loss function, and optimizer
    model = AttentionModel(input_size=input_size)
    criterion = nn.MSELoss()
    mae_loss = nn.L1Loss()
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5, verbose=True)

    # Device configuration
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # Early stopping parameters
    early_stopping = False
    patience = 10
    min_val_loss = np.Inf
    epochs_no_improve = 0

    # Training loop
    train_losses = []
    val_losses = []
    for epoch in range(num_epochs):
        model.train()
        epoch_train_loss = 0

        for sequences, targets in train_loader:
            sequences = sequences.to(device)
            targets = targets.to(device)

            outputs = model(sequences)
            loss = criterion(outputs.squeeze(), targets)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            epoch_train_loss += loss.item()

        avg_train_loss = epoch_train_loss / len(train_loader)
        train_losses.append(avg_train_loss)

        # Validation
        model.eval()
        epoch_val_loss = 0
        with torch.no_grad():
            for sequences, targets in val_loader:
                sequences = sequences.to(device)
                targets = targets.to(device)
                outputs = model(sequences)
                loss = criterion(outputs.squeeze(), targets)
                epoch_val_loss += loss.item()

        avg_val_loss = epoch_val_loss / len(val_loader)
        val_losses.append(avg_val_loss)
        scheduler.step(avg_val_loss)

        print(f'Epoch [{epoch+1}/{num_epochs}], Train Loss: {avg_train_loss:.6f}, Val Loss: {avg_val_loss:.6f}')

        # Early Stopping Check
        if avg_val_loss < min_val_loss:
            min_val_loss = avg_val_loss
            epochs_no_improve = 0
            torch.save(model.state_dict(), 'models/best_model.pth')
        else:
            epochs_no_improve += 1
            if epochs_no_improve == patience:
                print('Early stopping!')
                early_stopping = True
                break

    # Plotting training and validation loss
    plt.figure(figsize=(10,5))
    plt.plot(train_losses, label='Training Loss')
    plt.plot(val_losses, label='Validation Loss')
    plt.legend()
    plt.savefig('models/loss_plot.png')
    plt.show()

    # Load the best model
    model.load_state_dict(torch.load('models/best_model.pth'))
    print("Best model loaded from models/best_model.pth")

if __name__ == "__main__":
    train()

```


Вот так выглядит обучение:
Epoch [1/8], Train Loss: 0.000977, Val Loss: 0.000083
Epoch [2/8], Train Loss: 0.000111, Val Loss: 0.000059
Epoch [3/8], Train Loss: 0.000062, Val Loss: 0.000033
Epoch [4/8], Train Loss: 0.000056, Val Loss: 0.000035
Epoch [5/8], Train Loss: 0.000053, Val Loss: 0.000043
Epoch [6/8], Train Loss: 0.000045, Val Loss: 0.000035
Epoch [7/8], Train Loss: 0.000041, Val Loss: 0.000025

Вот так выглядит test:
Test RMSE: 76.41
Test MAE: 52.25
Test MAPE: 0.19%

Как по твоему, нормально?

Что можно улучшить?

Что не понятно - зачем нужен `Bidirectional LSTM`, ведь в реальности мы не можем получить данные из будущего
Проверь, не перемешиваются ли тренировочные и тестовые наборы данных.
Важно, чтобы при обучении выборки по 100 шагов были последовательно

Примени все свои навыки ML-специалиста и произведи улучшения.
Приведи обновленные файлы.