forked from robcarver17/pysystemtrade
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathforecasting.py
420 lines (296 loc) · 14.4 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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
from copy import copy
from systems.stage import SystemStage
from syscore.objects import resolve_function, resolve_data_method, hasallattr
from systems.system_cache import input, diagnostic, output
DEFAULT_PRICE_SOURCE = "data.daily_prices"
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=None):
"""
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)
:returns: Rules object
"""
super().__init__()
# We won't have trading rules we can use until we've parsed them
setattr(self, "_trading_rules", None)
# ... store the ones we've been passed for now
setattr(self, "_passed_trading_rules", trading_rules)
def _name(self):
return "rules"
def __repr__(self):
trading_rules = self._trading_rules
if trading_rules is not None:
rule_names = ", ".join(self._trading_rules.keys())
return "Rules object with rules " + rule_names
else:
return "Rules object with unknown trading rules [try Rules.tradingrules() ]"
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
# What where we passed when object was created?
passed_rules = self._passed_trading_rules
if passed_rules is None:
"""
We weren't passed anything in the command lines so need to inherit from the system config
"""
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"
self.log.critical(error_msg)
if not hasattr(self.parent, "config"):
error_msg = "A system needs to include a config with trading_rules, unless rules are passed when object created"
self.log.critical(error_msg)
if not hasattr(self.parent.config, "trading_rules"):
error_msg = "A system config needs to include trading_rules, unless rules are passed when object created"
self.log.critical(error_msg)
# self.parent.config.tradingrules will already be in dictionary
# form
forecasting_config = self.parent.config.trading_rules
new_rules = process_trading_rules(forecasting_config)
else:
# Okay, we've been passed a list manually which we'll use rather
# than getting it from the system
new_rules = process_trading_rules(passed_rules)
setattr(self, "_trading_rules", new_rules)
return(new_rules)
@output()
def get_raw_forecast(self, instrument_code, rule_variation_name):
"""
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.msg("Calculating raw forecast %s for %s" % (instrument_code, rule_variation_name),
instrument_code=instrument_code, rule_variation_name=rule_variation_name)
trading_rule = self.trading_rules()[rule_variation_name]
result = trading_rule.call(system, instrument_code)
result.columns = [rule_variation_name]
return result
class TradingRule(object):
"""
Container for trading rules
Can be called manually or will be called when configuring a system
"""
def __init__(self, rule, data=list(), other_args=dict()):
"""
Create a trading rule from a function
Functions must be of the form function(*dataargs, **kwargs), where
*dataargs are unnamed data items, and **kwargs are named configuration
items data, an ordered list of strings identifying data to be used
(default, just price) other_args: a dictionary of named arguments to be
passed to the trading rule
:param rule: Trading rule to be created
:type trading_rules:
The following describe a rule completely (ignore data and other_args arguments)
3-tuple ; containing (function, data, other_args)
dict (containing key "function", and optionally keys "other_args" and "data")
TradingRule (object is created out of this rule)
The following will be combined with the data and other_args arguments
to produce a complete TradingRule:
Other callable function
str (with path to function eg
"systems.provide.example.rules.ewmac_forecast_with_defaults")
:param data: (list of) str pointing to location of inputs in a system method call (eg "data.get_instrument_price")
(Either passed in separately, or as part of a TradingRule, 3-tuple, or dict object)
:type data: single str, or list of str
:param other_args: Other named arguments to be passed to trading rule function
(Either passed in separately , or as part of a TradingRule, 3-tuple, or dict object)
:type other_args: dict
:returns: single Tradingrule object
"""
if hasallattr(rule, ["function", "data", "other_args"]):
# looks like it is already a trading rule
(rule_function, data, other_args) = (
rule.function, rule.data, rule.other_args)
elif isinstance(rule, tuple):
if len(data) > 0 or len(other_args) > 0:
print(
"WARNING: Creating trade rule with 'rule' tuple argument, ignoring data and/or other args")
if len(rule) != 3:
raise Exception(
"Creating trading rule with a tuple, must be length 3 exactly (function/name, data [...], args dict(...))")
(rule_function, data, other_args) = rule
elif isinstance(rule, dict):
if len(data) > 0 or len(other_args) > 0:
print(
"WARNING: Creating trade rule with 'rule' dict argument, ignoring data and/or other args")
try:
rule_function = rule['function']
except KeyError:
raise Exception(
"If you specify a TradingRule as a dict it has to contain a 'function' keyname")
if "data" in rule:
data = rule['data']
else:
data = []
if "other_args" in rule:
other_args = rule['other_args']
else:
other_args = dict()
else:
rule_function = rule
# turn string into a callable function if required
rule_function = resolve_function(rule_function)
if isinstance(data, str):
# turn into a 1 item list or wont' get parsed properly
data = [data]
setattr(self, "function", rule_function)
setattr(self, "data", data)
setattr(self, "other_args", other_args)
def __repr__(self):
data_names = ", ".join(self.data)
args_names = ", ".join(self.other_args.keys())
return "TradingRule; function: %s, data: %s and other_args: %s" % (
str(self.function), data_names, args_names)
def call(self, system, instrument_code):
"""
Actually call a trading rule
To do this we need some data from the system
"""
assert isinstance(self.data, list)
if len(self.data) == 0:
# if no data provided defaults to using price
datalist = [DEFAULT_PRICE_SOURCE]
else:
datalist = self.data
data_methods = [resolve_data_method(
system, data_string) for data_string in datalist]
data = [data_method(instrument_code) for data_method in data_methods]
other_args = self.other_args
return self.function(*data, **other_args)
def process_trading_rules(trading_rules):
"""
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 trading_rules: Set of trading rules
:type trading_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(trading_rules, list):
# Give some arbitrary name
ans = dict([("rule%d" % ruleid, TradingRule(rule))
for (ruleid, rule) in enumerate(trading_rules)])
return ans
if isinstance(trading_rules, dict):
if "function" not in trading_rules:
# Note the system config will always come in as a dict
ans = dict([(keyname, TradingRule(trading_rules[keyname]))
for keyname in trading_rules])
return ans
# Must be an individual rule (string, function, dict with 'function' or
# tuple)
return process_trading_rules([trading_rules])
def create_variations_oneparameter(
baseRule, list_of_args, argname, nameformat="%s_%s"):
"""
Returns a dict of trading rule variations, varying only one named parameter
:param baseRule: Trading rule to copy
:type baseRule: TradingRule object
:param list_of_args: set of parameters to use
:type list_of_args: list
:param argname: Argument passed to trading rule which will be changed
:type argname: str
:param nameformat: Format to use when naming trading rules; nameformat % (argname, argvalue) will be used
:type nameformat: str containing two '%s' elements
:returns: dict of Tradingrule objects
>>>
>>> rule=TradingRule(("systems.provided.example.rules.ewmac_forecast_with_defaults", [], {}))
>>> variations=create_variations_oneparameter(rule, [4,10,100], "Lfast")
>>> ans=list(variations.keys())
>>> ans.sort()
>>> ans
['Lfast_10', 'Lfast_100', 'Lfast_4']
"""
list_of_args_dict = []
for arg_value in list_of_args:
thisdict = dict()
thisdict[argname] = arg_value
list_of_args_dict.append(thisdict)
ans = create_variations(baseRule, list_of_args_dict,
key_argname=argname, nameformat=nameformat)
return ans
def create_variations(baseRule, list_of_args_dict,
key_argname=None, nameformat="%s_%s"):
"""
Returns a dict of trading rule variations
eg create_variations(ewmacrule, [dict(fast=2, slow=8), dict(fast=4, ...) ], argname="fast", basename="ewmac")
:param baseRule: Trading rule to copy
:type baseRule: TradingRule object
:param list_of_args_dict: sets of parameters to use.
:type list_of_args: list of dicts; each dict contains a set of parameters to vary for each instance
:param key_argname: Non
:type key_argname: str or None (None is allowed if only one parameter is changed)
:param nameformat: Format to use when naming trading rules; nameformat % (argname, argvalue) will be used
:type nameformat: str containing two '%s' elements
:returns: dict of Tradingrule objects
>>> rule=TradingRule(("systems.provided.example.rules.ewmac_forecast_with_defaults", [], {}))
>>> variations=create_variations(rule, [dict(Lfast=2, Lslow=8), dict(Lfast=4, Lslow=16)], "Lfast", nameformat="ewmac_%s_%s")
>>> ans=list(variations.keys())
>>> ans.sort()
>>> ans
['ewmac_Lfast_2', 'ewmac_Lfast_4']
"""
if key_argname is None:
if all([len(args_dict) == 1 for args_dict in list_of_args_dict]):
# okay to use argname as only seems to be one of them
key_argname = args_dict[0].keys()[0]
else:
raise Exception(
"need to specify argname if more than one possibility")
baseRulefunction = baseRule.function
baseRuledata = baseRule.data
# these will be overwritten as we run through
baseRuleargs = copy(baseRule.other_args)
variations = dict()
for args_dict in list_of_args_dict:
if key_argname not in args_dict.keys():
raise Exception(
"Argname %s missing from at least one set of argument values" % key_argname)
for arg_name in args_dict.keys():
baseRuleargs[arg_name] = args_dict[arg_name]
rule_variation = TradingRule(
baseRulefunction, baseRuledata, baseRuleargs)
var_name = nameformat % (key_argname, str(args_dict[key_argname]))
variations[var_name] = rule_variation
return variations
if __name__ == '__main__':
import doctest
doctest.testmod()