brighter-trading/tests/test_strategy_execution.py

567 lines
22 KiB
Python

"""
Tests for strategy execution (run/stop/status) flow.
These tests cover the new run_strategy, stop_strategy, and get_strategy_status
message handlers and their underlying implementation.
"""
import pytest
import json
from unittest.mock import MagicMock, patch
class TestStartStrategyValidation:
"""Tests for start_strategy input validation and authorization."""
@pytest.fixture
def mock_brighter_trades(self):
"""Create a mock BrighterTrades instance with required dependencies."""
with patch.dict('sys.modules', {'eventlet': MagicMock()}):
from BrighterTrades import BrighterTrades
# Create mock dependencies
mock_socketio = MagicMock()
with patch.object(BrighterTrades, '__init__', lambda x, y: None):
bt = BrighterTrades(mock_socketio)
# Set up required attributes
bt.strategies = MagicMock()
bt.strategies.active_instances = {}
bt.strategies.data_cache = MagicMock()
# Mock users dependency
bt.users = MagicMock()
bt.users.get_username = MagicMock(return_value='test_user')
# Mock get_user_info
bt.get_user_info = MagicMock(return_value='test_user')
# Mock exchanges
bt.exchanges = MagicMock()
bt.exchanges.get_price = MagicMock(return_value=50000.0)
bt.users.get_exchanges = MagicMock(return_value=['binance'])
bt.users.get_api_keys = MagicMock(return_value={'key': 'k', 'secret': 's'})
mock_exchange = MagicMock()
mock_exchange.testnet = True
mock_exchange.configured = True
bt.exchanges.get_exchange = MagicMock(return_value=mock_exchange)
bt.exchanges.connect_exchange = MagicMock(return_value=True)
# Mock EDM client for exchange validation
bt.edm_client = MagicMock()
bt.edm_client.get_exchanges_sync = MagicMock(return_value=['binance', 'kucoin'])
# Mock strategies.get_strategy_by_tbl_key for exchange validation
bt.strategies.get_strategy_by_tbl_key = MagicMock(return_value={
'strategy_components': {},
'default_source': {}
})
return bt
def test_start_strategy_invalid_mode(self, mock_brighter_trades):
"""Test that invalid mode returns error."""
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='invalid_mode'
)
assert result['success'] is False
assert 'Invalid mode' in result['message']
def test_start_strategy_strategy_not_found(self, mock_brighter_trades):
"""Test that missing strategy returns error."""
# Mock empty result from database
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = MagicMock(empty=True)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='nonexistent',
mode='paper'
)
assert result['success'] is False
assert 'not found' in result['message']
def test_start_strategy_authorization_check(self, mock_brighter_trades):
"""Test that non-owner, non-subscriber cannot run private strategy."""
import pandas as pd
# Mock strategy owned by different user (creator is user_id 2, not 1)
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 2, # Different user_id
'public': False,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
# Mock that user is NOT subscribed
mock_brighter_trades.strategies.is_subscribed = MagicMock(return_value=False)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'subscribe' in result['message'].lower()
def test_start_strategy_authorization_non_subscriber_denied(self, mock_brighter_trades):
"""
Test that non-owner, non-subscriber cannot run strategy (even private).
"""
import pandas as pd
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 2, # Different user_id
'public': False,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
# Mock that user is NOT subscribed
mock_brighter_trades.strategies.is_subscribed = MagicMock(return_value=False)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'subscribe' in result['message'].lower()
def test_start_strategy_live_mode_uses_live_active_instance_key(self, mock_brighter_trades):
"""Live mode now runs in actual live mode with proper instance keying."""
import pandas as pd
# Strategy owned by the running user (no subscription needed)
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 'test_user', # Same as mock user (user_id=1)
'public': True,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
mock_brighter_trades.strategies.create_strategy_instance = MagicMock()
mock_brighter_trades.strategies.create_strategy_instance.return_value = MagicMock(
strategy_name='Test Strategy'
)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='live'
)
assert result['success'] is True
assert result['actual_mode'] == 'live'
assert (1, 'test-strategy', 'live') in mock_brighter_trades.strategies.active_instances
def test_start_strategy_subscribed_strategy_allowed(self, mock_brighter_trades):
"""Test that a subscribed user can run a public strategy."""
import pandas as pd
# Mock public strategy owned by different user
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Public Strategy',
'creator': 'other_user',
'public': True,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
mock_brighter_trades.strategies.create_strategy_instance = MagicMock()
mock_brighter_trades.strategies.create_strategy_instance.return_value = MagicMock(
strategy_name='Public Strategy'
)
# Mock the subscription check to return True
mock_brighter_trades.strategies.is_subscribed = MagicMock(return_value=True)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is True
def test_start_strategy_unsubscribed_public_strategy_denied(self, mock_brighter_trades):
"""Test that unsubscribed user cannot run a public strategy they don't own."""
import pandas as pd
# Mock public strategy owned by different user
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Public Strategy',
'creator': 'other_user',
'public': True,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
# Mock the subscription check to return False
mock_brighter_trades.strategies.is_subscribed = MagicMock(return_value=False)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'subscribe' in result['message'].lower()
def test_start_strategy_already_running(self, mock_brighter_trades):
"""Test that strategy cannot be started twice in same mode."""
import pandas as pd
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 'test_user',
'public': False,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
# Mark as already running
mock_brighter_trades.strategies.active_instances[(1, 'test-strategy', 'paper')] = MagicMock()
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'already running' in result['message']
def test_start_strategy_no_generated_code(self, mock_brighter_trades):
"""Test that strategy without code returns error."""
import pandas as pd
# Strategy with empty code
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Empty Strategy',
'creator': 'test_user',
'public': False,
'strategy_components': json.dumps({'generated_code': ''})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'no generated code' in result['message']
def test_start_strategy_wrong_code_key(self, mock_brighter_trades):
"""Test that old 'code' key doesn't work (must be 'generated_code')."""
import pandas as pd
# Strategy with old key name
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Old Format Strategy',
'creator': 'test_user',
'public': False,
'strategy_components': json.dumps({'code': 'pass'}) # Wrong key!
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'no generated code' in result['message']
def test_start_strategy_testnet_override_bypasses_prod_gate(self, mock_brighter_trades, monkeypatch):
"""If config forces testnet, a non-testnet request should not be blocked as production."""
import pandas as pd
import config
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 'test_user',
'public': False,
'strategy_components': json.dumps({'generated_code': 'pass'})
}])
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
mock_brighter_trades.strategies.create_strategy_instance = MagicMock()
mock_brighter_trades.strategies.create_strategy_instance.return_value = MagicMock(
strategy_name='Test Strategy'
)
monkeypatch.setattr(config, 'TESTNET_MODE', True, raising=False)
monkeypatch.setattr(config, 'ALLOW_LIVE_PRODUCTION', False, raising=False)
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='live',
testnet=False
)
assert result['success'] is True
assert result['testnet'] is True
class TestStopStrategy:
"""Tests for stop_strategy functionality."""
@pytest.fixture
def mock_brighter_trades(self):
"""Create a mock BrighterTrades instance."""
with patch.dict('sys.modules', {'eventlet': MagicMock()}):
from BrighterTrades import BrighterTrades
with patch.object(BrighterTrades, '__init__', lambda x, y: None):
bt = BrighterTrades(MagicMock())
bt.strategies = MagicMock()
bt.strategies.active_instances = {}
return bt
def test_stop_strategy_not_running(self, mock_brighter_trades):
"""Test stopping a strategy that isn't running."""
result = mock_brighter_trades.stop_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is False
assert 'not running' in result['message'].lower() or 'No running strategy' in result['message']
def test_stop_strategy_success(self, mock_brighter_trades):
"""Test successfully stopping a running strategy."""
# Set up running strategy
mock_instance = MagicMock()
mock_instance.strategy_name = 'Test Strategy'
mock_instance.broker = MagicMock()
mock_instance.broker.get_balance.return_value = 10500.0
mock_instance.broker.get_available_balance.return_value = 10500.0
mock_instance.trade_history = [{'pnl': 100}, {'pnl': 400}]
mock_brighter_trades.strategies.active_instances[(1, 'test-strategy', 'paper')] = mock_instance
result = mock_brighter_trades.stop_strategy(
user_id=1,
strategy_id='test-strategy',
mode='paper'
)
assert result['success'] is True
assert 'stopped' in result['message']
assert result['final_stats']['final_balance'] == 10500.0
assert result['final_stats']['total_trades'] == 2
# Verify removed from active instances
assert (1, 'test-strategy', 'paper') not in mock_brighter_trades.strategies.active_instances
def test_stop_strategy_live_mode_falls_back_to_paper_instance(self, mock_brighter_trades):
"""Stopping live should find fallback paper instance."""
mock_instance = MagicMock()
mock_instance.strategy_name = 'Test Strategy'
mock_instance.broker = MagicMock()
mock_instance.broker.get_balance.return_value = 10000.0
mock_instance.broker.get_available_balance.return_value = 10000.0
mock_instance.trade_history = []
mock_brighter_trades.strategies.active_instances[(1, 'test-strategy', 'paper')] = mock_instance
result = mock_brighter_trades.stop_strategy(
user_id=1,
strategy_id='test-strategy',
mode='live'
)
assert result['success'] is True
assert (1, 'test-strategy', 'paper') not in mock_brighter_trades.strategies.active_instances
class TestGetStrategyStatus:
"""Tests for get_strategy_status functionality."""
@pytest.fixture
def mock_brighter_trades(self):
"""Create a mock BrighterTrades instance."""
with patch.dict('sys.modules', {'eventlet': MagicMock()}):
from BrighterTrades import BrighterTrades
with patch.object(BrighterTrades, '__init__', lambda x, y: None):
bt = BrighterTrades(MagicMock())
bt.strategies = MagicMock()
bt.strategies.active_instances = {}
return bt
def test_get_status_no_running(self, mock_brighter_trades):
"""Test status when no strategies running."""
result = mock_brighter_trades.get_strategy_status(user_id=1)
assert result['success'] is True
assert result['running_strategies'] == []
assert result['count'] == 0
def test_get_status_with_running(self, mock_brighter_trades):
"""Test status with running strategies."""
# Set up running strategies
mock_instance1 = MagicMock()
mock_instance1.strategy_name = 'Strategy 1'
mock_instance1.strategy_instance_id = 'inst-1'
mock_instance1.broker = MagicMock()
mock_instance1.broker.get_balance.return_value = 10500.0
mock_instance1.broker.get_available_balance.return_value = 10000.0
mock_instance1.broker.get_all_positions.return_value = []
mock_instance1.trade_history = []
mock_instance2 = MagicMock()
mock_instance2.strategy_name = 'Strategy 2'
mock_instance2.strategy_instance_id = 'inst-2'
mock_instance2.broker = MagicMock()
mock_instance2.broker.get_balance.return_value = 9500.0
mock_instance2.broker.get_available_balance.return_value = 9500.0
mock_instance2.broker.get_all_positions.return_value = []
mock_instance2.trade_history = [{'pnl': -500}]
mock_brighter_trades.strategies.active_instances = {
(1, 'strat-1', 'paper'): mock_instance1,
(1, 'strat-2', 'paper'): mock_instance2,
(2, 'strat-3', 'paper'): MagicMock(), # Different user
}
result = mock_brighter_trades.get_strategy_status(user_id=1)
assert result['success'] is True
assert result['count'] == 2 # Only user 1's strategies
strat_ids = [s['strategy_id'] for s in result['running_strategies']]
assert 'strat-1' in strat_ids
assert 'strat-2' in strat_ids
assert 'strat-3' not in strat_ids # Different user
def test_get_status_filter_by_strategy(self, mock_brighter_trades):
"""Test status filtered by specific strategy."""
mock_instance = MagicMock()
mock_instance.strategy_name = 'Strategy 1'
mock_instance.strategy_instance_id = 'inst-1'
mock_instance.broker = MagicMock()
mock_instance.broker.get_balance.return_value = 10500.0
mock_instance.broker.get_available_balance.return_value = 10000.0
mock_instance.broker.get_all_positions.return_value = []
mock_instance.trade_history = []
mock_brighter_trades.strategies.active_instances = {
(1, 'strat-1', 'paper'): mock_instance,
(1, 'strat-2', 'paper'): MagicMock(),
}
result = mock_brighter_trades.get_strategy_status(
user_id=1,
strategy_id='strat-1'
)
assert result['count'] == 1
assert result['running_strategies'][0]['strategy_id'] == 'strat-1'
class TestMessageHandlerNumericParsing:
"""Tests for safe numeric parsing in message handlers."""
def test_run_strategy_invalid_initial_balance(self):
"""Test that invalid initial_balance is handled gracefully."""
# This would need integration test with actual message handler
# For now, verify the structure is correct
pass
def test_run_strategy_negative_balance_rejected(self):
"""Test that negative balance is rejected."""
pass
def test_run_strategy_invalid_commission_rejected(self):
"""Test that invalid commission is rejected."""
pass
class TestLiveModeWarning:
"""Tests for live mode fallback messaging."""
@pytest.fixture
def mock_brighter_trades(self):
"""Create a mock BrighterTrades instance."""
with patch.dict('sys.modules', {'eventlet': MagicMock()}):
from BrighterTrades import BrighterTrades
import pandas as pd
with patch.object(BrighterTrades, '__init__', lambda x, y: None):
bt = BrighterTrades(MagicMock())
bt.strategies = MagicMock()
bt.strategies.active_instances = {}
bt.strategies.data_cache = MagicMock()
bt.users = MagicMock()
bt.users.get_username = MagicMock(return_value='test_user')
bt.get_user_info = MagicMock(
side_effect=lambda user_name, info: 1 if info == 'User_id' else 'test_user'
)
bt.exchanges = MagicMock()
bt.exchanges.get_price = MagicMock(return_value=50000.0)
bt.users.get_exchanges = MagicMock(return_value=['binance'])
bt.users.get_api_keys = MagicMock(return_value={'key': 'k', 'secret': 's'})
mock_exchange = MagicMock()
mock_exchange.testnet = True
mock_exchange.configured = True
bt.exchanges.get_exchange = MagicMock(return_value=mock_exchange)
bt.exchanges.connect_exchange = MagicMock(return_value=True)
# Mock EDM client for exchange validation
bt.edm_client = MagicMock()
bt.edm_client.get_exchanges_sync = MagicMock(return_value=['binance', 'kucoin'])
# Set up valid strategy
mock_strategy = pd.DataFrame([{
'tbl_key': 'test-strategy',
'name': 'Test Strategy',
'creator': 'test_user',
'public': False,
'strategy_components': '{"generated_code": "pass"}'
}])
bt.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
bt.strategies.create_strategy_instance = MagicMock()
bt.strategies.create_strategy_instance.return_value = MagicMock(
strategy_name='Test Strategy'
)
# Mock strategies.get_strategy_by_tbl_key for exchange validation
bt.strategies.get_strategy_by_tbl_key = MagicMock(return_value={
'strategy_components': {},
'default_source': {}
})
return bt
def test_live_mode_returns_success(self, mock_brighter_trades):
"""Test that live mode request succeeds in live mode."""
result = mock_brighter_trades.start_strategy(
user_id=1,
strategy_id='test-strategy',
mode='live'
)
# Should succeed in live mode
assert result['success'] is True
assert result['actual_mode'] == 'live'