Разработка системы forward-тестирования (paper trading)
Paper trading — торговля с виртуальными деньгами на реальных рыночных данных в реальном времени. Это мост между бэктестингом и live trading: стратегия работает с реальной инфраструктурой, реальными API биржи, реальными данными, но без реальных денег. Позволяет выявить проблемы, невидимые в бэктесте.
Что выявляет paper trading, чего не показывает бэктест
Latency issues — бэктест мгновенный, реальная система имеет задержки. Сигнал генерируется, но к моменту исполнения цена ушла.
API quirks — биржевые API имеют особенности: rate limits, неожиданное поведение при market volatility, расхождение real-time и historical data.
Software bugs — production-код содержит ошибки, невидимые при бэктестинге на pandas DataFrame.
Order management complexity — реальный management ордеров сложнее симуляции: partial fills, unexpected cancellations, margin calls.
Mental psychology — для manual/semi-manual стратегий: готов ли трейдер следовать сигналам в реальности?
Архитектура paper trading системы
import asyncio
from dataclasses import dataclass
from decimal import Decimal
import time
class PaperBroker:
"""
Брокер для paper trading.
Использует real-time данные биржи, но не отправляет реальные ордера.
"""
def __init__(self, exchange_client, initial_balance: dict[str, Decimal]):
self.exchange = exchange_client
self.balance = dict(initial_balance)
self.orders: dict[str, PaperOrder] = {}
self.positions: dict[str, PaperPosition] = {}
self.trade_history = []
self.order_id_counter = 0
async def place_order(
self,
symbol: str,
side: str,
order_type: str,
quantity: Decimal,
price: Decimal = None,
) -> PaperOrder:
order_id = f"paper_{self.order_id_counter:06d}"
self.order_id_counter += 1
# Проверяем баланс
if side == 'BUY':
required_quote = quantity * (price or await self.get_market_price(symbol, 'ask'))
quote_asset = symbol.split('/')[1]
if self.balance.get(quote_asset, Decimal(0)) < required_quote:
raise InsufficientFundsError(f"Need {required_quote} {quote_asset}")
order = PaperOrder(
id=order_id,
symbol=symbol,
side=side,
type=order_type,
quantity=quantity,
price=price,
status='OPEN',
created_at=time.time(),
)
self.orders[order_id] = order
# Для market orders — немедленное исполнение
if order_type == 'MARKET':
await self.execute_market_order(order)
return order
async def get_market_price(self, symbol: str, side: str) -> Decimal:
"""Получаем real-time цену с биржи"""
orderbook = await self.exchange.fetch_order_book(symbol, limit=5)
if side == 'ask':
return Decimal(str(orderbook['asks'][0][0]))
else:
return Decimal(str(orderbook['bids'][0][0]))
async def execute_market_order(self, order: PaperOrder):
"""Исполняем market order по текущей рыночной цене + slippage"""
price = await self.get_market_price(order.symbol, 'ask' if order.side == 'BUY' else 'bid')
# Применяем реалистичное проскальзывание
slippage = Decimal('0.0005')
if order.side == 'BUY':
fill_price = price * (1 + slippage)
else:
fill_price = price * (1 - slippage)
commission = order.quantity * fill_price * Decimal('0.001')
await self.process_fill(order, fill_price, commission)
async def process_fill(self, order: PaperOrder, fill_price: Decimal, commission: Decimal):
base_asset, quote_asset = order.symbol.split('/')
cost = order.quantity * fill_price
if order.side == 'BUY':
self.balance[quote_asset] = self.balance.get(quote_asset, Decimal(0)) - cost - commission
self.balance[base_asset] = self.balance.get(base_asset, Decimal(0)) + order.quantity
else:
self.balance[base_asset] = self.balance.get(base_asset, Decimal(0)) - order.quantity
self.balance[quote_asset] = self.balance.get(quote_asset, Decimal(0)) + cost - commission
order.fill_price = fill_price
order.status = 'FILLED'
order.filled_at = time.time()
# Записываем сделку
self.trade_history.append({
'timestamp': order.filled_at,
'symbol': order.symbol,
'side': order.side,
'quantity': float(order.quantity),
'price': float(fill_price),
'commission': float(commission),
})
async def check_limit_orders(self):
"""Периодически проверяем pending limit orders"""
while True:
for order_id, order in list(self.orders.items()):
if order.status != 'OPEN' or order.type != 'LIMIT':
continue
current_price = await self.get_market_price(order.symbol, 'last')
should_fill = (
(order.side == 'BUY' and current_price <= order.price) or
(order.side == 'SELL' and current_price >= order.price)
)
if should_fill:
commission = order.quantity * order.price * Decimal('0.0001') # maker fee
await self.process_fill(order, order.price, commission)
await asyncio.sleep(1) # проверяем каждую секунду
Сравнение paper vs backtest результатов
class PaperVsBacktestComparator:
def compare(self, paper_result: PaperTradingResult, backtest_result: BacktestResult) -> dict:
"""Сравниваем результаты paper trading с ожиданиями из бэктеста"""
paper_sharpe = self.compute_sharpe(paper_result.equity_curve)
expected_sharpe = backtest_result.metrics.sharpe_ratio
# Execution quality
slippage_analysis = self.analyze_slippage(paper_result.trades, backtest_result.trades)
return {
'expected_sharpe': expected_sharpe,
'actual_sharpe': paper_sharpe,
'sharpe_ratio': paper_sharpe / expected_sharpe if expected_sharpe > 0 else 0,
'avg_slippage_pct': slippage_analysis['avg_slippage'],
'latency_cost_pct': slippage_analysis['latency_cost'],
'signal_timing_drift': slippage_analysis['timing_drift'],
'ready_for_live': paper_sharpe >= expected_sharpe * 0.5, # порог 50%
}
Мониторинг paper trading
class PaperTradingMonitor:
async def run_dashboard(self, broker: PaperBroker):
"""Реальное время dashboard для paper trading"""
while True:
portfolio_value = await broker.get_portfolio_value()
pnl = portfolio_value - broker.initial_value
pnl_pct = pnl / broker.initial_value * 100
print(f"\r Portfolio: ${portfolio_value:,.2f} | P&L: {pnl_pct:+.2f}% | "
f"Trades: {len(broker.trade_history)} | "
f"Open Orders: {sum(1 for o in broker.orders.values() if o.status == 'OPEN')}",
end='', flush=True)
await asyncio.sleep(5)
Минимальный период paper trading перед запуском в live — 2–4 недели для стратегий с частыми сделками, 2–3 месяца для долгосрочных. Если за этот период нет критических инцидентов и результаты близки к ожидаемым из бэктеста — стратегия готова к real trading с небольшим начальным капиталом.







