forked from robcarver17/pysystemtrade
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathforecasting.py
251 lines (171 loc) · 7.67 KB
/
forecasting.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import pandas as pd
from systems.stage import SystemStage
from syscore.constants import arg_not_supplied
from systems.system_cache import output, dont_cache
from systems.trading_rules import TradingRule
class Rules(SystemStage):
"""
Construct the forecasting stage
Ways we can do this:
a) We do this by passing a list of trading rules
forecasting([trading_rule1, trading_rule2, ..])
Note that trading rules can be created using the TradingRule class, or you
can just pass in the name of a function, or a function.
We can also use a generate_variations method to create a list of multiple
rules
b) or we can create from a system config
KEY INPUT: Depends on trading rule(s) data argument
KEY OUTPUT: system.rules.get_raw_forecast(instrument_code, rule_variation_name)
system.rules.trading_rules()
Name: rules
"""
def __init__(self, trading_rules=arg_not_supplied):
"""
Create a SystemStage for forecasting
We either pass a dict or a list of trading rules (functions, strings
specifying a function, or objects of class TradingRule)
... or we'll get it from the overall system config
(trading_rules=None)
:param trading_rules: Set of trading rules
:type trading_rules: None (rules will be inherited from self.parent
system) TradingRule, str, callable function, or tuple (single rule)
list or dict (multiple rules)
:param pre_calc_rules: bool, if True then the first call to get a rule will calculate the values for all markets
:returns: Rules object
"""
super().__init__()
# We won't have trading rules we can use until we've parsed them
self._trading_rules = None
# ... store the ones we've been passed for now
self._passed_trading_rules = trading_rules
@property
def name(self):
return "rules"
@property
def passed_trading_rules(self):
return self._passed_trading_rules
def __repr__(self):
trading_rules = self.trading_rules()
rule_names = ", ".join(trading_rules.keys())
return "Rules object with rules " + rule_names
@output()
def get_raw_forecast(
self, instrument_code: str, rule_variation_name: str
) -> pd.Series:
"""
Does what it says on the tin - pulls the forecast for the trading rule
This forecast will need scaling and capping later
KEY OUTPUT
"""
system = self.parent
self.log.debug(
"Calculating raw forecast %s for %s"
% (instrument_code, rule_variation_name),
instrument_code=instrument_code,
)
# this will process all the rules, if not already done
trading_rule_dict = self.trading_rules()
trading_rule = trading_rule_dict[rule_variation_name]
result = trading_rule.call(system, instrument_code)
result = pd.Series(result)
return result
@dont_cache
def trading_rules(self):
"""
Ensure self.trading_rules is actually a properly specified list of trading rules
We can't do this when we __init__ because we might not have a parent yet
:returns: List of TradingRule objects
"""
current_rules = self._trading_rules
# We have already parsed the trading rules for this object, just return
# them
if current_rules is not None:
return current_rules
trading_rules = self._get_trading_rules_from_passed_rules()
self._trading_rules = trading_rules
return trading_rules
def _get_trading_rules_from_passed_rules(self):
# What where we passed when object was created?
passed_rules = self.passed_trading_rules
if passed_rules is arg_not_supplied:
passed_rules = self._get_rules_from_parent_or_raise_errors()
new_rules = process_trading_rules(passed_rules)
return new_rules
@dont_cache
def _get_rules_from_parent_or_raise_errors(self):
"""
We weren't passed anything in the command lines so need to inherit from the system config
"""
error_msg = None
if not hasattr(self, "parent"):
error_msg = "A Rules stage needs to be part of a System to identify trading rules, unless rules are passed when object created"
elif not hasattr(self.parent, "config"):
error_msg = "A system needs to include a config with trading_rules, unless rules are passed when object created"
elif not hasattr(self.parent.config, "trading_rules"):
error_msg = "A system config needs to include trading_rules, unless rules are passed when object created"
if error_msg is not None:
self.log.critical(error_msg)
raise Exception(error_msg)
# self.parent.config.tradingrules will already be in dictionary
# form
forecasting_config_rules = self.parent.config.trading_rules
return forecasting_config_rules
def process_trading_rules(passed_rules) -> dict:
"""
There are a number of ways to specify a set of trading rules. This function processes them all,
and returns a dict of TradingRule objects.
data types handled:
dict - parse each element of the dict and use the names [unless has one or more of keynames: function, data, args]
list - parse each element of the list and give them arbitrary names
anything else is assumed to be something we can pass to TradingRule (string, function, tuple, (dict with keynames function, data, args), or TradingRule object)
:param passed_rules: Set of trading rules
:type passed_rules: Single rule:
dict(function=str, optionally: args=dict(), optionally: data=list()),
TradingRule, str, callable function, or tuple
Multiple rules:
list, dict without 'function' keyname
:returns: dict of Tradingrule objects
"""
if isinstance(passed_rules, list):
# Give some arbitrary name
processed_rules = _process_trading_rules_in_list(passed_rules)
elif _is_a_single_trading_rule_in_a_dict(passed_rules):
processed_rules = _process_single_trading_rule(passed_rules)
elif _is_a_dict_of_multiple_trading_rules(passed_rules):
processed_rules = _process_dict_of_trading_rules(passed_rules)
else:
# Must be an individual rule (string, function, dict with 'function' or
# tuple)
processed_rules = _process_single_trading_rule(passed_rules)
return processed_rules
def _process_trading_rules_in_list(trading_rules: list):
processed_rules = dict(
[
("rule%d" % ruleid, TradingRule(rule))
for (ruleid, rule) in enumerate(trading_rules)
]
)
return processed_rules
def _is_a_single_trading_rule_in_a_dict(trading_rules: dict):
if isinstance(trading_rules, dict):
if "function" in trading_rules:
return True
return False
def _is_a_dict_of_multiple_trading_rules(trading_rules: dict):
if isinstance(trading_rules, dict):
if "function" not in trading_rules:
return True
else:
return False
def _process_dict_of_trading_rules(trading_rules: dict):
processed_rules = dict(
[(keyname, TradingRule(trading_rules[keyname])) for keyname in trading_rules]
)
return processed_rules
def _process_single_trading_rule(trading_rule):
list_of_rules = [trading_rule]
processed_rules = _process_trading_rules_in_list(list_of_rules)
return processed_rules
if __name__ == "__main__":
import doctest
doctest.testmod()