pdmt5 0.2.1__py3-none-any.whl → 0.2.3__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/dataframe.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import time
6
6
  from datetime import datetime # noqa: TC003
7
- from typing import Any
7
+ from typing import Any, cast
8
8
 
9
9
  import pandas as pd
10
10
  from pydantic import BaseModel, ConfigDict, Field
@@ -73,12 +73,10 @@ class Mt5DataClient(Mt5Client):
73
73
  Mt5RuntimeError: If initialization fails after retries.
74
74
  """
75
75
  path = path or self.config.path
76
- login_kwargs = {
77
- "login": login or self.config.login,
78
- "password": password or self.config.password,
79
- "server": server or self.config.server,
80
- "timeout": timeout or self.config.timeout,
81
- }
76
+ login_value = login if login is not None else self.config.login
77
+ password_value = password if password is not None else self.config.password
78
+ server_value = server if server is not None else self.config.server
79
+ timeout_value = timeout if timeout is not None else self.config.timeout
82
80
  for i in range(1 + max(0, self.retry_count)):
83
81
  if i:
84
82
  self.logger.warning(
@@ -87,8 +85,20 @@ class Mt5DataClient(Mt5Client):
87
85
  self.retry_count,
88
86
  )
89
87
  time.sleep(i)
90
- if self.initialize(path=path, **login_kwargs) and (
91
- (not login_kwargs["login"]) or self.login(**login_kwargs)
88
+ if self.initialize(
89
+ path=path,
90
+ login=login_value,
91
+ password=password_value,
92
+ server=server_value,
93
+ timeout=timeout_value,
94
+ ) and (
95
+ login_value is None
96
+ or self.login(
97
+ login=login_value,
98
+ password=password_value,
99
+ server=server_value,
100
+ timeout=timeout_value,
101
+ )
92
102
  ):
93
103
  return
94
104
  error_message = (
@@ -359,14 +369,17 @@ class Mt5DataClient(Mt5Client):
359
369
  Returns:
360
370
  List of dictionaries with OHLCV data.
361
371
  """
362
- return self.copy_rates_from_as_df(
363
- symbol=symbol,
364
- timeframe=timeframe,
365
- date_from=date_from,
366
- count=count,
367
- skip_to_datetime=skip_to_datetime,
368
- index_keys=None,
369
- ).to_dict(orient="records")
372
+ return cast(
373
+ "list[dict[str, Any]]",
374
+ self.copy_rates_from_as_df(
375
+ symbol=symbol,
376
+ timeframe=timeframe,
377
+ date_from=date_from,
378
+ count=count,
379
+ skip_to_datetime=skip_to_datetime,
380
+ index_keys=None,
381
+ ).to_dict(orient="records"),
382
+ )
370
383
 
371
384
  @set_index_if_possible(index_parameters="index_keys")
372
385
  @detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
@@ -422,14 +435,17 @@ class Mt5DataClient(Mt5Client):
422
435
  Returns:
423
436
  List of dictionaries with OHLCV data.
424
437
  """
425
- return self.copy_rates_from_pos_as_df(
426
- symbol=symbol,
427
- timeframe=timeframe,
428
- start_pos=start_pos,
429
- count=count,
430
- skip_to_datetime=skip_to_datetime,
431
- index_keys=None,
432
- ).to_dict(orient="records")
438
+ return cast(
439
+ "list[dict[str, Any]]",
440
+ self.copy_rates_from_pos_as_df(
441
+ symbol=symbol,
442
+ timeframe=timeframe,
443
+ start_pos=start_pos,
444
+ count=count,
445
+ skip_to_datetime=skip_to_datetime,
446
+ index_keys=None,
447
+ ).to_dict(orient="records"),
448
+ )
433
449
 
434
450
  @set_index_if_possible(index_parameters="index_keys")
435
451
  @detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
@@ -486,14 +502,17 @@ class Mt5DataClient(Mt5Client):
486
502
  Returns:
487
503
  List of dictionaries with OHLCV data.
488
504
  """
489
- return self.copy_rates_range_as_df(
490
- symbol=symbol,
491
- timeframe=timeframe,
492
- date_from=date_from,
493
- date_to=date_to,
494
- skip_to_datetime=skip_to_datetime,
495
- index_keys=None,
496
- ).to_dict(orient="records")
505
+ return cast(
506
+ "list[dict[str, Any]]",
507
+ self.copy_rates_range_as_df(
508
+ symbol=symbol,
509
+ timeframe=timeframe,
510
+ date_from=date_from,
511
+ date_to=date_to,
512
+ skip_to_datetime=skip_to_datetime,
513
+ index_keys=None,
514
+ ).to_dict(orient="records"),
515
+ )
497
516
 
498
517
  @set_index_if_possible(index_parameters="index_keys")
499
518
  @detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
@@ -549,14 +568,17 @@ class Mt5DataClient(Mt5Client):
549
568
  Returns:
550
569
  List of dictionaries with tick data.
551
570
  """
552
- return self.copy_ticks_from_as_df(
553
- symbol=symbol,
554
- date_from=date_from,
555
- count=count,
556
- flags=flags,
557
- skip_to_datetime=skip_to_datetime,
558
- index_keys=None,
559
- ).to_dict(orient="records") # type: ignore[reportReturnType]
571
+ return cast(
572
+ "list[dict[str, Any]]",
573
+ self.copy_ticks_from_as_df(
574
+ symbol=symbol,
575
+ date_from=date_from,
576
+ count=count,
577
+ flags=flags,
578
+ skip_to_datetime=skip_to_datetime,
579
+ index_keys=None,
580
+ ).to_dict(orient="records"),
581
+ )
560
582
 
561
583
  @set_index_if_possible(index_parameters="index_keys")
562
584
  @detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
@@ -612,14 +634,17 @@ class Mt5DataClient(Mt5Client):
612
634
  Returns:
613
635
  List of dictionaries with tick data.
614
636
  """
615
- return self.copy_ticks_range_as_df(
616
- symbol=symbol,
617
- date_from=date_from,
618
- date_to=date_to,
619
- flags=flags,
620
- skip_to_datetime=skip_to_datetime,
621
- index_keys=None,
622
- ).to_dict(orient="records") # type: ignore[reportReturnType]
637
+ return cast(
638
+ "list[dict[str, Any]]",
639
+ self.copy_ticks_range_as_df(
640
+ symbol=symbol,
641
+ date_from=date_from,
642
+ date_to=date_to,
643
+ flags=flags,
644
+ skip_to_datetime=skip_to_datetime,
645
+ index_keys=None,
646
+ ).to_dict(orient="records"),
647
+ )
623
648
 
624
649
  @set_index_if_possible(index_parameters="index_keys")
625
650
  @detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
@@ -1110,10 +1135,11 @@ class Mt5DataClient(Mt5Client):
1110
1135
  Returns:
1111
1136
  Flattened dictionary.
1112
1137
  """
1113
- items = []
1138
+ items: list[tuple[str, Any]] = []
1114
1139
  for k, v in dictionary.items():
1115
1140
  if isinstance(v, dict):
1116
- items.extend((f"{k}{sep}{sk}", sv) for sk, sv in v.items())
1141
+ nested = cast("dict[str, Any]", v)
1142
+ items.extend((f"{k}{sep}{sk}", sv) for sk, sv in nested.items())
1117
1143
  else:
1118
1144
  items.append((k, v))
1119
1145
  return dict(items)
pdmt5/mt5.py CHANGED
@@ -8,7 +8,7 @@ import importlib
8
8
  import logging
9
9
  from functools import wraps
10
10
  from types import ModuleType # noqa: TC003
11
- from typing import TYPE_CHECKING, Any, Self
11
+ from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, Self, TypeVar
12
12
 
13
13
  from pydantic import BaseModel, ConfigDict, Field
14
14
 
@@ -17,6 +17,9 @@ if TYPE_CHECKING:
17
17
  from datetime import datetime
18
18
  from types import TracebackType
19
19
 
20
+ P = ParamSpec("P")
21
+ R = TypeVar("R")
22
+
20
23
 
21
24
  class Mt5RuntimeError(RuntimeError):
22
25
  """MetaTrader5 specific runtime error.
@@ -46,7 +49,9 @@ class Mt5Client(BaseModel):
46
49
  _is_initialized: bool = False
47
50
 
48
51
  @staticmethod
49
- def _log_mt5_last_status_code(func: Callable[..., Any]) -> Callable[..., Any]:
52
+ def _log_mt5_last_status_code(
53
+ func: Callable[Concatenate[Mt5Client, P], R],
54
+ ) -> Callable[Concatenate[Mt5Client, P], R]:
50
55
  """Decorator to log MetaTrader5 last status code after method execution.
51
56
 
52
57
  Args:
@@ -57,7 +62,11 @@ class Mt5Client(BaseModel):
57
62
  """
58
63
 
59
64
  @wraps(func)
60
- def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
65
+ def wrapper(
66
+ self: Mt5Client,
67
+ *args: P.args,
68
+ **kwargs: P.kwargs,
69
+ ) -> R:
61
70
  try:
62
71
  response = func(self, *args, **kwargs)
63
72
  except Exception as e:
pdmt5/trading.py CHANGED
@@ -589,30 +589,31 @@ class Mt5TradingClient(Mt5DataClient):
589
589
  price=symbol_info_tick["bid"],
590
590
  )
591
591
  result = (
592
- positions_df.assign(
592
+ positions_df
593
+ .assign(
593
594
  elapsed_seconds=lambda d: (
594
595
  symbol_info_tick["time"] - d["time"]
595
596
  ).dt.total_seconds(),
596
597
  underlier_increase_ratio=lambda d: (
597
598
  d["price_current"] / d["price_open"] - 1
598
599
  ),
599
- buy=lambda d: (d["type"] == self.mt5.POSITION_TYPE_BUY),
600
- sell=lambda d: (d["type"] == self.mt5.POSITION_TYPE_SELL),
600
+ buy=lambda d: d["type"] == self.mt5.POSITION_TYPE_BUY,
601
+ sell=lambda d: d["type"] == self.mt5.POSITION_TYPE_SELL,
601
602
  )
602
603
  .assign(
603
604
  buy_i=lambda d: d["buy"].astype(int),
604
605
  sell_i=lambda d: d["sell"].astype(int),
605
606
  )
606
607
  .assign(
607
- sign=lambda d: (d["buy_i"] - d["sell_i"]),
608
+ sign=lambda d: d["buy_i"] - d["sell_i"],
608
609
  margin=lambda d: (
609
610
  (d["buy_i"] * ask_margin + d["sell_i"] * bid_margin)
610
611
  * d["volume"]
611
612
  ),
612
613
  )
613
614
  .assign(
614
- signed_volume=lambda d: (d["volume"] * d["sign"]),
615
- signed_margin=lambda d: (d["margin"] * d["sign"]),
615
+ signed_volume=lambda d: d["volume"] * d["sign"],
616
+ signed_margin=lambda d: d["margin"] * d["sign"],
616
617
  underlier_profit_ratio=lambda d: (
617
618
  d["underlier_increase_ratio"] * d["sign"]
618
619
  ),
pdmt5/utils.py CHANGED
@@ -3,17 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from functools import wraps
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
7
7
 
8
8
  import pandas as pd
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Callable
12
12
 
13
+ P = ParamSpec("P")
14
+ R = TypeVar("R")
15
+
13
16
 
14
17
  def detect_and_convert_time_to_datetime(
15
18
  skip_toggle: str | None = None,
16
- ) -> Callable[..., Any]:
19
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
17
20
  """Decorator to convert time values/columns to datetime based on result type.
18
21
 
19
22
  Automatically detects result type and applies appropriate time conversion:
@@ -28,25 +31,32 @@ def detect_and_convert_time_to_datetime(
28
31
  Decorator function.
29
32
  """
30
33
 
31
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
34
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
32
35
  @wraps(func)
33
- def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
36
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
34
37
  result = func(*args, **kwargs)
35
38
  if skip_toggle and kwargs.get(skip_toggle):
36
39
  return result
37
40
  elif isinstance(result, dict):
38
- return _convert_time_values_in_dict(dictionary=result)
41
+ return cast(
42
+ "R",
43
+ _convert_time_values_in_dict(
44
+ dictionary=cast("dict[str, Any]", result)
45
+ ),
46
+ )
39
47
  elif isinstance(result, list):
40
48
  return [
41
49
  (
42
- _convert_time_values_in_dict(dictionary=d)
50
+ _convert_time_values_in_dict(
51
+ dictionary=cast("dict[str, Any]", d)
52
+ )
43
53
  if isinstance(d, dict)
44
54
  else d
45
55
  )
46
- for d in result
47
- ]
56
+ for d in cast("list[Any]", result)
57
+ ] # type: ignore[return-value]
48
58
  elif isinstance(result, pd.DataFrame):
49
- return _convert_time_columns_in_df(result)
59
+ return cast("R", _convert_time_columns_in_df(result))
50
60
  else:
51
61
  return result
52
62
 
@@ -87,13 +97,15 @@ def _convert_time_columns_in_df(df: pd.DataFrame) -> pd.DataFrame:
87
97
  new_df = df.copy()
88
98
  for c in new_df.columns:
89
99
  if c.startswith("time_") and c.endswith("_msc"):
90
- new_df[c] = pd.to_datetime(new_df[c], unit="ms")
100
+ new_df[c] = pd.to_datetime(new_df[c], unit="ms").astype("datetime64[ns]")
91
101
  elif c == "time" or c.startswith("time_"):
92
- new_df[c] = pd.to_datetime(new_df[c], unit="s")
102
+ new_df[c] = pd.to_datetime(new_df[c], unit="s").astype("datetime64[ns]")
93
103
  return new_df
94
104
 
95
105
 
96
- def set_index_if_possible(index_parameters: str | None = None) -> Callable[..., Any]:
106
+ def set_index_if_possible(
107
+ index_parameters: str | None = None,
108
+ ) -> Callable[[Callable[P, pd.DataFrame]], Callable[P, pd.DataFrame]]:
97
109
  """Decorator to set index on DataFrame results if not empty.
98
110
 
99
111
  Args:
@@ -103,11 +115,13 @@ def set_index_if_possible(index_parameters: str | None = None) -> Callable[...,
103
115
  Decorator function.
104
116
  """
105
117
 
106
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
118
+ def decorator(
119
+ func: Callable[P, pd.DataFrame],
120
+ ) -> Callable[P, pd.DataFrame]:
107
121
  @wraps(func)
108
- def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
122
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> pd.DataFrame:
109
123
  result = func(*args, **kwargs)
110
- if not isinstance(result, pd.DataFrame):
124
+ if not isinstance(result, pd.DataFrame): # type: ignore[reportUnnecessaryIsInstance]
111
125
  error_message = (
112
126
  f"Function {func.__name__} returned non-DataFrame result: "
113
127
  f"{type(result).__name__}. Expected DataFrame."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.2.1
3
+ Version: 0.2.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>
@@ -339,7 +339,7 @@ with Mt5TradingClient(config=config) as trader:
339
339
  sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
340
340
  print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
341
341
  print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
342
-
342
+
343
343
  # Calculate volume by margin
344
344
  available_margin = 1000.0
345
345
  max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
@@ -0,0 +1,9 @@
1
+ pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
2
+ pdmt5/dataframe.py,sha256=ju0q8z_hsZeuDb4eWj1SFIzmp5fQDX6UfB57bX_kqsg,38975
3
+ pdmt5/mt5.py,sha256=n_VpAC7CTTO0DcPVQ1sUhp2RfQVRsps8PuhEGY4f1Y0,32438
4
+ pdmt5/trading.py,sha256=OFBkONLTrut9aWPvi0-JJMVdoaZBFEsVIC8ZrErqfY8,25576
5
+ pdmt5/utils.py,sha256=8hSokAi8yJSmvOSMEAdgOKBazUpD_S7ui5chvc0RYp0,4438
6
+ pdmt5-0.2.3.dist-info/METADATA,sha256=GPRkjCqVSEuW22FFrPcX-i3bTnD78oLMGbrPQtTL5E0,16096
7
+ pdmt5-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ pdmt5-0.2.3.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
+ pdmt5-0.2.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,9 +0,0 @@
1
- pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
2
- pdmt5/dataframe.py,sha256=rUWtR23hrXBdBqzJhbOlIemNy73RrjSTZZJUhwoL6io,38084
3
- pdmt5/mt5.py,sha256=KgxHapIrh5b4L0wIOAQIjfXNZafalihbFrh9fhYHmrI,32254
4
- pdmt5/trading.py,sha256=Qd4RhZprDcWTzT3JmKl8XGVq8i9hExNdPSJbCRdUx-s,25569
5
- pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
- pdmt5-0.2.1.dist-info/METADATA,sha256=OjDjumI_5kGHyEjpIg-xgZyGJSULRUJM_LQnv2IeJ-4,16100
7
- pdmt5-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- pdmt5-0.2.1.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
- pdmt5-0.2.1.dist-info/RECORD,,