import numpy as np import pandas as pd import matplotlib.pyplot as plt from datetime import datetime, timedelta import time # ========================================== # 1. ГЕНЕРАЦИЯ ДАННЫХ (50k свечей, как у Google) # ========================================== def generate_test_data(n=50000): np.random.seed(42) returns = np.random.normal(0.00002, 0.002, n) price = 60000 * np.exp(np.cumsum(returns)) df = pd.DataFrame(index=pd.date_range('2024-01-01', periods=n, freq='min')) df['Close'] = price df['Open'] = df['Close'].shift(1).fillna(price[0]) df['High'] = df[['Open','Close']].max(axis=1) + np.random.exponential(10, n) df['Low'] = df[['Open','Close']].min(axis=1) - np.random.exponential(10, n) return df # ========================================== # 2. РАСЧЁТ ИШИМОКУ (ПОЛНЫЙ, С ЧИКУ) # ========================================== def ichimoku_full(df, tenkan=9, kijun=26, senkou_b=52): df = df.copy() # Tenkan-sen df['Tenkan'] = (df['High'].rolling(tenkan).max() + df['Low'].rolling(tenkan).min()) / 2 # Kijun-sen df['Kijun'] = (df['High'].rolling(kijun).max() + df['Low'].rolling(kijun).min()) / 2 # Senkou Span A (сдвиг вперёд на kijun) df['Senkou_A'] = ((df['Tenkan'] + df['Kijun']) / 2).shift(kijun) # Senkou Span B (сдвиг вперёд на kijun) df['Senkou_B'] = ((df['High'].rolling(senkou_b).max() + df['Low'].rolling(senkou_b).min()) / 2).shift(kijun) # Chikou Span (сдвиг НАЗАД на kijun — для сигналов) df['Chikou'] = df['Close'].shift(-kijun) # Границы Облака df['Kumo_Top'] = np.maximum(df['Senkou_A'], df['Senkou_B']) df['Kumo_Bottom'] = np.minimum(df['Senkou_A'], df['Senkou_B']) # СИГНАЛЫ (с учётом, что Chikou должна подтверждать) # Бычий: цена пересекает Облако снизу вверх И Chikou выше цены 26 свечей назад df['Signal_Buy'] = ( (df['Close'] > df['Kumo_Top']) & (df['Close'].shift(1) <= df['Kumo_Top'].shift(1)) & (df['Chikou'] > df['Close'].shift(kijun)) ) # Медвежий: цена пересекает Облако сверху вниз И Chikou ниже цены 26 свечей назад df['Signal_Sell'] = ( (df['Close'] < df['Kumo_Bottom']) & (df['Close'].shift(1) >= df['Kumo_Bottom'].shift(1)) & (df['Chikou'] < df['Close'].shift(kijun)) ) return df # ========================================== # 3. БЭКТЕСТ С ТРЕЙЛИНГ-СТОПОМ # ========================================== def backtest_trailing(df, stop_pct=0.02, take_profit=0.06): """ Вход по сигналу. Выход: либо TP (take_profit), либо SL (stop_pct от максимума после входа) """ trades = [] position = None entry_price = 0 entry_idx = 0 max_price = 0 min_price = 0 for i in range(len(df)): idx = df.index[i] # --- Вход в LONG --- if position is None and df['Signal_Buy'].iloc[i]: position = 'long' entry_price = df['Close'].iloc[i] entry_idx = idx max_price = entry_price min_price = entry_price continue # --- Вход в SHORT --- if position is None and df['Signal_Sell'].iloc[i]: position = 'short' entry_price = df['Close'].iloc[i] entry_idx = idx max_price = entry_price min_price = entry_price continue # --- Управление позицией --- if position == 'long': current = df['Close'].iloc[i] max_price = max(max_price, current) # Трейлинг-стоп от максимума sl = max_price * (1 - stop_pct) tp = entry_price * (1 + take_profit) if current <= sl or current >= tp: trades.append({ 'entry_idx': entry_idx, 'exit_idx': idx, 'type': 'long', 'entry': entry_price, 'exit': current, 'pnl': (current - entry_price) / entry_price, 'reason': 'SL' if current <= sl else 'TP' }) position = None elif position == 'short': current = df['Close'].iloc[i] min_price = min(min_price, current) sl = min_price * (1 + stop_pct) tp = entry_price * (1 - take_profit) if current >= sl or current <= tp: trades.append({ 'entry_idx': entry_idx, 'exit_idx': idx, 'type': 'short', 'entry': entry_price, 'exit': current, 'pnl': (entry_price - current) / entry_price, 'reason': 'SL' if current >= sl else 'TP' }) position = None return pd.DataFrame(trades) # ========================================== # 4. СТАТИСТИКА И ОТЧЁТ # ========================================== def print_stats(trades_df): if len(trades_df) == 0: print("Нет сделок") return win = trades_df[trades_df['pnl'] > 0] lose = trades_df[trades_df['pnl'] < 0] print(f"Всего сделок: {len(trades_df)}") print(f" Прибыльных: {len(win)} ({len(win)/len(trades_df)*100:.1f}%)") print(f" Убыточных: {len(lose)} ({len(lose)/len(trades_df)*100:.1f}%)") print(f"Средняя P&L: {trades_df['pnl'].mean()*100:.2f}%") print(f"Профит-фактор: {win['pnl'].sum() / abs(lose['pnl']).sum():.2f}" if len(lose)>0 else "Бесконечность") print(f"Макс. профит: {trades_df['pnl'].max()*100:.2f}%") print(f"Макс. убыток: {trades_df['pnl'].min()*100:.2f}%") # ========================================== # 5. ЗАПУСК # ========================================== print("Генерация 50 000 свечей...") raw = generate_test_data(50000) print("Расчёт Ишимоку...") start = time.time() df = ichimoku_full(raw) print(f"✅ За {time.time()-start:.4f} сек") print("\nБэктест с трейлинг-стопом 2% и TP 6%...") trades = backtest_trailing(df, stop_pct=0.02, take_profit=0.06) print_stats(trades) # ========================================== # 6. ВИЗУАЛИЗАЦИЯ (последние 300 свечей + сделки) # ========================================== plot_df = df.tail(300) fig, ax = plt.subplots(figsize=(16, 8)) # Цена ax.plot(plot_df.index, plot_df['Close'], label='Close', color='black', linewidth=1.5, alpha=0.7) # Линии ax.plot(plot_df.index, plot_df['Tenkan'], label='Tenkan (9)', color='blue', alpha=0.5) ax.plot(plot_df.index, plot_df['Kijun'], label='Kijun (26)', color='maroon', alpha=0.5) ax.plot(plot_df.index, plot_df['Senkou_A'], label='Senkou A', color='lime', alpha=0.4) ax.plot(plot_df.index, plot_df['Senkou_B'], label='Senkou B', color='red', alpha=0.4) # Заливка Облака ax.fill_between(plot_df.index, plot_df['Senkou_A'], plot_df['Senkou_B'], where=(plot_df['Senkou_A'] >= plot_df['Senkou_B']), color='green', alpha=0.15, label='Bullish Kumo') ax.fill_between(plot_df.index, plot_df['Senkou_A'], plot_df['Senkou_B'], where=(plot_df['Senkou_A'] < plot_df['Senkou_B']), color='red', alpha=0.15, label='Bearish Kumo') # Сигналы buys = plot_df[plot_df['Signal_Buy']] sells = plot_df[plot_df['Signal_Sell']] ax.scatter(buys.index, buys['Close'], marker='^', color='lime', s=120, label='BUY', zorder=5) ax.scatter(sells.index, sells['Close'], marker='v', color='red', s=120, label='SELL', zorder=5) # Сделки (вход/выход) — если есть if len(trades) > 0: for _, trade in trades.iterrows(): if trade['entry_idx'] in plot_df.index and trade['exit_idx'] in plot_df.index: ax.axvline(trade['entry_idx'], color='gray', linestyle='--', alpha=0.3) ax.axvline(trade['exit_idx'], color='gray', linestyle='--', alpha=0.3) ax.legend(loc='upper left') ax.set_title('Ichimoku + Трейлинг-стоп бэктест (300 свечей)') ax.grid(True, alpha=0.2) plt.tight_layout() plt.show()