pdmt5 0.1.4__py3-none-any.whl → 0.1.5__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
|
@@ -31,12 +31,11 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
31
31
|
description="Order filling mode: 'IOC' (Immediate or Cancel), "
|
|
32
32
|
"'FOK' (Fill or Kill), 'RETURN' (Return if not filled)",
|
|
33
33
|
)
|
|
34
|
-
dry_run: bool = Field(default=False, description="Enable dry run mode for testing.")
|
|
35
34
|
|
|
36
35
|
def close_open_positions(
|
|
37
36
|
self,
|
|
38
37
|
symbols: str | list[str] | tuple[str, ...] | None = None,
|
|
39
|
-
dry_run: bool
|
|
38
|
+
dry_run: bool = False,
|
|
40
39
|
**kwargs: Any, # noqa: ANN401
|
|
41
40
|
) -> dict[str, list[dict[str, Any]]]:
|
|
42
41
|
"""Close all open positions for specified symbols.
|
|
@@ -44,8 +43,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
44
43
|
Args:
|
|
45
44
|
symbols: Optional symbol or list of symbols to filter positions.
|
|
46
45
|
If None, all symbols will be considered.
|
|
47
|
-
dry_run:
|
|
48
|
-
`dry_run` attribute.
|
|
46
|
+
dry_run: If True, only check the order without sending it.
|
|
49
47
|
**kwargs: Additional keyword arguments for request parameters.
|
|
50
48
|
|
|
51
49
|
Returns:
|
|
@@ -67,15 +65,14 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
67
65
|
def _fetch_and_close_position(
|
|
68
66
|
self,
|
|
69
67
|
symbol: str | None = None,
|
|
70
|
-
dry_run: bool
|
|
68
|
+
dry_run: bool = False,
|
|
71
69
|
**kwargs: Any, # noqa: ANN401
|
|
72
70
|
) -> list[dict[str, Any]]:
|
|
73
71
|
"""Close all open positions for a specific symbol.
|
|
74
72
|
|
|
75
73
|
Args:
|
|
76
74
|
symbol: Optional symbol filter.
|
|
77
|
-
dry_run:
|
|
78
|
-
`dry_run` attribute.
|
|
75
|
+
dry_run: If True, only check the order without sending it.
|
|
79
76
|
**kwargs: Additional keyword arguments for request parameters.
|
|
80
77
|
|
|
81
78
|
Returns:
|
|
@@ -92,7 +89,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
92
89
|
f"ORDER_FILLING_{self.order_filling_mode}",
|
|
93
90
|
)
|
|
94
91
|
return [
|
|
95
|
-
self.
|
|
92
|
+
self._send_or_check_order(
|
|
96
93
|
request={
|
|
97
94
|
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
98
95
|
"symbol": p["symbol"],
|
|
@@ -112,17 +109,16 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
112
109
|
for p in positions_dict
|
|
113
110
|
]
|
|
114
111
|
|
|
115
|
-
def
|
|
112
|
+
def _send_or_check_order(
|
|
116
113
|
self,
|
|
117
114
|
request: dict[str, Any],
|
|
118
|
-
dry_run: bool
|
|
115
|
+
dry_run: bool = False,
|
|
119
116
|
) -> dict[str, Any]:
|
|
120
117
|
"""Send or check an order request.
|
|
121
118
|
|
|
122
119
|
Args:
|
|
123
120
|
request: Order request dictionary.
|
|
124
|
-
dry_run:
|
|
125
|
-
`dry_run` attribute.
|
|
121
|
+
dry_run: If True, only check the order without sending it.
|
|
126
122
|
|
|
127
123
|
Returns:
|
|
128
124
|
Dictionary with operation result.
|
|
@@ -131,17 +127,15 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
131
127
|
Mt5TradingError: If the order operation fails.
|
|
132
128
|
"""
|
|
133
129
|
self.logger.debug("request: %s", request)
|
|
134
|
-
|
|
135
|
-
self.logger.debug("is_dry_run: %s", is_dry_run)
|
|
136
|
-
if is_dry_run:
|
|
130
|
+
if dry_run:
|
|
137
131
|
response = self.order_check_as_dict(request=request)
|
|
138
132
|
order_func = "order_check"
|
|
139
133
|
else:
|
|
140
134
|
response = self.order_send_as_dict(request=request)
|
|
141
135
|
order_func = "order_send"
|
|
142
136
|
retcode = response.get("retcode")
|
|
143
|
-
if ((not
|
|
144
|
-
|
|
137
|
+
if ((not dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
|
|
138
|
+
dry_run and retcode == 0
|
|
145
139
|
):
|
|
146
140
|
self.logger.info("response: %s", response)
|
|
147
141
|
return response
|
|
@@ -159,6 +153,116 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
159
153
|
error_message = f"{order_func}() failed and aborted. <= `{comment}`"
|
|
160
154
|
raise Mt5TradingError(error_message)
|
|
161
155
|
|
|
156
|
+
def place_market_order(
|
|
157
|
+
self,
|
|
158
|
+
symbol: str,
|
|
159
|
+
volume: float,
|
|
160
|
+
order_side: Literal["BUY", "SELL"],
|
|
161
|
+
order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
|
|
162
|
+
order_time_mode: Literal["GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"] = "GTC",
|
|
163
|
+
dry_run: bool = False,
|
|
164
|
+
**kwargs: Any, # noqa: ANN401
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Send or check an order request to place a market order.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
symbol: Symbol for the order.
|
|
170
|
+
volume: Volume of the order.
|
|
171
|
+
order_side: Side of the order, either "BUY" or "SELL".
|
|
172
|
+
order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
|
|
173
|
+
order_time_mode: Order time mode, either "GTC", "DAY", "SPECIFIED",
|
|
174
|
+
or "SPECIFIED_DAY".
|
|
175
|
+
dry_run: If True, only check the order without sending it.
|
|
176
|
+
**kwargs: Additional keyword arguments for request parameters.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary with operation result.
|
|
180
|
+
"""
|
|
181
|
+
return self._send_or_check_order(
|
|
182
|
+
request={
|
|
183
|
+
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
184
|
+
"symbol": symbol,
|
|
185
|
+
"volume": volume,
|
|
186
|
+
"type": getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
|
|
187
|
+
"type_filling": getattr(
|
|
188
|
+
self.mt5, f"ORDER_FILLING_{order_filling_mode.upper()}"
|
|
189
|
+
),
|
|
190
|
+
"type_time": getattr(self.mt5, f"ORDER_TIME_{order_time_mode.upper()}"),
|
|
191
|
+
**kwargs,
|
|
192
|
+
},
|
|
193
|
+
dry_run=dry_run,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def update_sltp_for_open_positions(
|
|
197
|
+
self,
|
|
198
|
+
symbol: str,
|
|
199
|
+
stop_loss: float | None = None,
|
|
200
|
+
take_profit: float | None = None,
|
|
201
|
+
tickets: list[int] | None = None,
|
|
202
|
+
dry_run: bool = False,
|
|
203
|
+
**kwargs: Any, # noqa: ANN401
|
|
204
|
+
) -> list[dict[str, Any]]:
|
|
205
|
+
"""Change Stop Loss and Take Profit for open positions.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
symbol: Symbol for the position.
|
|
209
|
+
stop_loss: New Stop Loss price. If None, it will not be changed.
|
|
210
|
+
take_profit: New Take Profit price. If None, it will not be changed.
|
|
211
|
+
tickets: List of position tickets to filter positions. If None, all open
|
|
212
|
+
positions for the symbol will be considered.
|
|
213
|
+
dry_run: If True, only check the order without sending it.
|
|
214
|
+
**kwargs: Additional keyword arguments for request parameters.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of dictionaries with operation results for each updated position.
|
|
218
|
+
"""
|
|
219
|
+
positions_df = self.positions_get_as_df(symbol=symbol)
|
|
220
|
+
if positions_df.empty:
|
|
221
|
+
self.logger.warning("No open positions found for symbol: %s", symbol)
|
|
222
|
+
return []
|
|
223
|
+
elif tickets:
|
|
224
|
+
filtered_positions_df = positions_df.pipe(
|
|
225
|
+
lambda d: d[d["ticket"].isin(tickets)]
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
filtered_positions_df = positions_df
|
|
229
|
+
if filtered_positions_df.empty:
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
"No open positions found for symbol: %s with specified tickets: %s",
|
|
232
|
+
symbol,
|
|
233
|
+
tickets,
|
|
234
|
+
)
|
|
235
|
+
return []
|
|
236
|
+
else:
|
|
237
|
+
symbol_info = self.symbol_info_as_dict(symbol=symbol)
|
|
238
|
+
sl = round(stop_loss, symbol_info["digits"]) if stop_loss else None
|
|
239
|
+
tp = round(take_profit, symbol_info["digits"]) if take_profit else None
|
|
240
|
+
order_requests = [
|
|
241
|
+
{
|
|
242
|
+
"action": self.mt5.TRADE_ACTION_SLTP,
|
|
243
|
+
"symbol": p["symbol"],
|
|
244
|
+
"position": p["ticket"],
|
|
245
|
+
"sl": (sl or p["sl"]),
|
|
246
|
+
"tp": (tp or p["tp"]),
|
|
247
|
+
**kwargs,
|
|
248
|
+
}
|
|
249
|
+
for _, p in filtered_positions_df.iterrows()
|
|
250
|
+
if sl != p["sl"] or tp != p["tp"]
|
|
251
|
+
]
|
|
252
|
+
if order_requests:
|
|
253
|
+
return [
|
|
254
|
+
self._send_or_check_order(request=r, dry_run=dry_run)
|
|
255
|
+
for r in order_requests
|
|
256
|
+
]
|
|
257
|
+
else:
|
|
258
|
+
self.logger.info(
|
|
259
|
+
"No positions to update for symbol: %s with SL: %s and TP: %s",
|
|
260
|
+
symbol,
|
|
261
|
+
sl,
|
|
262
|
+
tp,
|
|
263
|
+
)
|
|
264
|
+
return []
|
|
265
|
+
|
|
162
266
|
def calculate_minimum_order_margins(self, symbol: str) -> dict[str, float]:
|
|
163
267
|
"""Calculate minimum order margins for a given symbol.
|
|
164
268
|
|
|
@@ -369,3 +473,50 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
369
473
|
)
|
|
370
474
|
.drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
|
|
371
475
|
)
|
|
476
|
+
|
|
477
|
+
def calculate_new_position_margin_ratio(
|
|
478
|
+
self,
|
|
479
|
+
symbol: str,
|
|
480
|
+
new_side: Literal["BUY", "SELL"] | None = None,
|
|
481
|
+
new_volume: float = 0,
|
|
482
|
+
) -> float:
|
|
483
|
+
"""Calculate the margin ratio for a new position.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
symbol: Symbol for which to calculate the margin ratio.
|
|
487
|
+
new_side: Side of the new position, either "BUY" or "SELL".
|
|
488
|
+
new_volume: Volume of the new position.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
float: Margin ratio for the new position as a fraction of account equity.
|
|
492
|
+
"""
|
|
493
|
+
account_info = self.account_info_as_dict()
|
|
494
|
+
if not account_info["equity"]:
|
|
495
|
+
return 0.0
|
|
496
|
+
else:
|
|
497
|
+
positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
|
|
498
|
+
current_signed_margin = (
|
|
499
|
+
positions_df["signed_margin"].sum() if positions_df.size else 0
|
|
500
|
+
)
|
|
501
|
+
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
502
|
+
if new_volume == 0:
|
|
503
|
+
new_signed_margin = 0
|
|
504
|
+
elif new_side == "BUY":
|
|
505
|
+
new_signed_margin = self.order_calc_margin(
|
|
506
|
+
action=self.mt5.ORDER_TYPE_BUY,
|
|
507
|
+
symbol=symbol,
|
|
508
|
+
volume=new_volume,
|
|
509
|
+
price=symbol_info_tick["ask"],
|
|
510
|
+
)
|
|
511
|
+
elif new_side == "SELL":
|
|
512
|
+
new_signed_margin = -self.order_calc_margin(
|
|
513
|
+
action=self.mt5.ORDER_TYPE_SELL,
|
|
514
|
+
symbol=symbol,
|
|
515
|
+
volume=new_volume,
|
|
516
|
+
price=symbol_info_tick["bid"],
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
new_signed_margin = 0
|
|
520
|
+
return abs(
|
|
521
|
+
(new_signed_margin + current_signed_margin) / account_info["equity"]
|
|
522
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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>
|
|
@@ -29,7 +29,6 @@ Pandas-based data handler for MetaTrader 5
|
|
|
29
29
|
[](https://www.python.org/downloads/)
|
|
30
30
|
[](https://opensource.org/licenses/MIT)
|
|
31
31
|
[](https://www.microsoft.com/windows)
|
|
32
|
-
[](https://github.com/dceoy/pdmt5)
|
|
33
32
|
|
|
34
33
|
## Overview
|
|
35
34
|
|
|
@@ -195,10 +194,12 @@ Advanced trading operations client that extends Mt5DataClient:
|
|
|
195
194
|
- `dry_run` - Test mode flag for simulating trades without execution
|
|
196
195
|
- **Position Management**:
|
|
197
196
|
- `close_open_positions()` - Close all positions for specified symbol(s)
|
|
198
|
-
- `
|
|
197
|
+
- `place_market_order()` - Place market orders with configurable side, volume, and execution modes
|
|
198
|
+
- `update_open_position_sltp()` - Modify stop loss and take profit levels for open positions
|
|
199
199
|
- **Market Analysis**:
|
|
200
200
|
- `calculate_minimum_order_margins()` - Calculate minimum required margins for buy/sell orders
|
|
201
201
|
- `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
|
|
202
|
+
- `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
|
|
202
203
|
- **Simplified Data Access**:
|
|
203
204
|
- `fetch_latest_rates_as_df()` - Get recent OHLC data with timeframe strings (e.g., "M1", "H1", "D1")
|
|
204
205
|
- `fetch_latest_ticks_as_df()` - Get tick data for specified seconds around last tick
|
|
@@ -288,18 +289,54 @@ from pdmt5 import Mt5TradingClient
|
|
|
288
289
|
|
|
289
290
|
# Create trading client with specific order filling mode
|
|
290
291
|
with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
292
|
+
# Place a market buy order
|
|
293
|
+
order_result = trader.place_market_order(
|
|
294
|
+
symbol="EURUSD",
|
|
295
|
+
volume=0.1,
|
|
296
|
+
order_side="BUY",
|
|
297
|
+
order_filling_mode="IOC", # Immediate or Cancel
|
|
298
|
+
order_time_mode="GTC" # Good Till Cancelled
|
|
299
|
+
)
|
|
300
|
+
print(f"Order placed: {order_result['retcode']}")
|
|
301
|
+
|
|
302
|
+
# Update stop loss and take profit for an open position
|
|
303
|
+
if positions := trader.positions_get_as_df(symbol="EURUSD"):
|
|
304
|
+
position_ticket = positions.iloc[0]['ticket']
|
|
305
|
+
update_result = trader.update_open_position_sltp(
|
|
306
|
+
symbol="EURUSD",
|
|
307
|
+
position_ticket=position_ticket,
|
|
308
|
+
sl=1.0950, # New stop loss
|
|
309
|
+
tp=1.1050 # New take profit
|
|
310
|
+
)
|
|
311
|
+
print(f"Position updated: {update_result['retcode']}")
|
|
312
|
+
|
|
313
|
+
# Calculate margin ratio for a new position
|
|
314
|
+
margin_ratio = trader.calculate_new_position_margin_ratio(
|
|
315
|
+
symbol="EURUSD",
|
|
316
|
+
new_side="SELL",
|
|
317
|
+
new_volume=0.2
|
|
318
|
+
)
|
|
319
|
+
print(f"New position margin ratio: {margin_ratio:.2%}")
|
|
320
|
+
|
|
291
321
|
# Close all EURUSD positions
|
|
292
322
|
results = trader.close_open_positions(symbols="EURUSD")
|
|
293
323
|
|
|
294
324
|
if results:
|
|
295
|
-
for
|
|
296
|
-
|
|
325
|
+
for symbol, close_results in results.items():
|
|
326
|
+
for result in close_results:
|
|
327
|
+
print(f"Closed position {result.get('position')} with result: {result['retcode']}")
|
|
297
328
|
|
|
298
329
|
# Using dry run mode for testing
|
|
299
330
|
trader_dry = Mt5TradingClient(config=config, dry_run=True)
|
|
300
331
|
with trader_dry:
|
|
301
|
-
# Test
|
|
302
|
-
|
|
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']}")
|
|
303
340
|
```
|
|
304
341
|
|
|
305
342
|
### Market Analysis with Mt5TradingClient
|
|
@@ -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=TJjxq2PP2eIRM8pe4G0DzxLkLjOFR-0ZV0rpzDwl-TU,19488
|
|
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.5.dist-info/METADATA,sha256=YjTgWZTyhoLtzbLii57WYM5UajB0pix15sM7CzhjgwY,16085
|
|
7
|
+
pdmt5-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
pdmt5-0.1.5.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
+
pdmt5-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|