pdmt5 0.1.5__tar.gz → 0.1.6__tar.gz
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-0.1.5 → pdmt5-0.1.6}/.github/workflows/ci.yml +1 -1
- {pdmt5-0.1.5 → pdmt5-0.1.6}/PKG-INFO +29 -20
- {pdmt5-0.1.5 → pdmt5-0.1.6}/README.md +28 -19
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/api/trading.md +187 -9
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pdmt5/trading.py +54 -34
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pyproject.toml +1 -1
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/test_trading.py +81 -45
- {pdmt5-0.1.5 → pdmt5-0.1.6}/uv.lock +1 -1
- {pdmt5-0.1.5 → pdmt5-0.1.6}/.claude/settings.json +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/.gitignore +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/CLAUDE.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/LICENSE +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/api/index.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/api/utils.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/docs/index.md +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/mkdocs.yml +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pdmt5/dataframe.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pdmt5/mt5.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/renovate.json +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/__init__.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/test_dataframe.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/test_init.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/test_mt5.py +0 -0
- {pdmt5-0.1.5 → pdmt5-0.1.6}/test/test_utils.py +0 -0
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
- name: Checkout repository
|
|
43
43
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
44
44
|
- name: Set up uv
|
|
45
|
-
uses: astral-sh/setup-uv@
|
|
45
|
+
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
|
46
46
|
- name: Install the package
|
|
47
47
|
run: >
|
|
48
48
|
uv sync
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
|
@@ -195,9 +197,10 @@ Advanced trading operations client that extends Mt5DataClient:
|
|
|
195
197
|
- **Position Management**:
|
|
196
198
|
- `close_open_positions()` - Close all positions for specified symbol(s)
|
|
197
199
|
- `place_market_order()` - Place market orders with configurable side, volume, and execution modes
|
|
198
|
-
- `
|
|
199
|
-
- **
|
|
200
|
-
- `
|
|
200
|
+
- `update_sltp_for_open_positions()` - Modify stop loss and take profit levels for open positions
|
|
201
|
+
- **Margin Calculations**:
|
|
202
|
+
- `calculate_minimum_order_margin()` - Calculate minimum required margin for a specific order side
|
|
203
|
+
- `calculate_volume_by_margin()` - Calculate maximum volume for given margin amount
|
|
201
204
|
- `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
|
|
202
205
|
- `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
|
|
203
206
|
- **Simplified Data Access**:
|
|
@@ -299,22 +302,20 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
|
299
302
|
)
|
|
300
303
|
print(f"Order placed: {order_result['retcode']}")
|
|
301
304
|
|
|
302
|
-
# Update stop loss and take profit for
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
print(f"Position updated: {update_result['retcode']}")
|
|
305
|
+
# Update stop loss and take profit for open positions
|
|
306
|
+
update_results = trader.update_sltp_for_open_positions(
|
|
307
|
+
symbol="EURUSD",
|
|
308
|
+
stop_loss=1.0950, # New stop loss
|
|
309
|
+
take_profit=1.1050 # New take profit
|
|
310
|
+
)
|
|
311
|
+
for result in update_results:
|
|
312
|
+
print(f"Position updated: {result['retcode']}")
|
|
312
313
|
|
|
313
314
|
# Calculate margin ratio for a new position
|
|
314
315
|
margin_ratio = trader.calculate_new_position_margin_ratio(
|
|
315
316
|
symbol="EURUSD",
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
new_position_side="SELL",
|
|
318
|
+
new_position_volume=0.2
|
|
318
319
|
)
|
|
319
320
|
print(f"New position margin ratio: {margin_ratio:.2%}")
|
|
320
321
|
|
|
@@ -347,10 +348,18 @@ with Mt5TradingClient(config=config) as trader:
|
|
|
347
348
|
spread_ratio = trader.calculate_spread_ratio("EURUSD")
|
|
348
349
|
print(f"EURUSD spread ratio: {spread_ratio:.5f}")
|
|
349
350
|
|
|
350
|
-
# Get minimum order
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
print(f"Minimum
|
|
351
|
+
# Get minimum order margin for BUY and SELL
|
|
352
|
+
buy_margin = trader.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
353
|
+
sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
354
|
+
print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
|
|
355
|
+
print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
|
|
356
|
+
|
|
357
|
+
# Calculate volume by margin
|
|
358
|
+
available_margin = 1000.0
|
|
359
|
+
max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
|
|
360
|
+
max_sell_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "SELL")
|
|
361
|
+
print(f"Max BUY volume for ${available_margin}: {max_buy_volume}")
|
|
362
|
+
print(f"Max SELL volume for ${available_margin}: {max_sell_volume}")
|
|
354
363
|
|
|
355
364
|
# Get recent OHLC data with custom timeframe
|
|
356
365
|
rates_df = trader.fetch_latest_rates_as_df(
|
|
@@ -19,6 +19,8 @@ Pandas-based data handler for MetaTrader 5
|
|
|
19
19
|
- 🚀 **Context Manager Support**: Clean initialization and cleanup with `with` statements
|
|
20
20
|
- 📈 **Time Series Ready**: OHLCV data with proper datetime indexing
|
|
21
21
|
- 🛡️ **Robust Error Handling**: Custom exceptions with detailed MT5 error information
|
|
22
|
+
- 💰 **Advanced Trading Operations**: Position management, margin calculations, and risk analysis tools
|
|
23
|
+
- 🧪 **Dry Run Mode**: Test trading strategies without executing real trades
|
|
22
24
|
|
|
23
25
|
## Requirements
|
|
24
26
|
|
|
@@ -172,9 +174,10 @@ Advanced trading operations client that extends Mt5DataClient:
|
|
|
172
174
|
- **Position Management**:
|
|
173
175
|
- `close_open_positions()` - Close all positions for specified symbol(s)
|
|
174
176
|
- `place_market_order()` - Place market orders with configurable side, volume, and execution modes
|
|
175
|
-
- `
|
|
176
|
-
- **
|
|
177
|
-
- `
|
|
177
|
+
- `update_sltp_for_open_positions()` - Modify stop loss and take profit levels for open positions
|
|
178
|
+
- **Margin Calculations**:
|
|
179
|
+
- `calculate_minimum_order_margin()` - Calculate minimum required margin for a specific order side
|
|
180
|
+
- `calculate_volume_by_margin()` - Calculate maximum volume for given margin amount
|
|
178
181
|
- `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
|
|
179
182
|
- `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
|
|
180
183
|
- **Simplified Data Access**:
|
|
@@ -276,22 +279,20 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
|
276
279
|
)
|
|
277
280
|
print(f"Order placed: {order_result['retcode']}")
|
|
278
281
|
|
|
279
|
-
# Update stop loss and take profit for
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
print(f"Position updated: {update_result['retcode']}")
|
|
282
|
+
# Update stop loss and take profit for open positions
|
|
283
|
+
update_results = trader.update_sltp_for_open_positions(
|
|
284
|
+
symbol="EURUSD",
|
|
285
|
+
stop_loss=1.0950, # New stop loss
|
|
286
|
+
take_profit=1.1050 # New take profit
|
|
287
|
+
)
|
|
288
|
+
for result in update_results:
|
|
289
|
+
print(f"Position updated: {result['retcode']}")
|
|
289
290
|
|
|
290
291
|
# Calculate margin ratio for a new position
|
|
291
292
|
margin_ratio = trader.calculate_new_position_margin_ratio(
|
|
292
293
|
symbol="EURUSD",
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
new_position_side="SELL",
|
|
295
|
+
new_position_volume=0.2
|
|
295
296
|
)
|
|
296
297
|
print(f"New position margin ratio: {margin_ratio:.2%}")
|
|
297
298
|
|
|
@@ -324,10 +325,18 @@ with Mt5TradingClient(config=config) as trader:
|
|
|
324
325
|
spread_ratio = trader.calculate_spread_ratio("EURUSD")
|
|
325
326
|
print(f"EURUSD spread ratio: {spread_ratio:.5f}")
|
|
326
327
|
|
|
327
|
-
# Get minimum order
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
print(f"Minimum
|
|
328
|
+
# Get minimum order margin for BUY and SELL
|
|
329
|
+
buy_margin = trader.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
330
|
+
sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
331
|
+
print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
|
|
332
|
+
print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
|
|
333
|
+
|
|
334
|
+
# Calculate volume by margin
|
|
335
|
+
available_margin = 1000.0
|
|
336
|
+
max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
|
|
337
|
+
max_sell_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "SELL")
|
|
338
|
+
print(f"Max BUY volume for ${available_margin}: {max_buy_volume}")
|
|
339
|
+
print(f"Max SELL volume for ${available_margin}: {max_sell_volume}")
|
|
331
340
|
|
|
332
341
|
# Get recent OHLC data with custom timeframe
|
|
333
342
|
rates_df = trader.fetch_latest_rates_as_df(
|
|
@@ -47,7 +47,7 @@ with client:
|
|
|
47
47
|
# Get current positions as DataFrame
|
|
48
48
|
positions_df = client.get_positions_as_df()
|
|
49
49
|
print(f"Open positions: {len(positions_df)}")
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
# Close positions for specific symbol
|
|
52
52
|
results = client.close_open_positions("EURUSD")
|
|
53
53
|
print(f"Closed positions: {results}")
|
|
@@ -62,7 +62,7 @@ client = Mt5TradingClient(config=config, dry_run=False)
|
|
|
62
62
|
with client:
|
|
63
63
|
# Close all positions for multiple symbols
|
|
64
64
|
results = client.close_open_positions(["EURUSD", "GBPUSD", "USDJPY"])
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
# Close all positions (all symbols)
|
|
67
67
|
all_results = client.close_open_positions()
|
|
68
68
|
```
|
|
@@ -73,19 +73,19 @@ with client:
|
|
|
73
73
|
# Configure different order filling modes
|
|
74
74
|
# IOC (Immediate or Cancel) - default
|
|
75
75
|
client_ioc = Mt5TradingClient(
|
|
76
|
-
config=config,
|
|
76
|
+
config=config,
|
|
77
77
|
order_filling_mode="IOC"
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
# FOK (Fill or Kill)
|
|
81
81
|
client_fok = Mt5TradingClient(
|
|
82
|
-
config=config,
|
|
82
|
+
config=config,
|
|
83
83
|
order_filling_mode="FOK"
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
# RETURN (Return if not filled)
|
|
87
87
|
client_return = Mt5TradingClient(
|
|
88
|
-
config=config,
|
|
88
|
+
config=config,
|
|
89
89
|
order_filling_mode="RETURN"
|
|
90
90
|
)
|
|
91
91
|
```
|
|
@@ -121,7 +121,7 @@ except Mt5TradingError as e:
|
|
|
121
121
|
with client:
|
|
122
122
|
# Check order (note: send_or_check_order is an internal method)
|
|
123
123
|
# For trading operations, use the provided methods like close_open_positions
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
# Example: Check if we can close a position
|
|
126
126
|
positions = client.get_positions_as_df()
|
|
127
127
|
if not positions.empty:
|
|
@@ -196,6 +196,173 @@ The `close_open_positions()` method returns a dictionary with symbols as keys:
|
|
|
196
196
|
- `TRADE_RETCODE_NO_MONEY`: Insufficient funds
|
|
197
197
|
- `TRADE_RETCODE_INVALID_VOLUME`: Invalid trade volume
|
|
198
198
|
|
|
199
|
+
## Margin Calculation Methods
|
|
200
|
+
|
|
201
|
+
The trading client provides advanced margin calculation capabilities:
|
|
202
|
+
|
|
203
|
+
### Calculate Minimum Order Margin
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
with client:
|
|
207
|
+
# Calculate minimum margin required for BUY order
|
|
208
|
+
min_margin_buy = client.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
209
|
+
print(f"Minimum volume: {min_margin_buy['volume']}")
|
|
210
|
+
print(f"Minimum margin: {min_margin_buy['margin']}")
|
|
211
|
+
|
|
212
|
+
# Calculate minimum margin required for SELL order
|
|
213
|
+
min_margin_sell = client.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Calculate Volume by Margin
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
with client:
|
|
220
|
+
# Calculate maximum volume for given margin amount
|
|
221
|
+
available_margin = 1000.0 # USD
|
|
222
|
+
max_volume_buy = client.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
|
|
223
|
+
max_volume_sell = client.calculate_volume_by_margin("EURUSD", available_margin, "SELL")
|
|
224
|
+
|
|
225
|
+
print(f"Max BUY volume for ${available_margin}: {max_volume_buy}")
|
|
226
|
+
print(f"Max SELL volume for ${available_margin}: {max_volume_sell}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Calculate New Position Margin Ratio
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
with client:
|
|
233
|
+
# Calculate margin ratio for potential new position
|
|
234
|
+
margin_ratio = client.calculate_new_position_margin_ratio(
|
|
235
|
+
symbol="EURUSD",
|
|
236
|
+
new_position_side="BUY",
|
|
237
|
+
new_position_volume=1.0
|
|
238
|
+
)
|
|
239
|
+
print(f"New position would use {margin_ratio:.2%} of account equity")
|
|
240
|
+
|
|
241
|
+
# Check if adding position would exceed risk limits
|
|
242
|
+
if margin_ratio > 0.1: # 10% risk limit
|
|
243
|
+
print("Position size too large for risk management")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Market Order Placement
|
|
247
|
+
|
|
248
|
+
Place market orders with flexible configuration:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
with client:
|
|
252
|
+
# Place a BUY market order
|
|
253
|
+
result = client.place_market_order(
|
|
254
|
+
symbol="EURUSD",
|
|
255
|
+
volume=1.0,
|
|
256
|
+
order_side="BUY",
|
|
257
|
+
order_filling_mode="IOC", # Immediate or Cancel
|
|
258
|
+
order_time_mode="GTC", # Good Till Cancelled
|
|
259
|
+
dry_run=False, # Set to True for testing
|
|
260
|
+
comment="My buy order"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Place a SELL market order with FOK filling
|
|
264
|
+
result = client.place_market_order(
|
|
265
|
+
symbol="EURUSD",
|
|
266
|
+
volume=0.5,
|
|
267
|
+
order_side="SELL",
|
|
268
|
+
order_filling_mode="FOK", # Fill or Kill
|
|
269
|
+
dry_run=True # Test mode
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
print(f"Order result: {result}")
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Stop Loss and Take Profit Management
|
|
276
|
+
|
|
277
|
+
Update SL/TP for existing positions:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
with client:
|
|
281
|
+
# Update SL/TP for all EURUSD positions
|
|
282
|
+
results = client.update_sltp_for_open_positions(
|
|
283
|
+
symbol="EURUSD",
|
|
284
|
+
stop_loss=1.0950,
|
|
285
|
+
take_profit=1.1100,
|
|
286
|
+
dry_run=False
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Update only specific positions by ticket
|
|
290
|
+
results = client.update_sltp_for_open_positions(
|
|
291
|
+
symbol="EURUSD",
|
|
292
|
+
stop_loss=1.0950,
|
|
293
|
+
tickets=[123456, 789012], # Specific position tickets
|
|
294
|
+
dry_run=True
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Market Data and Analysis Methods
|
|
299
|
+
|
|
300
|
+
### Spread Analysis
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
with client:
|
|
304
|
+
# Calculate spread ratio for symbol
|
|
305
|
+
spread_ratio = client.calculate_spread_ratio("EURUSD")
|
|
306
|
+
print(f"EURUSD spread ratio: {spread_ratio:.6f}")
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### OHLC Data Retrieval
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
with client:
|
|
313
|
+
# Fetch latest rate data as DataFrame
|
|
314
|
+
rates_df = client.fetch_latest_rates_as_df(
|
|
315
|
+
symbol="EURUSD",
|
|
316
|
+
granularity="M1", # 1-minute bars
|
|
317
|
+
count=1440, # Last 24 hours
|
|
318
|
+
index_keys="time"
|
|
319
|
+
)
|
|
320
|
+
print(f"Latest rates: {rates_df.tail()}")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Tick Data Analysis
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
with client:
|
|
327
|
+
# Fetch recent tick data
|
|
328
|
+
ticks_df = client.fetch_latest_ticks_as_df(
|
|
329
|
+
symbol="EURUSD",
|
|
330
|
+
seconds=300, # Last 5 minutes
|
|
331
|
+
index_keys="time_msc"
|
|
332
|
+
)
|
|
333
|
+
print(f"Tick count: {len(ticks_df)}")
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Position Analytics with Enhanced Metrics
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
with client:
|
|
340
|
+
# Get positions with additional calculated metrics
|
|
341
|
+
positions_df = client.fetch_positions_with_metrics_as_df("EURUSD")
|
|
342
|
+
|
|
343
|
+
if not positions_df.empty:
|
|
344
|
+
print("Position metrics:")
|
|
345
|
+
print(f"Total signed volume: {positions_df['signed_volume'].sum()}")
|
|
346
|
+
print(f"Total signed margin: {positions_df['signed_margin'].sum()}")
|
|
347
|
+
print(f"Average profit ratio: {positions_df['underlier_profit_ratio'].mean():.4f}")
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Deal History Analysis
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
with client:
|
|
354
|
+
# Collect entry deals for analysis
|
|
355
|
+
deals_df = client.collect_entry_deals_as_df(
|
|
356
|
+
symbol="EURUSD",
|
|
357
|
+
history_seconds=3600, # Last hour
|
|
358
|
+
index_keys="ticket"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if not deals_df.empty:
|
|
362
|
+
print(f"Entry deals found: {len(deals_df)}")
|
|
363
|
+
print(f"Deal types: {deals_df['type'].value_counts()}")
|
|
364
|
+
```
|
|
365
|
+
|
|
199
366
|
## Integration with Mt5DataClient
|
|
200
367
|
|
|
201
368
|
Since Mt5TradingClient inherits from Mt5DataClient, all data retrieval methods are available:
|
|
@@ -204,14 +371,25 @@ Since Mt5TradingClient inherits from Mt5DataClient, all data retrieval methods a
|
|
|
204
371
|
with Mt5TradingClient(config=config) as client:
|
|
205
372
|
# Get current positions as DataFrame
|
|
206
373
|
positions_df = client.get_positions_as_df()
|
|
207
|
-
|
|
374
|
+
|
|
208
375
|
# Analyze positions
|
|
209
376
|
if not positions_df.empty:
|
|
210
377
|
# Calculate total exposure
|
|
211
378
|
total_volume = positions_df['volume'].sum()
|
|
212
|
-
|
|
379
|
+
|
|
213
380
|
# Close losing positions
|
|
214
381
|
losing_positions = positions_df[positions_df['profit'] < 0]
|
|
215
382
|
for symbol in losing_positions['symbol'].unique():
|
|
216
383
|
client.close_open_positions(symbol)
|
|
217
|
-
|
|
384
|
+
|
|
385
|
+
# Risk management with margin calculations
|
|
386
|
+
for symbol in ["EURUSD", "GBPUSD", "USDJPY"]:
|
|
387
|
+
# Calculate current margin usage
|
|
388
|
+
current_ratio = client.calculate_new_position_margin_ratio(symbol)
|
|
389
|
+
print(f"{symbol} current margin ratio: {current_ratio:.2%}")
|
|
390
|
+
|
|
391
|
+
# Calculate maximum safe position size
|
|
392
|
+
safe_margin = 500.0 # USD
|
|
393
|
+
max_safe_volume = client.calculate_volume_by_margin(symbol, safe_margin, "BUY")
|
|
394
|
+
print(f"{symbol} max safe volume: {max_safe_volume}")
|
|
395
|
+
```
|
|
@@ -3,6 +3,7 @@
|
|
|
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
9
|
from pydantic import ConfigDict, Field
|
|
@@ -263,41 +264,60 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
263
264
|
)
|
|
264
265
|
return []
|
|
265
266
|
|
|
266
|
-
def
|
|
267
|
-
|
|
267
|
+
def calculate_minimum_order_margin(
|
|
268
|
+
self,
|
|
269
|
+
symbol: str,
|
|
270
|
+
order_side: Literal["BUY", "SELL"],
|
|
271
|
+
) -> dict[str, float]:
|
|
272
|
+
"""Calculate the minimum order margins for a given symbol.
|
|
268
273
|
|
|
269
274
|
Args:
|
|
270
|
-
symbol: Symbol for which to calculate minimum order margins.
|
|
275
|
+
symbol: Symbol for which to calculate the minimum order margins.
|
|
276
|
+
order_side: Optional side of the order, either "BUY" or "SELL".
|
|
271
277
|
|
|
272
278
|
Returns:
|
|
273
|
-
Dictionary with margin
|
|
274
|
-
|
|
275
|
-
Raises:
|
|
276
|
-
Mt5TradingError: If margin calculation fails.
|
|
279
|
+
Dictionary with minimum volume and margin for the specified order side.
|
|
277
280
|
"""
|
|
278
281
|
symbol_info = self.symbol_info_as_dict(symbol=symbol)
|
|
279
282
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
280
|
-
|
|
281
|
-
|
|
283
|
+
return {
|
|
284
|
+
"volume": symbol_info["volume_min"],
|
|
285
|
+
"margin": self.order_calc_margin(
|
|
286
|
+
action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
|
|
287
|
+
symbol=symbol,
|
|
288
|
+
volume=symbol_info["volume_min"],
|
|
289
|
+
price=(
|
|
290
|
+
symbol_info_tick["bid"]
|
|
291
|
+
if order_side == "SELL"
|
|
292
|
+
else symbol_info_tick["ask"]
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def calculate_volume_by_margin(
|
|
298
|
+
self,
|
|
299
|
+
symbol: str,
|
|
300
|
+
margin: float,
|
|
301
|
+
order_side: Literal["BUY", "SELL"],
|
|
302
|
+
) -> float:
|
|
303
|
+
"""Calculate volume based on margin for a given symbol and order side.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
symbol: Symbol for which to calculate the volume.
|
|
307
|
+
margin: Margin amount to use for the calculation.
|
|
308
|
+
order_side: Side of the order, either "BUY" or "SELL".
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Calculated volume as a float.
|
|
312
|
+
"""
|
|
313
|
+
min_order_margin_dict = self.calculate_minimum_order_margin(
|
|
282
314
|
symbol=symbol,
|
|
283
|
-
|
|
284
|
-
price=symbol_info_tick["ask"],
|
|
315
|
+
order_side=order_side,
|
|
285
316
|
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
volume=symbol_info["volume_min"],
|
|
290
|
-
price=symbol_info_tick["bid"],
|
|
317
|
+
return (
|
|
318
|
+
floor(margin / min_order_margin_dict["margin"])
|
|
319
|
+
* min_order_margin_dict["volume"]
|
|
291
320
|
)
|
|
292
|
-
min_order_margins = {"ask": min_ask_order_margin, "bid": min_bid_order_margin}
|
|
293
|
-
self.logger.info("Minimum order margins for %s: %s", symbol, min_order_margins)
|
|
294
|
-
if all(min_order_margins.values()):
|
|
295
|
-
return min_order_margins
|
|
296
|
-
else:
|
|
297
|
-
error_message = (
|
|
298
|
-
f"Failed to calculate minimum order margins for symbol: {symbol}."
|
|
299
|
-
)
|
|
300
|
-
raise Mt5TradingError(error_message)
|
|
301
321
|
|
|
302
322
|
def calculate_spread_ratio(
|
|
303
323
|
self,
|
|
@@ -477,15 +497,15 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
477
497
|
def calculate_new_position_margin_ratio(
|
|
478
498
|
self,
|
|
479
499
|
symbol: str,
|
|
480
|
-
|
|
481
|
-
|
|
500
|
+
new_position_side: Literal["BUY", "SELL"] | None = None,
|
|
501
|
+
new_position_volume: float = 0,
|
|
482
502
|
) -> float:
|
|
483
503
|
"""Calculate the margin ratio for a new position.
|
|
484
504
|
|
|
485
505
|
Args:
|
|
486
506
|
symbol: Symbol for which to calculate the margin ratio.
|
|
487
|
-
|
|
488
|
-
|
|
507
|
+
new_position_side: Side of the new position, either "BUY" or "SELL".
|
|
508
|
+
new_position_volume: Volume of the new position.
|
|
489
509
|
|
|
490
510
|
Returns:
|
|
491
511
|
float: Margin ratio for the new position as a fraction of account equity.
|
|
@@ -499,20 +519,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
499
519
|
positions_df["signed_margin"].sum() if positions_df.size else 0
|
|
500
520
|
)
|
|
501
521
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
502
|
-
if
|
|
522
|
+
if not (new_position_side and new_position_volume):
|
|
503
523
|
new_signed_margin = 0
|
|
504
|
-
elif
|
|
524
|
+
elif new_position_side.upper() == "BUY":
|
|
505
525
|
new_signed_margin = self.order_calc_margin(
|
|
506
526
|
action=self.mt5.ORDER_TYPE_BUY,
|
|
507
527
|
symbol=symbol,
|
|
508
|
-
volume=
|
|
528
|
+
volume=new_position_volume,
|
|
509
529
|
price=symbol_info_tick["ask"],
|
|
510
530
|
)
|
|
511
|
-
elif
|
|
531
|
+
elif new_position_side.upper() == "SELL":
|
|
512
532
|
new_signed_margin = -self.order_calc_margin(
|
|
513
533
|
action=self.mt5.ORDER_TYPE_SELL,
|
|
514
534
|
symbol=symbol,
|
|
515
|
-
volume=
|
|
535
|
+
volume=new_position_volume,
|
|
516
536
|
price=symbol_info_tick["bid"],
|
|
517
537
|
)
|
|
518
538
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdmt5"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.6"
|
|
4
4
|
description = "Pandas-based data handler for MetaTrader 5"
|
|
5
5
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
6
6
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
@@ -845,11 +845,11 @@ class TestMt5TradingClient:
|
|
|
845
845
|
mock_mt5_import.order_check.assert_called_once()
|
|
846
846
|
mock_mt5_import.order_send.assert_not_called()
|
|
847
847
|
|
|
848
|
-
def
|
|
848
|
+
def test_calculate_minimum_order_margin_buy_success(
|
|
849
849
|
self,
|
|
850
850
|
mock_mt5_import: ModuleType,
|
|
851
851
|
) -> None:
|
|
852
|
-
"""Test successful calculation of minimum order
|
|
852
|
+
"""Test successful calculation of minimum order margin for BUY orders."""
|
|
853
853
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
854
854
|
mock_mt5_import.initialize.return_value = True
|
|
855
855
|
client.initialize()
|
|
@@ -866,33 +866,24 @@ class TestMt5TradingClient:
|
|
|
866
866
|
"bid": 1.0998,
|
|
867
867
|
}
|
|
868
868
|
|
|
869
|
-
# Mock order_calc_margin to return successful
|
|
870
|
-
mock_mt5_import.order_calc_margin.
|
|
869
|
+
# Mock order_calc_margin to return successful result
|
|
870
|
+
mock_mt5_import.order_calc_margin.return_value = 100.5
|
|
871
871
|
|
|
872
|
-
result = client.
|
|
872
|
+
result = client.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
873
873
|
|
|
874
|
-
assert result == {"
|
|
875
|
-
|
|
874
|
+
assert result == {"volume": 0.01, "margin": 100.5}
|
|
875
|
+
mock_mt5_import.order_calc_margin.assert_called_once_with(
|
|
876
|
+
mock_mt5_import.ORDER_TYPE_BUY,
|
|
877
|
+
"EURUSD",
|
|
878
|
+
0.01,
|
|
879
|
+
1.1000,
|
|
880
|
+
)
|
|
876
881
|
|
|
877
|
-
|
|
878
|
-
first_call = mock_mt5_import.order_calc_margin.call_args_list[0]
|
|
879
|
-
assert first_call[0][0] == mock_mt5_import.ORDER_TYPE_BUY # action
|
|
880
|
-
assert first_call[0][1] == "EURUSD" # symbol
|
|
881
|
-
assert first_call[0][2] == 0.01 # volume
|
|
882
|
-
assert first_call[0][3] == 1.1000 # price
|
|
883
|
-
|
|
884
|
-
# Verify second call (sell order)
|
|
885
|
-
second_call = mock_mt5_import.order_calc_margin.call_args_list[1]
|
|
886
|
-
assert second_call[0][0] == mock_mt5_import.ORDER_TYPE_SELL # action
|
|
887
|
-
assert second_call[0][1] == "EURUSD" # symbol
|
|
888
|
-
assert second_call[0][2] == 0.01 # volume
|
|
889
|
-
assert second_call[0][3] == 1.0998 # price
|
|
890
|
-
|
|
891
|
-
def test_calculate_minimum_order_margins_failure_ask(
|
|
882
|
+
def test_calculate_minimum_order_margin_sell_success(
|
|
892
883
|
self,
|
|
893
884
|
mock_mt5_import: ModuleType,
|
|
894
885
|
) -> None:
|
|
895
|
-
"""Test
|
|
886
|
+
"""Test successful calculation of minimum order margin for SELL orders."""
|
|
896
887
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
897
888
|
mock_mt5_import.initialize.return_value = True
|
|
898
889
|
client.initialize()
|
|
@@ -909,17 +900,24 @@ class TestMt5TradingClient:
|
|
|
909
900
|
"bid": 1.0998,
|
|
910
901
|
}
|
|
911
902
|
|
|
912
|
-
# Mock order_calc_margin to return
|
|
913
|
-
mock_mt5_import.order_calc_margin.
|
|
903
|
+
# Mock order_calc_margin to return successful result
|
|
904
|
+
mock_mt5_import.order_calc_margin.return_value = 99.8
|
|
914
905
|
|
|
915
|
-
|
|
916
|
-
client.calculate_minimum_order_margins("EURUSD")
|
|
906
|
+
result = client.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
917
907
|
|
|
918
|
-
|
|
908
|
+
assert result == {"volume": 0.01, "margin": 99.8}
|
|
909
|
+
mock_mt5_import.order_calc_margin.assert_called_once_with(
|
|
910
|
+
mock_mt5_import.ORDER_TYPE_SELL,
|
|
911
|
+
"EURUSD",
|
|
912
|
+
0.01,
|
|
913
|
+
1.0998,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
def test_calculate_volume_by_margin_buy_success(
|
|
919
917
|
self,
|
|
920
918
|
mock_mt5_import: ModuleType,
|
|
921
919
|
) -> None:
|
|
922
|
-
"""Test
|
|
920
|
+
"""Test successful calculation of volume by margin for BUY orders."""
|
|
923
921
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
924
922
|
mock_mt5_import.initialize.return_value = True
|
|
925
923
|
client.initialize()
|
|
@@ -936,17 +934,20 @@ class TestMt5TradingClient:
|
|
|
936
934
|
"bid": 1.0998,
|
|
937
935
|
}
|
|
938
936
|
|
|
939
|
-
# Mock order_calc_margin to return
|
|
940
|
-
mock_mt5_import.order_calc_margin.
|
|
937
|
+
# Mock order_calc_margin to return margin for minimum volume
|
|
938
|
+
mock_mt5_import.order_calc_margin.return_value = 100.5
|
|
939
|
+
|
|
940
|
+
result = client.calculate_volume_by_margin("EURUSD", 1000.0, "BUY")
|
|
941
941
|
|
|
942
|
-
|
|
943
|
-
|
|
942
|
+
# Should return floor(1000 / 100.5) * 0.01 = 9 * 0.01 = 0.09
|
|
943
|
+
expected_volume = 9 * 0.01
|
|
944
|
+
assert result == expected_volume
|
|
944
945
|
|
|
945
|
-
def
|
|
946
|
+
def test_calculate_volume_by_margin_sell_success(
|
|
946
947
|
self,
|
|
947
948
|
mock_mt5_import: ModuleType,
|
|
948
949
|
) -> None:
|
|
949
|
-
"""Test
|
|
950
|
+
"""Test successful calculation of volume by margin for SELL orders."""
|
|
950
951
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
951
952
|
mock_mt5_import.initialize.return_value = True
|
|
952
953
|
client.initialize()
|
|
@@ -963,11 +964,14 @@ class TestMt5TradingClient:
|
|
|
963
964
|
"bid": 1.0998,
|
|
964
965
|
}
|
|
965
966
|
|
|
966
|
-
# Mock order_calc_margin to return
|
|
967
|
-
mock_mt5_import.order_calc_margin.
|
|
967
|
+
# Mock order_calc_margin to return margin for minimum volume
|
|
968
|
+
mock_mt5_import.order_calc_margin.return_value = 99.8
|
|
968
969
|
|
|
969
|
-
|
|
970
|
-
|
|
970
|
+
result = client.calculate_volume_by_margin("EURUSD", 500.0, "SELL")
|
|
971
|
+
|
|
972
|
+
# Should return floor(500 / 99.8) * 0.01 = 5 * 0.01 = 0.05
|
|
973
|
+
expected_volume = 5 * 0.01
|
|
974
|
+
assert result == expected_volume
|
|
971
975
|
|
|
972
976
|
def test_calculate_spread_ratio(
|
|
973
977
|
self,
|
|
@@ -1358,7 +1362,7 @@ class TestMt5TradingClient:
|
|
|
1358
1362
|
}
|
|
1359
1363
|
|
|
1360
1364
|
result = client.calculate_new_position_margin_ratio(
|
|
1361
|
-
symbol="EURUSD",
|
|
1365
|
+
symbol="EURUSD", new_position_side="BUY", new_position_volume=0.1
|
|
1362
1366
|
)
|
|
1363
1367
|
|
|
1364
1368
|
assert result == 0.0
|
|
@@ -1409,7 +1413,7 @@ class TestMt5TradingClient:
|
|
|
1409
1413
|
mock_mt5_import.order_calc_margin.return_value = 1000.0
|
|
1410
1414
|
|
|
1411
1415
|
result = client.calculate_new_position_margin_ratio(
|
|
1412
|
-
symbol="EURUSD",
|
|
1416
|
+
symbol="EURUSD", new_position_side="BUY", new_position_volume=0.1
|
|
1413
1417
|
)
|
|
1414
1418
|
|
|
1415
1419
|
# Should return (new_margin + current_margin) / equity
|
|
@@ -1444,7 +1448,7 @@ class TestMt5TradingClient:
|
|
|
1444
1448
|
mock_mt5_import.order_calc_margin.return_value = 1000.0
|
|
1445
1449
|
|
|
1446
1450
|
result = client.calculate_new_position_margin_ratio(
|
|
1447
|
-
symbol="EURUSD",
|
|
1451
|
+
symbol="EURUSD", new_position_side="SELL", new_position_volume=0.1
|
|
1448
1452
|
)
|
|
1449
1453
|
|
|
1450
1454
|
# Should return abs(-new_margin / equity) for sell
|
|
@@ -1475,10 +1479,10 @@ class TestMt5TradingClient:
|
|
|
1475
1479
|
}
|
|
1476
1480
|
|
|
1477
1481
|
result = client.calculate_new_position_margin_ratio(
|
|
1478
|
-
symbol="EURUSD",
|
|
1482
|
+
symbol="EURUSD", new_position_side="BUY", new_position_volume=0
|
|
1479
1483
|
)
|
|
1480
1484
|
|
|
1481
|
-
# Should return 0 since
|
|
1485
|
+
# Should return 0 since new_position_volume is 0
|
|
1482
1486
|
assert result == 0.0
|
|
1483
1487
|
|
|
1484
1488
|
def test_calculate_new_position_margin_ratio_invalid_side(
|
|
@@ -1505,12 +1509,44 @@ class TestMt5TradingClient:
|
|
|
1505
1509
|
}
|
|
1506
1510
|
|
|
1507
1511
|
result = client.calculate_new_position_margin_ratio(
|
|
1508
|
-
symbol="EURUSD",
|
|
1512
|
+
symbol="EURUSD", new_position_side=None, new_position_volume=0.1
|
|
1509
1513
|
)
|
|
1510
1514
|
|
|
1511
1515
|
# Should return 0 since side is invalid
|
|
1512
1516
|
assert result == 0.0
|
|
1513
1517
|
|
|
1518
|
+
def test_calculate_new_position_margin_ratio_invalid_side_string(
|
|
1519
|
+
self, mock_mt5_import: ModuleType
|
|
1520
|
+
) -> None:
|
|
1521
|
+
"""Test calculating margin ratio with invalid side string."""
|
|
1522
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1523
|
+
mock_mt5_import.initialize.return_value = True
|
|
1524
|
+
client.initialize()
|
|
1525
|
+
|
|
1526
|
+
# Mock account info
|
|
1527
|
+
mock_mt5_import.account_info.return_value._asdict.return_value = {
|
|
1528
|
+
"equity": 10000.0,
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
# Mock empty positions
|
|
1532
|
+
mock_mt5_import.positions_get.return_value = []
|
|
1533
|
+
|
|
1534
|
+
# Mock symbol tick info
|
|
1535
|
+
mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
|
|
1536
|
+
"time": pd.Timestamp("2009-02-14 00:31:30"),
|
|
1537
|
+
"ask": 1.1002,
|
|
1538
|
+
"bid": 1.1000,
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
result = client.calculate_new_position_margin_ratio(
|
|
1542
|
+
symbol="EURUSD",
|
|
1543
|
+
new_position_side="INVALID", # type: ignore[arg-type]
|
|
1544
|
+
new_position_volume=0.1,
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
# Should return 0 since side is invalid string
|
|
1548
|
+
assert result == 0.0
|
|
1549
|
+
|
|
1514
1550
|
def test_update_sltp_for_open_positions(self, mock_mt5_import: ModuleType) -> None:
|
|
1515
1551
|
"""Test update_sltp_for_open_positions method."""
|
|
1516
1552
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|