Project structure:

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


Project files:

model.py
```py

import torch
import torch.nn as nn

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

        # LSTM слои
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)

        # Полносвязный слой
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, x):
        # Инициализируем скрытые состояния
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)

        # Пропускаем через LSTM
        out, _ = self.lstm(x, (h0, c0))

        # Берём выход последнего временного шага
        out = self.fc(out[:, -1, :])

        return out
```


train.py
```py
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from model import BitcoinPricePredictor

# Определение датасета
class BitcoinDataset(Dataset):
    def __init__(self, data, sequence_length=100):
        self.sequence_length = sequence_length
        self.data = data

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

    def __getitem__(self, idx):
        seq = self.data.iloc[idx:idx + self.sequence_length].values
        target = self.data.iloc[idx + self.sequence_length]['Close']
        return torch.tensor(seq, dtype=torch.float32), torch.tensor(target, dtype=torch.float32)

def train():
    # Загружаем данные
    train_df = pd.read_csv('data/train_data.csv')

    # Параметры
    sequence_length = 100
    batch_size = 64
    num_epochs = 5#20
    learning_rate = 0.001
    input_size = train_df.shape[1]  # Количество признаков

    # Создаём датасет и загрузчик
    train_dataset = BitcoinDataset(train_df, sequence_length)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

    # Инициализируем модель, функцию потерь и оптимизатор
    model = BitcoinPricePredictor(input_size=input_size)
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

    # Переносим модель на устройство (CPU или GPU)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # Обучение модели
    model.train()
    for epoch in range(num_epochs):
        epoch_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_loss += loss.item()

        avg_loss = epoch_loss / len(train_loader)
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.6f}')

    # Сохраняем модель
    torch.save(model.state_dict(), 'models/model.pth')
    print("Модель сохранена в models/model.pth")

if __name__ == "__main__":
    train()
```


test.py
```py

import torch
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from model import BitcoinPricePredictor
import joblib

# Определение датасета для тестирования
class BitcoinTestDataset:
    def __init__(self, data, sequence_length=100):
        self.sequence_length = sequence_length
        self.data = data

    def get_random_sequence(self):
        idx = np.random.randint(0, len(self.data) - self.sequence_length - 1)
        seq = self.data.iloc[idx:idx + self.sequence_length].values
        target = self.data.iloc[idx + self.sequence_length]['Close']
        return seq, target, idx

def test():
    # Загружаем данные
    test_df = pd.read_csv('data/test_data.csv')

    # Загружаем скейлер
    scaler = joblib.load('models/scaler.gz')

    # Переносим данные в исходный масштаб для визуализации
    test_df_scaled = pd.DataFrame(scaler.inverse_transform(test_df), columns=test_df.columns)

    # Инициализируем модель
    input_size = test_df.shape[1]
    model = BitcoinPricePredictor(input_size=input_size)
    model.load_state_dict(torch.load('models/model.pth'))
    model.eval()

    # Переносим модель на устройство (CPU или GPU)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # Создаём датасет
    test_dataset = BitcoinTestDataset(test_df, sequence_length=100)

    # Получаем случайную последовательность
    seq, real_value, idx = test_dataset.get_random_sequence()

    # Подготовка данных для модели
    seq_tensor = torch.tensor(seq, dtype=torch.float32).unsqueeze(0).to(device)

    # Предсказание
    with torch.no_grad():
        predicted_value = model(seq_tensor).cpu().item()

    # Обратное преобразование предсказанного значения
    close_index = test_df.columns.get_loc('Close')
    mean = scaler.mean_[close_index]
    var = scaler.var_[close_index]
    std = np.sqrt(var)

    predicted_value = predicted_value * std + mean
    real_value = real_value * std + mean

    # Подготовка данных для визуализации
    seq_denorm = seq[:, close_index] * std + mean

    print(f"Предсказанное значение: {predicted_value:.2f} USD")
    print(f"Реальное значение: {real_value:.2f} USD")

    # Визуализация
    time_steps = np.arange(len(seq_denorm) + 1)

    # Построение графика
    plt.plot(time_steps[:-1], seq_denorm, color='blue', label='Входная последовательность')
    plt.plot(time_steps[-2:], [seq_denorm[-1], predicted_value], color='orange', marker='o', label='Предсказанный шаг')
    plt.plot(time_steps[-2:], [seq_denorm[-1], real_value], color='green', marker='o', label='Реальный шаг')

    plt.legend()
    plt.xlabel('Временные шаги')
    plt.ylabel('Цена Bitcoin (USD)')
    plt.title('Предсказание цены Bitcoin')

    plt.show()

if __name__ == "__main__":
    test()

```

data_loader.py
```py
import requests
import pandas as pd
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):
    # Простая скользящая средняя (SMA)
    df['SMA_10'] = df['Close'].rolling(window=10).mean()
    df['SMA_30'] = df['Close'].rolling(window=30).mean()

    # Экспоненциальная скользящая средняя (EMA)
    df['EMA_10'] = df['Close'].ewm(span=10, adjust=False).mean()
    df['EMA_30'] = df['Close'].ewm(span=30, adjust=False).mean()

    # Индекс относительной силы (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))

    return df

def collect_data(start_date_str, end_date_str):
    # Устанавливаем интервал в 5 минут
    interval = '5m'

    # Преобразуем строки дат в объекты datetime
    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)  # Задержка, чтобы не перегружать API
            pbar.update(1)

    # Преобразуем данные в DataFrame
    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)

    # Сохраняем данные в CSV
    df.to_csv('data/raw_data.csv', index=False)
    print("Данные успешно сохранены в data/raw_data.csv")

    # Добавляем индикаторы
    df = add_indicators(df)

    # Удаляем начальные строки с NaN (из-за расчёта индикаторов)
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)

    # Сохраняем данные в CSV
    df.to_csv('data/raw_data.csv', index=False)
    print("Данные успешно сохранены в data/raw_data.csv")


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

    # Выбираем нужные столбцы для модели
    features = ['Open', 'High', 'Low', 'Close', 'Volume', 'SMA_10', 'SMA_30', 'EMA_10', 'EMA_30', 'RSI']

    # Нормализация данных
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    scaled_features = scaler.fit_transform(df[features])

    # Сохраняем скейлер для использования при тестировании
    import joblib
    joblib.dump(scaler, 'models/scaler.gz')

    # Создаём DataFrame с нормализованными данными
    scaled_df = pd.DataFrame(scaled_features, columns=features)

    # Разбиваем данные на обучающую и тестовую выборки
    train_size = int(len(scaled_df) * 0.8)
    train_data = scaled_df.iloc[:train_size]
    test_data = scaled_df.iloc[train_size:]

    # Сохраняем данные
    train_data.to_csv('data/train_data.csv', index=False)
    test_data.to_csv('data/test_data.csv', index=False)
    print("Данные успешно разбиты на train_data.csv и test_data.csv")

if __name__ == "__main__":
    start_date_str = '2022-09-01'  # Задайте дату начала в формате YYYY-MM-DD
    end_date_str = '2023-09-30'  # Задайте дату окончания в формате YYYY-MM-DD
    print(f"collecting data from {start_date_str} to {end_date_str}")
    collect_data(start_date_str, end_date_str)
    print(f"preprocess data")
    preprocess_data()
```


Это моя модель для предсказания цены BitcoinUSDT, основываясь на предыдущих шагах.

Вот так выглядит обучение:
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-специалиста и произведи улучшения.
Приведи обновленные файлы.
Я хочу, чтобы test.py работал иначе - он брал бы случайные 100 шагов из тестового набора, рисовал бы их. После чего, оранжевым рисовал бы 101-й шаг, предсказанный моделью. И поверх него зеленым цветом реальный 101-й шаг.