The callable class below loads configuration data, formats it into a form compatible with my application(from its human-friendly version), performs some important checking, and finally makes it available to be accessed by calling the instance.
This is part of my(working) financial market trading application, written after 10 months of part-time programming. I'm trying to address it's many, many design flaws. One-by-one.
The BotOrderData
class is part of my attempt to move all configuration data out of the source code into a single configuration location. There is, therefore, a requirement for several of these types of classes(or whatever ends up being the best solution). For example, I'm often required to change parameters such as:
- pricing
- market volatility tolerances
- the times at which the application should refrain from trading
I'd like to know if this is a competent design choice, of any alternatives, and of any other smaller refactorings.
Code:
class BotOrderData():
"""A class for loading, formatting, checking, storing ``Bot`` pricing data
The BotOrderData class wraps a dict which holds the various pricing
parameters after loading, formatting and checking functions are performed.
This data is accessed by instance of ``Bot``.
:ivar DEFAULT_PARAMS: a dict of human-friendly pricing configuration data
to be used in the absence of local configuration file data.
"""
#
# TO DO:
# Finish docstrings
#
# The default bot pricing data, should none be available from a local
# config file listed in self.config_locations
DEFAULT_PARAMS = {
"S": {"EUR_USD": {"trigger": 1.2, "upper": 2.0, "ignore": 4.999},
"GBP_USD": {"trigger": 1.2, "upper": 2.0, "ignore": 4.999},
"EUR_GBP": {"trigger": 1.2, "upper": 2.0, "ignore": 4.999},
},
"B": {"EUR_USD": {"trigger": 0.0, "upper": -2.0, "ignore": 4.999},
"GBP_USD": {"trigger": 0.0, "upper": -2.0, "ignore": 4.999},
"EUR_GBP": {"trigger": 0.0, "upper": -2.0, "ignore": 4.999},
}
}
def __init__(self):
"""Initialize the Triggers object
:ivar params: a dict which holds the bot order parameters
:ivar config_name: a str. The basename of the config file to search the
system for.
:ivar config_locations: an iterable of folders locations which may
contain a qualifying config file. To be listed in the order in which
they should take precendence. Eg; that listed first takes priority
"""
self.params = dict()
self.config_name = "bot_config_data.py"
# List locations in order of highest priority first. Eg; if the
# first location exists, it's data will be loaded and used for
# configuration
self.config_locations = (
r"C:\Users\Dave\Bot",
os.path.expanduser("~trade_bot/"),
)
self.add_param_data()
def __call__(self, action: str, mkt_id: str):
"""Query and return pricing parameter data
:returns: A tuple of three float values in order
("trigger", "upper", "ignore")
"""
return (self.params[action][mkt_id]["trigger"],
self.params[action][mkt_id]["upper"],
self.params[action][mkt_id]["ignore"],
)
def discover_locations(self):
"""Return a list of configuration file locations"""
logging.debug("self.config_locations: {}".format(self.config_locations))
locations = [os.path.join(path, self.config_name)
for path in self.config_locations]
exist = [l for l in locations if os.path.exists(l)]
return exist
def get_config_from_file(self):
"""Load data from the first configuration file in available locations
"""
data = {}
locations = self.discover_locations()
if not locations:
return None
with open(locations[0]) as f:
code = compile(f.read(), locations[0], "exec")
exec(code, globals(), data)
return data["price_data"]
def process_params(self, params: dict):
"""Convert the human-friendly config data -> ``Bot`` friendly version
:param: params: a dict of config data, either from a local config file
or from DEFAULT_PARAMS if the former is not present
"""
sell_mkt = params["S"]
buy_mkt = params["B"]
for s_mkt in sell_mkt:
sell_mkt[s_mkt]["trigger"] = 1.0 + sell_mkt[s_mkt]["trigger"]/100
sell_mkt[s_mkt]["upper"] = 1.0 + sell_mkt[s_mkt]["upper"]/100
for b_mkt in buy_mkt:
buy_mkt[b_mkt]["trigger"] = 1.0 + buy_mkt[b_mkt]["trigger"]/100
buy_mkt[b_mkt]["upper"] = 1.0 + buy_mkt[b_mkt]["upper"]/100
return params
def add_param_data(self):
"""Manager method which adds pricing parameters to self.params"""
file_config = self.get_config_from_file()
params = self.DEFAULT_PARAMS if not file_config else file_config
params = self.process_params(params)
self.check_params(params)
self.params.update(params)
def check_params(self, params: dict):
"""Check the params data is valid. This check must ALWAYS pass.
:param params: a nested dict of Bot pricing parameters
"""
checks = list(
[e for e in params["B"] if params["B"][e]["trigger"] > 1.00
or params["B"][e]["upper"] > params["B"][e]["trigger"]
or params["B"][e]["ignore"] < 0]
+
[e for e in params["S"] if params["S"][e]["trigger"] < 1.00
or params["S"][e]["upper"] < params["S"][e]["trigger"]
or params["S"][e]["ignore"] < 0]
)
assert not checks, (self.__class__.__name__
+ " contains invalid data: {}".format(params))
bot_pricing_data = BotOrderData() # module level variable
Example usage:
from bot_prices import bot_pricing_data
class Bot():
def __init__():
self.trigger, self.upper, self.ignore =\
bot_pricing_data(self.action , self.mkt_id) # eg ("B", "EUR_USD")