481 lines
18 KiB
Python
481 lines
18 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)
|
|
|
|
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 cannot run private strategy."""
|
|
import pandas as pd
|
|
|
|
# Mock strategy owned by different user
|
|
mock_strategy = pd.DataFrame([{
|
|
'tbl_key': 'test-strategy',
|
|
'name': 'Test Strategy',
|
|
'creator': 'other_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 that requesting user is different
|
|
mock_brighter_trades.get_user_info = MagicMock(side_effect=lambda **kwargs: {
|
|
'user_name': 'test_user',
|
|
'User_id': 2 # Different user
|
|
}.get(kwargs.get('info')))
|
|
|
|
result = mock_brighter_trades.start_strategy(
|
|
user_id=1,
|
|
strategy_id='test-strategy',
|
|
mode='paper'
|
|
)
|
|
|
|
assert result['success'] is False
|
|
assert 'permission' in result['message'].lower()
|
|
|
|
def test_start_strategy_authorization_does_not_call_get_user_info_with_user_id_kwarg(self, mock_brighter_trades):
|
|
"""
|
|
Regression test: get_user_info should be called with (user_name, info) only.
|
|
"""
|
|
import pandas as pd
|
|
|
|
mock_strategy = pd.DataFrame([{
|
|
'tbl_key': 'test-strategy',
|
|
'name': 'Test Strategy',
|
|
'creator': 'other_user',
|
|
'public': False,
|
|
'strategy_components': json.dumps({'generated_code': 'pass'})
|
|
}])
|
|
mock_brighter_trades.strategies.data_cache.get_rows_from_datacache.return_value = mock_strategy
|
|
|
|
def strict_get_user_info(user_name, info):
|
|
if info == 'User_id' and user_name == 'other_user':
|
|
return 2
|
|
return None
|
|
|
|
mock_brighter_trades.get_user_info = MagicMock(side_effect=strict_get_user_info)
|
|
|
|
result = mock_brighter_trades.start_strategy(
|
|
user_id=1,
|
|
strategy_id='test-strategy',
|
|
mode='paper'
|
|
)
|
|
|
|
assert result['success'] is False
|
|
assert 'permission' in result['message'].lower()
|
|
|
|
def test_start_strategy_live_mode_uses_paper_active_instance_key(self, mock_brighter_trades):
|
|
"""Live mode currently falls back to paper execution keying."""
|
|
import pandas as pd
|
|
|
|
mock_strategy = pd.DataFrame([{
|
|
'tbl_key': 'test-strategy',
|
|
'name': 'Test 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='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'] == 'paper'
|
|
assert (1, 'test-strategy', 'paper') in mock_brighter_trades.strategies.active_instances
|
|
|
|
def test_start_strategy_public_strategy_allowed(self, mock_brighter_trades):
|
|
"""Test that anyone 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'
|
|
)
|
|
|
|
result = mock_brighter_trades.start_strategy(
|
|
user_id=1,
|
|
strategy_id='test-strategy',
|
|
mode='paper'
|
|
)
|
|
|
|
assert result['success'] is True
|
|
|
|
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']
|
|
|
|
|
|
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)
|
|
|
|
# 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'
|
|
)
|
|
|
|
return bt
|
|
|
|
def test_live_mode_returns_success(self, mock_brighter_trades):
|
|
"""Test that live mode request still succeeds (falls back to paper)."""
|
|
result = mock_brighter_trades.start_strategy(
|
|
user_id=1,
|
|
strategy_id='test-strategy',
|
|
mode='live'
|
|
)
|
|
|
|
# Should succeed but with warning
|
|
assert result['success'] is True
|
|
assert result['actual_mode'] == 'paper'
|