Phase 4: Paper trading implementation
- Create PaperStrategyInstance extending StrategyInstance - Integrate PaperBroker for simulated order execution - Add trade_order() method translating Blockly calls to broker - Add mode selection in Strategies.create_strategy_instance() - Update execute_strategy() to support paper/backtest/live modes - Include comprehensive tests for paper trading functionality Paper trading now works with: - Market and limit orders - Position tracking with P&L - Balance management - Trade history - Reset functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1bb224b15d
commit
51ec74175d
|
|
@ -7,9 +7,10 @@ import datetime as dt
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from PythonGenerator import PythonGenerator
|
from PythonGenerator import PythonGenerator
|
||||||
from StrategyInstance import StrategyInstance
|
from StrategyInstance import StrategyInstance
|
||||||
|
from brokers import TradingMode
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -61,7 +62,100 @@ class Strategies:
|
||||||
self.default_exchange = 'Binance'
|
self.default_exchange = 'Binance'
|
||||||
self.default_symbol = 'BTCUSD'
|
self.default_symbol = 'BTCUSD'
|
||||||
|
|
||||||
self.active_instances: dict[tuple[int, str], StrategyInstance] = {} # Key: (user_id, strategy_id)
|
self.active_instances: dict[tuple[int, str, str], StrategyInstance] = {} # Key: (user_id, strategy_id, mode)
|
||||||
|
|
||||||
|
def create_strategy_instance(
|
||||||
|
self,
|
||||||
|
mode: str,
|
||||||
|
strategy_instance_id: str,
|
||||||
|
strategy_id: str,
|
||||||
|
strategy_name: str,
|
||||||
|
user_id: int,
|
||||||
|
generated_code: str,
|
||||||
|
initial_balance: float = 10000.0,
|
||||||
|
commission: float = 0.001,
|
||||||
|
slippage: float = 0.0,
|
||||||
|
price_provider: Any = None,
|
||||||
|
) -> StrategyInstance:
|
||||||
|
"""
|
||||||
|
Factory method to create the appropriate strategy instance based on mode.
|
||||||
|
|
||||||
|
:param mode: Trading mode ('backtest', 'paper', 'live').
|
||||||
|
:param strategy_instance_id: Unique instance identifier.
|
||||||
|
:param strategy_id: Strategy identifier.
|
||||||
|
:param strategy_name: Strategy name.
|
||||||
|
:param user_id: User identifier.
|
||||||
|
:param generated_code: Generated Python code from Blockly.
|
||||||
|
:param initial_balance: Starting balance for paper/backtest.
|
||||||
|
:param commission: Commission rate.
|
||||||
|
:param slippage: Slippage rate.
|
||||||
|
:param price_provider: Callable for getting current prices.
|
||||||
|
:return: Strategy instance appropriate for the mode.
|
||||||
|
"""
|
||||||
|
mode = mode.lower()
|
||||||
|
|
||||||
|
if mode == TradingMode.PAPER:
|
||||||
|
from paper_strategy_instance import PaperStrategyInstance
|
||||||
|
return PaperStrategyInstance(
|
||||||
|
strategy_instance_id=strategy_instance_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
user_id=user_id,
|
||||||
|
generated_code=generated_code,
|
||||||
|
data_cache=self.data_cache,
|
||||||
|
indicators=self.indicators_manager,
|
||||||
|
trades=self.trades,
|
||||||
|
initial_balance=initial_balance,
|
||||||
|
commission=commission,
|
||||||
|
slippage=slippage if slippage > 0 else 0.0005,
|
||||||
|
price_provider=price_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif mode == TradingMode.BACKTEST:
|
||||||
|
from backtest_strategy_instance import BacktestStrategyInstance
|
||||||
|
return BacktestStrategyInstance(
|
||||||
|
strategy_instance_id=strategy_instance_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
user_id=user_id,
|
||||||
|
generated_code=generated_code,
|
||||||
|
data_cache=self.data_cache,
|
||||||
|
indicators=self.indicators_manager,
|
||||||
|
trades=self.trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif mode == TradingMode.LIVE:
|
||||||
|
# Live trading not yet implemented - fall back to paper for safety
|
||||||
|
logger.warning("Live trading mode not yet implemented. Using paper trading instead.")
|
||||||
|
from paper_strategy_instance import PaperStrategyInstance
|
||||||
|
return PaperStrategyInstance(
|
||||||
|
strategy_instance_id=strategy_instance_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
user_id=user_id,
|
||||||
|
generated_code=generated_code,
|
||||||
|
data_cache=self.data_cache,
|
||||||
|
indicators=self.indicators_manager,
|
||||||
|
trades=self.trades,
|
||||||
|
initial_balance=initial_balance,
|
||||||
|
commission=commission,
|
||||||
|
slippage=slippage,
|
||||||
|
price_provider=price_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Default to standard StrategyInstance for unknown modes
|
||||||
|
logger.warning(f"Unknown mode '{mode}'. Using standard StrategyInstance.")
|
||||||
|
return StrategyInstance(
|
||||||
|
strategy_instance_id=strategy_instance_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
user_id=user_id,
|
||||||
|
generated_code=generated_code,
|
||||||
|
data_cache=self.data_cache,
|
||||||
|
indicators=self.indicators_manager,
|
||||||
|
trades=self.trades,
|
||||||
|
)
|
||||||
|
|
||||||
def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict:
|
def _save_strategy(self, strategy_data: dict, default_source: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -351,11 +445,24 @@ class Strategies:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating stats for strategy '{strategy_id}': {e}", exc_info=True)
|
logger.error(f"Error updating stats for strategy '{strategy_id}': {e}", exc_info=True)
|
||||||
|
|
||||||
def execute_strategy(self, strategy_data: dict[str, Any]) -> dict[str, Any]:
|
def execute_strategy(
|
||||||
|
self,
|
||||||
|
strategy_data: dict[str, Any],
|
||||||
|
mode: str = 'backtest',
|
||||||
|
initial_balance: float = 10000.0,
|
||||||
|
commission: float = 0.001,
|
||||||
|
slippage: float = 0.0,
|
||||||
|
price_provider: Any = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Executes a strategy based on the provided strategy data.
|
Executes a strategy based on the provided strategy data.
|
||||||
|
|
||||||
:param strategy_data: A dictionary containing strategy details.
|
:param strategy_data: A dictionary containing strategy details.
|
||||||
|
:param mode: Trading mode ('backtest', 'paper', 'live').
|
||||||
|
:param initial_balance: Starting balance for paper/backtest modes.
|
||||||
|
:param commission: Commission rate.
|
||||||
|
:param slippage: Slippage rate for market orders.
|
||||||
|
:param price_provider: Callable for getting current prices (paper/live).
|
||||||
:return: A dictionary indicating success or failure with relevant messages.
|
:return: A dictionary indicating success or failure with relevant messages.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -367,9 +474,9 @@ class Strategies:
|
||||||
if not strategy_id or not strategy_name or not user_id:
|
if not strategy_id or not strategy_name or not user_id:
|
||||||
return {"success": False, "message": "Strategy data is incomplete."}
|
return {"success": False, "message": "Strategy data is incomplete."}
|
||||||
|
|
||||||
# Generate a deterministic strategy_instance_id
|
# Generate a deterministic strategy_instance_id (include mode for uniqueness)
|
||||||
strategy_instance_id = f"{user_id}_{strategy_name}"
|
strategy_instance_id = f"{user_id}_{strategy_name}_{mode}"
|
||||||
instance_key = (user_id, strategy_id) # Unique key for the strategy-user pair
|
instance_key = (user_id, strategy_id, mode) # Include mode in key
|
||||||
|
|
||||||
# Retrieve or create StrategyInstance
|
# Retrieve or create StrategyInstance
|
||||||
if instance_key not in self.active_instances:
|
if instance_key not in self.active_instances:
|
||||||
|
|
@ -377,16 +484,18 @@ class Strategies:
|
||||||
if not generated_code:
|
if not generated_code:
|
||||||
return {"success": False, "message": "No 'next()' method defined for the strategy."}
|
return {"success": False, "message": "No 'next()' method defined for the strategy."}
|
||||||
|
|
||||||
# Instantiate StrategyInstance
|
# Use factory method to create appropriate instance for mode
|
||||||
strategy_instance = StrategyInstance(
|
strategy_instance = self.create_strategy_instance(
|
||||||
|
mode=mode,
|
||||||
strategy_instance_id=strategy_instance_id,
|
strategy_instance_id=strategy_instance_id,
|
||||||
strategy_id=strategy_id,
|
strategy_id=strategy_id,
|
||||||
strategy_name=strategy_name,
|
strategy_name=strategy_name,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
generated_code=generated_code,
|
generated_code=generated_code,
|
||||||
data_cache=self.data_cache,
|
initial_balance=initial_balance,
|
||||||
indicators=self.indicators_manager,
|
commission=commission,
|
||||||
trades=self.trades
|
slippage=slippage,
|
||||||
|
price_provider=price_provider,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store in active_instances
|
# Store in active_instances
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
"""
|
||||||
|
Paper Trading Strategy Instance for BrighterTrading.
|
||||||
|
|
||||||
|
Extends StrategyInstance with paper trading capabilities using
|
||||||
|
the PaperBroker for simulated order execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Optional
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from StrategyInstance import StrategyInstance
|
||||||
|
from brokers import PaperBroker, OrderSide, OrderType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PaperStrategyInstance(StrategyInstance):
|
||||||
|
"""
|
||||||
|
Strategy instance for paper trading mode.
|
||||||
|
|
||||||
|
Uses PaperBroker for simulated order execution with live price data.
|
||||||
|
Maintains full strategy execution context while simulating trades.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
strategy_instance_id: str,
|
||||||
|
strategy_id: str,
|
||||||
|
strategy_name: str,
|
||||||
|
user_id: int,
|
||||||
|
generated_code: str,
|
||||||
|
data_cache: Any,
|
||||||
|
indicators: Any | None,
|
||||||
|
trades: Any | None,
|
||||||
|
initial_balance: float = 10000.0,
|
||||||
|
commission: float = 0.001,
|
||||||
|
slippage: float = 0.0005,
|
||||||
|
price_provider: Any = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the PaperStrategyInstance.
|
||||||
|
|
||||||
|
:param strategy_instance_id: Unique identifier for this instance.
|
||||||
|
:param strategy_id: Strategy identifier.
|
||||||
|
:param strategy_name: Strategy name.
|
||||||
|
:param user_id: User identifier.
|
||||||
|
:param generated_code: Python code generated from Blockly.
|
||||||
|
:param data_cache: DataCache instance.
|
||||||
|
:param indicators: Indicators manager.
|
||||||
|
:param trades: Trades manager (not used directly, kept for compatibility).
|
||||||
|
:param initial_balance: Starting paper trading balance.
|
||||||
|
:param commission: Commission rate for paper trades.
|
||||||
|
:param slippage: Slippage rate for market orders.
|
||||||
|
:param price_provider: Callable to get current prices.
|
||||||
|
"""
|
||||||
|
# Initialize the paper broker
|
||||||
|
self.paper_broker = PaperBroker(
|
||||||
|
initial_balance=initial_balance,
|
||||||
|
commission=commission,
|
||||||
|
slippage=slippage,
|
||||||
|
price_provider=price_provider,
|
||||||
|
data_cache=data_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set broker before calling parent __init__
|
||||||
|
self.broker = self.paper_broker
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
strategy_instance_id, strategy_id, strategy_name, user_id,
|
||||||
|
generated_code, data_cache, indicators, trades
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize balance attributes from paper broker
|
||||||
|
self.starting_balance = initial_balance
|
||||||
|
self.current_balance = initial_balance
|
||||||
|
self.available_balance = initial_balance
|
||||||
|
self.available_strategy_balance = initial_balance
|
||||||
|
|
||||||
|
# Update exec_context with balance attributes
|
||||||
|
self.exec_context['starting_balance'] = self.starting_balance
|
||||||
|
self.exec_context['current_balance'] = self.current_balance
|
||||||
|
self.exec_context['available_balance'] = self.available_balance
|
||||||
|
self.exec_context['available_strategy_balance'] = self.available_strategy_balance
|
||||||
|
|
||||||
|
logger.info(f"PaperStrategyInstance created with balance: {initial_balance}")
|
||||||
|
|
||||||
|
def trade_order(
|
||||||
|
self,
|
||||||
|
trade_type: str,
|
||||||
|
size: float,
|
||||||
|
order_type: str,
|
||||||
|
source: dict = None,
|
||||||
|
tif: str = 'GTC',
|
||||||
|
stop_loss: dict = None,
|
||||||
|
trailing_stop: dict = None,
|
||||||
|
take_profit: dict = None,
|
||||||
|
limit: dict = None,
|
||||||
|
trailing_limit: dict = None,
|
||||||
|
target_market: dict = None,
|
||||||
|
name_order: dict = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Place an order via the paper broker.
|
||||||
|
|
||||||
|
This method translates the Blockly-generated order call to
|
||||||
|
the PaperBroker interface.
|
||||||
|
"""
|
||||||
|
# Extract symbol from source
|
||||||
|
symbol = 'BTC/USDT' # Default
|
||||||
|
if source:
|
||||||
|
symbol = source.get('symbol') or source.get('market', 'BTC/USDT')
|
||||||
|
|
||||||
|
# Map trade_type to OrderSide
|
||||||
|
if trade_type.lower() == 'buy':
|
||||||
|
side = OrderSide.BUY
|
||||||
|
elif trade_type.lower() == 'sell':
|
||||||
|
side = OrderSide.SELL
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Map order_type to OrderType
|
||||||
|
order_type_upper = order_type.upper()
|
||||||
|
if order_type_upper == 'MARKET':
|
||||||
|
bt_order_type = OrderType.MARKET
|
||||||
|
price = None
|
||||||
|
elif order_type_upper == 'LIMIT':
|
||||||
|
bt_order_type = OrderType.LIMIT
|
||||||
|
price = limit.get('value') if limit else self.get_current_price()
|
||||||
|
else:
|
||||||
|
bt_order_type = OrderType.MARKET
|
||||||
|
price = None
|
||||||
|
|
||||||
|
# Extract stop loss and take profit
|
||||||
|
stop_loss_price = stop_loss.get('value') if stop_loss else None
|
||||||
|
take_profit_price = take_profit.get('value') if take_profit else None
|
||||||
|
|
||||||
|
# Place the order
|
||||||
|
result = self.paper_broker.place_order(
|
||||||
|
symbol=symbol,
|
||||||
|
side=side,
|
||||||
|
order_type=bt_order_type,
|
||||||
|
size=size,
|
||||||
|
price=price,
|
||||||
|
stop_loss=stop_loss_price,
|
||||||
|
take_profit=take_profit_price,
|
||||||
|
time_in_force=tif
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
message = f"{trade_type.upper()} order placed: {size} {symbol} @ {order_type_upper}"
|
||||||
|
self.notify_user(message)
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
# Track order in history
|
||||||
|
self.orders.append({
|
||||||
|
'order_id': result.order_id,
|
||||||
|
'symbol': symbol,
|
||||||
|
'side': trade_type,
|
||||||
|
'size': size,
|
||||||
|
'type': order_type,
|
||||||
|
'status': result.status.value,
|
||||||
|
'timestamp': dt.datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.warning(f"Order failed: {result.message}")
|
||||||
|
self.notify_user(f"Order failed: {result.message}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update_prices(self, price_data: dict):
|
||||||
|
"""
|
||||||
|
Update current prices in the paper broker.
|
||||||
|
|
||||||
|
:param price_data: Dict mapping symbols to prices.
|
||||||
|
"""
|
||||||
|
for symbol, price in price_data.items():
|
||||||
|
self.paper_broker.update_price(symbol, price)
|
||||||
|
|
||||||
|
# Process any pending orders
|
||||||
|
events = self.paper_broker.update()
|
||||||
|
for event in events:
|
||||||
|
if event['type'] == 'fill':
|
||||||
|
self.trade_history.append(event)
|
||||||
|
logger.info(f"Order filled: {event}")
|
||||||
|
|
||||||
|
# Update balance attributes
|
||||||
|
self._update_balances()
|
||||||
|
|
||||||
|
def _update_balances(self):
|
||||||
|
"""Update balance attributes from paper broker."""
|
||||||
|
self.current_balance = self.paper_broker.get_balance()
|
||||||
|
self.available_balance = self.paper_broker.get_available_balance()
|
||||||
|
|
||||||
|
self.exec_context['current_balance'] = self.current_balance
|
||||||
|
self.exec_context['available_balance'] = self.available_balance
|
||||||
|
|
||||||
|
def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance',
|
||||||
|
symbol: str = 'BTC/USDT') -> float:
|
||||||
|
"""Get current price from paper broker."""
|
||||||
|
return self.paper_broker.get_current_price(symbol)
|
||||||
|
|
||||||
|
def get_available_balance(self) -> float:
|
||||||
|
"""Get available cash balance."""
|
||||||
|
self.available_balance = self.paper_broker.get_available_balance()
|
||||||
|
self.exec_context['available_balance'] = self.available_balance
|
||||||
|
return self.available_balance
|
||||||
|
|
||||||
|
def get_current_balance(self) -> float:
|
||||||
|
"""Get current total balance including unrealized P&L."""
|
||||||
|
self.current_balance = self.paper_broker.get_balance()
|
||||||
|
self.exec_context['current_balance'] = self.current_balance
|
||||||
|
return self.current_balance
|
||||||
|
|
||||||
|
def get_starting_balance(self) -> float:
|
||||||
|
"""Get starting balance."""
|
||||||
|
return self.starting_balance
|
||||||
|
|
||||||
|
def get_active_trades(self) -> int:
|
||||||
|
"""Get number of active positions."""
|
||||||
|
return len(self.paper_broker.get_all_positions())
|
||||||
|
|
||||||
|
def get_filled_orders(self) -> int:
|
||||||
|
"""Get number of filled orders."""
|
||||||
|
return len([o for o in self.paper_broker._orders.values()
|
||||||
|
if o.status.value == 'filled'])
|
||||||
|
|
||||||
|
def get_position(self, symbol: str):
|
||||||
|
"""Get position for a symbol."""
|
||||||
|
return self.paper_broker.get_position(symbol)
|
||||||
|
|
||||||
|
def close_position(self, symbol: str):
|
||||||
|
"""Close a position."""
|
||||||
|
return self.paper_broker.close_position(symbol)
|
||||||
|
|
||||||
|
def close_all_positions(self):
|
||||||
|
"""Close all positions."""
|
||||||
|
return self.paper_broker.close_all_positions()
|
||||||
|
|
||||||
|
def get_trade_history(self):
|
||||||
|
"""Get all executed trades."""
|
||||||
|
return self.paper_broker.get_trade_history()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset the paper trading state."""
|
||||||
|
self.paper_broker.reset()
|
||||||
|
self._update_balances()
|
||||||
|
self.trade_history = []
|
||||||
|
self.orders = []
|
||||||
|
logger.info("PaperStrategyInstance reset")
|
||||||
|
|
||||||
|
def save_context(self):
|
||||||
|
"""Save strategy context including paper trading state."""
|
||||||
|
self._update_balances()
|
||||||
|
super().save_context()
|
||||||
|
|
||||||
|
def notify_user(self, message: str):
|
||||||
|
"""Send notification to user."""
|
||||||
|
logger.info(f"[Paper] {message}")
|
||||||
|
# Could emit via SocketIO if available
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
"""
|
||||||
|
Tests for paper trading functionality.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from paper_strategy_instance import PaperStrategyInstance
|
||||||
|
from brokers import OrderSide, OrderType, OrderStatus
|
||||||
|
|
||||||
|
|
||||||
|
class TestPaperStrategyInstance:
|
||||||
|
"""Tests for PaperStrategyInstance."""
|
||||||
|
|
||||||
|
def test_create_instance(self):
|
||||||
|
"""Test creating a paper strategy instance."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-instance-1',
|
||||||
|
strategy_id='test-strategy-1',
|
||||||
|
strategy_name='Test Strategy',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
initial_balance=10000.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert instance.strategy_instance_id == 'test-instance-1'
|
||||||
|
assert instance.starting_balance == 10000.0
|
||||||
|
assert instance.get_current_balance() == 10000.0
|
||||||
|
assert instance.get_available_balance() == 10000.0
|
||||||
|
|
||||||
|
def test_update_prices(self):
|
||||||
|
"""Test updating prices in paper broker."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0, 'ETH/USDT': 3000.0})
|
||||||
|
|
||||||
|
assert instance.get_current_price(symbol='BTC/USDT') == 50000.0
|
||||||
|
assert instance.get_current_price(symbol='ETH/USDT') == 3000.0
|
||||||
|
|
||||||
|
def test_trade_order_market_buy(self):
|
||||||
|
"""Test placing a market buy order."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
initial_balance=10000.0,
|
||||||
|
commission=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set price
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0})
|
||||||
|
|
||||||
|
# Place order
|
||||||
|
result = instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.success
|
||||||
|
assert result.status == OrderStatus.FILLED
|
||||||
|
|
||||||
|
# Check position exists
|
||||||
|
pos = instance.get_position('BTC/USDT')
|
||||||
|
assert pos is not None
|
||||||
|
assert pos.size == 0.1
|
||||||
|
|
||||||
|
# Check balance reduced
|
||||||
|
assert instance.get_available_balance() < 10000.0
|
||||||
|
|
||||||
|
def test_trade_order_sell(self):
|
||||||
|
"""Test selling a position."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
initial_balance=10000.0,
|
||||||
|
commission=0,
|
||||||
|
slippage=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Buy first
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0})
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now sell at higher price
|
||||||
|
instance.update_prices({'BTC/USDT': 51000.0})
|
||||||
|
result = instance.trade_order(
|
||||||
|
trade_type='sell',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.success
|
||||||
|
|
||||||
|
# Position should be closed
|
||||||
|
pos = instance.get_position('BTC/USDT')
|
||||||
|
assert pos is None
|
||||||
|
|
||||||
|
def test_close_position(self):
|
||||||
|
"""Test closing a position via close_position method."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0})
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = instance.close_position('BTC/USDT')
|
||||||
|
assert result.success
|
||||||
|
|
||||||
|
def test_close_all_positions(self):
|
||||||
|
"""Test closing all positions."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0, 'ETH/USDT': 3000.0})
|
||||||
|
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=1.0,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'ETH/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert instance.get_active_trades() == 2
|
||||||
|
|
||||||
|
results = instance.close_all_positions()
|
||||||
|
assert len(results) == 2
|
||||||
|
assert all(r.success for r in results)
|
||||||
|
assert instance.get_active_trades() == 0
|
||||||
|
|
||||||
|
def test_reset(self):
|
||||||
|
"""Test resetting paper trading state."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
initial_balance=10000.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0})
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert instance.get_available_balance() < 10000.0
|
||||||
|
|
||||||
|
instance.reset()
|
||||||
|
|
||||||
|
assert instance.get_available_balance() == 10000.0
|
||||||
|
assert instance.get_active_trades() == 0
|
||||||
|
|
||||||
|
def test_get_trade_history(self):
|
||||||
|
"""Test getting trade history."""
|
||||||
|
instance = PaperStrategyInstance(
|
||||||
|
strategy_instance_id='test-1',
|
||||||
|
strategy_id='strat-1',
|
||||||
|
strategy_name='Test',
|
||||||
|
user_id=1,
|
||||||
|
generated_code='def next(self): pass',
|
||||||
|
data_cache=None,
|
||||||
|
indicators=None,
|
||||||
|
trades=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.update_prices({'BTC/USDT': 50000.0})
|
||||||
|
instance.trade_order(
|
||||||
|
trade_type='buy',
|
||||||
|
size=0.1,
|
||||||
|
order_type='MARKET',
|
||||||
|
source={'symbol': 'BTC/USDT'}
|
||||||
|
)
|
||||||
|
|
||||||
|
history = instance.get_trade_history()
|
||||||
|
assert len(history) == 1
|
||||||
|
assert history[0]['symbol'] == 'BTC/USDT'
|
||||||
|
assert history[0]['side'] == 'buy'
|
||||||
|
|
||||||
|
|
||||||
|
class TestStrategiesModeSeletion:
|
||||||
|
"""Tests for strategy mode selection."""
|
||||||
|
|
||||||
|
def test_mode_selection_imports(self):
|
||||||
|
"""Test that mode selection imports work."""
|
||||||
|
from Strategies import Strategies
|
||||||
|
from brokers import TradingMode
|
||||||
|
|
||||||
|
assert TradingMode.PAPER == 'paper'
|
||||||
|
assert TradingMode.BACKTEST == 'backtest'
|
||||||
|
assert TradingMode.LIVE == 'live'
|
||||||
Loading…
Reference in New Issue