pdmt5 0.1.2__tar.gz → 0.1.3__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.2 → pdmt5-0.1.3}/PKG-INFO +1 -1
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/trading.py +86 -1
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pyproject.toml +1 -1
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_trading.py +146 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/uv.lock +1 -1
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.claude/settings.json +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/workflows/ci.yml +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/.gitignore +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/CLAUDE.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/LICENSE +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/README.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/index.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/trading.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/utils.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/index.md +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/mkdocs.yml +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/dataframe.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/mt5.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/renovate.json +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/__init__.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_dataframe.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_init.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_mt5.py +0 -0
- {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_utils.py +0 -0
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
7
|
|
|
7
8
|
from pydantic import ConfigDict, Field
|
|
8
9
|
|
|
9
10
|
from .dataframe import Mt5DataClient
|
|
10
11
|
from .mt5 import Mt5RuntimeError
|
|
11
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
class Mt5TradingError(Mt5RuntimeError):
|
|
14
18
|
"""MetaTrader5 trading error."""
|
|
@@ -190,3 +194,84 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
190
194
|
f"Failed to calculate minimum order margins for symbol: {symbol}."
|
|
191
195
|
)
|
|
192
196
|
raise Mt5TradingError(error_message)
|
|
197
|
+
|
|
198
|
+
def calculate_spread_ratio(
|
|
199
|
+
self,
|
|
200
|
+
symbol: str,
|
|
201
|
+
) -> float:
|
|
202
|
+
"""Calculate the spread ratio for a given symbol.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
symbol: Symbol for which to calculate the spread ratio.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Spread ratio as a float.
|
|
209
|
+
"""
|
|
210
|
+
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
211
|
+
return (
|
|
212
|
+
(symbol_info_tick["ask"] - symbol_info_tick["bid"])
|
|
213
|
+
/ (symbol_info_tick["ask"] + symbol_info_tick["bid"])
|
|
214
|
+
* 2
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def copy_latest_rates_as_df(
|
|
218
|
+
self,
|
|
219
|
+
symbol: str,
|
|
220
|
+
granularity: str = "M1",
|
|
221
|
+
count: int = 1440,
|
|
222
|
+
index_keys: str | None = "time",
|
|
223
|
+
) -> pd.DataFrame:
|
|
224
|
+
"""Fetch rate (OHLC) data as a DataFrame.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
symbol: Symbol to fetch data for.
|
|
228
|
+
granularity: Time granularity as a timeframe suffix (e.g., "M1", "H1").
|
|
229
|
+
count: Number of bars to fetch.
|
|
230
|
+
index_keys: Optional index keys for the DataFrame.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
pd.DataFrame: OHLC data with time index.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
Mt5TradingError: If the granularity is not supported by MetaTrader5.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
timeframe = getattr(self.mt5, f"TIMEFRAME_{granularity.upper()}")
|
|
240
|
+
except AttributeError as e:
|
|
241
|
+
error_message = (
|
|
242
|
+
f"MetaTrader5 does not support the given granularity: {granularity}"
|
|
243
|
+
)
|
|
244
|
+
raise Mt5TradingError(error_message) from e
|
|
245
|
+
else:
|
|
246
|
+
return self.copy_rates_from_pos_as_df(
|
|
247
|
+
symbol=symbol,
|
|
248
|
+
timeframe=timeframe,
|
|
249
|
+
start_pos=0,
|
|
250
|
+
count=count,
|
|
251
|
+
index_keys=index_keys,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def copy_latest_ticks_as_df(
|
|
255
|
+
self,
|
|
256
|
+
symbol: str,
|
|
257
|
+
seconds: int = 300,
|
|
258
|
+
index_keys: str | None = "time_msc",
|
|
259
|
+
) -> pd.DataFrame:
|
|
260
|
+
"""Fetch tick data as a DataFrame.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
symbol: Symbol to fetch tick data for.
|
|
264
|
+
seconds: Time range in seconds to fetch ticks around the last tick time.
|
|
265
|
+
index_keys: Optional index keys for the DataFrame.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
pd.DataFrame: Tick data with time index.
|
|
269
|
+
"""
|
|
270
|
+
last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
|
|
271
|
+
return self.copy_ticks_range_as_df(
|
|
272
|
+
symbol=symbol,
|
|
273
|
+
date_from=(last_tick_time - timedelta(seconds=seconds)),
|
|
274
|
+
date_to=(last_tick_time + timedelta(seconds=seconds)),
|
|
275
|
+
flags=self.mt5.COPY_TICKS_ALL,
|
|
276
|
+
index_keys=index_keys,
|
|
277
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdmt5"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
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"}]
|
|
@@ -7,6 +7,7 @@ from collections.abc import Generator
|
|
|
7
7
|
from types import ModuleType
|
|
8
8
|
from typing import NamedTuple
|
|
9
9
|
|
|
10
|
+
import numpy as np
|
|
10
11
|
import pytest
|
|
11
12
|
from pydantic import ValidationError
|
|
12
13
|
from pytest_mock import MockerFixture
|
|
@@ -54,6 +55,8 @@ def mock_mt5_import(
|
|
|
54
55
|
mock_mt5.order_check = mocker.MagicMock() # type: ignore[attr-defined]
|
|
55
56
|
mock_mt5.order_send = mocker.MagicMock() # type: ignore[attr-defined]
|
|
56
57
|
mock_mt5.order_calc_margin = mocker.MagicMock() # type: ignore[attr-defined]
|
|
58
|
+
mock_mt5.copy_rates_from_pos = mocker.MagicMock() # type: ignore[attr-defined]
|
|
59
|
+
mock_mt5.copy_ticks_range = mocker.MagicMock() # type: ignore[attr-defined]
|
|
57
60
|
|
|
58
61
|
# Trading-specific constants
|
|
59
62
|
mock_mt5.TRADE_ACTION_DEAL = 1
|
|
@@ -909,3 +912,146 @@ class TestMt5TradingClient:
|
|
|
909
912
|
|
|
910
913
|
with pytest.raises(Mt5TradingError):
|
|
911
914
|
client.calculate_minimum_order_margins("EURUSD")
|
|
915
|
+
|
|
916
|
+
def test_calculate_spread_ratio(
|
|
917
|
+
self,
|
|
918
|
+
mock_mt5_import: ModuleType,
|
|
919
|
+
) -> None:
|
|
920
|
+
"""Test calculation of spread ratio."""
|
|
921
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
922
|
+
mock_mt5_import.initialize.return_value = True
|
|
923
|
+
client.initialize()
|
|
924
|
+
|
|
925
|
+
# Mock symbol tick info
|
|
926
|
+
mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
|
|
927
|
+
"ask": 1.1002,
|
|
928
|
+
"bid": 1.1000,
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
result = client.calculate_spread_ratio("EURUSD")
|
|
932
|
+
|
|
933
|
+
# Expected calculation: (1.1002 - 1.1000) / (1.1002 + 1.1000) * 2
|
|
934
|
+
expected = (1.1002 - 1.1000) / (1.1002 + 1.1000) * 2
|
|
935
|
+
assert result == expected
|
|
936
|
+
mock_mt5_import.symbol_info_tick.assert_called_once_with("EURUSD")
|
|
937
|
+
|
|
938
|
+
def test_copy_latest_rates_as_df_success(
|
|
939
|
+
self,
|
|
940
|
+
mock_mt5_import: ModuleType,
|
|
941
|
+
) -> None:
|
|
942
|
+
"""Test successful fetching of rate data as DataFrame."""
|
|
943
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
944
|
+
mock_mt5_import.initialize.return_value = True
|
|
945
|
+
client.initialize()
|
|
946
|
+
|
|
947
|
+
# Mock TIMEFRAME constant
|
|
948
|
+
mock_mt5_import.TIMEFRAME_M1 = 1
|
|
949
|
+
|
|
950
|
+
# Create structured array that mimics MT5 rates structure
|
|
951
|
+
rates_dtype = np.dtype([
|
|
952
|
+
("time", "i8"),
|
|
953
|
+
("open", "f8"),
|
|
954
|
+
("high", "f8"),
|
|
955
|
+
("low", "f8"),
|
|
956
|
+
("close", "f8"),
|
|
957
|
+
("tick_volume", "i8"),
|
|
958
|
+
("spread", "i4"),
|
|
959
|
+
("real_volume", "i8"),
|
|
960
|
+
])
|
|
961
|
+
|
|
962
|
+
mock_rates_data = np.array(
|
|
963
|
+
[
|
|
964
|
+
(1234567890, 1.1000, 1.1010, 1.0990, 1.1005, 100, 2, 10000),
|
|
965
|
+
],
|
|
966
|
+
dtype=rates_dtype,
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
mock_mt5_import.copy_rates_from_pos.return_value = mock_rates_data
|
|
970
|
+
|
|
971
|
+
result = client.copy_latest_rates_as_df("EURUSD", granularity="M1", count=10)
|
|
972
|
+
|
|
973
|
+
assert result is not None
|
|
974
|
+
mock_mt5_import.copy_rates_from_pos.assert_called_once_with(
|
|
975
|
+
"EURUSD", # symbol
|
|
976
|
+
1, # timeframe
|
|
977
|
+
0, # start_pos
|
|
978
|
+
10, # count
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
def test_copy_latest_rates_as_df_invalid_granularity(
|
|
982
|
+
self,
|
|
983
|
+
mock_mt5_import: ModuleType,
|
|
984
|
+
) -> None:
|
|
985
|
+
"""Test fetching rate data with invalid granularity."""
|
|
986
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
987
|
+
mock_mt5_import.initialize.return_value = True
|
|
988
|
+
client.initialize()
|
|
989
|
+
|
|
990
|
+
# Ensure the attribute doesn't exist for invalid granularity
|
|
991
|
+
if hasattr(mock_mt5_import, "TIMEFRAME_INVALID"):
|
|
992
|
+
delattr(mock_mt5_import, "TIMEFRAME_INVALID")
|
|
993
|
+
|
|
994
|
+
with pytest.raises(
|
|
995
|
+
Mt5TradingError,
|
|
996
|
+
match="MetaTrader5 does not support the given granularity: INVALID",
|
|
997
|
+
):
|
|
998
|
+
client.copy_latest_rates_as_df("EURUSD", granularity="INVALID")
|
|
999
|
+
|
|
1000
|
+
def test_copy_latest_ticks_as_df(
|
|
1001
|
+
self,
|
|
1002
|
+
mock_mt5_import: ModuleType,
|
|
1003
|
+
) -> None:
|
|
1004
|
+
"""Test fetching tick data as DataFrame."""
|
|
1005
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1006
|
+
mock_mt5_import.initialize.return_value = True
|
|
1007
|
+
client.initialize()
|
|
1008
|
+
|
|
1009
|
+
# Mock symbol tick info with time
|
|
1010
|
+
mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
|
|
1011
|
+
"time": 1234567890,
|
|
1012
|
+
"ask": 1.1002,
|
|
1013
|
+
"bid": 1.1000,
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
# Mock copy ticks flag
|
|
1017
|
+
mock_mt5_import.COPY_TICKS_ALL = 1
|
|
1018
|
+
|
|
1019
|
+
# Create structured array that mimics MT5 ticks structure
|
|
1020
|
+
ticks_dtype = np.dtype([
|
|
1021
|
+
("time", "i8"),
|
|
1022
|
+
("bid", "f8"),
|
|
1023
|
+
("ask", "f8"),
|
|
1024
|
+
("last", "f8"),
|
|
1025
|
+
("volume", "i8"),
|
|
1026
|
+
("time_msc", "i8"),
|
|
1027
|
+
("flags", "i4"),
|
|
1028
|
+
("volume_real", "f8"),
|
|
1029
|
+
])
|
|
1030
|
+
|
|
1031
|
+
mock_ticks_data = np.array(
|
|
1032
|
+
[
|
|
1033
|
+
(1234567890, 1.1000, 1.1002, 1.1001, 100, 1234567890000, 0, 100.0),
|
|
1034
|
+
],
|
|
1035
|
+
dtype=ticks_dtype,
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
mock_mt5_import.copy_ticks_range.return_value = mock_ticks_data
|
|
1039
|
+
|
|
1040
|
+
result = client.copy_latest_ticks_as_df("EURUSD", seconds=60)
|
|
1041
|
+
|
|
1042
|
+
assert result is not None
|
|
1043
|
+
# Verify the method was called
|
|
1044
|
+
mock_mt5_import.symbol_info_tick.assert_called_once_with("EURUSD")
|
|
1045
|
+
|
|
1046
|
+
# Verify copy_ticks_range was called with correct arguments
|
|
1047
|
+
call_args = mock_mt5_import.copy_ticks_range.call_args[0]
|
|
1048
|
+
assert call_args[0] == "EURUSD" # symbol
|
|
1049
|
+
assert call_args[3] == 1 # flags (COPY_TICKS_ALL)
|
|
1050
|
+
|
|
1051
|
+
# Verify result has the expected structure
|
|
1052
|
+
assert len(result) == 1
|
|
1053
|
+
# time_msc is likely the index, not a column
|
|
1054
|
+
assert "bid" in result.columns
|
|
1055
|
+
assert "ask" in result.columns
|
|
1056
|
+
assert "last" in result.columns
|
|
1057
|
+
assert "volume" in result.columns
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|