Project structure:

o1_made_2/
├── data/
│   ├── raw_data.csv
│   ├── test_data.csv
│   └── train_data.csv
├── models/
│   ├── best_model.pth
│   └── scaler.gz
├── data_loader.py
├── model.py
├── test.py
└── train.py


Project files:

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']

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

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

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

    # Создаём DataFrames с нормализованными данными
    scaled_train_df = pd.DataFrame(scaled_train_features, columns=features)
    scaled_test_df = pd.DataFrame(scaled_test_features, columns=features)

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

if __name__ == "__main__":
    start_date_str = '2023-09-16'  # Задайте дату начала в формате YYYY-MM-DD
    end_date_str = '2024-09-16'  # Задайте дату окончания в формате 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()

```


model.py
```py

import torch
import torch.nn as nn
import numpy as np

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # Создаем матрицу с позиционными энкодингами
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))

        pe[:, 0::2] = torch.sin(position * div_term)  # Четные индексы
        pe[:, 1::2] = torch.cos(position * div_term)  # Нечетные индексы
        pe = pe.unsqueeze(1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        # x.shape: [seq_len, batch_size, d_model]
        x = x + self.pe[:x.size(0)]
        return self.dropout(x)

class BitcoinPricePredictor(nn.Module):
    def __init__(self, input_size, num_layers=4, d_model=128, nhead=8, dim_feedforward=512, dropout=0.1):
        super(BitcoinPricePredictor, self).__init__()

        self.input_projection = nn.Linear(input_size, d_model)
        self.pos_encoder = PositionalEncoding(d_model, dropout)

        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout)
        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers)

        self.decoder = nn.Linear(d_model, 1)

    def forward(self, src):
        # src.shape: [batch_size, seq_len, input_size]
        src = self.input_projection(src)  # [batch_size, seq_len, d_model]
        src = src.permute(1, 0, 2)  # [seq_len, batch_size, d_model]
        src = self.pos_encoder(src)

        device = src.device
        seq_len = src.size(0)
        mask = torch.triu(torch.ones(seq_len, seq_len, device=device) * float('-inf'), diagonal=1)

        output = self.transformer_encoder(src, mask=mask)
        output = output[-1, :, :]  # Выход последнего шага
        output = self.decoder(output)

        return output.squeeze(-1)

```


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=200):
        self.sequence_length = sequence_length
        self.data = data.reset_index(drop=True)

    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, num_layers=4, d_model=128, nhead=8, dim_feedforward=512, dropout=0.1)
    model.load_state_dict(torch.load('models/best_model.pth', map_location='cpu'))
    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=200)

    # Получаем случайную последовательность
    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]
    std = np.sqrt(scaler.var_[close_index])

    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()

```


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=200):
        self.sequence_length = sequence_length
        self.data = data.reset_index(drop=True)

    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')

    # Разделяем данные на обучающую и валидационную выборки
    from sklearn.model_selection import train_test_split
    train_data_df, val_data_df = train_test_split(train_df, test_size=0.2, shuffle=False)

    # Параметры
    sequence_length = 200
    batch_size = 64
    num_epochs = 8
    learning_rate = 0.001
    input_size = train_df.shape[1]

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

    val_dataset = BitcoinDataset(val_data_df, sequence_length)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)

    # Инициализируем модель, функцию потерь и оптимизатор
    model = BitcoinPricePredictor(input_size=input_size, num_layers=4, d_model=128, nhead=8, dim_feedforward=512, dropout=0.1)
    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)

    # Обучение модели с валидацией
    best_val_loss = float('inf')
    for epoch in range(num_epochs):
        model.train()
        epoch_loss = 0
        for sequences, targets in train_loader:
            sequences = sequences.to(device)
            targets = targets.to(device)

            # Прямой проход
            outputs = model(sequences)

            loss = criterion(outputs, targets)

            # Обратный проход и оптимизация
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()
        avg_loss = epoch_loss / len(train_loader)

        # Валидация
        model.eval()
        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, targets)
                val_loss += loss.item()
        avg_val_loss = val_loss / len(val_loader)

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

        # Сохраняем лучшую модель
        if avg_val_loss < best_val_loss:
            best_val_loss = avg_val_loss
            torch.save(model.state_dict(), 'models/best_model.pth')

    print("Лучшие веса модели сохранены в models/best_model.pth")

if __name__ == "__main__":
    train()

```


Это моя модель для предсказания цены криптовалюты от входной последовательности.
Можешь пожаулйста ее просмотреть и основываясь на примеденном коде написать полноценную работу с вводной частью, основной и заключением.
Важно, чтобы ты рассписал про Transformer, про модель, ее архитектуру. Привел формулы, чтобы было все профессилнально.
Эксмерименты. Вот так, например выглядит обучение:
```
Epoch [1/8], Loss: 0.014390, Val Loss: 0.001343
Epoch [2/8], Loss: 0.001410, Val Loss: 0.003038
Epoch [3/8], Loss: 0.001188, Val Loss: 0.002520
Epoch [4/8], Loss: 0.000825, Val Loss: 0.000902
Epoch [5/8], Loss: 0.000791, Val Loss: 0.001163
Epoch [6/8], Loss: 0.000584, Val Loss: 0.000662
Epoch [7/8], Loss: 0.000540, Val Loss: 0.001032
Epoch [8/8], Loss: 0.000456, Val Loss: 0.000356
Лучшие веса модели сохранены в models/best_model.pth
```
Сделай, чтобы это была полноценная работа для arXiv. Пиши просто текстом.
Если нужны изображения, то пиши просто <описание изображения>.

Спасибо! Работа должна быть большой и с формулами и писанием конкретно моей модели и в чем может быть ее польза.