Skip to content

Commit 5152cab

Browse files
committed
Pytest trade.py
1 parent 034baa8 commit 5152cab

File tree

3 files changed

+617
-15
lines changed

3 files changed

+617
-15
lines changed

mqpy/trade.py

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
"""Module for trading operations with MetaTrader 5.
22
33
Provides a Trade class for managing trading operations.
4+
5+
Trade Modes:
6+
- 0: Disabled - Trading is completely disabled for the symbol
7+
- 1: Long only - Only buy positions allowed
8+
- 2: Short only - Only sell positions allowed
9+
- 3: Long and Short - Both buy and sell positions allowed (regular trading)
10+
- 4: Close only - Only position closing is allowed, no new positions can be opened
11+
12+
The Trade class automatically respects these limitations when attempting to open or close positions.
413
"""
514

615
import logging
@@ -113,7 +122,8 @@ def select_symbol(self) -> None:
113122
Returns:
114123
None
115124
"""
116-
Mt5.symbol_select(self.symbol, select=True)
125+
# Using positional arguments as the MetaTrader5 library doesn't support keywords
126+
Mt5.symbol_select(self.symbol, True) # noqa: FBT003
117127

118128
def prepare_symbol(self) -> None:
119129
"""Prepare the trading symbol for opening positions.
@@ -130,25 +140,60 @@ def prepare_symbol(self) -> None:
130140

131141
if not symbol_info.visible:
132142
logger.warning(f"The {self.symbol} is not visible, needed to be switched on.")
133-
if not Mt5.symbol_select(self.symbol, select=True):
143+
# Using positional arguments as the MetaTrader5 library doesn't support keywords
144+
if not Mt5.symbol_select(self.symbol, True): # noqa: FBT003
134145
logger.error(
135146
f"The expert advisor {self.expert_name} failed in select the symbol {self.symbol}, turning off."
136147
)
137148
Mt5.shutdown()
138149
logger.error("Turned off")
139150
sys.exit(1)
140151

152+
# Check the trade mode
153+
if symbol_info.trade_mode == 0:
154+
logger.warning(
155+
f"Trading is disabled for {self.symbol} (trade_mode = 0). No positions can be opened or closed."
156+
)
157+
elif symbol_info.trade_mode == 4:
158+
logger.warning(
159+
f"{self.symbol} is in 'Close only' mode (trade_mode = 4). Only existing positions can be closed."
160+
)
161+
162+
def get_trade_mode_description(self) -> str:
163+
"""Get a description of the symbol's trade mode.
164+
165+
Returns:
166+
str: A description of the trade mode.
167+
"""
168+
trade_mode = Mt5.symbol_info(self.symbol).trade_mode
169+
170+
if trade_mode == 0:
171+
return "Disabled (trading disabled for the symbol)"
172+
if trade_mode == 1:
173+
return "Long only (only buy positions allowed)"
174+
if trade_mode == 2:
175+
return "Short only (only sell positions allowed)"
176+
if trade_mode == 3:
177+
return "Long and Short (both buy and sell positions allowed)"
178+
if trade_mode == 4:
179+
return "Close only (only position closing is allowed)"
180+
return f"Unknown trade mode: {trade_mode}"
181+
141182
def summary(self) -> None:
142183
"""Print a summary of the expert advisor parameters.
143184
144185
Returns:
145186
None
146187
"""
188+
trade_mode = Mt5.symbol_info(self.symbol).trade_mode
189+
trade_mode_desc = self.get_trade_mode_description()
190+
147191
logger.info(
148192
f"Summary:\n"
149193
f"ExpertAdvisor name: {self.expert_name}\n"
150194
f"ExpertAdvisor version: {self.version}\n"
151195
f"Running on symbol: {self.symbol}\n"
196+
f"Symbol trade mode: {trade_mode} - {trade_mode_desc}\n"
152197
f"MagicNumber: {self.magic_number}\n"
153198
f"Number of lot(s): {self.lot}\n"
154199
f"StopLoss: {self.stop_loss}\n"
@@ -185,6 +230,18 @@ def open_buy_position(self, comment: str = "") -> None:
185230
Returns:
186231
None
187232
"""
233+
# Check trade mode to see if Buy operations are allowed
234+
symbol_info = Mt5.symbol_info(self.symbol)
235+
if symbol_info.trade_mode == 0:
236+
logger.warning(f"Cannot open Buy position for {self.symbol} - trading is disabled.")
237+
return
238+
if symbol_info.trade_mode == 2: # Short only
239+
logger.warning(f"Cannot open Buy position for {self.symbol} - only Sell positions are allowed.")
240+
return
241+
if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0:
242+
logger.warning(f"Cannot open Buy position for {self.symbol} - symbol is in 'Close only' mode.")
243+
return
244+
188245
point = Mt5.symbol_info(self.symbol).point
189246
price = Mt5.symbol_info_tick(self.symbol).ask
190247

@@ -217,6 +274,18 @@ def open_sell_position(self, comment: str = "") -> None:
217274
Returns:
218275
None
219276
"""
277+
# Check trade mode to see if Sell operations are allowed
278+
symbol_info = Mt5.symbol_info(self.symbol)
279+
if symbol_info.trade_mode == 0:
280+
logger.warning(f"Cannot open Sell position for {self.symbol} - trading is disabled.")
281+
return
282+
if symbol_info.trade_mode == 1: # Long only
283+
logger.warning(f"Cannot open Sell position for {self.symbol} - only Buy positions are allowed.")
284+
return
285+
if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0:
286+
logger.warning(f"Cannot open Sell position for {self.symbol} - symbol is in 'Close only' mode.")
287+
return
288+
220289
point = Mt5.symbol_info(self.symbol).point
221290
price = Mt5.symbol_info_tick(self.symbol).bid
222291

@@ -261,6 +330,64 @@ def request_result(self, price: float, result: int) -> None:
261330
else:
262331
logger.info(f"Position Closed: {result.price}")
263332

333+
def _handle_trade_mode_restrictions(self, symbol_info: Mt5.SymbolInfo) -> bool:
334+
"""Handle trade mode restrictions for different symbol types.
335+
336+
Args:
337+
symbol_info (Mt5.SymbolInfo): The symbol information.
338+
339+
Returns:
340+
bool: True if a position was opened or a restriction was handled, False otherwise.
341+
"""
342+
# Check if the symbol is in "Disabled" mode (trade_mode = 0)
343+
if symbol_info.trade_mode == 0:
344+
logger.warning(f"Cannot open new positions for {self.symbol} - trading is disabled.")
345+
return True
346+
347+
# Check if the symbol is in "Close only" mode (trade_mode = 4)
348+
if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0:
349+
logger.warning(f"Cannot open new positions for {self.symbol} - symbol is in 'Close only' mode.")
350+
return True
351+
352+
# No restrictions that prevent all trading
353+
return False
354+
355+
def _handle_position_by_trade_mode(
356+
self, symbol_info: Mt5.SymbolInfo, *, should_buy: bool, should_sell: bool, comment: str
357+
) -> None:
358+
"""Open a position based on trade mode and buy/sell conditions.
359+
360+
Args:
361+
symbol_info (Mt5.SymbolInfo): The symbol information.
362+
should_buy (bool): Whether a buy position should be opened.
363+
should_sell (bool): Whether a sell position should be opened.
364+
comment (str): A comment for the trade.
365+
"""
366+
# For "Long only" mode (trade_mode = 1), only allow Buy positions
367+
if symbol_info.trade_mode == 1:
368+
if should_buy:
369+
self.open_buy_position(comment)
370+
self.total_deals += 1
371+
elif should_sell:
372+
logger.warning(f"Cannot open Sell position for {self.symbol} - only Buy positions are allowed.")
373+
374+
# For "Short only" mode (trade_mode = 2), only allow Sell positions
375+
elif symbol_info.trade_mode == 2:
376+
if should_sell:
377+
self.open_sell_position(comment)
378+
self.total_deals += 1
379+
elif should_buy:
380+
logger.warning(f"Cannot open Buy position for {self.symbol} - only Sell positions are allowed.")
381+
382+
# For regular trading (trade_mode = 3) or other modes, allow both Buy and Sell
383+
else:
384+
if should_buy and not should_sell:
385+
self.open_buy_position(comment)
386+
self.total_deals += 1
387+
if should_sell and not should_buy:
388+
self.open_sell_position(comment)
389+
self.total_deals += 1
390+
264391
def open_position(self, *, should_buy: bool, should_sell: bool, comment: str = "") -> None:
265392
"""Open a position based on buy and sell conditions.
266393
@@ -272,16 +399,22 @@ def open_position(self, *, should_buy: bool, should_sell: bool, comment: str = "
272399
Returns:
273400
None
274401
"""
402+
symbol_info = Mt5.symbol_info(self.symbol)
403+
404+
# Check trade mode restrictions
405+
if self._handle_trade_mode_restrictions(symbol_info):
406+
return
407+
408+
# Open a position if no existing positions and within trading time
275409
if (len(Mt5.positions_get(symbol=self.symbol)) == 0) and self.trading_time():
276-
if should_buy and not should_sell:
277-
self.open_buy_position(comment)
278-
self.total_deals += 1
279-
if should_sell and not should_buy:
280-
self.open_sell_position(comment)
281-
self.total_deals += 1
410+
self._handle_position_by_trade_mode(
411+
symbol_info, should_buy=should_buy, should_sell=should_sell, comment=comment
412+
)
282413

414+
# Check for stop loss and take profit conditions
283415
self.stop_and_gain(comment)
284416

417+
# Check if it's the end of the trading day
285418
if self.days_end():
286419
logger.info("It is the end of trading the day.")
287420
logger.info("Closing all positions.")
@@ -297,6 +430,13 @@ def close_position(self, comment: str = "") -> None:
297430
Returns:
298431
None
299432
"""
433+
symbol_info = Mt5.symbol_info(self.symbol)
434+
435+
# If trading is completely disabled for the symbol, log a warning and return
436+
if symbol_info.trade_mode == 0:
437+
logger.warning(f"Cannot close position for {self.symbol} - trading is disabled for this symbol.")
438+
return
439+
300440
if len(Mt5.positions_get(symbol=self.symbol)) == 1:
301441
if Mt5.positions_get(symbol=self.symbol)[0].type == 0: # Buy position
302442
self.open_sell_position(comment)

pyproject.toml

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,43 @@ markers = []
6060
warn_unused_configs = true
6161
ignore_missing_imports = true
6262
disable_error_code = [
63-
"misc",
64-
"attr-defined",
65-
"call-arg",
66-
"name-defined",
63+
"attr-defined",
64+
"name-defined",
65+
"assignment",
66+
"return-value",
67+
"arg-type",
68+
"index",
69+
"misc",
70+
"operator"
6771
]
68-
show_error_codes = true
6972
files = "**/*.py"
70-
exclude = ["venv", "mt5", "site-packages", ".*"]
73+
exclude = [
74+
"venv",
75+
"mt5",
76+
"site-packages",
77+
"^build/",
78+
"^dist/"
79+
]
7180

7281
[[tool.mypy.overrides]]
7382
module = "MetaTrader5.*"
74-
ignore_missing_imports = true
83+
ignore_errors = true
84+
disallow_untyped_defs = false
85+
disallow_incomplete_defs = false
86+
disallow_untyped_decorators = false
87+
disallow_any_generics = false
88+
disallow_untyped_calls = false
89+
check_untyped_defs = false
7590

7691
[[tool.mypy.overrides]]
7792
module = "_virtualenv"
7893
ignore_errors = true
94+
disallow_untyped_defs = false
95+
disallow_incomplete_defs = false
96+
disallow_untyped_decorators = false
97+
disallow_any_generics = false
98+
disallow_untyped_calls = false
99+
check_untyped_defs = false
79100

80101
[tool.ruff]
81102
line-length = 120

0 commit comments

Comments
 (0)