pdmt5 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pdmt5/trading.py
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import timedelta
|
|
6
|
+
from math import floor
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
7
8
|
|
|
8
|
-
from pydantic import ConfigDict
|
|
9
|
+
from pydantic import ConfigDict
|
|
9
10
|
|
|
10
11
|
from .dataframe import Mt5DataClient
|
|
11
12
|
from .mt5 import Mt5RuntimeError
|
|
@@ -26,15 +27,11 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
29
|
model_config = ConfigDict(frozen=True)
|
|
29
|
-
order_filling_mode: Literal["IOC", "FOK", "RETURN"] = Field(
|
|
30
|
-
default="IOC",
|
|
31
|
-
description="Order filling mode: 'IOC' (Immediate or Cancel), "
|
|
32
|
-
"'FOK' (Fill or Kill), 'RETURN' (Return if not filled)",
|
|
33
|
-
)
|
|
34
30
|
|
|
35
31
|
def close_open_positions(
|
|
36
32
|
self,
|
|
37
33
|
symbols: str | list[str] | tuple[str, ...] | None = None,
|
|
34
|
+
order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
|
|
38
35
|
dry_run: bool = False,
|
|
39
36
|
**kwargs: Any, # noqa: ANN401
|
|
40
37
|
) -> dict[str, list[dict[str, Any]]]:
|
|
@@ -43,6 +40,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
43
40
|
Args:
|
|
44
41
|
symbols: Optional symbol or list of symbols to filter positions.
|
|
45
42
|
If None, all symbols will be considered.
|
|
43
|
+
order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
|
|
46
44
|
dry_run: If True, only check the order without sending it.
|
|
47
45
|
**kwargs: Additional keyword arguments for request parameters.
|
|
48
46
|
|
|
@@ -58,13 +56,19 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
58
56
|
symbol_list = self.symbols_get()
|
|
59
57
|
self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
|
|
60
58
|
return {
|
|
61
|
-
s: self._fetch_and_close_position(
|
|
59
|
+
s: self._fetch_and_close_position(
|
|
60
|
+
symbol=s,
|
|
61
|
+
order_filling_mode=order_filling_mode,
|
|
62
|
+
dry_run=dry_run,
|
|
63
|
+
**kwargs,
|
|
64
|
+
)
|
|
62
65
|
for s in symbol_list
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
def _fetch_and_close_position(
|
|
66
69
|
self,
|
|
67
70
|
symbol: str | None = None,
|
|
71
|
+
order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
|
|
68
72
|
dry_run: bool = False,
|
|
69
73
|
**kwargs: Any, # noqa: ANN401
|
|
70
74
|
) -> list[dict[str, Any]]:
|
|
@@ -72,6 +76,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
72
76
|
|
|
73
77
|
Args:
|
|
74
78
|
symbol: Optional symbol filter.
|
|
79
|
+
order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
|
|
75
80
|
dry_run: If True, only check the order without sending it.
|
|
76
81
|
**kwargs: Additional keyword arguments for request parameters.
|
|
77
82
|
|
|
@@ -84,10 +89,6 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
84
89
|
return []
|
|
85
90
|
else:
|
|
86
91
|
self.logger.info("Closing open positions for symbol: %s", symbol)
|
|
87
|
-
order_filling_type = getattr(
|
|
88
|
-
self.mt5,
|
|
89
|
-
f"ORDER_FILLING_{self.order_filling_mode}",
|
|
90
|
-
)
|
|
91
92
|
return [
|
|
92
93
|
self._send_or_check_order(
|
|
93
94
|
request={
|
|
@@ -99,7 +100,10 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
99
100
|
if p["type"] == self.mt5.POSITION_TYPE_BUY
|
|
100
101
|
else self.mt5.ORDER_TYPE_BUY
|
|
101
102
|
),
|
|
102
|
-
"type_filling":
|
|
103
|
+
"type_filling": getattr(
|
|
104
|
+
self.mt5,
|
|
105
|
+
f"ORDER_FILLING_{order_filling_mode}",
|
|
106
|
+
),
|
|
103
107
|
"type_time": self.mt5.ORDER_TIME_GTC,
|
|
104
108
|
"position": p["ticket"],
|
|
105
109
|
**kwargs,
|
|
@@ -263,41 +267,69 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
263
267
|
)
|
|
264
268
|
return []
|
|
265
269
|
|
|
266
|
-
def
|
|
267
|
-
|
|
270
|
+
def calculate_minimum_order_margin(
|
|
271
|
+
self,
|
|
272
|
+
symbol: str,
|
|
273
|
+
order_side: Literal["BUY", "SELL"],
|
|
274
|
+
) -> dict[str, float]:
|
|
275
|
+
"""Calculate the minimum order margins for a given symbol.
|
|
268
276
|
|
|
269
277
|
Args:
|
|
270
|
-
symbol: Symbol for which to calculate minimum order margins.
|
|
278
|
+
symbol: Symbol for which to calculate the minimum order margins.
|
|
279
|
+
order_side: Optional side of the order, either "BUY" or "SELL".
|
|
271
280
|
|
|
272
281
|
Returns:
|
|
273
|
-
Dictionary with margin
|
|
274
|
-
|
|
275
|
-
Raises:
|
|
276
|
-
Mt5TradingError: If margin calculation fails.
|
|
282
|
+
Dictionary with minimum volume and margin for the specified order side.
|
|
277
283
|
"""
|
|
278
284
|
symbol_info = self.symbol_info_as_dict(symbol=symbol)
|
|
279
285
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
280
|
-
|
|
281
|
-
action=self.mt5.
|
|
286
|
+
margin = self.order_calc_margin(
|
|
287
|
+
action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
|
|
282
288
|
symbol=symbol,
|
|
283
289
|
volume=symbol_info["volume_min"],
|
|
284
|
-
price=
|
|
290
|
+
price=(
|
|
291
|
+
symbol_info_tick["bid"]
|
|
292
|
+
if order_side == "SELL"
|
|
293
|
+
else symbol_info_tick["ask"]
|
|
294
|
+
),
|
|
285
295
|
)
|
|
286
|
-
|
|
287
|
-
|
|
296
|
+
if margin:
|
|
297
|
+
return {"volume": symbol_info["volume_min"], "margin": margin}
|
|
298
|
+
else:
|
|
299
|
+
self.logger.warning(
|
|
300
|
+
"No margin available for symbol: %s with order side: %s",
|
|
301
|
+
symbol,
|
|
302
|
+
order_side,
|
|
303
|
+
)
|
|
304
|
+
return {"volume": symbol_info["volume_min"], "margin": 0.0}
|
|
305
|
+
|
|
306
|
+
def calculate_volume_by_margin(
|
|
307
|
+
self,
|
|
308
|
+
symbol: str,
|
|
309
|
+
margin: float,
|
|
310
|
+
order_side: Literal["BUY", "SELL"],
|
|
311
|
+
) -> float:
|
|
312
|
+
"""Calculate volume based on margin for a given symbol and order side.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
symbol: Symbol for which to calculate the volume.
|
|
316
|
+
margin: Margin amount to use for the calculation.
|
|
317
|
+
order_side: Side of the order, either "BUY" or "SELL".
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Calculated volume as a float.
|
|
321
|
+
"""
|
|
322
|
+
min_order_margin_dict = self.calculate_minimum_order_margin(
|
|
288
323
|
symbol=symbol,
|
|
289
|
-
|
|
290
|
-
price=symbol_info_tick["bid"],
|
|
324
|
+
order_side=order_side,
|
|
291
325
|
)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
else:
|
|
297
|
-
error_message = (
|
|
298
|
-
f"Failed to calculate minimum order margins for symbol: {symbol}."
|
|
326
|
+
if min_order_margin_dict["margin"]:
|
|
327
|
+
return (
|
|
328
|
+
floor(margin / min_order_margin_dict["margin"])
|
|
329
|
+
* min_order_margin_dict["volume"]
|
|
299
330
|
)
|
|
300
|
-
|
|
331
|
+
else:
|
|
332
|
+
return 0.0
|
|
301
333
|
|
|
302
334
|
def calculate_spread_ratio(
|
|
303
335
|
self,
|
|
@@ -477,15 +509,15 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
477
509
|
def calculate_new_position_margin_ratio(
|
|
478
510
|
self,
|
|
479
511
|
symbol: str,
|
|
480
|
-
|
|
481
|
-
|
|
512
|
+
new_position_side: Literal["BUY", "SELL"] | None = None,
|
|
513
|
+
new_position_volume: float = 0,
|
|
482
514
|
) -> float:
|
|
483
515
|
"""Calculate the margin ratio for a new position.
|
|
484
516
|
|
|
485
517
|
Args:
|
|
486
518
|
symbol: Symbol for which to calculate the margin ratio.
|
|
487
|
-
|
|
488
|
-
|
|
519
|
+
new_position_side: Side of the new position, either "BUY" or "SELL".
|
|
520
|
+
new_position_volume: Volume of the new position.
|
|
489
521
|
|
|
490
522
|
Returns:
|
|
491
523
|
float: Margin ratio for the new position as a fraction of account equity.
|
|
@@ -499,20 +531,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
499
531
|
positions_df["signed_margin"].sum() if positions_df.size else 0
|
|
500
532
|
)
|
|
501
533
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
502
|
-
if
|
|
534
|
+
if not (new_position_side and new_position_volume):
|
|
503
535
|
new_signed_margin = 0
|
|
504
|
-
elif
|
|
536
|
+
elif new_position_side.upper() == "BUY":
|
|
505
537
|
new_signed_margin = self.order_calc_margin(
|
|
506
538
|
action=self.mt5.ORDER_TYPE_BUY,
|
|
507
539
|
symbol=symbol,
|
|
508
|
-
volume=
|
|
540
|
+
volume=new_position_volume,
|
|
509
541
|
price=symbol_info_tick["ask"],
|
|
510
542
|
)
|
|
511
|
-
elif
|
|
543
|
+
elif new_position_side.upper() == "SELL":
|
|
512
544
|
new_signed_margin = -self.order_calc_margin(
|
|
513
545
|
action=self.mt5.ORDER_TYPE_SELL,
|
|
514
546
|
symbol=symbol,
|
|
515
|
-
volume=
|
|
547
|
+
volume=new_position_volume,
|
|
516
548
|
price=symbol_info_tick["bid"],
|
|
517
549
|
)
|
|
518
550
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Pandas-based data handler for MetaTrader 5
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/pdmt5.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -42,6 +42,8 @@ Pandas-based data handler for MetaTrader 5
|
|
|
42
42
|
- 🚀 **Context Manager Support**: Clean initialization and cleanup with `with` statements
|
|
43
43
|
- 📈 **Time Series Ready**: OHLCV data with proper datetime indexing
|
|
44
44
|
- 🛡️ **Robust Error Handling**: Custom exceptions with detailed MT5 error information
|
|
45
|
+
- 💰 **Advanced Trading Operations**: Position management, margin calculations, and risk analysis tools
|
|
46
|
+
- 🧪 **Dry Run Mode**: Test trading strategies without executing real trades
|
|
45
47
|
|
|
46
48
|
## Requirements
|
|
47
49
|
|
|
@@ -189,15 +191,13 @@ Extends Mt5Client with pandas DataFrame and dictionary conversions:
|
|
|
189
191
|
|
|
190
192
|
Advanced trading operations client that extends Mt5DataClient:
|
|
191
193
|
|
|
192
|
-
- **Trading Configuration**:
|
|
193
|
-
- `order_filling_mode` - Order execution mode: "IOC" (default), "FOK", or "RETURN"
|
|
194
|
-
- `dry_run` - Test mode flag for simulating trades without execution
|
|
195
194
|
- **Position Management**:
|
|
196
195
|
- `close_open_positions()` - Close all positions for specified symbol(s)
|
|
197
196
|
- `place_market_order()` - Place market orders with configurable side, volume, and execution modes
|
|
198
|
-
- `
|
|
199
|
-
- **
|
|
200
|
-
- `
|
|
197
|
+
- `update_sltp_for_open_positions()` - Modify stop loss and take profit levels for open positions
|
|
198
|
+
- **Margin Calculations**:
|
|
199
|
+
- `calculate_minimum_order_margin()` - Calculate minimum required margin for a specific order side
|
|
200
|
+
- `calculate_volume_by_margin()` - Calculate maximum volume for given margin amount
|
|
201
201
|
- `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
|
|
202
202
|
- `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
|
|
203
203
|
- **Simplified Data Access**:
|
|
@@ -210,7 +210,6 @@ Advanced trading operations client that extends Mt5DataClient:
|
|
|
210
210
|
- Comprehensive error handling with `Mt5TradingError`
|
|
211
211
|
- Support for batch operations on multiple symbols
|
|
212
212
|
- Automatic position closing with proper order type reversal
|
|
213
|
-
- Dry run mode for strategy testing without real trades
|
|
214
213
|
|
|
215
214
|
### Configuration
|
|
216
215
|
|
|
@@ -287,8 +286,8 @@ with Mt5DataClient(config=config) as client:
|
|
|
287
286
|
```python
|
|
288
287
|
from pdmt5 import Mt5TradingClient
|
|
289
288
|
|
|
290
|
-
# Create trading client
|
|
291
|
-
with Mt5TradingClient(config=config
|
|
289
|
+
# Create trading client
|
|
290
|
+
with Mt5TradingClient(config=config) as trader:
|
|
292
291
|
# Place a market buy order
|
|
293
292
|
order_result = trader.place_market_order(
|
|
294
293
|
symbol="EURUSD",
|
|
@@ -299,44 +298,33 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
|
299
298
|
)
|
|
300
299
|
print(f"Order placed: {order_result['retcode']}")
|
|
301
300
|
|
|
302
|
-
# Update stop loss and take profit for
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
print(f"Position updated: {update_result['retcode']}")
|
|
301
|
+
# Update stop loss and take profit for open positions
|
|
302
|
+
update_results = trader.update_sltp_for_open_positions(
|
|
303
|
+
symbol="EURUSD",
|
|
304
|
+
stop_loss=1.0950, # New stop loss
|
|
305
|
+
take_profit=1.1050 # New take profit
|
|
306
|
+
)
|
|
307
|
+
for result in update_results:
|
|
308
|
+
print(f"Position updated: {result['retcode']}")
|
|
312
309
|
|
|
313
310
|
# Calculate margin ratio for a new position
|
|
314
311
|
margin_ratio = trader.calculate_new_position_margin_ratio(
|
|
315
312
|
symbol="EURUSD",
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
new_position_side="SELL",
|
|
314
|
+
new_position_volume=0.2
|
|
318
315
|
)
|
|
319
316
|
print(f"New position margin ratio: {margin_ratio:.2%}")
|
|
320
317
|
|
|
321
|
-
# Close all EURUSD positions
|
|
322
|
-
results = trader.close_open_positions(
|
|
318
|
+
# Close all EURUSD positions with specific order filling mode
|
|
319
|
+
results = trader.close_open_positions(
|
|
320
|
+
symbols="EURUSD",
|
|
321
|
+
order_filling_mode="FOK" # Fill or Kill
|
|
322
|
+
)
|
|
323
323
|
|
|
324
324
|
if results:
|
|
325
325
|
for symbol, close_results in results.items():
|
|
326
326
|
for result in close_results:
|
|
327
327
|
print(f"Closed position {result.get('position')} with result: {result['retcode']}")
|
|
328
|
-
|
|
329
|
-
# Using dry run mode for testing
|
|
330
|
-
trader_dry = Mt5TradingClient(config=config, dry_run=True)
|
|
331
|
-
with trader_dry:
|
|
332
|
-
# Test placing an order without actual execution
|
|
333
|
-
test_order = trader_dry.place_market_order(
|
|
334
|
-
symbol="GBPUSD",
|
|
335
|
-
volume=0.1,
|
|
336
|
-
order_side="SELL",
|
|
337
|
-
dry_run=True # Override instance setting
|
|
338
|
-
)
|
|
339
|
-
print(f"Test order validation: {test_order['retcode']}")
|
|
340
328
|
```
|
|
341
329
|
|
|
342
330
|
### Market Analysis with Mt5TradingClient
|
|
@@ -347,10 +335,18 @@ with Mt5TradingClient(config=config) as trader:
|
|
|
347
335
|
spread_ratio = trader.calculate_spread_ratio("EURUSD")
|
|
348
336
|
print(f"EURUSD spread ratio: {spread_ratio:.5f}")
|
|
349
337
|
|
|
350
|
-
# Get minimum order
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
print(f"Minimum
|
|
338
|
+
# Get minimum order margin for BUY and SELL
|
|
339
|
+
buy_margin = trader.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
340
|
+
sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
341
|
+
print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
|
|
342
|
+
print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
|
|
343
|
+
|
|
344
|
+
# Calculate volume by margin
|
|
345
|
+
available_margin = 1000.0
|
|
346
|
+
max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
|
|
347
|
+
max_sell_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "SELL")
|
|
348
|
+
print(f"Max BUY volume for ${available_margin}: {max_buy_volume}")
|
|
349
|
+
print(f"Max SELL volume for ${available_margin}: {max_sell_volume}")
|
|
354
350
|
|
|
355
351
|
# Get recent OHLC data with custom timeframe
|
|
356
352
|
rates_df = trader.fetch_latest_rates_as_df(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
2
|
pdmt5/dataframe.py,sha256=rUWtR23hrXBdBqzJhbOlIemNy73RrjSTZZJUhwoL6io,38084
|
|
3
3
|
pdmt5/mt5.py,sha256=KgxHapIrh5b4L0wIOAQIjfXNZafalihbFrh9fhYHmrI,32254
|
|
4
|
-
pdmt5/trading.py,sha256=
|
|
4
|
+
pdmt5/trading.py,sha256=LRHfh1bjC_day4A4Az4Q5TM4cBgZB18pWxj3Wfyo5Yg,20608
|
|
5
5
|
pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
|
|
6
|
-
pdmt5-0.1.
|
|
7
|
-
pdmt5-0.1.
|
|
8
|
-
pdmt5-0.1.
|
|
9
|
-
pdmt5-0.1.
|
|
6
|
+
pdmt5-0.1.7.dist-info/METADATA,sha256=c5HJ6Xm3upGHXdGQ3j1sOBE_QHY_kBVCRJJWPdgunt0,16175
|
|
7
|
+
pdmt5-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
pdmt5-0.1.7.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
+
pdmt5-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|