mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-23 00:38:07 +00:00
Update tests and docs across core, lib, and contrib modules
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
from aiomql.core.sync.meta_trader import MetaTrader
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def sync_mt():
|
||||
"""Provides a synchronous MetaTrader instance for the test class."""
|
||||
mt = MetaTrader()
|
||||
mt.initialize()
|
||||
mt.login()
|
||||
yield mt
|
||||
mt.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def buy_order_sync(btc_usd):
|
||||
"""Creates a buy order request for BTCUSD."""
|
||||
mt = MetaTrader()
|
||||
sym_info = mt.symbol_info(btc_usd.name)
|
||||
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
|
||||
sl = sym_info.ask - dsl
|
||||
tp = sym_info.ask + dsl
|
||||
return {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": btc_usd.name,
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_BUY,
|
||||
"price": sym_info.ask,
|
||||
"sl": sl,
|
||||
"tp": tp,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sell_order_sync(eth_usd):
|
||||
"""Creates a sell order request for ETHUSD."""
|
||||
mt = MetaTrader()
|
||||
sym_info = mt.symbol_info(eth_usd.name)
|
||||
return {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": eth_usd.name,
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_SELL,
|
||||
"price": sym_info.bid,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def make_buy_sell_orders_sync():
|
||||
"""Creates buy and sell orders for BTCUSD to ensure open positions exist."""
|
||||
mt = MetaTrader()
|
||||
sym_info = mt.symbol_info("BTCUSD")
|
||||
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
|
||||
sl = sym_info.ask - dsl
|
||||
tp = sym_info.ask + dsl
|
||||
req = {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": "BTCUSD",
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_BUY,
|
||||
"price": sym_info.ask,
|
||||
"sl": sl,
|
||||
"tp": tp,
|
||||
}
|
||||
mt.order_send(req)
|
||||
req["type"] = mt.ORDER_TYPE_SELL
|
||||
req["price"] = sym_info.bid
|
||||
req["sl"] = sym_info.bid + dsl
|
||||
req["tp"] = sym_info.bid - dsl
|
||||
mt.order_send(req)
|
||||
@@ -0,0 +1,668 @@
|
||||
"""Comprehensive tests for the synchronous history module.
|
||||
|
||||
Tests cover:
|
||||
- History class initialization with various parameter combinations
|
||||
- Class variable sharing (BaseMeta metaclass behavior)
|
||||
- Synchronous initialization and data fetching
|
||||
- Deal retrieval and filtering methods
|
||||
- Order retrieval and filtering methods
|
||||
- Edge cases and error handling
|
||||
- UTC timezone handling
|
||||
"""
|
||||
from datetime import datetime, UTC, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.sync.history import History
|
||||
from aiomql.core.models import TradeDeal, TradeOrder
|
||||
from aiomql.core.sync.meta_trader import MetaTrader
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def make_buy_sell_orders_sync():
|
||||
"""Create buy and sell orders synchronously for testing history."""
|
||||
mt = MetaTrader()
|
||||
sym_info = mt.symbol_info("BTCUSD")
|
||||
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
|
||||
sl = sym_info.ask - dsl
|
||||
tp = sym_info.ask + dsl
|
||||
req = {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": "BTCUSD",
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_BUY,
|
||||
"price": sym_info.ask,
|
||||
"sl": sl,
|
||||
"tp": tp,
|
||||
}
|
||||
mt.order_send(req)
|
||||
req["type"] = mt.ORDER_TYPE_SELL
|
||||
req["price"] = sym_info.bid
|
||||
req["sl"] = sym_info.bid + dsl
|
||||
req["tp"] = sym_info.bid - dsl
|
||||
mt.order_send(req)
|
||||
|
||||
|
||||
class TestHistoryInitialization:
|
||||
"""Test History class initialization and configuration."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures for initialization tests."""
|
||||
cls.now = datetime.now()
|
||||
cls.start = cls.now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = cls.now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
|
||||
def test_init_with_datetime_objects(self):
|
||||
"""Test initialization with datetime objects."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
assert history.date_from == self.start
|
||||
assert history.date_to == self.end
|
||||
|
||||
def test_init_with_timestamps(self):
|
||||
"""Test initialization with Unix timestamp floats."""
|
||||
start_ts = self.start.timestamp()
|
||||
end_ts = self.end.timestamp()
|
||||
history = History(date_from=start_ts, date_to=end_ts)
|
||||
assert history.date_from.timestamp() == start_ts
|
||||
assert history.date_to.timestamp() == end_ts
|
||||
|
||||
def test_init_with_mixed_types(self):
|
||||
"""Test initialization with mixed datetime and timestamp."""
|
||||
start_ts = self.start.timestamp()
|
||||
history = History(date_from=start_ts, date_to=self.end)
|
||||
assert history.date_from.timestamp() == start_ts
|
||||
assert history.date_to == self.end
|
||||
|
||||
def test_init_with_group_filter(self):
|
||||
"""Test initialization with symbol group filter."""
|
||||
history = History(date_from=self.start, date_to=self.end, group="*USD*")
|
||||
assert history.group == "*USD*"
|
||||
|
||||
def test_init_with_empty_group(self):
|
||||
"""Test initialization with empty group (default)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
assert history.group == ""
|
||||
|
||||
def test_init_with_use_utc_true(self):
|
||||
"""Test initialization with use_utc=True converts to UTC."""
|
||||
local_time = datetime.now()
|
||||
history = History(date_from=local_time, date_to=local_time, use_utc=True)
|
||||
assert history.date_from.tzinfo == UTC
|
||||
assert history.date_to.tzinfo == UTC
|
||||
|
||||
def test_init_with_use_utc_false(self):
|
||||
"""Test initialization with use_utc=False keeps original timezone."""
|
||||
local_time = datetime.now()
|
||||
history = History(date_from=local_time, date_to=local_time, use_utc=False)
|
||||
# When use_utc is False, timezone is not modified
|
||||
assert history.date_from == local_time
|
||||
assert history.date_to == local_time
|
||||
|
||||
def test_init_default_attributes(self):
|
||||
"""Test default attribute values after initialization."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
assert history.deals == ()
|
||||
assert history.orders == ()
|
||||
assert history.total_deals == 0
|
||||
assert history.total_orders == 0
|
||||
|
||||
def test_init_class_variables_set(self):
|
||||
"""Test that class variables mt5 and config are set."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
assert hasattr(History, 'mt5')
|
||||
assert hasattr(History, 'config')
|
||||
assert hasattr(history, 'mt5')
|
||||
assert hasattr(history, 'config')
|
||||
|
||||
def test_multiple_instances_share_mt5(self):
|
||||
"""Test that multiple History instances share the same mt5 object."""
|
||||
history1 = History(date_from=self.start, date_to=self.end)
|
||||
history2 = History(date_from=self.start, date_to=self.end)
|
||||
assert history1.mt5 is history2.mt5
|
||||
|
||||
def test_multiple_instances_share_config(self):
|
||||
"""Test that multiple History instances share the same config object."""
|
||||
history1 = History(date_from=self.start, date_to=self.end)
|
||||
history2 = History(date_from=self.start, date_to=self.end)
|
||||
assert history1.config is history2.config
|
||||
|
||||
|
||||
class TestHistoryLive:
|
||||
"""Live tests for synchronous History class with actual MT5 connection."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize history with live trades."""
|
||||
self.history.initialize()
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures with today's date range."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
cls.history = History(date_from=cls.start, date_to=cls.end)
|
||||
|
||||
def test_initialize_populates_deals(self):
|
||||
"""Test that initialize() populates deals attribute."""
|
||||
assert self.history.deals is not None
|
||||
assert isinstance(self.history.deals, tuple)
|
||||
|
||||
def test_initialize_populates_orders(self):
|
||||
"""Test that initialize() populates orders attribute."""
|
||||
assert self.history.orders is not None
|
||||
assert isinstance(self.history.orders, tuple)
|
||||
|
||||
def test_initialize_sets_total_deals(self):
|
||||
"""Test that initialize() sets correct total_deals count."""
|
||||
assert self.history.total_deals >= 0
|
||||
assert self.history.total_deals == len(self.history.deals)
|
||||
|
||||
def test_initialize_sets_total_orders(self):
|
||||
"""Test that initialize() sets correct total_orders count."""
|
||||
assert self.history.total_orders >= 0
|
||||
assert self.history.total_orders == len(self.history.orders)
|
||||
|
||||
def test_get_deals_returns_trade_deal_objects(self):
|
||||
"""Test that get_deals returns TradeDeal objects."""
|
||||
deals = self.history.get_deals()
|
||||
assert isinstance(deals, tuple)
|
||||
if deals:
|
||||
assert all(isinstance(deal, TradeDeal) for deal in deals)
|
||||
|
||||
def test_get_orders_returns_trade_order_objects(self):
|
||||
"""Test that get_orders returns TradeOrder objects."""
|
||||
orders = self.history.get_orders()
|
||||
assert isinstance(orders, tuple)
|
||||
if orders:
|
||||
assert all(isinstance(order, TradeOrder) for order in orders)
|
||||
|
||||
def test_deals_have_required_attributes(self):
|
||||
"""Test that deals have expected TradeDeal attributes."""
|
||||
if self.history.deals:
|
||||
deal = self.history.deals[0]
|
||||
assert hasattr(deal, 'ticket')
|
||||
assert hasattr(deal, 'order')
|
||||
assert hasattr(deal, 'time')
|
||||
assert hasattr(deal, 'time_msc')
|
||||
assert hasattr(deal, 'type')
|
||||
assert hasattr(deal, 'position_id')
|
||||
assert hasattr(deal, 'profit')
|
||||
assert hasattr(deal, 'symbol')
|
||||
|
||||
def test_orders_have_required_attributes(self):
|
||||
"""Test that orders have expected TradeOrder attributes."""
|
||||
if self.history.orders:
|
||||
order = self.history.orders[0]
|
||||
assert hasattr(order, 'ticket')
|
||||
assert hasattr(order, 'time_setup')
|
||||
assert hasattr(order, 'time_done')
|
||||
assert hasattr(order, 'time_done_msc')
|
||||
assert hasattr(order, 'type')
|
||||
assert hasattr(order, 'position_id')
|
||||
assert hasattr(order, 'symbol')
|
||||
|
||||
|
||||
class TestHistoryDealsFiltering:
|
||||
"""Test deal filtering methods with live data."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize history with live trades."""
|
||||
self.history.initialize()
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
cls.history = History(date_from=cls.start, date_to=cls.end)
|
||||
|
||||
def test_filter_deals_by_ticket_returns_tuple(self):
|
||||
"""Test filter_deals_by_ticket returns a tuple."""
|
||||
if self.history.deals:
|
||||
ticket = self.history.deals[0].order
|
||||
deals = self.history.filter_deals_by_ticket(ticket=ticket)
|
||||
assert isinstance(deals, tuple)
|
||||
|
||||
def test_filter_deals_by_ticket_finds_matching_deals(self):
|
||||
"""Test filter_deals_by_ticket finds deals with matching order ticket."""
|
||||
if self.history.deals:
|
||||
ticket = self.history.deals[0].order
|
||||
deals = self.history.filter_deals_by_ticket(ticket=ticket)
|
||||
if deals:
|
||||
assert all(deal.order == ticket for deal in deals)
|
||||
|
||||
def test_filter_deals_by_ticket_nonexistent_returns_empty(self):
|
||||
"""Test filter_deals_by_ticket returns empty tuple for nonexistent ticket."""
|
||||
nonexistent_ticket = 999999999999
|
||||
deals = self.history.filter_deals_by_ticket(ticket=nonexistent_ticket)
|
||||
assert deals == ()
|
||||
|
||||
def test_filter_deals_by_position_returns_tuple(self):
|
||||
"""Test filter_deals_by_position returns a tuple."""
|
||||
if self.history.deals:
|
||||
position = self.history.deals[0].position_id
|
||||
deals = self.history.filter_deals_by_position(position=position)
|
||||
assert isinstance(deals, tuple)
|
||||
|
||||
def test_filter_deals_by_position_finds_matching_deals(self):
|
||||
"""Test filter_deals_by_position finds deals with matching position_id."""
|
||||
if self.history.deals:
|
||||
position = self.history.deals[0].position_id
|
||||
deals = self.history.filter_deals_by_position(position=position)
|
||||
if deals:
|
||||
assert all(deal.position_id == position for deal in deals)
|
||||
|
||||
def test_filter_deals_by_position_nonexistent_returns_empty(self):
|
||||
"""Test filter_deals_by_position returns empty tuple for nonexistent position."""
|
||||
nonexistent_position = 999999999999
|
||||
deals = self.history.filter_deals_by_position(position=nonexistent_position)
|
||||
assert deals == ()
|
||||
|
||||
|
||||
class TestHistoryOrdersFiltering:
|
||||
"""Test order filtering methods with live data."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize history with live trades."""
|
||||
self.history.initialize()
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
cls.history = History(date_from=cls.start, date_to=cls.end)
|
||||
|
||||
def test_filter_orders_by_ticket_returns_tuple(self):
|
||||
"""Test filter_orders_by_ticket returns a tuple."""
|
||||
if self.history.orders:
|
||||
ticket = self.history.orders[0].ticket
|
||||
orders = self.history.filter_orders_by_ticket(ticket=ticket)
|
||||
assert isinstance(orders, tuple)
|
||||
|
||||
def test_filter_orders_by_ticket_finds_matching_orders(self):
|
||||
"""Test filter_orders_by_ticket finds orders with matching ticket."""
|
||||
if self.history.orders:
|
||||
ticket = self.history.orders[0].ticket
|
||||
orders = self.history.filter_orders_by_ticket(ticket=ticket)
|
||||
if orders:
|
||||
assert all(order.ticket == ticket for order in orders)
|
||||
|
||||
def test_filter_orders_by_ticket_nonexistent_returns_empty(self):
|
||||
"""Test filter_orders_by_ticket returns empty tuple for nonexistent ticket."""
|
||||
nonexistent_ticket = 999999999999
|
||||
orders = self.history.filter_orders_by_ticket(ticket=nonexistent_ticket)
|
||||
assert orders == ()
|
||||
|
||||
def test_filter_orders_by_position_returns_tuple(self):
|
||||
"""Test filter_orders_by_position returns a tuple."""
|
||||
if self.history.orders:
|
||||
position = self.history.orders[0].position_id
|
||||
orders = self.history.filter_orders_by_position(position=position)
|
||||
assert isinstance(orders, tuple)
|
||||
|
||||
def test_filter_orders_by_position_finds_matching_orders(self):
|
||||
"""Test filter_orders_by_position finds orders with matching position_id."""
|
||||
if self.history.orders:
|
||||
position = self.history.orders[0].position_id
|
||||
orders = self.history.filter_orders_by_position(position=position)
|
||||
if orders:
|
||||
assert all(order.position_id == position for order in orders)
|
||||
|
||||
def test_filter_orders_by_position_nonexistent_returns_empty(self):
|
||||
"""Test filter_orders_by_position returns empty tuple for nonexistent position."""
|
||||
nonexistent_position = 999999999999
|
||||
orders = self.history.filter_orders_by_position(position=nonexistent_position)
|
||||
assert orders == ()
|
||||
|
||||
|
||||
class TestHistoryWithGroupFilter:
|
||||
"""Test History with group filter for specific symbols."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures with group filter."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Ensure trades are created for group filter tests."""
|
||||
pass
|
||||
|
||||
def test_group_filter_btcusd(self):
|
||||
"""Test filtering history by BTCUSD symbol group."""
|
||||
history = History(date_from=self.start, date_to=self.end, group="*BTCUSD*")
|
||||
history.initialize()
|
||||
for deal in history.deals:
|
||||
assert "BTCUSD" in deal.symbol
|
||||
|
||||
def test_group_filter_usd(self):
|
||||
"""Test filtering history by USD symbol group."""
|
||||
history = History(date_from=self.start, date_to=self.end, group="*USD*")
|
||||
history.initialize()
|
||||
for deal in history.deals:
|
||||
assert "USD" in deal.symbol
|
||||
|
||||
def test_group_filter_nonexistent_symbol(self):
|
||||
"""Test filtering with nonexistent symbol group returns empty."""
|
||||
history = History(date_from=self.start, date_to=self.end, group="NONEXISTENT12345")
|
||||
history.initialize()
|
||||
assert history.total_deals == 0
|
||||
assert history.total_orders == 0
|
||||
|
||||
|
||||
class TestHistoryDateRanges:
|
||||
"""Test History with various date ranges."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Ensure trades are made for date range tests."""
|
||||
pass
|
||||
|
||||
def test_today_date_range(self):
|
||||
"""Test history retrieval for today's date range."""
|
||||
now = datetime.now()
|
||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
history = History(date_from=start, date_to=end)
|
||||
history.initialize()
|
||||
# Should have at least the test trades
|
||||
assert history.total_deals >= 0
|
||||
|
||||
def test_past_date_range(self):
|
||||
"""Test history retrieval for a past date range."""
|
||||
now = datetime.now()
|
||||
end = now - timedelta(days=7)
|
||||
start = end - timedelta(days=7)
|
||||
history = History(date_from=start, date_to=end)
|
||||
history.initialize()
|
||||
assert isinstance(history.deals, tuple)
|
||||
assert isinstance(history.orders, tuple)
|
||||
|
||||
def test_wide_date_range(self):
|
||||
"""Test history retrieval for a wide date range (30 days)."""
|
||||
now = datetime.now()
|
||||
start = now - timedelta(days=30)
|
||||
end = now
|
||||
history = History(date_from=start, date_to=end)
|
||||
history.initialize()
|
||||
assert isinstance(history.deals, tuple)
|
||||
assert isinstance(history.orders, tuple)
|
||||
|
||||
def test_narrow_date_range(self):
|
||||
"""Test history retrieval for a narrow date range (1 hour)."""
|
||||
now = datetime.now()
|
||||
start = now - timedelta(hours=1)
|
||||
end = now
|
||||
history = History(date_from=start, date_to=end)
|
||||
history.initialize()
|
||||
assert isinstance(history.deals, tuple)
|
||||
assert isinstance(history.orders, tuple)
|
||||
|
||||
|
||||
class TestHistoryEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.now = datetime.now()
|
||||
cls.start = cls.now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = cls.now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
|
||||
def test_empty_history_no_trades(self):
|
||||
"""Test handling of date range with no trades."""
|
||||
# Use a future date range where no trades exist
|
||||
future_start = datetime.now() + timedelta(days=365)
|
||||
future_end = future_start + timedelta(days=1)
|
||||
history = History(date_from=future_start, date_to=future_end)
|
||||
history.initialize()
|
||||
assert history.deals == ()
|
||||
assert history.orders == ()
|
||||
assert history.total_deals == 0
|
||||
assert history.total_orders == 0
|
||||
|
||||
def test_filter_deals_by_ticket_with_no_deals(self):
|
||||
"""Test filtering by ticket when deals is empty."""
|
||||
future_start = datetime.now() + timedelta(days=365)
|
||||
future_end = future_start + timedelta(days=1)
|
||||
history = History(date_from=future_start, date_to=future_end)
|
||||
history.initialize()
|
||||
deals = history.filter_deals_by_ticket(ticket=12345)
|
||||
assert deals == ()
|
||||
|
||||
def test_filter_deals_by_position_with_no_deals(self):
|
||||
"""Test filtering by position when deals is empty."""
|
||||
future_start = datetime.now() + timedelta(days=365)
|
||||
future_end = future_start + timedelta(days=1)
|
||||
history = History(date_from=future_start, date_to=future_end)
|
||||
history.initialize()
|
||||
deals = history.filter_deals_by_position(position=12345)
|
||||
assert deals == ()
|
||||
|
||||
def test_filter_orders_by_ticket_with_no_orders(self):
|
||||
"""Test filtering by ticket when orders is empty."""
|
||||
future_start = datetime.now() + timedelta(days=365)
|
||||
future_end = future_start + timedelta(days=1)
|
||||
history = History(date_from=future_start, date_to=future_end)
|
||||
history.initialize()
|
||||
orders = history.filter_orders_by_ticket(ticket=12345)
|
||||
assert orders == ()
|
||||
|
||||
def test_filter_orders_by_position_with_no_orders(self):
|
||||
"""Test filtering by position when orders is empty."""
|
||||
future_start = datetime.now() + timedelta(days=365)
|
||||
future_end = future_start + timedelta(days=1)
|
||||
history = History(date_from=future_start, date_to=future_end)
|
||||
history.initialize()
|
||||
orders = history.filter_orders_by_position(position=12345)
|
||||
assert orders == ()
|
||||
|
||||
def test_initialize_can_be_called_multiple_times(self):
|
||||
"""Test that initialize() can be safely called multiple times."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
history.initialize()
|
||||
first_deals = history.deals
|
||||
first_orders = history.orders
|
||||
|
||||
history.initialize()
|
||||
# Should still have data after reinitialization
|
||||
assert isinstance(history.deals, tuple)
|
||||
assert isinstance(history.orders, tuple)
|
||||
|
||||
def test_filtering_before_initialize(self):
|
||||
"""Test filtering methods work on uninitialized history (empty tuples)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
# Don't call initialize
|
||||
deals = history.filter_deals_by_ticket(ticket=12345)
|
||||
assert deals == ()
|
||||
|
||||
deals = history.filter_deals_by_position(position=12345)
|
||||
assert deals == ()
|
||||
|
||||
orders = history.filter_orders_by_ticket(ticket=12345)
|
||||
assert orders == ()
|
||||
|
||||
orders = history.filter_orders_by_position(position=12345)
|
||||
assert orders == ()
|
||||
|
||||
|
||||
class TestHistoryUtcConversion:
|
||||
"""Test UTC timezone conversion functionality."""
|
||||
|
||||
def test_utc_conversion_with_naive_datetime(self):
|
||||
"""Test UTC conversion with naive datetime objects."""
|
||||
now = datetime.now()
|
||||
history = History(date_from=now, date_to=now, use_utc=True)
|
||||
assert history.date_from.tzinfo == UTC
|
||||
assert history.date_to.tzinfo == UTC
|
||||
|
||||
def test_utc_conversion_with_timestamps(self):
|
||||
"""Test UTC conversion when dates are provided as timestamps."""
|
||||
now = datetime.now()
|
||||
ts = now.timestamp()
|
||||
history = History(date_from=ts, date_to=ts, use_utc=True)
|
||||
assert history.date_from.tzinfo == UTC
|
||||
assert history.date_to.tzinfo == UTC
|
||||
|
||||
def test_no_utc_conversion_preserves_datetime(self):
|
||||
"""Test that use_utc=False preserves the original datetime."""
|
||||
now = datetime.now()
|
||||
history = History(date_from=now, date_to=now, use_utc=False)
|
||||
# Without UTC conversion, dates should equal original
|
||||
assert history.date_from == now
|
||||
assert history.date_to == now
|
||||
|
||||
|
||||
class TestHistoryConsistency:
|
||||
"""Test data consistency between different retrieval methods."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize history fixture."""
|
||||
self.history.initialize()
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
cls.history = History(date_from=cls.start, date_to=cls.end)
|
||||
|
||||
def test_get_deals_matches_deals_attribute(self):
|
||||
"""Test that get_deals() returns same data as deals attribute."""
|
||||
deals = self.history.get_deals()
|
||||
# After initialization, deals attribute should have same count
|
||||
# Note: Fresh call may have different data if trades occurred between calls
|
||||
assert isinstance(deals, tuple)
|
||||
if deals:
|
||||
assert all(isinstance(d, TradeDeal) for d in deals)
|
||||
|
||||
def test_get_orders_matches_orders_attribute(self):
|
||||
"""Test that get_orders() returns same data as orders attribute."""
|
||||
orders = self.history.get_orders()
|
||||
assert isinstance(orders, tuple)
|
||||
if orders:
|
||||
assert all(isinstance(o, TradeOrder) for o in orders)
|
||||
|
||||
def test_total_counts_match_tuple_lengths(self):
|
||||
"""Test that total_deals and total_orders match tuple lengths."""
|
||||
assert self.history.total_deals == len(self.history.deals)
|
||||
assert self.history.total_orders == len(self.history.orders)
|
||||
|
||||
def test_filtered_deals_subset_of_all_deals(self):
|
||||
"""Test that filtered deals are a subset of all deals."""
|
||||
if self.history.deals:
|
||||
ticket = self.history.deals[0].order
|
||||
filtered = self.history.filter_deals_by_ticket(ticket=ticket)
|
||||
for deal in filtered:
|
||||
assert deal in self.history.deals
|
||||
|
||||
def test_filtered_orders_subset_of_all_orders(self):
|
||||
"""Test that filtered orders are a subset of all orders."""
|
||||
if self.history.orders:
|
||||
position = self.history.orders[0].position_id
|
||||
filtered = self.history.filter_orders_by_position(position=position)
|
||||
for order in filtered:
|
||||
assert order in self.history.orders
|
||||
|
||||
|
||||
class TestHistorySyncVsAsync:
|
||||
"""Test that sync History behaves correctly as a synchronous implementation."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
|
||||
def test_initialize_is_synchronous(self):
|
||||
"""Test that initialize() is a synchronous method (not a coroutine)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
import inspect
|
||||
assert not inspect.iscoroutinefunction(history.initialize)
|
||||
|
||||
def test_get_deals_is_synchronous(self):
|
||||
"""Test that get_deals() is a synchronous method (not a coroutine)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
import inspect
|
||||
assert not inspect.iscoroutinefunction(history.get_deals)
|
||||
|
||||
def test_get_orders_is_synchronous(self):
|
||||
"""Test that get_orders() is a synchronous method (not a coroutine)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
import inspect
|
||||
assert not inspect.iscoroutinefunction(history.get_orders)
|
||||
|
||||
def test_initialize_returns_none(self):
|
||||
"""Test that initialize() returns None (not a coroutine object)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
result = history.initialize()
|
||||
assert result is None
|
||||
|
||||
def test_get_deals_returns_tuple_directly(self):
|
||||
"""Test that get_deals() returns a tuple directly (not a coroutine)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
result = history.get_deals()
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
def test_get_orders_returns_tuple_directly(self):
|
||||
"""Test that get_orders() returns a tuple directly (not a coroutine)."""
|
||||
history = History(date_from=self.start, date_to=self.end)
|
||||
result = history.get_orders()
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
|
||||
class TestHistoryClassMethods:
|
||||
"""Test History class methods."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
now = datetime.now()
|
||||
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
cls.history = History(date_from=cls.start, date_to=cls.end)
|
||||
cls.history.initialize()
|
||||
|
||||
def test_get_deal_by_ticket_exists(self):
|
||||
"""Test get_deal_by_ticket returns a TradeDeal."""
|
||||
if self.history.deals:
|
||||
ticket = self.history.deals[0].ticket
|
||||
deal = History.get_deal_by_ticket(ticket=ticket)
|
||||
assert isinstance(deal, TradeDeal)
|
||||
assert deal.ticket == ticket
|
||||
|
||||
def test_get_deals_by_position_exists(self):
|
||||
"""Test get_deals_by_position returns a tuple of TradeDeals."""
|
||||
if self.history.deals:
|
||||
position = self.history.deals[0].position_id
|
||||
deals = History.get_deals_by_position(position=position)
|
||||
assert isinstance(deals, tuple)
|
||||
assert all(deal.position_id == position for deal in deals)
|
||||
|
||||
def test_get_order_by_ticket_exists(self):
|
||||
"""Test get_order_by_ticket returns a TradeOrder."""
|
||||
if self.history.orders:
|
||||
ticket = self.history.orders[0].ticket
|
||||
order = History.get_order_by_ticket(ticket=ticket)
|
||||
assert isinstance(order, TradeOrder)
|
||||
assert order.ticket == ticket
|
||||
|
||||
def test_get_orders_by_position_exists(self):
|
||||
"""Test get_orders_by_position returns a tuple of TradeOrders."""
|
||||
if self.history.orders:
|
||||
position = self.history.orders[0].position_id
|
||||
orders = History.get_orders_by_position(position=position)
|
||||
assert isinstance(orders, tuple)
|
||||
assert all(order.position_id == position for order in orders)
|
||||
@@ -0,0 +1,190 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
import MetaTrader5
|
||||
|
||||
from aiomql.core.sync.meta_trader import MetaTrader
|
||||
|
||||
|
||||
class TestMetaTraderSync:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.mt = MetaTrader()
|
||||
cls.mt5 = MetaTrader5
|
||||
cls.symbol = "BTCUSD"
|
||||
now = datetime.now(tz=pytz.UTC)
|
||||
cls.start = now - timedelta(hours=10)
|
||||
cls.end = now + timedelta(hours=1)
|
||||
cls.tf = cls.mt.TIMEFRAME_H1
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
cls.mt.shutdown()
|
||||
|
||||
def test_initialize(self):
|
||||
res = self.mt.initialize()
|
||||
assert res == True
|
||||
|
||||
def test_login(self):
|
||||
res = self.mt.login()
|
||||
assert res == True
|
||||
|
||||
def test_last_error(self):
|
||||
res = self.mt.last_error()
|
||||
assert isinstance(res, tuple)
|
||||
assert res[0] == 1
|
||||
assert res[1] == "Success"
|
||||
|
||||
def test_version(self):
|
||||
res = self.mt.version()
|
||||
res2 = self.mt5.version()
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_account_info(self):
|
||||
res = self.mt.account_info()
|
||||
res2 = self.mt5.account_info()
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_terminal_info(self):
|
||||
res = self.mt.terminal_info()
|
||||
res2 = self.mt5.terminal_info()
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_symbols_total(self):
|
||||
res = self.mt.symbols_total()
|
||||
res2 = self.mt5.symbols_total()
|
||||
assert isinstance(res, int)
|
||||
assert res == res2
|
||||
|
||||
def test_symbols_get(self):
|
||||
res = self.mt.symbols_get()
|
||||
res2 = self.mt5.symbols_get()
|
||||
assert res is not None
|
||||
assert len(res) == len(res2)
|
||||
|
||||
def test_symbol_info(self):
|
||||
res = self.mt.symbol_info(self.symbol)
|
||||
res2 = self.mt5.symbol_info(self.symbol)
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_symbol_info_tick(self):
|
||||
res = self.mt.symbol_info_tick(self.symbol)
|
||||
res2 = self.mt5.symbol_info_tick(self.symbol)
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_symbol_select(self):
|
||||
res = self.mt.symbol_select(self.symbol, True)
|
||||
assert res == True
|
||||
|
||||
def test_market_book_add(self):
|
||||
res = self.mt.market_book_add(self.symbol)
|
||||
assert res == True
|
||||
|
||||
def test_market_book_get(self):
|
||||
res = self.mt.market_book_get(self.symbol)
|
||||
res2 = self.mt5.market_book_get(self.symbol)
|
||||
assert res is not None
|
||||
assert res == res2
|
||||
|
||||
def test_market_book_release(self):
|
||||
res = self.mt.market_book_release(self.symbol)
|
||||
assert res == True
|
||||
|
||||
def test_copy_rates_from(self):
|
||||
res = self.mt.copy_rates_from(self.symbol, self.tf, self.start, 10)
|
||||
assert res is not None
|
||||
assert res.shape[0] == 10
|
||||
|
||||
def test_copy_rates_from_pos(self):
|
||||
res = self.mt.copy_rates_from_pos(self.symbol, self.tf, 0, 10)
|
||||
assert res is not None
|
||||
assert res.shape[0] == 10
|
||||
|
||||
def test_copy_rates_range(self):
|
||||
res = self.mt.copy_rates_range(self.symbol, self.tf, self.start, self.end)
|
||||
assert res is not None
|
||||
assert res.shape[0] == 10
|
||||
|
||||
def test_copy_ticks_from(self):
|
||||
res = self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL)
|
||||
assert res is not None
|
||||
assert res.shape[0] == 10
|
||||
|
||||
def test_copy_ticks_range(self):
|
||||
res = self.mt.copy_ticks_range(self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL)
|
||||
res2 = self.mt5.copy_ticks_range(self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL)
|
||||
assert res is not None
|
||||
assert res.shape[0] == res2.shape[0]
|
||||
|
||||
def test_orders_total(self):
|
||||
res = self.mt.orders_total()
|
||||
assert isinstance(res, int)
|
||||
|
||||
def test_orders_get(self):
|
||||
res = self.mt.orders_get()
|
||||
assert res is not None
|
||||
assert isinstance(res, tuple)
|
||||
assert len(res) == 0
|
||||
|
||||
def test_order_calc_margin(self, sell_order_sync):
|
||||
price = sell_order_sync["price"]
|
||||
volume = sell_order_sync["volume"]
|
||||
type_ = sell_order_sync["type"]
|
||||
res = self.mt.order_calc_margin(type_, self.symbol, volume, price)
|
||||
assert isinstance(res, float)
|
||||
|
||||
def test_order_calc_profit(self, buy_order_sync):
|
||||
volume = buy_order_sync["volume"]
|
||||
price_open = buy_order_sync["price"]
|
||||
price_close = buy_order_sync["tp"]
|
||||
type_ = buy_order_sync["type"]
|
||||
res = self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close)
|
||||
assert isinstance(res, float)
|
||||
|
||||
def test_order_check(self, buy_order_sync):
|
||||
res = self.mt.order_check(buy_order_sync)
|
||||
assert res is not None
|
||||
assert res.retcode == 0
|
||||
|
||||
def test_order_send(self, sell_order_sync):
|
||||
res = self.mt.order_send(sell_order_sync)
|
||||
assert res is not None
|
||||
assert res.retcode == 10009
|
||||
|
||||
def test_positions_total(self):
|
||||
res = self.mt.positions_total()
|
||||
assert isinstance(res, int)
|
||||
assert res >= 0
|
||||
|
||||
def test_positions_get(self):
|
||||
res = self.mt.positions_get()
|
||||
assert res is not None
|
||||
assert isinstance(res, tuple)
|
||||
assert len(res) >= 0
|
||||
|
||||
def test_history_orders_total(self):
|
||||
res = self.mt.history_orders_total(self.start, self.end)
|
||||
assert isinstance(res, int)
|
||||
assert res >= 0
|
||||
|
||||
def test_history_orders_get(self):
|
||||
res = self.mt.history_orders_get(self.start, self.end)
|
||||
assert res is not None
|
||||
assert isinstance(res, tuple)
|
||||
assert len(res) >= 0
|
||||
|
||||
def test_history_deals_total(self):
|
||||
res = self.mt.history_deals_total(self.start, self.end)
|
||||
assert isinstance(res, int)
|
||||
assert res >= 0
|
||||
|
||||
def test_history_deals_get(self):
|
||||
res = self.mt.history_deals_get(self.start, self.end)
|
||||
assert res is not None
|
||||
assert isinstance(res, tuple)
|
||||
assert len(res) >= 0
|
||||
@@ -0,0 +1,696 @@
|
||||
"""Comprehensive tests for the synchronous Order module.
|
||||
|
||||
Tests cover:
|
||||
- Order initialization and default values
|
||||
- Order modification
|
||||
- Order checking (margin sufficiency)
|
||||
- Order sending (market orders)
|
||||
- Margin calculations
|
||||
- Profit/loss calculations
|
||||
- Pending order management
|
||||
- Request property and filtering
|
||||
- Class methods for order operations
|
||||
- cancel_order and send_order retry logic
|
||||
- Error handling and __getstate__
|
||||
- Verification that methods are synchronous
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aiomql.lib.sync.order import Order
|
||||
from aiomql.core.constants import TradeAction, OrderTime, OrderFilling, OrderType
|
||||
from aiomql.core.models import OrderCheckResult, OrderSendResult, TradeOrder
|
||||
from aiomql.core.exceptions import OrderError
|
||||
|
||||
|
||||
class TestOrderInitialization:
|
||||
"""Test Order class initialization and default values."""
|
||||
|
||||
def test_init_with_minimal_args(self):
|
||||
"""Test Order can be initialized with minimal arguments."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert order.symbol == "BTCUSD"
|
||||
assert order.type == OrderType.BUY
|
||||
assert order.volume == 0.01
|
||||
assert order.price == 50000.0
|
||||
|
||||
def test_init_default_action(self):
|
||||
"""Test Order has default action of DEAL."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert order.action == TradeAction.DEAL
|
||||
|
||||
def test_init_default_type_time(self):
|
||||
"""Test Order has default type_time of DAY."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert order.type_time == OrderTime.DAY
|
||||
|
||||
def test_init_default_type_filling(self):
|
||||
"""Test Order has default type_filling of FOK."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert order.type_filling == OrderFilling.FOK
|
||||
|
||||
def test_init_override_defaults(self):
|
||||
"""Test Order defaults can be overridden."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
action=TradeAction.PENDING,
|
||||
type_time=OrderTime.GTC,
|
||||
type_filling=OrderFilling.IOC,
|
||||
)
|
||||
assert order.action == TradeAction.PENDING
|
||||
assert order.type_time == OrderTime.GTC
|
||||
assert order.type_filling == OrderFilling.IOC
|
||||
|
||||
def test_init_with_sl_tp(self):
|
||||
"""Test Order can be initialized with stop loss and take profit."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
sl=49000.0,
|
||||
tp=51000.0,
|
||||
)
|
||||
assert order.sl == 49000.0
|
||||
assert order.tp == 51000.0
|
||||
|
||||
def test_init_with_magic(self):
|
||||
"""Test Order can be initialized with magic number."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
magic=12345,
|
||||
)
|
||||
assert order.magic == 12345
|
||||
|
||||
def test_init_with_comment(self):
|
||||
"""Test Order can be initialized with comment."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
comment="Test order",
|
||||
)
|
||||
assert order.comment == "Test order"
|
||||
|
||||
|
||||
class TestOrderModification:
|
||||
"""Test Order modification method."""
|
||||
|
||||
def test_modify_single_attribute(self):
|
||||
"""Test modifying a single attribute."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.modify(volume=0.02)
|
||||
assert order.volume == 0.02
|
||||
|
||||
def test_modify_multiple_attributes(self):
|
||||
"""Test modifying multiple attributes at once."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.modify(volume=0.02, price=51000.0, sl=49000.0)
|
||||
assert order.volume == 0.02
|
||||
assert order.price == 51000.0
|
||||
assert order.sl == 49000.0
|
||||
|
||||
def test_modify_type(self):
|
||||
"""Test modifying order type."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.modify(type=OrderType.SELL)
|
||||
assert order.type == OrderType.SELL
|
||||
|
||||
def test_modify_preserves_other_attributes(self):
|
||||
"""Test modifying doesn't affect other attributes."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
comment="Original",
|
||||
)
|
||||
order.modify(volume=0.02)
|
||||
assert order.comment == "Original"
|
||||
assert order.symbol == "BTCUSD"
|
||||
|
||||
def test_modify_returns_none(self):
|
||||
"""Test modify returns None (modifies in place)."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
result = order.modify(volume=0.02)
|
||||
assert result is None
|
||||
|
||||
def test_modify_action_to_pending(self):
|
||||
"""Test modifying action to PENDING."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.modify(action=TradeAction.PENDING)
|
||||
assert order.action == TradeAction.PENDING
|
||||
|
||||
|
||||
class TestOrderRequest:
|
||||
"""Test Order request property."""
|
||||
|
||||
def test_request_is_dict(self):
|
||||
"""Test request property returns a dictionary."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert isinstance(order.request, dict)
|
||||
|
||||
def test_request_contains_required_fields(self):
|
||||
"""Test request contains required trade fields."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
request = order.request
|
||||
assert "symbol" in request
|
||||
assert "type" in request
|
||||
assert "volume" in request
|
||||
assert "price" in request
|
||||
assert "action" in request
|
||||
|
||||
def test_request_filters_invalid_fields(self):
|
||||
"""Test request only contains valid TradeRequest fields."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
)
|
||||
request = order.request
|
||||
# Should not contain fields that aren't part of TradeRequest
|
||||
for key in request:
|
||||
assert key in order.mt5.TradeRequest.__match_args__
|
||||
|
||||
def test_request_includes_sl_tp_when_set(self):
|
||||
"""Test request includes sl and tp when they are set."""
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.01,
|
||||
price=50000.0,
|
||||
sl=49000.0,
|
||||
tp=51000.0,
|
||||
)
|
||||
request = order.request
|
||||
assert request["sl"] == 49000.0
|
||||
assert request["tp"] == 51000.0
|
||||
|
||||
def test_request_reflects_modify(self):
|
||||
"""Test request reflects changes after modify."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.modify(price=51000.0)
|
||||
assert order.request["price"] == 51000.0
|
||||
|
||||
|
||||
class TestOrderCheckLive:
|
||||
"""Live tests for Order check method."""
|
||||
|
||||
def test_check_returns_order_check_result(self, buy_order_sync):
|
||||
"""Test check returns OrderCheckResult."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.check()
|
||||
assert isinstance(result, OrderCheckResult)
|
||||
|
||||
def test_check_success_retcode(self, buy_order_sync):
|
||||
"""Test successful check has retcode 0."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.check()
|
||||
assert result.retcode == 0
|
||||
|
||||
def test_check_has_margin_info(self, buy_order_sync):
|
||||
"""Test check result contains margin information."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.check()
|
||||
assert hasattr(result, 'margin')
|
||||
assert hasattr(result, 'margin_free')
|
||||
|
||||
def test_check_has_balance_info(self, buy_order_sync):
|
||||
"""Test check result contains balance information."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.check()
|
||||
assert hasattr(result, 'balance')
|
||||
assert hasattr(result, 'equity')
|
||||
|
||||
def test_check_with_kwargs_override(self, buy_order_sync):
|
||||
"""Test check can use kwargs to override order params."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.check(volume=buy_order_sync["volume"] * 2)
|
||||
assert isinstance(result, OrderCheckResult)
|
||||
|
||||
def test_check_sell_order(self, sell_order_sync):
|
||||
"""Test check works for sell orders."""
|
||||
order = Order(**sell_order_sync)
|
||||
result = order.check()
|
||||
assert result.retcode == 0
|
||||
|
||||
def test_check_raises_order_error_when_none(self):
|
||||
"""Test check raises OrderError when mt5.order_check returns None."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order.mt5.order_check = MagicMock(return_value=None)
|
||||
with pytest.raises(OrderError):
|
||||
order.check()
|
||||
|
||||
|
||||
class TestOrderSendLive:
|
||||
"""Live tests for Order send method."""
|
||||
|
||||
def test_send_returns_order_send_result(self, buy_order_sync):
|
||||
"""Test send returns OrderSendResult."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.send()
|
||||
assert isinstance(result, OrderSendResult)
|
||||
|
||||
def test_send_success_retcode(self, buy_order_sync):
|
||||
"""Test successful send has retcode 10009."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.send()
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_send_has_deal_ticket(self, buy_order_sync):
|
||||
"""Test send result contains deal ticket."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.send()
|
||||
assert hasattr(result, 'deal')
|
||||
assert result.deal > 0
|
||||
|
||||
def test_send_has_order_ticket(self, buy_order_sync):
|
||||
"""Test send result contains order ticket."""
|
||||
order = Order(**buy_order_sync)
|
||||
result = order.send()
|
||||
assert hasattr(result, 'order')
|
||||
assert result.order > 0
|
||||
|
||||
def test_send_sell_order(self, sell_order_sync):
|
||||
"""Test send works for sell orders."""
|
||||
order = Order(**sell_order_sync)
|
||||
result = order.send()
|
||||
assert result.retcode == 10009
|
||||
|
||||
|
||||
class TestCancelOrderSync:
|
||||
"""Tests for cancel_order class method."""
|
||||
|
||||
def test_cancel_order_raises_for_none_result(self):
|
||||
"""Test cancel_order raises OrderError when send_order returns None."""
|
||||
original = Order.mt5.order_send
|
||||
Order.mt5.order_send = MagicMock(return_value=None)
|
||||
try:
|
||||
with pytest.raises(OrderError):
|
||||
Order.cancel_order(order=999999999)
|
||||
finally:
|
||||
Order.mt5.order_send = original
|
||||
|
||||
def test_cancel_order_sends_remove_action(self):
|
||||
"""Test cancel_order sends REMOVE action."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.retcode = 10009
|
||||
mock_result._asdict = MagicMock(return_value={
|
||||
"retcode": 10009, "deal": 0, "order": 12345, "volume": 0.0,
|
||||
"price": 0.0, "bid": 0.0, "ask": 0.0, "comment": "",
|
||||
"request_id": 1, "retcode_external": 0,
|
||||
"request": {"action": 8, "order": 12345, "symbol": "BTCUSD"},
|
||||
})
|
||||
original = Order.mt5.order_send
|
||||
Order.mt5.order_send = MagicMock(return_value=mock_result)
|
||||
try:
|
||||
result = Order.cancel_order(order=12345, symbol="BTCUSD")
|
||||
assert isinstance(result, OrderSendResult)
|
||||
call_args = Order.mt5.order_send.call_args[0][0]
|
||||
assert call_args["action"] == TradeAction.REMOVE
|
||||
finally:
|
||||
Order.mt5.order_send = original
|
||||
|
||||
|
||||
class TestSendOrderRetrySync:
|
||||
"""Test send_order retry logic."""
|
||||
|
||||
def test_send_order_retries_on_10031(self):
|
||||
"""Test send_order retries when retcode is 10031 (no connection)."""
|
||||
mock_result_fail = MagicMock()
|
||||
mock_result_fail.retcode = 10031
|
||||
mock_result_fail._asdict = MagicMock(return_value={
|
||||
"retcode": 10031, "deal": 0, "order": 0, "volume": 0.0,
|
||||
"price": 0.0, "bid": 0.0, "ask": 0.0, "comment": "No connection",
|
||||
"request_id": 1, "retcode_external": 0,
|
||||
"request": {"action": 1, "symbol": "BTCUSD"},
|
||||
})
|
||||
mock_result_ok = MagicMock()
|
||||
mock_result_ok.retcode = 10009
|
||||
mock_result_ok._asdict = MagicMock(return_value={
|
||||
"retcode": 10009, "deal": 12345, "order": 67890, "volume": 0.01,
|
||||
"price": 50000.0, "bid": 49999.0, "ask": 50001.0, "comment": "",
|
||||
"request_id": 2, "retcode_external": 0,
|
||||
"request": {"action": 1, "symbol": "BTCUSD"},
|
||||
})
|
||||
|
||||
original = Order.mt5.order_send
|
||||
Order.mt5.order_send = MagicMock(side_effect=[mock_result_fail, mock_result_ok])
|
||||
try:
|
||||
result = Order.send_order(request={"symbol": "BTCUSD", "action": 1})
|
||||
assert result.retcode == 10009
|
||||
assert Order.mt5.order_send.call_count == 2
|
||||
finally:
|
||||
Order.mt5.order_send = original
|
||||
|
||||
def test_send_order_raises_when_none(self):
|
||||
"""Test send_order raises OrderError when result is None."""
|
||||
original = Order.mt5.order_send
|
||||
Order.mt5.order_send = MagicMock(return_value=None)
|
||||
try:
|
||||
with pytest.raises(OrderError):
|
||||
Order.send_order(request={"symbol": "BTCUSD", "action": 1})
|
||||
finally:
|
||||
Order.mt5.order_send = original
|
||||
|
||||
|
||||
class TestOrderMarginCalculationLive:
|
||||
"""Live tests for Order margin calculation."""
|
||||
|
||||
def test_calc_margin_returns_float(self, buy_order_sync):
|
||||
"""Test calc_margin returns a float."""
|
||||
order = Order(**buy_order_sync)
|
||||
margin = order.calc_margin()
|
||||
assert isinstance(margin, float)
|
||||
|
||||
def test_calc_margin_positive(self, buy_order_sync):
|
||||
"""Test calc_margin returns positive value."""
|
||||
order = Order(**buy_order_sync)
|
||||
margin = order.calc_margin()
|
||||
assert margin > 0
|
||||
|
||||
def test_calc_margin_buy_order(self, buy_order_sync):
|
||||
"""Test calc_margin works for buy orders."""
|
||||
order = Order(**buy_order_sync)
|
||||
margin = order.calc_margin()
|
||||
assert margin is not None
|
||||
assert margin > 0
|
||||
|
||||
def test_calc_margin_sell_order(self, sell_order_sync):
|
||||
"""Test calc_margin works for sell orders."""
|
||||
order = Order(**sell_order_sync)
|
||||
margin = order.calc_margin()
|
||||
assert margin is not None
|
||||
assert margin > 0
|
||||
|
||||
|
||||
class TestOrderProfitCalculationLive:
|
||||
"""Live tests for Order profit/loss calculations."""
|
||||
|
||||
def test_calc_profit_returns_float(self, buy_order_sync):
|
||||
"""Test calc_profit returns a float."""
|
||||
order = Order(**buy_order_sync)
|
||||
profit = order.calc_profit()
|
||||
assert isinstance(profit, float)
|
||||
|
||||
def test_calc_profit_is_positive_for_tp(self, buy_order_sync):
|
||||
"""Test calc_profit is positive when price reaches TP."""
|
||||
order = Order(**buy_order_sync)
|
||||
profit = order.calc_profit()
|
||||
assert profit > 0
|
||||
|
||||
def test_calc_loss_returns_float(self, buy_order_sync):
|
||||
"""Test calc_loss returns a float."""
|
||||
order = Order(**buy_order_sync)
|
||||
loss = order.calc_loss()
|
||||
assert isinstance(loss, float)
|
||||
|
||||
def test_calc_loss_is_negative_for_sl(self, buy_order_sync):
|
||||
"""Test calc_loss is negative when price reaches SL."""
|
||||
order = Order(**buy_order_sync)
|
||||
loss = order.calc_loss()
|
||||
assert loss < 0
|
||||
|
||||
def test_calc_profit_sell_order(self, sell_order_sync):
|
||||
"""Test calc_profit works for sell orders (may be None if no TP)."""
|
||||
order = Order(**sell_order_sync)
|
||||
# sell_order may not have tp set
|
||||
profit = order.calc_profit()
|
||||
# May be None if tp is not set
|
||||
assert profit is None or isinstance(profit, float)
|
||||
|
||||
|
||||
class TestOrdersTotalLive:
|
||||
"""Live tests for orders_total class method."""
|
||||
|
||||
def test_orders_total_returns_int(self):
|
||||
"""Test orders_total returns an integer."""
|
||||
total = Order.orders_total()
|
||||
assert isinstance(total, int)
|
||||
|
||||
def test_orders_total_non_negative(self):
|
||||
"""Test orders_total returns non-negative value."""
|
||||
total = Order.orders_total()
|
||||
assert total >= 0
|
||||
|
||||
|
||||
class TestGetPendingOrdersLive:
|
||||
"""Live tests for pending order retrieval."""
|
||||
|
||||
def test_get_pending_orders_returns_tuple(self):
|
||||
"""Test get_pending_orders returns a tuple."""
|
||||
orders = Order.get_pending_orders()
|
||||
assert isinstance(orders, tuple)
|
||||
|
||||
def test_get_pending_orders_contains_trade_orders(self):
|
||||
"""Test get_pending_orders contains TradeOrder objects."""
|
||||
orders = Order.get_pending_orders()
|
||||
for order in orders:
|
||||
assert isinstance(order, TradeOrder)
|
||||
|
||||
def test_get_pending_orders_by_symbol(self):
|
||||
"""Test get_pending_orders can filter by symbol."""
|
||||
orders = Order.get_pending_orders(symbol="BTCUSD")
|
||||
for order in orders:
|
||||
assert order.symbol == "BTCUSD"
|
||||
|
||||
def test_get_pending_orders_by_group(self):
|
||||
"""Test get_pending_orders can filter by group."""
|
||||
orders = Order.get_pending_orders(group="*USD*")
|
||||
for order in orders:
|
||||
assert "USD" in order.symbol
|
||||
|
||||
def test_get_pending_order_nonexistent(self):
|
||||
"""Test get_pending_order returns None for nonexistent ticket."""
|
||||
order = Order.get_pending_order(ticket=999999999999)
|
||||
assert order is None
|
||||
|
||||
|
||||
class TestGetHistoryOrderByTicketLive:
|
||||
"""Live tests for get_history_order_by_ticket class method."""
|
||||
|
||||
def test_get_history_order_by_ticket_nonexistent(self):
|
||||
"""Test get_history_order_by_ticket returns None for nonexistent ticket."""
|
||||
order = Order.get_history_order_by_ticket(ticket=999999999999)
|
||||
assert order is None
|
||||
|
||||
def test_get_history_order_by_ticket_returns_trade_order_or_none(self):
|
||||
"""Test get_history_order_by_ticket returns TradeOrder or None."""
|
||||
# Get list of pending orders first
|
||||
orders = Order.get_pending_orders()
|
||||
if orders:
|
||||
# If there are pending orders, test with a real ticket
|
||||
ticket = orders[0].ticket
|
||||
order = Order.get_history_order_by_ticket(ticket=ticket)
|
||||
assert order is None or isinstance(order, TradeOrder)
|
||||
else:
|
||||
# If no pending orders, just verify nonexistent returns None
|
||||
order = Order.get_history_order_by_ticket(ticket=999999999999)
|
||||
assert order is None
|
||||
|
||||
|
||||
class TestProfitToPriceLive:
|
||||
"""Live tests for profit_to_price class method."""
|
||||
|
||||
def test_profit_to_price_buy_order(self, sync_mt):
|
||||
"""Test profit_to_price calculates correct price for buy order."""
|
||||
sym_info = sync_mt.symbol_info("BTCUSD")
|
||||
price_open = sym_info.ask
|
||||
volume = sym_info.volume_min
|
||||
profit = 10.0 # $10 profit target
|
||||
|
||||
price = Order.profit_to_price(
|
||||
profit=profit,
|
||||
order_type=OrderType.BUY,
|
||||
volume=volume,
|
||||
symbol="BTCUSD",
|
||||
price_open=price_open,
|
||||
)
|
||||
assert isinstance(price, float)
|
||||
assert price > price_open # For buy, profit price should be higher
|
||||
|
||||
def test_profit_to_price_sell_order(self, sync_mt):
|
||||
"""Test profit_to_price calculates correct price for sell order."""
|
||||
sym_info = sync_mt.symbol_info("BTCUSD")
|
||||
price_open = sym_info.bid
|
||||
volume = sym_info.volume_min
|
||||
profit = 10.0 # $10 profit target
|
||||
|
||||
price = Order.profit_to_price(
|
||||
profit=profit,
|
||||
order_type=OrderType.SELL,
|
||||
volume=volume,
|
||||
symbol="BTCUSD",
|
||||
price_open=price_open,
|
||||
)
|
||||
assert isinstance(price, float)
|
||||
assert price < price_open # For sell, profit price should be lower
|
||||
|
||||
|
||||
class TestOrderClassAttributes:
|
||||
"""Test Order class attributes and inheritance."""
|
||||
|
||||
def test_order_has_mt5_attribute(self):
|
||||
"""Test Order class has mt5 attribute."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert hasattr(order, 'mt5')
|
||||
|
||||
def test_order_has_config_attribute(self):
|
||||
"""Test Order class has config attribute."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert hasattr(order, 'config')
|
||||
|
||||
def test_order_inherits_trade_request(self):
|
||||
"""Test Order inherits from TradeRequest."""
|
||||
from aiomql.core.models import TradeRequest
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert isinstance(order, TradeRequest)
|
||||
|
||||
def test_order_getstate_excludes_mt5(self):
|
||||
"""Test __getstate__ excludes mt5 attribute for pickling."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
state = order.__getstate__()
|
||||
assert "mt5" not in state
|
||||
|
||||
def test_order_getstate_preserves_other_attrs(self):
|
||||
"""Test __getstate__ preserves trade attributes."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
state = order.__getstate__()
|
||||
assert state["symbol"] == "BTCUSD"
|
||||
|
||||
def test_order_mode_is_sync(self):
|
||||
"""Test sync Order has mode set to 'sync'."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert order.mode == "sync"
|
||||
|
||||
|
||||
class TestOrderEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
def test_check_with_zero_volume(self, sync_mt):
|
||||
"""Test check with zero volume."""
|
||||
sym_info = sync_mt.symbol_info("BTCUSD")
|
||||
order = Order(
|
||||
symbol="BTCUSD",
|
||||
type=OrderType.BUY,
|
||||
volume=0.0,
|
||||
price=sym_info.ask,
|
||||
)
|
||||
# Should either raise error or return failed check
|
||||
try:
|
||||
result = order.check()
|
||||
assert result.retcode != 0
|
||||
except OrderError:
|
||||
pass # Also acceptable
|
||||
|
||||
def test_multiple_orders_share_mt5(self):
|
||||
"""Test multiple Order instances share the same mt5 object."""
|
||||
order1 = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order2 = Order(symbol="ETHUSD", type=OrderType.SELL, volume=0.01, price=3000.0)
|
||||
assert order1.mt5 is order2.mt5
|
||||
|
||||
def test_multiple_orders_share_config(self):
|
||||
"""Test multiple Order instances share the same config object."""
|
||||
order1 = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
order2 = Order(symbol="ETHUSD", type=OrderType.SELL, volume=0.01, price=3000.0)
|
||||
assert order1.config is order2.config
|
||||
|
||||
def test_calc_margin_returns_none_on_error(self):
|
||||
"""Test calc_margin returns None when an error occurs."""
|
||||
order = Order(symbol="INVALIDSYMBOL", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
result = order.calc_margin()
|
||||
assert result is None
|
||||
|
||||
def test_calc_profit_returns_none_when_no_tp(self):
|
||||
"""Test calc_profit returns None when tp is not set."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
result = order.calc_profit()
|
||||
assert result is None or isinstance(result, float)
|
||||
|
||||
def test_calc_loss_returns_none_when_no_sl(self):
|
||||
"""Test calc_loss returns None when sl is not set."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
result = order.calc_loss()
|
||||
assert result is None or isinstance(result, float)
|
||||
|
||||
def test_get_pending_orders_returns_empty_for_nonexistent_symbol(self):
|
||||
"""Test get_pending_orders returns empty tuple for nonexistent symbol."""
|
||||
orders = Order.get_pending_orders(symbol="NONEXISTENT")
|
||||
assert orders == ()
|
||||
|
||||
|
||||
class TestOrderSyncMethods:
|
||||
"""Test that Order methods are truly synchronous."""
|
||||
|
||||
def test_orders_total_is_synchronous(self):
|
||||
"""Test orders_total is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.orders_total)
|
||||
|
||||
def test_get_pending_order_is_synchronous(self):
|
||||
"""Test get_pending_order is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.get_pending_order)
|
||||
|
||||
def test_get_pending_orders_is_synchronous(self):
|
||||
"""Test get_pending_orders is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.get_pending_orders)
|
||||
|
||||
def test_cancel_order_is_synchronous(self):
|
||||
"""Test cancel_order is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.cancel_order)
|
||||
|
||||
def test_check_is_synchronous(self):
|
||||
"""Test check is a synchronous method."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert not inspect.iscoroutinefunction(order.check)
|
||||
|
||||
def test_send_is_synchronous(self):
|
||||
"""Test send is a synchronous method."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert not inspect.iscoroutinefunction(order.send)
|
||||
|
||||
def test_calc_margin_is_synchronous(self):
|
||||
"""Test calc_margin is a synchronous method."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert not inspect.iscoroutinefunction(order.calc_margin)
|
||||
|
||||
def test_calc_profit_is_synchronous(self):
|
||||
"""Test calc_profit is a synchronous method."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert not inspect.iscoroutinefunction(order.calc_profit)
|
||||
|
||||
def test_calc_loss_is_synchronous(self):
|
||||
"""Test calc_loss is a synchronous method."""
|
||||
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
|
||||
assert not inspect.iscoroutinefunction(order.calc_loss)
|
||||
|
||||
def test_profit_to_price_is_synchronous(self):
|
||||
"""Test profit_to_price is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.profit_to_price)
|
||||
|
||||
def test_get_history_order_by_ticket_is_synchronous(self):
|
||||
"""Test get_history_order_by_ticket is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.get_history_order_by_ticket)
|
||||
|
||||
def test_send_order_is_synchronous(self):
|
||||
"""Test send_order is a synchronous method."""
|
||||
assert not inspect.iscoroutinefunction(Order.send_order)
|
||||
|
||||
def test_orders_total_returns_directly(self):
|
||||
"""Test orders_total returns a value directly (not a coroutine)."""
|
||||
result = Order.orders_total()
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_get_pending_orders_returns_directly(self):
|
||||
"""Test get_pending_orders returns a value directly (not a coroutine)."""
|
||||
result = Order.get_pending_orders()
|
||||
assert isinstance(result, tuple)
|
||||
@@ -0,0 +1,378 @@
|
||||
"""Comprehensive tests for the synchronous Positions module.
|
||||
|
||||
Tests cover:
|
||||
- Positions class initialization (BaseMeta metaclass behavior)
|
||||
- Getting positions with various filters
|
||||
- Getting positions by ticket and symbol
|
||||
- Closing positions (individual and all)
|
||||
- Class methods for position operations
|
||||
- Edge cases and error handling
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.sync.positions import Positions
|
||||
from aiomql.core.models import TradePosition, OrderSendResult
|
||||
from aiomql.core.exceptions import InvalidRequest
|
||||
|
||||
|
||||
class TestPositionsInitialization:
|
||||
"""Test Positions class initialization."""
|
||||
|
||||
def test_has_mt5_attribute(self):
|
||||
"""Test Positions has mt5 class attribute."""
|
||||
assert hasattr(Positions, 'mt5')
|
||||
|
||||
def test_has_config_attribute(self):
|
||||
"""Test Positions has config class attribute."""
|
||||
assert hasattr(Positions, 'config')
|
||||
|
||||
def test_class_attributes_are_shared(self):
|
||||
"""Test that class attributes are shared across access points."""
|
||||
assert Positions.mt5 is Positions.mt5
|
||||
assert Positions.config is Positions.config
|
||||
|
||||
|
||||
class TestGetPositionsLive:
|
||||
"""Live tests for getting positions."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_get_positions_returns_tuple(self):
|
||||
"""Test get_positions returns a tuple."""
|
||||
positions = Positions.get_positions()
|
||||
assert isinstance(positions, tuple)
|
||||
|
||||
def test_get_positions_contains_trade_positions(self):
|
||||
"""Test get_positions contains TradePosition objects."""
|
||||
positions = Positions.get_positions()
|
||||
for position in positions:
|
||||
assert isinstance(position, TradePosition)
|
||||
|
||||
def test_get_positions_by_symbol(self):
|
||||
"""Test get_positions can filter by symbol."""
|
||||
if self.positions:
|
||||
symbol = self.positions[0].symbol
|
||||
positions = Positions.get_positions(symbol=symbol)
|
||||
for position in positions:
|
||||
assert position.symbol == symbol
|
||||
|
||||
def test_get_positions_by_ticket(self):
|
||||
"""Test get_positions can filter by ticket."""
|
||||
if self.positions:
|
||||
ticket = self.positions[0].ticket
|
||||
positions = Positions.get_positions(ticket=ticket)
|
||||
assert len(positions) <= 1
|
||||
if positions:
|
||||
assert positions[0].ticket == ticket
|
||||
|
||||
def test_get_positions_by_group(self):
|
||||
"""Test get_positions can filter by group."""
|
||||
positions = Positions.get_positions(group="*USD*")
|
||||
for position in positions:
|
||||
assert "USD" in position.symbol
|
||||
|
||||
def test_get_positions_symbol_overrides_ticket(self):
|
||||
"""Test that symbol filter takes precedence over ticket."""
|
||||
if self.positions:
|
||||
symbol = self.positions[0].symbol
|
||||
# Pass both symbol and ticket, symbol should take precedence
|
||||
positions = Positions.get_positions(symbol=symbol, ticket=99999999)
|
||||
# Should still return positions for symbol, not error on ticket
|
||||
for position in positions:
|
||||
assert position.symbol == symbol
|
||||
|
||||
|
||||
class TestGetPositionByTicketLive:
|
||||
"""Live tests for get_position_by_ticket class method."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_get_position_by_ticket_returns_trade_position(self):
|
||||
"""Test get_position_by_ticket returns TradePosition."""
|
||||
if self.positions:
|
||||
ticket = self.positions[0].ticket
|
||||
position = Positions.get_position_by_ticket(ticket=ticket)
|
||||
assert isinstance(position, TradePosition)
|
||||
|
||||
def test_get_position_by_ticket_correct_ticket(self):
|
||||
"""Test get_position_by_ticket returns position with matching ticket."""
|
||||
if self.positions:
|
||||
ticket = self.positions[0].ticket
|
||||
position = Positions.get_position_by_ticket(ticket=ticket)
|
||||
assert position.ticket == ticket
|
||||
|
||||
def test_get_position_by_ticket_nonexistent_returns_none(self):
|
||||
"""Test get_position_by_ticket returns None for nonexistent ticket."""
|
||||
position = Positions.get_position_by_ticket(ticket=999999999999)
|
||||
assert position is None
|
||||
|
||||
def test_get_position_by_ticket_has_required_attributes(self):
|
||||
"""Test returned position has required attributes."""
|
||||
if self.positions:
|
||||
ticket = self.positions[0].ticket
|
||||
position = Positions.get_position_by_ticket(ticket=ticket)
|
||||
assert hasattr(position, 'ticket')
|
||||
assert hasattr(position, 'symbol')
|
||||
assert hasattr(position, 'volume')
|
||||
assert hasattr(position, 'type')
|
||||
assert hasattr(position, 'price_open')
|
||||
assert hasattr(position, 'price_current')
|
||||
assert hasattr(position, 'profit')
|
||||
|
||||
|
||||
class TestGetPositionsBySymbolLive:
|
||||
"""Live tests for get_positions_by_symbol class method."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Ensure trades exist."""
|
||||
pass
|
||||
|
||||
def test_get_positions_by_symbol_returns_tuple(self):
|
||||
"""Test get_positions_by_symbol returns a tuple."""
|
||||
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
|
||||
assert isinstance(positions, tuple)
|
||||
|
||||
def test_get_positions_by_symbol_contains_trade_positions(self):
|
||||
"""Test get_positions_by_symbol contains TradePosition objects."""
|
||||
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
|
||||
for position in positions:
|
||||
assert isinstance(position, TradePosition)
|
||||
|
||||
def test_get_positions_by_symbol_correct_symbol(self):
|
||||
"""Test all returned positions have the requested symbol."""
|
||||
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
|
||||
for position in positions:
|
||||
assert position.symbol == "BTCUSD"
|
||||
|
||||
def test_get_positions_by_symbol_nonexistent_returns_empty(self):
|
||||
"""Test get_positions_by_symbol returns empty tuple for nonexistent symbol."""
|
||||
positions = Positions.get_positions_by_symbol(symbol="NONEXISTENT123")
|
||||
assert positions == ()
|
||||
|
||||
|
||||
class TestGetTotalPositionsLive:
|
||||
"""Live tests for get_total_positions class method."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Ensure trades exist."""
|
||||
pass
|
||||
|
||||
def test_get_total_positions_returns_int(self):
|
||||
"""Test get_total_positions returns an integer."""
|
||||
total = Positions.get_total_positions()
|
||||
assert isinstance(total, int)
|
||||
|
||||
def test_get_total_positions_non_negative(self):
|
||||
"""Test get_total_positions returns non-negative value."""
|
||||
total = Positions.get_total_positions()
|
||||
assert total >= 0
|
||||
|
||||
def test_get_total_positions_matches_get_positions(self):
|
||||
"""Test get_total_positions matches length of get_positions."""
|
||||
total = Positions.get_total_positions()
|
||||
positions = Positions.get_positions()
|
||||
assert total == len(positions)
|
||||
|
||||
|
||||
class TestClosePositionLive:
|
||||
"""Live tests for closing positions."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_close_position_returns_tuple(self):
|
||||
"""Test close_position returns a tuple of (bool, OrderSendResult)."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
result = Positions.close_position(position=position)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_close_position_success(self):
|
||||
"""Test close_position successfully closes a position."""
|
||||
# Refresh positions
|
||||
positions = Positions.get_positions()
|
||||
if positions:
|
||||
position = positions[0]
|
||||
success, result = Positions.close_position(position=position)
|
||||
if success:
|
||||
assert isinstance(result, OrderSendResult)
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_close_position_by_ticket_returns_tuple(self):
|
||||
"""Test close_position_by_ticket returns a tuple."""
|
||||
# Refresh positions
|
||||
positions = Positions.get_positions()
|
||||
if positions:
|
||||
ticket = positions[0].ticket
|
||||
result = Positions.close_position_by_ticket(ticket=ticket)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_close_position_by_ticket_nonexistent_raises(self):
|
||||
"""Test close_position_by_ticket raises InvalidRequest for nonexistent ticket."""
|
||||
with pytest.raises(InvalidRequest):
|
||||
Positions.close_position_by_ticket(ticket=999999999999)
|
||||
|
||||
|
||||
class TestCloseStaticMethodLive:
|
||||
"""Live tests for the static close method."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_close_returns_tuple(self):
|
||||
"""Test close static method returns tuple of (bool, OrderSendResult)."""
|
||||
# Refresh positions
|
||||
positions = Positions.get_positions()
|
||||
if positions:
|
||||
position = positions[0]
|
||||
result = Positions.close(
|
||||
ticket=position.ticket,
|
||||
symbol=position.symbol,
|
||||
price=position.price_current,
|
||||
volume=position.volume,
|
||||
order_type=position.type,
|
||||
)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
class TestClosePositionsLive:
|
||||
"""Live tests for close_positions class method."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_close_positions_returns_tuple(self):
|
||||
"""Test close_positions returns a tuple."""
|
||||
positions = Positions.get_positions()
|
||||
result = Positions.close_positions(positions=positions)
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
def test_close_positions_empty_positions(self):
|
||||
"""Test close_positions with empty positions returns empty tuple."""
|
||||
result = Positions.close_positions(positions=())
|
||||
assert result == ()
|
||||
|
||||
|
||||
class TestCloseAllPositionsLive:
|
||||
"""Live tests for closing all positions."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
pass
|
||||
|
||||
def test_close_all_positions_returns_tuple(self):
|
||||
"""Test close_all_positions class method returns a tuple."""
|
||||
result = Positions.close_all_positions()
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
def test_close_all_positions_contains_order_send_results(self):
|
||||
"""Test close_all_positions returns OrderSendResult objects."""
|
||||
result = Positions.close_all_positions()
|
||||
for res in result:
|
||||
assert isinstance(res, OrderSendResult)
|
||||
|
||||
|
||||
class TestPositionAttributes:
|
||||
"""Test TradePosition attributes from positions."""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init(self, make_buy_sell_orders_sync):
|
||||
"""Initialize with live trades."""
|
||||
cls = type(self)
|
||||
cls.positions = Positions.get_positions()
|
||||
|
||||
def test_position_has_ticket(self):
|
||||
"""Test position has ticket attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'ticket')
|
||||
assert isinstance(position.ticket, int)
|
||||
|
||||
def test_position_has_symbol(self):
|
||||
"""Test position has symbol attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'symbol')
|
||||
assert isinstance(position.symbol, str)
|
||||
|
||||
def test_position_has_volume(self):
|
||||
"""Test position has volume attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'volume')
|
||||
assert isinstance(position.volume, float)
|
||||
|
||||
def test_position_has_type(self):
|
||||
"""Test position has type attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'type')
|
||||
|
||||
def test_position_has_price_open(self):
|
||||
"""Test position has price_open attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'price_open')
|
||||
assert isinstance(position.price_open, float)
|
||||
|
||||
def test_position_has_price_current(self):
|
||||
"""Test position has price_current attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'price_current')
|
||||
assert isinstance(position.price_current, float)
|
||||
|
||||
def test_position_has_profit(self):
|
||||
"""Test position has profit attribute."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'profit')
|
||||
assert isinstance(position.profit, float)
|
||||
|
||||
def test_position_has_sl_tp(self):
|
||||
"""Test position has sl and tp attributes."""
|
||||
if self.positions:
|
||||
position = self.positions[0]
|
||||
assert hasattr(position, 'sl')
|
||||
assert hasattr(position, 'tp')
|
||||
|
||||
|
||||
class TestPositionsEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
def test_get_positions_empty_when_no_positions(self):
|
||||
"""Test get_positions returns empty tuple when no positions exist."""
|
||||
# Close all positions first
|
||||
Positions.close_all_positions()
|
||||
result = Positions.get_positions()
|
||||
# Result should be a tuple (possibly empty)
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
def test_get_positions_with_invalid_group(self):
|
||||
"""Test get_positions with nonexistent group returns empty."""
|
||||
result = Positions.get_positions(group="NONEXISTENT_GROUP_12345")
|
||||
assert result == ()
|
||||
@@ -0,0 +1,620 @@
|
||||
"""Comprehensive tests for the synchronous Sessions module.
|
||||
|
||||
Tests cover:
|
||||
- Duration NamedTuple
|
||||
- delta helper function
|
||||
- Session initialization and attributes
|
||||
- Session __contains__, __str__, __repr__, __len__
|
||||
- Session in_session method
|
||||
- Session begin and close methods
|
||||
- Session duration method
|
||||
- Session close_positions, close_all, close_win, close_loss methods
|
||||
- Session action method
|
||||
- Session until method
|
||||
- Sessions initialization
|
||||
- Sessions find and find_next methods
|
||||
- Sessions __contains__
|
||||
- Sessions context manager
|
||||
- Sessions check method
|
||||
- Integration tests
|
||||
"""
|
||||
|
||||
from datetime import time, datetime, timedelta, UTC
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.sync.sessions import Session, Sessions, Duration, delta, backtest_sleep
|
||||
from aiomql.core.config import Config
|
||||
from aiomql.core.models import TradePosition, OrderSendResult
|
||||
|
||||
|
||||
class TestDuration:
|
||||
"""Test Duration NamedTuple."""
|
||||
|
||||
def test_duration_creation(self):
|
||||
"""Test creating Duration with values."""
|
||||
d = Duration(hours=2, minutes=30, seconds=45)
|
||||
assert d.hours == 2
|
||||
assert d.minutes == 30
|
||||
assert d.seconds == 45
|
||||
|
||||
def test_duration_unpacking(self):
|
||||
"""Test Duration can be unpacked."""
|
||||
d = Duration(hours=1, minutes=15, seconds=30)
|
||||
hours, minutes, seconds = d
|
||||
assert hours == 1
|
||||
assert minutes == 15
|
||||
assert seconds == 30
|
||||
|
||||
def test_duration_is_tuple(self):
|
||||
"""Test Duration is a tuple subclass."""
|
||||
d = Duration(hours=1, minutes=0, seconds=0)
|
||||
assert isinstance(d, tuple)
|
||||
|
||||
|
||||
class TestDeltaFunction:
|
||||
"""Test delta helper function."""
|
||||
|
||||
def test_delta_basic_time(self):
|
||||
"""Test delta with basic time."""
|
||||
t = time(hour=2, minute=30, second=45)
|
||||
result = delta(t)
|
||||
expected = timedelta(hours=2, minutes=30, seconds=45)
|
||||
assert result == expected
|
||||
|
||||
def test_delta_midnight(self):
|
||||
"""Test delta with midnight."""
|
||||
t = time(hour=0, minute=0, second=0)
|
||||
result = delta(t)
|
||||
assert result == timedelta(0)
|
||||
|
||||
def test_delta_with_microseconds(self):
|
||||
"""Test delta includes microseconds."""
|
||||
t = time(hour=1, minute=2, second=3, microsecond=456789)
|
||||
result = delta(t)
|
||||
expected = timedelta(hours=1, minutes=2, seconds=3, microseconds=456789)
|
||||
assert result == expected
|
||||
|
||||
def test_delta_end_of_day(self):
|
||||
"""Test delta with end of day time."""
|
||||
t = time(hour=23, minute=59, second=59)
|
||||
result = delta(t)
|
||||
expected = timedelta(hours=23, minutes=59, seconds=59)
|
||||
assert result == expected
|
||||
|
||||
|
||||
class TestSessionInitialization:
|
||||
"""Test Session class initialization."""
|
||||
|
||||
def test_init_with_time_objects(self):
|
||||
"""Test Session init with datetime.time objects."""
|
||||
start = time(8, 0)
|
||||
end = time(16, 0)
|
||||
session = Session(start=start, end=end)
|
||||
|
||||
assert session.start.hour == 8
|
||||
assert session.end.hour == 16
|
||||
assert session.start.tzinfo == UTC
|
||||
|
||||
def test_init_with_integers(self):
|
||||
"""Test Session init with integer hours."""
|
||||
session = Session(start=9, end=17)
|
||||
|
||||
assert session.start.hour == 9
|
||||
assert session.end.hour == 17
|
||||
assert session.start.tzinfo == UTC
|
||||
|
||||
def test_init_with_on_start(self):
|
||||
"""Test Session init with on_start action."""
|
||||
session = Session(start=8, end=16, on_start="close_all")
|
||||
assert session.on_start == "close_all"
|
||||
|
||||
def test_init_with_on_end(self):
|
||||
"""Test Session init with on_end action."""
|
||||
session = Session(start=8, end=16, on_end="close_loss")
|
||||
assert session.on_end == "close_loss"
|
||||
|
||||
def test_init_with_custom_functions(self):
|
||||
"""Test Session init with custom start/end functions."""
|
||||
def my_start():
|
||||
pass
|
||||
|
||||
def my_end():
|
||||
pass
|
||||
|
||||
session = Session(start=8, end=16, custom_start=my_start, custom_end=my_end)
|
||||
assert session.custom_start == my_start
|
||||
assert session.custom_end == my_end
|
||||
|
||||
def test_init_with_name(self):
|
||||
"""Test Session init with custom name."""
|
||||
session = Session(start=8, end=16, name="Morning Session")
|
||||
assert session.name == "Morning Session"
|
||||
|
||||
def test_init_default_name(self):
|
||||
"""Test Session generates default name."""
|
||||
session = Session(start=8, end=16)
|
||||
assert "<-->" in session.name
|
||||
|
||||
def test_init_creates_positions_manager(self):
|
||||
"""Test Session creates positions manager."""
|
||||
session = Session(start=8, end=16)
|
||||
assert session.positions_manager is not None
|
||||
|
||||
def test_init_creates_config(self):
|
||||
"""Test Session creates config."""
|
||||
session = Session(start=8, end=16)
|
||||
assert isinstance(session.config, Config)
|
||||
|
||||
|
||||
class TestSessionContains:
|
||||
"""Test Session __contains__ method."""
|
||||
|
||||
def test_contains_time_in_session(self):
|
||||
"""Test time within session returns True."""
|
||||
session = Session(start=8, end=16)
|
||||
test_time = time(12, 0)
|
||||
assert test_time in session
|
||||
|
||||
def test_contains_time_at_start(self):
|
||||
"""Test time at start of session."""
|
||||
session = Session(start=8, end=16)
|
||||
test_time = time(8, 0)
|
||||
assert test_time in session
|
||||
|
||||
def test_contains_time_at_end(self):
|
||||
"""Test time at end of session."""
|
||||
session = Session(start=8, end=16)
|
||||
test_time = time(16, 0)
|
||||
assert test_time in session
|
||||
|
||||
def test_contains_time_before_session(self):
|
||||
"""Test time before session returns False."""
|
||||
session = Session(start=8, end=16)
|
||||
test_time = time(7, 0)
|
||||
assert test_time not in session
|
||||
|
||||
def test_contains_time_after_session(self):
|
||||
"""Test time after session returns False."""
|
||||
session = Session(start=8, end=16)
|
||||
test_time = time(17, 0)
|
||||
assert test_time not in session
|
||||
|
||||
|
||||
class TestSessionStringMethods:
|
||||
"""Test Session string representation methods."""
|
||||
|
||||
def test_str(self):
|
||||
"""Test __str__ returns formatted string."""
|
||||
session = Session(start=8, end=16)
|
||||
result = str(session)
|
||||
assert "<-->" in result
|
||||
|
||||
def test_repr(self):
|
||||
"""Test __repr__ returns formatted string."""
|
||||
session = Session(start=8, end=16)
|
||||
result = repr(session)
|
||||
assert "<-->" in result
|
||||
|
||||
|
||||
class TestSessionLen:
|
||||
"""Test Session __len__ method."""
|
||||
|
||||
def test_len_full_hours(self):
|
||||
"""Test __len__ returns duration in seconds."""
|
||||
session = Session(start=8, end=16)
|
||||
expected = 8 * 3600 # 8 hours in seconds
|
||||
assert len(session) == expected
|
||||
|
||||
def test_len_partial_hours(self):
|
||||
"""Test __len__ with partial hours."""
|
||||
session = Session(start=time(8, 30), end=time(16, 45))
|
||||
expected = 8 * 3600 + 15 * 60 # 8 hours 15 minutes
|
||||
assert len(session) == expected
|
||||
|
||||
|
||||
class TestSessionDuration:
|
||||
"""Test Session duration method."""
|
||||
|
||||
def test_duration_returns_duration_tuple(self):
|
||||
"""Test duration returns Duration NamedTuple."""
|
||||
session = Session(start=8, end=16)
|
||||
result = session.duration()
|
||||
assert isinstance(result, Duration)
|
||||
|
||||
def test_duration_values(self):
|
||||
"""Test duration returns correct values."""
|
||||
session = Session(start=8, end=16)
|
||||
result = session.duration()
|
||||
assert result.hours == 8
|
||||
assert result.minutes == 0
|
||||
assert result.seconds == 0
|
||||
|
||||
def test_duration_with_partial_hours(self):
|
||||
"""Test duration with non-full hours."""
|
||||
session = Session(start=time(8, 0), end=time(10, 30, 45))
|
||||
result = session.duration()
|
||||
assert result.hours == 2
|
||||
assert result.minutes == 30
|
||||
assert result.seconds == 45
|
||||
|
||||
|
||||
class TestSessionInSession:
|
||||
"""Test Session in_session method."""
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_in_session_live_mode(self, mock_config):
|
||||
"""Test in_session in live mode."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
# Test depends on current time, just verify it runs
|
||||
session = Session(start=0, end=23)
|
||||
result = session.in_session()
|
||||
assert isinstance(result, bool)
|
||||
|
||||
|
||||
class TestSessionActions:
|
||||
"""Test Session action methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def session(self):
|
||||
"""Create a session for testing."""
|
||||
return Session(start=8, end=16)
|
||||
|
||||
def test_begin_calls_action(self, session):
|
||||
"""Test begin calls action with on_start."""
|
||||
session.on_start = "close_all"
|
||||
session.close_all = MagicMock()
|
||||
session.begin()
|
||||
session.close_all.assert_called_once()
|
||||
|
||||
def test_close_calls_action(self, session):
|
||||
"""Test close calls action with on_end."""
|
||||
session.on_end = "close_loss"
|
||||
session.close_loss = MagicMock()
|
||||
session.close()
|
||||
session.close_loss.assert_called_once()
|
||||
|
||||
def test_action_close_all(self, session):
|
||||
"""Test action dispatches to close_all."""
|
||||
session.close_all = MagicMock()
|
||||
session.action(action="close_all")
|
||||
session.close_all.assert_called_once()
|
||||
|
||||
def test_action_close_win(self, session):
|
||||
"""Test action dispatches to close_win."""
|
||||
session.close_win = MagicMock()
|
||||
session.action(action="close_win")
|
||||
session.close_win.assert_called_once()
|
||||
|
||||
def test_action_close_loss(self, session):
|
||||
"""Test action dispatches to close_loss."""
|
||||
session.close_loss = MagicMock()
|
||||
session.action(action="close_loss")
|
||||
session.close_loss.assert_called_once()
|
||||
|
||||
def test_action_custom_start(self, session):
|
||||
"""Test action calls custom_start."""
|
||||
session.custom_start = MagicMock()
|
||||
session.action(action="custom_start")
|
||||
session.custom_start.assert_called_once()
|
||||
|
||||
def test_action_custom_end(self, session):
|
||||
"""Test action calls custom_end."""
|
||||
session.custom_end = MagicMock()
|
||||
session.action(action="custom_end")
|
||||
session.custom_end.assert_called_once()
|
||||
|
||||
def test_action_none_does_nothing(self, session):
|
||||
"""Test action with None does nothing."""
|
||||
# Should not raise
|
||||
session.action(action=None)
|
||||
|
||||
def test_action_handles_exception(self, session):
|
||||
"""Test action handles exceptions gracefully."""
|
||||
session.close_all = MagicMock(side_effect=Exception("Test error"))
|
||||
# Should not raise, just log warning
|
||||
session.action(action="close_all")
|
||||
|
||||
|
||||
class TestSessionClosePositions:
|
||||
"""Test Session position closing methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def session(self):
|
||||
"""Create a session for testing."""
|
||||
return Session(start=8, end=16)
|
||||
|
||||
def test_close_positions(self, session):
|
||||
"""Test close_positions calls positions manager."""
|
||||
position = MagicMock(spec=TradePosition)
|
||||
result = MagicMock(spec=OrderSendResult)
|
||||
result.retcode = 10009
|
||||
|
||||
session.positions_manager.close_position = MagicMock(return_value=result)
|
||||
session.close_positions(positions=(position,))
|
||||
session.positions_manager.close_position.assert_called_once_with(position=position)
|
||||
|
||||
def test_close_all(self, session):
|
||||
"""Test close_all gets and closes all positions."""
|
||||
positions = (MagicMock(spec=TradePosition),)
|
||||
session.positions_manager.get_positions = MagicMock(return_value=positions)
|
||||
session.close_positions = MagicMock()
|
||||
|
||||
session.close_all()
|
||||
session.positions_manager.get_positions.assert_called_once()
|
||||
session.close_positions.assert_called_once_with(positions=positions)
|
||||
|
||||
def test_close_win_filters_profit(self, session):
|
||||
"""Test close_win only closes profitable positions."""
|
||||
win_pos = MagicMock(spec=TradePosition)
|
||||
win_pos.profit = 100
|
||||
loss_pos = MagicMock(spec=TradePosition)
|
||||
loss_pos.profit = -50
|
||||
|
||||
session.positions_manager.get_positions = MagicMock(return_value=(win_pos, loss_pos))
|
||||
session.close_positions = MagicMock()
|
||||
|
||||
session.close_win()
|
||||
session.close_positions.assert_called_once()
|
||||
closed_positions = session.close_positions.call_args[1]["positions"]
|
||||
assert win_pos in closed_positions
|
||||
assert loss_pos not in closed_positions
|
||||
|
||||
def test_close_loss_filters_loss(self, session):
|
||||
"""Test close_loss only closes losing positions."""
|
||||
win_pos = MagicMock(spec=TradePosition)
|
||||
win_pos.profit = 100
|
||||
loss_pos = MagicMock(spec=TradePosition)
|
||||
loss_pos.profit = -50
|
||||
|
||||
session.positions_manager.get_positions = MagicMock(return_value=(win_pos, loss_pos))
|
||||
session.close_positions = MagicMock()
|
||||
|
||||
session.close_loss()
|
||||
session.close_positions.assert_called_once()
|
||||
closed_positions = session.close_positions.call_args[1]["positions"]
|
||||
assert loss_pos in closed_positions
|
||||
assert win_pos not in closed_positions
|
||||
|
||||
|
||||
class TestSessionUntil:
|
||||
"""Test Session until method."""
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_until_returns_seconds(self, mock_config):
|
||||
"""Test until returns seconds until session start."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
session = Session(start=23, end=0) # Future session
|
||||
result = session.until()
|
||||
assert isinstance(result, int)
|
||||
assert result >= 0
|
||||
|
||||
|
||||
class TestSessionsInitialization:
|
||||
"""Test Sessions class initialization."""
|
||||
|
||||
def test_init_with_sessions(self):
|
||||
"""Test Sessions init with list of Session objects."""
|
||||
s1 = Session(start=8, end=12)
|
||||
s2 = Session(start=13, end=17)
|
||||
sessions = Sessions(sessions=[s1, s2])
|
||||
|
||||
assert len(sessions.sessions) == 2
|
||||
assert sessions.current_session is None
|
||||
|
||||
def test_init_sorts_sessions(self):
|
||||
"""Test Sessions sorts by start time."""
|
||||
s1 = Session(start=13, end=17)
|
||||
s2 = Session(start=8, end=12)
|
||||
sessions = Sessions(sessions=[s1, s2])
|
||||
|
||||
assert sessions.sessions[0].start.hour == 8
|
||||
assert sessions.sessions[1].start.hour == 13
|
||||
|
||||
def test_init_creates_config(self):
|
||||
"""Test Sessions creates config."""
|
||||
s1 = Session(start=8, end=12)
|
||||
sessions = Sessions(sessions=[s1])
|
||||
assert isinstance(sessions.config, Config)
|
||||
|
||||
|
||||
class TestSessionsFind:
|
||||
"""Test Sessions find method."""
|
||||
|
||||
@pytest.fixture
|
||||
def sessions(self):
|
||||
"""Create Sessions for testing."""
|
||||
s1 = Session(start=8, end=12)
|
||||
s2 = Session(start=13, end=17)
|
||||
return Sessions(sessions=[s1, s2])
|
||||
|
||||
def test_find_returns_session(self, sessions):
|
||||
"""Test find returns matching session."""
|
||||
result = sessions.find(moment=time(10, 0))
|
||||
assert result is not None
|
||||
assert result.start.hour == 8
|
||||
|
||||
def test_find_returns_none_when_not_found(self, sessions):
|
||||
"""Test find returns None when no match."""
|
||||
result = sessions.find(moment=time(12, 30))
|
||||
assert result is None
|
||||
|
||||
def test_find_second_session(self, sessions):
|
||||
"""Test find can find second session."""
|
||||
result = sessions.find(moment=time(15, 0))
|
||||
assert result is not None
|
||||
assert result.start.hour == 13
|
||||
|
||||
|
||||
class TestSessionsFindNext:
|
||||
"""Test Sessions find_next method."""
|
||||
|
||||
@pytest.fixture
|
||||
def sessions(self):
|
||||
"""Create Sessions for testing."""
|
||||
s1 = Session(start=8, end=12)
|
||||
s2 = Session(start=13, end=17)
|
||||
return Sessions(sessions=[s1, s2])
|
||||
|
||||
def test_find_next_returns_next_session(self, sessions):
|
||||
"""Test find_next returns next session."""
|
||||
result = sessions.find_next(moment=time(7, 0))
|
||||
assert result.start.hour == 8
|
||||
|
||||
def test_find_next_between_sessions(self, sessions):
|
||||
"""Test find_next when between sessions."""
|
||||
result = sessions.find_next(moment=time(12, 30))
|
||||
assert result.start.hour == 13
|
||||
|
||||
def test_find_next_wraps_to_first(self, sessions):
|
||||
"""Test find_next wraps to first session at end of day."""
|
||||
result = sessions.find_next(moment=time(18, 0))
|
||||
assert result.start.hour == 8
|
||||
|
||||
|
||||
class TestSessionsContains:
|
||||
"""Test Sessions __contains__ method."""
|
||||
|
||||
@pytest.fixture
|
||||
def sessions(self):
|
||||
"""Create Sessions for testing."""
|
||||
s1 = Session(start=8, end=12)
|
||||
s2 = Session(start=13, end=17)
|
||||
return Sessions(sessions=[s1, s2])
|
||||
|
||||
def test_contains_time_in_session(self, sessions):
|
||||
"""Test time within any session returns True."""
|
||||
assert time(10, 0) in sessions
|
||||
|
||||
def test_contains_time_between_sessions(self, sessions):
|
||||
"""Test time between sessions returns False."""
|
||||
assert time(12, 30) not in sessions
|
||||
|
||||
def test_contains_time_outside_sessions(self, sessions):
|
||||
"""Test time outside all sessions returns False."""
|
||||
assert time(18, 0) not in sessions
|
||||
|
||||
|
||||
class TestSessionsContextManager:
|
||||
"""Test Sessions sync context manager."""
|
||||
|
||||
@pytest.fixture
|
||||
def sessions(self):
|
||||
"""Create Sessions for testing."""
|
||||
s1 = Session(start=0, end=23) # All day session
|
||||
return Sessions(sessions=[s1])
|
||||
|
||||
def test_enter_calls_check(self, sessions):
|
||||
"""Test __enter__ calls check."""
|
||||
sessions.check = MagicMock()
|
||||
with sessions:
|
||||
sessions.check.assert_called_once()
|
||||
|
||||
def test_exit_closes_session(self, sessions):
|
||||
"""Test __exit__ closes current session."""
|
||||
sessions.check = MagicMock()
|
||||
mock_session = MagicMock()
|
||||
|
||||
with sessions:
|
||||
sessions.current_session = mock_session
|
||||
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
class TestSessionsCheck:
|
||||
"""Test Sessions check method."""
|
||||
|
||||
@pytest.fixture
|
||||
def sessions(self):
|
||||
"""Create Sessions for testing."""
|
||||
s1 = Session(start=8, end=12)
|
||||
s2 = Session(start=13, end=17)
|
||||
return Sessions(sessions=[s1, s2])
|
||||
|
||||
def test_check_returns_if_in_session(self, sessions):
|
||||
"""Test check returns early if already in session."""
|
||||
mock_session = MagicMock()
|
||||
mock_session.in_session.return_value = True
|
||||
sessions.current_session = mock_session
|
||||
|
||||
sessions.check()
|
||||
# Should return without changing current_session
|
||||
assert sessions.current_session == mock_session
|
||||
|
||||
def test_check_starts_new_session(self, sessions):
|
||||
"""Test check starts new session when found."""
|
||||
sessions.find = MagicMock(return_value=sessions.sessions[0])
|
||||
sessions.sessions[0].begin = MagicMock()
|
||||
|
||||
sessions.check()
|
||||
assert sessions.current_session == sessions.sessions[0]
|
||||
sessions.sessions[0].begin.assert_called_once()
|
||||
|
||||
def test_check_transitions_session(self, sessions):
|
||||
"""Test check handles session transition."""
|
||||
old_session = MagicMock()
|
||||
old_session.in_session.return_value = False
|
||||
old_session.close = MagicMock()
|
||||
sessions.current_session = old_session
|
||||
|
||||
new_session = sessions.sessions[0]
|
||||
new_session.begin = MagicMock()
|
||||
sessions.find = MagicMock(return_value=new_session)
|
||||
|
||||
sessions.check()
|
||||
old_session.close.assert_called_once()
|
||||
assert sessions.current_session == new_session
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for Sessions."""
|
||||
|
||||
def test_create_multiple_sessions(self):
|
||||
"""Test creating multiple sessions."""
|
||||
morning = Session(start=8, end=12, name="Morning", on_end="close_loss")
|
||||
afternoon = Session(start=13, end=17, name="Afternoon", on_end="close_all")
|
||||
evening = Session(start=18, end=22, name="Evening")
|
||||
|
||||
sessions = Sessions(sessions=[morning, afternoon, evening])
|
||||
|
||||
assert len(sessions.sessions) == 3
|
||||
assert sessions.sessions[0].name == "Morning"
|
||||
assert sessions.sessions[1].name == "Afternoon"
|
||||
assert sessions.sessions[2].name == "Evening"
|
||||
|
||||
def test_session_duration_calculations(self):
|
||||
"""Test session duration calculations are correct."""
|
||||
session = Session(start=time(9, 30), end=time(16, 45))
|
||||
duration = session.duration()
|
||||
|
||||
assert duration.hours == 7
|
||||
assert duration.minutes == 15
|
||||
assert duration.seconds == 0
|
||||
|
||||
def test_custom_action_functions(self):
|
||||
"""Test custom action functions work."""
|
||||
called = {"start": False, "end": False}
|
||||
|
||||
def on_start():
|
||||
called["start"] = True
|
||||
|
||||
def on_end():
|
||||
called["end"] = True
|
||||
|
||||
session = Session(
|
||||
start=8, end=16,
|
||||
on_start="custom_start", on_end="custom_end",
|
||||
custom_start=on_start, custom_end=on_end
|
||||
)
|
||||
|
||||
session.begin()
|
||||
session.close()
|
||||
|
||||
assert called["start"] is True
|
||||
assert called["end"] is True
|
||||
@@ -0,0 +1,779 @@
|
||||
"""Comprehensive tests for the sync Strategy module.
|
||||
|
||||
Tests cover (excluding backtest-related methods):
|
||||
- Strategy initialization and attributes
|
||||
- Strategy __repr__ method
|
||||
- Strategy __getattr__ and __setattr__ for parameter access
|
||||
- Strategy __enter__ and __exit__ context manager
|
||||
- Strategy initialize method
|
||||
- Strategy live_sleep static method
|
||||
- Strategy sleep method in live mode
|
||||
- Strategy delay method in live mode
|
||||
- Strategy live_strategy method
|
||||
- Strategy trade method (abstract)
|
||||
- Integration tests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import time as dtime
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.sync.strategy import Strategy
|
||||
from aiomql.lib.sync.sessions import Session, Sessions
|
||||
from aiomql.lib.sync.symbol import Symbol
|
||||
from aiomql.core.config import Config
|
||||
from aiomql.core.meta_trader import MetaTrader
|
||||
from aiomql.core.exceptions import StopTrading
|
||||
|
||||
|
||||
class ConcreteStrategy(Strategy):
|
||||
"""Concrete implementation of Strategy for testing."""
|
||||
|
||||
name = "TestStrategy"
|
||||
|
||||
def trade(self):
|
||||
"""Implement abstract trade method."""
|
||||
pass
|
||||
|
||||
|
||||
class CountingStrategy(Strategy):
|
||||
"""Strategy that counts trade calls for testing."""
|
||||
|
||||
def __init__(self, *args, max_trades: int = 3, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.trade_count = 0
|
||||
self.max_trades = max_trades
|
||||
|
||||
def trade(self):
|
||||
self.trade_count += 1
|
||||
if self.trade_count >= self.max_trades:
|
||||
self.running = False
|
||||
|
||||
|
||||
class ErrorStrategy(Strategy):
|
||||
"""Strategy that raises an error in trade."""
|
||||
|
||||
def trade(self):
|
||||
raise Exception("Test error in trade")
|
||||
|
||||
|
||||
class StopTradingStrategy(Strategy):
|
||||
"""Strategy that raises StopTrading exception."""
|
||||
|
||||
def trade(self):
|
||||
raise StopTrading("Stop trading requested")
|
||||
|
||||
|
||||
class TestStrategyInitialization:
|
||||
"""Test Strategy class initialization."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_with_symbol_only(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init with only symbol."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
assert strategy.symbol == mock_symbol
|
||||
assert strategy.name == "ConcreteStrategy" # Class name used
|
||||
assert strategy.running is True
|
||||
assert "symbol" in strategy.parameters
|
||||
assert strategy.parameters["symbol"] == "EURUSD"
|
||||
assert "name" in strategy.parameters
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_with_custom_name(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init with custom name."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol, name="MyCustomStrategy")
|
||||
|
||||
assert strategy.name == "MyCustomStrategy"
|
||||
assert strategy.parameters["name"] == "MyCustomStrategy"
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_with_params(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init with custom parameters."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
params = {"risk_percent": 0.02, "take_profit_pips": 50}
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol, params=params)
|
||||
|
||||
assert strategy.parameters["risk_percent"] == 0.02
|
||||
assert strategy.parameters["take_profit_pips"] == 50
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_with_sessions(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init with custom sessions."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
sessions = Sessions(sessions=[Session(start=8, end=16)])
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol, sessions=sessions)
|
||||
|
||||
assert strategy.sessions == sessions
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_default_sessions(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init creates default sessions."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
assert strategy.sessions is not None
|
||||
assert isinstance(strategy.sessions, Sessions)
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_creates_config(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init creates config."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
assert strategy.config is not None
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_creates_meta_trader_in_live_mode(self, mock_config, mock_symbol):
|
||||
"""Test Strategy init creates MetaTrader in live mode."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
assert isinstance(strategy.mt5, MetaTrader)
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_init_class_parameters_merged(self, mock_config, mock_symbol):
|
||||
"""Test class-level parameters are merged with instance params."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
class StrategyWithDefaults(Strategy):
|
||||
parameters = {"default_sl": 50, "default_tp": 100}
|
||||
|
||||
def trade(self):
|
||||
pass
|
||||
|
||||
strategy = StrategyWithDefaults(
|
||||
symbol=mock_symbol, params={"custom_param": "value"}
|
||||
)
|
||||
|
||||
assert strategy.parameters["default_sl"] == 50
|
||||
assert strategy.parameters["default_tp"] == 100
|
||||
assert strategy.parameters["custom_param"] == "value"
|
||||
|
||||
|
||||
class TestStrategyRepr:
|
||||
"""Test Strategy __repr__ method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
symbol.__repr__ = MagicMock(return_value="Symbol(EURUSD)")
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_repr(self, mock_config, mock_symbol):
|
||||
"""Test __repr__ returns formatted string."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
result = repr(strategy)
|
||||
|
||||
assert "ConcreteStrategy" in result
|
||||
assert "Symbol(EURUSD)" in result
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_repr_with_custom_name(self, mock_config, mock_symbol):
|
||||
"""Test __repr__ with custom strategy name."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol, name="MyStrategy")
|
||||
result = repr(strategy)
|
||||
|
||||
assert "MyStrategy" in result
|
||||
|
||||
|
||||
class TestStrategyGetSetAttr:
|
||||
"""Test Strategy __getattr__ and __setattr__ methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_getattr_returns_parameter(self, mock_config, mock_symbol):
|
||||
"""Test __getattr__ returns parameter value."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(
|
||||
symbol=mock_symbol, params={"risk_percent": 0.02}
|
||||
)
|
||||
|
||||
assert strategy.risk_percent == 0.02
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_getattr_raises_for_missing(self, mock_config, mock_symbol):
|
||||
"""Test __getattr__ raises AttributeError for missing attribute."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
with pytest.raises(AttributeError) as exc_info:
|
||||
_ = strategy.nonexistent_attribute
|
||||
|
||||
assert "nonexistent_attribute" in str(exc_info.value)
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_setattr_updates_parameter(self, mock_config, mock_symbol):
|
||||
"""Test __setattr__ updates parameter value."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(
|
||||
symbol=mock_symbol, params={"risk_percent": 0.02}
|
||||
)
|
||||
strategy.risk_percent = 0.05
|
||||
|
||||
assert strategy.parameters["risk_percent"] == 0.05
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_setattr_regular_attribute(self, mock_config, mock_symbol):
|
||||
"""Test __setattr__ works for regular attributes."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.running = False
|
||||
|
||||
assert strategy.running is False
|
||||
|
||||
|
||||
class TestStrategyContextManager:
|
||||
"""Test Strategy sync context manager."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_enter_checks_session(self, mock_config, mock_symbol):
|
||||
"""Test __enter__ calls sessions.check."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = MagicMock()
|
||||
|
||||
strategy.__enter__()
|
||||
|
||||
strategy.sessions.check.assert_called_once()
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_enter_sets_running_true(self, mock_config, mock_symbol):
|
||||
"""Test __enter__ sets running to True."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.running = False
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = MagicMock()
|
||||
|
||||
strategy.__enter__()
|
||||
|
||||
assert strategy.running is True
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_enter_sets_current_session(self, mock_config, mock_symbol):
|
||||
"""Test __enter__ sets current_session."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
mock_session = MagicMock()
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = mock_session
|
||||
|
||||
strategy.__enter__()
|
||||
|
||||
assert strategy.current_session == mock_session
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_exit_closes_session(self, mock_config, mock_symbol):
|
||||
"""Test __exit__ closes current session."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
mock_session = MagicMock()
|
||||
mock_session.close = MagicMock()
|
||||
strategy.current_session = mock_session
|
||||
|
||||
strategy.__exit__(None, None, None)
|
||||
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_exit_sets_running_false(self, mock_config, mock_symbol):
|
||||
"""Test __exit__ sets running to False."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.running = True
|
||||
strategy.current_session = MagicMock()
|
||||
strategy.current_session.close = MagicMock()
|
||||
|
||||
strategy.__exit__(None, None, None)
|
||||
|
||||
assert strategy.running is False
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_exit_handles_no_session(self, mock_config, mock_symbol):
|
||||
"""Test __exit__ handles no current session."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.current_session = None
|
||||
|
||||
# Should not raise
|
||||
strategy.__exit__(None, None, None)
|
||||
assert strategy.running is False
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_exit_handles_exception(self, mock_config, mock_symbol):
|
||||
"""Test __exit__ handles exception in close."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
mock_session = MagicMock()
|
||||
mock_session.close = MagicMock(side_effect=Exception("Close error"))
|
||||
strategy.current_session = mock_session
|
||||
|
||||
# Should not raise, just log
|
||||
strategy.__exit__(None, None, None)
|
||||
|
||||
|
||||
class TestStrategyInitialize:
|
||||
"""Test Strategy initialize method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
symbol.initialize_sync = MagicMock(return_value=True)
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_initialize_calls_symbol_initialize_sync(self, mock_config, mock_symbol):
|
||||
"""Test initialize calls symbol.initialize_sync."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
result = strategy.initialize()
|
||||
|
||||
mock_symbol.initialize_sync.assert_called_once()
|
||||
assert result is True
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_initialize_returns_symbol_result(self, mock_config, mock_symbol):
|
||||
"""Test initialize returns symbol.initialize_sync result."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
mock_symbol.initialize_sync = MagicMock(return_value=False)
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
result = strategy.initialize()
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestStrategyLiveSleep:
|
||||
"""Test Strategy live_sleep static method."""
|
||||
|
||||
def test_live_sleep_sleeps_remaining_time(self):
|
||||
"""Test live_sleep calculates correct sleep time."""
|
||||
with patch('time.sleep') as mock_sleep:
|
||||
Strategy.live_sleep(secs=60)
|
||||
|
||||
# Should have been called once
|
||||
mock_sleep.assert_called_once()
|
||||
# Sleep time should be between 0.1 and 60.1
|
||||
call_args = mock_sleep.call_args[0][0]
|
||||
assert 0.1 <= call_args <= 60.1
|
||||
|
||||
def test_live_sleep_short_duration(self):
|
||||
"""Test live_sleep with short duration."""
|
||||
with patch('time.sleep') as mock_sleep:
|
||||
Strategy.live_sleep(secs=1)
|
||||
|
||||
mock_sleep.assert_called_once()
|
||||
call_args = mock_sleep.call_args[0][0]
|
||||
assert call_args >= 0.1
|
||||
|
||||
|
||||
class TestStrategySleep:
|
||||
"""Test Strategy sleep method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_sleep_live_mode(self, mock_config, mock_symbol):
|
||||
"""Test sleep calls live_sleep in live mode."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
with patch.object(Strategy, 'live_sleep') as mock_live_sleep:
|
||||
strategy.sleep(secs=60)
|
||||
mock_live_sleep.assert_called_once_with(secs=60)
|
||||
|
||||
|
||||
class TestStrategyDelay:
|
||||
"""Test Strategy delay method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_delay_live_mode(self, mock_config, mock_symbol):
|
||||
"""Test delay calls time.sleep in live mode."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
|
||||
with patch('time.sleep') as mock_sleep:
|
||||
strategy.delay(secs=5)
|
||||
mock_sleep.assert_called_once_with(5)
|
||||
|
||||
|
||||
class TestStrategyRunStrategy:
|
||||
"""Test Strategy run_strategy method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_run_strategy_live_mode(self, mock_config, mock_symbol):
|
||||
"""Test run_strategy calls live_strategy in live mode."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.live_strategy = MagicMock()
|
||||
|
||||
strategy.run_strategy()
|
||||
|
||||
strategy.live_strategy.assert_called_once()
|
||||
|
||||
|
||||
class TestStrategyLiveStrategy:
|
||||
"""Test Strategy live_strategy method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_live_strategy_runs_trade_loop(self, mock_config, mock_symbol):
|
||||
"""Test live_strategy runs trade in a loop."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = CountingStrategy(symbol=mock_symbol, max_trades=3)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = MagicMock()
|
||||
strategy.sessions.current_session.close = MagicMock()
|
||||
|
||||
strategy.live_strategy()
|
||||
|
||||
assert strategy.trade_count == 3
|
||||
assert strategy.running is False
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_live_strategy_handles_stop_trading(self, mock_config, mock_symbol):
|
||||
"""Test live_strategy handles StopTrading exception."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = StopTradingStrategy(symbol=mock_symbol)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = MagicMock()
|
||||
strategy.sessions.current_session.close = MagicMock()
|
||||
|
||||
strategy.live_strategy()
|
||||
|
||||
assert strategy.running is False
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_live_strategy_handles_general_exception(self, mock_config, mock_symbol):
|
||||
"""Test live_strategy handles and logs general exceptions."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ErrorStrategy(symbol=mock_symbol)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
strategy.sessions.current_session = MagicMock()
|
||||
strategy.sessions.current_session.close = MagicMock()
|
||||
|
||||
strategy.live_strategy()
|
||||
|
||||
assert strategy.running is False
|
||||
|
||||
|
||||
class TestStrategyTrade:
|
||||
"""Test Strategy trade abstract method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_trade_not_implemented(self, mock_config, mock_symbol):
|
||||
"""Test trade raises NotImplementedError in base class."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
# Need to bypass ABC
|
||||
strategy = Strategy.__new__(Strategy)
|
||||
strategy.parameters = {}
|
||||
strategy.symbol = mock_symbol
|
||||
strategy.name = "TestStrategy"
|
||||
strategy.running = True
|
||||
strategy.config = config
|
||||
strategy.mt5 = MagicMock()
|
||||
|
||||
with pytest.raises(NotImplementedError) as exc_info:
|
||||
strategy.trade()
|
||||
|
||||
assert "Implement this method" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestStrategyTest:
|
||||
"""Test Strategy test method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_test_calls_trade(self, mock_config, mock_symbol):
|
||||
"""Test test method calls trade."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.trade = MagicMock()
|
||||
|
||||
strategy.test()
|
||||
|
||||
strategy.trade.assert_called_once()
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for Strategy."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_symbol(self):
|
||||
"""Create a mock Symbol for testing."""
|
||||
symbol = MagicMock(spec=Symbol)
|
||||
symbol.name = "EURUSD"
|
||||
symbol.initialize_sync = MagicMock(return_value=True)
|
||||
return symbol
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_strategy_with_complete_setup(self, mock_config, mock_symbol):
|
||||
"""Test strategy with complete configuration."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
sessions = Sessions(
|
||||
sessions=[
|
||||
Session(start=8, end=12, name="Morning"),
|
||||
Session(start=13, end=17, name="Afternoon"),
|
||||
]
|
||||
)
|
||||
params = {
|
||||
"risk_percent": 0.02,
|
||||
"max_trades": 5,
|
||||
"stop_loss_pips": 30,
|
||||
"take_profit_pips": 60,
|
||||
}
|
||||
|
||||
strategy = ConcreteStrategy(
|
||||
symbol=mock_symbol,
|
||||
params=params,
|
||||
sessions=sessions,
|
||||
name="CompleteStrategy",
|
||||
)
|
||||
|
||||
assert strategy.name == "CompleteStrategy"
|
||||
assert strategy.symbol == mock_symbol
|
||||
assert strategy.risk_percent == 0.02
|
||||
assert strategy.max_trades == 5
|
||||
assert len(strategy.sessions.sessions) == 2
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_strategy_full_lifecycle(self, mock_config, mock_symbol):
|
||||
"""Test strategy through full lifecycle."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = CountingStrategy(symbol=mock_symbol, max_trades=2)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
mock_session = MagicMock()
|
||||
mock_session.close = MagicMock()
|
||||
strategy.sessions.current_session = mock_session
|
||||
|
||||
# Enter context
|
||||
strategy.__enter__()
|
||||
assert strategy.running is True
|
||||
|
||||
# Run trades
|
||||
while strategy.running:
|
||||
strategy.trade()
|
||||
|
||||
# Exit context
|
||||
strategy.__exit__(None, None, None)
|
||||
assert strategy.running is False
|
||||
assert strategy.trade_count == 2
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_parameter_inheritance(self, mock_config, mock_symbol):
|
||||
"""Test parameter inheritance from class to instance."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
class BaseStrategy(Strategy):
|
||||
parameters = {"base_param": "base_value"}
|
||||
|
||||
def trade(self):
|
||||
pass
|
||||
|
||||
class DerivedStrategy(BaseStrategy):
|
||||
parameters = {**BaseStrategy.parameters, "derived_param": "derived_value"}
|
||||
|
||||
strategy = DerivedStrategy(
|
||||
symbol=mock_symbol, params={"instance_param": "instance_value"}
|
||||
)
|
||||
|
||||
assert strategy.parameters["base_param"] == "base_value"
|
||||
assert strategy.parameters["derived_param"] == "derived_value"
|
||||
assert strategy.parameters["instance_param"] == "instance_value"
|
||||
|
||||
@patch.object(Config, '__new__')
|
||||
def test_context_manager_with_statement(self, mock_config, mock_symbol):
|
||||
"""Test strategy works with 'with' statement."""
|
||||
config = MagicMock()
|
||||
config.mode = "live"
|
||||
mock_config.return_value = config
|
||||
|
||||
strategy = ConcreteStrategy(symbol=mock_symbol)
|
||||
strategy.sessions = MagicMock()
|
||||
strategy.sessions.check = MagicMock()
|
||||
mock_session = MagicMock()
|
||||
mock_session.close = MagicMock()
|
||||
strategy.sessions.current_session = mock_session
|
||||
|
||||
with strategy as _:
|
||||
assert strategy.running is True
|
||||
assert strategy.current_session == mock_session
|
||||
|
||||
assert strategy.running is False
|
||||
mock_session.close.assert_called_once()
|
||||
@@ -0,0 +1,815 @@
|
||||
"""Comprehensive tests for the synchronous Trader module.
|
||||
|
||||
Tests cover:
|
||||
- Trader initialization with default and custom values
|
||||
- set_trade_stop_levels_pips method
|
||||
- set_trade_stop_levels_points method
|
||||
- create_order_with_stops method
|
||||
- create_order_with_sl method
|
||||
- create_order_with_points method
|
||||
- create_order_no_stops method
|
||||
- check_order method
|
||||
- send_order method
|
||||
- record_trade method
|
||||
- Integration tests with various order types
|
||||
- Edge cases and boundary conditions
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.ram import RAM
|
||||
from aiomql.lib.sync.trader import Trader
|
||||
from aiomql.contrib.traders.sync import SimpleTrader
|
||||
from aiomql.contrib.symbols.sync import ForexSymbol
|
||||
from aiomql.lib.sync.symbol import Symbol
|
||||
from aiomql.core.constants import OrderType
|
||||
from aiomql.lib.sync.account import Account
|
||||
from aiomql.lib.sync.order import Order
|
||||
from aiomql.core.config import Config
|
||||
from aiomql.core.models import OrderSendResult, OrderCheckResult
|
||||
|
||||
|
||||
class TestTraderInitialization:
|
||||
"""Test Trader class initialization."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol before tests."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_init_with_symbol_only(self):
|
||||
"""Test Trader can be initialized with just a symbol."""
|
||||
trader = SimpleTrader(symbol=self.symbol)
|
||||
assert trader.symbol == self.symbol
|
||||
assert isinstance(trader.ram, RAM)
|
||||
assert isinstance(trader.order, Order)
|
||||
|
||||
def test_init_with_symbol_and_ram(self):
|
||||
"""Test Trader initialized with symbol and custom RAM."""
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=self.ram)
|
||||
assert trader.symbol == self.symbol
|
||||
assert trader.ram == self.ram
|
||||
|
||||
def test_init_creates_order_with_symbol_name(self):
|
||||
"""Test Trader creates order with correct symbol name."""
|
||||
trader = SimpleTrader(symbol=self.symbol)
|
||||
assert trader.order.symbol == self.symbol.name
|
||||
|
||||
def test_init_has_config_attribute(self):
|
||||
"""Test Trader has config attribute."""
|
||||
trader = SimpleTrader(symbol=self.symbol)
|
||||
assert hasattr(trader, 'config')
|
||||
assert isinstance(trader.config, Config)
|
||||
|
||||
def test_init_has_parameters_attribute(self):
|
||||
"""Test Trader has empty parameters dict."""
|
||||
trader = SimpleTrader(symbol=self.symbol)
|
||||
assert hasattr(trader, 'parameters')
|
||||
assert isinstance(trader.parameters, dict)
|
||||
assert trader.parameters == {}
|
||||
|
||||
def test_init_with_default_ram_values(self):
|
||||
"""Test Trader uses default RAM if not provided."""
|
||||
trader = SimpleTrader(symbol=self.symbol)
|
||||
assert trader.ram.risk_to_reward == 2
|
||||
assert trader.ram.risk == 1
|
||||
|
||||
|
||||
class TestSetTradeStopLevelsPips:
|
||||
"""Test set_trade_stop_levels_pips method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="EURUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_set_stop_levels_pips_buy_order(self):
|
||||
"""Test setting stop levels for buy order using pips."""
|
||||
tick = self.symbol.info_tick()
|
||||
self.trader.order.price = tick.ask
|
||||
self.trader.order.type = OrderType.BUY
|
||||
pips = 50
|
||||
|
||||
self.trader.set_trade_stop_levels_pips(pips=pips)
|
||||
|
||||
expected_sl = round(tick.ask - (pips * self.symbol.pip), self.symbol.digits)
|
||||
expected_tp = round(tick.ask + (pips * self.ram.risk_to_reward * self.symbol.pip), self.symbol.digits)
|
||||
assert self.trader.order.sl == expected_sl
|
||||
assert self.trader.order.tp == expected_tp
|
||||
|
||||
def test_set_stop_levels_pips_sell_order(self):
|
||||
"""Test setting stop levels for sell order using pips."""
|
||||
tick = self.symbol.info_tick()
|
||||
self.trader.order.price = tick.bid
|
||||
self.trader.order.type = OrderType.SELL
|
||||
pips = 50
|
||||
|
||||
self.trader.set_trade_stop_levels_pips(pips=pips)
|
||||
|
||||
expected_sl = round(tick.bid + (pips * self.symbol.pip), self.symbol.digits)
|
||||
expected_tp = round(tick.bid - (pips * self.ram.risk_to_reward * self.symbol.pip), self.symbol.digits)
|
||||
assert self.trader.order.sl == expected_sl
|
||||
assert self.trader.order.tp == expected_tp
|
||||
|
||||
def test_set_stop_levels_pips_custom_risk_to_reward(self):
|
||||
"""Test setting stop levels with custom risk to reward ratio."""
|
||||
tick = self.symbol.info_tick()
|
||||
self.trader.order.price = tick.ask
|
||||
self.trader.order.type = OrderType.BUY
|
||||
pips = 30
|
||||
custom_rr = 3
|
||||
|
||||
self.trader.set_trade_stop_levels_pips(pips=pips, risk_to_reward=custom_rr)
|
||||
|
||||
expected_sl = round(tick.ask - (pips * self.symbol.pip), self.symbol.digits)
|
||||
expected_tp = round(tick.ask + (pips * custom_rr * self.symbol.pip), self.symbol.digits)
|
||||
assert self.trader.order.sl == expected_sl
|
||||
assert self.trader.order.tp == expected_tp
|
||||
|
||||
|
||||
class TestSetTradeStopLevelsPoints:
|
||||
"""Test set_trade_stop_levels_points method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="EURUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_set_stop_levels_points_buy_order(self):
|
||||
"""Test setting stop levels for buy order using points."""
|
||||
tick = self.symbol.info_tick()
|
||||
self.trader.order.price = tick.ask
|
||||
self.trader.order.type = OrderType.BUY
|
||||
points = 500
|
||||
|
||||
self.trader.set_trade_stop_levels_points(points=points)
|
||||
|
||||
expected_sl = round(tick.ask - (points * self.symbol.point), self.symbol.digits)
|
||||
expected_tp = round(tick.ask + (points * self.ram.risk_to_reward * self.symbol.point), self.symbol.digits)
|
||||
assert self.trader.order.sl == expected_sl
|
||||
assert self.trader.order.tp == expected_tp
|
||||
|
||||
def test_set_stop_levels_points_custom_risk_to_reward(self):
|
||||
"""Test setting stop levels with custom risk to reward."""
|
||||
tick = self.symbol.info_tick()
|
||||
self.trader.order.price = tick.ask
|
||||
self.trader.order.type = OrderType.BUY
|
||||
points = 500
|
||||
custom_rr = 4
|
||||
|
||||
self.trader.set_trade_stop_levels_points(points=points, risk_to_reward=custom_rr)
|
||||
|
||||
expected_sl = round(tick.ask - (points * self.symbol.point), self.symbol.digits)
|
||||
expected_tp = round(tick.ask + (points * custom_rr * self.symbol.point), self.symbol.digits)
|
||||
assert self.trader.order.sl == expected_sl
|
||||
assert self.trader.order.tp == expected_tp
|
||||
|
||||
|
||||
class TestCreateOrderNoStops:
|
||||
"""Test create_order_no_stops method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_create_order_no_stops_buy(self):
|
||||
"""Test creating buy order without stops."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
|
||||
assert self.trader.order.type == OrderType.BUY
|
||||
assert self.trader.order.volume == self.symbol.volume_min
|
||||
assert self.trader.order.price is not None
|
||||
|
||||
def test_create_order_no_stops_sell(self):
|
||||
"""Test creating sell order without stops."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.SELL)
|
||||
|
||||
assert self.trader.order.type == OrderType.SELL
|
||||
assert self.trader.order.volume == self.symbol.volume_min
|
||||
assert self.trader.order.price is not None
|
||||
|
||||
def test_create_order_no_stops_with_custom_volume(self):
|
||||
"""Test creating order with custom volume."""
|
||||
custom_volume = self.symbol.volume_min * 2
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY, volume=custom_volume)
|
||||
|
||||
assert self.trader.order.volume == custom_volume
|
||||
|
||||
def test_create_order_no_stops_uses_correct_price(self):
|
||||
"""Test order uses ask for buy and bid for sell."""
|
||||
tick = self.symbol.info_tick()
|
||||
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
# Price should be close to ask (may differ slightly due to timing)
|
||||
assert abs(self.trader.order.price - tick.ask) < tick.ask * 0.01
|
||||
|
||||
def test_create_order_no_stops_send_success(self):
|
||||
"""Test sending order without stops succeeds."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.order.send()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
|
||||
class TestCreateOrderWithSl:
|
||||
"""Test create_order_with_sl method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
cls.account = Account()
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol and account."""
|
||||
self.symbol.initialize()
|
||||
self.account.refresh()
|
||||
|
||||
def test_create_order_with_sl_sell(self):
|
||||
"""Test creating sell order with stop loss."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||
|
||||
assert self.trader.order.type == OrderType.SELL
|
||||
assert self.trader.order.sl == sl
|
||||
assert self.trader.order.tp is not None
|
||||
assert self.trader.order.volume > 0
|
||||
|
||||
def test_create_order_with_sl_buy(self):
|
||||
"""Test creating buy order with stop loss."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
|
||||
|
||||
assert self.trader.order.type == OrderType.BUY
|
||||
assert self.trader.order.sl == sl
|
||||
assert self.trader.order.tp is not None
|
||||
|
||||
def test_create_order_with_sl_respects_risk_to_reward(self):
|
||||
"""Test TP is set according to risk to reward ratio."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||
|
||||
# TP should be approximately at dsl * risk_to_reward distance from price
|
||||
expected_dtp = dsl * self.ram.risk_to_reward
|
||||
actual_dtp = abs(self.trader.order.price - self.trader.order.tp)
|
||||
assert abs(actual_dtp - expected_dtp) < self.symbol.point * 10
|
||||
|
||||
def test_create_order_with_sl_custom_amount_to_risk(self):
|
||||
"""Test creating order with custom amount to risk."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
custom_amount = 20
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl, amount_to_risk=custom_amount)
|
||||
|
||||
assert self.trader.order.volume > 0
|
||||
|
||||
def test_create_order_with_sl_send_success(self):
|
||||
"""Test order with SL can be sent successfully."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||
result = self.trader.order.send()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
|
||||
class TestCreateOrderWithStops:
|
||||
"""Test create_order_with_stops method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
cls.account = Account()
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol and account."""
|
||||
self.symbol.initialize()
|
||||
self.account.refresh()
|
||||
|
||||
def test_create_order_with_stops_buy(self):
|
||||
"""Test creating buy order with SL and TP."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
dtp = dsl * self.ram.risk_to_reward
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
tp = tick.ask + dtp
|
||||
|
||||
self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
|
||||
|
||||
assert self.trader.order.type == OrderType.BUY
|
||||
assert self.trader.order.sl == sl
|
||||
assert self.trader.order.tp == tp
|
||||
assert self.trader.order.volume > 0
|
||||
|
||||
def test_create_order_with_stops_sell(self):
|
||||
"""Test creating sell order with SL and TP."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
dtp = dsl * self.ram.risk_to_reward
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
tp = tick.bid - dtp
|
||||
|
||||
self.trader.create_order_with_stops(order_type=OrderType.SELL, sl=sl, tp=tp)
|
||||
|
||||
assert self.trader.order.type == OrderType.SELL
|
||||
assert self.trader.order.sl == sl
|
||||
assert self.trader.order.tp == tp
|
||||
|
||||
def test_create_order_with_stops_send_success(self):
|
||||
"""Test order with stops can be sent successfully."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
dtp = dsl * self.ram.risk_to_reward
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
tp = tick.ask + dtp
|
||||
|
||||
self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
|
||||
result = self.trader.order.send()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_create_order_with_stops_custom_amount(self):
|
||||
"""Test creating order with custom amount to risk."""
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
dtp = dsl * 2
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
tp = tick.ask + dtp
|
||||
custom_amount = 25
|
||||
|
||||
self.trader.create_order_with_stops(
|
||||
order_type=OrderType.BUY, sl=sl, tp=tp, amount_to_risk=custom_amount
|
||||
)
|
||||
|
||||
assert self.trader.order.volume > 0
|
||||
|
||||
|
||||
class TestCreateOrderWithPoints:
|
||||
"""Test create_order_with_points method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
cls.account = Account()
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
self.account.refresh()
|
||||
|
||||
def test_create_order_with_points_buy(self):
|
||||
"""Test creating buy order with points."""
|
||||
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
|
||||
|
||||
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
|
||||
|
||||
assert self.trader.order.type == OrderType.BUY
|
||||
assert self.trader.order.volume > 0
|
||||
assert self.trader.order.sl is not None
|
||||
assert self.trader.order.tp is not None
|
||||
|
||||
def test_create_order_with_points_sell(self):
|
||||
"""Test creating sell order with points."""
|
||||
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
|
||||
|
||||
self.trader.create_order_with_points(order_type=OrderType.SELL, points=points)
|
||||
|
||||
assert self.trader.order.type == OrderType.SELL
|
||||
assert self.trader.order.volume > 0
|
||||
|
||||
def test_create_order_with_points_send_success(self):
|
||||
"""Test order with points can be sent successfully."""
|
||||
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
|
||||
|
||||
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
|
||||
result = self.trader.order.send()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_create_order_with_points_custom_risk_to_reward(self):
|
||||
"""Test order with custom risk to reward."""
|
||||
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
|
||||
custom_rr = 3
|
||||
|
||||
self.trader.create_order_with_points(
|
||||
order_type=OrderType.BUY, points=points, risk_to_reward=custom_rr
|
||||
)
|
||||
|
||||
# TP should be at points * custom_rr distance from price
|
||||
expected_tp_distance = points * custom_rr * self.symbol.point
|
||||
actual_tp_distance = abs(self.trader.order.tp - self.trader.order.price)
|
||||
assert abs(actual_tp_distance - expected_tp_distance) < self.symbol.point * 10
|
||||
|
||||
|
||||
class TestCheckOrder:
|
||||
"""Test check_order method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_check_order_returns_order_check_result(self):
|
||||
"""Test check_order returns OrderCheckResult."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.check_order()
|
||||
|
||||
assert result is None or isinstance(result, OrderCheckResult)
|
||||
|
||||
def test_check_order_success(self):
|
||||
"""Test check_order succeeds for valid order."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.check_order()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 0
|
||||
|
||||
def test_check_order_has_margin_info(self):
|
||||
"""Test check result contains margin information."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.check_order()
|
||||
|
||||
assert result is not None
|
||||
assert hasattr(result, 'margin')
|
||||
|
||||
def test_check_order_sell(self):
|
||||
"""Test check_order works for sell orders."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.SELL)
|
||||
result = self.trader.check_order()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 0
|
||||
|
||||
|
||||
class TestSendOrder:
|
||||
"""Test send_order method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_send_order_returns_order_send_result(self):
|
||||
"""Test send_order returns OrderSendResult."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
assert result is None or isinstance(result, OrderSendResult)
|
||||
|
||||
def test_send_order_success(self):
|
||||
"""Test send_order succeeds for valid order."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_send_order_has_deal_ticket(self):
|
||||
"""Test send result contains deal ticket."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
assert result is not None
|
||||
assert hasattr(result, 'deal')
|
||||
assert result.deal > 0
|
||||
|
||||
def test_send_order_has_order_ticket(self):
|
||||
"""Test send result contains order ticket."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
assert result is not None
|
||||
assert hasattr(result, 'order')
|
||||
assert result.order > 0
|
||||
|
||||
|
||||
class TestRecordTrade:
|
||||
"""Test record_trade method."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_record_trade_with_successful_order(self):
|
||||
"""Test recording a successful trade."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
# Should not raise an error
|
||||
self.trader.record_trade(result=result, parameters={"test": "value"}, name="TestStrategy", use_task_queue=False)
|
||||
|
||||
def test_record_trade_with_parameters(self):
|
||||
"""Test recording trade with custom parameters."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
params = {"strategy": "test", "risk": 1, "timeframe": "H1"}
|
||||
self.trader.record_trade(result=result, parameters=params, name="MyStrategy", use_task_queue=False)
|
||||
|
||||
def test_record_trade_without_parameters(self):
|
||||
"""Test recording trade without parameters."""
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = self.trader.send_order()
|
||||
|
||||
# Should not raise an error
|
||||
self.trader.record_trade(result=result, name="SimpleStrategy", use_task_queue=False)
|
||||
|
||||
|
||||
class TestTraderWithDifferentSymbols:
|
||||
"""Test Trader with different symbols."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.btc_usd = ForexSymbol(name="BTCUSD")
|
||||
cls.eur_jpy = ForexSymbol(name="EURJPY")
|
||||
cls.ram = RAM(fixed_amount=10)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbols."""
|
||||
self.btc_usd.initialize()
|
||||
self.eur_jpy.initialize()
|
||||
|
||||
def test_trader_btc_usd(self):
|
||||
"""Test trader with BTCUSD symbol."""
|
||||
trader = SimpleTrader(symbol=self.btc_usd, ram=self.ram)
|
||||
trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result = trader.send_order()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
def test_trader_eur_jpy(self):
|
||||
"""Test trader with EURJPY symbol."""
|
||||
trader = SimpleTrader(symbol=self.eur_jpy, ram=self.ram)
|
||||
trader.create_order_no_stops(order_type=OrderType.SELL)
|
||||
result = trader.send_order()
|
||||
|
||||
assert result is not None
|
||||
assert result.retcode == 10009
|
||||
|
||||
|
||||
class TestTraderIntegration:
|
||||
"""Integration tests for Trader."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
|
||||
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
|
||||
cls.account = Account()
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol and account."""
|
||||
self.symbol.initialize()
|
||||
self.account.refresh()
|
||||
|
||||
def test_full_trade_flow_buy(self):
|
||||
"""Test complete trade flow for buy order."""
|
||||
# Create order
|
||||
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
|
||||
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
|
||||
|
||||
# Check order
|
||||
check_result = self.trader.check_order()
|
||||
assert check_result is not None
|
||||
assert check_result.retcode == 0
|
||||
|
||||
# Send order
|
||||
send_result = self.trader.send_order()
|
||||
assert send_result is not None
|
||||
assert send_result.retcode == 10009
|
||||
|
||||
# Record trade
|
||||
self.trader.record_trade(result=send_result, parameters={"test": True}, name="IntegrationTest", use_task_queue=False)
|
||||
|
||||
def test_full_trade_flow_sell(self):
|
||||
"""Test complete trade flow for sell order."""
|
||||
# Create order
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = self.symbol.info_tick()
|
||||
sl = tick.bid + dsl
|
||||
|
||||
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||
|
||||
# Check order
|
||||
check_result = self.trader.check_order()
|
||||
assert check_result is not None
|
||||
assert check_result.retcode == 0
|
||||
|
||||
# Send order
|
||||
send_result = self.trader.send_order()
|
||||
assert send_result is not None
|
||||
assert send_result.retcode == 10009
|
||||
|
||||
def test_multiple_orders_same_trader(self):
|
||||
"""Test creating multiple orders with same trader."""
|
||||
# First order
|
||||
self.trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
result1 = self.trader.send_order()
|
||||
assert result1 is not None
|
||||
assert result1.retcode == 10009
|
||||
|
||||
# Second order (different type)
|
||||
self.trader.create_order_no_stops(order_type=OrderType.SELL)
|
||||
result2 = self.trader.send_order()
|
||||
assert result2 is not None
|
||||
assert result2.retcode == 10009
|
||||
|
||||
|
||||
class TestTraderEdgeCases:
|
||||
"""Test edge cases and boundary conditions."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_trader_with_zero_fixed_amount(self):
|
||||
"""Test trader with zero fixed amount RAM."""
|
||||
ram = RAM(fixed_amount=0)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
assert trader.ram.fixed_amount == 0
|
||||
|
||||
def test_trader_with_high_risk_to_reward(self):
|
||||
"""Test trader with high risk to reward ratio."""
|
||||
ram = RAM(fixed_amount=10, risk_to_reward=10)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
assert trader.ram.risk_to_reward == 10
|
||||
|
||||
def test_trader_with_low_risk_to_reward(self):
|
||||
"""Test trader with low risk to reward ratio."""
|
||||
ram = RAM(fixed_amount=10, risk_to_reward=0.5)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
assert trader.ram.risk_to_reward == 0.5
|
||||
|
||||
def test_trader_order_modification_after_creation(self):
|
||||
"""Test modifying order attributes after creation."""
|
||||
ram = RAM(fixed_amount=10)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
|
||||
original_volume = trader.order.volume
|
||||
trader.order.volume = original_volume * 2
|
||||
assert trader.order.volume == original_volume * 2
|
||||
|
||||
def test_trader_parameters_modification(self):
|
||||
"""Test modifying trader parameters."""
|
||||
ram = RAM(fixed_amount=10)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
|
||||
trader.parameters["custom_param"] = "value"
|
||||
trader.parameters["risk"] = 5
|
||||
|
||||
assert trader.parameters["custom_param"] == "value"
|
||||
assert trader.parameters["risk"] == 5
|
||||
|
||||
def test_trader_with_minimum_volume(self):
|
||||
"""Test creating order with minimum volume."""
|
||||
ram = RAM(fixed_amount=1) # Very small amount
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
trader.create_order_no_stops(order_type=OrderType.BUY)
|
||||
|
||||
assert trader.order.volume >= self.symbol.volume_min
|
||||
|
||||
|
||||
class TestTraderRAMIntegration:
|
||||
"""Test Trader integration with RAM."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Set up test fixtures."""
|
||||
cls.symbol = ForexSymbol(name="BTCUSD")
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def initialize(self):
|
||||
"""Initialize symbol."""
|
||||
self.symbol.initialize()
|
||||
|
||||
def test_trader_uses_ram_get_amount(self):
|
||||
"""Test trader uses RAM get_amount for volume calculation."""
|
||||
ram = RAM(fixed_amount=20)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = trader.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
|
||||
trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
|
||||
|
||||
# Volume should be calculated based on RAM fixed_amount (20)
|
||||
assert trader.order.volume > 0
|
||||
|
||||
def test_trader_uses_ram_risk_to_reward(self):
|
||||
"""Test trader uses RAM risk_to_reward for TP calculation."""
|
||||
ram = RAM(fixed_amount=10, risk_to_reward=3)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
|
||||
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
|
||||
tick = trader.symbol.info_tick()
|
||||
sl = tick.ask - dsl
|
||||
|
||||
trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
|
||||
|
||||
# TP distance should be 3x the SL distance
|
||||
sl_distance = abs(trader.order.price - trader.order.sl)
|
||||
tp_distance = abs(trader.order.tp - trader.order.price)
|
||||
|
||||
assert abs(tp_distance - (sl_distance * 3)) < self.symbol.point * 10
|
||||
|
||||
def test_trader_modifying_ram_after_init(self):
|
||||
"""Test modifying RAM after trader initialization."""
|
||||
ram = RAM(fixed_amount=10)
|
||||
trader = SimpleTrader(symbol=self.symbol, ram=ram)
|
||||
|
||||
trader.ram.modify_ram(fixed_amount=25, risk_to_reward=4)
|
||||
|
||||
assert trader.ram.fixed_amount == 25
|
||||
assert trader.ram.risk_to_reward == 4
|
||||
Reference in New Issue
Block a user