Skip to content

Commit 8b08b1a

Browse files
committed
test calendar
1 parent f01d82b commit 8b08b1a

File tree

4 files changed

+361
-233
lines changed

4 files changed

+361
-233
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,32 @@ def out_deal(deal: mt5.TradeDeal):
126126
out_deals = mt5.history_deals_get(function=out_deal)
127127
```
128128

129+
### Calendar Events
130+
Get economic calendar events from mql5.com using the `calendar_events` function. The function is memoized so
131+
so repeated calls pull event data from cache instead of repeated calls to the server.
132+
133+
Example:
134+
```python
135+
from pymt5adapter.calendar import calendar_events
136+
from pymt5adapter.calendar import Currency
137+
from pymt5adapter.calendar import Importance
138+
139+
symbol = mt5.symbols_get(function=forex_symbol)[0]
140+
one_week = timedelta(weeks=1)
141+
now = datetime.now()
142+
default_one_week_ahead_all_events = calendar_events()
143+
filtered_by_callback = calendar_events(function=lambda e: 'fed' in e['event_name'].lower())
144+
filtered_by_flags = calendar_events(importance=Importance.MEDIUM | Importance.HIGH,
145+
currencies=Currency.USD | Currency.JPY)
146+
filtered_by_strings = calendar_events(importance='medium high',
147+
currencies='usdjpy')
148+
filtered_by_iterables = calendar_events(importance=('medium', ' high'),
149+
currencies=('usd', 'jpy'))
150+
filtered_by_specific_times = calendar_events(time_from=now, time_to=now + one_week)
151+
filtered_by_timedeltas = calendar_events(time_from=(-one_week), time_to=one_week)
152+
filtered_by_timedelta_lookback = calendar_events(-one_week)
153+
calendar_events_in_russian = calendar_events(language='ru')
154+
```
155+
129156
[intellisence_screen]: https://github.com/nicholishen/pymt5adapter/raw/master/images/intellisense_screen.jpg "intellisence example"
130157
[docs_screen]: https://github.com/nicholishen/pymt5adapter/raw/master/images/docs_screen.jpg "quick docs example"

pymt5adapter/calendar.py

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,47 @@ class Currency(enum.IntFlag):
4545
INR = enum.auto()
4646

4747

48+
def calendar_events(time_to: Union[datetime, timedelta] = None,
49+
*,
50+
time_from: Union[datetime, timedelta] = None,
51+
importance: Union[Iterable[str], str, int] = None,
52+
currencies: Union[Iterable[str], str, int] = None,
53+
function: Callable = None,
54+
round_minutes: int = 15,
55+
cache_clear: bool = False,
56+
language: str = None,
57+
**kwargs,
58+
) -> List[dict]:
59+
"""Get economic events from mql5.com/calendar. A call with empty args will results in all events for the next week.
60+
Since the function is memoized, the time is rounded to ``round_minutes`` and cached. This avoids repeat requests
61+
to the mql5.com server. In order to refresh the results on subsequest function calls you'll need to set the
62+
``cache_clear`` param to True.
63+
64+
:param time_to: Can be a timedelta or datetime object. If timedelta then the to time is calculated as
65+
datetime.now() + time_to. If the the time_from param is specified the the time_to will be calculated as
66+
time_from + time_to. Can also do a history look-back by passing in a negative timedelta to this param.
67+
:param time_from: Can be a timedelta or datetime object. If a timedelta object is passed then the time_from is
68+
calculated as time_from + now(), thus it needs to be a negative delta.
69+
:param importance: Can be int flags from the Importance enum class, and iterable of strings or a (space and/or
70+
comma separated string)
71+
:param currencies: Can be int flags from the Importance enum class, and iterable of strings or a (space and/or
72+
comma separated string) Pairs are automatically separated to the respective currency codes.
73+
:param function: A callback function that receives an event dict and returns bool for filtering.
74+
:param round_minutes: Round the number of minutes to this factor. Rounding aides the memoization of parameters.
75+
:param cache_clear: Clear the memo cache to refresh events from the server.
76+
:param language: The language code for the calendar. Default is 'en'
77+
:param kwargs:
78+
:return:
79+
"""
80+
cal_args = _construct_args(**locals())
81+
if cache_clear:
82+
_get_calendar_events.cache_clear()
83+
events = _get_calendar_events(language=language, **cal_args)
84+
if function:
85+
events = list(filter(function, events))
86+
return events
87+
88+
4889
@functools.lru_cache
4990
def _get_calendar_events(datetime_from: datetime,
5091
datetime_to: datetime,
@@ -72,11 +113,11 @@ def _get_calendar_events(datetime_from: datetime,
72113
e['Url'] = _BASE_URL + e['Url']
73114
e['ReleaseDate'] = time
74115
e['request'] = data
75-
filtered_events.append(e)
76-
filtered_events = [
77-
{_camel_to_snake(k): v for k, v in x.items() if k not in _OMIT_RESULT_KEYS}
78-
for x in filtered_events
79-
]
116+
formatted_event = {}
117+
for k, v in e.items():
118+
if k not in _OMIT_RESULT_KEYS:
119+
formatted_event[_camel_to_snake(k)] = v
120+
filtered_events.append(formatted_event)
80121
return filtered_events
81122

82123

@@ -106,10 +147,10 @@ def _camel_to_snake(w):
106147
return w
107148

108149

109-
_camel_to_snake.pattern = re.compile(r'[A-Z][a-z]+')
150+
_camel_to_snake.pattern = re.compile(r'[A-Z][a-z]*')
110151

111152

112-
@functools.lru_cache
153+
@functools.lru_cache(maxsize=128, typed=True)
113154
def _make_flag(enum_cls: Union[Type[Importance], Type[Currency]],
114155
flags: Union[Iterable[str], int, str] = None
115156
) -> int:
@@ -126,40 +167,12 @@ def _make_flag(enum_cls: Union[Type[Importance], Type[Currency]],
126167
return int(flag)
127168

128169

129-
def calendar_events(time_to: Union[datetime, timedelta] = None,
130-
*,
131-
time_from: Union[datetime, timedelta] = None,
132-
importance: Union[Iterable[str], str, int] = None,
133-
currencies: Union[Iterable[str], str, int] = None,
134-
function: Callable = None,
135-
round_minutes: int = 15,
136-
cache_clear: bool = False,
137-
language: str = None,
138-
**kwargs,
139-
) -> List[dict]:
140-
"""Get economic events from mql5.com/calendar. A call with empty args will results in all events for the next week.
141-
Since the function is memoized, the time is rounded to ``round_minutes`` and cached. This avoids repeat requests
142-
to the mql5.com server. In order to refresh the results on subsequest function calls you'll need to set the
143-
``cache_clear`` param to True.
144-
145-
:param time_to: Can be a timedelta or datetime object. If timedelta then the to time is calculated as
146-
datetime.now() + time_to. If the the time_from param is specified the the time_to will be calculated as
147-
time_from + time_to. Can also do a history look-back by passing in a negative timedelta to this param.
148-
:param time_from: Can be a timedelta or datetime object. If a timedelta object is passed then the time_from is
149-
calculated as time_from + now(), thus it needs to be a negative delta.
150-
:param importance: Can be int flags from the Importance enum class, and iterable of strings or a (space and/or
151-
comma separated string)
152-
:param currencies: Can be int flags from the Importance enum class, and iterable of strings or a (space and/or
153-
comma separated string) Pairs are automatically separated to the respective currency codes.
154-
:param function: A callback function that receives an event dict and returns bool for filtering.
155-
:param round_minutes: Round the number of minutes to this factor. Rounding aides the memoization of parameters.
156-
:param cache_clear: Clear the memo cache to refresh events from the server.
157-
:param language: The language code for the calendar. Default is 'en'
158-
:param kwargs:
159-
:return:
160-
"""
161-
if cache_clear:
162-
_get_calendar_events.cache_clear()
170+
def _construct_args(**kw):
171+
time_to = kw.get('time_to')
172+
time_from = kw.get('time_from')
173+
round_minutes = kw.get('round_minutes')
174+
importance = kw.get('importance')
175+
currencies = kw.get('currencies')
163176
now = datetime.now()
164177
if time_to is None and time_from is None:
165178
time_to = now + timedelta(weeks=1)
@@ -174,11 +187,5 @@ def calendar_events(time_to: Union[datetime, timedelta] = None,
174187
_f = lambda x: tuple(x) if isinstance(x, Iterable) and not isinstance(x, str) else x
175188
importance, currencies = _f(importance), _f(currencies)
176189
i_flag, c_flag = _make_flag(Importance, importance), _make_flag(Currency, currencies)
177-
events = _get_calendar_events(datetime_from=time_from,
178-
datetime_to=time_to,
179-
importance=i_flag,
180-
currencies=c_flag,
181-
language=language)
182-
if function:
183-
events = list(filter(function, events))
184-
return events
190+
res_args = dict(datetime_to=time_to, datetime_from=time_from, importance=i_flag, currencies=c_flag)
191+
return res_args

tests/test_calendar.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from .context import pymt5adapter as mt5
2+
from pymt5adapter.calendar import calendar_events, Importance, Currency
3+
import pymt5adapter.calendar as cal
4+
from datetime import datetime, timedelta
5+
import pytest
6+
7+
8+
def test_calendar_events():
9+
from time import perf_counter
10+
f = datetime.now()
11+
t = f + timedelta(days=30)
12+
t1 = perf_counter()
13+
events = calendar_events(time_from=f, time_to=t, importance=Importance.HIGH)
14+
t1 = perf_counter() - t1
15+
t2 = perf_counter()
16+
events = calendar_events(time_from=f, time_to=t, importance=Importance.HIGH)
17+
t2 = perf_counter() - t2
18+
assert t2 * 1000 < t1 # verify cache speed
19+
assert type(events) is list
20+
assert len(events) > 0
21+
for e in events:
22+
etime = e['release_date']
23+
assert f <= etime <= t
24+
assert e['importance'].upper() not in ['MEDIUM', 'LOW', 'HOLIDAY']
25+
26+
27+
def test_construct_args():
28+
test_args = dict(time_to=timedelta(days=-30), round_minutes=15)
29+
res_args = cal._construct_args(**test_args)
30+
assert isinstance(res_args['datetime_to'], datetime)
31+
assert isinstance(res_args['datetime_from'], datetime)
32+
assert isinstance(res_args['importance'], int)
33+
assert isinstance(res_args['currencies'], int)
34+
t = test_args['time_to'] = datetime(2020, 1, 1)
35+
test_args['time_from'] = t + timedelta(weeks=1)
36+
res_args = cal._construct_args(**test_args)
37+
assert res_args['datetime_from'] < res_args['datetime_to']
38+
39+
40+
def test_camel_to_snake():
41+
controls = {
42+
'Url' : 'url',
43+
'EventName' : 'event_name',
44+
'CamelCaseKey': 'camel_case_key',
45+
'ok_id' : 'ok_id',
46+
'NItems' : 'n_items'
47+
}
48+
for k, v in controls.items():
49+
assert cal._camel_to_snake(k) == v
50+
51+
52+
def test_normalize_time():
53+
control_minutes = 15
54+
control_time = datetime(2020, 1, 1, 1, control_minutes)
55+
delta = timedelta(minutes=7)
56+
test_time = control_time - delta
57+
result = cal._time_ceil(test_time, minutes=control_minutes)
58+
assert result == control_time
59+
test_time = control_time + delta
60+
result = cal._time_floor(test_time, minutes=control_minutes)
61+
assert result == control_time
62+
63+
64+
def test_split_pairs():
65+
control = set('EUR USD AUD'.split())
66+
67+
def clean(res):
68+
return set(map(str.upper, res))
69+
70+
result = clean(cal._split_pairs(['eurusd', 'audusd', 'eur', 'aud', 'usd']))
71+
assert result == control
72+
73+
74+
def test_make_flags():
75+
control_currency = Currency.USD | Currency.EUR | Currency.AUD
76+
control_importance = Importance.HOLIDAY | Importance.LOW
77+
test_currency = cal._make_flag(Currency, control_currency)
78+
assert test_currency == control_currency
79+
test_currency = cal._make_flag(Currency, 'eur aud usd')
80+
assert test_currency == control_currency
81+
test_currency = cal._make_flag(Currency, 'eur, aud, usd')
82+
assert test_currency == control_currency
83+
test_currency = cal._make_flag(Currency, ('eurusd', 'audusd'))
84+
assert test_currency == control_currency
85+
test_currency = cal._make_flag(Currency, ('eur', 'usd', 'aud'))
86+
assert test_currency == control_currency
87+
test_currency = cal._make_flag(Currency, 'eurusd, audusd, usdjpy')
88+
assert test_currency != control_currency
89+
test_importance = cal._make_flag(Importance, 'low, holiday')
90+
assert test_importance == control_importance
91+
92+
93+
if __name__ == '__main__':
94+
pass

0 commit comments

Comments
 (0)