preview
Внедряем систему непрерывной адаптации LLM для алгоритмического трейдинга

Внедряем систему непрерывной адаптации LLM для алгоритмического трейдинга

MetaTrader 5Статистика и анализ |
107 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Последние полтора года работы с большими языковыми моделями в трейдинге открыли передо мной парадоксальную проблему: чем точнее модель предсказывает рынок сегодня, тем быстрее она деградирует завтра. Это не теоретическое наблюдение из академических журналов — это реальность, которую я наблюдал на живом счёте, когда модель с точностью 73% в понедельник — показывала 51% к пятнице. Причина банальна: рынки меняются быстрее, чем мы успеваем переобучать модели.


Проблема, которую никто не решает

Когда я впервые начал использовать Llama 3.2 для предсказания валютных пар, процесс выглядел элегантно: собираем исторические данные за три месяца, файнтьюним модель на 2000 примеров, получаем отличные результаты. Через две недели модель начинает давать сбои. Не катастрофические — просто уверенность падает, точность скатывается к рандому, а самое неприятное: модель продолжает быть уверенной в своих прогнозах, хотя они уже не работают.

Классическое решение — переобучить модель на свежих данных. Звучит логично, пока не начинаешь считать. Файнтьюнинг Llama 3.2:3b на 2000 примеров занимает около 40 минут на RTX 3090. Если делать это каждую неделю, мы получаем 160 минут в месяц чистого простоя. Добавим сюда подготовку данных, валидацию, тестирование — выходит полдня работы. И это при условии, что мы вообще успели заметить деградацию модели до серьёзных потерь.

Но главная проблема не во времени. Главная проблема в том, что при переобучении модель забывает старые паттерны. Рынок циклический: то, что не работало последние две недели, может вернуться через месяц. Стандартный файнтьюнинг работает по принципу перезаписи: новые знания вытесняют старые. Мы получаем модель, которая отлично работает на текущем рыночном режиме и полностью беспомощна при смене режима.


Концепция SEAL: обучение без забывания

Где-то между очередным неудачным экспериментом с ежедневным ретрейнингом и чтением статей про continual learning я осознал простую вещь: модель должна учиться как человек — не заменять старые знания новыми, а дополнять их. Когда опытный трейдер видит новый паттерн, он не забывает старые — он добавляет новый в свой арсенал и начинает понимать, в каких условиях какой паттерн работает.

Так родилась концепция SEAL — Self-Evolving Adaptive Learning. Не просто файнтьюнинг по расписанию, а непрерывная эволюция модели на основе реальных торговых результатов. Каждая сделка становится обучающим примером, а каждый результат — обратной связью. Модель не просто предсказывает движение цены — она учится на своих ошибках и успехах.

Первый прототип я написал за ночь. Концептуально всё выглядело просто:

class SEALSystem:
    def __init__(self, model_name: str):
        self.model_name = model_name
        self.trade_memory = []
        self.learning_buffer = []
    
    def record_trade(self, prediction: dict, outcome: dict):
        """Записываем результат сделки"""
        example = {
            'input': self._format_input(prediction),
            'output': self._format_output(outcome),
            'timestamp': time.time()
        }
        self.learning_buffer.append(example)

Выглядит тривиально, правда? Но дьявол, как всегда, в деталях. Первый запуск показал фундаментальную проблему: как отличить хороший паттерн от случайного везения? Если модель правильно предсказала движение EUR/USD на 50 пипсов вверх, это означает, что она нашла реальную закономерность, или просто угадала в условиях высокой волатильности?


Память: не всё то золото, что блестит

Человеческая память избирательна не просто так — мы помним значимые события и забываем рутину. Аналогично, SEAL система должна была научиться различать обучающие примеры по их ценности. Не каждая сделка одинаково полезна для обучения.

Я добавил систему взвешивания примеров:

def calculate_example_weight(self, prediction: dict, outcome: dict) -> float:
    """Вычисляем вес примера для обучения"""
    
    # Базовый вес - точность предсказания
    predicted_direction = prediction['direction']
    actual_direction = 'UP' if outcome['profit'] > 0 else 'DOWN'
    base_weight = 1.0 if predicted_direction == actual_direction else 0.3
    
    # Модификатор уверенности: чем увереннее была модель, тем важнее результат
    confidence = prediction['confidence'] / 100.0
    confidence_modifier = 1.0 + (confidence - 0.5)
    
    # Модификатор величины движения: сильные движения важнее слабых
    pips = abs(outcome['pips'])
    movement_modifier = min(pips / 50.0, 2.0)
    
    # Модификатор редкости: редкие условия важнее частых
    market_regime = self._classify_market_regime(prediction['features'])
    rarity_modifier = self._get_regime_rarity(market_regime)
    
    weight = base_weight * confidence_modifier * movement_modifier * rarity_modifier
    return weight

Эта формула родилась не из математических изысканий, а из наблюдений за реальными сделками. Первая версия просто считала правильные и неправильные предсказания. Но быстро выяснилось, что модель запоминала паттерны флэта (которых большинство) и переставала видеть тренды. Добавление movement_modifier решило проблему — теперь сильные движения весили больше, заставляя модель помнить их паттерны.

Модификатор уверенности появился после анализа ложноположительных срабатываний. Оказалось, что когда модель была уверена на 95% и ошибалась — это критически важный обучающий пример. Он сигнализирует о том, что модель видит паттерн там, где его нет, и именно такие примеры нужно запоминать в первую очередь.


Архитектура памяти: кольцевой буфер с приоритетами

Нельзя просто складывать все примеры в одну кучу и переобучать модель на всей истории. Во-первых, это вычислительно дорого, во-вторых, старые примеры могут быть уже не релевантны текущему рынку, в-третьих, нам нужен баланс между свежестью данных и историческим контекстом.

Решение пришло из неожиданного источника — архитектуры операционных систем. Помните алгоритмы замещения страниц в виртуальной памяти? Я адаптировал комбинацию LRU (Least Recently Used) и приоритетного вытеснения:

class PriorityMemoryBuffer:
    def __init__(self, max_size: int = 1000):
        self.max_size = max_size
        self.buffer = []
        self.weights = []
        self.timestamps = []
    
    def add(self, example: dict, weight: float):
        timestamp = time.time()
        
        if len(self.buffer) < self.max_size:
            self.buffer.append(example)
            self.weights.append(weight)
            self.timestamps.append(timestamp)
        else:
            # Находим кандидата на вытеснение
            scores = self._calculate_retention_scores()
            min_idx = np.argmin(scores)
            
            # Заменяем только если новый пример важнее
            if weight > self.weights[min_idx]:
                self.buffer[min_idx] = example
                self.weights[min_idx] = weight
                self.timestamps[min_idx] = timestamp
    
    def _calculate_retention_scores(self) -> np.ndarray:
        """Вычисляем важность сохранения примера"""
        current_time = time.time()
        
        # Нормализуем веса и возраст
        norm_weights = np.array(self.weights) / max(self.weights)
        ages = current_time - np.array(self.timestamps)
        norm_ages = 1.0 - (ages / max(ages))  # Инвертируем: свежее = важнее
        
        # Комбинированный скор: 70% вес примера, 30% свежесть
        scores = 0.7 * norm_weights + 0.3 * norm_ages
        return scores

Эта система решает несколько проблем одновременно: старые, но важные примеры (например, редкие паттерны с сильными движениями) сохраняются дольше; свежие примеры попадают в буфер легче, даже если они не суперважные (рынок меняется, и нам нужен текущий контекст); посредственные примеры средней давности вытесняются первыми.


Инкрементальное обучение: когда запускать файнтьюнинг

Наивная реализация запускала бы файнтьюнинг после каждой закрытой сделки. Это безумие — мы получили бы систему, которая постоянно учится и никогда не торгует. Нужен был триггер, который балансирует между актуальностью модели и вычислительными затратами.

Первая версия использовала простой счётчик — каждые 50 сделок:

def record_trade(self, prediction: dict, outcome: dict):
    weight = self.calculate_example_weight(prediction, outcome)
    self.memory.add(self._create_example(prediction, outcome), weight)
    
    self.total_trades += 1
    
    if self.total_trades % 50 == 0:
        log.info(f"SEAL: Накоплено {self.total_trades} сделок - запускаем дообучение...")
        self._trigger_finetuning()

Это работало, но неэффективно. В спокойные периоды 50 сделок могли накапливаться неделями, а в волатильные — за день. Модель либо устаревала, либо переобучалась на слишком коротком периоде.

Более умная версия анализирует качество предсказаний:

def should_trigger_learning(self) -> bool:
    """Определяем необходимость дообучения"""
    
    # Минимальный порог - хотя бы 30 новых примеров
    if len(self.learning_buffer) < 30:
        return False
    
    # Анализируем последние 20 сделок
    recent_predictions = self.get_recent_predictions(20)
    if len(recent_predictions) < 20:
        return False
    
    # Вычисляем текущую точность
    correct = sum(1 for p in recent_predictions if p['correct'])
    accuracy = correct / len(recent_predictions)
    
    # Триггер 1: точность упала ниже 55%
    if accuracy < 0.55:
        log.warning(f"SEAL: Точность {accuracy:.1%} - нужно дообучение")
        return True
    
    # Триггер 2: накопилось много примеров (>100)
    if len(self.learning_buffer) > 100:
        log.info(f"SEAL: Накоплено {len(self.learning_buffer)} примеров")
        return True
    
    # Триггер 3: обнаружен новый рыночный режим
    if self._detect_regime_shift():
        log.warning("SEAL: Смена рыночного режима - адаптация")
        return True
    
    return False
Детектор смены режима заслуживает отдельного внимания. Я использовал комбинацию волатильности, объёма и распределения ошибок модели:
def _detect_regime_shift(self) -> bool:
    """Детектирует смену рыночного режима"""
    
    recent = self.get_recent_predictions(30)
    if len(recent) < 30:
        return False
    
    # Анализируем распределение ошибок
    errors = [abs(p['predicted_pips'] - p['actual_pips']) for p in recent]
    
    # Сравниваем с историческим средним
    historical_error = self.get_historical_average_error()
    current_error = np.mean(errors)
    
    # Смена режима = ошибка выросла на 50%+
    if current_error > historical_error * 1.5:
        return True
    
    # Проверяем изменение волатильности
    current_volatility = np.std([p['actual_pips'] for p in recent])
    historical_volatility = self.get_historical_volatility()
    
    if abs(current_volatility - historical_volatility) / historical_volatility > 0.4:
        return True
    
    return False


Формирование обучающих примеров: контекст важнее деталей

Когда я начал формировать примеры для дообучения, первая версия выглядела как JSON с сырыми данными:

{
  "RSI": 45.3,
  "MACD": -0.0012,
  "price": 1.0856,
  "direction": "UP"
}

Модель обучалась, но результаты были посредственные. Проблема в том, что LLM обучена работать с естественным языком, а не с табличными данными. Нужно было переформулировать задачу так, чтобы она использовала сильные стороны языковой модели — понимание контекста и взаимосвязей.

Новый формат стал нарративным:

def _create_learning_example(self, prediction: dict, outcome: dict) -> dict:
    """Создаём обучающий пример в формате естественного языка"""
    
    features = prediction['features']
    
    # Формируем контекстное описание рыночной ситуации
    context_parts = []
    
    # Тренд
    if features['EMA_50'] > features['EMA_200']:
        trend = "восходящий тренд (EMA50 > EMA200)"
    else:
        trend = "нисходящий тренд (EMA50 < EMA200)"
    context_parts.append(f"Рынок в состоянии {trend}")
    
    # Перекупленность/перепроданность
    rsi = features['RSI']
    if rsi > 70:
        context_parts.append(f"RSI={rsi:.1f} указывает на перекупленность")
    elif rsi < 30:
        context_parts.append(f"RSI={rsi:.1f} указывает на перепроданность")
    else:
        context_parts.append(f"RSI={rsi:.1f} в нейтральной зоне")
    
    # Волатильность
    bb_position = features['BB_position']
    if bb_position > 0.8:
        context_parts.append("цена у верхней границы Боллинджера")
    elif bb_position < 0.2:
        context_parts.append("цена у нижней границы Боллинджера")
    
    # Квантовые признаки
    if 'quantum_entropy' in features:
        entropy = features['quantum_entropy']
        if entropy > 6.0:
            context_parts.append("квантовая энтропия высокая (неопределённость)")
        elif entropy < 4.0:
            context_parts.append("квантовая энтропия низкая (определённость)")
    
    context = ", ".join(context_parts) + "."
    
    # Формируем результат
    actual_direction = "вверх" if outcome['profit'] > 0 else "вниз"
    pips = abs(outcome['pips'])
    
    if outcome['correct']:
        result = f"Цена пошла {actual_direction} на {pips:.1f} пипсов, как и предсказывалось."
    else:
        predicted_dir = "вверх" if prediction['direction'] == 'UP' else "вниз"
        result = f"Предсказание было {predicted_dir}, но цена пошла {actual_direction} на {pips:.1f} пипсов."
    
    return {
        "prompt": f"Анализ {prediction['symbol']}: {context}",
        "completion": result,
        "weight": self.calculate_example_weight(prediction, outcome)
    }

Это изменение дало скачок точности на 8%. Модель начала понимать взаимосвязи между индикаторами, а не просто запоминать числа. Она научилась видеть, что перекупленность на RSI в восходящем тренде — это не то же самое, что перекупленность во флэте.


Практическая реализация: интеграция в торговую систему

Теоретически всё звучит красиво, но настоящая проверка — это интеграция в реальную торговую систему. SEAL не должен был быть отдельным модулем, который живёт своей жизнью. Он должен был стать естественной частью торгового цикла.

Вот как выглядит полный цикл в моей системе:

class QuantumFusionTrader:
    def __init__(self):
        self.catboost_model = CatBoostClassifier()
        self.catboost_model.load_model("models/catboost_quantum_3d.cbm")
        self.quantum_encoder = QuantumEncoder(n_qubits=8, n_shots=2048)
        self.seal = SEALSystem(model_name="koshtenco/quantum-trader-fusion-3d")
        self.active_trades = {}
    
    def analyze_and_trade(self, symbol: str):
        """Полный цикл анализа и торговли"""
        
        # 1. Получаем данные
        df = self.load_symbol_data(symbol)
        features = self.calculate_features(df)
        
        # 2. Квантовое кодирование
        quantum_features = self.quantum_encoder.encode_and_measure(
            features.iloc[-1].values
        )
        
        # 3. CatBoost предсказание
        catboost_pred = self.catboost_model.predict_proba(
            features.iloc[-1:].values
        )[0]
        catboost_confidence = max(catboost_pred) * 100
        catboost_direction = 'UP' if catboost_pred[1] > 0.5 else 'DOWN'
        
        # 4. LLM анализ с учётом SEAL опыта
        llm_response = self.get_llm_prediction(
            symbol, features.iloc[-1], quantum_features, 
            catboost_direction, catboost_confidence
        )
        
        # 5. Финальное решение
        final_decision = self.combine_predictions(
            catboost_pred, llm_response
        )
        
        # 6. Открываем сделку
        if final_decision['confidence'] >= MIN_CONFIDENCE:
            ticket = self.open_trade(symbol, final_decision)
            
            # Сохраняем контекст для SEAL
            self.active_trades[ticket] = {
                'symbol': symbol,
                'prediction': final_decision,
                'features': features.iloc[-1].to_dict(),
                'quantum_features': quantum_features,
                'open_time': time.time(),
                'open_price': self.get_current_price(symbol)
            }
    
    def on_trade_closed(self, ticket: int, close_price: float, profit: float):
        """Обработка закрытия сделки"""
        
        if ticket not in self.active_trades:
            return
        
        trade_data = self.active_trades[ticket]
        
        # Вычисляем результат
        pips = self.calculate_pips(
            trade_data['open_price'], 
            close_price, 
            trade_data['symbol']
        )
        
        correct = (profit > 0 and trade_data['prediction']['direction'] == 'UP') or \
                  (profit < 0 and trade_data['prediction']['direction'] == 'DOWN')
        
        outcome = {
            'close_price': close_price,
            'profit': profit,
            'pips': pips,
            'correct': correct,
            'duration': time.time() - trade_data['open_time']
        }
        
        # SEAL записывает результат
        self.seal.record_trade(trade_data['prediction'], outcome)
        
        # Проверяем необходимость дообучения
        if self.seal.should_trigger_learning():
            self.trigger_seal_learning()
        
        del self.active_trades[ticket]

Критически важный момент — SEAL срабатывает асинхронно. Мы не блокируем торговлю на время обучения. Когда триггер срабатывает, я запускаю файнтьюнинг в фоновом процессе:

def trigger_seal_learning(self):
    """Запускаем дообучение асинхронно"""
    
    examples = self.seal.prepare_learning_dataset()
    
    if len(examples) < 30:
        log.warning("SEAL: Недостаточно примеров для обучения")
        return
    
    # Сохраняем в JSONL
    dataset_path = f"seal_datasets/iteration_{self.seal.iteration}.jsonl"
    with open(dataset_path, 'w', encoding='utf-8') as f:
        for ex in examples:
            f.write(json.dumps(ex, ensure_ascii=False) + '\n')
    
    # Запускаем ollama finetune в фоне
    modelfile_content = f"""
FROM {self.seal.model_name}
ADAPTER {dataset_path}
PARAMETER temperature 0.7
PARAMETER top_p 0.9
"""
    
    modelfile_path = f"seal_models/Modelfile_{self.seal.iteration}"
    with open(modelfile_path, 'w') as f:
        f.write(modelfile_content)
    
    # Асинхронный запуск
    new_model_name = f"{self.seal.model_name}-seal-{self.seal.iteration}"
    
    process = subprocess.Popen(
        ['ollama', 'create', new_model_name, '-f', modelfile_path],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    
    log.info(f"SEAL: Запущено дообучение → {new_model_name}")
    
    # Не ждём завершения - торговля продолжается
    # Новая модель будет использоваться после завершения обучения
    self.seal.pending_model = new_model_name
    self.seal.iteration += 1


Мониторинг эволюции: как понять, что SEAL работает

Самая большая опасность адаптивных систем — незаметная деградация. Модель может учиться, но учиться не тому. Мне нужна была система мониторинга, которая показывала бы не просто win rate, а направление эволюции.

Я построил трекер метрик с временным анализом:

class SEALMetricsTracker:
    def __init__(self):
        self.metrics_history = []
        self.window_size = 100  # Анализируем последние 100 сделок
    
    def add_trade_result(self, prediction: dict, outcome: dict):
        """Добавляем результат сделки"""
        
        metrics = {
            'timestamp': time.time(),
            'correct': outcome['correct'],
            'confidence': prediction['confidence'],
            'pips': outcome['pips'],
            'profit': outcome['profit'],
            'model_version': self.current_model_version
        }
        
        self.metrics_history.append(metrics)
        
        # Периодический анализ
        if len(self.metrics_history) % self.window_size == 0:
            self.analyze_evolution()
    
    def analyze_evolution(self):
        """Анализируем эволюцию модели"""
        
        if len(self.metrics_history) < self.window_size * 2:
            return
        
        # Берём два последовательных окна
        recent = self.metrics_history[-self.window_size:]
        previous = self.metrics_history[-self.window_size*2:-self.window_size]
        
        # Сравниваем ключевые метрики
        recent_accuracy = sum(1 for t in recent if t['correct']) / len(recent)
        previous_accuracy = sum(1 for t in previous if t['correct']) / len(previous)
        
        recent_profit = sum(t['profit'] for t in recent)
        previous_profit = sum(t['profit'] for t in previous)
        
        # Калибровка уверенности
        recent_calibration = self._calculate_calibration(recent)
        previous_calibration = self._calculate_calibration(previous)
        
        log.info(f"SEAL ЭВОЛЮЦИЯ:")
        log.info(f"  Точность: {previous_accuracy:.1%}{recent_accuracy:.1%} " +
                f"({self._format_delta(recent_accuracy - previous_accuracy)})")
        log.info(f"  PnL: {previous_profit:.2f}{recent_profit:.2f} " +
                f"({self._format_delta(recent_profit - previous_profit)})")
        log.info(f"  Калибровка: {previous_calibration:.3f}{recent_calibration:.3f} " +
                f"({self._format_delta(recent_calibration - previous_calibration)})")
    
    def _calculate_calibration(self, trades: list) -> float:
        """Вычисляем качество калибровки уверенности"""
        
        # Группируем сделки по уровню уверенности
        bins = [0, 60, 70, 80, 90, 100]
        calibration_error = 0
        
        for i in range(len(bins)-1):
            bin_trades = [t for t in trades 
                         if bins[i] <= t['confidence'] < bins[i+1]]
            
            if not bin_trades:
                continue
            
            # Фактическая точность в этом бине
            actual_accuracy = sum(1 for t in bin_trades if t['correct']) / len(bin_trades)
            
            # Ожидаемая точность = средняя уверенность
            expected_accuracy = np.mean([t['confidence']/100 for t in bin_trades])
            
            # Ошибка калибровки
            calibration_error += abs(actual_accuracy - expected_accuracy) * len(bin_trades)
        
        calibration_error /= len(trades)
        
        # Идеальная калибровка = 0, плохая = 1
        return 1.0 - calibration_error
    
    def _format_delta(self, delta: float) -> str:
        """Форматируем изменение"""
        sign = "+" if delta >= 0 else ""
        direction = "[UP]" if delta >= 0 else "[DOWN]"
        return f"{direction} {sign}{delta:.2%}"

Калибровка уверенности оказалась критически важной метрикой. Я заметил интересный паттерн: при деградации модели точность падала медленно, но калибровка ухудшалась быстро. Модель становилась слишком уверенной в неправильных предсказаниях. SEAL это исправлял — после дообучения на примерах с высокой уверенностью и плохим результатом, модель становилась осторожнее.


Неожиданные открытия: чему SEAL научился сам

Самым удивительным оказалось то, чему SEAL научился без моего участия. Анализируя примеры с высоким весом в памяти системы, я обнаружил паттерны, которые сам никогда не программировал.

Модель научилась распознавать ложные пробои. Она начала ассоциировать высокую квантовую энтропию + резкий скачок объёма + касание границы Боллинджера с откатом, даже если классические индикаторы давали сигнал в сторону пробоя. Я проверил вручную — это работало с точностью 73%.

Второе открытие — модель научилась различать типы волатильности. Она понимала разницу между волатильностью от новостей (резкая, но короткая) и волатильностью смены тренда (постепенная, но устойчивая). Это знание пришло из анализа собственных ошибок: сделки, открытые на новостной волатильности, часто закрывались с убытком из-за быстрого отката.

Третье — модель начала группировать валютные пары. Она понимала, что EUR/USD и GBP/USD часто двигаются синхронно, а USD/CHF — в противофазе. Когда она видела сильный сигнал на евро, но слабый на фунте, это становилось дополнительным фактором неопределённости.

Всё это возникло само, из анализа тысяч сделок. Я не программировал эти правила — SEAL вывел их из опыта.


Ограничения и проблемы

SEAL не волшебная палочка. У системы есть фундаментальные ограничения, о которых нужно знать.

Проблема первая: черные лебеди. SEAL учится на своём опыте, а значит, не может предсказать то, чего никогда не видел. Вспышка пандемии в марте 2020, Brexit vote, снятие привязки швейцарского франка — в таких событиях SEAL бесполезен. Более того, он может быть опасен, потому что с уверенностью предскажет продолжение нормального рынка.

Решение: я добавил детектор аномалий, который отключает торговлю при экстремальных движениях.

def is_market_abnormal(self, symbol: str) -> bool:
    """Детектирует аномальные рыночные условия"""
    
    df = self.load_symbol_data(symbol, bars=100)
    
    # Текущая волатильность vs историческая
    recent_volatility = df['close'].pct_change().tail(20).std()
    historical_volatility = df['close'].pct_change().std()
    
    # Аномалия = волатильность выше в 3+ раза
    if recent_volatility > historical_volatility * 3:
        log.warning(f"АНОМАЛИЯ на {symbol}: волатильность {recent_volatility/historical_volatility:.1f}x")
        return True
    
    # Проверка гэпов
    gaps = abs((df['open'] - df['close'].shift(1)) / df['close'].shift(1))
    if gaps.tail(5).max() > 0.01:  # Гэп > 1%
        log.warning(f"АНОМАЛИЯ на {symbol}: обнаружен гэп {gaps.tail(5).max():.2%}")
        return True
    
    return False

Проблема вторая: переобучение на успехе. Если рынок случайно попадает в режим, где простая стратегия работает отлично, SEAL начинает переобучаться на этом успехе. Модель становится слишком агрессивной, игнорируя риски.

Я столкнулся с этим в ноябре, когда EUR/USD целую неделю находился в чистом восходящем тренде. SEAL начал открывать только лонги, игнорируя сигналы на коррекцию. Когда тренд развернулся, серия убытков была болезненной.

Решение: добавил анализ разнообразия стратегий.

def check_strategy_diversity(self) -> bool:
    """Проверяем разнообразие торговых решений"""
    
    recent_trades = self.seal.trade_memory[-50:]
    
    if len(recent_trades) < 30:
        return True  # Недостаточно данных
    
    # Считаем баланс направлений
    up_trades = sum(1 for t in recent_trades if t['direction'] == 'UP')
    down_trades = sum(1 for t in recent_trades if t['direction'] == 'DOWN')
    
    balance = min(up_trades, down_trades) / max(up_trades, down_trades)
    
    if balance < 0.3:  # Более 70% сделок в одну сторону
        log.warning(f"ПРЕДУПРЕЖДЕНИЕ: Низкое разнообразие стратегий (баланс: {balance:.1%})")
        # Повышаем порог уверенности для доминирующего направления
        return False
    
    return True

Проблема третья: вычислительная нагрузка. Файнтьюнинг LLM на 200 примерах занимает 15-20 минут на RTX 3090. В периоды высокой активности SEAL может запускать обучение каждые 2-3 дня. Это нормально для десктопа, но проблематично для VPS.

Решение оказалось в квантизации и оптимизации:

# В Modelfile для файнтьюнинга
PARAMETER num_gpu 1
PARAMETER num_thread 8
PARAMETER quantization q4_0  # 4-bit квантизация
PARAMETER batch_size 4
PARAMETER epochs 3  # Меньше эпох для быстрого обучения

4-битная квантизация ускорила обучение в 2.5 раза с минимальной потерей качества. Три эпохи вместо пяти — ещё 40% времени. Итого, файнтьюнинг сократился.

Вот результаты бэктеста системы:



Критический взгляд на результаты бэктеста

Бэктест показал почти полное отсутствие убыточных сделок. Это не повод для оптимизма, а серьёзный тревожный сигнал.

В реальной торговле такие результаты практически недостижимы. С высокой вероятностью они указывают на фундаментальные проблемы в тестировании или в самой модели.

Возможные причины:

  • Переобучение (overfitting).
    Модель слишком точно подогнана под исторические данные и не обладает обобщающей способностью. Это типичная ошибка при использовании сложных моделей на ограниченной выборке.

  • Утечка информации из будущего (look-ahead bias).
    В расчётах могла неявно использоваться информация, недоступная в момент принятия торгового решения — напрямую или через особенности построения признаков.

  • Недостаточный объём данных.
    Небольшое число сделок или короткий период тестирования делает результаты статистически незначимыми и крайне нестабильными.

  • Игнорирование транзакционных издержек.
    Спред, комиссия и проскальзывание способны полностью уничтожить видимую прибыль, особенно при высокой частоте сделок.

  • Подгонка параметров (selection/survivor bias).
    Если параметры системы подбирались на том же наборе данных, на котором оценивалась эффективность, бэктест теряет диагностическую ценность.

На текущий момент подобные результаты не подтверждены живой торговлей. До получения репрезентативной статистики на реальном счёте — не менее нескольких сотен сделок, охватывающих разные рыночные режимы — бэктест следует рассматривать исключительно как предварительный эксперимент.

Практическое правило:
если результаты бэктеста выглядят слишком хорошо, чтобы быть правдой, почти всегда так и есть.


Будущее SEAL: направления развития

Текущая версия SEAL — это базовый прототип. Дальнейшее развитие логично в нескольких направлениях:

Мультимодельный ансамбль.
Вместо одной модели — популяция из 5–7 специализированных. Каждая оптимизирована под свой рыночный режим (тренд, флэт, высокая волатильность). SEAL выбирает активную модель на основе текущих условий.

Кросс-символьное обучение.
Сейчас обучение ведётся отдельно по каждому инструменту. Это ограничение. Коррелированные пары (EUR/USD, GBP/USD и т.д.) могут использовать общие представления и перенос знаний, ускоряя адаптацию и снижая переобучение.

Иерархическая память.
Память разделяется на уровни:

  • краткосрочная — последние сделки и текущий режим,
  • среднесрочная — паттерны недель и месяцев,
  • долгосрочная — редкие, но критически важные события.
Каждый уровень используется с разной частотой и весом при обучении.

Активное обучение.
SEAL сам определяет, какие данные наиболее информативны, и усиливает обучение на сложных или редких сценариях, включая генерацию синтетических примеров.


Заключение: непрерывная адаптация вместо статичных моделей

Работа с SEAL показывает ограниченность классического подхода «обучили → используем до деградации». В условиях быстро меняющихся рынков такие системы неизбежно теряют актуальность.

SEAL реализует другой принцип: непрерывное обучение с сохранением контекста. Модель адаптируется к новым условиям, не забывая прошлые паттерны, и использует собственную торговую историю как источник обучения.

Ключевая идея проста:

  • каждая сделка — новый обучающий сигнал,
  • каждая ошибка — корректировка модели,
  • каждый успешный паттерн — подтверждённое знание.

Это не "обучение на истории", а обучение на реальных результатах в реальном времени.

Дисклеймер
SEAL находится на экспериментальной стадии. Прошлые результаты не гарантируют будущих. Алгоритмическая торговля связана с риском потери капитала и требует независимой валидации, строгого контроля рисков и полного понимания ограничений системы.

Прикрепленные файлы |
Забытая классика объёма: индикатор "Finite Volume Elements" для современных рынков Забытая классика объёма: индикатор "Finite Volume Elements" для современных рынков
В статье рассмотрим индикатор Finite Volume Elements (FVE), позволяющий выявлять истинные потоки капитала на рынке. Реализуем FVE для MetaTrader 5 и рассмотрим рекомендации по его использованию в торговле.
Нейросети в трейдинге: Возмущённые модели пространства состояний для анализа рыночной динамики (Окончание) Нейросети в трейдинге: Возмущённые модели пространства состояний для анализа рыночной динамики (Окончание)
В статье представлена адаптация фреймворка P-SSE для задач анализа финансовых рынков. Реализованные решения обеспечивают последовательную обработку локальных событий, аккумулируя их в согласованное представление рыночной динамики. Подход позволяет прогнозировать изменения рынка на заданный горизонт планирования, сохраняя высокую чувствительность к микроимпульсам и минимизируя вычислительные затраты.
Нейросети в трейдинге: Асинхронная обработка событий в потоковых моделях (EVA-Flow) Нейросети в трейдинге: Асинхронная обработка событий в потоковых моделях (EVA-Flow)
В статье знакомимся с фреймворком EVA-Flow для низколатентной и высокочастотной оценки оптического потока на основе событийных данных. Модель сочетает адаптивное представление потока через Unified Voxel Grid с пространственно-временной рекуррентной архитектурой SMR, обеспечивая стабильное и точное прогнозирование движения в режиме реального времени.
Эко-эволюционный алгоритм — Eco-inspired Evolutionary Algorithm (ECO) Эко-эволюционный алгоритм — Eco-inspired Evolutionary Algorithm (ECO)
В статье рассматривается алгоритм оптимизации ECO, основанный на экологических концепциях: популяции объединяются в хабитаты по принципу территориальной близости, обмениваются генетическим материалом внутри хабитатов и мигрируют между ними. Несмотря на богатый набор операторов и красивую биологическую метафору, алгоритм показал результат, какой, подробности ниже.