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.
Files changed (31) hide show
  1. {pdmt5-0.1.2 → pdmt5-0.1.3}/PKG-INFO +1 -1
  2. {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/trading.py +86 -1
  3. {pdmt5-0.1.2 → pdmt5-0.1.3}/pyproject.toml +1 -1
  4. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_trading.py +146 -0
  5. {pdmt5-0.1.2 → pdmt5-0.1.3}/uv.lock +1 -1
  6. {pdmt5-0.1.2 → pdmt5-0.1.3}/.claude/settings.json +0 -0
  7. {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/FUNDING.yml +0 -0
  8. {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/copilot-instructions.md +0 -0
  9. {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/dependabot.yml +0 -0
  10. {pdmt5-0.1.2 → pdmt5-0.1.3}/.github/workflows/ci.yml +0 -0
  11. {pdmt5-0.1.2 → pdmt5-0.1.3}/.gitignore +0 -0
  12. {pdmt5-0.1.2 → pdmt5-0.1.3}/CLAUDE.md +0 -0
  13. {pdmt5-0.1.2 → pdmt5-0.1.3}/LICENSE +0 -0
  14. {pdmt5-0.1.2 → pdmt5-0.1.3}/README.md +0 -0
  15. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/dataframe.md +0 -0
  16. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/index.md +0 -0
  17. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/mt5.md +0 -0
  18. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/trading.md +0 -0
  19. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/api/utils.md +0 -0
  20. {pdmt5-0.1.2 → pdmt5-0.1.3}/docs/index.md +0 -0
  21. {pdmt5-0.1.2 → pdmt5-0.1.3}/mkdocs.yml +0 -0
  22. {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/__init__.py +0 -0
  23. {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/dataframe.py +0 -0
  24. {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/mt5.py +0 -0
  25. {pdmt5-0.1.2 → pdmt5-0.1.3}/pdmt5/utils.py +0 -0
  26. {pdmt5-0.1.2 → pdmt5-0.1.3}/renovate.json +0 -0
  27. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/__init__.py +0 -0
  28. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_dataframe.py +0 -0
  29. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_init.py +0 -0
  30. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_mt5.py +0 -0
  31. {pdmt5-0.1.2 → pdmt5-0.1.3}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.1.2
3
+ Version: 0.1.3
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>
@@ -2,13 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Literal
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.2"
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
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.1.2"
616
+ version = "0.1.3"
617
617
  source = { editable = "." }
618
618
  dependencies = [
619
619
  { name = "metatrader5", marker = "sys_platform == 'win32'" },
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