642 lines
26 KiB
Python
642 lines
26 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 = {}
|
|
# Mock wallet_manager for fee settlement
|
|
bt.wallet_manager = MagicMock()
|
|
bt.wallet_manager.settle_accumulated_fees.return_value = {'success': True, 'settled': 0}
|
|
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'
|
|
|
|
|
|
class TestConvertAsset:
|
|
"""Tests for the convert_asset method in StrategyInstance."""
|
|
|
|
@pytest.fixture
|
|
def mock_strategy_instance(self):
|
|
"""Create a mock strategy instance for testing convert_asset."""
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
from StrategyInstance import StrategyInstance
|
|
|
|
with patch.object(StrategyInstance, '__init__', lambda self, *args, **kwargs: None):
|
|
instance = StrategyInstance.__new__(StrategyInstance)
|
|
|
|
# Set up minimal required attributes
|
|
instance.strategy_instance_id = 'test-instance'
|
|
instance.strategy_id = 'test-strategy'
|
|
instance.user_id = 1
|
|
|
|
return instance
|
|
|
|
def test_convert_same_asset_returns_amount(self, mock_strategy_instance):
|
|
"""Converting to the same asset should return the original amount."""
|
|
result = mock_strategy_instance.convert_asset(100, 'USD', 'USD')
|
|
assert result == 100
|
|
|
|
def test_convert_stablecoins_one_to_one(self, mock_strategy_instance):
|
|
"""Converting between stablecoins should be 1:1."""
|
|
result = mock_strategy_instance.convert_asset(100, 'USD', 'USDT')
|
|
assert result == 100
|
|
|
|
result = mock_strategy_instance.convert_asset(50, 'USDC', 'USDT')
|
|
assert result == 50
|
|
|
|
def test_convert_usd_to_btc(self, mock_strategy_instance):
|
|
"""Test converting USD to BTC with mocked price."""
|
|
# Mock get_current_price to return a BTC price of $50,000
|
|
mock_strategy_instance.get_current_price = MagicMock(return_value=50000.0)
|
|
|
|
result = mock_strategy_instance.convert_asset(10, 'USD', 'BTC')
|
|
|
|
# $10 / $50,000 = 0.0002 BTC
|
|
assert result == pytest.approx(0.0002, rel=1e-6)
|
|
|
|
def test_convert_btc_to_usd(self, mock_strategy_instance):
|
|
"""Test converting BTC to USD with mocked price."""
|
|
# Mock get_current_price to return a BTC price of $50,000
|
|
mock_strategy_instance.get_current_price = MagicMock(return_value=50000.0)
|
|
|
|
result = mock_strategy_instance.convert_asset(0.001, 'BTC', 'USD')
|
|
|
|
# 0.001 BTC * $50,000 = $50
|
|
assert result == pytest.approx(50.0, rel=1e-6)
|
|
|
|
def test_convert_returns_zero_on_error(self, mock_strategy_instance):
|
|
"""Test that conversion returns 0.0 when price is unavailable."""
|
|
# Mock get_current_price to return None (price not available)
|
|
mock_strategy_instance.get_current_price = MagicMock(return_value=None)
|
|
|
|
result = mock_strategy_instance.convert_asset(100, 'USD', 'UNKNOWN')
|
|
|
|
assert result == 0.0
|
|
|
|
def test_convert_normalizes_asset_names(self, mock_strategy_instance):
|
|
"""Test that asset names are normalized (uppercase, trimmed)."""
|
|
mock_strategy_instance.get_current_price = MagicMock(return_value=50000.0)
|
|
|
|
result = mock_strategy_instance.convert_asset(10, ' usd ', ' btc ')
|
|
|
|
assert result == pytest.approx(0.0002, rel=1e-6)
|