From e48ba998207bd2fff08c6ce57d0ad850c7a1317a Mon Sep 17 00:00:00 2001 From: msincenselee Date: Thu, 22 Oct 2015 10:40:59 +0800 Subject: [PATCH] daily update --- vn.data/mysqldb.sql | 3 + vn.data/test_tushare.py | 2 +- vn.strategy/strategydemo/backtestingEngine.py | 2 +- vn.strategy/strategydemo/demoBacktesting.py | 2 +- vn.strategy/strategydemo/demoStrategy.py | 2 +- vn.strategy/strategydemo/strategyEngine.py | 82 +- vn.training/Public/Public.py | 480 ++++++ vn.training/Public/__init__.py | 5 + vn.training/SubPlot/__init__.py | 3 + vn.training/SubPlot/公司信息子图.py | 284 ++++ vn.training/SubPlot/分时价格子图.py | 580 +++++++ vn.training/SubPlot/分时手数子图.py | 436 ++++++ vn.training/SubPlot/实盘价格子图.py | 23 + vn.training/SubPlot/实盘手数子图.py | 22 + vn.training/SubPlot/日线价格子图.py | 1327 +++++++++++++++++ vn.training/SubPlot/日线换手子图.py | 488 ++++++ vn.training/display_bar.py | 35 +- vn.training/draw.py | 1002 +++++++++++++ vn.training/draw_data.pickle | Bin 0 -> 286829 bytes vn.training/quotation.py | 35 +- 20 files changed, 4748 insertions(+), 65 deletions(-) create mode 100644 vn.training/Public/Public.py create mode 100644 vn.training/Public/__init__.py create mode 100644 vn.training/SubPlot/__init__.py create mode 100644 vn.training/SubPlot/公司信息子图.py create mode 100644 vn.training/SubPlot/分时价格子图.py create mode 100644 vn.training/SubPlot/分时手数子图.py create mode 100644 vn.training/SubPlot/实盘价格子图.py create mode 100644 vn.training/SubPlot/实盘手数子图.py create mode 100644 vn.training/SubPlot/日线价格子图.py create mode 100644 vn.training/SubPlot/日线换手子图.py create mode 100644 vn.training/draw.py create mode 100644 vn.training/draw_data.pickle diff --git a/vn.data/mysqldb.sql b/vn.data/mysqldb.sql index 96cf1c46..802a7212 100644 --- a/vn.data/mysqldb.sql +++ b/vn.data/mysqldb.sql @@ -44,3 +44,6 @@ create table TB_Ema ); +delete from TB_Trade; +delete from TB_Bar; +delete from TB_Ema; diff --git a/vn.data/test_tushare.py b/vn.data/test_tushare.py index f7e3cbd4..546809c0 100644 --- a/vn.data/test_tushare.py +++ b/vn.data/test_tushare.py @@ -5,7 +5,7 @@ import ipdb data = ts.get_hist_data('600848', start='2015-04-01', end='2015-10-20') #一次性获取全部日k线数据 -#print data.tail(10) +print data.tail(10) data.plot() diff --git a/vn.strategy/strategydemo/backtestingEngine.py b/vn.strategy/strategydemo/backtestingEngine.py index 28634b65..0a164fbc 100644 --- a/vn.strategy/strategydemo/backtestingEngine.py +++ b/vn.strategy/strategydemo/backtestingEngine.py @@ -380,7 +380,7 @@ class BacktestingEngine(object): def saveTradeDataToMysql(self): """保存交易记录到mysql,added by Incense Lee""" if self.__mysqlConnected: - sql='insert into BackTest.TB_Trade (Id,symbol,orderRef,tradeID,direction,offset,price,volume,tradeTime) values ' + sql='insert into BackTest.TB_Trade (Id,symbol,orderRef,tradeID,direction,offset,price,volume,tradeTime,amount) values ' values = '' print u'共{0}条交易记录.'.format(len(self.listTrade)) diff --git a/vn.strategy/strategydemo/demoBacktesting.py b/vn.strategy/strategydemo/demoBacktesting.py index f26e5f26..07bff70b 100644 --- a/vn.strategy/strategydemo/demoBacktesting.py +++ b/vn.strategy/strategydemo/demoBacktesting.py @@ -22,7 +22,7 @@ def main(): be.connectMysql() # be.loadMongoDataHistory(symbol, datetime(2015,5,1), datetime.today()) # be.loadMongoDataHistory(symbol, datetime(2012,1,9), datetime(2012,1,14)) - be.loadMysqlDataHistory(symbol, datetime(2012,6,9), datetime(2012,6,20)) + be.loadMysqlDataHistory(symbol, datetime(2012,6,9), datetime(2012,7,20)) # 创建策略对象 setting = {} diff --git a/vn.strategy/strategydemo/demoStrategy.py b/vn.strategy/strategydemo/demoStrategy.py index 37238617..1e4e990a 100644 --- a/vn.strategy/strategydemo/demoStrategy.py +++ b/vn.strategy/strategydemo/demoStrategy.py @@ -382,7 +382,7 @@ def main(): # 创建策略对象 setting = {} setting['fastAlpha'] = 0.2 - setting['slowAlpha'] = 0.05 + setting['slowAlpha'] = 0.09 #se.createStrategy(u'EMA演示策略', 'IF1506', SimpleEmaStrategy, setting) se.createStrategy(u'EMA演示策略', 'a', SimpleEmaStrategy, setting) diff --git a/vn.strategy/strategydemo/strategyEngine.py b/vn.strategy/strategydemo/strategyEngine.py index 95b3788c..d09e861b 100644 --- a/vn.strategy/strategydemo/strategyEngine.py +++ b/vn.strategy/strategydemo/strategyEngine.py @@ -300,7 +300,7 @@ class StrategyEngine(object): self.writeLog(u'策略引擎连接MysqlDB成功') except ConnectionFailure: self.writeLog(u'策略引擎连接MysqlDB失败') - #---------------------------------------------------------------------- + #---------------------------------------------------------------------- def __recordTickToMysql(self, data): """将Tick数据插入到MysqlDB中""" #if self.__mongoConnected: @@ -309,6 +309,27 @@ class StrategyEngine(object): # self.__mongoTickDB[symbol].insert(data) pass + #---------------------------------------------------------------------- + def __executeMysql(self, sql): + """执行mysql语句""" + if not self.__mysqlConnected: + self.__connectMysql() + + cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) + + try: + cur.execute(sql) + self.__mysqlConnection.commit() + + except Exception, e: + print e + print sql + + self.__connectMysql() + cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) + cur.execute(sql) + self.__mysqlConnection.commit() + #---------------------------------------------------------------------- def loadTickFromMysql(self, symbol, startDate, endDate=None): """从MysqlDB中读取Tick数据""" @@ -424,7 +445,7 @@ class StrategyEngine(object): if len(barList) == 0: return - steps = 0 + counts = 0 for bar in barList: @@ -444,30 +465,25 @@ class StrategyEngine(object): bar.volume, bar.openInterest) - if steps > 3600: + if counts >= 3600: - cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) - steps = 0 - values = EMPTY_STRING + self.__executeMysql(sql+values) - try: - cur.execute(sql+values) - self.__mysqlConnection.commit() - except Exception, e: - print e + print u'写入{0}条Bar记录'.format(counts) + + counts = 0 + values = '' else: - steps = steps + 1 + counts = counts + 1 - cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) + if counts > 0: + + self.__executeMysql(sql+values) + print u'写入{0}条Bar记录'.format(counts) - try: - cur.execute(sql+values) - self.__mysqlConnection.commit() - except Exception, e: - print e #---------------------------------------------------------------------- def saveEmaToMysql(self, id, emaList): @@ -484,7 +500,7 @@ class StrategyEngine(object): if len(emaList) == 0: return - steps = 0 + counts = 0 for ema in emaList: @@ -500,31 +516,21 @@ class StrategyEngine(object): ema.time, ema.datetime.strftime('%Y-%m-%d %H:%M:%S')) - if steps > 3600: + if counts >= 3600: - cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) + self.__executeMysql(sql+values) + print u'写入{0}条EMA记录'.format(counts) - steps = 0 - - values = EMPTY_STRING - - try: - cur.execute(sql+values) - self.__mysqlConnection.commit() - except Exception, e: - print e + counts = 0 + values = '' else: - steps = steps + 1 + counts = counts + 1 - cur = self.__mysqlConnection.cursor(MySQLdb.cursors.DictCursor) + if counts > 0: - try: - cur.execute(sql+values) - self.__mysqlConnection.commit() - - except Exception, e: - print e + self.__executeMysql(sql+values) + print u'写入{0}条EMA记录'.format(counts) #---------------------------------------------------------------------- diff --git a/vn.training/Public/Public.py b/vn.training/Public/Public.py new file mode 100644 index 00000000..cf19b820 --- /dev/null +++ b/vn.training/Public/Public.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- + + + +import os +import copy +import itertools +import math +import datetime +import logging +import logging.handlers + + + + + +class IndentLogger: + ''' + + ''' + + def __init__(self, logger, indent): + self._logger= logger + self._indent= indent + + + + def indent_levelup(self, level=1): + self._indent= self._indent - level + + def indent_leveldown(self, level=1): + self._indent= self._indent + level + + def set_indent_level(self, level): + self._indent= level + + #--------------------------------------------------------------- + + def set_critical(self): + self._logger.setLevel(logging.CRITICAL) + + def set_error(self): + self._logger.setLevel(logging.ERROR) + + def set_warning(self): + self._logger.setLevel(logging.WARNING) + + def set_info(self): + self._logger.setLevel(logging.INFO) + + def set_debug(self): + self._logger.setLevel(logging.DEBUG) + + def set_notset(self): + self._logger.setLevel(logging.NOTSET) + + #--------------------------------------------------------------- + + def critical(self, message): + self._logger.critical('\t' * self._indent + message) + + def error(self, message): + self._logger.error('\t' * self._indent + message) + + def warning(self, message): + self._logger.warning('\t' * self._indent + message) + + def info(self, message): + self._logger.info('\t' * self._indent + message) + + def debug(self, message): + self._logger.debug('\t' * self._indent + message) + + def noset(self, message): + self._logger.noset('\t' * self._indent + message) + + + +def TempLogger(loggername, filename=None, taskdir=None): + ''' + + ''' + + if not taskdir: + taskdir= __dir_tmpfiles__ + + if not os.path.exists(taskdir): + os.mkdir(taskdir, 0o700) + + if not filename: + timestamp= datetime.datetime.now() + filename= os.path.join(taskdir, loggername + '_' + timestamp.strftime('%Y-%m-%d_%H:%M:%S,%f')) + else: + filename= os.path.join(taskdir, filename) + + if not os.path.exists(filename): + os.mknod(filename, 0o700) + + myformatstr= "%(asctime)s %(levelname)-9s>> %(message)s" + myformatter= logging.Formatter(myformatstr) + + myhandler= logging.handlers.RotatingFileHandler(filename=filename, mode='a', encoding='utf-8') + myhandler.setFormatter(myformatter) + + mylogger= logging.getLogger(name=loggername) + mylogger.setLevel(level=logging.DEBUG) + mylogger.addHandler(myhandler) + + ilogger= IndentLogger(logger=mylogger, indent=0) + return ilogger + + + +def 计算个股换手率(个股行情, 个股股本变更记录): + ''' + + ''' + 个股股本变更列表= [rec for rec in 个股股本变更记录 if rec['流通股'] != 0 and rec['变更日期'] <= 个股行情['日期'][-1]] + + 个股股本变更字典= {} + for rec in 个股股本变更列表: + if rec['变更日期'] in 个股行情['日期']: + 个股股本变更字典[rec['变更日期']]= rec + else: + 个股股本变更字典[ [ds for ds in 个股行情['日期'] if ds > rec['变更日期']][0] ]= rec + + 当前流通股= 个股股本变更字典[min(个股股本变更字典.keys())]['流通股'] + + 换手率= [] + for ds, vol in zip(个股行情['日期'], 个股行情['成交量']): + if ds in 个股股本变更字典: + 当前流通股= 个股股本变更字典[ds]['流通股'] + 换手率.append( vol*100000/当前流通股 ) + 个股行情['换手率']= 换手率 + + + +def 计算复权行情(个股行情, 均线参数=None): + ''' + + ''' + 日期= 个股行情['日期'] + + 复权开盘= copy.copy(个股行情['开盘']) + 复权最高= copy.copy(个股行情['最高']) + 复权收盘= copy.copy(个股行情['收盘']) + 复权最低= copy.copy(个股行情['最低']) + 复权开收中= copy.copy(个股行情['开收中']) + + 复权记录= [] + + sidx= 1 + done= False + while not done: + done= True + + for idx, date in enumerate(日期[sidx:], start=sidx): + 涨幅= (复权开盘[idx] - 复权收盘[idx-1]) / 复权收盘[idx-1] + if 涨幅 <= -0.12: + 复权因子= round(复权收盘[idx-1]/复权开盘[idx], 2) + 调整因子= round(复权因子, 1) + if abs(round(复权因子-调整因子, 2)) <= 0.01: + 复权因子= 调整因子 + 复权开盘[:idx]= [nr/复权因子 for nr in 复权开盘[:idx]] + 复权最高[:idx]= [nr/复权因子 for nr in 复权最高[:idx]] + 复权收盘[:idx]= [nr/复权因子 for nr in 复权收盘[:idx]] + 复权最低[:idx]= [nr/复权因子 for nr in 复权最低[:idx]] + 复权开收中[:idx]= [nr/复权因子 for nr in 复权开收中[:idx]] + + 复权记录.append( (date, 复权因子) ) + sidx= idx + done= False + break + + 复权行情= {} + 复权行情['复权记录']= 复权记录 + + 复权行情['日期']= copy.copy(日期) + 复权行情['开盘']= 复权开盘 + 复权行情['最高']= 复权最高 + 复权行情['收盘']= 复权收盘 + 复权行情['最低']= 复权最低 + 复权行情['开收中']= 复权开收中 + if 均线参数: + 复权行情['均线集']= { n : 计算序列加权均线(复权开盘, 复权最高, 复权收盘, 复权最低, n) for n in 均线参数 } + + return 复权行情 + + + +def 计算序列加权均线(开盘序列, 最高序列, 收盘序列, 最低序列, n): + ''' + + ''' + length= len(开盘序列) + if length < n: + return [None] * length + + sumhilo= sum(最高序列[:n]) + sum(最低序列[:n]) + sumopen= sum(开盘序列[:n]) + sumclose= sum(收盘序列[:n]) + + 输出序列= [ ((sumhilo / 2 + sumopen) / 2 + sumclose) / (2*n) ] + + for idx in range(n, length): + sumhilo= sumhilo - 最高序列[idx-n] - 最低序列[idx-n] + 最高序列[idx] + 最低序列[idx] + sumopen= sumopen - 开盘序列[idx-n] + 开盘序列[idx] + sumclose= sumclose - 收盘序列[idx-n] + 收盘序列[idx] + 输出序列.append( ((sumhilo / 2 + sumopen) / 2 + sumclose) / (2*n) ) + + return [None] * (n-1) + 输出序列 + + + +def 补全个股行情(完整日期, 个股行情): + ''' + + ''' + 代码= 个股行情.pop('代码') if '代码' in 个股行情 else None + 日期= 个股行情.pop('日期') + + for idx, dstr in enumerate(完整日期): + if dstr not in 日期: + 日期.insert(idx, dstr) + for seq in 个股行情.values(): + seq.insert(idx, None) + + if 代码: + 个股行情['代码']= 代码 + 个股行情['日期']= 日期 + + + +def 计算个股行情衍生数据(ilogger, 个股行情, 均线参数=None): + ''' + + ''' + length= len(个股行情['开盘']) + + 开盘= 个股行情['开盘'] + 最高= 个股行情['最高'] + 收盘= 个股行情['收盘'] + 最低= 个股行情['最低'] + + if 均线参数 and '均线集' not in 个股行情: + 个股行情['均线集']= {n : 计算序列加权均线(开盘, 最高, 收盘, 最低, n) for n in 均线参数} + if '均线集' in 个股行情: + 个股行情['均线走势标记集']= {n : [None]*(n-1)+计算走势标记(序列=序列[n-1:]) if length>=n else [None]*length for n, 序列 in 个股行情['均线集'].items()} + + 开收中= 个股行情['开收中'] + + 开收中线走势标记= 计算走势标记(序列=开收中) + + 个股行情['开收中线走势标记']= 开收中线走势标记 + + 最小长度= 个股行情.pop('目标偏移') if '目标偏移' in 个股行情 else 0 + 截去行情头部无效片断(行情数据=个股行情, 最小长度=最小长度) + + 开收中线走势拐点= 计算走势拐点(目标序列=开收中, 走势标记=开收中线走势标记) + 个股行情['开收中线走势拐点']= 开收中线走势拐点 + + + +def 截去行情头部无效片断(行情数据, 最小长度): + ''' + + ''' + length= len(行情数据['开盘']) + + dirlists= [seq for seq in 行情数据.values() if (type(seq) is list) and (len(seq)==length)] + subkeys= ('均线集', '均线走势标记集') + sublists= [行情数据[key].values() for key in subkeys if key in 行情数据] + + cntlist= [seq.count(None) for seq in dirlists] + itor= itertools.chain.from_iterable(seq for seq in sublists) + cntlist.extend( [seq.count(None) for seq in itor] ) + 截去长度= max(最小长度, max(cntlist)) + + for seq in dirlists: + del seq[:截去长度] + itor= itertools.chain.from_iterable(seq for seq in sublists) + for seq in itor: + del seq[:截去长度] + + + +def 计算均值(序列): + ''' + + ''' + if not 序列: + return None + 长度= len(序列) + 均值= sum(序列)/长度 + 最大值= max(序列) + 最小值= min(序列) + 标准差= math.sqrt(sum([(nr-均值)**2 for nr in 序列]) / 长度) + + return (均值, 最大值, 最小值, 标准差, 长度) + + + +def 计算日内定时均线(价格序列, 调整时间序列, 格点粒度, 间隔点数, 定时点数, 需要规整=True): + ''' + + ''' + 日期对象= 调整时间序列[0].date() + + datetime_0925= datetime.datetime.combine(日期对象, datetime.time(hour=9, minute=25)) + + 格点序列= [datetime_0925 + datetime.timedelta(seconds=格点粒度*i) for i in range(int((3600*4+1000)/格点粒度))] + + if 需要规整: + 规整时间序列= [] + 格点当前位置= 0 + for 时间 in 调整时间序列: + while 格点序列[格点当前位置] < 时间: + 格点当前位置 += 1 + 前方格点= 格点序列[格点当前位置] + 后方格点= 格点序列[格点当前位置-1] + 规整时间= 前方格点 if 前方格点-时间 <= 时间-后方格点 else 后方格点 + 规整时间序列.append(规整时间) + else: + 规整时间序列= 调整时间序列 + + 目标格点序列= [时间 for 时间 in 格点序列 if 时间>=规整时间序列[0] and 时间<=规整时间序列[-1]] + + 补全价格序列= [] + 当前价格= 价格序列[0] + for 格点 in 目标格点序列: + if 格点 in 规整时间序列: + 当前价格= 价格序列[规整时间序列.index(格点)] + 补全价格序列.append(当前价格) + + 定时均线= {} + for 点数 in 定时点数: + 偏移序列= range(点数-1, len(目标格点序列), 间隔点数) + 时间序列= [目标格点序列[偏移] for 偏移 in 偏移序列] + 均线序列= [ sum(补全价格序列[偏移-点数+1 : 偏移+1]) / 点数 for 偏移 in 偏移序列 ] + 定时均线[点数]= { + '时间序列': 时间序列, + '均线序列': 均线序列, + } + + return 定时均线 + + + +def 计算走势标记(序列): + ''' + + ''' + length= len(序列) + if length < 2: + return ['-'] * length + + 标记序列= [] + 当前方向= '/' if 序列[1] > 序列[0] else \ + '\\' if 序列[1] < 序列[0] else \ + '-' + + for idx in range(1, length-1): + sign= '/' if 序列[idx] > 序列[idx-1] and 序列[idx+1] >= 序列[idx] else \ + '\\' if 序列[idx] < 序列[idx-1] and 序列[idx+1] <= 序列[idx] else \ + '^' if 序列[idx] > 序列[idx-1] and 序列[idx+1] < 序列[idx] else \ + 'v' if 序列[idx] < 序列[idx-1] and 序列[idx+1] > 序列[idx] else \ + '/' if 当前方向 in '/-' and 序列[idx+1] > 序列[idx] else \ + '\\' if 当前方向 in '\\-' and 序列[idx+1] < 序列[idx] else \ + '^' if 当前方向 == '/' and 序列[idx+1] < 序列[idx] else \ + 'v' if 当前方向 == '\\' and 序列[idx+1] > 序列[idx] else \ + '-' + + 当前方向= '/' if sign in '/v' else \ + '\\' if sign in '\\^' else \ + 当前方向 + + 标记序列.append(sign) + + return ['-'] + 标记序列 + ['/' if 序列[-1] > 序列[-2] else '\\' if 序列[-1] < 序列[-2] else '-'] + + + +def 计算走势拐点(目标序列, 走势标记, 扩展=True): + ''' + + ''' + + length= len(目标序列) + if length <= 2: + return [] + + 走势拐点= [] + + for idx, sign in [(i, s) for i, s in enumerate(走势标记) if s in ('^', 'v')]: + 拐点记录= {} + 拐点记录['偏移']= idx + 拐点记录['类型']= sign + if 扩展: + # 计算关键度 + 拐点记录['关键度']= 计算最新极点关键度(序列=目标序列[:idx+1], 类型=sign)['关键度'] + + 走势拐点.append(拐点记录) + + return 走势拐点 + + + +def 计算最新极点关键度(序列, 类型=None): + ''' + + ''' + 长度= len(序列) + + if 类型 is None: + for i in range(1, 长度): + if 序列[-i] > 序列[-(i+1)]: + 类型= '^' + break + elif 序列[-i] < 序列[-(i+1)]: + 类型= 'v' + break + + 结果= { + '类型': 类型, + '关键度': 长度, + '偏移': 长度-1, + } + + if 长度 < 2: + return 结果 + + if 类型 == '^': + chunk= [idx for idx, item in enumerate(reversed(序列)) if item > 序列[-1]] + elif 类型 == 'v': + chunk= [idx for idx, item in enumerate(reversed(序列)) if item < 序列[-1]] + else: + return 长度 + + 结果['关键度']= chunk[0] if chunk else 长度 + + return 结果 + + + +def repr_data(data, indent=0): + ''' + + ''' + tlist= (list, dict, set, tuple) + dtype= type(data) + + if dtype is list: + head= '\t'*indent + '[' + body= ',\n'.join( [repr_data(data=item, indent=indent+1) for item in data] ) + tail= '\n' + '\t'*indent + ']' + return head + '\t' + body.lstrip() + tail + + elif dtype is dict: + head= '\t'*indent + '{' + body= ',\n'.join( ['\t'*(indent+1) + str(key) + ' :' + ( ('\n' + repr_data(data=val, indent=indent+1)) if type(val) in tlist else ('\t' + str(val)) ) for key, val in sorted(data.items())] ) + tail= '\n' + '\t'*indent + '}' + return head + '\t' + body.lstrip() + tail + + elif dtype is set: + head= '\t'*indent + '{' + body= ',\n'.join( [repr_data(data=item, indent=indent+1) for item in sorted(data)] ) + tail= '\n' + '\t'*indent + '}' + return head + '\t' + body.lstrip() + tail + + elif dtype is tuple: + head= '\t'*indent + '(' + body= ',\n'.join( [repr_data(data=item, indent=indent+1) for item in data] ) + tail= '\n' + '\t'*indent + ')' + return head + '\t' + body.lstrip() + tail + + else: + return '\t'*indent + str(data) + + + diff --git a/vn.training/Public/__init__.py b/vn.training/Public/__init__.py new file mode 100644 index 00000000..f2120afc --- /dev/null +++ b/vn.training/Public/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + + + + diff --git a/vn.training/SubPlot/__init__.py b/vn.training/SubPlot/__init__.py new file mode 100644 index 00000000..faaaf799 --- /dev/null +++ b/vn.training/SubPlot/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + + diff --git a/vn.training/SubPlot/公司信息子图.py b/vn.training/SubPlot/公司信息子图.py new file mode 100644 index 00000000..551d874e --- /dev/null +++ b/vn.training/SubPlot/公司信息子图.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + + + + + +import matplotlib.ticker as ticker +import matplotlib.font_manager as font_manager + + + +__font_properties__= font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc') + + + +__横轴倍率__= 10.0 / 230.0 +__纵轴倍率__= 0.3 + + + + + +class 公司信息子图: + ''' + 公司的基本信息 + ''' + + def __init__(self, parent, 绘图数据): + self._parent= parent + self._公司信息= 绘图数据['公司信息'] + + self._Axes= None + + self._横轴尺寸, \ + self._纵轴尺寸= self.计算本图尺寸() + + self._横轴宽度= self._横轴尺寸 * __横轴倍率__ + self._纵轴高度= self._纵轴尺寸 * __纵轴倍率__ + + + + def 计算本图尺寸(self): + return (300.0, 1.8) + + + + def 返回本图大小(self): + return (self._横轴尺寸*__横轴倍率__, self._纵轴尺寸*__纵轴倍率__) + + + + def 平面初始化(self, 图片对象, 子图偏移, 全图大小): + 子图横移, \ + 子图纵移= 子图偏移 + + 本图宽度= self._横轴宽度 + 本图高度= self._纵轴高度 + + 全图宽度, \ + 全图高度= 全图大小 + + 布局参数= ( 子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 本图高度/全图高度 ) + + axes= 图片对象.add_axes(布局参数) + axes.set_frame_on(False) + self._Axes= axes + + self.设置横轴参数() + self.设置纵轴参数() + + + + def 设置横轴参数(self): + axes= self._Axes + xaxis= axes.get_xaxis() + + # 设定 X 轴坐标的范围 + #================================================================================================================================================== + axes.set_xlim(0, self._横轴尺寸) + + xaxis.set_major_locator(ticker.NullLocator()) + + for mal in axes.get_xticklabels(minor=False): + mal.set_visible(False) + + for mil in axes.get_xticklabels(minor=True): + mil.set_visible(False) + + + + def 设置纵轴参数(self): + axes= self._Axes + yaxis= axes.get_yaxis() + + # 设定 X 轴坐标的范围 + #================================================================================================================================================== + axes.set_ylim(0, self._纵轴尺寸) + + yaxis.set_major_locator(ticker.NullLocator()) + + for mal in axes.get_yticklabels(minor=False): + mal.set_visible(False) + + for mil in axes.get_yticklabels(minor=True): + mil.set_visible(False) + + + + def 绘图(self): + self.绘制公司代码简称(xbase=0.0, ybase=self._纵轴尺寸) + self.绘制指数简称(xbase=self._横轴尺寸, ybase=self._纵轴尺寸) + self.绘制公司名称(xbase=0.0, ybase=self._纵轴尺寸-0.8) + self.绘制公司地域行业(xbase=48.0, ybase=self._纵轴尺寸) + self.绘制公司主营业务(xbase=48.0, ybase=self._纵轴尺寸) + self.绘制公司简介(xbase=90.0, ybase=self._纵轴尺寸) + self.绘制公司分类信息(xbase=165.0, ybase=self._纵轴尺寸) + + + + def 绘制公司代码简称(self, xbase, ybase): + ''' + 交易代码、公司简称 + ''' + + txtstr= self._公司信息['个股代码'] + ' ' + self._公司信息['个股简称'] + label= self._Axes.text(xbase, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left') + label.set_fontsize(16.0) + + + + def 绘制指数简称(self, xbase, ybase): + txtstr= self._公司信息['指数简称'] + label= self._Axes.text(xbase, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='right') + label.set_fontsize(16.0) + + + + def 绘制公司名称(self, xbase, ybase): + ''' + 曾用名、全名、英文名 + ''' + + txtstr= self._公司信息['基本情况']['曾用名'] + txtlist= txtstr.split('->') + if len(txtlist) > 15: + txtstr= ' -> '.join(txtlist[:5]) + ' ->\n' + ' -> '.join(txtlist[5:10]) + ' ->\n' + ' -> '.join(txtlist[10:15]) + ' ->\n' + ' -> '.join(txtlist[15:]) + '\n' + elif len(txtlist) > 10: + txtstr= ' -> '.join(txtlist[:5]) + ' ->\n' + ' -> '.join(txtlist[5:10]) + ' ->\n' + ' -> '.join(txtlist[10:]) + '\n' + elif len(txtlist) > 5: + txtstr= ' -> '.join(txtlist[:5]) + ' ->\n' + ' -> '.join(txtlist[5:]) + '\n' + else: + txtstr= ' -> '.join(txtlist) + '\n' + txtstr += self._公司信息['基本情况']['公司名称'] + '\n' + txtstr += self._公司信息['基本情况']['英文名称'] + + label= self._Axes.text(xbase, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left') + label.set_fontsize(4.5) + + + + def 绘制公司地域行业(self, xbase, ybase): + ''' + 地域、所属行业、上市日期 + ''' + + txtstr= self._公司信息['公司概况']['区域'] + ' ' + self._公司信息['公司概况']['所属行业'] + ' ' + self._公司信息['发行相关']['上市日期'] + + label= self._Axes.text(xbase, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left') + label.set_fontsize(6.5) + + + + def 绘制公司主营业务(self, xbase, ybase): + ''' + 主营业务 + ''' + # 查找表: (<文字长度>, <每行字数>, <字体大小>, ) + lookups= ( + (20, 10, 12.0, 0.5), + (45, 15, 8.2, 0.5), + (80, 20, 6.2, 0.5), + (125, 25, 5.0, 0.5), + (180, 30, 4.1, 0.5), + (245, 35, 3.5, 0.4), + (999999, 37, 3.4, 0.4) + ) + + txtstr= self._公司信息['基本情况']['主营业务'] + length= len(txtstr) + for sizelimit, linelimit, fontsize, yshift in lookups: + if length <= sizelimit: + txtstr= '\n'.join([txtstr[linelimit*idx : linelimit*(idx+1)] for idx in range(length//linelimit + 1)]) + fsize= fontsize + ycoord= ybase - yshift + break + + label= self._Axes.text(xbase, ycoord, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left', color='blue') + label.set_fontsize(fsize) + + + + def 绘制公司简介(self, xbase, ybase): + ''' + 公司简介 + ''' + # 查找表: (<文字长度>, <每行字数>, <字体大小>) + lookups= ( + (150, 30, 7.0), + (240, 40, 5.6), + (329, 47, 4.8), + (432, 54, 4.2), + (576, 64, 3.5), + (670, 67, 3.4), + (792, 72, 3.1), + (960, 80, 2.8), + (1222, 94, 2.4), + (1428, 102, 2.26), + (1620, 108, 2.12), + (1938, 114, 2.00), + (999999, 130, 1.75) + ) + + txtstr= self._公司信息['公司概况']['公司简介'] # 26 ~ 2600 字符 + length= len(txtstr) + + for sizelimit, linelimit, fontsize in lookups: + if length <= sizelimit: + txtstr= '\n'.join([txtstr[linelimit*idx : linelimit*(idx+1)] for idx in range(length//linelimit + 1)]) + fsize= fontsize + break + + label= self._Axes.text(xbase, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left') + label.set_fontsize(fsize) + + + + def 绘制公司分类信息(self, xbase, ybase): + ''' + 行业板块信息 + ''' + infolist= self._公司信息['行业板块'] + + for idx in range(len(infolist)//10 + 1): + txtstr= '\n'.join(infolist[10*idx : 10*(idx+1)]) + if not txtstr: + break + xcoord= xbase + 25.0*idx + label= self._Axes.text(xcoord, ybase, txtstr, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left', color='blue') + label.set_fontsize(3.4) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vn.training/SubPlot/分时价格子图.py b/vn.training/SubPlot/分时价格子图.py new file mode 100644 index 00000000..40a6bf6c --- /dev/null +++ b/vn.training/SubPlot/分时价格子图.py @@ -0,0 +1,580 @@ +# -*- coding: utf-8 -*- + + + + + +import datetime +import numpy +import math + + + +import matplotlib.ticker as ticker +import matplotlib.font_manager as font_manager + + + +import Public.Public as Public + + + + + +__font_properties__= font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc') + +__纵轴倍率__= 3.0 + + + + + +class 分时价格子图: + + def __init__(self, parent, 目标日期, 绘图数据): + ''' + + ''' + self._parent= parent + self._ilogger= Public.IndentLogger(logger=parent._ilogger._logger, indent=parent._ilogger._indent+1) + + # 原始数据 + 任务参数= 绘图数据['任务参数'] + self._公司信息= 绘图数据['公司信息'] + + # 当日数据 + 当日数据= 绘图数据['分时数据'][目标日期] + self._目标日期= 目标日期 + # self._日期对象= 当日数据['日期对象'] + # self._时间常数= 当日数据['时间常数'] + + # 横轴参数(需放在前面) + #=================================================================================================== + self._横轴参数= parent._横轴参数 + self._纵轴参数= None # XXX: 现在还不能计算,因为上下限与其他子图有关。 + self._坐标底数= 1.1 + + # 个股行情 + #=================================================================================================== + + # 分时行情是否存在 + self._个股行情有效= 当日数据['个股行情有效'] + + # 日线数据 + self._个股当日开盘= 当日数据['个股当日开盘'] + self._个股当日最高= 当日数据['个股当日最高'] + self._个股前日收盘= 当日数据['个股前日收盘'] # 前日收盘可能是 None + self._个股当日最低= 当日数据['个股当日最低'] + + if self._个股行情有效: + # 分时数据 + 个股分时行情= 当日数据['个股分时行情'] + self._个股价格序列= 个股分时行情['价格序列'] + + # 分时行情坐标序列 + self._个股调整时间= parent._个股调整时间 + self._个股坐标序列= parent._个股坐标序列 + + # 分时衍生数据(均线等) + self._分时格点粒度= 格点粒度= 任务参数['分时格点粒度'] + 间隔点数= 任务参数['均线间隔点数'] + 定时点数= 任务参数['均线定时点数'] + self._个股定时均线= Public.计算日内定时均线( \ + 价格序列=self._个股价格序列, \ + 调整时间序列=self._个股调整时间, \ + 格点粒度=格点粒度, \ + 间隔点数=间隔点数, \ + 定时点数=定时点数, \ + 需要规整=False \ + ) + for 均线 in self._个股定时均线.values(): + 均线['坐标序列']= self._parent.计算调整时间序列坐标(调整时间序列=均线['时间序列']) + 均线['走势标记']= Public.计算走势标记(序列=均线['均线序列']) + + # TODO: 指数行情 + #=================================================================================================== + + # 日线数据 + self._指数当日开盘= 当日数据['指数当日开盘'] + self._指数当日最高= 当日数据['指数当日最高'] + self._指数前日收盘= 当日数据['指数前日收盘'] # 前日收盘可能是 None + self._指数当日最低= 当日数据['指数当日最低'] + + + + # 平面对象,留待后面初始化 + #=================================================================================================== + self._布局参数= None + + self._指数平面= None + self._指数横轴= None + self._指数纵轴= None + + self._个股平面= None + self._个股横轴= None + self._个股纵轴= None + + + + def 计算纵轴坐标区间(self): + ''' + + ''' + 个股开盘= self._个股当日开盘 + 指数开盘= self._指数当日开盘 + + 个股最高= max(self._个股前日收盘, self._个股当日最高) * 1.01 if self._个股前日收盘 else self._个股当日最高 * 1.01 + 个股最低= min(self._个股前日收盘, self._个股当日最低) * 0.99 if self._个股前日收盘 else self._个股当日最低 * 0.99 + + 指数最高= self._指数当日最高 * 1.01 + 指数最低= self._指数当日最低 * 0.99 + + 个股综合最高= max(个股最高, 指数最高*个股开盘/指数开盘) + 个股综合最低= min(个股最低, 指数最低*个股开盘/指数开盘) + + 指数综合最高= max(指数最高, 个股最高*指数开盘/个股开盘) + 指数综合最低= min(指数最低, 个股最低*指数开盘/个股开盘) + + 纵标区间= {} + 纵标区间['个股最高']= 个股综合最高 + 纵标区间['个股最低']= 个股综合最低 + 纵标区间['指数最高']= 指数综合最高 + 纵标区间['指数最低']= 指数综合最低 + + return 纵标区间 + + + + def 计算纵轴参数(self, 坐标区间=None): + ''' + + ''' + def 计算个股坐标参数(基准价格, 坐标起点, 坐标终点): + ''' + + ''' + # 计算主坐标值 + 步进= 基准价格 / 100.0 + 主坐标值= [基准价格] + + 当前价格= 基准价格 + 步进 + while 当前价格 < 坐标终点: + 主坐标值.append( round(当前价格, -1) ) + 当前价格= 当前价格 + 步进 + + 当前价格= 基准价格 - 步进 + while 当前价格 > 坐标起点: + 主坐标值.append( round(当前价格, -1) ) + 当前价格= 当前价格 - 步进 + + 主坐标值= sorted(set(主坐标值)) + + # 计算副坐标值 + 步进= max(round(基准价格*0.01/4.0, -1), 10.0) # 选择一个步进值,10.0 代表最小刻度: 0.01元 + 副坐标值= [] + 当前价格= round(坐标起点+5.0, -1) + while 当前价格 < 坐标终点: + 副坐标值.append(当前价格) + 当前价格= 当前价格 + 步进 + 副坐标值= [价格 for 价格 in 副坐标值 if 价格 not in 主坐标值] + + 坐标参数= { + '个股主坐标值': 主坐标值, + '个股副坐标值': 副坐标值, + } + + return 坐标参数 + + if 坐标区间 is None: + 坐标区间= self.计算纵轴坐标区间() + + 个股坐标起点= 坐标区间['个股最低'] + 个股坐标终点= 坐标区间['个股最高'] + 纵轴尺寸= math.log(个股坐标终点, self._坐标底数) - math.log(个股坐标起点, self._坐标底数) + 纵轴高度= 纵轴尺寸 * __纵轴倍率__ + + 纵轴参数= {} + 纵轴参数['个股坐标起点']= 个股坐标起点 + 纵轴参数['个股坐标终点']= 个股坐标终点 + 纵轴参数['纵轴尺寸']= 纵轴尺寸 + 纵轴参数['纵轴高度']= 纵轴高度 + + # 指数部分,暂时这样 + 纵轴参数['指数坐标起点']= 坐标区间['指数最低'] + 纵轴参数['指数坐标终点']= 坐标区间['指数最高'] + + 基准价格= self._个股前日收盘 if self._个股前日收盘 else self._个股当日开盘 + 纵轴参数['基准价格']= 基准价格 + 纵轴参数.update( 计算个股坐标参数(基准价格=基准价格, 坐标起点=个股坐标起点, 坐标终点=个股坐标终点) ) + + self._纵轴参数= 纵轴参数 + + + + def 返回纵轴高度(self): + ''' + + ''' + return self._纵轴参数['纵轴高度'] + + + + def 返回指数平面(self): + ''' + + ''' + return self._指数平面 + + + + def 平面初始化(self, 图片对象, 子图偏移, 全图大小, sharex): + ''' + + ''' + # 计算自身的布局参数 + 子图横移, \ + 子图纵移= 子图偏移 + + 全图宽度, \ + 全图高度= 全图大小 + + 本图宽度= self._横轴参数['横轴宽度'] + 本图高度= self._纵轴参数['纵轴高度'] + + 布局参数= (子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 本图高度/全图高度) + + self._布局参数= 布局参数 + 坐标底数= self._坐标底数 + + # 指数部分 + #======================================================================================= + + # XXX: 指数与个股布局参数一样的话,label 一定要设成不一样,见 add_axes() 官方文档。 + 指数平面= 图片对象.add_axes(布局参数, axis_bgcolor='none', label='指数平面', sharex=sharex) + 指数平面.set_frame_on(False) + 指数平面.set_axisbelow(True) # 网格线放在底层 + 指数平面.set_yscale('log', basey=坐标底数) # 使用对数坐标 + + 指数横轴= 指数平面.get_xaxis() + 指数纵轴= 指数平面.get_yaxis() + + self._指数平面= 指数平面 + self._指数横轴= 指数横轴 + self._指数纵轴= 指数纵轴 + + self.设置指数横轴() + self.设置指数纵轴() + + # 个股部分 + #======================================================================================= + # 个股平面= 指数平面.twinx() # XXX: twinx 有问题,使用了以后指数的 ticks 就关不掉。可能是 bug + + # XXX: 指数与个股布局参数一样的话,label 一定要设成不一样,见 add_axes() 官方文档。 + 个股平面= 图片对象.add_axes(布局参数, axis_bgcolor='none', label='个股平面', sharex=sharex) + 个股平面.set_frame_on(False) + 个股平面.set_axisbelow(True) # 网格线放在底层 + 个股平面.set_yscale('log', basey=坐标底数) # 使用对数坐标 + + 个股横轴= 个股平面.get_xaxis() + 个股纵轴= 个股平面.get_yaxis() + + self._个股平面= 个股平面 + self._个股横轴= 个股横轴 + self._个股纵轴= 个股纵轴 + + self.设置个股横轴() + self.设置个股纵轴() + + + + def 设置指数横轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 指数横轴= self._指数横轴 + 横轴参数= self._横轴参数 + + 指数横轴.set_ticks_position('none') + + #================================================================================= + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + xMajorFormatter= 横轴参数['xMajorFormatter'] + xMinorFormatter= 横轴参数['xMinorFormatter'] + + 指数横轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 指数横轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 指数平面.set_xlim(坐标起点, 坐标终点) + 指数横轴.set_major_locator(xMajorLocator) + 指数横轴.set_minor_locator(xMinorLocator) + #================================================================================= + + for 主坐标 in 指数平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 指数平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置指数纵轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 指数纵轴= self._指数纵轴 + 纵轴参数= self._纵轴参数 + + 坐标起点= 纵轴参数['指数坐标起点'] + 坐标终点= 纵轴参数['指数坐标终点'] + + 指数平面.set_ylim(坐标起点, 坐标终点) + # 指数纵轴.set_label_position('left') # XXX: 不顶用 + 指数纵轴.set_ticks_position('none') + + for 主坐标 in 指数平面.get_yticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 指数平面.get_yticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置个股横轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 个股横轴= self._个股横轴 + 横轴参数= self._横轴参数 + + 个股横轴.set_ticks_position('none') + + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + 个股平面.set_xlim(坐标起点, 坐标终点) + + # xMajorLocator= 横轴参数['xMajorLocator'] + # xMinorLocator= 横轴参数['xMinorLocator'] + # xMajorFormatter= 横轴参数['xMajorFormatter'] + # xMinorFormatter= 横轴参数['xMinorFormatter'] + + # 个股横轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + # 个股横轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + # 个股横轴.set_major_locator(xMajorLocator) + # 个股横轴.set_minor_locator(xMinorLocator) + + for 主坐标 in 个股平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 个股平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置个股纵轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 个股纵轴= self._个股纵轴 + 纵轴参数= self._纵轴参数 + + 个股纵轴.set_ticks_position('none') + + 坐标起点= 纵轴参数['个股坐标起点'] + 坐标终点= 纵轴参数['个股坐标终点'] + + 主坐标值= 纵轴参数['个股主坐标值'] + 副坐标值= 纵轴参数['个股副坐标值'] + + # 个股纵轴.set_label_position('right') # XXX: 不顶用 + + 个股纵轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 个股纵轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 个股平面.set_ylim(坐标起点, 坐标终点) + + # 主坐标点 + yMajorLocator= ticker.FixedLocator( numpy.array(主坐标值) ) + 个股纵轴.set_major_locator(yMajorLocator) + + # def y_major_formatter(num, pos=None): + # return str(round(num/1000.0, 3)) + # yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + # 个股纵轴.set_major_formatter(yMajorFormatter) + + for 主坐标 in 个股平面.get_yticklabels(minor=False): + 主坐标.set_visible(False) + + # 副坐标点 + yMinorLocator= ticker.FixedLocator( numpy.array(副坐标值) ) + 个股纵轴.set_minor_locator(yMinorLocator) + + # def y_minor_formatter(num, pos=None): + # return str(round(num/1000.0, 3)) + # yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + # 个股纵轴.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 个股平面.get_yticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 绘图(self): + ''' + + ''' + self.绘制辅助标记() + self.绘制个股价格走势() + + if self._个股行情有效: + self.绘制个股价格均线() + + + + def 绘制个股价格走势(self): + ''' + + ''' + 个股平面= self._个股平面 + + # 特征价格 + 横标起点, 横标终点= self._横轴参数['坐标起点'], self._横轴参数['坐标终点'] + + if self._个股前日收盘: + 个股平面.plot((横标起点, 横标终点), (self._个股前日收盘, self._个股前日收盘), '-', color='lightblue', linewidth=0.7, alpha=0.5) + 个股平面.plot((横标起点, 横标终点), (self._个股当日开盘, self._个股当日开盘), '-', color='yellow', linewidth=0.7, alpha=0.5) + 个股平面.plot((横标起点, 横标终点), (self._个股当日最高, self._个股当日最高), '-', color='red', linewidth=0.7, alpha=0.3) + 个股平面.plot((横标起点, 横标终点), (self._个股当日最低, self._个股当日最低), '-', color='green', linewidth=0.7, alpha=0.5) + + if self._个股行情有效: + 坐标阵列= numpy.array(self._个股坐标序列) + 价格阵列= numpy.array(self._个股价格序列) + # 价格走势 + 个股平面.plot(坐标阵列, 价格阵列, 'o-', color='white', linewidth=0.15, label='_nolegend_', \ + markersize=0.3, markeredgecolor='white', markeredgewidth=0.1, alpha=1.0) + + + + + + def 绘制个股价格均线(self): + ''' + + ''' + 个股平面= self._个股平面 + 格点粒度= self._分时格点粒度 + 定时均线= self._个股定时均线 + + for 类别, 均线 in 定时均线.items(): + 坐标序列= 均线['坐标序列'] + 均线序列= 均线['均线序列'] + 走势标记= 均线['走势标记'] + + # 绘制均线 + 坐标阵列= numpy.array(坐标序列) + 均线阵列= numpy.array(均线序列) + + lcolor= 'yellow' if 类别*格点粒度 <= 300 else \ + 'cyan' if 类别*格点粒度 <= 600 else \ + 'magenta' + + 个股平面.plot(坐标阵列, 均线阵列, 'o-', color=lcolor, linewidth=0.1, label='_nolegend_', \ + markersize=0.2, markeredgecolor=lcolor, markeredgewidth=0.1, alpha=0.7) + + # 绘制均线拐点 + 底点阵列= numpy.array([均值 if 标记=='v' else None for 均值, 标记 in zip(均线序列, 走势标记)]) + 顶点阵列= numpy.array([均值 if 标记=='^' else None for 均值, 标记 in zip(均线序列, 走势标记)]) + + 个股平面.plot(坐标阵列, 底点阵列, '^', color=lcolor, label='均线底点', \ + markersize=1.5, markeredgecolor=lcolor, markeredgewidth=0.0, alpha=1.0) + + 个股平面.plot(坐标阵列, 顶点阵列, 'v', color=lcolor, label='均线顶点', \ + markersize=1.5, markeredgecolor=lcolor, markeredgewidth=0.0, alpha=1.0) + + + + def 绘制辅助标记(self): + ''' + + ''' + 指数平面= self._指数平面 + 个股平面= self._个股平面 + 横轴参数= self._横轴参数 + 纵轴参数= self._纵轴参数 + 公司信息= self._公司信息 + + 目标日期= self._目标日期 + + 指数纵标起点= 纵轴参数['指数坐标起点'] + 指数纵标终点= 纵轴参数['指数坐标终点'] + + 个股纵标起点= 纵轴参数['个股坐标起点'] + 个股纵标终点= 纵轴参数['个股坐标终点'] + + 标注位置= 横轴参数['标注位置'] + + # 画公司名称、目标日期 + #============================================================================================================ + 标注内容= 公司信息['个股代码'] + ' ' + 公司信息['个股简称'] + ' ' + 目标日期 + + 纵标= (指数纵标起点*指数纵标终点)**0.5 + 指数平面.text( 标注位置['09:30'], 纵标, 标注内容, fontproperties=__font_properties__, \ + color='0.3', fontsize=27, alpha=0.3, verticalalignment='center') + 指数平面.text( 标注位置['15:00']-300.0, 纵标, 标注内容, fontproperties=__font_properties__, \ + color='0.3', fontsize=27, alpha=0.3, horizontalalignment='right', verticalalignment='center') + + # 画时间标记 + #============================================================================================================ + # 15 分钟主时间点 + for iy in [指数纵标起点*1.004, 指数纵标终点*0.993]: + for ix, 时间, 表示 in zip(横轴参数['主坐标值'], 横轴参数['主标时间'], 横轴参数['主标表示']): + 标注= 指数平面.text(ix, iy, 表示, color='0.3', fontsize=8, zorder=0) + if 表示 in ('11:30', '15:00'): 标注.set_horizontalalignment('right') + + # 5 分钟副时间点 + for iy in [指数纵标起点*1.001, 指数纵标终点*0.997]: + for ix, 时间, 表示 in zip(横轴参数['副坐标值'], 横轴参数['副标时间'], 横轴参数['副标表示']): + 指数平面.text(ix, iy, 表示, color='0.3', fontsize=5, zorder=0) + + # 画价格标记 + #============================================================================================================ + 标注位置组一= 横轴参数['标注位置组一'] + + 主标价格= [nr for nr in 纵轴参数['个股主坐标值'] if nr > 个股纵标起点*1.01 and nr < 个股纵标终点*0.99] + 副标价格= [nr for nr in 纵轴参数['个股副坐标值'] if nr > 个股纵标起点*1.01 and nr < 个股纵标终点*0.99] + for 横标 in 标注位置组一: + for 纵标 in 主标价格: + 个股平面.text(横标-30.0, 纵标, str(纵标/1000.0), color='0.3', fontsize=3.0, horizontalalignment='right', zorder=0) + for 纵标 in 副标价格: + 个股平面.text(横标+30.0, 纵标, str(纵标/1000.0), color='0.3', fontsize=3.0, zorder=0) + + # 画档位标记 + #============================================================================================================ + 标注位置组二= 横轴参数['标注位置组二'] + 基准价格= 纵轴参数['基准价格'] + + 正向档位= [nr for nr in 纵轴参数['个股主坐标值'] if nr >= 基准价格 and nr < 个股纵标终点*0.99] + 负向档位= list(reversed([nr for nr in 纵轴参数['个股主坐标值'] if nr < 基准价格 and nr > 个股纵标起点])) + + for 横标 in 标注位置组二: + for 档位, 纵标 in enumerate(正向档位, start=1): + 个股平面.text(横标+30.0, 纵标*1.001, str(档位), color='red', fontsize=25, alpha=0.17, zorder=0) + for 档位, 纵标 in enumerate(负向档位, start=1): + 个股平面.text(横标+30.0, 纵标*1.001, str(档位), color='green', fontsize=25, alpha=0.2, zorder=0) + + + + + + + + + + + diff --git a/vn.training/SubPlot/分时手数子图.py b/vn.training/SubPlot/分时手数子图.py new file mode 100644 index 00000000..18962bb9 --- /dev/null +++ b/vn.training/SubPlot/分时手数子图.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- + + + + + +import numpy + + + +# import matplotlib.spines as spines +import matplotlib.ticker as ticker + + + +import Public.Public as Public + + + + + + + +class 分时手数子图: + + def __init__(self, parent, 目标日期, 绘图数据): + ''' + + ''' + self._parent= parent + self._ilogger= Public.IndentLogger(logger=parent._ilogger._logger, indent=parent._ilogger._indent+1) + + # 原始数据 + 任务参数= 绘图数据['任务参数'] + + # 当日数据 + 当日数据= 绘图数据['分时数据'][目标日期] + self._本图日期= 目标日期 + # self._日期对象= 当日数据['日期对象'] + # self._时间常数= 当日数据['时间常数'] + + # 个股行情 + #=================================================================================================== + + # 分时行情是否存在 + self._个股行情有效= 当日数据['个股行情有效'] + + # 日线数据 + self._个股平均成交= 当日数据['个股平均成交'] # 格式: 行情['平均手数']= {Public.计算均值(个股成交量[-n:]) for n in 个股量均参数} + + if self._个股行情有效: + # 分时数据 + 个股分时行情= 当日数据['个股分时行情'] + self._个股手数序列= 个股分时行情['手数序列'] + self._个股金额序列= 个股分时行情['金额序列'] + self._个股备注序列= 个股分时行情['备注序列'] + + # self._个股上涨标记= 个股分时行情['上涨标记'] + # self._个股下跌标记= 个股分时行情['下跌标记'] + # self._个股平盘标记= 个股分时行情['平盘标记'] + + self._个股买盘标记= [True if 备注.startswith('买') else False for 备注 in self._个股备注序列] + self._个股卖盘标记= [True if 备注.startswith('卖') else False for 备注 in self._个股备注序列] + self._个股中性标记= [True if 备注.startswith('中') else False for 备注 in self._个股备注序列] + + # 分时衍生数据(均线等) + self._个股坐标序列= parent._个股坐标序列 + self._个股平均手数= Public.计算均值(序列=[nr for nr in self._个股手数序列 if nr>0]) + + # TODO: 指数行情 + #=================================================================================================== + + + + # 横轴参数、纵轴参数 + #=================================================================================================== + self._横轴参数= parent._横轴参数 + self._纵轴参数= None + + # 平面对象,留待后面初始化 + #=================================================================================================== + self._个股布局参数= None + self._指数布局参数= None + + self._指数平面= None + self._指数横轴= None + self._指数纵轴= None + + self._个股平面= None + self._个股横轴= None + self._个股纵轴= None + + + + def 计算成交步进记录(self): + ''' + 本函数是 naive 的,只考虑本图。 + ''' + if self._个股行情有效: + # 决定个股步进值 + 个股平均手数= self._个股平均手数[0] + 个股步进= 25 # 步进代表主坐标的间隔距离 + + while 个股平均手数/个股步进 > 1.0: + 个股步进= 个股步进*2 + else: + 个股步进= 25 + + return {'个股步进': 个股步进} + + + + def 计算纵轴参数(self, 步进记录=None): + ''' + + ''' + 纵轴参数= {} + + # 大小固定 + #======================================================================================= + 纵轴尺寸= 4.0 + 纵轴倍率= 0.3 + 纵轴参数['纵轴倍率']= 纵轴倍率 + 纵轴参数['纵轴高度']= 纵轴尺寸 * 纵轴倍率 + + if 步进记录 is None: + 步进记录= self.计算成交步进记录() + + # 个股部分 + #======================================================================================= + 步进= 步进记录['个股步进'] + + # 坐标起点 与 坐标终点 + 个股坐标起点= 0.0 + 个股坐标终点= max(self._个股手数序列) if self._个股行情有效 else 步进*纵轴尺寸 + 纵轴参数['个股坐标起点']= 个股坐标起点 + 纵轴参数['个股坐标终点']= 个股坐标终点 + + 个股纵轴尺寸= max(个股坐标终点/步进, 纵轴尺寸) + 纵轴参数['个股纵轴尺寸']= 个股纵轴尺寸 + 纵轴参数['个股纵轴高度']= 个股纵轴尺寸 * 纵轴倍率 + + # 计算 坐标值 与 坐标点 + 个股主坐标值= [步进 * i for i in range(1, 4)] + 个股副坐标值= [(步进/2.0) + 步进*i for i in range(4)] + if 个股副坐标值[-1] > 纵轴参数['个股坐标终点']: del 个股副坐标值[-1] + + 纵轴参数['个股主坐标值']= 个股主坐标值 + 纵轴参数['个股副坐标值']= 个股副坐标值 + + self._纵轴参数= 纵轴参数 + + + + def 返回纵轴高度(self): + ''' + + ''' + return self._纵轴参数['纵轴高度'] + + + + def 返回指数平面(self): + ''' + + ''' + return self._指数平面 + + + + def 平面初始化(self, 图片对象, 子图偏移, 全图大小): + ''' + + ''' + 子图横移, \ + 子图纵移= 子图偏移 + + 全图宽度, \ + 全图高度= 全图大小 + + 本图宽度= self._横轴参数['横轴宽度'] + 指数平面高度= self._纵轴参数['纵轴高度'] # XXX: 以后指数平面可以有自己的高度 + 个股平面高度= self._纵轴参数['个股纵轴高度'] + + 指数布局参数= (子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 指数平面高度/全图高度) + 个股布局参数= (子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 个股平面高度/全图高度) + + self._指数布局参数= 指数布局参数 + self._个股布局参数= 个股布局参数 + + # 指数部分 + #======================================================================================= + 指数平面= 图片对象.add_axes(指数布局参数, axis_bgcolor='black') + 指数平面.set_frame_on(False) # XXX + 指数平面.set_axisbelow(True) # 网格线放在底层 + + 指数横轴= 指数平面.get_xaxis() + 指数纵轴= 指数平面.get_yaxis() + + self._指数平面= 指数平面 + self._指数横轴= 指数横轴 + self._指数纵轴= 指数纵轴 + + self.设置指数横轴() + self.设置指数纵轴() + + # 个股部分 + #======================================================================================= + + # XXX: 不用 twinx(),原因见分时价格子图。 + 个股平面= 图片对象.add_axes(个股布局参数, axis_bgcolor='black') + 个股平面.set_frame_on(False) # XXX + 个股平面.set_axisbelow(True) # 网格线放在底层 + + for 方位, 边框 in 个股平面.spines.items(): + 边框.set_color(None) + + 个股横轴= 个股平面.get_xaxis() + 个股纵轴= 个股平面.get_yaxis() + + self._个股平面= 个股平面 + self._个股横轴= 个股横轴 + self._个股纵轴= 个股纵轴 + + self.设置个股横轴() + self.设置个股纵轴() + + + + def 设置指数横轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 指数横轴= self._指数横轴 + 横轴参数= self._横轴参数 + + 指数横轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 指数横轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 指数横轴.set_ticks_position('none') + + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + + 指数平面.set_xlim(坐标起点, 坐标终点) + 指数横轴.set_major_locator(xMajorLocator) + 指数横轴.set_minor_locator(xMinorLocator) + + for 主坐标 in 指数平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 指数平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置指数纵轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 指数纵轴= self._指数纵轴 + + # 指数纵轴.set_label_position('right') # XXX: 不顶用 + 指数纵轴.set_ticks_position('none') + + for 主坐标 in 指数平面.get_yticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 指数平面.get_yticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置个股横轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 个股横轴= self._个股横轴 + 横轴参数= self._横轴参数 + + 个股横轴.set_ticks_position('none') + + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + xMajorFormatter= 横轴参数['xMajorFormatter'] + xMinorFormatter= 横轴参数['xMinorFormatter'] + + 个股平面.set_xlim(坐标起点, 坐标终点) + 个股横轴.set_major_locator(xMajorLocator) + 个股横轴.set_minor_locator(xMinorLocator) + + 个股横轴.set_major_formatter(xMajorFormatter) + 个股横轴.set_minor_formatter(xMinorFormatter) + + for 主坐标 in 个股平面.get_xticklabels(minor=False): + 主坐标.set_fontsize(5) + 主坐标.set_horizontalalignment('right') + 主坐标.set_rotation('45') + + for 副坐标 in 个股平面.get_xticklabels(minor=True): + 副坐标.set_fontsize(4) + 副坐标.set_color('blue') + 副坐标.set_horizontalalignment('right') + 副坐标.set_rotation('45') + + + + def 设置个股纵轴(self): + ''' + + ''' + 平面对象= self._个股平面 + 个股纵轴= self._个股纵轴 + 纵轴参数= self._纵轴参数 + + # 个股纵轴.set_label_position('right') # XXX: 不顶用 + 个股纵轴.set_ticks_position('none') + + 个股纵轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 个股纵轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 坐标起点= 纵轴参数['个股坐标起点'] + 坐标终点= 纵轴参数['个股坐标终点'] + + 平面对象.set_ylim(坐标起点, 坐标终点) + + # 主坐标点 + #====================================================================================== + 主坐标值= 纵轴参数['个股主坐标值'] + yMajorLocator= ticker.FixedLocator(numpy.array(主坐标值)) + 个股纵轴.set_major_locator(yMajorLocator) + + # def y_major_formatter(num, pos=None): + # return str(num) + # yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + # 个股纵轴.set_major_formatter(yMajorFormatter) + + for 主坐标 in 平面对象.get_yticklabels(minor=False): + 主坐标.set_visible(False) + + # 副坐标点 + #====================================================================================== + 副坐标值= 纵轴参数['个股副坐标值'] + yMinorLocator= ticker.FixedLocator(numpy.array(副坐标值)) + 个股纵轴.set_minor_locator(yMinorLocator) + + # def y_minor_formatter(num, pos=None): + # return str(num) + # yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + # 个股纵轴.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 平面对象.get_yticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 绘图(self): + ''' + + ''' + self.绘制辅助标记() + self.绘制个股手数() + + + + def 绘制辅助标记(self): + ''' + + ''' + 个股平面= self._个股平面 + + 标注位置组一= self._横轴参数['标注位置组一'] + 主标手数= self._纵轴参数['个股主坐标值'] + 副标手数= self._纵轴参数['个股副坐标值'] + + for 横标 in 标注位置组一: + for 纵标 in 主标手数: + 个股平面.text(横标+30.0, 纵标, str(int(纵标)), color='0.3', fontsize=5.0, zorder=0) + for 纵标 in 副标手数: + 个股平面.text(横标+30.0, 纵标, str(int(纵标)), color='0.3', fontsize=5.0, zorder=0) + + + + def 绘制个股手数(self): + ''' + + ''' + 个股平面= self._个股平面 + + if self._个股行情有效: + 坐标序列= numpy.array(self._个股坐标序列) + 手数序列= numpy.array(self._个股手数序列) + 序列长度= len(手数序列) + 起点序列= numpy.zeros(序列长度) + + # 正向标记= numpy.array(self._个股上涨标记) + # 负向标记= numpy.array(self._个股下跌标记) + # 中性标记= numpy.array(self._个股平盘标记) + + 正向标记= numpy.array(self._个股买盘标记) + 负向标记= numpy.array(self._个股卖盘标记) + 中性标记= numpy.array(self._个股中性标记) + + lwidth, alpha= (0.15, 1.0) + + if True in 正向标记: + 个股平面.vlines(坐标序列[正向标记], 起点序列[正向标记], 手数序列[正向标记], edgecolor='red', linewidth=lwidth, label='_nolegend_', alpha=alpha) + if True in 负向标记: + 个股平面.vlines(坐标序列[负向标记], 起点序列[负向标记], 手数序列[负向标记], edgecolor='green', linewidth=lwidth, label='_nolegend_', alpha=alpha) + if True in 中性标记: + 个股平面.vlines(坐标序列[中性标记], 起点序列[中性标记], 手数序列[中性标记], edgecolor='white', linewidth=lwidth, label='_nolegend_', alpha=alpha) + + # 绘制平均手数数值(直线) + 平均手数= self._个股平均手数[0] + 横轴参数= self._横轴参数 + 横标起点= 横轴参数['坐标起点'] + 横标终点= 横轴参数['坐标终点'] + + 个股平面.plot([横标起点, 横标终点], [平均手数, 平均手数], '-', color='yellow', linewidth=0.2, alpha=0.7) + + + + + + + + + + diff --git a/vn.training/SubPlot/实盘价格子图.py b/vn.training/SubPlot/实盘价格子图.py new file mode 100644 index 00000000..2a08a67c --- /dev/null +++ b/vn.training/SubPlot/实盘价格子图.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + + + + + +class 实盘价格子图: + ''' + + ''' + + def __init__(self, parent, 绘图数据): + ''' + + ''' + pass + + + + + + + diff --git a/vn.training/SubPlot/实盘手数子图.py b/vn.training/SubPlot/实盘手数子图.py new file mode 100644 index 00000000..eca94e93 --- /dev/null +++ b/vn.training/SubPlot/实盘手数子图.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + + + + + +class 实盘手数子图: + ''' + + ''' + + def __init__(self, parent, 绘图数据, 价格子图): + ''' + + ''' + pass + + + + + + diff --git a/vn.training/SubPlot/日线价格子图.py b/vn.training/SubPlot/日线价格子图.py new file mode 100644 index 00000000..13c6a10c --- /dev/null +++ b/vn.training/SubPlot/日线价格子图.py @@ -0,0 +1,1327 @@ +# -*- coding: utf-8 -*- + + + + +import copy +import numpy +import datetime +import math + +import matplotlib.spines as spines +import matplotlib.ticker as ticker +import matplotlib.patches as patches +import matplotlib.font_manager as font_manager + + + +import Public.Public as Public + + + +__color_gold__= '#FDDB05' +__color_gray70__= '0.7' +__font_properties__= font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc') + +__横轴倍率__= 10.0 / 230.0 +__纵轴倍率__= 0.3 + + + + + +class 日线价格子图: + + def __init__(self, parent, 绘图数据): + ''' + + ''' + self._parent= parent + self._ilogger= Public.IndentLogger(logger=parent._ilogger._logger, indent=parent._ilogger._indent+1) + + # 原始数据 + self._公司信息= 绘图数据['公司信息'] + self._任务参数= 绘图数据['任务参数'] + self._日线数据= 绘图数据['日线数据'] + # self._实盘数据= 绘图数据['实盘数据'] + + self._个股代码= self._公司信息['个股代码'] + self._复权记录= self._日线数据['复权记录'] + + # 任务参数数据 + self._复权绘图= self._任务参数['复权绘图'] + self._绘制实盘= self._任务参数['绘制实盘'] + self._绘制个股均线= self._任务参数['绘制个股均线'] + self._绘制指数均线= self._任务参数['绘制指数均线'] + + # 全局日线数据 + self._全局指数日线= self._日线数据['指数日线'] + self._全局绘图日线= self._日线数据['绘图日线'] # 绘图日线 就是补全日线,根据任务参数已经选择了是否复权 + self._全局日期序列= self._全局指数日线['日期'] + + # 目标日线数据 + self._目标起始偏移= self._日线数据['目标起始偏移'] + self._目标结束偏移= self._日线数据['目标结束偏移'] + + self._目标指数日线= { 项目 : 序列[self._目标起始偏移:self._目标结束偏移+1] for 项目, 序列 in self._全局指数日线.items() } + self._目标绘图日线= { 项目 : 序列[self._目标起始偏移:self._目标结束偏移+1] for 项目, 序列 in self._全局绘图日线.items() } + + # 均线数据 + if self._绘制指数均线: + self._全局指数均线= self._日线数据['指数均线'] + self._目标指数均线= { 项目 : 序列[self._目标起始偏移:self._目标结束偏移+1] for 项目, 序列 in self._全局指数均线.items() } + if self._绘制个股均线: + self._全局绘图均线= self._日线数据['绘图均线'] + self._目标绘图均线= { 项目 : 序列[self._目标起始偏移:self._目标结束偏移+1] for 项目, 序列 in self._全局绘图均线.items() } + + self._目标日期序列= self._目标绘图日线['日期'] + + self._目标指数开盘= self._目标指数日线['开盘'] + self._目标指数收盘= self._目标指数日线['收盘'] + self._目标指数最高= self._目标指数日线['最高'] + self._目标指数最低= self._目标指数日线['最低'] + self._目标指数中线= self._目标指数日线['开收中'] + + self._目标绘图开盘= self._目标绘图日线['开盘'] + self._目标绘图收盘= self._目标绘图日线['收盘'] + self._目标绘图最高= self._目标绘图日线['最高'] + self._目标绘图最低= self._目标绘图日线['最低'] + self._目标绘图中线= self._目标绘图日线['开收中'] + + # 行情附加数据 + self._对数坐标底数= 1.1 + + self._横轴坐标序列= numpy.arange(self._目标起始偏移, self._目标结束偏移+1) + self._目标行情长度= len(self._目标日期序列) + + self._目标指数上涨= numpy.array( [True if po is not None and po < pc else False for po, pc in zip(self._目标指数开盘, self._目标指数收盘)] ) # 标示出该天股价日内上涨的一个序列 + self._目标指数下跌= numpy.array( [True if po is not None and po > pc else False for po, pc in zip(self._目标指数开盘, self._目标指数收盘)] ) # 标示出该天股价日内下跌的一个序列 + self._目标指数平盘= numpy.array( [True if po is not None and po == pc else False for po, pc in zip(self._目标指数开盘, self._目标指数收盘)] ) # 标示出该天股价日内走平的一个序列 + + self._目标绘图上涨= numpy.array( [True if po is not None and po < pc else False for po, pc in zip(self._目标绘图开盘, self._目标绘图收盘)] ) # 标示出该天股价日内上涨的一个序列 + self._目标绘图下跌= numpy.array( [True if po is not None and po > pc else False for po, pc in zip(self._目标绘图开盘, self._目标绘图收盘)] ) # 标示出该天股价日内下跌的一个序列 + self._目标绘图平盘= numpy.array( [True if po is not None and po == pc else False for po, pc in zip(self._目标绘图开盘, self._目标绘图收盘)] ) # 标示出该天股价日内走平的一个序列 + + # 行情衍生数据 + self._指数衍生行情= self._日线数据['指数衍生行情'] + self._个股衍生行情= self._日线数据['个股衍生行情'] + + # 平面对象数据 + self._布局参数= None + self._指数平面= None + self._指数横轴= None + self._指数纵轴= None + + self._个股平面= None + self._个股横轴= None + self._个股纵轴= None + + # 横轴参数、纵轴参数 + self._横轴参数= self.计算横轴参数() + self._纵轴参数= self.计算纵轴参数() + + # 行情衍生数据 + self._定制绘图函数= self._任务参数['定制绘图函数'] + self._定制绘图参数= self._任务参数['定制绘图参数'] + + + + def 返回目标日线数据(self): + ''' + 日线换手子图用 + ''' + 日线行情= {} + 日线行情['目标指数日线']= self._目标指数日线 + 日线行情['目标绘图日线']= self._目标绘图日线 + + return 日线行情 + + + + def 返回行情附加数据(self): + ''' + 日线换手子图用 + ''' + 附加行情= {} + + 附加行情['横轴坐标序列']= self._横轴坐标序列 + 附加行情['目标行情长度']= self._目标行情长度 + + 附加行情['目标指数上涨']= self._目标指数上涨 + 附加行情['目标指数下跌']= self._目标指数下跌 + 附加行情['目标指数平盘']= self._目标指数平盘 + + 附加行情['目标绘图上涨']= self._目标绘图上涨 + 附加行情['目标绘图下跌']= self._目标绘图下跌 + 附加行情['目标绘图平盘']= self._目标绘图平盘 + + return 附加行情 + + + + def 返回本图大小(self): + ''' + + ''' + return (self._横轴参数['横轴宽度'], self._纵轴参数['纵轴高度']) + + + + def 平面初始化(self, 图片对象, 子图偏移, 全图大小): + ''' + + ''' + + + # 计算布局参数 + #======================================================================================= + 子图横移, \ + 子图纵移= 子图偏移 + + 全图宽度, \ + 全图高度= 全图大小 + + 本图宽度= self._横轴参数['横轴宽度'] + 本图高度= self._纵轴参数['纵轴高度'] + + 布局参数= ( 子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 本图高度/全图高度 ) + + self._布局参数= 布局参数 + 对数坐标底数= self._对数坐标底数 + + # 指数部分 + #======================================================================================= + 指数平面= 图片对象.add_axes(布局参数, axis_bgcolor='black') + + 指数平面.set_axisbelow(True) # 网格线放在底层 + 指数平面.set_yscale('log', basey=对数坐标底数) # 使用对数坐标 + + # 改变坐标线的颜色 + for 方位, 边框 in 指数平面.spines.items(): # 方位: 'left' | 'right' | 'top' | 'bottom' + 边框.set_color(__color_gold__) + + 指数横轴= 指数平面.get_xaxis() + 指数纵轴= 指数平面.get_yaxis() + + # 指数纵轴.set_label_position('left') # XXX: 不顶用 + + self._指数平面= 指数平面 + self._指数横轴= 指数横轴 + self._指数纵轴= 指数纵轴 + + self.设置指数横轴() + self.设置指数纵轴() + + # 个股部分 + #======================================================================================= + 个股平面= 指数平面.twinx() + 个股平面.set_axis_bgcolor('black') + 个股平面.set_axisbelow(True) # 网格线放在底层 + 个股平面.set_yscale('log', basey=对数坐标底数) # 使用对数坐标 + + # for 方位, 边框 in 个股平面.spines.items(): # 方位: 'left' | 'right' | 'top' | 'bottom' + # 边框.set_color(__color_gold__) + + 个股横轴= 个股平面.get_xaxis() + 个股纵轴= 个股平面.get_yaxis() + + # 个股纵轴.set_label_position('right') # XXX: 不顶用 + + self._个股平面= 个股平面 + self._个股横轴= 个股横轴 + self._个股纵轴= 个股纵轴 + + self.设置个股横轴() + self.设置个股纵轴() + + + + def 返回指数平面(self): + ''' + + ''' + return self._指数平面 + + + + def 计算横轴余度(self, 横轴裕量): + ''' + 被 计算横轴参数() 调用 + ''' + 目标起始偏移= self._目标起始偏移 + 目标结束偏移= self._目标结束偏移 + + 坐标起点= 目标起始偏移 - 横轴裕量 + 坐标终点= 目标结束偏移 + 横轴裕量 + 横轴尺寸= 坐标终点 - 坐标起点 + + 横轴余度= {} + 横轴余度['横轴裕量']= 横轴裕量 + 横轴余度['坐标起点']= 坐标起点 + 横轴余度['坐标终点']= 坐标终点 + 横轴余度['横轴尺寸']= 横轴尺寸 + 横轴余度['横轴倍率']= __横轴倍率__ + 横轴余度['横轴宽度']= 横轴尺寸 * __横轴倍率__ + + return 横轴余度 + + + + def 计算纵轴余度(self): + ''' + 被 计算纵轴参数() 调用 + ''' + 对数坐标底数= self._对数坐标底数 + + 个股最高= [nr for nr in self._目标绘图最高 if nr is not None] + 个股最低= [nr for nr in self._目标绘图最低 if nr is not None] + 个股开盘= [nr for nr in self._目标绘图开盘 if nr is not None] + + 指数基点= self._目标指数开盘[0] + 个股基点= round(个股开盘[0], -1) # XXX: 对复权行情来说,基点价格可能不是整数,所以需要取整。 + + 指数极高= max(self._目标指数最高) + 指数极低= min(self._目标指数最低) + + 个股极高= max(个股最高) + 个股极低= min(个股最低) + + 指数修正极低= min(指数极低, 个股极低*指数基点/个股基点) + 指数修正极高= max(指数极高, 个股极高*指数基点/个股基点) + + 个股修正极低= min(个股极低, 指数极低*个股基点/指数基点) + 个股修正极高= max(个股极高, 指数极高*个股基点/指数基点) + + 指数坐标起点= 指数修正极低 / 对数坐标底数 + 指数坐标终点= 指数修正极高 * 对数坐标底数 + 个股坐标起点= 个股修正极低 / 对数坐标底数 + 个股坐标终点= 个股修正极高 * 对数坐标底数 + + # 计算 纵轴尺寸。 + # XXX: 注意,用个股坐标或指数坐标计算都可以,其中包含的倍数因子对结果无影响,即: + # log(base, n1) - log(base, n2) == + # log(base, n1/n2) == + # log(base, k*n1/k*n2) == + # log(base, k*n1) - log(base, k*n2) + # ,这是对数运算的性质。 + 纵轴尺寸= math.log(个股坐标终点, 对数坐标底数) - math.log(个股坐标起点, 对数坐标底数) + + 纵轴余度= { + '纵轴倍率': __纵轴倍率__, + '纵轴尺寸': 纵轴尺寸, + '纵轴高度': 纵轴尺寸 * __纵轴倍率__, + '个股基点': 个股基点, + '指数基点': 指数基点, + '个股坐标起点': 个股坐标起点, + '个股坐标终点': 个股坐标终点, + '指数坐标起点': 指数坐标起点, + '指数坐标终点': 指数坐标终点, + } + + return 纵轴余度 + + + + def 计算横轴参数(self): + ''' + + ''' + 全局日期序列= self._全局日期序列 + 目标起始偏移= self._目标起始偏移 + 目标结束偏移= self._目标结束偏移 + + 全局日期列表= [ datetime.date(int(ys), int(ms), int(ds)) for ys, ms, ds in [ dstr.split('-') for dstr in 全局日期序列 ] ] + 目标日期列表= 全局日期列表[目标起始偏移:目标结束偏移+1] + + # 确定 X 轴的 主要坐标点 + 每月首日偏移= [] # 每个月第一个交易日在所有日期列表中的 index + 所有年份= sorted(set([d.year for d in 目标日期列表])) + + for 年份 in 所有年份: + 当年月份= sorted(set([d.month for d in 目标日期列表 if d.year==年份])) # 当年所有的交易月份 + for 月份 in 当年月份: + 当月首日= min([d for d in 目标日期列表 if d.year==年份 and d.month==月份]) # 当月的第一个交易日 + 每月首日偏移.append(全局日期列表.index(当月首日)) + + xMajorLocator= ticker.FixedLocator(numpy.array(每月首日偏移)) + + # 确定 X 轴的 辅助坐标点 + 每周首日偏移= {} # value: 每周第一个交易日在所有日期列表中的 index; key: 当周的序号 week number(当周是第几周) + + for 日期 in 目标日期列表: + isoyear, weekno= 日期.isocalendar()[0:2] + dmark= isoyear*100 + weekno + if dmark not in 每周首日偏移: + 每周首日偏移[dmark]= 全局日期列表.index(日期) + + 每周首日偏移= sorted(每周首日偏移.values()) + + xMinorLocator= ticker.FixedLocator(numpy.array(每周首日偏移)) + + # 确定 X 轴的 MajorFormatter 和 MinorFormatter + def x_major_formatter(idx, pos=None): + return 全局日期列表[idx].strftime('%Y-%m-%d') + + def x_minor_formatter(idx, pos=None): + return 全局日期列表[idx].strftime('%m-%d') + + xMajorFormatter= ticker.FuncFormatter(x_major_formatter) + xMinorFormatter= ticker.FuncFormatter(x_minor_formatter) + + # 填充并返回 + 横轴参数= {} + + 横轴参数['xMajorLocator']= xMajorLocator + 横轴参数['xMinorLocator']= xMinorLocator + 横轴参数['xMajorFormatter']= xMajorFormatter + 横轴参数['xMinorFormatter']= xMinorFormatter + 横轴参数['每月首日偏移']= 每月首日偏移 + 横轴参数['每周首日偏移']= 每周首日偏移 + + 横轴裕量= 1 + 横轴余度= self.计算横轴余度(横轴裕量=横轴裕量) + 横轴参数.update(横轴余度) + + return 横轴参数 + + + + def 返回横轴参数(self): + ''' + + ''' + return self._横轴参数 + + + + def 计算纵轴参数(self): + ''' + 计算纵轴坐标点 + ''' + 步进倍率= 1.1 + + 纵轴余度= self.计算纵轴余度() + + 个股坐标起点= 纵轴余度['个股坐标起点'] + 个股坐标终点= 纵轴余度['个股坐标终点'] + + 个股基点= 纵轴余度['个股基点'] + 指数基点= 纵轴余度['指数基点'] + + # 计算个股主坐标值 + #===================================================================================================== + 个股主坐标值= [个股基点] + + # 往下拓展 + for i in range(1, 999): + 当前坐标= round(个股基点/(步进倍率**i), -1) # 坐标值 7890 表示 7.89 元 + if 当前坐标 <= 个股坐标起点: + break + 个股主坐标值.insert(0, 当前坐标) + # 往上拓展 + for i in range(1, 999): + 当前坐标= round(个股基点*(步进倍率**i), -1) # 坐标值 7890 表示 7.89 元 + if 当前坐标 >= 个股坐标终点: + break + 个股主坐标值.append(当前坐标) + + # 计算个股副坐标值 + #===================================================================================================== + 个股副坐标值= [round((curr*next)**0.5, -1) for curr, next in zip(个股主坐标值[:-1], 个股主坐标值[1:])] + 极值= round((个股主坐标值[0]*个股主坐标值[0]/步进倍率)**0.5, -1) + if 极值 > 个股坐标起点: 个股副坐标值.insert(0, 极值) + 极值= round((个股主坐标值[-1]*个股主坐标值[-1]*步进倍率)**0.5, -1) + if 极值 < 个股坐标终点: 个股副坐标值.append(极值) + + # 计算指数主、副坐标值 + #===================================================================================================== + 指数主坐标值= [数值*指数基点/个股基点 for 数值 in 个股主坐标值] + 指数副坐标值= [数值*指数基点/个股基点 for 数值 in 个股副坐标值] + + # 生成、返回 + #===================================================================================================== + 纵轴参数= { + '个股主坐标值': 个股主坐标值, + '个股副坐标值': 个股副坐标值, + '指数主坐标值': 指数主坐标值, + '指数副坐标值': 指数副坐标值, + } + 纵轴参数.update(纵轴余度) + + return 纵轴参数 + + + + def 设置指数横轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 横轴对象= self._指数横轴 + 横轴参数= self._横轴参数 + + 横轴裕量= 横轴参数['横轴裕量'] + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + xMajorFormatter= 横轴参数['xMajorFormatter'] + xMinorFormatter= 横轴参数['xMinorFormatter'] + + 横轴对象.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 横轴对象.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + 指数平面.set_xlim(坐标起点, 坐标终点) + 横轴对象.set_major_locator(xMajorLocator) + 横轴对象.set_minor_locator(xMinorLocator) + + for 主坐标 in 指数平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 指数平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置个股横轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 横轴对象= self._个股横轴 + 横轴参数= self._横轴参数 + + 横轴裕量= 横轴参数['横轴裕量'] + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + xMajorFormatter= 横轴参数['xMajorFormatter'] + xMinorFormatter= 横轴参数['xMinorFormatter'] + + # 横轴对象.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + # 横轴对象.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + 个股平面.set_xlim(坐标起点, 坐标终点) + 横轴对象.set_major_locator(xMajorLocator) + 横轴对象.set_minor_locator(xMinorLocator) + + for 主坐标 in 个股平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + for 副坐标 in 个股平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置指数纵轴(self): + ''' + + ''' + 指数平面= self._指数平面 + 纵轴对象= self._指数纵轴 + 纵轴参数= self._纵轴参数 + 坐标起点= 纵轴参数['指数坐标起点'] + 坐标终点= 纵轴参数['指数坐标终点'] + + 纵轴对象.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 纵轴对象.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + 指数平面.set_ylim(坐标起点, 坐标终点) + + # 主坐标点 + #====================================================================================== + 主坐标值= 纵轴参数['指数主坐标值'] + yMajorLocator= ticker.FixedLocator(numpy.array(主坐标值)) + def y_major_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + + 纵轴对象.set_major_locator(yMajorLocator) + 纵轴对象.set_major_formatter(yMajorFormatter) + + for 主坐标 in 指数平面.get_yticklabels(minor=False): + 主坐标.set_fontsize(6) + + # 副坐标点 + #====================================================================================== + 副坐标值= 纵轴参数['指数副坐标值'] + yMinorLocator= ticker.FixedLocator(numpy.array(副坐标值)) + def y_minor_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + + 纵轴对象.set_minor_locator(yMinorLocator) + 纵轴对象.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 指数平面.get_yticklabels(minor=True): + 副坐标.set_fontsize(5) + 副坐标.set_color('blue') + + + + def 设置个股纵轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 纵轴对象= self._个股纵轴 + 纵轴参数= self._纵轴参数 + 坐标起点= 纵轴参数['个股坐标起点'] + 坐标终点= 纵轴参数['个股坐标终点'] + + # 纵轴对象.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + # 纵轴对象.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + 个股平面.set_ylim(坐标起点, 坐标终点) + + # 主坐标点 + #====================================================================================== + 主坐标值= 纵轴参数['个股主坐标值'] + yMajorLocator= ticker.FixedLocator(numpy.array(主坐标值)) + def y_major_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + + 纵轴对象.set_major_locator(yMajorLocator) + 纵轴对象.set_major_formatter(yMajorFormatter) + + for 主坐标 in 个股平面.get_yticklabels(minor=False): + 主坐标.set_fontsize(6) + # 副坐标点 + #====================================================================================== + 副坐标值= 纵轴参数['个股副坐标值'] + yMinorLocator= ticker.FixedLocator(numpy.array(副坐标值)) + def y_minor_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + + 纵轴对象.set_minor_locator(yMinorLocator) + 纵轴对象.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 个股平面.get_yticklabels(minor=True): + 副坐标.set_fontsize(5) + 副坐标.set_color('blue') + + + + def 绘制指数行情走势(self): + ''' + 画 K 线 + ''' + 指数平面= self._指数平面 + 横轴坐标序列= self._横轴坐标序列 + + 开盘序列= copy.copy(self._目标指数开盘) + 收盘序列= copy.copy(self._目标指数收盘) + 最高序列= self._目标指数最高 + 最低序列= self._目标指数最低 + + 上涨序列= self._目标指数上涨 + 下跌序列= self._目标指数下跌 + 平盘序列= self._目标指数平盘 + + # 对开收盘价进行视觉修正 + for idx, (po, pc) in enumerate(zip(开盘序列, 收盘序列)): + if po is not None and po==pc: + 调整= int(po * 0.0005) + 开盘序列[idx]= po + 调整 + 收盘序列[idx]= pc - 调整 + + 开盘阵列= numpy.array(开盘序列) + 收盘阵列= numpy.array(收盘序列) + 最高阵列= numpy.array(最高序列) + 最低阵列= numpy.array(最低序列) + + if True in 上涨序列: + 指数平面.vlines(横轴坐标序列[上涨序列], 最低阵列[上涨序列], 最高阵列[上涨序列], edgecolor='0.7', linewidth=0.6, label='_nolegend_', alpha=0.5) + 指数平面.vlines(横轴坐标序列[上涨序列], 开盘阵列[上涨序列], 收盘阵列[上涨序列], edgecolor='0.7', linewidth=3.0, label='_nolegend_', alpha=0.5) + + if True in 下跌序列: + 指数平面.vlines(横轴坐标序列[下跌序列], 最低阵列[下跌序列], 最高阵列[下跌序列], edgecolor='0.3', linewidth=0.6, label='_nolegend_', alpha=0.5) + 指数平面.vlines(横轴坐标序列[下跌序列], 开盘阵列[下跌序列], 收盘阵列[下跌序列], edgecolor='0.3', linewidth=3.0, label='_nolegend_', alpha=0.5) + + if True in 平盘序列: + 指数平面.vlines(横轴坐标序列[平盘序列], 最低阵列[平盘序列], 最高阵列[平盘序列], edgecolor='1.0', linewidth=0.6, label='_nolegend_', alpha=1.0) + 指数平面.vlines(横轴坐标序列[平盘序列], 开盘阵列[平盘序列], 收盘阵列[平盘序列], edgecolor='1.0', linewidth=3.0, label='_nolegend_', alpha=1.0) + + + + def 绘制个股行情走势(self): + ''' + 画 K 线 + ''' + 个股平面= self._个股平面 + 横轴坐标序列= self._横轴坐标序列 + + 开盘序列= copy.copy(self._目标绘图开盘) + 收盘序列= copy.copy(self._目标绘图收盘) + 最高序列= self._目标绘图最高 + 最低序列= self._目标绘图最低 + + 上涨序列= self._目标绘图上涨 + 下跌序列= self._目标绘图下跌 + 平盘序列= self._目标绘图平盘 + + # 对开收盘价进行视觉修正 + for idx, (po, pc) in enumerate(zip(开盘序列, 收盘序列)): + if po is not None and po==pc: + 调整= int(po * 0.0005) + 开盘序列[idx]= po + 调整 + 收盘序列[idx]= pc - 调整 + + 开盘阵列= numpy.array(开盘序列) + 收盘阵列= numpy.array(收盘序列) + 最高阵列= numpy.array(最高序列) + 最低阵列= numpy.array(最低序列) + + if True in 上涨序列: + 个股平面.vlines(横轴坐标序列[上涨序列], 最低阵列[上涨序列], 最高阵列[上涨序列], edgecolor='red', linewidth=0.6, label='_nolegend_', alpha=0.5) + 个股平面.vlines(横轴坐标序列[上涨序列], 开盘阵列[上涨序列], 收盘阵列[上涨序列], edgecolor='red', linewidth=3.0, label='_nolegend_', alpha=0.5) + + if True in 下跌序列: + 个股平面.vlines(横轴坐标序列[下跌序列], 最低阵列[下跌序列], 最高阵列[下跌序列], edgecolor='green', linewidth=0.6, label='_nolegend_', alpha=0.5) + 个股平面.vlines(横轴坐标序列[下跌序列], 开盘阵列[下跌序列], 收盘阵列[下跌序列], edgecolor='green', linewidth=3.0, label='_nolegend_', alpha=0.5) + + if True in 平盘序列: + 个股平面.vlines(横轴坐标序列[平盘序列], 最低阵列[平盘序列], 最高阵列[平盘序列], edgecolor='0.7', linewidth=0.6, label='_nolegend_', alpha=0.5) + 个股平面.vlines(横轴坐标序列[平盘序列], 开盘阵列[平盘序列], 收盘阵列[平盘序列], edgecolor='0.7', linewidth=3.0, label='_nolegend_', alpha=0.5) + + + + def 绘制个股开收中线走势(self): + ''' + + ''' + 个股平面= self._个股平面 + + 横轴坐标序列= self._横轴坐标序列 + 目标行情长度= self._目标行情长度 + 个股衍生日期= self._个股衍生行情['日期'] + 全局日期序列= self._全局日期序列 + 目标日期序列= self._目标日期序列 + 目标绘图中线= self._目标绘图中线 + + 目标首日= 目标日期序列[0] + 目标尾日= 目标日期序列[-1] + + 中线阈值= 30 + 长线阈值= 90 + + # 绘制中线 + 中线阵列= numpy.array(目标绘图中线) + 个股平面.plot(横轴坐标序列, 中线阵列, 'o-', color='white', linewidth=0.2, label='个股中线', \ + markersize=0.3, markeredgecolor='white', markeredgewidth=0.1, alpha=1.0) + + # 计算拐点位置及关键度 + 走势拐点= self._个股衍生行情['开收中线走势拐点'] + + 顶点序列= [None] * 目标行情长度 + 底点序列= [None] * 目标行情长度 + + for 拐点 in 走势拐点: + 日期= 个股衍生日期[拐点['偏移']] + if 日期 < 目标首日 or 日期 > 目标尾日: + continue + + 偏移= 目标日期序列.index(日期) + 横标= 全局日期序列.index(日期) + 位置= 目标绘图中线[偏移] + if 拐点['类型'] == '^': + 顶点序列[偏移]= 位置 + 纵标= 位置 * 1.01 + valign= 'bottom' + else: + 底点序列[偏移]= 位置 + 纵标= 位置 / 1.01 + valign= 'top' + + 关键度= 拐点['关键度'] + + if 关键度 < 中线阈值: + continue + + if 关键度 < 中线阈值: + fsize= 2.3 + weight= 300 + elif 关键度 < 长线阈值: + fsize= 2.7 + weight= 600 + else: + fsize= 3.3 + weight= 900 + + 注释= 个股平面.text(横标, 纵标, str(关键度), weight=weight, fontsize=fsize, verticalalignment=valign, horizontalalignment='center', color='white', alpha=0.7) + + # 绘制拐点 + 底点阵列= numpy.array(底点序列) + 顶点阵列= numpy.array(顶点序列) + + 个股平面.plot(横轴坐标序列, 底点阵列, marker=6, color='white', label='个股中线底点', \ + markersize=2.3, markeredgecolor='white', markeredgewidth=0.0, alpha=0.7) + + 个股平面.plot(横轴坐标序列, 顶点阵列, marker=7, color='white', label='个股中线顶点', \ + markersize=2.3, markeredgecolor='white', markeredgewidth=0.0, alpha=0.7) + + + + def 绘制指数均线走势(self): + ''' + + ''' + 横轴坐标序列= self._横轴坐标序列 + 指数平面= self._指数平面 + 均线字典= self._目标指数均线 + + widthw= 0.2 + widthn= 0.1 + mksize= 0.3 + mkwidth= 0.1 + alpha= 1.0 + + for n, seq in 均线字典.items(): + rarray= numpy.array( seq ) + color= 'white' if n < 10 else \ + 'yellow' if n < 30 else \ + 'cyan' if n < 60 else \ + 'magenta' + + 指数平面.plot(横轴坐标序列, rarray, 'o-', color=color, linewidth=widthw, label='指数均线_'+str(n), \ + markersize=mksize, markeredgecolor=color, markeredgewidth=mkwidth, alpha=alpha) + + + + def 绘制个股均线走势(self): + ''' + + ''' + 横轴坐标序列= self._横轴坐标序列 + 个股平面= self._个股平面 + 均线字典= self._目标绘图均线 + + widthw= 0.2 + widthn= 0.1 + mksize= 0.3 + mkwidth= 0.1 + alpha= 0.7 + + for n, seq in 均线字典.items(): + rarray= numpy.array( seq ) + color= 'white' if n < 10 else \ + 'yellow' if n < 30 else \ + 'cyan' if n < 60 else \ + 'magenta' + + 个股平面.plot(横轴坐标序列, rarray, 'o-', color=color, linewidth=widthw, label='个股均线_'+str(n), \ + markersize=mksize, markeredgecolor=color, markeredgewidth=mkwidth, alpha=alpha) + + + + def 绘制复权提示(self): + ''' + + ''' + 个股平面= self._个股平面 + 纵轴参数= self._纵轴参数 + 目标日期序列= self._目标日期序列 + 全局日期序列= self._全局日期序列 + + 纵标起点= 纵轴参数['个股坐标起点'] + 纵标终点= 纵轴参数['个股坐标终点'] + 目标首日= 目标日期序列[0] + 目标尾日= 目标日期序列[-1] + + 复权记录= self._复权记录 + + fsize= 5.0 + ycoord= 纵标终点/1.1 + alpha= 0.7 + + for 日期, 比率 in 复权记录: + if 日期<目标首日 or 日期>目标尾日: continue + 横标= 全局日期序列.index(日期) + 个股平面.plot([横标, 横标], [纵标起点, 纵标终点], '-', color='purple', linewidth=0.1) + label= 个股平面.text( \ + 横标, ycoord, \ + '复权 ' + str(比率) + '\n' + 日期, \ + fontproperties=__font_properties__, \ + horizontalalignment='left', \ + verticalalignment='top', \ + color='purple', \ + alpha=alpha + ) + label.set_fontsize(fsize) + + + + def 绘制流通股本变更提示(self): + ''' + 注意两个问题: + 1. 流通股本变更提示中的日期可能不是交易日期 + 2. 流通股本变更提示涵盖个股的所有历史,有些内容可能在绘图目标区间以外 + ''' + 个股平面= self._个股平面 + 纵轴参数= self._纵轴参数 + 公司信息= self._公司信息 + 目标日期序列= self._目标日期序列 + 全局日期序列= self._全局日期序列 + + 纵标起点= 纵轴参数['个股坐标起点'] + 纵标终点= 纵轴参数['个股坐标终点'] + 目标首日= 目标日期序列[0] + 目标尾日= 目标日期序列[-1] + + 变更记录= [记录 for 记录 in 公司信息['流通股变更'] if 记录['变更日期']>=目标首日 and 记录['变更日期']<=目标尾日] + + fsize= 5.0 + ycoord= 纵标终点/1.05 + alpha= 0.7 + + for 记录 in 变更记录: + 变更日期= 记录['变更日期'] + 比率= 记录['变更比'] + 调整日期= [ds for ds in 目标日期序列 if ds >= 变更日期][0] + 横标= 全局日期序列.index(调整日期) + 个股平面.plot([横标, 横标], [纵标起点, 纵标终点], '-', color='yellow', linewidth=0.1) + label= 个股平面.text( \ + 横标, ycoord, \ + '流通股 ' + str(比率) + '\n' + 变更日期, \ + fontproperties=__font_properties__, \ + horizontalalignment='left', \ + verticalalignment='top', \ + color='yellow', \ + alpha=alpha + ) + label.set_fontsize(fsize) + + + + def 绘制首尾股本信息(self): + ''' + + ''' + def 寻找纵轴最大空隙(偏移): + ''' + 找出 X 轴某个位置图中最大的空隙 + ''' + 指数基点= 纵轴参数['指数基点'] + 指数目标= 全局指数开盘[偏移] + + 个股基点= 纵轴参数['个股基点'] + 个股目标= 全局绘图开盘[偏移] + + if 个股目标 is None: + try: + 个股目标= [nr for nr in 全局绘图开盘[:偏移] if nr is not None][-1] + except IndexError: + 个股目标= [nr for nr in 全局绘图开盘[偏移:] if nr is not None][0] + 调整目标= 指数目标 * 个股基点 / 指数基点 + + 比较列表= [坐标起点, 个股目标, 调整目标, 坐标终点] + 比较列表.sort() + 差值, 低点, 高点= max( [ (math.log(n2/n1, 对数坐标底数), n1, n2) for n1, n2 in zip(比较列表[:-1], 比较列表[1:]) ] ) + + return (低点*高点)**0.5 # 相乘再开平方,在 log 坐标系里看起来就是在中间位置。 + + + + def 整数分位表示(整数): + ''' + 123456789 --> '1,2345,6789' + ''' + if type(整数) is not int: return str(整数) + + if 整数 == 0: return '0' + + if 整数 < 0: + 整数= -整数 + 为负= True + else: + 为负= False + + intstr= str(整数) + intstr= '0'*((4-len(intstr)%4)%4) + intstr + intlist= [intstr[i:i+4] for i in range(0, len(intstr), 4)] + + intlist[0]= intlist[0].lstrip('0') + + return ('-' + ','.join(intlist)) if 为负 else ','.join(intlist) + + 纵轴参数= self._纵轴参数 + 公司信息= self._公司信息 + 对数坐标底数= self._对数坐标底数 + + 全局指数开盘= self._全局指数日线['开盘'] + 全局绘图开盘= self._全局绘图日线['开盘'] + 目标日期序列= self._目标日期序列 + 目标起始偏移= self._目标起始偏移 + 目标结束偏移= self._目标结束偏移 + + 坐标起点= 纵轴参数['个股坐标起点'] + 坐标终点= 纵轴参数['个股坐标终点'] + 变更记录= 公司信息['股本变更记录'] + + 个股平面= self._个股平面 + + fsize= 5.0 + + # 首日总股本与流通股信息 + #==================================================================================================================================== + 目标首日= 目标日期序列[0] + 前期记录= [rec for rec in 变更记录 if rec['变更日期'] <= 目标首日] + + if 前期记录: + 总股本= 整数分位表示(前期记录[-1]['总股本']) + 流通股= 整数分位表示(前期记录[-1]['流通股']) + else: + 总股本= 'None' + 流通股= 'None' + + 显示字串= '总股本: ' + 总股本 + '\n' + '流通股: ' + 流通股 + + label= 个股平面.text(目标起始偏移, 寻找纵轴最大空隙(偏移=目标起始偏移), 显示字串, fontproperties=__font_properties__, color='0.7') + label.set_fontsize(fsize) + + # 尾日总股本与流通股信息 + #==================================================================================================================================== + 目标尾日= 目标日期序列[-1] + 前期记录= [rec for rec in 变更记录 if rec['变更日期'] <= 目标尾日] + + if 前期记录: + 总股本= 整数分位表示(前期记录[-1]['总股本']) + 流通股= 整数分位表示(前期记录[-1]['流通股']) + else: + 总股本= 'None' + 流通股= 'None' + + 显示字串= '总股本: ' + 总股本 + '\n' + '流通股: ' + 流通股 + + label= 个股平面.text(目标结束偏移, 寻找纵轴最大空隙(偏移=目标结束偏移), 显示字串, fontproperties=__font_properties__, horizontalalignment='right', color='0.7') + label.set_fontsize(fsize) + + + + def 绘制日期提示(self): + ''' + + ''' + 个股平面= self._个股平面 + 横轴参数= self._横轴参数 + 纵轴参数= self._纵轴参数 + 全局日期序列= self._全局日期序列 + + 每月首日偏移= 横轴参数['每月首日偏移'] + 每周首日偏移= 横轴参数['每周首日偏移'] + + 坐标起点= 纵轴参数['个股坐标起点'] + 坐标终点= 纵轴参数['个股坐标终点'] + + # 每月第一个交易日 + for iy in [坐标起点*1.2, 坐标终点/1.12]: + for ix in 每月首日偏移: + newlab= 个股平面.text(ix-1, iy, 全局日期序列[ix]) + newlab.set_font_properties(__font_properties__) + newlab.set_color('0.3') + newlab.set_fontsize(4) + newlab.set_rotation('vertical') + # newlab.set_horizontalalignment('left') + # newlab.set_verticalalignment('center') + # newlab.set_verticalalignment('bottom') + newlab.set_zorder(0) # XXX: 放在底层 + + # 每周第一个交易日,根据这个可以推算出全部确切的日期。 + for iy in [坐标起点*1.08, 坐标终点/1.03]: + for ix in 每周首日偏移: + newlab= 个股平面.text(ix-0.8, iy, 全局日期序列[ix]) + newlab.set_font_properties(__font_properties__) + newlab.set_color('0.3') + newlab.set_fontsize(3) + newlab.set_rotation('vertical') + # newlab.set_horizontalalignment('left') + # newlab.set_verticalalignment('top') # 不行 + # newlab.set_verticalalignment('center') + # newlab.set_verticalalignment('bottom') + newlab.set_zorder(0) # XXX: 放在底层 + + + + def 绘制价格提示(self): + ''' + + ''' + + 个股平面= self._个股平面 + 横轴参数= self._横轴参数 + 纵轴参数= self._纵轴参数 + + 每月首日偏移= 横轴参数['每月首日偏移'] + + 个股主坐标值= 纵轴参数['个股主坐标值'] + 个股副坐标值= 纵轴参数['个股副坐标值'] + 指数主坐标值= 纵轴参数['指数主坐标值'] + 指数副坐标值= 纵轴参数['指数副坐标值'] + + for iy, iy2 in zip(sorted(个股主坐标值[:-1] + 个股副坐标值[1:-1]), sorted(指数主坐标值[:-1] + 指数副坐标值[1:-1])): + for ix in 每月首日偏移[1:-1:3]: + newlab= 个股平面.text( ix+6, iy*1.001, str(iy/1000.0) + ' / ' + str(round(iy2/1000.0, 3)) ) + newlab.set_font_properties(__font_properties__) + newlab.set_color('0.3') + newlab.set_fontsize(3) + newlab.set_zorder(0) # XXX: 放在底层 + + + + def 绘制常用辅助标记(self): + ''' + + ''' + + def 绘制指数均线拐点(): + ''' + + ''' + 均线走势标记= 指数衍生行情['均线走势标记集'] + + for n in 指数衍生均线.keys(): + 均线序列= 指数衍生均线[n] + 走势标记= 均线走势标记[n] + + 拐点序列= [ (目标日期序列.index(日期), 数值, 标记) for 日期, 数值, 标记 in zip(指数衍生日期, 均线序列, 走势标记) \ + if 日期>=目标首日 and 日期<=目标尾日 and 标记 in ('v', '^') ] + + 底点序列= [None] * 目标行情长度 + 顶点序列= [None] * 目标行情长度 + for 偏移, 数值, 标记 in 拐点序列: + if 标记=='v': + 底点序列[偏移]= 数值 + elif 标记=='^': + 顶点序列[偏移]= 数值 + + 底点阵列= numpy.array(底点序列) + 顶点阵列= numpy.array(顶点序列) + + color= 'white' if n < 10 else \ + 'yellow' if n < 30 else \ + 'cyan' if n < 60 else \ + 'magenta' + + 指数平面.plot(横轴坐标序列, 底点阵列, marker=6, color=color, label='指数均线底点_'+str(n), \ + markersize=2.3, markeredgecolor=color, markeredgewidth=0.0, alpha=0.5) + 指数平面.plot(横轴坐标序列, 顶点阵列, marker=7, color=color, label='指数均线顶点_'+str(n), \ + markersize=2.3, markeredgecolor=color, markeredgewidth=0.0, alpha=0.5) + + + + def 绘制三日线拐点关键度(): + ''' + + ''' + 三日线拐点= 个股衍生行情['三日线走势拐点'] + 中线阈值= 30 + 长线阈值= 90 + + for 拐点 in 三日线拐点: + 关键度= 拐点['关键度'] + if 关键度 < 中线阈值: + continue + + 偏移= 拐点['偏移'] + 日期= 个股衍生日期[偏移] + if 日期 < 目标首日 or 日期 > 目标尾日: + continue + + 类型= 拐点['类型'] + 位置= 三日线[偏移] if 复权绘图 else 三日线[目标日期序列.index(日期)] + + 横标= 全局日期序列.index(日期) + 纵标= 位置*1.01 if 类型=='^' else 位置/1.01 + + # 添加文字注释 + 文字= str(关键度) + # 文字= str(关键度) + '(' + str(偏移) + ')' + valign= 'bottom' if 类型=='^' else 'top' + if 关键度 < 中线阈值: + fsize= 2.3 + weight= 300 + elif 关键度 < 长线阈值: + fsize= 2.7 + weight= 600 + else: + fsize= 3.3 + weight= 900 + + 注释= 个股平面.text(横标, 纵标, 文字, weight=weight, verticalalignment=valign, horizontalalignment='left', color='white', alpha=0.7) + 注释.set_fontsize(fsize) + + + + def 绘制个股均线拐点(): + ''' + + ''' + 均线走势标记= 个股衍生行情['均线走势标记集'] + + for n in 个股衍生均线.keys(): + 走势标记= 均线走势标记[n] + + if 复权绘图: + 均线序列= 个股衍生均线[n] # 使用复权均线 + 拐点序列= [ (目标日期序列.index(日期), 数值, 标记) for 日期, 数值, 标记 in zip(个股衍生日期, 均线序列, 走势标记) \ + if 日期>=目标首日 and 日期<=目标尾日 and 标记 in ('v', '^') ] + 底点序列= [None] * 目标行情长度 + 顶点序列= [None] * 目标行情长度 + for 偏移, 数值, 标记 in 拐点序列: + if 标记=='v': + 底点序列[偏移]= 数值 + elif 标记=='^': + 顶点序列[偏移]= 数值 + else: + 均线序列= 目标绘图均线[n] # 使用未复权的均线 + 拐点序列= [ (目标日期序列.index(日期), 日期, 标记) for 日期, 标记 in zip(个股衍生日期, 走势标记) \ + if 日期>=目标首日 and 日期<=目标尾日 and 标记 in ('v', '^') ] + 底点序列= [None] * 目标行情长度 + 顶点序列= [None] * 目标行情长度 + for 偏移, 日期, 标记 in 拐点序列: + if 标记=='v': + 底点序列[偏移]= 均线序列[目标日期序列.index(日期)] + elif 标记=='^': + 顶点序列[偏移]= 均线序列[目标日期序列.index(日期)] + + 底点阵列= numpy.array(底点序列) + 顶点阵列= numpy.array(顶点序列) + + color= 'white' if n < 10 else \ + 'yellow' if n < 30 else \ + 'cyan' if n < 60 else \ + 'magenta' + + 个股平面.plot(横轴坐标序列, 底点阵列, marker=6, color=color, label='个股均线底点_'+str(n), \ + markersize=2.3, markeredgecolor=color, markeredgewidth=0.0, alpha=0.7) + + 个股平面.plot(横轴坐标序列, 顶点阵列, marker=7, color=color, label='个股均线顶点_'+str(n), \ + markersize=2.3, markeredgecolor=color, markeredgewidth=0.0, alpha=0.7) + + + + def 绘制三日线波段(): + ''' + + ''' + 阶梯记录= 个股衍生行情['三日线走势阶梯'] + + for 记录 in 阶梯记录: + 升幅= 记录['升幅'] + 升跨= 记录['升跨'] + 降幅= 记录['降幅'] + 降跨= 记录['降跨'] + + 前底偏移= 记录['前底偏移'] + 顶点偏移= 记录['顶点偏移'] + 后底偏移= 顶点偏移 + 降跨 + 前底日期= 个股衍生日期[前底偏移] + 顶点日期= 个股衍生日期[顶点偏移] + 后底日期= 个股衍生日期[后底偏移] + + # 上升波段 + if 前底日期>=目标首日 and 顶点日期<=目标尾日 and 升幅>=900.0: + 横标= 全局日期序列.index(前底日期) + 0.5 + 纵标= 三日线[前底偏移] if 复权绘图 else 三日线[目标日期序列.index(前底日期)] + 宽度= 全局日期序列.index(顶点日期) - 横标 + 0.5 + 高度= 三日线[顶点偏移]-纵标 if 复权绘图 else 三日线[目标日期序列.index(顶点日期)]-纵标 + rectobj= patches.Rectangle((横标, 纵标), 宽度, 高度, fill=True, edgecolor=None, facecolor='magenta', linewidth=0.3, alpha=0.1) + rectobj.set_zorder(-1) # 放在底层 + 个股平面.add_patch(rectobj) + + 文字 = str(round(升幅, 2)) + 文字 += '\n' + str(round(升幅/升跨, 2)) + 文字 += '\n' + str(升跨) + 注释= 个股平面.text(横标, 纵标, 文字, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left', color=__color_gray70__) + 注释.set_fontsize(2.5) + + # 下降波段 + if 顶点日期>=目标首日 and 后底日期<=目标尾日 and 降幅<=-900.0: + 横标= 全局日期序列.index(顶点日期) + 0.5 + 纵标= 三日线[后底偏移] if 复权绘图 else 三日线[目标日期序列.index(后底日期)] + 宽度= 全局日期序列.index(后底日期) - 横标 + 0.5 + 高度= 三日线[顶点偏移]-纵标 if 复权绘图 else 三日线[目标日期序列.index(顶点日期)]-纵标 + rectobj= patches.Rectangle((横标, 纵标), 宽度, 高度, fill=True, edgecolor=None, facecolor='cyan', linewidth=0.3, alpha=0.1) + rectobj.set_zorder(-1) # 放在底层 + 个股平面.add_patch(rectobj) + + 文字 = str(round(降幅, 2)) + 文字 += '\n' + str(round(降幅/降跨, 2)) + 文字 += '\n' + str(降跨) + 注释= 个股平面.text(横标, 纵标, 文字, fontproperties=__font_properties__, verticalalignment='top', horizontalalignment='left', color=__color_gray70__) + 注释.set_fontsize(2.5) + + + + 横轴坐标序列= self._横轴坐标序列 + 目标行情长度= self._目标行情长度 + 目标日期序列= self._目标日期序列 + 全局日期序列= self._全局日期序列 + 目标首日= 目标日期序列[0] + 目标尾日= 目标日期序列[-1] + + 指数衍生行情= self._指数衍生行情 + 个股衍生行情= self._个股衍生行情 + + 指数衍生日期= 指数衍生行情['日期'] + 个股衍生日期= 个股衍生行情['日期'] + + 复权绘图= self._复权绘图 + + # 三日线= 个股衍生均线[3] if 复权绘图 else 目标绘图均线[3] + + 指数平面= self._指数平面 + 个股平面= self._个股平面 + + if self._绘制指数均线: + 指数衍生均线= 指数衍生行情['均线集'] + 绘制指数均线拐点() + if self._绘制个股均线: + 目标绘图均线= self._目标绘图均线 + 个股衍生均线= 个股衍生行情['均线集'] + 绘制个股均线拐点() + + # 绘制三日线波段() + # 绘制三日线拐点关键度() + + + + def 绘图(self): + ''' + + ''' + self.绘制指数行情走势() + self.绘制个股行情走势() + if self._绘制指数均线: + self.绘制指数均线走势() + if self._绘制个股均线: + self.绘制个股均线走势() + + self.绘制个股开收中线走势() + + self.绘制流通股本变更提示() + self.绘制复权提示() + self.绘制首尾股本信息() + self.绘制日期提示() + self.绘制价格提示() + + self.绘制常用辅助标记() + + if self._定制绘图函数: + 用户参数= self._定制绘图参数 + + 用户参数.update( + { '日线子图': self, + 'ilogger': self._ilogger, + 'Public': Public, + } + ) + + self._定制绘图函数(**用户参数) + + + + # 日线活跃度.绘图(日线子图=self, 环境参数=环境参数) + # 短线组合形态.绘图(日线子图=self, 环境参数=环境参数) + # 三日线涨幅差值.绘图(日线子图=self, 环境参数=环境参数) + # 日线情势判断.绘图(日线子图=self, 环境参数=环境参数) + + + diff --git a/vn.training/SubPlot/日线换手子图.py b/vn.training/SubPlot/日线换手子图.py new file mode 100644 index 00000000..bd3cc01a --- /dev/null +++ b/vn.training/SubPlot/日线换手子图.py @@ -0,0 +1,488 @@ +# -*- coding: utf-8 -*- + + + +import itertools +import numpy + +import matplotlib.spines as spines +import matplotlib.ticker as ticker +import matplotlib.font_manager as font_manager + + + +import Public.Public as Public + + + +__color_gold__= '#FDDB05' +__font_properties__= font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc') + + + +__横轴倍率__= 10.0 / 230.0 +__纵轴倍率__= 0.3 + + + + + +class 日线换手子图: + + def __init__(self, parent, 绘图数据): + ''' + + ''' + self._parent= parent + self._ilogger= Public.IndentLogger(logger=parent._ilogger._logger, indent=parent._ilogger._indent+1) + + # 原始数据 + self._任务参数= 绘图数据['任务参数'] + + # 行情数据 + 日线价格子图= parent._日线价格子图 + + 日线行情= 日线价格子图.返回目标日线数据() + + self._目标指数日线= 日线行情['目标指数日线'] + self._目标绘图日线= 日线行情['目标绘图日线'] + + self._目标指数换手= self._目标指数日线['换手率'] + self._目标绘图换手= self._目标绘图日线['换手率'] + + # 行情衍生数据 + 附加行情= 日线价格子图.返回行情附加数据() + + self._横轴坐标序列= 附加行情['横轴坐标序列'] + self._目标行情长度= 附加行情['目标行情长度'] + + self._目标指数上涨= 附加行情['目标指数上涨'] + self._目标指数下跌= 附加行情['目标指数下跌'] + self._目标指数平盘= 附加行情['目标指数平盘'] + + self._目标绘图上涨= 附加行情['目标绘图上涨'] + self._目标绘图下跌= 附加行情['目标绘图下跌'] + self._目标绘图平盘= 附加行情['目标绘图平盘'] + + # 平面对象 + self._指数平面= None + self._指数横轴= None + self._指数纵轴= None + + self._个股平面= None + self._个股横轴= None + self._个股纵轴= None + + # 横轴参数、纵轴参数 + self._横轴参数= 日线价格子图.返回横轴参数() + self._纵轴参数= self.计算纵轴参数() + + + + def 计算纵轴参数(self): + ''' + + ''' + + def _compute_torange(maxto, tostep): + return int(round((maxto + tostep/2.0 - 1) / float(tostep), 0)) + + def _compute_tostep(maxto): + ''' + maxto 是 换手率 最大值。返回每格单位(最小 500, 代表 0.5%)以及格数 + ''' + for i in range(9): + if maxto > (4 * 500 * (2**i)): # 换手率最大是 100000, 代表 100% + continue + else: + tostep= 500 * (2**i) + torange= _compute_torange(maxto, tostep) + break + return (tostep, torange) + + 指数换手= self._目标指数换手 + 个股换手= self._目标绘图换手 + + 个股最大= max( [nr for nr in 个股换手 if nr is not None] ) + 个股步进, \ + 个股格数= _compute_tostep(个股最大) + + 指数最大= max(指数换手) + 指数步进, \ + 指数格数= _compute_tostep(指数最大) + + 纵轴格数= max(个股格数, 指数格数) + 纵轴尺寸= 纵轴格数 * 1.0 + + 指数坐标终点= 指数步进 * 纵轴格数 + 个股坐标终点= 个股步进 * 纵轴格数 + + 指数主坐标值= [指数步进*i for i in range(纵轴格数)] + 指数副坐标值= list( itertools.chain.from_iterable( mi for mi in [[ma + (指数步进/4.0)*i for i in range(1, 4)] for ma in 指数主坐标值] ) ) + + 个股主坐标值= [个股步进*i for i in range(纵轴格数)] + 个股副坐标值= list( itertools.chain.from_iterable( mi for mi in [[ma + (个股步进/4.0)*i for i in range(1, 4)] for ma in 个股主坐标值] ) ) + + 纵轴参数= { + '指数步进': 指数步进, + '个股步进': 个股步进, + '纵轴格数': 纵轴格数, + '纵轴尺寸': 纵轴尺寸, + '纵轴高度': 纵轴尺寸 * __纵轴倍率__, + '指数坐标终点': 指数坐标终点, + '个股坐标终点': 个股坐标终点, + '指数主坐标值': 指数主坐标值, + '指数副坐标值': 指数副坐标值, + '个股主坐标值': 个股主坐标值, + '个股副坐标值': 个股副坐标值, + } + + return 纵轴参数 + + + + def 返回纵轴高度(self): + ''' + + ''' + return self._纵轴参数['纵轴高度'] + + + + def 平面初始化(self, 图片对象, 子图偏移, 全图大小, sharex): + ''' + + ''' + # 计算 布局参数 + #================================================================================================================================================== + 子图横移, \ + 子图纵移= 子图偏移 + + 全图宽度, \ + 全图高度= 全图大小 + + 本图宽度= self._横轴参数['横轴宽度'] + 本图高度= self._纵轴参数['纵轴高度'] + + 布局参数= ( 子图横移/全图宽度, 子图纵移/全图高度, 本图宽度/全图宽度, 本图高度/全图高度 ) + + # 指数部分 + #================================================================================================================================================== + 指数平面= 图片对象.add_axes(布局参数, axis_bgcolor='black', sharex=sharex) + 指数平面.set_axisbelow(True) # 网格线放在底层 + + for 方位, 边框 in 指数平面.spines.items(): # 方位: 'left' | 'right' | 'top' | 'bottom' + 边框.set_color(__color_gold__) + + 指数横轴= 指数平面.get_xaxis() + 指数纵轴= 指数平面.get_yaxis() + + self._指数平面= 指数平面 + self._指数横轴= 指数横轴 + self._指数纵轴= 指数纵轴 + + self.设置指数横轴() + self.设置指数纵轴() + + # 个股部分 + #================================================================================================================================================== + 个股平面= 指数平面.twinx() + 个股平面.set_axis_bgcolor('black') + 个股平面.set_axisbelow(True) # 网格线放在底层 + + # for 方位, 边框 in 个股平面.spines.items(): # 方位: 'left' | 'right' | 'top' | 'bottom' + # 边框.set_color(__color_gold__) + + 个股横轴= 个股平面.get_xaxis() + 个股纵轴= 个股平面.get_yaxis() + + self._个股平面= 个股平面 + self._个股横轴= 个股横轴 + self._个股纵轴= 个股纵轴 + + self.设置个股横轴() + self.设置个股纵轴() + + + + def 设置指数横轴(self): + ''' + + ''' + 任务参数= self._任务参数 + + 指数平面= self._指数平面 + 指数横轴= self._指数横轴 + + 横轴参数= self._横轴参数 + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + 指数横轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 指数横轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 指数平面.set_xlim(坐标起点, 坐标终点) + + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + xMajorFormatter= 横轴参数['xMajorFormatter'] + xMinorFormatter= 横轴参数['xMinorFormatter'] + + 指数横轴.set_major_locator(xMajorLocator) + 指数横轴.set_minor_locator(xMinorLocator) + + 指数横轴.set_major_formatter(xMajorFormatter) + 指数横轴.set_minor_formatter(xMinorFormatter) + + if 任务参数['绘制实盘']: + # 设为不可见 + for 主坐标 in axes.get_xticklabels(minor=False): + 主坐标.set_visible(False) + + for 副坐标 in axes.get_xticklabels(minor=True): + 副坐标.set_visible(False) + else: + for 主坐标 in 指数平面.get_xticklabels(minor=False): + 主坐标.set_fontsize(4) + 主坐标.set_horizontalalignment('right') + 主坐标.set_rotation('45') + + for 副坐标 in 指数平面.get_xticklabels(minor=True): + 副坐标.set_fontsize(4) + 副坐标.set_color('blue') + 副坐标.set_horizontalalignment('right') + 副坐标.set_rotation('45') + + + def 设置指数纵轴(self): + ''' + + ''' + 平面对象= self._指数平面 + 指数纵轴= self._指数纵轴 + 纵轴参数= self._纵轴参数 + + 主坐标值= 纵轴参数['指数主坐标值'] + 副坐标值= 纵轴参数['指数副坐标值'] + 坐标终点= 纵轴参数['指数坐标终点'] + + 指数纵轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + 指数纵轴.grid(True, 'minor', color='0.3', linestyle='solid', linewidth=0.1) + + 平面对象.set_ylim(0, 坐标终点) + + yMajorLocator= ticker.FixedLocator(numpy.array(主坐标值)) + def y_major_formatter(num, pos=None): + return str(round(num/1000.0, 2)) + '%' + yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + + 指数纵轴.set_major_locator(yMajorLocator) + 指数纵轴.set_major_formatter(yMajorFormatter) + + for 主坐标 in 平面对象.get_yticklabels(minor=False): + 主坐标.set_font_properties(__font_properties__) + 主坐标.set_fontsize(5) # 这个必须放在前一句后面,否则作用会被覆盖 + + + yMinorLocator= ticker.FixedLocator(numpy.array(副坐标值)) + def y_minor_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + '%' + yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + + 指数纵轴.set_minor_locator(yMinorLocator) + 指数纵轴.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 平面对象.get_yticklabels(minor=True): + 副坐标.set_font_properties(__font_properties__) + 副坐标.set_fontsize(4) # 这个必须放在前一句后面,否则作用会被覆盖 + + + + def 设置个股横轴(self): + ''' + + ''' + 个股平面= self._个股平面 + 个股横轴= self._个股横轴 + + 横轴参数= self._横轴参数 + 坐标起点= 横轴参数['坐标起点'] + 坐标终点= 横轴参数['坐标终点'] + + # 个股横轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + # 个股横轴.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.15) + + 个股平面.set_xlim(坐标起点, 坐标终点) + + xMajorLocator= 横轴参数['xMajorLocator'] + xMinorLocator= 横轴参数['xMinorLocator'] + + 个股横轴.set_major_locator(xMajorLocator) + 个股横轴.set_minor_locator(xMinorLocator) + + for 主坐标 in 个股平面.get_xticklabels(minor=False): + 主坐标.set_visible(False) + + for 副坐标 in 个股平面.get_xticklabels(minor=True): + 副坐标.set_visible(False) + + + + def 设置个股纵轴(self): + ''' + + ''' + + 平面对象= self._个股平面 + 个股纵轴= self._个股纵轴 + 纵轴参数= self._纵轴参数 + + 主坐标值= 纵轴参数['个股主坐标值'] + 副坐标值= 纵轴参数['个股副坐标值'] + 坐标终点= 纵轴参数['个股坐标终点'] + + # 坐标线 + # 个股纵轴.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) + # 个股纵轴.grid(True, 'minor', color='0.3', linestyle='solid', linewidth=0.1) + + # 设置坐标范围 + 平面对象.set_ylim(0, 坐标终点) + + # 主坐标点 + yMajorLocator= ticker.FixedLocator(numpy.array(主坐标值)) + def y_major_formatter(num, pos=None): + return str(round(num/1000.0, 2)) + '%' + yMajorFormatter= ticker.FuncFormatter(y_major_formatter) + + 个股纵轴.set_major_locator(yMajorLocator) + 个股纵轴.set_major_formatter(yMajorFormatter) + + for 主坐标 in 平面对象.get_yticklabels(minor=False): + 主坐标.set_font_properties(__font_properties__) + 主坐标.set_fontsize(5) # 这个必须放在前一句后面,否则作用会被覆盖 + + # 副坐标点 + yMinorLocator= ticker.FixedLocator(numpy.array(副坐标值)) + def y_minor_formatter(num, pos=None): + return str(round(num/1000.0, 3)) + '%' + yMinorFormatter= ticker.FuncFormatter(y_minor_formatter) + + 个股纵轴.set_minor_locator(yMinorLocator) + 个股纵轴.set_minor_formatter(yMinorFormatter) + + for 副坐标 in 平面对象.get_yticklabels(minor=True): + 副坐标.set_font_properties(__font_properties__) + 副坐标.set_fontsize(4) # 这个必须放在前一句后面,否则作用会被覆盖 + + + + def 绘制指数换手(self): + ''' + + ''' + 横轴坐标序列= self._横轴坐标序列 + 平面对象= self._指数平面 + + 行情长度= self._目标行情长度 + 上涨序列= self._目标指数上涨 + 下跌序列= self._目标指数下跌 + 平盘序列= self._目标指数平盘 + 换手序列= self._目标指数换手 + + 换手阵列= numpy.array(换手序列) + 起点阵列= numpy.zeros(行情长度) + + lwidth, alpha= (0.7, 0.5) + + if True in 上涨序列: + 平面对象.vlines(横轴坐标序列[上涨序列], 起点阵列[上涨序列], 换手阵列[上涨序列], edgecolor='0.7', linewidth=lwidth, label='_nolegend_', alpha=alpha) + if True in 下跌序列: + 平面对象.vlines(横轴坐标序列[下跌序列], 起点阵列[下跌序列], 换手阵列[下跌序列], edgecolor='0.3', linewidth=lwidth, label='_nolegend_', alpha=alpha) + if True in 平盘序列: + 平面对象.vlines(横轴坐标序列[平盘序列], 起点阵列[平盘序列], 换手阵列[平盘序列], edgecolor='0.7', linewidth=lwidth, label='_nolegend_', alpha=1.0) + + + + def 绘制个股换手(self): + ''' + + ''' + 横轴坐标序列= self._横轴坐标序列 + 平面对象= self._个股平面 + + 行情长度= self._目标行情长度 + 上涨序列= self._目标绘图上涨 + 下跌序列= self._目标绘图下跌 + 平盘序列= self._目标绘图平盘 + 换手序列= self._目标绘图换手 + + 横轴参数= self._横轴参数 + 横标起点= 横轴参数['坐标起点'] + 横标终点= 横轴参数['坐标终点'] + 每月首日偏移= 横轴参数['每月首日偏移'] + + 换手阵列= numpy.array(换手序列) + 起点阵列= numpy.zeros(行情长度) + + lwidth= 3.0 + + if True in 上涨序列: + 平面对象.vlines(横轴坐标序列[上涨序列], 起点阵列[上涨序列], 换手阵列[上涨序列], edgecolor='red', linewidth=lwidth, label='_nolegend_', alpha=0.5) + if True in 下跌序列: + 平面对象.vlines(横轴坐标序列[下跌序列], 起点阵列[下跌序列], 换手阵列[下跌序列], edgecolor='green', linewidth=lwidth, label='_nolegend_', alpha=0.5) + if True in 平盘序列: + 平面对象.vlines(横轴坐标序列[平盘序列], 起点阵列[平盘序列], 换手阵列[平盘序列], edgecolor='0.7', linewidth=lwidth, label='_nolegend_', alpha=1.0) + + # 绘制 平均换手数值(直线) + 个股换手= [nr for nr in 换手序列 if nr is not None] + 平均换手= sum(个股换手) / float(len(个股换手)) + 平面对象.plot([横标起点, 横标终点], [平均换手, 平均换手], '-', color='yellow', linewidth=0.2, alpha=0.7) + + # 平均换手率数值在图中间的显示 + for ix in 每月首日偏移[2:-1:3]: + newlab= 平面对象.text(ix+8, 平均换手, str(round(平均换手/1000.0, 2)) + '%') + newlab.set_font_properties(__font_properties__) + newlab.set_color('yellow') + newlab.set_fontsize(3) + + + + def 绘制换手提示(self): + ''' + + ''' + def formatter(nr): + return str(round(nr/1000.0, 2)) + '%' + + 平面对象= self._指数平面 + 纵轴参数= self._纵轴参数 + + 指数步进= 纵轴参数['指数步进'] + 个股步进= 纵轴参数['个股步进'] + 纵轴格数= 纵轴参数['纵轴格数'] + + shift= 7 + 横轴参数= self._横轴参数 + 每月首日偏移= 横轴参数['每月首日偏移'] + for iy, iy2 in zip( range(int(指数步进/2.0), 指数步进*纵轴格数, int(指数步进/2.0)), range(int(个股步进/2.0), 个股步进*纵轴格数, int(个股步进/2.0)) ): + for ix in 每月首日偏移[1:-1:3]: + newlab= 平面对象.text(ix+shift, iy, formatter(iy2) + ' / ' + formatter(iy)) + newlab.set_font_properties(__font_properties__) + newlab.set_color('0.3') + newlab.set_fontsize(3) + newlab.set_zorder(0) # XXX: 放在底层 + + + + def 绘图(self): + ''' + + ''' + self.绘制指数换手() + self.绘制个股换手() + self.绘制换手提示() + + + + + diff --git a/vn.training/display_bar.py b/vn.training/display_bar.py index d773c85f..ad6f4cb3 100644 --- a/vn.training/display_bar.py +++ b/vn.training/display_bar.py @@ -13,32 +13,49 @@ try: symbol = 'a' - #执行脚本,返回记录数 - count = cur.execute('select open,high,low, close,datetime from TB_Bar where Id=\'20151019-161753\'') - print 'there has %s rows record' % count + backtestingId = '20151021-170626' + #执行脚本,返回记录数 + count = cur.execute('select open,high,low, close,datetime from TB_Bar where Id=\'{0}\''.format(backtestingId)) + print 'there has %s rows bar record' % count results = cur.fetchall() - - t = [] - + t1 = [] o = [] - i = 0 for r in results: i = i+1 - t.append(i) + t1.append(i) # t.append(r['datetime']) o.append(r['open']) + count = cur.execute('select fastEMA,slowEMA,datetime from TB_Ema where Id=\'{0}\''.format(backtestingId)) + print 'there has %s rows ema record' % count + + results = cur.fetchall() + + fastEma = [] + slowEma = [] + t2 = [] + i = 0 + + for r in results: + i = i+1 + t2.append(i) + + fastEma.append(r['fastEMA']) + slowEma.append(r['slowEMA']) + #关闭指针,关闭连接 cur.close() conn.close() - plt.plot(t,o,label='data') + plt.plot(t1,o,label='data') + plt.plot(t2,fastEma,label='fastEma') + plt.plot(t2,slowEma,label='slowEma') plt.xlabel('Time') plt.ylabel('price') plt.show() diff --git a/vn.training/draw.py b/vn.training/draw.py new file mode 100644 index 00000000..adbb0d8d --- /dev/null +++ b/vn.training/draw.py @@ -0,0 +1,1002 @@ +# -*- coding: utf-8 -*- + + + +import sys +import os +import pickle +import copy +import datetime +import numpy + + + +import matplotlib.pyplot as pyplot +import matplotlib.ticker as ticker + + + +import Public.Public as Public + + + +from 子图定义.公司信息子图 import 公司信息子图 +from 子图定义.日线价格子图 import 日线价格子图 +from 子图定义.日线换手子图 import 日线换手子图 +from 子图定义.分时价格子图 import 分时价格子图 +from 子图定义.分时手数子图 import 分时手数子图 +from 子图定义.实盘价格子图 import 实盘价格子图 +from 子图定义.实盘手数子图 import 实盘手数子图 + + + +__color_pink__= '#ffc0cb' +__color_navy__= '#000080' +__color_gold__= '#fddb05' + +__横轴倍率__= 10.0 / 230.0 +__纵轴倍率__= 0.3 + + + + + +class 分时行情图系: + ''' + + ''' + + def __init__(self, parent, 目标日期, 绘图数据): + ''' + + ''' + self._parent= parent # 分时大图区块 对象 + self._ilogger= Public.IndentLogger(logger=parent._ilogger._logger, indent=parent._ilogger._indent+1) + + # 当日数据 + 当日数据= 绘图数据['分时数据'][目标日期] + self._目标日期= 目标日期 + self._时间常数= 当日数据['时间常数'] + + # 横轴参数(分时价格 与 分时手数 共用,所以放在这里) + #=================================================================================================== + self._横轴参数= self.计算横轴参数() + + # 个股行情 + #=================================================================================================== + if 当日数据['个股行情有效']: + # 分时数据 + 个股分时行情= 当日数据['个股分时行情'] + self._个股时间序列= 个股分时行情['时间序列'] # 成员是 datetime.datetime 对象 + + # 分时行情坐标序列 + self._个股调整时间= self.计算调整时间序列(时间序列=self._个股时间序列) + self._个股坐标序列= self.计算调整时间序列坐标(调整时间序列=self._个股调整时间) + + # 子图对象 + #=================================================================================================== + self._分时价格子图= 分时价格子图(parent=self, 目标日期=目标日期, 绘图数据=绘图数据) + self._分时手数子图= 分时手数子图(parent=self, 目标日期=目标日期, 绘图数据=绘图数据) + + # 衬底平面 + #=================================================================================================== + self._衬底平面= None + + + + def 计算横轴参数(self): + ''' + + ''' + 时间常数= self._时间常数 + + 上午开始时间= 时间常数['上午开始'] + 上午结束时间= 时间常数['上午结束'] + 下午开始时间= 时间常数['下午开始'] + 下午结束时间= 时间常数['下午结束'] + dtc_092500= 时间常数['09:25:00'] + dtc_093000= 时间常数['09:30:00'] + dtc_130000= 时间常数['13:00:00'] + + # XXX: 这些内容只能在各个图系里分别计算,因为只有带具体日期的 datetime.datetime 对象才支持减法操作 + 横轴参数= {} + 横轴参数['左侧裕量']= 30.0 + 横轴参数['右侧裕量']= 30.0 + 横轴参数['坐标起点']= -横轴参数['左侧裕量'] + 横轴参数['坐标终点']= ((上午结束时间-上午开始时间) + (下午结束时间-下午开始时间)).total_seconds() + 横轴参数['右侧裕量'] + 午间时差= 下午开始时间-上午结束时间 + 横轴参数['午间时差']= 午间时差 + 横轴参数['横轴尺寸']= 横轴参数['坐标终点'] - 横轴参数['坐标起点'] + 横轴参数['横轴倍率']= 2.0 / 2300.0 + 横轴参数['横轴宽度']= 横轴参数['横轴尺寸'] * 横轴参数['横轴倍率'] + + # 计算主坐标点 + #-------------------------------------------------------------------------------------------------------------- + 上午主标时间= [时间 for 时间 in [dtc_093000 + datetime.timedelta(minutes=15*i) for i in range(9)] \ + if 时间 >= 上午开始时间 and 时间 <= 上午结束时间] + 上午主坐标值= [(时间-上午开始时间).total_seconds() for 时间 in 上午主标时间] + + 下午主标时间= [时间 for 时间 in [dtc_130000 + datetime.timedelta(minutes=15*i) for i in range(9)] \ + if 时间 >= 下午开始时间 and 时间 <= 下午结束时间] + 下午主坐标值= [(时间-午间时差-上午开始时间).total_seconds() for 时间 in 下午主标时间] + + 主标时间序列= 上午主标时间 + 下午主标时间 + 主坐标值序列= 上午主坐标值 + 下午主坐标值 + + # 计算副坐标点 + #-------------------------------------------------------------------------------------------------------------- + 上午副标时间= [时间 for 时间 in [dtc_092500 + datetime.timedelta(minutes=5*i) for i in range(25)] \ + if 时间 >= 上午开始时间 and 时间 <= 上午结束时间 and 时间 not in 上午主标时间] + 上午副坐标值= [(时间-上午开始时间).total_seconds() for 时间 in 上午副标时间] + + 下午副标时间= [时间 for 时间 in [dtc_130000 + datetime.timedelta(minutes=5*i) for i in range(25)] \ + if 时间 >= 下午开始时间 and 时间 <= 下午结束时间 and 时间 not in 下午主标时间] + 下午副坐标值= [(时间-午间时差-上午开始时间).total_seconds() for 时间 in 下午副标时间] + + 副标时间序列= 上午副标时间 + 下午副标时间 + 副坐标值序列= 上午副坐标值 + 下午副坐标值 + + 主标表示序列= [时间.strftime('%H:%M') for 时间 in 主标时间序列] + 副标表示序列= [时间.strftime('%H:%M') for 时间 in 副标时间序列] + + 横轴参数['主标时间']= 主标时间序列 + 横轴参数['副标时间']= 副标时间序列 + 横轴参数['主坐标值']= 主坐标值序列 + 横轴参数['副坐标值']= 副坐标值序列 + 横轴参数['主标表示']= 主标表示序列 + 横轴参数['副标表示']= 副标表示序列 + + 横轴参数['xMajorLocator']= ticker.FixedLocator( numpy.array(主坐标值序列) ) + 横轴参数['xMinorLocator']= ticker.FixedLocator( numpy.array(副坐标值序列) ) + + def x_major_formatter(坐标, pos=None): + 偏移= 主坐标值序列.index(坐标) + return 主标表示序列[偏移] + + def x_minor_formatter(坐标, pos=None): + 偏移= 副坐标值序列.index(坐标) + return 副标表示序列[偏移] + + xMajorFormatter= ticker.FuncFormatter(x_major_formatter) + xMinorFormatter= ticker.FuncFormatter(x_minor_formatter) + + 横轴参数['xMajorFormatter']= xMajorFormatter + 横轴参数['xMinorFormatter']= xMinorFormatter + + # 计算标注位置 + #-------------------------------------------------------------------------------------------------------------- + 横轴参数['标注位置组一']= [坐标 for 表示, 坐标 in zip(主标表示序列, 主坐标值序列) if 表示 in ('09:30', '10:15', '11:00', '13:15', '14:00', '14:45')] + 横轴参数['标注位置组二']= [坐标 for 表示, 坐标 in zip(主标表示序列, 主坐标值序列) if 表示 in ('09:45', '10:30', '11:15', '13:30', '14:15')] + 横轴参数['标注位置组三']= [坐标 for 表示, 坐标 in zip(主标表示序列, 主坐标值序列) if 表示 in ('10:00', '10:45', '11:30', '13:45', '14:30')] + + 横轴参数['标注位置']= dict(zip(主标表示序列, 主坐标值序列)) + + return 横轴参数 + + + + def 返回图系横轴宽度(self): + ''' + + ''' + return self._横轴参数['横轴宽度'] + + + + def 返回图系纵轴高度(self): + ''' + + ''' + return self._分时价格子图.返回纵轴高度() + self._分时手数子图.返回纵轴高度() + + + + def 计算调整时间序列(self, 时间序列, 午间时差=None): + ''' + 午间时差 是 datetime.timedelta 对象 + ''' + if 午间时差 is None: + 午间时差= self._横轴参数['午间时差'] + dtc_120000= self._时间常数['12:00:00'] + return [ 时间 if 时间 0 and 日期 not in self._左侧连续记录: + 当前偏移 += self._图系间隙 + 偏移记录[日期]= 当前偏移 + 当前偏移 += 图系.返回图系横轴宽度() + + 横轴宽度= 当前偏移 + return 偏移记录, 横轴宽度 + + + + def 计算连续日期记录(self, 日线数据): + ''' + + ''' + 连续日期记录= [] + 左侧连续记录= [] + 目标日期列表= self._目标日期列表 + 目标日期个数= len(目标日期列表) + if 目标日期个数 > 1: + 个股日期= 日线数据['个股日线']['日期'] + 位置序列= [个股日期.index(日期) for 日期 in 目标日期列表] + 新建记录= set() + for (前日偏移, 前日日期), (当日偏移, 当日日期) in zip(enumerate(目标日期列表[:-1]), enumerate(目标日期列表[1:], start=1)): + if 位置序列[当日偏移] - 位置序列[前日偏移] == 1: # 连续 + 左侧连续记录.append(当日日期) + 新建记录.add(前日日期) + 新建记录.add(当日日期) + elif 新建记录: # 连续段已结束 + 连续日期记录.append(tuple(sorted(新建记录))) + 新建记录= set() + if 新建记录: # 补上最后一个(如果存在) + 连续日期记录.append(tuple(sorted(新建记录))) + return 连续日期记录, 左侧连续记录 + + + + def 返回区块大小(self): + ''' + + ''' + return (self._区块横轴宽度, self._区块纵轴高度) + + + + def 区块平面初始化(self, 图片对象, 区块偏移, 全图大小): + ''' + + ''' + + 本区横移, \ + 本区纵移= 区块偏移 + + 图系偏移记录= self._图系偏移记录 + + # 下级图系平面初始化 + for 日期, 图系 in self._分时图系集合.items(): + 图系偏移= (本区横移+图系偏移记录[日期], 本区纵移+0.0) + 图系.图系平面初始化(图片对象=图片对象, 图系偏移=图系偏移, 全图大小=全图大小) + + + + def 区块绘图(self): + ''' + + ''' + for 图系 in self._分时图系集合.values(): + 图系.图系绘图() + + + + + + + +class 分时小图区块: + ''' + 区分大图区块和小图区块的目的是可以为分时行情(包括当日实盘行情)采用两种不同的缩放比 + ''' + + def __init__(self, parent, 绘图数据): + ''' + + ''' + self._parent= parent + self._ilogger= parent._ilogger + + + + def 返回区块大小(self): + ''' + + ''' + return (0, 0) + + + + def 区块平面初始化(self, 图片对象, 区块偏移, 全图大小): + ''' + + ''' + pass + + + + def 区块绘图(self): + ''' + + ''' + pass + + + + + +class MyFigure: + ''' + + ''' + def __init__(self, tname, ilogger, 绘图数据): + ''' + + ''' + self._tname= tname + self._ilogger= ilogger + + # 图片相关数据 + #=============================================================================================================== + # 图片名称、目录 + self._图片名称= 绘图数据['图片名称'] + self._图片目录= 绘图数据['图片目录'] + self._图片路径= os.path.join(self._图片目录, self._图片名称) + + # 下级数据 + #=============================================================================================================== + self._任务参数= 绘图数据['任务参数'] + self._公司信息= 绘图数据['公司信息'] + self._日线数据= 绘图数据['日线数据'] + self._分时数据= 绘图数据['分时数据'] + self._实盘数据= 绘图数据['实盘数据'] + + # 处理日线数据 + #=============================================================================================================== + self.处理日线数据() + self.处理分时数据() + + # 建立下级区块对象 + #=============================================================================================================== + self._文字信息区块= 文字信息区块(parent=self, 绘图数据=绘图数据) + self._日线行情区块= 日线行情区块(parent=self, 绘图数据=绘图数据) + self._分时大图区块= 分时大图区块(parent=self, 绘图数据=绘图数据) if self._任务参数['绘制分时'] or self._任务参数['绘制实盘'] else None + self._分时小图区块= 分时小图区块(parent=self, 绘图数据=绘图数据) + + # 计算全图大小 + #=============================================================================================================== + + # 图形部件颜色、尺寸等基本配置 + self._facecolor= __color_pink__ + self._edgecolor= __color_navy__ + self._dpi= self._任务参数['dpi'] if 'dpi' in self._任务参数 else 300 + self._linewidth= 1.0 + + self._左侧空白= 12.0 * __横轴倍率__ + self._右侧空白= 12.0 * __横轴倍率__ + self._顶部空白= 0.3 * __纵轴倍率__ + self._底部空白= 1.2 * __纵轴倍率__ + + # 开始计算 + self._全图大小= self.计算全图大小() + + # 根据计算出的尺寸建立 Figure 对象 + #=============================================================================================================== + self._图片对象= pyplot.figure( \ + figsize= self._全图大小, \ + dpi= self._dpi, \ + facecolor= self._facecolor, \ + edgecolor= self._edgecolor, \ + linewidth= self._linewidth ) # Figure 对象 + + # 把图片对象交给下级完成下级子图的初始化 + #=============================================================================================================== + 区块布局= self.计算区块布局() + self._文字信息区块.区块平面初始化(图片对象=self._图片对象, 区块偏移=区块布局['文字信息区块'], 全图大小=self._全图大小) + self._日线行情区块.区块平面初始化(图片对象=self._图片对象, 区块偏移=区块布局['日线行情区块'], 全图大小=self._全图大小) + if self._分时大图区块: self._分时大图区块.区块平面初始化(图片对象=self._图片对象, 区块偏移=区块布局['分时大图区块'], 全图大小=self._全图大小) + if self._分时小图区块: self._分时小图区块.区块平面初始化(图片对象=self._图片对象, 区块偏移=区块布局['分时小图区块'], 全图大小=self._全图大小) + + + + def 处理日线数据(self): + ''' + + ''' + 任务参数= self._任务参数 + 公司信息= self._公司信息 + 日线数据= self._日线数据 + + 个股日线= 日线数据['个股日线'] + 指数日线= 日线数据['指数日线'] + + 复权绘图= 任务参数['复权绘图'] + 均线参数= 任务参数['个股均线参数'] + 绘制均线= 任务参数['绘制个股均线'] + + # TODO: 如果需要,把今日实盘数据加入个股行情以及指数行情 + if self._任务参数['绘制实盘']: + pass + + # 计算个股换手率 + 股本变更记录= 公司信息['股本变更记录'] + Public.计算个股换手率(个股行情=个股日线, 个股股本变更记录=股本变更记录) + + # 计算复权行情。XXX: 注意 复权日线 中是否含均线集取决于传递的 均线参数 + 复权日线= Public.计算复权行情(个股行情=个股日线, 均线参数=(均线参数 if 绘制均线 else None)) + 复权日线['换手率']= 个股日线['换手率'] + 复权记录= 复权日线.pop('复权记录') + + 日线数据['复权记录']= 复权记录 + if 绘制均线: + 复权均线= 复权日线.pop('均线集') + + # 计算绘图用的补全行情 + 绘图行情= {} + + 绘图日线= copy.deepcopy(复权日线 if 复权绘图 else 个股日线) + 绘图行情.update(绘图日线) + + if 绘制均线: # 计算绘图用的个股均线(是否复权由任务参数控制) + if 复权绘图: + 绘图均线= copy.deepcopy(复权均线) + else: + 开盘= 个股日线['开盘'] + 最高= 个股日线['最高'] + 收盘= 个股日线['收盘'] + 最低= 个股日线['最低'] + 绘图均线= { n : Public.计算序列加权均线(开盘, 最高, 收盘, 最低, n) for n in 均线参数 } + 绘图行情.update(绘图均线) + + Public.补全个股行情(完整日期=指数日线['日期'], 个股行情=绘图行情) + + 日线数据['绘图日线']= 绘图日线 + if 绘制均线: + 日线数据['绘图均线']= 绘图均线 + + # 计算个股衍生行情。这里不用 deepcopy() 了 + 衍生行情= 复权日线 # XXX: 注意: 复权日线 内可能包含 均线集 + if 绘制均线: + 衍生行情['均线集']= 复权均线 + Public.计算个股行情衍生数据(ilogger=self._ilogger, 个股行情=衍生行情, 均线参数=None) # 这里不用传递 均线参数,衍生行情 里已经包含均线集。 + + 日线数据['个股衍生行情']= 衍生行情 + + + + def 处理分时数据(self): + ''' + 把 日线行情 中的数据添加到 分时行情 当中 + ''' + 任务参数= self._任务参数 + # 公司信息= self._公司信息 + 日线数据= self._日线数据 + 分时数据= self._分时数据 + + # 处理个股分时数据 + 个股量均参数= 任务参数['个股量均参数'] + 指数量均参数= 任务参数['指数量均参数'] + + 个股日线= 日线数据['个股日线'] + 个股日期= 个股日线['日期'] + 个股开盘= 个股日线['开盘'] + 个股最高= 个股日线['最高'] + 个股收盘= 个股日线['收盘'] + 个股最低= 个股日线['最低'] + 个股成交量= 个股日线['成交量'] + + 指数日线= 日线数据['指数日线'] + 指数日期= 指数日线['日期'] + 指数开盘= 指数日线['开盘'] + 指数最高= 指数日线['最高'] + 指数收盘= 指数日线['收盘'] + 指数最低= 指数日线['最低'] + 指数成交量= 指数日线['成交量'] + + 时刻常数= { + '上午开始' : datetime.time(hour=9, minute=25, second=0 ), # 如果是实盘,设成 9:30 + '上午结束' : datetime.time(hour=11, minute=30, second=10), + '下午开始' : datetime.time(hour=12, minute=59, second=50), + '下午结束' : datetime.time(hour=15, minute=0, second=30), + '12:00:00' : datetime.time(hour=12, minute=0, second=0 ), # 把任意时间转化成坐标的时候要用到 + '09:25:00' : datetime.time(hour=9, minute=25, second=0 ), + '09:30:00' : datetime.time(hour=9, minute=30, second=0 ), + '13:00:00' : datetime.time(hour=13, minute=0, second=0 ), + } + + for 日期, 分时 in 分时数据.items(): + # 时间常数 + #======================================================================================== + 日期对象= 分时['日期对象'] + 时间常数= {key : datetime.datetime.combine(日期对象, val) for key, val in 时刻常数.items()} + 分时['时间常数']= 时间常数 + + # 添加日线相关数据 + #======================================================================================== + index= 个股日期.index(日期) + 分时['个股当日开盘']= 个股开盘[index] + 分时['个股当日最高']= 个股最高[index] + 分时['个股前日收盘']= 个股收盘[index-1] if index>0 else None + 分时['个股当日最低']= 个股最低[index] + + # 计算前期平均手数 XXX: 如果以后 日线数据 中增加了成交量均线,那么这部分数据可以直接从中截取。 + 分时['个股平均成交']= {Public.计算均值(个股成交量[-n:]) for n in 个股量均参数} # 结果: (均值, 最大值, 最小值, 标准差, 长度) + + index= 指数日期.index(日期) + 分时['指数当日开盘']= 指数开盘[index] + 分时['指数当日最高']= 指数最高[index] + 分时['指数前日收盘']= 指数收盘[index-1] if index>0 else None + 分时['指数当日最低']= 指数最低[index] + + # 计算前期平均手数 XXX: 如果以后 日线数据 中增加了成交量均线,那么这部分数据可以直接从中截取。 + 分时['指数平均成交']= {Public.计算均值(指数成交量[-n:]) for n in 指数量均参数} # 结果: (均值, 最大值, 最小值, 标准差, 长度) + + # 修改分时数据,截掉边界外的部分(主要是为了与未来的指数行情统一) + #======================================================================================== + # if 分时['个股行情有效']: + # 行情= 分时['个股分时行情'] + # 时间序列= 行情['时间序列'] + # 价格序列= 行情['价格序列'] + # 手数序列= 行情['手数序列'] + # 金额序列= 行情['金额序列'] + # 备注序列= 行情['备注序列'] + + # 分时序列= [ + # (时间, 价格, 手数, 金额, 备注) \ + # for 时间, 价格, 手数, 金额, 备注 in zip(时间序列, 价格序列, 手数序列, 金额序列, 备注序列) \ + # if (时间>=时间常数['上午开始'] and 时间<=时间常数['上午结束']) or (时间>=时间常数['下午开始'] and 时间<=时间常数['下午结束']) + # ] + + # 行情['时间序列'], \ + # 行情['价格序列'], \ + # 行情['手数序列'], \ + # 行情['金额序列'], \ + # 行情['备注序列']= zip(*分时序列) + + # # 价格序列= 行情['价格序列'] + # # 行情['上涨标记']= [False] + [True if p2>p1 else False for p1, p2 in zip(价格序列[:-1], 价格序列[1:])] + # # 行情['下跌标记']= [False] + [True if p2Dwf3 zq-0dp=L^=pICw-;k?hWrpHG_v?b)(P2udC!CC?bQI4P4~9ZSvMN4hj?)~t@DR>?N; zXcbP()`OciZHCjmZw3qXE|kPV9lE_{q1N}UOo{4{&YZBjty!r0jl0Zg^UR-x25I>(%<`x9BRTQ^>HYF>)AGiTl6SmxOLSoNA~F0ek=Tv4p2TD|7X z+?$v)EbXLvh?@OgRb@)d8>?D9`rlmM%$eltqvk}nZfRmlo35;@(FGsWYu*0I8^*eOSQtYa-leLTj_BI;?! zJ?gQdK184rT8_G(@l!ok)baI;nR9Y3JNKxId#xSj%@tL7;QbNS~l8(Z%YP zSCgYd9Un>(qKy zJ!;-OlbRgW_m4kw+9{=3PQxo1%&|Pb!JLp~tevRHE?V`<<|I}tsLx_{UIpsCW=@W1 z){f{_^;E z<~;awS8b4gu-Bg{=U3a9bL1gwhu_zWjTpc4l@DuY$-x`UnO0J(HUFPIOj&11WYw32 zveV%w=3?F8XWq~ny#6*rO}3O;RGK+WSJ~BCme?o{rub0=iImw(V86nJRyNLD2 zGGdXIoRBqv4a>9}87yZ|6K$XdT+mvcvZ)peZFOU(Y}sBki5+xjBdx&{pP#Iq2lcHi z^lFMXb6#vK#6oYQZm@PT%9vQ_--EpBM0w?8?L_6Okws08DmzbLPQ*H-(9G;Oe-l$C zbz<#At-PQ)>vprDi<)=7i+YHt`QO=DM6L2nXVq71ea)(mSRc=nP93yby(+{qW&BYs zXK{$8%qp{oHQ2{_n+tEMvUV&B*sw&^PRn395t&k1I~DviC0~8bG0yf^LzYk16V&RK zE4SyVwJZzLtSr>#66>?2f&7exdYZJ6A8pif#vk!!$>W;an6u)A!kn|&Sf4F#N@(q* z$FlxdzUDp1`s_J?9W<(}T|3jN?eDTs#1S+@Xxdk)|`HOn6mJ0 zE#@q__nL(k)QMHQVHvuo5L06QF)^j!0jBWZ$IMe}@y{ZR%%lTZKDg3KNYngI!I?s}K855Z^Z#U~9KPzSe%UO2yAZvNi1kITeV`s_3GsQD! zLSfctKJga>%ng3R;B%~2-};-F(=s%KHP~SJFs9UQXk*FdF+Z8&x5&Dob`YVG*N`7@>9 z6Cc)K!AGT;Q(%Ohg^Enql-xN()KKK>4J;J7^O{4=iBxj&%;|n?9?NN%(Npb4WIel< z6XVXh8d>9!z(T=iPzfUzIkGeBbL6+F39R!KeeNO^&WQgwi8TDsaSfGniKKkmqS$|-oFV^b0Qv^ZZPMayN~KbmU!2bH8|)V z&#GTAuv% zk}EwN$ZC~{6Id;e`XQ(7+gS(r2AP*3Kp8f<4heB#ZsL*%NY9joAL?Rw=+ zM}&s=*D!@S7h2z7Ew8<*wLEo{cC3M|5W%dAtXc0IJ4B8jOo@09pP_~#4ma7Ph9ZvM zvZ@V6oSMgyBW`x)nR27!JeHjDOiLbJxhJbtZIV{2;A^eH>_fCr_Dz}-;cH{<%qRwn z&PRODQD+vb z{ozO5%V0UfkHoUji~v}=g?DVxLN~{)WjSw#lvit6JOks^97{mWfojOod>Cuc(z&_9 zLe{M8bSzdri78F2>C9=}L~CcjG&@t~Un#^|Ub51MH8>|hJH(QkHkPw>o4|5*yl^mO z(@JNKvm2{RYSj;W__Lh!**4XQ^655DZ8<8Wtmd?QWn!WJkq+ie+Qw={jeVNVl=1Hq z)PqKi84dx%5+!{;$=YdkUtx;x0PRda_zq(|Jb?`L*|cduteRt4Q`wt2yGm+jdUKUF z2p3~nJC>aZ9<0`S%U#yaVeP|+o&3E^(V9j58G@kv~#P8IjcwV%;^`phgGjxurzb>5AtD7)?}n$ zSS%iUSfBY%lWVCV{_VviHN?NRXE5bQCx51V%BqF#O03WP5k;%MD_1JZ*;9rci@&nH zHM-iRKl?7;MV)0x(L{h!QY- z_b)}MFJjupT%>#;~qJF&O*y)fBT? zLa2hhLH&F%j=CXv$Sf{NO64Af=cbEvYg7=MOr%p}U2QJ?8{6-m^zirnmWHb{NIYi%k5@5@hTP zw`7H}F6>PyrriIKnDlW_HW>fKxydNm>}2e-IEs0XoJtkEPsEb(?_HjZt>K*o3KK&=@3hmsguy|UU$myJ~2;Wd=%@q0egg)@bZP%nTnipIsdB@4x@ z2^Uqfb3v0mg-4IG@%OV+uqXfSerzVaqJmLq9G-_TAHYMp_CNlMgtOW_?JqCC2o}BU-}do zTemr69Q|+6@$4}vWcXf{NlctlToo@3rF!D53o-l=z} zg0+=Dl~Q{QrXlB#1=0{G69gKwq#;k#%Q)gC8H4XH%Cukdpuyt%oL0@JM-;P%q-5x{0GFl4vh?gIY1=anN+*Z6)zou576CY4eb zW}=a>u5Cw|);V6(!PH+js6uLkeH|UvHt7-_$U2rIk)KnGM3eYUJ+TTM$ykRsq@EeJOj9eYK2FC| zay+FHKUY0>+x-Bwm2>atc((F(O0fn{R~rlaq?%jR)RZ-EX(ZxjucE<94SGVxQ06v? z$}OX6vO8$Z3`&@KgrV8!=-!hqsPkoi7B-M{WtX^V=9-Rj>Il+ z>TXiL#cDqX7Nc0~$crQuMxEgL|kq&5%9q%L|y zqQP6e-;g}X_ z&99C`wNM%Ye$NlJm2-EfXZE{)C^OcULB?^QK2`T`dWKTcYvm$i?K_8#9`8AqN_>}Z zR5FcNNyb*apxR1_3^MVO=pzYU=^9z{jQ1fKUKV?VAwBJEk0Qa_a(CFrI!WT>~c|F={c2}hIq z6f>16L!Gxj>89?2cBIhNG|bCE%Z2rdx=tweHR=N4xr16sjk~5^RF5OH5;;rRtsIFwGAO3O#;O8at+TF>(gtzdk<8?;H} zZ~msOpOV>&DwwWKA;Z^=Rad?~2Pjj?GKaM}n>GukhDmA%kD2KlY%6_e1L^pai;Ssl z4prPxjY`VCaV25w;}@v0O|7VDyJG;Iv~o5J^~DzNOQLe+sA`_Cq61l%45tc)@||gx zn{MZ!;~D%mQi?Tgn!38x{7oISpZumaR=oi|xlE=?baX=#f3bPDV~cOv!=Pz5tQ3y)HeMyk+C(HPNKbwx`T*a=tEP(dMH7i zA!+l~J7?H8Dy3fOuHMD9Lh^NCp97|^dOl_9iy4!oY;jU{Khe)9-Z6@^jbeABxZfzc z8AYQ}v>3&bMzOC^9BC94qgcu)e&oa_oVbD$XBoxqoOq8DQ#tW4C&qE&GEN-EiEUM7 zs!{A>6l0BIuu-gG6tfz|Z=ASX{eLMZcHzX%oH&XTI~v7PoVbk>lR2>!CpO^38k}h1 z#Nui@QJh$T6H9VpHBPL}i3K>Z2q$Le#2=ivixVfS`ADwGi7hD<^I06giFu9Uznp00 z#KxR>krSJ6;&4tZ$calhF{e@NXA~de$VTx6N9Tv*`?!gsQ9Q$mr#bNiN8LofZgApR zPArU*=ERkp=*x-aII$EbR^miYPRy)!^8hD?8R^LAVSJEivF^ah0yD>W;8pQ;o$Qi{{qZq0F-^oZLT*fG#=ft&~_)9%y%pF_;bQta9 z`rqcnL+ZI?F^c1i;yX^<%!%tb5og*{oe=^@7q`7StQdqzoH$q=9E@^VPQ*;X1S!FZ z`P3o?7T3MGdc81rZ0gwZoEV`_RWm2{Rj*nP_0X8PmDO6AII$Qf7UIPGs)I{|BK{x4 ziL*IzA}3BzD~{qsR4m2Ce2RI8ONuLv2Jyc|?FOa_`XF#IU&Uxn3{?Lwk8@QA0Oxds z6W4I!GfteZw(Y}-UDba1b20ylSvYa7IwF|!ht+;hQ7=5EeSmrwU<5m>lW#OfQ}dWQ zWax3Edck&c;v98c24QNdw5&M3|_im%azQ7ov=HJs59qqxi{wla$I)T`aWC=N4< z1C3%Yqc{P{MzOh@nBPt8Y7{3L#l2Wi+(avu12++~23782m2ndXyNRRR#5gxGlN(LR z9!4?1C^j{ULycmHQGBAVI47|>7{xD~IK?P-Gm6#Jd41I=o-@)~fa!zi6}7ibjI<*BwPJ4pDP;isr1; z{$RT1;^SKK@J^c3RS$kJM%UE7f{t!~UrQdW4Q6!Gb~~%}w2BrwxKUHK)z*}GQ+aqe z==9y1V?C{fGQF~^p{VO!wNOq?i7r=JQ}Xv#SkA55cIJHW;hFPpxx$=pKVfl*sQp$u zb5`Wil=nw9lRIhHq-HRs@6&3SI+4Qh_Lgu-vcx!eG9KD(UoQ}CAwai zrsPORgpPH&3JXQec*jCfeV1uU+jE-IO1qn)%+5bT4cTlrsMV_kX-@0qnv+)>ji_o( zd6wfnn}sZ|YeB$4Ek`Es%sE+9Q%;SyGi6hx=FGgHh5q;5Z}u`w;ZLq+IsC=qJfu1n z{&AwhoU|93@?bXv3ay)^DJeJXOgZ4GDT|*fOc`6v&YUS_c;@VEr7&mjPECn-DzbJH z@kb8W)f`K}dssTJ&V@ClnT2OcR75Ywlk-vSyQ@&Ys!k2nlf*crVO?i z@E=<4&{hkL(k7%OsRPeK7qY-o&VmR#b9OFKnDgQyuQ}RHWO=B~L(7Xbh)}IBH&{-T zv8d*F6tJtIs3Pu~Q+T5miu|f6ttTM`)uWVaT4?hTEwss5ec>QY8Qw~BHk8wxgQqk{ znP$L$XvgAgr?XXI%FI|ha~4$LnX@jZ!kmfDsa3w37K-SmIjvV|4j-vG6|}`Fszf`x zK|Pki6PC{X-4vcVpZjUb+QFJr`>5trf1|)da+l7UGHa9OjB2GhqdVB)A!pKio;ha< z!jkg6rox<$1$pK?buM~t4ehEDRr-X&oEigp=F~Z_IRP;WQ)-@pMT1doB|CE()YhCS zD-@XLx`nNT&hAi${%ly4xOyQqQ)ttRIkwPs`c&Is3 z&e@r=qPOO(KcqPcW`%`%l0db_jn7|4Iv$ zKg_dG?l3J>EQjWl@)15hy3$rQk@CV$Z4rxx`e>BGuf(V7G9nh49 zvo$5Qq~;Xz(46pQn)Ab(Iec_h1s=K@JeFrp^%0s==BJ%0VT)iX$9)&CI+02>rbH?c znp6I~qJ|Zto4~U4zMGJW0h*nGbf*Rr|@~I zD@@6(-EllOUQ=>b)tri#G$*35=7g=b8`K7aJT<5A9SC#?pNY(gJgX%~#`n?`>A9lj zM2_vLDR<1w;R{c)t0CT0TXPB=;|*$q*(We3@>mx|RU)TrGa+)_cnCOXA4 zQb1x(#Env#b0k4?QnXtp;M256wO62AxnzQw>7TQx;bLMDQna`cb6uy6)-JlM_&?1^L zd^OLM(f*n;G7$oeeEd#3b2iSc-65~`K>vB894t z+o^^2cx%q*rz{jzAiEtN*7<Oj4}+z4 z8ZWjpr}_%!SU!#6RmGCj8vrM}Tff@`htA%o#H78RwI}3ScH%!pKyvRZxDXf0X+8t z=Dxt1mB6Usz>hw_!GFsc1bcRPgJ%HSR{=F|07d5k8Qp=!<$(effz2=c48rI;fb<;5 z_zLWQ1?2KXD~SW(EmeR9*MLU$vN%xDG6o?aurv;S172`A@O1zJE&2gPcLJxE0?BSb z>o$mb)&xAP0KW@ZPStMCLbPpFphq#_3~4hRfZP-4pNi%eOaiW+04&9z_4o|0lmuKb z15GXg?@s}h{DFp!AMIwYd`4TQR1QH4|UIF1&jrN31(=O{eWIYfP!yv3O!SRJja1=7pZt3sP`L~dhC~gpn=ZANa1Csxs$fW^S+5*!W0Y@4Di`;qjfMc;gMjh=K<^Sj;1BdFcq(wV zKX7g?uucMoYy#e%0>os%-VSKy1+@E66e-VuUyp!XM}R`!2*g|f3O)v2C}=rpK5%Oi zklGu#Ob0jIM6Vtk0IEC!UhD*f)4gV^cGyuL2H0cH2*4@FddWn0`KPo$0q^3 zh5#>@0nvqkCxr9WF@c+$fmiwBnekJ8a-)DIw-?#tJSKowd$ z=dK3qy;00j4rtQ|$dncMJp+NG9e{rcKFxsl<$+r{fb24`s}}NZH3mv`1(sie*6AG} zurUxJ6-`$8#Wf(O6q^Gz5-wa$q!rYqCBlGuCjUKs1p`y7mI`A-9xDd9Ys zed#4(3=`7&!q~;x3Jky5I-X4H(|$14srSEdXw2U91&raus8v)7%CUed)b73$C0jzx zM=(n0k!w_1a{DNyeB9ig#Nq)fNvzU#B#eFUi*_)Mf=#Ydj2|$ITFD>L6UHz&vN;TY z=VCaFX>^rn7+bL!-Z1t@zyCo6tM_ASOvzeeJq+LH&Kj8Xiw}Ck7zSE;!6aU&G7-ih z6zE5dwed_vNof?lkIeW%Cn$Dps};uXKD-%>_sYRFVXP^}x-f~0I+rHnSk(!JZy?vB zQvXo_Ft#a6%fUEGL`6~Q_9c-_3D06MCi`?>81FJC^1v8c?s2DHmo}$R%JH(ZVE7Gn zn!}ie1yh^$)h+8&Y11X@OXAy~RNY$V>URUGH*Z3NW&O8r3AAEL+n$xov(-HyD!s)v zF!uGvyDtO7?Q;MRVp24i@cxdO#X*#b#?R<9C_?alpA7{{>=Ibf^>XXU0+ z>`b}eUR)R{)vz5vC-Htv_jlafDW+tC@OQp~;ZCGB}cAof{8>;U^@h zmt$>@<0v_H)u$@98c|`D_tMoINcR#wPG+R-OgdurDHW+)^r4%#tI$j*cvFXQ8O1Ua`fH6h+ld;a+I+?^fzv5vGuM_CR zY(Fxd<3Ns%;fLvXRp%ClsJt6M%adtJJDNRwnX?`cP5uk=!B|BfHFM1xiW%yEryJJ# zr?7g(y89nM$+6j;E~+vn_8^SmVT1fI_H5DWl2<(6d1~|6*n$+hac>Gr@n;=0!R(if z-C&ec_vcbdlXau06jHYfm2Rz|McmQhJ55^a+wO~C3^^@c>2^Sxl=BM0V#vF0jClQwaD za~gU7brZ-Wp4~wM$(I;I*TI_5fx64z4xmMjk7z~{S}|s!CyA|hPa1h!Go?99((>gr z61GOo8o=;1yrVE99kV;C_fc$zM-Uya+R{X^=PZ4TQc}}sn%ZKTokmINy@t-#zke4x zkkYoQ+WGFSsS~NSrcu-U&+f%x;%D}#2xD**JqzO)HvJAvdby=E@_gYebjv8gt!Y6r z_0N0=#*jH}KE-m3vyhmsuC)AY2TcuomR*@>4tA?J2PNC$CUovfQq>(W)-v%ly4Gp` z{37w$B=uo4DJc@A#DX4l?uJ&EJHqguS>IDimPRyROe4C{{IuncpzEMKe%}GcQ2hhV z3GaS^(J-bF?;63xUoPC6N(D9)gK?~GLF=`B^`x>ewkZ|p36%cy!f!k~ykE_$K`FI2 z<%IFCUn>YksrWJrnFSN6vDhA&sk)-%pcwlCTY;c{xJ5Y1!#5SpHF^< zk}Xf?Z74ae9okOfyvg*4w53F zi(*0Rse|^+gI~cIE}m+KOl5DAdsJb-pV`#dkLM>5GgaUG28Pdj%$HK$JQ+r@kKL$E z!|kg_P_mc#QI=A2SN#oRe>YBqXvp1aB*hvX;HfmCMI9>nZGR5KmzXt{#Mck^qJm>} z1GO)KMG~l$?*n#|*e2rGhr*Fs3$@Q(zqL^U~U?_-y|I zV^1n}5r+R-=maWQ@mIvDUg4esnObfZ80GmrTKtu{E$JpP?QKS_*!$Ng24l*x;2}gu zmT2l3pDRZN80+>-SzrueR1Q?<{YIUlQdX`FjIC{J>XKtxB5j@>$(15tOnYB^hf$^( z|3zblKix0F@ct?JAR0DLc|qbpUwS|*`3lj5ww;;eAaTLty)cQr`puwH3f?OIJvF<0K!ymRaz@AkaS`ppt)BCI0{^pI!)- z^6P~VseoPxl?v*GFsYDUFiVAXLO_62L@xwNMfF0ER7@`fOU3mvw^g^f0Lk(nA)GEllwJswqV+yJ>4i|Kv0ez1n&<_y)Ko8oOU-lwM!os}3jzL>FzH(8g#f9gUI>(0>4hMv zwO$C8+USKasjXfxOL2N3TxzEiFhK3~LV#q|3&Bzcy$~XG)C-|fC%q6Rb=C`Jsf$hs z3Xr<~N5C2zAa&CVfl_z95G3``3&B!Py$~k#(hFv(w_XUB`sf5)hQ4|sKAyks}LYUNFFPNnPIw3GX8mJcnr9pZjNE)mcf~6sPAw(Lg7tGQyy$~)9*9n+~ zBmN@<(x{Kr3xU!oy$~Xe)(fGMq8GxXF?zu)jnxa`(m0(E5Gak;3qjHZy$~!-)C(cf zB)t$SP1XzH(iEM5shyw~0;H*WAyAs87lNeedLdMrp%=oWnR>x2CF+H6X_ijFsLyr^ zG~|{1gQYooAw-(17eb|ZdLdkzuM;qF7wClmX`x;SlosiQAZf8)2$h!Tg)nKUUNB3` z^g_6_Tqj_VSLg(stxYckNGty%sFQA$UI>&{>xCd`ja~?q*6M{YX`Nm$OY8MQxU@kh zV8k}+g&=8@UI>;p>xB?$i(Uwow(5m2X`4>Kcx=}T0aB7)2$Xi{g&=9CUI>4h+9 zw_Y$yd-OuMwD&(k5Z&4P^g^JtUoV762lPUybWksZNy&P_EFIDd;ZllDz@2?qF9b

dPzPQbKG)e8aA6}=EBUDXRg(lxyhDqYtLVbTq~ zV3uy`g>dPXPQaMn)(b(>9la1N-PH>r(mlNpD&5x$VbX*D2qBgH1Eq(0AxL_p7lI{+ zUI>%Y^nzJ>tQW$iCprOR`cy9jNYC^_u=HFngh(&+La6joFN8_2^nzJ>trPH6f1?)y zrMG$^NP4Fif~EI*Aw>G17tB(+UI>>y>IBTfPkJFh`urauloo?8dLdZK&7cEH6?4kwBSzNRbIjf5nDra-i z!sYCLYXSap4i_yz&gr5B%HA$oketg!3zc)bXkl_57tJi^b7s?prT*4%Elay-L2?-vEm$t=qJ_wQE?THu&P5B8%m1z68dY%70_2J=TA*CX zMGKNEyJ*336&Ec`_IJ_Da)65#E(iXt;aUc{XaRDtUc)ySL2`(T7A%LlXd!Z#ixw)I zU9>Pc{BI4{sH%$=AXjtI0_Ey1T991BMGKZAT(mGb(nT}Nyo(ktTmIH?Eu&nt06E%4 z3zloTXd!Yf7cEq-?V^RrbzC&FTvw;z%MO1t{@DM&Yc^@&vhcTtF^zH20_6HGTA(bt zXd!X~7cEq7=%R(mu`ZffZsekc%Z>lm0t4kHE?SV>)I|%Ho4IHqa&s3gRBqv-h087f z)-dl|xo81$YrRHaqR4Grv_QG7ixz@E)Yn}P)D4x}xoBZ>dl$_tTV1qpxx?RDK%m^w zMGKNUxoE+1XBRC*?&6|_%3WQwaJk#x8s>d>7cD^U;i3h~JzcaQxtEI;D))BL!sI?K znpy7aqJ_)-{?>3Uv%xTs{X)`*ZiXd-2bX$mb!%$rnIB;aDNO z)ie7I(CG)z?=et#HE<~ja6bX$^8&7~2kKL_o>v9@J?WqD_H6|o(ECEQgMqH}n$c@| z4`^ouux18Ot{Jd099VAw?xo?ipc@Z?Nne1$c>wDSyxTHpDzN_<;NBVU_spZ$eXiz3 zw9Ex~m39E>*@2szk+Pr@u=+bt@)Ypk1p@c)0YzhhXDfM|L%C!h|zNhY8@y>4-dUI+PiG_WE+ zu=@}G#&i>U+vMpD!1NB#LHv;35fGr_7c-2}!89Id*F~HJb;M+tX=>}2`-2=|eLosa;FmnxxwdVmlAED(! zcY#K!z=5fNJP)up0uIt!Bt0$z&*>eLExq8i^Z+VRan?wn>O$cDG{Cbd(2CyIn9&fZ z!~w5uINo<;&6HzMFgFN=83LosV+csV)&?;oHz_8%Z=3DCP9w9+#H1C8$C z@6fVbh39h@=<@{l@EC}uYxKSd&T_<8cq!k2i{2;>Y7V$<1sc=!xDbxHydiC*jqeh-wP8WqX{pN9a4M?jll29zDZnNz@|n!q)Bf2J)D zJh_8|XL<;1P6Zm#87XmqJH2#Jq6D-b0l*a+mpWD8)u6xa@BIiEN)yhPdY_rzcJgnE z_7Y-%%Ljno^x9I#{=hOSwxS*^O$17XplgZMfXY3AH*{`a;()1kM88reyU{G!(*#Ja z4y0Z{^AqdCt4l8fWuxhx9ExHk8nuzs7ikHQ-wVa@gMcEPfCPF^sXD#Ca4HHGml_Kqcs8vA!robzDRme92nA!{AssKCk0}UDivj?ME zpE*EJdQ0f83~UYp>U0N6(OWqA`v8Nd0~4O%4Vhza0Y6U^-+hI5fnK{AXGO8#4IpSe zu)H?vdbR?Z<^wtx19Ex-LkPJ`052y3+lB)9==5hs1M}+v_36c_67=rPk)A+SdiN%A z7SOv00((W^jRlzL3yh_vZY)7GW;Rg%ATTQu_=nzSy807c^EeFLq?du(tOq`jHvA|s zE= z6Ao0T*}iiG&0?DJedxx{LF@gpiNH8|gC}QCU?RPG+5Ib`nJ>XR`Y$lGHSU=lU4idpUr&1MY%L9Rd;x8CQFx)B;YHCB{&Nm+gqG{w4S{Op#r=y$-Rr|!L@()GH2@=$ zp!tr5rY<+N2E)rkZ?Yb>0NLqnO50SR)*6620eDvi3emek5p9u@n`+#oQ%F{TS~-E3 zqQH#eNV!NaP8BWy6!{lpHS-JLbp+@~Yx^a-O-noks&4^8V}Ru&f$>9uj+23sysvYE zvQ&Asd7*q!-Jo1jHz*amVpq}p9S~>0#$){pc&iJ;YkCu2A;RmSz}0PlnWFcyA$r*t zc=7;6kKe%C{lK06z=gsInRt2$;J7yRrfncObGd(4{rDAlsf`8*y|W z9Hvssz4^Y_+ShyAer4(>)53X!pdQ&Y8 zzwID2Ylmp*4nURXz!Q2i`JNjNxsqPxuDczG*#msp3CyLv$*ozyA$qkrC-vD%dy~zZ zfSn7en1%*MwFN4^M)OA6PboCJztQ8j>o#D?Y#_%qV0ki-=^vn)0DQ{WuiBK zm($HNoF4ln>DBA6^tkNakESF&D#p@NV)8Sfe&x>f9 zmrLhS@h0##7U)RNkn-PwFKG^=3qt{9UOK2zA>>0F#s_?eZ=D`*Ol#2(VS^=$-fVB?N z=pkPA3Q%}F5Yrdoz7O!H zX))*!5J^Q zz^=cJXvhA*Mj5b92BK&m(w3I-+IfMFbhTR1%alg^E{#xq81R8s*K_oiWo?=k=Q;x4 zX&-VrFHkrg^Cjd5@S`A#4yp<*DAom(z@^ZkBp0WkT0V8NXkb64NDjkUa0xYL(Nq8Vo zI1GsK06h00;7JW6cLK7s1*(SttLfe7lsF)^DKIJk_%mEjGxV6Bden1&|(YEar z;Os~s(gHN6H-z6DL=CSEz=}bD?+Wr1U=?kQ`n3lV=xKbI-XG2#5480ouM}XWm!YS5 z0NrK)E9v#*ale2K%~8#(18_MOI8Se9N74(@rNV)P2%tF)MCm2KW;*0zI@&|p6LqG2 z-0VC+_L*pk+X`G>0GR!unW^Q-uE2|$z+ig&+2=i?6?Xznl7T&Y0KX2v{${|8FW6et zrx%cu9YCdj0rgGn6WI{WLc0{VLqPj_z%(!Q9z}~*QChFwO$8#>0HbJ*N7AiZgf{RSF9Y*v6_`VJ za`w@{s*k{a`fOWPeTZ1#btwrHpwG(g^cm^67mzU@d9~>y#2}rvJ_7*n9Z0!e7wA)iK=*3(AmH^e3|$u*wN!e^d3zYpoR<1(Yk|~pfIIC? zV(4S&D7-YyzCppynbJ2Xm(>kQs=7hBTMZiu^(R&W>1z~qgOZu{E05{xkp_u?xK;58J{Rg`WYc^~_&eNACmij=qa)7!)sXPZ! z^G9IdKiE7B-3Zt>V8B}ygcn{HPuc#n;r(0#IS9g3LK*ned$nj9JD(;Fnc|UKk0F_`7vPm1E?F6 z1HSN1bOh?tZXx_4E_BC-z=yNIqRa^Fqi6bzlE5jNZ|7(WVpxsKX1W3|B@a^e(&qQm z4-^N}Po3N-2qex#O6R`7;le1sSpeLjr^JL=K;Ip}l1{*TdMf^Gh=W(349NX}CUL;| z=76sW$e=AvJbAwofPe+S5IQ3_+Mu+h=k4uPKpcISk{|*f=t1IR1#&L|4$vcX?HJ&5 zb>Ib^$9TH6s?w)`u<5{$rob(lwYP3!mi40P7k?SkYL5k|vjbSY4_K24fou`*UeI0t z&k*279MG6PXPW49tNIfwOYG2$^szIDe$c?f3q>#bfV}bs8i=MJOu0>;QdW9VaWeu} z^Wfn3=b+fM5Il8*GBypxW3Pa!I{|AT0)ESZ8uX(vo#|^7(@A*44*?74))H?4-Sz@M zXgOF{1gK4GLKXU?al0{~UcoVReJi)b_+6zv$J{1B=a0RsXwt& z)E&jy^bJK!abOOmw5KO@(YnCV)quJ|sYPEf6t4w@lmUv}MqhIM2Bz)@#;2gqn|cHN z1_0aX`8}rqz|#*JjG&J>P3Z&AB3fgjF9I9Wft&|`$uVfpldj;=Ex?6Fz&P4>^|b?| zHvp|^(F>tT-HH~yqEW!$|7}q2oJ92CUZ6JZW_lF^a+U=)rlR>LKJXSEgO`_fBp<2w zV>#gB0~Biw2R3`6nBE1rRSxJ*-?Y4>wKsbMpj0%!6D$v&`59s1e z$PZK%0d<4&wIICz-=O?Bhk3ApHoh0=$&pSElGOaDYolHAw>05@iX5g0k5HAHUjrJz7Wq>mk z0Cj^B)&>vG@w6iuLq9pvC>&T61k{p%Rz-le^yF24V&&)%R8u!7`A5U!X`@h-_A49P z0PW`>&!az}Zcxr$$5T81F`yMa`x|})mLvk66_N7sGNAs%N+;^%{wQDu{celB7_gXr z2j+HRc#SIpOX+Asn*h^ja4wYwF3`qfd{!Vo{b0&5`qrl=9o(IM)})6C0slx~7!SUX`{rmf= zzp3zI9|2kDYk;!vfsILk`V%W%KVYtH-w5=jp9hNG0My$JjClws^o>GZ`cC&fE${;> z0vvro*tj=vm%hZDDdx9^M2>RyQcW=#$&)?m#xW-`3{@=JW%Uyhur+_3!&0bZt9rOPbOmIE(s{ zOdk!(R0j@TMRY|kV4MwDun^c70o3acjBJmTe|iBqZNMih@X-fQHz+=SNV)C~_!I&L z{-NK3TqkeM-#8|3qIT&w0LVg%-VvsJNR`|Gm4X`!~HDyORBTck(p^M#|U0 zzy8%b`3AH&`6fjKg`dj_d9&9s3Q6X7Zo=fF%Z$SKu$*o};)-k@g4den9>TQ37mY&f zw)RHBl>LsI@OI-Hqu~EK#VA-GmNp7rvpN|C?+Xt&A^qxSPAC$Z#Z7oWc^W5dxoCA0 zs?Uuz3KgG>6apPB@qC4kzq?Gu|jTY9BHRbDN`;xplS~ zg~{_PWD?F?OZ5=CXD{m^oC({FzASp?A*^{=(oInE?Q#>EHu`83ij1o1CRo3xxd{cH z{`3_5zSQ*;Zu{8Xc}{44^%y6dQqb|e8MQd!?XoPK5c%mWmbUpf zuzY5}iN!{oN52oX*(j`$BQYd>gWQA&{Jj#Pd%KQCp~0;VoRB*-6DMTt`xv*#Vg69C=m) zH`P91%NyiH9$bvCj#DagLR8Q7c@lid~Z*>h8|thE`^k+bKGwI2UC{eB3-Gk$MsdQg2R2TqJmWAYbaeCKgo72sammV5 z;)J?a0`Pz?)Ct$p_9v^5tyEMi^kW1T|H=38c}&4v+ka)dQLsM@@e=m$DCsGDFT6C9V7i_kvwfJ9Nq89( zpGojO?&T>sPM&ZRvh`kJ6t0h4Y7}0c|LrbpD2$sab!>5?(6kwDmoIHr8--tG6;Alx ztUpfmbW=`9{U?nRp050A6bf`2$qCaQrWpl3tv}9uumWw|+_z_XwDQ0B7|p90=m zIib|qI-HPa-bc*I58v@Y;e-d8@()E*y+lsvd8rrbZqJF8VZ}dqazFZj&#BXKk*ie2 z^l6%myJzQP%-Ht$w+RK0BHi%Nb|CO}(R6I~({YTxtxTNoIs?;j#uZ$YX5E(|+J7SE zOFm5bMH>g=Q>@qn-z1G&jc37>DR^|5F!tM4;VMiU5Rd6&#d>fc22H)_-5#4Vx7zs9 zY;0pZ&Bx$&34ezK~9kASF!>s#J7dOd~`svujKg76rea2nkza3*bY%ivAW&h9Elr@`+-t(y# zu=~fcQ<;ME9pPI5mtr+;wGQLn<3rGzVw}+XJEpvlgo|=Dt}G|4^1+SmA6dymczJNT zr*NiY4NpwHQl3KM&p>zKOjH{;;Y`(+IKko5IpIW`6fE2W=)$kWqUFe$!%aB(`49T? zAp`U3_etDaDUb1l=zK7f5PQ9Zr|>eiqPq}(_%sf5r5h(4S`^3$i*F@!g5m2WKty|q zZ7`+mlWrsL!xN+Mv-?Lp2(C54oUD#VN49&ZM#0?rxQEd4O*0QciJ$8(_$M{aDj3e! z$Rl_UU7lS~ZpvQ5?MnC1`%mlK1;-6Myb8=+;3*7I?s*7&-+<*PUDe^7#X?vTVwsm-W z7X=S_1DuIu$ywoa#wnU2JC*59CB>=yFK4JTbkOPCb~-nm$t#`8WT(@{>D17iBhGFd z&M?q_b@(Iuoz6C=)6VJC(wxJGosMcz>+mE^Nm-ydhvGHopz{QiBQ+(tqcgdm<{X}{ zIY)do$L^z$hw&+=IY+*bK#I3h`BzhpG;})7>PLQPirpEqcXBF&osL-{52x%r!NW_m z(BYC!XRXt@>U3f>C&lb^vO1mOPN$CM96ITAdO4kFr&C39l7~B;txjjP)2XRB2m3gk zdCt&s%{g>Vb5f$5&OxU$-RacVoI`e}$0!>%Px1@)RJ??)yyPrFAoDJ^bojJQSXRnu2ao(PLoom8g=QaJ`fG1bg zk`E2joCEir&Mc>+THM0>FKNnt=b7$b=T!dZ{O?lUc2>{pbaFbAo#(#aTT}LxcRJZL zXYW&|v&iWz|3AvU0xYVv`+jCHvFH>Su$vA82E_KYThuYoW8<;0#%{%KL`4h?L+`#opmC+B|O^ZkA1S$m!JcI`Qy*)y0)hMC-wNk){J^fnXoe$LRC zGn-FJMq#sZRwjEUnn@cov6V@hc`efZF((x@6Z1CS^II|R@HDfZ-(%kVnI3Yf%$73Q z>ux69%;ca<_6cTU_OyG=c{2CNNtx@+=*SxVaXP7gaOG!UEPy5Gwx-)*7^Gq<4M`jW#lk~49b>@?sr~KZBRn3`ym-?MF(wwx!OybPschU~C^1BIh zJ$W13?1X=Nt+ZF>Qa)zVGe1fDT`EXUN~>lj3Fgf2%u1S>)HjoYGTCk3y}M7F74yd4 zJ=m=1%_Kr5sYWvik;yLePS`cotVGHr|(wR?O#gcamB8A(J#;GYL17K{81%pJbR~`$z_@BH!t_W2eOiPk8LL_ z2h59`l`AV*&&^5Zj%Asr4j%!~zXrPf@iNI;E|UZ1op8`nRu0vZiP6JA4ENX#%p}-M z`k6^nnH;iJ(LdZjC(6Y5O(uuU&OA@?HBedu-C=Xbj4#cZTbPrk%E}RuE7E7SkN0nQ zmM~Ggsm53F3L%5Hw|78*pLolBE&ag^@iwlzhCXI1vzXHC zDu1PyX7R3Ugexmvn%TIr4z8@GD?8%C?l`joE-cQ4bZu!C?8e%;v7e<`!_w@nD=W|Q zB)U+4B3)TCS2oL)WxKM?u56kMTkOidx-v~^suoGeT$e>mILeHz=}AquJ&xKJu}%eCwunVmesanE$rB8 zTXxTeRkvlqwydEIGukjKTjp-Z((KqlJIbcFW&O;Vq1>@!TkY5>J9gPlt}w@*W!keI z4(yl{%>X)<=1dcw;>04H*g_}jMz9lW=ftWzu^Ue80_R3YmhHeEJ5YaSIj~3vs$sJO zi+5lSjx5!Iy>(z&4n&ZI4mY-EbL^WG+t3(wPl&VhPS{ zsWW@+#H^fHQ{Ih2d_rE@Gr^wiuxH*5>;$hr#*xaRl~_mWB>J|`i6OJUBa3om&m9>u zfUkF?IYd84I53PCeMSv69oQ#(nppG@13|Z^I1-{}h-eFW&`-=H5S_HNV|IMbpW4xI z&=W6v_MXoqCQ}+C7O0IqO)OGDPzauQ|3Hd`};HS1x`a;(@iD^|psEw*Oy*0kJHY*=Of0xx9C%Gxp)TlUk2 zMR1N?G2WJT2xdAY>rR-4S241q{wo70MH{I&w)$g`QOxo4e*)UBs|AlQ>s0l zjc(jd#Z$z}OYrQla`7yD7XB5_Km(mp@xh=g z9#NZ;dLs1O2hX2-H$*2Vw?Z2eri{aL(eXF%@#m~V_&93`p0my`-VYyzP40uIu%>4C zI9l(A&x;4+Ge-Yn^$?GL5dEt@))~(>ldB;ZVXzY(@+Q0D8EB3pMpVrf!=Dp|n34_X zUx_{lmHBcP`Lr#yAon@HiWdsIP^miYvUY&K+`UGf_p8xErg zgS#R6SHw`)4@17I;~L|m*m$%t#sOJ|-bMur?9dIpzA3^+t*YZO{hL4pJaoZ0wnwAg zFOe8vMhc)`6q-C)3GBo|D}22D#u^W%yJD2hvMONEM=D|>Vv!}SmJ8NsyA9&jwwJ_b z|9g?|N^VhnX#54RW((#Zr4h1}9pjI$53F{>XXX^+SMCu?9PNnAj~-$E-km6gb*qMk z&V8}N-Y5sHYA;+awL*!z6YcS7fu$FoQ-AQq=hWMS@E~~$2D9b`YToCEDYhDossxIP z{^5(RHOs(=JnNySuX4d2enx9GT(Jr_USKUJY{k6HyUH-=Fdyu{^O%EO7UiFw=WW|&&9}dx}ezR;v=!h z&s*a${j*u)IE;@1 z#{6oBL3hT<=>Nz7}`3H=xP!{@ZZ-~XTw-3cUJCDJJYgh*#Jk)=VsDkVg z>c$~kR%62E%*H$)=t03rs4vPR3Lh)BZi$bryP%ebozQM*YfRJM^xd~fdd$@~6IS)G zFD7SU)7tnfVL>p$b|^63=QI-fg>}F@SL}+<>JJ2AVsmR@?|Ncl%futgrhQobMLV%0 zZobDp&87&nTB|TuUgNQ)S`~#>Fb8vW@hP&zuEE5<9e_1|OwmnWzo5w-%K%?9P{J_< zB_5wdW>YwdHCT)loPHY#*5R1vm6ZU_Pr@;dMtn!@I6waOQOCD4I9F_*7y+4XBBub`9IH_z-OLi{H?O zZZ1yf|GN+5X>%(kG57v6~t#$JyWrp!WN*(6DYCe z7+PC5wF!c(8n=Oe-i)1df8Tha}Ps#r30$*YuiY>Zv z2-dC6V;rj=7clZ;I}wYKx&oikI}Fh@zHlWCpV;N%BelZC-{5V>a~(b`?zs)0=Z(6L zrQLA_{`uh=jQ99%1Z^EUf>F9$gG+n(5z|z65k5hkkB<;jZWPAn;w5h2)6H8LzU6Qa zd@_1y2l81Vvwu%q{%R8scXMq5_I(&W2~JF0jcs8#i&`pSnqrQp;d8%?T=oC`;c4+c zf2%doTdnyAuJFqP^z+l=1BicW{^CQ079c)SXo2Emg%%_}QE0*9)BGB~!2L|2`HIgK znxFVWq4|q16 ze8qnhT7an1{IS=9tN8?qYK0agY7|{E$vTh2|#~QfU5SVTBeT7Ex${ zVo`+_B3kCx@Z%E26q=7%{7;R30$VJh(0s*`3N1h^rO*OJD}@#$S}U|*(MF+#h_?AP zA3xDfq4|sU3N1i%P-uaoqe2T2o$_lqTh0p2M|4qWzM`u_^Ak%ev_R2Kp#_QV3N2Xl zP-r2dHot~-^ZY}@4@hB;dnq&@v5Z3V70W6#Ke3!b^B2o2v>>s9LJJlvDzp%>Qhp6{ zT3Mm_h*cDtzgShF1&GxYTA)~6p#_OG6k4$8onONb+W7pb@gIxwRcOAVpF;B!{S}(O z7@*LC#6X1>ECwmG5HUEvhP4b)Xg*?1h2}5TQfL8UZG{#n)=_9dVyHq3Mrhmr^>z4b z5BM=0okH^!!xfsJ7@^So#kvYDK#Wvq!D2my79!TquVLRe_*3IQeA7^&`HGDcT7cMC zp#_Rf6k3qjRG|fnOreE{&GKvbk+0?o%};Eh(EP=g3N1ivrO*P!)(R~|Y?EKZ*=noM ze8hGN%~x!%(EP*>3N29VsL+DMP6{np?5xm2#HjzH(E`zrMRieV0b*B$7ASU8Xd$9L zzlQVCU7`7iJrtU+*i)hTiMxGm8rIFA(0s&+e`@rj z!r~-_<}XfGXaV9Bg%&7IRcJxtG=&x{PS3C5|0FXMny)xhq4|lk6q>&{TcHJra}-*z zI9H*Ci1YGm*!S}lnvWQ#&;rE;3N1)nsL+DNMG7rMT>PiTe`I)xLh}=sDl~s_nL-N? zmn*bDafLz)5m)Bda6VQkG#_!bLh}{ZC^SDYUZDkwYZY3Mn4r*t#dQiTL|mU=!+LB` zXnta%Lh~0lDzpGGNudRbn-p4*xH-RuGqXjZ`G{Np)cF4qlNFk;xJ{w?iQ5%gptwV! z1&KQqTCkX+&_cvr`8BL%szUP@@ELJJnpE3^>tLVgWvc~PPHh}jCw zU%aHy0>sM-El|9o(1OIP3N2W?_J@Z558_9La}-*Lcs;*{IlZCKe8igy%~!mo&;rEU z3N28)qtJrHy9zB>yr!No8vg(BPNDgU?-iQA_?JQp z5I-oiK=GqO3lTr%*RbzDD>NVRi$cTmKZSsCym;rX9J^A}4fv;eWBLJJg2DYPKbN}&ad*7-F&ld<_f zn)yeDZ53LOXs6JEMSF!7B0A*PupW*I%|~=nX#S$JLJJUG6k4F@s?dVO(h4nDbjz>d z|2Xan4S!%kq2UiKC^Uc3Q=tWjUJ4C=U_qhb4=m)@a6ZZ@G#|12pBn$aX$6JmFIH4& z0b(VE7ARI$XhC8Xg%&JU&9C7{hN~&G5V3lG4g0xoq|gGyV1*VahA6Zkv8F-`7Hj3#@c+Tu3e8unqtN`sP=)3% zhAFfFQK!&?#c+idB1YubupV_4nvWQ%(0s*u3N1jauh0U;1_~`mY^c!i2No0>{=mYY z8vl{uCJN10Y^u=wM5fS!#bydEL~Nd4gU@N9(0s&}3e8t+rO*Pz)(S09Y@^VE#I_18 zSZt@zLd5pa{`isM4iLJC9f1|uW-SZ#WLh3GnXP5ZJ=qjbimr6v!BGf{LQIr~r_Rzc z1Y2%(XI>uo!AutC!JIr;8xL07ogn}c;gN{5>cJxz5t=xqG{spVGeRg+wG>;0ut^(U zs-y?Q3*nlG(nN)52*H*Jv+U%~dbv}W7O97N)sCl|KTh2p-}mu3&$*g`i7_p0tjk(_uLTv-uU!V|76)s+o!Ww%@@P7EBunUzbk zC?2tiOT<_reiARlWFm|cIne~-HW5*o<3h2QRb5yhkFGrDLeYb0caSs11R{tLh!{3R zFye(kLBzaC0dG(MYC9)xw z4?&E9Jp2zjf*28$h$u>g7b0$Ou>;jFg;Sz?2<+^{BPtO~`H=SnVTw1LD8>?Da!~X< zxDg?Um3hYyLWz11jEBfigxEnW?#RA!Md~?X3(;rv0bz$|9U+BMAR-79)Po8Uy?L3p zi%?aB;bAt=7DV(4fq9q^Oa;2yjEDJYcq^D0#P1>c6fu4Xsze|qN`2v>edq>a{7?je zlxG|%6cBwwuh35f3If3qMTv+(OetzZap{F{N?anG(#C-eaG-8u($IfIR3a7>bBl?^ z^kVjo@PJ8h1W8JH61fgUI3+?Br`gjyW7HTjmHeUij?1=bPU1M7(Wj5y(Yr77%rkUQ(=Mq$Z_NygDb zfHY1twi(Vv1V3L0ct)%>&a%HJg;N4`9_+n4o#Gi@EW(Se_hkFL$Z6p6wHF&zhJ}{@ zl>Y1ZgAg`mrm^;w}aL2b&)+At{s-O9Gw{sVz}WU^8akrf(HTqyhL3ZN-l8qE0{?@#f~!HJm@5>=G@_GmM-UOzjXA{O z0zn4!4bzBY1^)xbg2M%llZaW>II`uu2o58397aqFPAIZrCgCpNe=z61W{&}%Be@z7 zB$ecj1=$eGjaM8O(N%DSb7Pq>;UMroQXW)=H9~MW5IzvaLEs!D|AY0y3qE8RUsK8d zAeLM5KZxaq|B+(3CI5qXZz+}=!Q1dTKomz@H;ClQuq)uuuwn3KBsm|97uyLtqT8X~JQa=t&IjKj!Cer(2C~D0P2D!l zc~U`mFgPD{8}0=T4b`GKAil1Gn;+DW;7(o?skmi=^TF8RdvQMlf`Ow>Abc9!1^f@( z2s{H`NQKiw5qxh2-*dUaoewf2C!7ynm^a)A;r<8i3*H7E4BiF`Zl&O|;HFR;`iy%d zxE+*25;DNQ!AD^@a5%X6f#<;+J=O>}Kj3)dhV#MO2No1B5WKL^IASS|q?=^q#!=awUoeyq=u-VWncpmA6dL(avcSGE1V8{06H*EMe z2ks^CwS{y1NTLrofVfY9F9N|K74696EH6yd+646h1OK_Ylcph-nX5?oO zoxjSzdx8UqsYViJTXLJ|C*~6#8-62`-{D|Hm_z7rJ{S?^96DZ-Q-be;lf(cqWOyFD zs`47(hmjMP_<~6<{_cPykK(vTlAHt9QF=Fl$H3P>;7s6rv^Ff;nw_v_HM!?0Zo}Yk za9;%XvWt1Pm|@%`fpRb$qbn;ZOD}2y6u^V|J!Lo~}Co-#` zN+*1BgE3eQEDfWYv04kGum8r8()!1fNht8u8OBu2mW(PqoC@goW|2`PRdI(g?%Y6W z3E=@$z}T#*1B|I*fGwGoWvFWX+=(`1X2#gVXgii7qxv$DvUs=bQ3}R*u_DiM)83k{ zo|UzN(SI3i2cw$b)s?PX9e7_VhdAL%dy-Lm`k9Tnxm$zR>1#nIVcbP3uJX;G%1q^| zk?~#-&KpX~D2Xfm_~DdAYwAFi>AruX)>T3{8DpubWULmBqnuX3f0ckSSQH@>ljBLo zxO`hN82$1AltpXvgT$1-MihtfUTH(dP~1qyxIj&5R^w(<0jpyz$aoK^OeUe<3>u&Q z-eBtB``k;^!IUi-RKQU50k16N8yVff`dloYWl34iH7N>{y=)D&p73fjO{BJ189txW z7V!3l4<=)L#V0bRXLqVa1UniX4Ns8YlE2O0b^|6fX{sYt~6QN!Oy9jZlw>O$YM{aK*B078jo2Kb%=AEP7{*S_B@}ZOG#w3?Z0v}?ih7fJL>`&tA&mDOmxYnatXaE zQ?I?}bl`&yZA}HrTWqc>e;_Z>-7t~=sOQ(&0dnC2{H)FZx(W1o{zdHtlE#tQ{$vUCGaa`WLnr5Pu0 zBx71(OPzn;-iNBTa%o0AQZ8zQZva}pV zUnde{#tx>OhSq&3&Ctt0X}Wop_~Mu<@nyW;iTY{i)0-dkG4p6^s6IF4UgOd8oXxv+!vKUJ}1* zhYyrR>zGPi(v2I?&~ z$MB=}rV5Fwu=~{Ogs(}|Y{K*l)R2CXnnc~AdL(LDE+1Hx9%S@mnp4kI+ve~B8;(+Y zhLVrzO1GGgJ>iZIujR}eYA>O(k+SIh=km&eK5%or7SEFSnr0$q;d+{^Y`;fjRON4w zXt=K7ri6*+Ol1h-EsQQsty@h$$M;L7r8JR}n{ndDDZ54%70^CDLdN8+q1~!$Kb`ij zw{tpOy*Kj3vFhSZ#Z?Pz$Yi(kp#oN^S~5m;EHz~6S%yr)xLzcBZyiiVTWJ=biQQJz z-4utFyr09}DXqo0^CZR=&Q+<|ge7usGI6Ia@wCuDayr^W z73fN}#g>eA$WUs?c)Ymofbzf1OmN?~jbLybZ#f8u3i=uR8*qFsv^V=-yu5MRF zDxPqy3>mAyIGVHUn=VwfVajTrHt8cZtIzn0w?3vgl~YZ1pvtr_2T~U8bbg!+UmW>N z)C#02(CzugYdLe1dS<-!59Mr;@R^KhZ&NDn-R2Z!d0)?(jDFA}YCWdVV!F~r=hBsS z>^d?DwVin_o}bCYj44jF#BE$e?Pa@YsJ#}A_(q9IGEf2U>OUw;g8gW!Ocm=y<+MG| zkxALsgVIv2b)pf)HCs=nMdm^>DOLNBQPntJn9_1;Q2|5qTXdDO;02}4n7fCkJ>gw4 zy(~)_cln=y0EV+UJS9T%l9s!E_GS=q6E@^NVi% z3`L7j0qvEUWONN%aL+ezFXc2?E|Th8NH+`G<|({`M}lb#3G3|X2GaD8H5qMGNiJ@# zMOTK5$z@=SQ)8p*py4!QA@GcAdWpT3f4ytkO=jGoD-AS;@h)jC^HEyfOyXjKxr>-l>}{3m{8!`~FO z#~BM91m-%O#h$&wP3hWp4aJJm`p;mxg;8+`5i=y<@U5S^jWj`4tcPYpPblS zy2`#J@Kanbz#&>F-bL(AxBG0Nh6x&$sbLlxrqQr)4J)HzgEVZMh8Z-htcLxiW{=ct zt(q;+u$^jlTg|f7?4X);QL{B_7OQ4aT$!z5(Hho5!~8X@j)oP}uv|5Z=daaj7OiHz z)NH((_0X^rYPLho_N!S(HEX73b<`|e%{+KL5o%UN&B~}*Ej6pJW=?8WTFpwR**7)Y zt!6WLeq@eRvksKhnItd*LbSF_e?Hb%{y)ohiTmC~@m8g>sYYuGb2 zjSubHTQH_!r_}7EnjKS9H_@+aYIa)9Trp@hTc>7TYF0_j%Bop4H7lfMMR_;(s#%bR zT1FoWtDC%K71T|hGti=~E5*048fmaJw;YKD>a<2$0UnijVkpH@u5 zbTu2vCkL}!LCvsJut7Z4%z_bUg5kO*ed8lV|9}+ ztgf2*@>gYyl}`Z1bV$t-)$ECyE#-CFt66W}FK=~|znP_)E#@ma>!e|wHSD>XtyHtom~spDM#JW7*h|!*Va|N7VML=eY>kF> z)UYLdwYzIrtcDHOu>Kl06`3`xodt8WV7)bLnuevrp;$0I+<^tdu0fGo@G=%`qy-yq z!Ma$m!WOh8`)ZhvhPBbK(Ha(@VUM`SIS%ijVV~4&riS&=uv&ayU)HcQ8uA6$K1jX9 zd)rz=E(w#2jgPj_+jnYauVJs%v_o;!W~u3{V@G80{e=NyAGJ}l0sQcF=A@b?x&!?} z-;*`dpY1#JWanNyDS5GKn%Dy_a-7%`*uj()%O z<0_H3Yb#v;AyU-gbiip4&Ct?Ei}VChw>Awbx3xjI=GV7!GY+Cr&G(Fln}lf~Un8ef ziMk7tdk=rDw+AT|?tK0Hos{#d<}5dWe7WFMyof)@=h-7wL&8Bm-A)WUkPGr*dC2$Z z?jUbAmI#__3-aXMl!+rRgFO7{=CQvC$fFWJi|mg8dHQR%OV3D<7qOAU-LHYXwl8z+ z%_1VPHEOxF0(qI!dHVjx-fyghn9J@{+H)ofc{j>uqnJ4-OZUDLZzUZ}1$sm{0 z##CPX4CK<;=2t`pl3l;-oyoLJ=f6#6j;lbb z2#gF1NP5Z1&-IiPn=z!2CCJTE57@76AlFhC^(b@_fGzLi|4Z#$5S59}8O&{AG3P~nO^MW=r#{gUn$4^fgE`J#*yI9FLYz)Yr7v-)_qnh_TntOIh z8YN90JE67<$nG_RJPdz>q~>0mI?jT~+76QTZl+pg}g5U}? zyOJh{F*<`OpF;c3r9-r{a%A5ga59>muP)uVPW9{wUR?f!2gv@Ky$?L3J$CS1wTiAZ z_JgCFZ##5^lE#M|8eSb_-?KJ_OEv?^vhA_RM91?`k2LjKTHM1~SJw@{3UVZKS^J;g ziM&2IsNZf%YEWV7+j&GPk2_bkF(vI9`=m;FkmGevC8wx~^gDQ`kS&oU$G^JKX*w}5 zs&Z*Mu%`}g-Mi^1$m#em1vPhxjQzXKa9a1%**Tjk(Iz=FwBD~=@=<3!f7Gf^8{q8O zTW81j2014#{?zt0$c4hL-a|TrTuj>Vc;_LI%ax07>~{m?YQ}|C?Kgwu%)EJO8?9-M zN$b3vADQ$PHyr6)TsvFp?h0~oIdgA>xzliRdQIImmKN^Hw>6zB(zIL-u5MJ%CvxQ7 z!bi(NuFecBe2;uWPJwbG3pNC~)~oZ&8no*!zey}Kd>Kggq`OupyMSE0ZP-1aA1O7* z9`~cRF0PvK{7fN`3k|ma@~4S9cOiP;ju{|l`}{C$r)_+u;hSE?i-DYc9oX`IL6D>S zuGX%w2INrd{ng#cSsiR&tk)s(kjC!E?o`MCIsCH7`zzTXN7}~5&Ra{QmdEHh{vgL% z)Vg|h6v)ZO)#^5<={=oQaa~gfkTWjd&zz^-a%M_n+sZVGvpbS)zf=afU|FNYq(qR5 z9nZdRNi%kFb9Ck4Q$)_3?(#DlB)dt(vgiXuUbo3Q)(7PB@Q}OjM}u5>lyv@1Taar% zdggAWJUKgWXRb;Dxn9xtWuK;$^hDnw=mf~s(Bwh6!$2-OTR#3pN!d?NO>%h$a?#Pr zv-2#F^A?B4s4jz?eL6R&9Zl5PqUW8QmV%twuxVWEHX;efKcApedA4>=scIXEBsQ#A zj&|>v=%C2=Q6Q%cR{qauTxV8qJ@{@N$hp+R_Y0HzI{&zeE}FL1`Q!yn>d=lllRc~9 zF>33Sbsz7&=RuBpcIZ=)Hp#JiW1Ft70dmxD%>2{zmULw5^3=FDAcyb%3jB0|l$|>} z460A$x`mjw73652+^sLC5jppve)>(2V+H|GwD{!qG?@o0)_+?l z9%Ns?vOn(-$#{7+aVZ_E^x-!bo!UdhYi(Qs+CS;@JC^sPdCtgeb)tz0By--gi)rmh znRKjLJ!&-lOvgdxDuJYJ&fa594kUG5(~~~5pHsTM^73{hQe;o$WIf0(RfDFVtw>4k z*7Ysbyt7|;fj0ukj%H8JzoOynbSyvPryu3f%pX;C6)Em5U!7kGvb|;9eg>M}RS|fJl!4!}!PH$dLpi{Xi=Y+%X=|nt}{XA&U{Ft?>)YUfo$)Te#wCx?2eJ$@mB&sb_SMn?e>xsgXU$CZF{Rn!|6}SQ2Tl zm!0YdviiWCg*nBEbnWeOb_>Ybwl`larX!OW>FfAx9LQ$3Va=-3f!*RWWy5gVNSpPM zf`1(%v)XuH=mL^-e?>~csUV5&;+r?*1{1y;H*KMPxaRN2B@2+BUv06zNXfz=D?apy zd_!xoVqBT4nbi97GvA*ChLR%WzFSBmUA5dM!qE+6t@WvDapZf~PwCw-aVSXQ%ZlwI zy+D!%cpO?nTQq5X{gYY$fNVTd{CHm)^2QG5y~mIv+E`uOc7WcJk_MLYi<$|tS$%QL zu*D!-0>V@SBZ)Y4Zg_VPC55k={o)tMrn=c{Q)xqPEF5*ZGfnvVu$0LkXmQtGT@W&oI8$==klXFlD#T` zEE{EbSfV45HQ%jF%ZOypD6xXv>GDxWF7>6iiRFVz6|(+8Nkd=liCqh__-cpNZg)Ty ze7hPSI}jvp&Q$xZloVIsO1g>KiYxRvu;Chzg$|B6U7vz18Ft*QS^&t3d!8M4(b(4v zf1do;PLQ>~a;JE01z9({O!wM#h!n`W`C}Hyh8lC{j-<`DsZFibT{eJh^DLZbOKxyS z*9pv<_WX_&^M^-LS9h$ksyLnU?C4TsLoH8`ooDN&N0Jjxo$j>ai95)i_EWAt%K=G$ zyST&6gCLn7UW;Ssq-0g?V)KcHbMT#IqgpiNL!UBRA0Q8KXv4wEqsi?Y+LYX5OHay^ z*7Ng-W2&v)OGS~=_hsYdFGwjcztb)ckmD^p zKifDEi617^qxCxOIP3f%n$@G<-ptx@59HXKPTH?KKu(NbS7Q;Kf-@f5H(ApA#kqqO znp(wzT=;RT$$RSarEv}1PSCinwA#e7N`PF=`p`6(w&+zMsAV7O*wr~r-8$%qG_SIL z3bl2m*7DTE-XNFe)qUf*9OV4g!4U%tAZH5ps#uh2KJhW|yd|yIal3jfkVblJZPxAw z5v1&iTsx;R$f-!(mASP2PA)$)?;Fp&)qBgW36%N9i(YTZ8=shxRkKAI%2PY}?@x3* zk6#U*IDQ-Dsp)b!k(ToCkqY-?sjG+Ls%{PL3vw`d&)Hv;`M|O1pU#mFKj1uHy^T6{ z;D@WNC#}U{ec?vSAAuZu*xyv=7RZU2H)9J1f*jwrzu*>{gkuYz*45Lkn(?sV`n-!E z2Nq^l?@1nDU+A&giOwKiFCUIOKpvOWROV8PNPH~*MOy@eOqK@M}1jYGFvAl%}tYu{-m5#_=S-tMO^JI zCzX>m>3f-XM|O@33e+B2#aa$Xfo3+fd#fmX*tEbyCup z7+A{OAw^b%cQWY_ZIqInmKY>bBwHfkpQq_0#r>6}M3gN8f$Ev~LRMzCmX+n}Wzx>o z2!=e>Hc2G>Rdt!{yCsvSdYy{r3HCHdM2Is=WWg_)T)8BZoFhn~3Xc~yNaR*|iPSA; zUdjeevQj-q$Me)JJH{xHpcn%u5x<7X|W3^C93V&G-0*Z$p znyQn?aW`2xF~uk;Tf<~B|D2rEXoXHnDp6iibjLRsxuQGop_9nNID5XX%Nts zE@^?R9Jpqbl)Z&yW#waoq)e)1l*r8TI*FuqG)N>pRaRomiYOl<-J!iko+sSrEi8?z zm#eI_4cAFZ*iKmL#_L1_C*d_(870y$SXTOk$jaIdva)2ntc(m-;U8-59wjG@mp5d1 zN_U-trGyqVRk2J`8`Bh!iUI>6(-$LgOZrf=))B6j8c> zOkB6fNnu}PrPFj|LGcK~6*(#SkesyDTzvTmSsBw&CYvkCWZwyy80M((5A}qb>*=L8 zNXq;cMu{w|p_52bDT73&nYUKuT5?ioADMJoFB4suOsdGvD#Ej?QN`O*6@sO)znQ6% z$j8C5vT>wL>K~R#?N1jT>EoSJXJdp?GB!|-mu7v;0m6gIxBodx$DU&BJkcIkVX|AWl zNds5HzlIqla<93pn65*h%pP(*5x#Y0#nDM8CAs#Ml<*_{4O|J|7^;&nb_?_7PO^%z28h8 z86@S#BBM+uN+jIEob-1m9Zw1`AlIzB{ZmqOk7mmxJqKB+=Bf8&GV_d4Qq~TT$)+rs z%nCM0N&QHmQj;ghWSRUfqg%SqASLy9qLWCgcv)%j-Y6;I9x~CrkdrDO)JaJ;L2{CN zNtu+bh!kqeucl1eZeEgqk=FHKNvR|6Rh?%wImu(J zoK$kAOx)I+lV-?@vsNaB3P>bud2s_+gYdBNSuz>)Lnh$?NTH-!du65hLRo21MkX!= zWfIa>Cf}?iqN`iX0EVvmPtr-G_E?#e|8A6&pcSx`r@(FqoFMJJI-qhzxDs8Ld~_RA#gx}3Dxd~dUU60%V7$*FSE z9xIuAd@LnJIF&Gh!O#2H$x71#21%LH%qWrAM7X9ASnxGOG^0M`m%D{K_+K= z>yU;Dr9Zd@l6W5iDKm;2CDMMWK_Yd(!BSF3Pnq~^F-l5RZY0SHu&O5XFj z2bGOn(PdPWl5|NmWO5)^CTXW+@_B&{ELyPYBKEexN7aoBrQ=7%g9iZ;*8#?l0el+> z*!MSnsNGlsY~(z^&h>!EYXG+;fX@P8Wo3X{ zVPLBbfM!>3f57aofPu4+aJC@Y7!nKEI0A6J1z@)qz%v3pEz$wd ztu7#~9N_SGB-El2h3p43cZ9Zc5Ws01F5RX9yw0G~jQId@FkpNvV0u+RNq<0XU%<;4 zK#N}J!PQ8xc_+bYP5~6t0lstrq)b%uQ(B<52x(f`(sk z*<}l0$t1wbq0nZI2253es*yOr^&FQ$T zAOf0g0F1Z^5Z(h0mqhBP&R`8)06}E{HM;@2g#)G)20XL@Jf|Mi+XaX@4!C&{-K`V^ zh^`Nq*a^@l7qF6sx^X0+=NW)H2$_u+0YL`mJGpiw!%LpQ+RpYcZ@=GlPFiv^UYuC=ZJxKtfb)fW&K zNNhJ!-P;jRrQ7eJ*!8o3QY!$vZUV+m0xX^jaQ8x%zo@=|qkzKps910DJKs z5W5`EaumR71Yn9cV1OsU_ZxcUKO1m*DB#RuK++JvsI7q4Cje|ez}OYgmi~%I*LV0s zA4yLDKkoyq4*^`PkkI5D!1*EInE^GYECpPj4#*w=xJZqwa?q>0djU1>1D>S<8lMCl zJ`bpV1WjHR0aJDu!aO+cG<@Ny< zKL9kT3T?O>AlVXNd#5dstDNi2=Fu+;76yn-wd$O>wtQ< z0c(Q*-sI8!sI}!a0R>tE?!QN+5#Ip*bS_`U0cw!%T)Y8b9DvJ`l>nVv0ty!c{F;Y^ z!`%UY6WF%}ysZqlUJ_731njPdeAin6%Jv4tUx3#04WO|NfN22YPt4G=2Y|BI0O9ok zRW1N79Rc`|=CA=UfZhX~iUShs0IvD~iY)fTjfhPbu@BvdAo#deuQ<)Dg4#=Sdc)`j zj7*2okG`XZNr?ZhA~Q2YM<(Na85q^n2ZLaYY@q>0m#m*crqjuaF#6edKB-ZevCT6W z)v@vG>B_IF4g|cyFXjwOd%#;zwDDBF2J&duyn6@xh>qbVx z=nrTb!o;oURgR2leNPx&GqDj}4IS?TlQ462B^Z-uL?}s+$%eiV4Fgzb7~>|*Eg0`(kE38zpTA6nX|X6x3!~Q!NrEvBIN*kI zs;+}+QB_X9uORAH-I$Lno!6evxKdeNx5ib%?hV5z%bSwbEzr=Tz-}ZSvCp9bA70Z^ zsV-H#3}gHnTo`F_Hi5I~>SpD~FbSV(-GDKiT~QTjDHZO_#+9~etsO9`=S8bfTG<3& z5}!1x29xmWP9TiwNcWO3dglc;bk$-$Er20%!zmb5VO3L9=Dq4hFG|Y@34+OfveTN< zbSHB$OUBL@s7~GPj&EtsCf9roqaQ4GA#rq8AsEx^!mVJ8*V@oZ8@{dW0He1aQxirv zTUP@nE_n%U)0Abyf?*6*K3l-F(2cZ$dA~ZR0*vm?UQ0^TtPUWvcN*;wea|wqlXdYE zXheqKX{TYdw$iGl-6$75W4ahau-IsJa;>c%HYShjeB33C4H}pyw^|b)~Bk^9iW|M zvW@#mIa{9kN@iL2=@1jxb05mGBQAxmN-VttV{|L-hM7?9-&`EVSYb7#srn!FfH7_T z_MNg^#!uCw^$2e=`uW>uka+WF42gix*$ZY_c8|tU_QPT&;aN^El%F;S%JY5AejHavWYsrb5x__lj ztA8!5fKipI+84&)8q*sl+kR{WOv=`HIyt85I+|;hZf-4@gwyrC$qfBSSqu+?2f-L0 zWQZ`{K?jDzn4S%51fwc3r6Y{NtreYjqh>d)QH#y%s7~$Du5^l3Rp-5c!51&v(^cV! zg><#|HTj9`raR}582*?~NX?lOC~eH0CS;1Xj3o1Jh9``7Y9RH@WYdS7j>`TiKgHA6 zhLXs3>tM2frO=hChC4Tfg6WWGpSPhd864WBlDXN8PO7o&q-8J&QQrciwLU^!GPayh z4@NgjR{|zs-_jy5F=4|UVD#2yXw$|`X-6~foivq9-0593kvh+bv<~`N-Ko2}8$RUZ zbfIl&LmM=f^iGm6pdZbALR&*Sn3VWR3f!N17J);OD4C_xlh{FQRCp z7)zDAPFb=a&^AqI()J{-3HN)RdP*3s;+zZ8*i{`b_JGk9D)yGL zShl47q8;0Z_Gf}kD6NCx;oI&os@m^pPgo81tqY?a`=%vK%thA$bmg?!9mcewJ^5?n zhUpbx5@uGRccAx=&;7!Cht-QEbtp^yt)*bRo7VG#F;snSNoLtJs;oudqEy^ewIMG) zqI7Fqsg{lz43jeA{7@KU8z=H^x~DUq;3~nk*A85nu4L^bamfsNi%d8W#)osgWSSF4H_xX1)k0yk>Ce8x80KjHMrEphg!3>u?*k4HRmn4-kvQCo-k=Q* zF0`Q&PE9wFxa?s%OkDrL^XMw<3vF*>w=;KOv{h>?gE1VrN1`q(VXh`xuIwCQ20}xjk8=O3>QWSBZX1IXdzY@Ba9Ws3FCzcf0&h3Acqi!d>B> za9=P94}^!pBjK^|M0hGZ6P^n%gqOlA;kEEacq_aU-V1*TAB2y>C*ia3MaUJt3g3k9 z!Vlr6@Jskx_@|#rty1+<6KDu52nrAsBq&5sn4kzjQ36YXVg$trN)VJJC`DjJU`=2{ zU`t>}U{Byc;7H&^;7s5`;7U-Mz>UD2z=J?b;7Q;`P==r^K{5>z6nOi+cO zDnT`Z>I5|iya{{=dZZ-PDq0zqGbegypq1`rG+7(_6bAckNFfk-fvU>L!0f)NBG z2}Ti&CWs{%Lok+L9Km>k2?Pd$i3F1fCKF5{m`X5>U^>AJf|&%f2xb$^A(%@rk6=DQ z9KiyDg#?QT785KXSW2*rU^&4Gf|UfT2v!rUA&4hfOOQaYj$l2(27*L_jRZ*qn+P@& zY$4c6kW8?RU^~GMf}I2@1iJ`Q33d~t5$qvIC&(bkB-l%^k6=GR7Qq36g9L{Nj0A@X zju0FrI7V=s-~_=*f>Q*i3C;kbJJ{pnB3__ZufDv?yb}Mt#u9zqgw&bG{Mf_29qf%bU)Bt0142mpPC9$1HgX zdE@x+eaTmXXU;48A7_mx27hW&<*x01EU%T+0#)bGdAX z)bi)Rs_g>2F9FC&MwVqg0UN#o%A5e)eTIZPw*hV~07okUwp9hpQURP6Bg>-8_-t`( zD&YSyb|r8*W$!;ilnId~Tb2^iR4Qetx3Zo*WEo=}*-h4Y?R%DJQ=y1L_H4sau;A0T~&IPQi0oeZ^-W2xaQx-Yb z0Jgs&ZxNrTiKzv+(grY_PtiDZ1Q?e=<#vBS-R8JFa|+PM94hsi0ZtwTtlkD#kOtW4 zj>22F2iS8JasvQKZh$`*0n)BP<;ZQo*?3$&SOtjLgv$=`fW7y@Y;z0H{Sx4CBtRbz z$m$Nr;L{V|Tm%^TY{kyeNcAZTXu+3DX9HR%0PaKq%Jl;D<#P(r-2g300qS?e>&!DR z0Ds>EyuAo;91rjs0GO}|P;WHg=x2a6bpX?wF~k6zYyiW~;Jx#ArIGse4?tQrNH6eP z z$NmKTXbP2y?~tmn5^(!B7Ssn^D+gHV0;t*-u(cmx;`iuv4c`JRYT+`xH^93F;MPv$ z)ja|DDgm&60$}z`z%3U*k6M7B5r7MP`lL-HVABJD+Y`Xoe*soFg5@bwSu#P}J%9#Z z0#3IE1hfa>XUEa?dH{Ty0-U`8ou1%?MxCz#ej5N2djazKh)hFoK;&pZsm}p>Y5=-+ z11y^gv$4wo!F)RAmL9O9C7|OdKx00AQe!M&>JmVx0VhLFJ^?tC!{u*(Aa#z9n=A;z zWvy#~mRkX9JHWPFUqG+wfMN9jmC6C8F<3PKc{v4R&)k*;R8br_{_=i zV1NamIf-2g7*hv=g9Lc&3y5h5n9o$#5+My(256Q6Sn31#o6qrFc?;Ld9s^wGLooff z0-kZ+%oBiV&j9IX0RIdIRILlhevOkb*F2E=Di^7djgV?o3RDNUs<$R1mGKiGFa@yX z2f(IDfCrc0#`Qk{w*LWq{slCsj>h}?Hd5z*2R!~Bm$U8weoO&us)}xQW*s1ePvp7X z2N-$2s+bOlxCuDm1!%*)eeW#p#oXn`^1!y_`F_nJzydzGQYjd)h>skO`UBDm7mzyf zFCelX#*F0%K$m>LTRy)Q%*)O$JHU`!zjd^qij31C(l@*2)Xo-*CEpNdpfKEZjy7w{RMB2111wBH0MbqY|~31Gu#V!Q`H z#fmMia}_c}0PQOQ0_p;y>qF%{9~i1#6Hw=0#46?$p!{*bc%IuY@Gxy~571^Oz#{;# zb~a$)bij}>KqFm4li|VVmA!~w*^3M`!5(94F7^`}Ya`X`I#M0dgh)=9C3gBevj;hSf-87Q-ebf!1oYv>{U~>oo4h#Pasm817T{zGNZ1TPNMpe5 z18Br*d>HEizN{d={2Efb!GO<=fQNke_O>aCT+gSKJM9Jp900u93y9-w$c?3dBYb+d z68jv)+mIdG0echp@&O3u3;;Mj0)1)TI)!jY{*zbQ5vhR0WdO^c0c$e>W`6^|5P&}| z0k7iVpBW!+UCZNSCa>^~_;m3fyfTg&&%KXVh55W7gc$%6TOiM-G2kUH2_ zP=GTp4gTB(lj8aEI^anlU4_zP%ew(p_@wUKt56nvl-s8*U=??eg>wMu zX8>z?Z&T(Fz?r+nlp_FF4*R}QfaBcyPu{|zDOHeiWXJ#IL#sh?fWRiWEYIgvBbotv z@~-4OFWMKmTALkUanTA8%B@wC&j2?Z1DL~Y`NvOy&^v%NoZ4^`kaHYh*$ZAh;12w7 z0zlb|M2|x1CeLqsc=r3|E}%S{E#m{eSuTLH0|DM#q+c08iWguY@2O6>1A<<_q1XyY z9bX8DoC8?Gv*c6`?AEK04w(qprUwLt0sMH&F@R_64%GldxV8Fr2b9Jatwfs{fM-0B zp5=pt9k^SZ9Rhg6TaF*90ct--e{uf{@K-HdW@8!?_RRr#yeqEq4-2jUmbU>sb_cjE z0)$2Z>JJ5cWe!NM3#bo^qx*uZoXEC5EwF_`@CBV}ed7jMdGXjv?4ltDu_*y@O)NwDM zXC~ml0f56`K>C+}=vUZ9bm23$nc09A{{oc5#-}PmYR(%H(<6XEodHpvfJ}eB>K@_^SX zpx1%#7XIV2xOa8~h93ef=OVB2_XNJ>p*xIk8M^USqWc3tJl~%^c7|wxU>LJhPNRBe8)Kl=Yog#sbEs@l81lsaw7I5v-OjJ{95%VF?i%7De8A8Napwrel8QOZuOI*HrH;+ zM1O;`6iq5MS@KNSR#qlPo}D3esP9>sXq_G_l}$(LltjA={#i1yWp$uTBs)m+cAL%( zS9sd3s~4=8w~MjbE)z2zoR*^T`K@J6z~dmr-2dMym1SaaLu*CHzh56)nbUuSbk%=l zTj`Jg(Q(q;-*8lx<^MQaRlYJ#I`6+~M}$&@{|L9|k|z+OngiuO@Q-Yn2pgUub@mMk zkvTuQPL`#g3`mwaTf%LmPQpLPVDs67Rpu_cbTX&%SGG#R@2zc?%qe?Jb*y$@S!MsS zTO_mJ?KxyREBb;&hy>yWjbPK~m?$c3B<2h^4Ad zaDSC~V8;-t(=jYiChV^!%S4%gN0PAA_tvt|Dnnb#9RHEAQfKsk88T6oF z45+{Jvse=%sE;^6>THQ<2yUeAVr4q-mno4-}$!kT~xA7`Fndr44OR@3|^eZcK99V)G z$=BBRv`l>d+Fd3Ftdjot&RV4^M_5G2utZ&ZEp?{!R0B2XoXQ-&y}cCeGL=>K-CZ|L z7P_~)N_f=zt>n4eB}j_$!z;@~ZmNwGJ@LCHd0yMwO3}X=Iwj#(zLMnetJ2n7QTA&R zFJ!`d3sksg4q35X<}4m6dHmL&QxjYE%h377|1?}F!f(YJSqr}n<(^CPb-Nx(^I2Ob z$ef{rRjbj>17*&_6ROT?cQt3J{Q*fhwqTfx$E_ug?@AdKzYY&xOC4{sOOmI#gPK#l zi<&6C%vllnzPK8pnEPJ78LwFRu6z(AMg1>GpMAUP4N??rt44lqX;o+8@yb$pLGNUl zSeF|j6F*jzKKnjypz=Hql>YesQ7uFITyByq)UQdFTGQ5pZb^~%anNv_b?O4-*YPsE zKu7WmybvlABQ6N3JTrK*%vp8NP9`?K&X(reFT~57gelSuzmTZa(rWOASg8{@*I6b! zwySQ`@>V^puo%|dd;b|Bm3{v_w_P#!{d1E}TA8#=mWi^z220Va!R9iN`%77A{%7!J z=|;8h)1oezOn11x$okFu0ChrHEABc zG+rhmcea*R%f}y(ISIGg%S7VsM^d!1W1!-O@ALyUGAH0~TbWbqu*}iDnHR5E=?vb( zWg`2vxuUEyMqg5tb+2<&pG%cGEp=YjmpQupRhwnb`6W83ysvbuOvLY(9_p3`gi4(? zS285?s!%l%9*`xKXPQlriO|~8XI<<&WT0>8LZ_aUR^z&Cmx(?e?vk+U+8HvZL$_q9 z>^bkP%yC$iC3EWUhAtXcS2tW0HT{0I)M<0CvefzN*a4|C?(`!mnq66CUbLf`nlo3m z3Qkga=+1Ms&>WS=VW6|jsrArW64ttJClfViWl2$;C2EdUC3i*S^C(4%eD?mFt?2lK zRME-AsFU$hr`zjb#SNd%S*lJz8R@D|+xtR_+|IxV5%ZZnO#1BeXJmwQ-qHG&On4oh zpy=p6AAC(=*11d$kvVn7KaxBz?>dzAG<3$ z-uG;;$;5AEtd)dMgWrNB;gs7tX@1T`n(IvOy;gK|)lUT~B3-R9XQhXhSJa#e9fwOD zGxyd~RQ*zAsoX5RpgMUfq32*#xy8N7(yGBgA+5@GaaVGDPJb04{Rv-YE)&y^1k1#R zsaev)hn01*h$=1AvT8k2+pF4hNI1OjW`xMZxqjCq^X4lmbL1SgtjT>LL9g>^*ZHh0 z!tyVf<9&C+YenRJtmk$`=GB)v{#H%uCKPG3U%`#UI_A+pne0%;SMl6zzilc?j>>sfw;I*erD(Piv-F z`Id8?py>Fzw41Dme0$H3gucUihe%P7g{+QmkSibWX_7qHj+7U zy|pA<9-$VIcs*I_>`E0n83>Etw=Yjip1yZNWKP3LYE7RtoFP3t z1r23w+CC{z(ed5nR9PnWHBxJOeS;c=^MR7bcW*>lX|>h&mgG66-i-L32|O!v_Fs^W z`K~>rD$jb5Aw`2qy_Ja`k3%I-2X(~xwoQL6a|(Y{Zt9lpQhDnsmGD?{vh;B8E?b${ zI9n$Z<2?>Y^LDlDWTM6tYniafgbKpqTlRqTS@$BWy&}>*IiIG8bdR!L%bdT4I?J3F z7OLphx6)_b@etK~UzJNz=YYK|OLuv9Ke#%4qH?A(Cq3M_ax?zYgnFzpGB$VqVf_?S zKANm|6JNTK_05fHTyDB z?hN@NO|IFyNqI}i7i)6OUah|yuf}gE)Aixn@5@+E0gk zr6$+x)&4UR@&rw;+0T*ovmsxl$u;|GQa%sz)tX$hSL3@7@omD$?`9@8y*{ku{3i&2YuGv?S_B$ZotjRTdwSGGx-=fJiduM5X0P?MxT(eipPlr53 zlWX=W|6$0tX>!eeoa8?X`F2gN*;kkHtB~)|t;sd}@=~4$d738I>?cV1GsyR7a?QS+l)r#{uO`>*)$#WR@_m|I zvse4iUy$$Dbr7R!G+r zG>Ymfg*9s&)-*H<=>-0+7!)!z1&yMfN>P_JGBpj2LiRKKH8Ut2(G)a_1}cRuYaG=y zGzvKt;SZld;h3gSND-ixVaE!`H3f}9Rj_A;EKQ-1fj(EAaAJ)UnubPUDWmDm3MVy% zLW%&{;qmv_pzxihpfSi94S!M)3a2y$jX_Qc_b> zdKt(XKWG{nh1#M$S>cSPpi%TwDf+U;kD7)?QA3sy#0qCMg+hvg<`~QhKWPdYLqEwd zk`>Nr3L1mz=I!&FwdqA6$$atfentnjm@pfRkG3^Q5bs-~bZs6NbQg=?CE#*iTyLRsOurl2vX z)m+31H#7x}K~-4H3O6+cjX~{|F|6>5rl2vXPA+AITbhE#pen?%!fj1KV^EuW1uNXq z6f_38S;pV_gu-1-L1U1+P5i}7DBRN&GzOgv)DBj-uPJB@st-F^AzM?>7&=IXv{I9Y z2VYdqO+8>W;rbXSo|JP_6@EkNS3aQKH5Q0xhbdRkb3?OnEW@+G)>!1$vKI6XXChT@KVYC9((O9|=lKZHXtwzE7E;+PRnAR)8H3b3KFHnC z15oA}q{_Ley6aFz3qJPTdI9o&(E*li#pPQ*C9~sKfbV|*<=oWahDed~DuyKH6wQ>1)CEyo6yb`(;Fm4YZaVX#^AJ%!>4TZM}1L(&CdJY8q)Em&y7Vw%+ zT20{8y9j{GO29O(ktv^>8o{1bD{hTdacsR*8Vae6nZrJiyB~fLyM} z!VtLDdKRG762M=*05>`^T*u4r(fn3?!bQC8KHv-JxCgND5MYxT1QorJ%H_ide@_Ek z8wlvZCtYpd0+b&&Ow2-F>A!H<@&`cK^0+L|M|9U;1A#yPRN+lNC%L{nU+#e5N>vno zXE`o=*&wByo0|UsmnZW9t@i?gTp@5+3urqGFpLj!+nz>h<`KZkcmUl1j64YVi%%(R zsRQW1M-@KjvyL}=0F)M-$L;IX2l2bYN1@_+0*2)wPx*1f?n$^D+8VHN7V?xIH>_I* zm&^E!bU=N;a#k6{2Z!o*0-V?gP|i)Y=OfPb+XLL~0rhUemn#1NBGUm2kHY8eV*ukP z0d`kI-tw9N9siWrEIw%2%NLic9svB$1GYT}R5}a@3jkj^Zov~f0q42{7W4&-%L2?z z0rcau81CGu`|??gx_*GEh3BSzISpyXK|lvSWHq`Tpi&dSwo9OYXpL0DNu;X10Q}D0 z&o2cye;1eSX99MV!{zhgfE!H#qxh_M-bDyL>k4T4B_L}W>p24=xuxrsMXEU;n!>~3ZKV2#|K)T^ZAxbHDH^}hxh)hij;D0sz)*|S7!n? zRtChqMh{DFj8vC)fbl~B_tKE}JD+%R<#T&G{Q=6kse$3h8?yuO({e!5FhKq3fF+*; zZdU_5_{dZs zUSf(f%>l}}saE}Qo@XH+l$yss_SfADu&O1X{kMR=bpQkSK#=m|h9{=MOgT4IV=huU zKEG6#k5O&w4;Zu@dSxd9lyg&Oui`LKjgx@Bd?cyc?|{TuKsiUK{C*Lj{J7yzb~4=$ z5bXrWss~ujzrc92Hc~wt0ZCli^qzny4$cKTz`1sSg%*Gs{8NP|`P_LsF1!r?px`&Q z5IFk)X6OLRs{#JVgTN;YaE}jiE9a(awMDA32f&|?oO`DOy0hcUHv#(I0PMa3*m0j> zA0WFU;HVAYE9Dmsnx}^AEY-i4kP5sHFz18FO`Za_r2&*5HyrT{eQozPz&QS4PX81@=ly_r_W&V$ zrny>Y^xdaC!B27olqwJCF$QpJE?}D%VE#zJU>@Ddj~ia(QMR)z#*E+BfbTm1e0f&+ zizlPa^^nT04tRVYmwS2WHhqdzC@U-Hrrv!Ef=8nO6?wdEtpr#;9uQIuDrr3by*U8a zb|)g$izmUQ>`P{MfPEW4#uZ4{jRq`82CPf~Z1VJG;!E+lnaho9*5Q$^F=?ItSL{xd@6Q)e$UAVzur*RucnF12%P04#2zx{5PD4z3 z&hhoedAs#F5X{yafDVdn-u(5(B*+ttiTm-rd&ALqy%EBJ`dn6-t7c4pH5r*{n`WLJsiwIUoD>n1x=hiPvCT;1We^mmh5}(x7Xc}Y0 zX2a45{p*j={!xt9t%b8&3)fcqKMQH6{no}*`pS>y^F!uw6yAJD7}o6^`}tQ%gdQzg zI_uw-Kv>Mrzdxi+?*0?gzAJ&&#kB>h|Ieq;{`(=V>g|8UXx&>lE8d!vDQGIK`o+90 z#Y#mROQkK1{P%kRXj^eqO$%wA6Wz-ciE1eKIDMH92~Yj-Re4muY)OP(Eu3BTU>D9>#&c>>y|{WjHr5rw8yC{n8CcM|8ot>RkZ=FAd*5xC#t|if8RCwrH7Sd|Hm)G>XqdU=o zwQ=G05oY%im}MM1N?&5H0&!^DpK0@nT01;}e}uMOFS%cdue5 z_eng1VOBJ*SzriTlzTK)Ke&Ka#Jc=oDsMm3g>py<%rXu`3z<_YS`}e76utczR>*vF z%J=Ux4=={dlQ~Z~BR++BVpf5$l0{SOok;SY6PFg8C^mV2#brSeoSEz6@MM67E$kmfEV6VYTK%@g=M{J z-1+^o#+6(a){*fglvQkbn(%SvZ;LS(?1J=#%xR@YskJD^Jh6~DugqeA@7ld_xxQ@jJII5 zU0BGQ-1Vcpp@qDs(%<7qGPpgszS2Tx1*KY0#OK6u(4r;kQn zSEU~W^MfMgnNKi3c2XYl1ib%jKz%wC!#u%7ER`od{In?egU3TfA1tU(fnp1dJNmqP zeKKZ%!ymGE<#CKc^1*YPq9p3miKRtIUY+BA=OsxQ2xJ{hIi)nB~hP9Ec+Nq ziuPW5t8>jQEnC#k`Hb#ijt_FtSCbAGPGWL_s5~q6J=plNIqDHnWXq)yHexP zpXH^lNHxArRO(l;q zvnf0uOpVO*{%j{lqkO!Yo0x70-KSp-S{&$Oa`{9yb#4BtP($RpyaNmMNyzSFiJG@` z^>A}`a(2#1Uu+0npkKrJ%9N!%sm;keWK2GyU(2~>tnK9H$!hBg)Qn-VxjPD;LML;s z_LSI#cNyyoH06U0^?CA!_cfKLw;MT+=ZX9+E1+gL5|W#k00;D&SWp^*7A^Ff3+QB3 zjqz#ui(~X#IG1bVg1ca~Hkg+5TMH6ymxF{rWT(;NSv& z9CuHFjtp?l@ilo%VskPw^m|#*_*0A82koNo4~$#p8x|eV??*OHwE@+W<8cS_cCXPN zL`I*wTxX9Ktz0}>pyr+)EnUATa_QRARey-p_({GRe>7$~XBF*AhdD#(amss(+@nhk zYeMuHoXagbb?Q{7iG#vA%-a>L-*`=X@0CA$q#D-4G=Bh#>aUQT)jS}u3>!ki;WuXe&k z-j;dB1Iu$4uo*PaR5^!N<%g!_#_i2pkdeFb(8G{*2PTQJuIM%o}32B_+H^eQKJciiyP&UMeBi|4j30p(Tyxd*kUr0aT zS5ar-O77lcaDo{R?#M<$GE>_={5Y<_4ujGbh^GK3z2LEhr+d9j-fi!%%{8*!8J z1;4{NzBngiQRklByquj~^Y*7;Y~>tXp1UtIFLe`l(q&PQxwttyyW~diHf&gso4P(X zc2Q9kC;ica7BWU}Mf|x5(v$SZIG67M3fl(13}8sypmy}*EaulA=--BQ>-1TCt-JzK z8Z_j#54(;}8X^1(b|B;p1joiqcm>;Ff`m>zd0J(YL$8xtu=4Nc*R$3EnICjXe zC=qGHkz`}&R$S_TDj<+^gSx>w$GJSg7Dmxmf4)G|xGx^VJ|}Zy-ue{%1R^XCfS}OAca-JOt=QoHMJ1|5sjiM75HVKO0_K<@)dj7K`QFupc$jr#e zh<5RE_cR>d|1e~Jp*A|MG48NoYaE6~{(_YJl_|#X!-mC+@YYfedPs9K7owQ|cV0nB zSPBa@(P6QQVVcQ1o`^muD{Kfmn72C~U(4aayJ;)Ffg`==DJOlkED`SE6{{;8^)e=f zd14*pIwYlh;1V;e%g9|Hf=Qnhl+YM=Z8RK-kt;HI46)H@X05Ur8OF4w#<=5#j3il2 z7-B_aAC&bcH*sn1nvHTuBPNEW2{}hr7jKwC6X-)hZI9IT#^iK4gB1Fs1Rdky@O~E; zR3^RtU7Q`=yjnW7^wRW055h;aC!lf%78>^8Pwr$O;qKVV&BLh`>tYwf+)I*+@*%A5 zZceRQ*(ssPkOd+$cPB=72-=JrG96PjH9?*3VVYr zrI4gBKWv%d=zD}bI+PA1y*2LIghX*drD?GtP$wx_RdgH<7M}kti_#n}AT&m4`Yujh zE$uY*&=kx@A9F;p3+MWU?vZJCSErWlYGB>bcI>LsmAD~4>`Jjw#)UD^Ku=>`Pqed_ z?7xK}MxA+ka9|;}IqGzP0hG4L5PDD+2u};E+7kP1*H#6CFAVLBp(*a3vK2zoaJ5UGI;%qTb0CfiGOn?zo&|k>~@6xvNW&D6~S& ze_D6+02D*DC;?(XEOO~802Bqy7qT4*6x zM_0F&PM)rk6ca&V7z?`$v|KzRXCHNj<`w7UyUu!K{;=z=1NbKgmT!i zdFnB~zb>h0ZtRZ?%EGCnaNK>2A#d?MbxtdoyFM6q@b+W29}K{vJ-HQHfmbzGo?B1` zFDLGDnn49oUqi(D{8by|>WIw-nuXIbSgXR&B&~RhcX+>RLFZnv_{09K>g<^^}6vhe<#a2%4%36$L1H24yg1SzRZe=e zI{20QV{ET6+%RV4T*F&LWg97PNZ%VBXot`gp00PPk2DZ^egt!LFJgh=rjDnZ89zl zDVjj0wfK7sY~`WF|AQ`pF_82AQ!ySD1@BaGgN7STjtqXhU}b9lh6L{JMW)}veTwQ0 z?+W;?ifPl6if^xFxBP!^_e$_V*^A5miMc>6j<;6^CA=UmZiIlNR~Eh(VTGif9}IIe z9d7mTpp#oiIcdwsS@GeW?d}-4Ba@r{LMhe-zS}ysGJnn5qU>xEw>u}3qvYzuHvqUD z!&60O%RGt#it8IJB0Vv=!g^)A$Jlb_cPuShZi=P zhog(Lms3k^y8Cc*>Fng*inmT`u$8S7_S(4Q^`|9b&*dd1a|o4f(WlIpiZUJ#D)*={ z>3RDP8FwBtMuuX|&do^9%{YWxuAIz+%)I+Z-s1gv8+PEy3l=lOkqkcD9JUOjig)~x zOAJdua?r(9-3}ttO$`@@0M}G`VuQG$4yt-=^v5E@ZG>H}qZiiM{}~hKf{C4jz!Q7X zU9cLe5AF)MGVkw#G5gAPzzch>Pr=)I9$xBQCuX&!81;CFqPWJLQR#+KT}d*pvNWnq z!lN%IgCF(C4JzbP8@)z)$(^Xk-KD!1dKqqS)IB5gKcxxPG2!Cq>4``8nr-p^QxGc` ze&V9^9~TeI4kfmMe7eC8Zc_2Mk8kMtQBzWeG45dA{$0G8i(H2%hIpRL(_z7aS2*bx zavBaF$B}VaUFj(YwP5?@>8$Kb5C?p)C*ct7-<5M1uL?@;8rZYv;pvMqbX>3(_xOLR z?BdQ(H;iH1jk`9uI?IX|t_hG7Z3n}mCGQPjWs|Kg%=_0FLeSw;5H98`@E?5|M^ofe zNUU>sI8pd;_Sum{wbn(RtFprX|3DR{G92>xAR84dRKENSwcx_9P!S&FCeMQ+wt4RE zEun-ys3hj=uN6G4%#AyMhQe1Ra#Q!{uXAo!JmZ`=W|(g9NWIh0QNza$9revPym%Nt z_KPv!%osI(+{kI)lqefKZe;K`-+nW$xKMwipftYaz%(QuC*S1UPko&H3oG;UYdlEK z-I}1k#n7j&a#!J*eVYCb=kg0AH`E>*00rEVE&MOE<9 ztEDT}D=&S1L5cbEQ*oo2dt?PVqW)2VwOdPW1^wd!Ejc9A=j=~7m){WlzdI29Z%~!* zFpN=)u{}^<5j-v6#jOvjlAC!1V*Rs%T=_!8xaPQF%}V`q#$NyGG z8{<_o3R2bo{NFC&$*ym4m)@{ZouV$45JLT5tktTB7QX6*EqXy@a@XxM?mdiN$Wh$I z?+p#>m*y=vC=(p6w@kpps~}wbqW1U!>AANz26ArZHoTuUELp7o`+w`oSAY2-A-O5h z`gi}2ZVd15_~_o+CHj9@mj`IcKA^SuHdg=d|E82=28CGvAM4tH(zqhccwjsFh}!)=$o<3jEDaAV63WYo+wL)pu;^AMQC5I5-1%)j_cUSn>hlBYc$x@~_ z{WKfcYQwI~r`e#@(cWq~W!XlVKh;P`9}9Yj`fe*~Mc(|)`f|l|Ki(P4{WM*RVLr26cs<(t}w!RWjeqmDntztfHyYe7j-LXn# z2nXt`unIYrqCTZ5t;}%c+m+}&P{WBCe#y6Y8UL1^71RPsDN=b>P9>D3RaK~}F;#$h zKt1&o&!#1~!OhJ~FeDz+TSFEm$*~e)l|8b}7379bqG89JV&JPPf?Pc2=DDt zj9Q}_N*OhkGRg+j3)nAxW&SWc(C4*7`lPR=l%>43R)pB3V_wwTAUnKB4c=p~t+3bm zki96c>PKBgvmP+Obg-<^n#K6RLY!yGUmd5$wmyr?%2(&o2PLF>TjZ-@NW=5P_ruTt z!j3-t^N1V+OKbn|5rG z@ht+k7A{_TdqvU&D7%JArx>GS;t+w}K~ZU{sBj$7t?_`*kP%}DTd8lx*<#AN^7x9f zPbCxD6ii3<&~|gG<3wjVP;4_=m_W_i(5XR`y@W;ui-j{w;j7Ar0GC^oG9fp2D(yr^ zUeRCnbf^kdGowgTO7o;)jmX@P__B=82SgL47$sLd*dT)pDq&>B1M2=}xK2d6?3=#I{ zE1FP{C)Kp1&&>GKuMrLIL-)hU%$=J1Bb35?0(`pDfpp(egegs#hK3MrMu2ZoFqL{u z2VH23pi7>@eYU9yb-G61SD`kR!sY<3Jg?JIQ)*~Nm6uVy9<*>T+3Lx!uP~nh^SRAw z^eL2YJ>K7p{zsUl7_#{YZU(Edj9Bc?upaha9Ky;X%?PNvqQ zsq;Z`?gnzV;Q*De#r#gr!ru)A}}at%{g^2uV0M zJDKwRMLTB%8vkdwLA$q(DrbnBWi3tUSR?9XiNBi%+mM@;@tX!ukXpU~;A#OlcN)+? zn^UeRdV-~BwZAM5cRqv9Ii&&JZ$j1$+iY>u3J!X)gJU*`z9Gm-os6=^^`liAC?b(Q zzea0JMeWN_xM5CX+w$kfQNleI88ckz!67j%5lQDxbaEwmno*J|WqFBJ`QRUdgM-vz zKeaZaWtW9nZK%ClPMyY(XDqAXvnEQ!PJ)uP2U#|zEF;zFM9s#~h5$ zwh=z!&$}?M)?B3T#{bV7iQARncfZ-VfPC_88fHlY#)xvJsNChb2w_SO8h?qV9-}lJ zIrOHLrZni4n5tv_uC#YF{d$nrI-wJZGksA8t^zITN)X*?L}%8JqZzF*6|daj+N(7} z{{oeHQ4yvi&WV2C;$nU$*t}~-`BlWU`?z+)FN#suixjzx`uC+b(X_^0Oc@M;%~mKL z+{+DmWwJ=F3yyPH^fa5U4iF=n!dGiIv`gMj`TLvTzv+o7j$&Tt{pfioW;Y z3Nx0i#f@%nM)g+Er0ZmMi_BfA3#O4OBKap6RIDW?`r!ZjRbYrw!#)2>BXTg~UOVp= z#hZ%#Q&G%2e=6UczFtrICgSRM$grtJ>2>I|Ii+``Tz3xC=VsK+m2*~_iewy|q*kLi zyK*;C*&SE?+adm}ad--KIz-a~s6j3IbsssIiB>#+P4J@GO+Oji+l%I@b{mDr`N_T0;ZGEN8w5FOA$(bxPZvj%ZF8>v zAWJg4N-wK$M+n?WzqoT6QUn4ChbTH@&h}wVxQ<&-(_Am=)Q&=Sau|-+;ZJr);p}H& zA`DJwc~lTF8$VE#?}&mxj!)!zQ7Hvi;06t10{RGR5pE0p?2fn+$b3ed@JI0L>|u~(6QM^Tl$Jqo+wzmNOBatTpv6Z4t9a)UevY&-XUueJ!VVVy9ykGjTG*4{ISzLe~b8X#wKK9C!%dW36Q$_2`VU>G z{K;JOxq|51cN4#iMjO5gLZ+*kxcL=+67JDhY^sv1h2IEt0!tr=-}J-iAJd6mZs3nc z3|0MHgb++T8Vjo_;K^qqC9*m!Y_H<~ zhA)NZsj4QTL1G=uQ_V#RZ|mwz16*)KiMxZ!n^53V-dFW%EP7jEUA(ah1=S1`CYvB1 z#*1Ef7rObc=$M5ncfg)StbB{qAZKd9H?ANN^4F74ER%qke8o{ z+$*KXIa%Df0o7$2ah+C!nnj82_{IWt8A2OPDWwxFoJqlpXzND$+)6CYM_ES(h*CeH zX`Za0=&$MP#&m57ZE7xtRRcjYE4uz8Z8|`r3+Js)g+4T*EQQk)6q?;0*R{J+m?hb^ zrnM8O_2;4szREz!retR&D*K=j=30t)9NiZydZCQ3W5oBoS>7;)t~C+Q?UB6Jgl?Hr zZ7Z}Qts5u0V2eaq)|B0jGiqCjW7nYsJ~UY!+%U6Cpne#P+|_rfa$VYBD$+d=rcR^b z(VrYW(O6tRhKtud5Uv53BZO^ts0Y;&i}+>$b+J`QHlXQ6b{H(`e?!xnh?}3o^$1@Y zYE9d0#q5_A6dOD4_1PG0Z7Dc`j%QPrE9n|jvMs%-!?ifUo5R(_k+7nQUsI>yc2@0V;4GzbV9J&9$U;rXssDi0_U;SzM_d>*%yKTqmEI^hXn_K7`sY zqF>EP=gYD9Zz9LQQ66nc%F`Fl& zDZbQst_b42()9ykOKptCz@y?depiWFAEdlnWR)S}zk$?tw+QTwhMqAWqWiYuK7M&s z+`(={1oKc!wiJhw@V{;urI?DCb5OeJL4UlUZoi9mS@1IJE7nPvhG@P4pQ!mQS|iU^ z1inG55BranpP;MFY4#0~KMKTs`h)AWIdX;WHms8@g{Kv=5^8a-?JV6N|V*hyL-E`xD zwb@kknTV_Nw$$r$%IU;b7=Lk=B9VF`SlBIv zV{<5OHYH(3J3KhyWkoN{>7F&kekBt2R=}^#jYY*rMWcQV7F(H!a$jR4XM`Z{Humu3 z9U|7Oz)Y0#kZSq~TPK7zyC-zx`XKCg@GUU$t|3T{bVbViFao@x7iG7mSAk+$e{P1w zm~dAP2T{9il+an^@MQSBwW#S>1^uU$aB2mUL^pq}xEdF($sCPzRA*9}=A-O{+`UjcK$M9qvW_iS}h9EtY3E%w;( zohDm}?03))C1KJP1v#@HEAV9|deoR+Oc#x7;kq3!SMRpb?JMF^5}M)SGOQ1_^B@U6 z0!dUWF{-p9ZXOfq`vJlR-%J(RchF>(`2ATjc}iX9h;W{4D!Yl;fiN=lMcFGxP~Ih~ zGEg}16NhPo0TZ|ZC%T|R1}=x%xhq`sPndcBc_F5UBl^p1E$};jJHeV3FIM!kG!eS; zxX2kLJgmWgl7`>5tsCEVp2s^NbCUVsX?I1Y6cyXXJQjpC~-;$L)|{Qb5-0K zhZ2`zmndrNLm@_URDRMZx_y)yZz5|OaR(2Zgx5Xjzjdd5USw}aXA@|+Gi|UE?W&@A ztMJ_LXgm$OKns)TuqpW-6JCCBBxwnS3FfN2PxRXInF(FJPgBgOua9W?D_rxh2`8V} z!1$$uh}n&y4br;~q9X1P#F10Tcd(#)ezfN`Ii3((LXpvHJUPyx`WE!>7K*ka*J0GI zy4a4dwTe@%A--uz2TfV&iUW-?6Q@j(SI3Uh>QY(|1$`mD8~&VIg!60DFA zdw<1Dw!M|8$%7h#W$VC%zc!1jcGW4&k<0A&CFjMaAq+n_h%%{=z4B&H%G>d0p_Qt5ngJ&CQI%P_cp0wc>UA3a2 zHlhIroOm(`#p61~4u3Kjv{4-?sW+{)qeoqtd2mxM<@DdwAYDA|0D14G)M+t|>n+lG ztY>wl-|y1x2DEgz$l@EDEGuYIh#5QE-hvZQ$x5Lzl|)EY^q=g0P}13uNdSM|SyEdI zVZsj`LhQ)oJ63LM5im%^m4$16%tS{_Z9tDQL>;UkG{la&45l`7MThTEJDLAs_?xsr&*-h^!H zaf7{?K=s>DSaqt_jk>)dhXA_LKxFg7Bd1%mVm*B~m*P;(D;$6b30o=~*_I_SeWu*tMiDBz(LU*3%8h(8f5 z)w_{NJzBYncKaZ*Tv*)&5Qz;<5a7L7ODX0K%`)Q}&Ztgf>{tvvFv8x&lR~O<&EZRh z3+yi}Tl0P?Bw93$LF;gVRulmZHuB?jz=7`2S7PD$U9&or|5ntWf;0yC;HR=nYD zD`)O!72Y8_qQ^@}0t2Yi5p-HHyc(_`+c{j6^+fo>@OZGg==hnEE0TFU zthE!Dd28a_hQ2f9PiG4;#SH>M5yIf5Xq40B4jm85D++xNFvq<7h(m( z$l{2bk>h5i4eUg3Uxa*$ooIRDjK(7vtXX23ysw2xqGn@2do(dO4M4Y62Q~}T zpbi$a*$1s6&Td1ONa9HyV#p3!(Tc8&XtZWc#mb&2q|^X5v>VR8;IR~{_{vdu{szNg zcr+?i&vtox4K&o5eu&lW&yaTvB7 zQ}MNVu?cU8#TFOzrrwP)HqyVw#9j;U?kOrjq)$iAnIM$*W*%uoM_~9)~Qh9shFhC402gM&y0ZQ=_4U^#h7msMyNE%^JkKWPr{q)};@v=N@ zjtmut+k@E~1S0SpAcE(!*$qX-08o{`C&~=Nb@_V~P>EtH(3S%9{?xZ2wW}lo5l`A+CJz0A|Cfi7o4vS;{Wc}^r^}{vBTnq(Es%9pvI`Wx zycZs0fomi7%YTv%H?nwTgU(ZlexEHC^3KQEi$eN~ENu74q^+pNYnU#a##)F>yiTRc zRrwS2gwkxNV?$wz#~hUQ6|+DixjRA-zd_1iO;ljw+v3E}M#- z-la_fs=7&_JoIEMUxn2Zmv~`rW-m6)23xN#_=ualW*^;w%7~!9@!n(u-exVK zjVD>nfcqLzttr~+QE3>SYeB=pIm|(NS~`ldT2T5m4)fpLsB&ArY~V$v%c+Mu$K;YJ z8Ja><81eX%lJHK=j00g&RXFf2%d9$z{XUBJkD`k9{PDM?q!wInvrsAxXXkRKH(gD} zZo-lWM!97ivQ5F_-%+T$l@}N0*oA`*L_963vL7h2hr4)%KYPSU_-7A5ri4mKF@5t09zQN7p>* zg%!=;PRq@NlQ-Cq7t%(I8Vh3xCLN_%GrD9aF7yF$<)aj8!x~>((7HBsCV*A0$IzG- zv^a)Ro3g0#K~cXIjDzY?&O&yqPdkcjAueCS)Oe;kIX0xcZ)t@Y{bb6H&(u+Z8QHi) zB%am8E(~=Ds)C|)2e}%EdgW3)t$9`=4|f_nn?Cy!!$#b(h0273-00u9h!c-+bv>1C zHKmZ3FZ{)Fq&M~VUk0wO6W*3g~wM4Ndr5aC^Vt8Y8 zVdq1l*SzZ30oscMp7hQ{U@kjlj}lI~$a}%84UO>|{J4P@XX|3Zj-z_G7oJF^TZuk~!^|4dp1N11@*bGGp*J*=PMqTm{V>X|M>|%C ztvW8M4=s8|)goz9GxFO&7PZBlJg~&@4d|vMDD)X7_PqmJnc5U*K^Ml;pqUhHFY4?- zFS#?@J$G8Y zo)e}I#myjSq?yqh7s{$fJ)4Oaqfq>{@AzY0gIZSQ&xE#=Y9{P2L4sm>Pv^W)3v!)E zeSE2!6WyGHC53L93daak7%rL2qzU$v(2zpXMNkOLl1Ee5nH082=)U9XW58`=+dLbx zbrPpoqkL!KfKZ5I%i$1wJ5!HwNETQ5RToji6s@xtzb;L?+lni*F#*@9!C_0V7tis0 zf^O6kwI6t3mBuq=DlBFrq^xW)o$qUsu~Q{FYfnK~JX|SxF{eYEL@;hXgl-I|bYFs%D)gnGRBH5u zOa{_}m$alaTbg)t&g3n0(4Mk`gsumA`+aO=(0n9<+)@4n+-}lEUpnzO+@<6J)CF;_ zMCoSoDIdB_#qP9lDjhIGuc6VE_&o^vKs{Fu2%Z(K=p%E)r0H-a?FPkKumqTViprBn z@4Q}3$O*n>K_3MUV=Vmm^H$iTB3ifG0%6oCYac!M!w#*G(!g7berz@q!6 z;_E{otkPPvF$4RK^VnV0!f%-i+uM9sd6{}`7rES5TJzo_#0f1SW?V*#SsrJl?#Q&Q zO7*HzuP;J)?m#n#JadwYfyGWr3X|c-6YYe2J*OLRHDh< zDUKi%7nn23iN<_M2QluJi^qK5kB*c%iiTGeFMYw4dM~od5&KV?<-<+TK;DC zQ-+AtUkz2KBS&U2N()< zoPXc<%hdFTbo37_3fwN0+Aisn{4Ftl+JNnCpw^Y*A%4xCfs}zb%jqSX9b+2>im$ep zIv))4PaF-TC}x{^;f{-xR7;z_rL9@`|;tE+vbv6CTrNs&wk8rt~G@p+atwpSq@&exiv7y zj*NbZoPk~kO;{DL7WOBX4Yb^B_jZ0^1q_tzWoz6N6ZpWLejc!Ms$%}CVu6ait(7+m zSkwaL?E25X)WjwXi~ZgxKUY=%cpbm|85>!F9t~|heS9}J_B$Uqn#S(gq?2r;n^@+g zwH4a|_CmOuu;#`DisrKYtlH3)Mr7si99ZejEOM?6?661N>|@7byB+hX9o+rhH3CWQ ziX~s^G$Db__DWFTm}k#-$ALhoO-^>ycBcWy=lPc--48GL{+(I=!k9qAZ){~{n_7eX zYX{r}#i1jU8sC47HuVLNjO40|J;{T3?RogUno?6)23-@9c~v_I>H|MhhL-h{xw8}`fz zvjUgx>d&1$*z_LV#m_a;A5kE%ZKmzRk~!AQD(T$eU_l-`$QbW8(7wMVlg)f1I5+a! zR`BC%_;)h6AL_;VS?ro3G0@?#JvrXqhY^@-FBk-h4zy00Zy$FU=;1EBJGfs=U2fwa zSmrNYWzP;gvSH;8{B}KQrxD>R4 z`+mMIevJlxHtVYmcH9f>w&^@DFta^j@A5#1&2s+93V|chRs=fdth^r?d71c7J5P`VkXPU-l>G@xKcXbaHd9 zokQi??vITLTo`4mf0Dt!H_hK##y>p7Z`j)(+C6YT(q_qO7j3WC^FD7#4a z&rI@(p;yvi0-zvma|{_OIsv|Xh%5A3y(>MpsK z)edy-Vyl`}+aELDE<*wjj@koua-D5jAj`+rj%0=Xi>>`04gHlizUBu0DQ?@}Tiaa- z+9k8CYsF&!%?tK;;IixBJ!b;zvY5CwK2SBzS~EF^E!^M6KUmlw)zBY%&+nbb#lbGw zZ}jt<-uH*a*l%;~P|{+Izwk4=xcB$3^7o7j^y}v`5@F9B*UmqY#Sd%^>~wE9Ioii2 zndR4Qd0^}RG7;%^q(YKA@r*C_?4-|5-hR33P6P+O>rM*oOmCcBUkA$dvXGT4Xh)Py z8w2xF*}g?41rp|3YfqR{H0>;gAcP-y#=L9O{x5p2S@=JCIWQ(vL?Nn%X zl|YA0w$Yk(0@wbr^>(UfcZ&Yu@9J$QM}h6`#f#;6+fFh85AsH@A>83%r~aZaBsAJ_DL?9 zQT82)UMho?y$rUBXs^6i&{mMnd)w>frT2G*diiZ_Yi+%ewx+bUuAJT;d#JjX*(>8^ z^CIlunzlUJmW6tqz1p@cncXkoHxBiZdsFQH_3VL+UOBIe=h>6YcY` z>8<_o-ZpDQSud~G-d5JkbTt1>@q%_gw{5PJH_>k%>h}n>b+~qww_m{!Lec2(N+t zHGntG3K;Olc$2J%5&m1D-Z5{M_p0A6)H`dvljv6s^)}kp6Ri^~+1`$~eMw{gW%pjO z?kHugjrR)MHeJo9_<>OCh%m3BHK&j@BEc4idIkN;q1Mi5t7g0{bX^$X9k=_@w$WH` znEiXkdM?_lV#{-QuX_)yKPy_@(|Vcg>9s9I8EkneOGQ<0jr|*8S&6pd*;d_N74>FV z%`14_y_E!LvY#r{o?pmXonYJT?CmBDxxFUV-_^V<-Xec+sBJd6?L>sP-WrwMpAc$$ zm*7R&dc$mgDqCAp*@KbZ0b91*{!MQC>_QV}Ypr6f>S>)BYrPU2y0(fudyvmvYxzQ)IA((8w#~mB)8p9vW-uo`;u%~v<)3skYsinY2ixi za1O75Whu;_9BB`^-7ZVrqHP6X)|#NI#?Y$fMyrLJ_*-c&QCd7ir$>z8)hkMw(mns(D`q;+Hti?(a4TYt3I z&r%=bwY8sIZqnLvH+@9RUSItEGub^$ot#6|j{&CtPpxf( zLcQBwecP2(w$^Gk-S)8KMp1uZs5j6WSir9nYK3$=oyRJjXe~M7Wp)#AAxmqtwK>*G zA8Fx8vQ5R>8r}5P-Qtnn{!MK=kYjFl$FdL@#CKD3Rc zv!5a@V@`Lm8~d+|#W=YwSZqJdw{?%Sr#G@LiSnA;7MvLGeQ1@9_7g&_2yS%l4zowQ z*{ai6SgP7?MO#eWIG$j$RHU`qRoJz>khQLs{S@h^3$;~5*k;;TyHXe`YORU3J&v(` z&u?vXfpG_B!5Gw9SqXAIlHnwtFu?aFA-`-uM}>rd%ellm;r9AxMN71O-MDp&u!bT zV>|ohLXE8mwM~|>jaBk~x7CK*&j;*r&)VjK9A`)Llzwe@XsB=dw=vWv2zM+>vYl^9 zuUD{+O|ojIvTBD~ccrlA#@iA%cQvtj%nq~GY&Xta*mK(d)7X+`X2Wdlt~lY=N16Qh zT-G1>sUrQMZfYLo<@H;KS+}S47I-)9Yu5ezq5kMFn<@Qpcb1Ua!nn$B=4Rqzw!OAq zYJYWvt?-g%zICYWXEHa{HnyEB;f=Lj9&de9-j58mmiqRqej8C&y#7|KD8EXm?LkIs z|8UE1GFuQG>XNq1c6hK~(2d*C*6a+n?xNOQH|;mFopXn2HLnt$h`2y!Z_wy&is;mBBwBV@1qt{qJXr^pl6$l$ytakjkpR$YN67 z=DE|}P4Ak&Hq=JjXn#qVSHrI#=06Ox^Ogj=(D1&o-VfMpxz?-V%01JTx3|u&Zga^c z+usWQj!=JIsNXWo&)`lyN?FUJ{0@X5mF-8gCDsOr{hgEbTMip?g>A=5*~SZ4PiC_S ztqHd@+0n||=VuPJeTea=gxWJ=ycXUFD_2_U-qKd19Mr@o z?3%?o?1=S4e!Ew~x+mH{=YrV9?iMlJ%Q`&P){@bFeqv7>WZQgp_=xm2+fT`Dm0he! zF*al7wo!l2%>eE{H&qs}C&$|pli9;vtYIna3GV2c%l@?q+C{#S)uggL9A$ZmwHmt9 zfv)!dA;FkRxZF6Nh+k%^`qU`ZVoApZCNl=W8-z0*pBtJ zM@m`~;%t*)wlvXNc-$V3q7Nc$sjGi!dnDW@jXM4in~1C}4Q*i-3&U=!Y&m<1D|)02 zi%7qw>-Km%adgv10XIX(n~1j9$J+*?O}gBw><*=A>>$xI)K(Db{qCO&w^IkdMX1-* zW+)pNep9QC9VP5U|6^NMb(`DWDSBT2R+!De-PwGkJE!bLTdP~ax_hhr_M!e?ZolV+ z+C@ZT|D-z<_|~?^Hk!9tU%C_j7_(XJ%&m%>yffJqQV};UHkx#oFg3Xi;DE2yJjuu+WN9P5$kR%NMXyHSl`$Y&Ythv61%>#VeR^4fUP5$9W>+Z z5m$rUHp$1C=wtiR#X2P1Z}}`JRq2%17>xGjM0aIg+Sbt2db)s(-aPiK7#mw|E)Dmx z__LoKGV53eyE7%1RW~f$l|@c|j$-aFV9Q*wGTMEK%GJFOjyZ1#GS>V?V`M z!9#4du7Lw=mT|NEF}v+z>xt*ACXfG{8~6Eb9&zVcC%r?q*?hKNIczntHqs(&PgC1Z zakkP_wnBG|&EXxj=Y-kPNSg>6*t1>lx|PS;bL|!O{5adBn;G1$ zq_?%CvQr;7FU44o?zL^?u{FBOOgHmnv)xK%z3)y$B5nOHmxh+3&)`ul*T`BFnr}L(s+gZKJIF1Yq%Z#uekkbWS11aJ%5@%HHDuw)ISsIpN_Hy#94;xS}bz8D}^22 zDgW@Z(`7eTO4F5ARrgDEWFJ*+LR z6AD;-bJ`Q!8KFDT3bRL}E&Lnoez*yDW$BuhWUF>v6zyFzv(J7`Wj+;WF_rAJF3eaQio>{r}mt9%~(CL(kvpHtw2}!g|b|Yvi(C-)_|?YTfKEkh<93 z_V9kQO}OK^drd0dJK#>QQ~Q10rI+hSJGS^GQrU>fVYHl0amlRPYue)Cwoi5KNi}TA z`_`ren-<=-7P#2D>lGL5SnKNcmaCe!Ux_xYWU?bnIy0&5XLkji*)Qfg#BH~U&5vnp zWzS|?H%Gr@Fum=TOM-h*#qGR%%^@Q{ce2s;Yz`=8&u(syx*WQfe=1mWTo<~T)xGf7 z#Tw~m70ljgq7`8tTwzf;M(e@topVGZVqur@YeRs47Q;Hc8=oa=}6nlk@oo4_Nc3kJIQjB zo4d_ukGd0vX4ZoZto6ygQ`Qs5?DY1laC>@D49Rcy(^;3oU--h{9!)wR{xt*SI~3@Q+lt3%Rv9ZPe_yahfHk_11Vhk?-i23iXphZH#>p zZdW9Jqfo0&UayIt=&o;?TR%>-rwySBZ;R$|1gw@xb zkVN}+V!f>Xt#JPfceETHX0VJsy@EH+PRspd?j^Sj-fElHXZv;Sow5;rr!X7nWkUV& zp*9`W@m~zHhShg3tGdp12gfcpg3j6>7>C+L(tOKn1^;f8eIK&7Xlu|k+aveFZ#{dyi+;Gh*wD&KSl2G* zirCMQmQ8n(nalQZs6QyeyKYbIVl{P#tb+EGbhaXQ!M)R(P=RA(7uz>?d=Ir=b}v@j zS+2Wy%3$ql!QmjCUD)TaC%Q{JcWPpPj^<8}!>!MAyIr$kozD&lW4!$Sn{Fm`XRGeE zyPZ2k+Rmh~qhAicXEIyZ$eQn7Ws9@YxY07q`_^7ZS>-PFw%Fv7$y)DTeeY}!xJ5s*!taRLz4BoJ0Wzdbz#oJj=Dq3NQ-n~8=!Hv z9ybcp+7)6eTkhtWKGqcXdW5}7X;Eu}L!>)zbv0;X!`-g>!uC&%W(KgMdwiVsC4s^%X zFssHKd&HeCxa-b}wyeMH(raexS&TB7i+9H-Yg~klq8fINvn$*>JjoUnv45}I(9dLL zE$)5i4-T{5FK+9J_FiS4bF*`jO_$L|-CW(oTka>g^P9u=@Oo>jd%?~fv7fzWTEKeP z9RVj>vr=0#(puYF+SA>sL2cV!H2=ESE9Tf#)X*L+X0HKUOJ=!BX*+$~tLDGyrs6)> ztaeDtYCH14y<(Bl^_Mj?y*JxV`ZHLw?KIyXWhZ@p!%)9<%%EW6|NnAk{x9HKm;f{N5mR76xl*n4oFUq?D8svxQ&>~B_| z7nz-?j;MjCslt8aRInDJHlmJ-{7%>(+t|L?M;_ST)>o0>L<2+s;i)L*L_>tVN!!+8 ze-ZqAOWlW~1sfxpAeySE=tMI_b3}{(6fF_05UoYzq;fAHUPQd4!hJ_ounnRuqMZtA zYk%Bm{qZuQg9>Wf5zz@@AAa%N-sYsX_IqMmM^{8Q71Xvnq6ea<3irJ{!Cr{oh(0Q) zzWrj`w$cyriVF9UX~F)80f?Xq_o08mfrvqf!75155X4Z#FctaTI_wYYtsNr}_7^kH z_Xi0Yg|PPoT0X|8a32*I9E%u-7_WkiO+dVgn5d$(TgN2CWW*E|q;M+2{=D7VFdSvf=jFIAABy@;<6`&5vi{fGmIgDObSA;e+C*D9E$jv$UAzEMHOjv>B999O|Cbpmk` zaY_XlJB|1b@x2N%b_Q`4aZUvpJCC@4xTqq(>-9^B%ZMu~$kKf<+kq(r1rk>B-c93nL$jSBK%Z{f3bq(j83p!(?%84&iFde84I)z5^; zjL4#b>e~lZ*jBP3vWv(|^>ZL{BJ2|(p4&=Zs-GK?2a#6=)z62>k0_vm>K8;LAPT9V z`h^ii5JgomeF#?v4Bo!Q^N+C)k%BZ0FWfA2NLVH;0xHOdhiHiKRd57qglLRt zA|f9NYKmxvXs&_;wLr8)v{FHWS|i-&ZZ`|QsDcE&glL0ktAYfzL$pV{tb%-WKy*ZO zQo;Pu8PNsNRRsy^hUkvyp@JEvC!!akw+b@W2hkT{A3^cln9E1TUP1In3{XMFf{1~L zK`O}DV8jr_(Ek*}5W^88RFJWeh*5~qD#+Lv#8|{Q6=ZBYVglk-5&5b9M8qV-WEE6y z3Suf^nhGj69WetjQw5cK4KWMxx(X^c8}SBWjtc7XCSoq)EfviC^APh93sg|Kw-E~w zi&Rj##fWzhOH?pBEJZ9sELTBYRv=a)R;i#a?;=(s-c!NYUxQeSSSO+YqjEjseZ&S8 zjLMCOO^6RvFt2Y$Y(adeg34`0e1zDhg35i2*pAropJFHC6T~hRRPIy6Zp3FQsLSVw zJ%}$_vQ~g1YQO>_;3>L0t|a4j~S!pe|n{jv$VTu)hGe^N(*3#}MDDU`{@c zIDt5+f>C)2aT@WR3abA-;tb-f3aWn&aUO9&1=YWZxP-W@g0X)EaTW1{3i5FcaUF3( z1^M_9aTD>A3g+aW5w{S(s9@~>iueujy9yF?8*vA5R|RAL55zshpDGyHebOQMZv!#=Q;fM$oc0VK-iAaWsQb9hF zBT^uuRZ#sHL@Xku3aXz95r;^vf|)-JA}u1F3i1(;NRP;%f_!8|WI|+CK|Zn|vLdpn zARqRJ#nxjv5II$lk6eh{2>Uau=X;%eM%W+2 zJ=fAgWUMN}K3vL@TwMioPz^*)L@gDhur{I&qOJ;3SPxMj(Le<$3?Mv2LlvaZM>Ik- zRzV7zAethYsUU^T5iJlcRgl6~h}MV~RFJV35%%$+)?;n{Q?y02L)hn=Jm2f2@MS~? zL`M~*uoI#)qKgVr*cH(Y(Om^8?1AWs=%s=b_D1wU*ryad*V4jd%szO_M&&Dr{whe& z07MWmPz4FH&!@6=3`Pu5K|Y2eh9QQlARi+TBN3xikdM)bF^I7$$j3Ouc*F!1JA-0O(hh&NPFxjBe85pzWpVg7gvF%L0c z1+`s(cpI@$1+`s-Sd4f_1+`s*Sc+Jtg4tm?Vg+KQ3aY;f@h)Pu3M%&=Vhv)g3M#h_ zu^#ch3dZdQ#74v>71Z_v#Ad`66;$p+#8$*dDj1d95FaD9tDv?!5IYf{sGzpH5T7D; zizrHMKSO+u*rS5leu4NBu~!AP{R*)Uv0nw1JAgQdIHZEg9Y%bOIHH1adlc~v;+P7? z?YD^Ih!ZNP{z=3s#Ay{&|2xF@h%+jf*Uuu(Ai$D$E2h}i!WDG{ja%uL=F-5SF^UaIT5)KxmA#{JczuAd@8J3A;J8J0*Hbtm|+qS zg%E{RkisH}qKHHlq_7yGIHH6KQdkm^geawg6qZJmL6lWN3dW=7v=&6DP^+NPU^ie^A`Xc%vUQt1U`XdG)f+|SRK*S)#U=;~&R1QH5 zMGRBHEHxZ40x?nr85@NdjTobX1dT3R79kd^p!)A1 zmLQg@V0KuBSdLhsg4(V`tU|o2g4(V|yoXq$g85@DVjW_=3abA;Vgq8M3aY;e@d0A9 z2>Yx`%jp)xhls5zsQyQYZHSLmQ2p(Q9f+MOm_I&2>_U91f_&^oe1`a31^L*6_yX~z z3aY;s@fBj93aY;!aR6~p1ta?q;xOWC6;%HS;wa)96$!3Sk0HKA99Ka;P9RPqPO0Ga z)6IeUsO=@hWyBQ~RPHL`2gEfMRPH+B2I5B*RPH9? zC&bSxnE7uZenI@Ig3A4d_#JWkKgAuyUBn+MsO>$(pNPLyP?x_E_Yn_NP?v{@e-MvU zP?yJuCy1vi7`GwzP0{xL54*MZf7tE6UmZ=Na^Z*wM5GA&Y6#0%GDH+2xe7*Q3Pdy_ zMg_HvMWjTeQbBFw5UCMqR8ZTrh;)c}71TC8A_F3$3dU_FL}o-570k(55!n#gRZ!a; zh@6OADj2u95qS`KRV27R&4FT6htH-?DNQ;t6VAaQ5aDKQB(!_NJJDv6j#B_ zUjk7QVV_d>{8q?EDMV>R84>nj+O{uc5#D0xD#%A=L={9;738BDqB^36 z3i44CQ43L91xNKdh`NY+D#%BDL<2-X1+#;PXo&Dtkf270#)u{=NKjKmGemP0B&Y?V zC8CuI64V;uz6svm|Dl4~zJzFlXe+`#KGQ1K4$&U*vI@q22Si6iCl%DTGolNks|sq{ z4bdIZLj|?%iRgvst%BP2LG(rRQ^DAO1<@ZdKn2wgA_gJ`si69U5kn9|RZ!buh~bD4 zDyZ#9#3;mQ6^!gLh_Q%qDj2up5fc!vs$f)3L`*_V7GWO=XSJPzn2MODBF2g7h#82P zD$+Rd8e$gWbrtbW%tpL{n4`jdMRo8^#9YK%Dsnk74>2FHKt*0B-bO4$EK*UxiN%O_ z5KB}fIk6P646$5A87EdCRw7oZDDT9(h}DSqM3jqkVhv&~Vx0;*LWcy`Bi=`BP?5@s zjfhQ%4^&W>&4?|C4^?Dz>)49;2(e8?7AHPNY)9-+L9g#be1h1eqNrQPr--!M<5eHOMa$7lwID|MX!oD2B#Mg);h@&c~+&740h;LO; zx#Nfvh?6R)%PGWZ#CIwfFW)21AkM0wa_11|5f@Z2qAnsXAug-P<@WXp;ws_?6^xf_ zi0geVjYK3v zM5$m@CP$<|M5~~-F^E`%eG2jO?O@!dLc}3btDyR65NQ$VR1|X6k4L0OWKcnEGa@n} zGOM7rSrAzf*;G*5>?3TA`wwc1+^`XD1j)cf^nOKD1|7kg36Ualtq+NL7$dKR6tZzL7!Ga zR7O-$!FZ{PsD`Mnf?ltIsEMd0q7oyjHlhxqt_ns&Jw$y(0~PdX0O27Ts-RDOL?c9F z6^xfAh^B~UD(KVZh!%*JD#&^(L~FzgD(LkW5icRysGv{VBHAI^KQE#~=&_d(9S|K= zFb8!)bVhVhK~B3Ox*@u&AY(lcJrTWB&>y`KeGq+BFt7JRyn^Vjf{YD71Q7#O&>w>k zgAqeS*mpkL3^Nol3^7~<<8}mMBw~~b@-Z4Q1~FCz`51>7kC>o>9(xrr5iv;xJvJFJ z1u<0x^XW9ibi@o5^yy5*YlvAY$k^+M*@!n((Cc#$ZzAT3u&->eK79)@4>4Z_v*iNB z+lYlK7`KZMixKarAcad1OA*Ud(Cf<)D-bJH(5I^q?;=*KU{t<`Sc6!rg8o>CSdVyL zL^%Di0kILWNd;r>1H@*;78Uf^hls6+k5rJ;ZHSK%+f~q~I}ke&pQvCi+=ciQv0DXw z`WfPL#2ytS`3uCCh`lN}clZji53yebbvb}Ih&ZHzx*SG)jX0u$Bp*e5gE*#w+2LEn zal{D`_Q}+iSE;kTAB5tanPk%!EjJTzO`QsPFuZZ7N(Cfb=ZX@o9h$QQG5q}`=si05)MEr&L zTLoFak9dH1sDfVq2k{8;SOvZQ1o0FRVr%`^nRIj{y&h^y?foBiYw!P1L7zq-A`!_{ zFegVLk|R>6piiR_F^E_doa3fM*tgGFJK|K(>!}fG5NTB~OQl1^BhstjY%2pIBf`EM z>bd@~ub8vZkQtE$kyQn?&4$R1$f1JT=0w<+0^7diR$=!;f_V^m5&2XwkL5=cKonHL zXh=X5LKIfP98?5R6p^TcIj9(-IHH6K#!E>=5~7p}YFipn22oZ8wJnDzkEo!6%2h;E zLR3~k<*Fd6BC3gqqAt}DH4rsbFkWgQY9s2XV7$~t)I-!)!FXwa2p~Ka^m;>tk7%TV zUT=(Of@rFO5!DRQ9MM7rb!mxcg=npUy1amR5%H1=MpPR_TSPk*)TKS*Wkd%R)TJY$ z6QZ+-z^QYAwCrm!^qx^ z_zdy63i@;p;tRx=D(KU_h_4X)RM4mU5eEVh{K4lRWK(XK^#SVqk>*PhWHk7 zTm?OL0&x;?N(DW38u1VnJKD~+f3GuTE`r{Vj7sRhB=#SqJzawsoNXg#bLEJ_Bp~CKm1n(jKMEs?K zk^MK~KH`B2&I}$R{y{uaL61E~JV88FK~6(r-SH*VZteXacKh#h?UeLsI3fZOse-Ye z3=xG$uA->h+Z2dsM2rf0Jr?-Jw9EhBVTq=sXt>i}JLD<*+J=g20=#PAe{D=Z77<2aJPFA@D zL?IDz>}_F05kyfH>}?{V7^1ieQdj~}5|N~W6qZ7iMwC%O#>yhfAzin_YgLDWUmQ^66aKB564pn}SIh=vGX1(j=r zXpCqgA~i{Fim)#$UP1In3{XK` zf{1~LK_b#{j2es>f*7iTx(q`MM~qNGua88GLX1|yco~Bjix{VZIcPj$0^(H_)MX-K z5@NCn>M{i}6){akQ8yZG)a~t3#4^Nk6=ZA$VkKgg3NrRCVm0DD5$Twd*C5s+ z)~O(4>k;oGHmD$D8xfljAE;o~-i+9S_)rB2+KTuHu}uZN{xM=ZVuuQPeJA1*#4Z(N z>{G;U#AhlP`=2BBAihvR#=b=CMSP`#y6i*jM;uT=#ttG5Ar6a(r`Nwm96=maL9c&< zIEMIE1ta@7;soNP3VQt%;xytr74-V|h%<<@D(KU5i1UaGD(KUTh)ameD(KTIh^vSn zRM4l_5Z4hmRFL%_5jPP(si4$eei5O-CO^*<2z5Pz!R zNcb1xZ^V5SRPF)dA>tnu%*l@sj}cE)P`Rgw5PQF(z5l~*|9vK%p2~&UQhWagB0>eT zR3su9B1%P~o28N?QXryLlyV{l5sOHvqO23C5OIjqBHSAxLxO1#X%XpEggX(BNRP;% zBAF8z5t$H~Rajqz1hXKrBC@GS??iS)4n$5BS)Is*$c@OOBDWKH5&01LRU|r508tQ; zpn@JNgeZ(CqN0LZM^Qu~qL_$`^ha?-2}DU1^hXk+6r!|>IJcEDh_Z-sD$+Vp9#H{N zQ3X9#2~inQMMVy`j;e@ii0Ufn(;A4Hh*~NNx^>h>)Iro$QP_!ki28^IDoAnw;UOBT zpx1pwBSd2nnQS8=!6t~Nh-NBCa&trrL`xOar4^zz;sq7dwX(Om^~>4E5p=%u2B>#^R5K8U_57!CapuORx1ux}@~ zbqqiR5d&4EaAFW*Fk*-b`gAB_7-F~z`g8O#U6vr0B9^J3F3S-s5Gz$smsN;&5vx_?bG`l^Vhv)g z3M#h_u^#ch3g(~eI%DyYi`h|P#CDyYkch^>f^M7W&|32sAtjM%P%@v;N46Y+@( zdVLq-Q^al+jF-<4pCk6DV21eu@g-uf3O8K_ze4Oo>{mhM4j>L94yj-cI*j-laYO~R zJ&O1SaZClZ{T6W?aY6-^JBc`jIISYl?aOzF?-6HIFe=X?&LPgLp!ydO7ZI0KFy<~J zt{|?8aDNUI68r&i4RKus)xUxG5ph!myOP98p3A`6!75-7Z5KZUQ$7V+928@+NmHx?GY~{I;bE) z9TA-nomG&aE{Lv(ZYr2zx+8iZda58}y%4<-eN>RKzKDK^S5%O({)hpHpb9cJ5HScb zSOs&?5X4Z#FcoBMIAR21qzW=N3NacnMuhvrNBjO4#5lxw6=ZAz;#I^%6=ZA@VlrZi z3KBFGF%2{P)l^$B7Z;!_o5Y&YUF#OEr= z*dD|eh%Z%;vAu||5c^b+vHgexh=VG~*dfGW#Mdgw*b&50#5XFK$BrSsMI2W_3Qr(T zB2K9wg{KkUA--2Z#?BzlBF?EGLFW+{5EoUDpi79$h$|{c&{f0_h-)HplaK3&8;BoO zkdK>)pAbK*p!&BEzaV~9LFIl!{EoP-g1X#6+(rDMg1X#8{E7HW1$FrwaUbzO1!Mmq z;vd8#6^!i1h$o1rD(LkP%d@@z!*1>UA9nlivz*-YdN?8i5vhV>Lo!4ZBDsh>jO-MM zXhe((yB`vaMWjTeQo)FdL!?HeQNeggi%5rvS3$3*M`S=`R6(z2LS#l{Q9-X~MPx%{ zSHWn=fyjx-rGmQTM&v={Rl#`4hscj8pn}R3L?j>zsi1O&5k(M1RZzJ^L@`8h5qYUx z2}DUmk_tvtDMV>R85PX9WfA2N5r90nriBNdss)D+7Lv%;DfR78I2G7K>sF+v4>IubDo zFaQ1-(8UF#|DE1-<%FVkKgg3TEwh5vvjJsUSgX5Ni?ZL=z^WaBR*3>uYZo%gZM%PGmLx{sFsLR)gBZ#9S66n)!5XTVTs<8VZ!Q+S%h?6Slk5h=#i0@R8vF{OQ z5NB0z+&_mnkGPJ5LXdDs33*c5Z4hmRL~zkB5opnQo%g-GvXHF z7Zvo!uZZ6ezpJ47w-I*`cU4gRKM?m2f2v?q{)PA(abE>}`T+3|@sA4f@d)u4@k9l^ z{uB|Cn)iR$?Z3~Y3)3HAw$$GLfrwCH&$jP>K_o*&sbJhDN2EYRt6)^dAYu_IRnY6H z5OIjqDj1b%5NQ$VRFKnnM0!L96^yxzh)jsgD(JB+h^&ZgB8o86Wk=*dxbJ2Dcb^s^ zg}D&95qVTFkL5+=L*!S%k+1-wAR<8peOd@n7*Rw8JysNvh$yCloEArvK$KKLk0l{W zAxf(tg=G+B5#>Y_jdlqtkEnpCsDl2egs6MN~snSHa%aK-5IkQo+8|M$|#n zRZ+sVv>u{9qJawX5kPo|h9VM~rF=vqL}L{ss0pGeqL~WDTysPVL`xNnxmJkQh!<4Q zr!OL2LbOpq#@Zsllg{h8V6Q%83z(k%&<$ z7!9KlV-RCiq;u;ShZv8Tpdy14uOcQQCaK^PA15QGAf~FIw$l*P5i?X!xtWO95VKU2 zbM1H?F&puQh-aU_J_qq8Vy+7M^ex0Z#C#R>=>o*th=nRrx^^r=EJnPef<9e>Sc+Jt zf*xCrSbDXyo*?^!d+?w-$SfHtW{COiFJtei1$^H^$m!Ph)pW!^$!r65nEJ{ z^$!tS5g&Oh-50Le-t7)B83X- zAB~7X#HuLi_9Z1E6(UXr)lH2^gGj4_86+Jd9+6%|QmETX21G_gCKXgaGa?Hjs|u>0 z4Urv@Lk06gPDCz5ZWUGw`ved~UPL|>K8#2MI@@AF2xYV z5hYZVbS*83NJ5lS!MH4qD1#`gg36Uclt)xhLFFnUDj_P1c=p-oRS;DX)l@KVS4Y%9 z)Ko!TY9VSP>ZqVDbrJOt^;Ix$H$Vgso(d}05aA;lsbJo2jA(*rs)EWjLo`RUP{F8d ziD-prt%6bc0^&u)ODd>-8$??~I~CNnJ>q3V2NhJVBcchj>K=mFtfffC#Ffasv^A5Q9}P=7u1KB8I7;a>EfL5F=GE z-UOdl6qD_Nic0 z?nfLz98^K|4bw+tY~e5Z|jHL1z$W z5$9BtbO}0-xPZ8*f&^VcTt-|`L4vL#en4DPL4vL$ZXkXXQH})NMEr#KSp^BYh4=;W zs|xb*8{&7w?f(>a5O)!Os9;{dhxilmmkKiWH{w3xfeI4z5b+Px5m5|LTm>mCfhdVcQb7t!Axb04s33)9 z5#D0xDo9~vL={9;6{N5lqB^363Q|}TQ43L91u3k9sEeqlf)v(AG(ZGY zkU|g95aFvJV~r4v5lvK(v8IS-h~^?Hkg*nsmWWm=nCV(0UO>F4f)u`lXoG00f)utx zv`4(Gf)sW@bVPJgLB={Gx*)o$AYO8#7_EX?Y7Am5Vw?&RG#)Vl@u~_EG!Zcg zFB37U;~12IPh33?MT7x9(~=COH*`G^H7 zNYLAeg@{EeNYG-$JBTGJNYGNmGQ@HfIkf8O5 z_YoUZFb8c!Y(jjXf&^_wY(adef&^_ve1zDhf&_hx*pApCq7n((iTDJuO9lD(6tNre znF{jpIbsju3l-$!OT=EpS1Ooc_96Bo4yYg>2N8!5hgFb|uMtNOM^%uIZxF{2->P7S zIgU7iIH`hsoI;#Ne5Zoye~&nWIIDu{pF^BSTu{Lg=py10;<5^=e+6+B@q-Gge+_XR zapQRr6GHWWMBGIDq=Nb5XT&YUFDj`1uZZ6ezpJ47w-I*`cU3Su{DHWK_)`V>_zUqj z;=T&<@c{7<@sA4f@d)u4@k9mrc!~(IE!ro5*zLd0q+>$JN0=?OcYq)wR4~ItB9bAZ zR4{)eN2EYRs~|x!h*(6*{}ibZafs9^NKhI?T0}Y(%rNnY^oR^9$XG^1CPZcxWGo9J zDeNRFI(Zhzf{`Do9WzL}f%36(p!Cq8g(5e~KE2 znuuB|NKkD=9YkFfB&Z&uKB9pN5)?pqh=wXikdJ7DXe=U(1T{f4MKn`Ef|?^*AX=*6 zDAo$m8u5Y(GWH_kB}5w)WUMWs9iqJo=CPL%9S|K=*nN8k2%^03wJOsDcy@LJUR>QNeL;C}J35xC$~h0x=RX zN(C7kjTnO%tAdP;LySjEP(j9CMNC9Y5)n?uCL^XGrm7%g(-6}UGgOeVnTXdAvs93= z*AcT3Z>S(+a}aML=BglLZz1L(=Bpqd3lMK37OEg0ix7(u@2DUjOAt#D%TzE+Ek~?C ztW-gQRw3R+tX4sS-b1WGtW`mR)*;p--d90_HXt@4HmM*VA0Rd(wx}QC>K6W5>B0f<;K6W8KMeJ5V^*=*=j@YBZ?uP`wKzxbVtAhFCE5ts;eih{7 z0OBCxkP7l~81XgYhzjy?6!8t>m(0* zs4k)&qP_|e)Bq7ccq;6ENU$NoM>JAF#u_7s-YNb*9&BE(`9^!hu9C5WXeNb)koa>NQ1BzYxb z72;hL%yg>}?;+NRh^BID5$h1^RWQ@NkJy0NsDip|LVSSOtb$o;3*tk>Ru#-aA0f6O zK2|~Hwj*{RcB-IqpCEQ2K2?#+^Q1;vd8#5wTSNG2#j0sS2telAiZ} z*sZ<)!*2ims&y>Y4@X2GB2`e^WQZt4auw7z1tJ;|qr%!{p8$eLiAbe_+QuPLBhsj# za%mCi5b-J)x9JfX5E)f4ZZjb=BeJNVa#<1C5ZP6far=@3krRzsbEwVMifC5RYBzv5ycS2RZzJSh?0mT71X5^qBNq63dUSnL^(ux z6;!SQq9US_3My9_Q3X*|1;^WJi0X(MDyVHuL@h*Z6^z?Dh`NY+DmWt7M>IeLRFDr3 z(GcOQU}QH!G)6R0!N_ikXohGm;@P(ywLr8)v{FHWS|eUSyr_Z%y@Y6kXsd!z*$&Yj z@v;hP+X2xL(MbiB>x}4v=&FLsbwhMV^iVDfR74yjdl+IkVuT9nG7>QgF?w$;h-oUQ{&d6)#7q@b|24!c#Oo>;*|QOEAm*sxX!<5%F5)c}BxoLDK4O6i z67)7=A!3mV60{ic4q}N460{Vt46$4V30i?ziCCqgjGOu2MXW}=r-G5a2C){gPDE;I zyB_gAVuK1Qw-K=k@qr5FunOw(HR1^3sE9O->~9dq z5Z|g`+#W}qK%7)TZBHRiBfe9?xcwe+260ve)jx+gkGP3#@vsHn~0xOP}`pow-CRmptipvenb4OqKvEUZNweKT@_UB55zshpCZyy zxxWy9BkrqUPJV!Ri17EyLBt|bs-P~Z5OIjqDi|+m5NQ$VRM6}3i1dgIBA$KMRYpW6 zL}nHASQbQ9L^c(ShU|zOh@2|u(_Dz$h&(DdiseP*L*!RMpB6wAL?o!7PYWRmBZ{b? zPm3ZF5ye!H_2P&Uh>|Mk^&~_oL}?ZDdKpAnL^%=hWW7A10-~Y{vR(;M8Bs+AS+9zy zhN!NB%GE&BMATA2NS6N)^<072;jQY8BM>J;WNsS`}1o9b!G=eHEO;Y(Q*8Y*ImOKR|3oY*9gN zKSXRre58WfZbN*G*sg++y#uim@rj6xjN4s^PZ7ISP}|QCpCk6DVBCIz_!6;K1>^QB z#6HA+71Z_s;vnLX3XZpj5nm&YsG#~s5#Jz=sbExoi#U!rp@Q0;M4UpLRzc;yLwt`o zqk_tvMVv#NSHV&J0^%a#k_u{j8F2-1RR!br2gEhRbrp=;8;BnfH$`Nk`adClM%+?C z^?yP9iug?hBl~y6ZNwcFl55zqcd|P}}>62Z)C%7}@_I9w8p9U}Qf* zJVk`qTK|0}or!#e+ERP}huzxyKU9#92t*_znF_{!6e2kyg$fcBjfg?Ssvtor5vdSy zDo9XjL>fd|6(lGfA|8=m1+zm2L`Fm=6(lG#A`2p`h|JVB8zMU*hYD(&6Ojv%`#(h< zL|#Nb71TCAq5z_x3Tm5xD1<1ig4z~A6h$PepmN0!#StY`P`Q$bBt$6{RIW6l45F+G zdc7Q?JfeaMdc7i|5~8w*ER5SKh^mNcDj2ub5j7AsRip^9^TJw)+K4(TVx6dqsE4Sp zBF>2hhycPtd@GemPl3l)W(Xo+ZrXssgAi5CzrB3@Ea z)`>QVwup8jTr)y~?GY~{I;dc8J0dzEI;*hj_>f>1L{~&N74%1UL=Qwy6?~FnFGO!d z9~HUXR{A3PAzo3D&x!ts0f?ZAf=&!X3_=W6L9Y)%3`Go6QOd1jIAR21q>2hoj6#e? zj1iG7nG<6X;}GLjkmL!7R}m9ckkd(s$%rW`sN7V}RBjgHb;N8H zB>4@*9K@R{sN7t{TZnlosLOoB0>s-Y==Ft&MTo^J$oe~oC5WXesLL|Ma>NP|Zo&-- zu0*UtysIMIiPebr5NlM>r)v@G5bIUYWA7t2AU3KX$(s-#AU3O@$F?9oL~K<-l0QOh zLwu})`C~g`2V$p+5^irlLF__&s)AnMjra`lxe9uH58?~Nmm=KI4+-u?e1+Jjf?nT` zIDk0#yhs^>ID|Ni_*w;H?g-*2;u{r=xnqcL5yw?9=1w3^B2KAb%$-JjhxlFvBkBy| zEaIFB#>;uc1;j-a5dTcf@TK^ywYMUBn+M=&^ffohu!|`TsvhLDGWzMAR<+e!eodjL~<3(A1M&gh!_=i-`@X$NQp?L zf}?31A~hn73VJLpA{`=L1@lLGLg#{4_h(iA<3L}ajimIS4iHKr|;wtQZNU#K=BqB)#Gk+;W zX+#+nRIV(d9HP7m#(o7vMMNbPRIW0j3ZkkC>QW6+9Z};yMNLF4L~Rif%$9W!brJPc zFtY0-8Xy8Hm@Pd-Lxlf-Y~6K~6=&AB;hqF{cXxM(puq|5PH>0D-7PpIcno)Umk19ua2tEx`dv#XlP^kh^7IwZ6zq8g&Q3hrqQL`_6372MO> zh&qV6DtP18L)1q!P{BQIh-idptb#X86GT%)GZnmHnj=~uz7`Rld)gAw3ej2xliUW; z7ST=xbJ`x!0nt$fcdQelGop(M-VR+6-4NYX@a%^odLVkL;JfXG=#A(jA_C8CUqnAd ze-*qf2OtI_2B~0z1|xO8#7_EZ&7=sv#7^i~W{{}H0F+l}y zx`~KMh{+-%@}`@Dn2MODf;%=H@h##z72L5Ih?$64D!3oB5pxi8RRlUDbROb+#1AUC zA3q{~Lj0_Px72*Z0>nZU%-ABtV#E>^JabDC%Mi;|@E%)%SczDrg1f#Nu?DeLMDQQ= zg@mp{tVe86!9CrG*o4@uf;Zh3#8$*M70lRn#16zx72MNZh~0=iD!5~N5&IDPRdB}+ zAbvsos)FbCH^lFVgDRNhLx?{Re~O6A6dpz#K^#@V9Xp2j3-PxKp4)#A#}Ow~aK}y} zP9aXK;EtU^oJE{d!4#fHTtHk@!9Bf%xQw`>f;)BJcCsNlK%hzLo{?|%foe)>1*C_J~H1^zVf`yYsKB7*;rHt_!dgNT5LsDis5 z2@x3)MFmqB6%h>)T?KE;7>JmNSSq;du@P|)aaHj5k9dgqhy*H_(}ajbh{P(G(4cL z94h#GP)h$JCWxjg3J32;GemPl3lYKpf+q0q z|3I`vv{J$M(i+hQ(N+Z$)DF=e(Ln`ohmMF&h|VhbUb-NG3Vs3&LJUR>QNjHfiWr6%u7dk90x=RXN(Il{ zXv7%ASQSjrIK(%I@hW&KCm<#wCaGY?CL^XGrmEmwI1Mo!@vRD;+wTxF5HnR24&KvQ zh}noaB4Y8(%|*;Ze6ND<_6Nj|h@VvOz5I-rk656BpB@Vlix7)d@Y7=nVku&o3Z`&5 zVg+KQ3Z`%sVl`roio(JBu@`=jz zy%Vtuv0DXGxCgNpu}=m2vLA5(@rw$k@K?ldh~HJPF9#8a5PzuPuK$TRj5wl#DLjfe zhWJZG;ov>}8}Sd~xQO6m8xnc~aT0M#1$X^4;tb-f3Vt@6L!3ukP{F=jL|j5#R>8hp zL0m;#Q^8%oj<|uise-5B7UDMIjtbtmcMFDA0Qqg9;qlCykn0MPY_Q<#N~cG zL;Q<)u7dCO1>z;*l?vXcuMuw$Z&fg-{~_KX-m757J|I3KLIS^6@af;A< z-~R}F4gCIx3hr1qM0i9570hWwL?lFH6@`O$ED9njBASTcuQ~<(|6dR>5HVHocbHg+ z*oZhPnA5n3c!>Baxa$cJ2@#1@aMu$fk|2_*;HOwJL~=w573@n&L@GpT6-;s(L|Q~T z72MPGhzy8~B7z?y@bCXXWJY9B!TrdJ$cD(Sf;r8B$cf0MB0=yfxe<8~c~u1FDkL-? zB0r*l3jRGt1rdc1g;k^vo}&n&D598(tU>VwqBx?2iX1^v5>X0KT1CE~D1-PCQC3C4 zpeTnZkEo!cNKjNnR6 zi1sSN1w{u$M?@zTQG%i~q6?y{3hsJ0M0Z4}inzgZ^g#4P^isiH?~Uk#=&OQR?}zA* z7@&fE8HgB!7_1^q@N*173`Go6!Ojgwj6jT3!Oo3Bj7E%6!SgZ}F%I#Kirm4^F&;4i zF;NA3I|(rvF-1j@;5nuurXi+_NW|WLi}(&PLj_ONOvEh2Y!&SO9K>A2JQX}q-y?oN z{HTK6{|WIkV!jIAg$oc15sOqXABz!75KC1sAIlKS5i3+MA1e{75UW)%A8Qb65$jYi zAL|ht5F1r6ADa-H5nEI+A6pUI5ZhHSA3G2`5xZ0{AG;BI5PMbd#@&b5k2s*BNN|FF zLHvsNO+;eeAHO3GA`Yow#{NM3i8!o+89RbFia4f%8T$+IH{u@^ykU+bP9RRIU^e+0e;e*Yuz_0zv=Ct>$LM}$L!SHbQ_Ktx1DQo-&=MnpkG zRl)8@Lqtc!P{BJWCL$IhwhHDW4k9iho(kq8J|Y1kp$gtXi4chqNmTF-N{UE^NUnkz zOMys$gHABaDuWRvLdpHNXk1XJ0b@nrwV2) z7a}(zj|yfiFCrfzzY5+#1rP-hg;X$wg%L#%MO83`#SmX0imPA>OCU-jN~vH9OC!o4 zzEr^!mPM39lvlwNRzOrlR8qkdRz_4oe5HaZtcs|HsIG!3tbwSBsHK7_tc|FHsH=h* ztB0tMXrO`_YlvusXsm+wSQA83L^BmkVRJ+a#MdgA!j_0uh}I&KF=K5IZ4vEMFk|fz z9S|K=Fk_t%oe^DBFhN}r-4NYXFhQY+9*CYQn2%nF-iSUbn2)}Qeu(}mn2!O7frvpW zn2*7TA&8+W*!^LM;fN6`*!_`+QHaqh*xNCPv50Xh*xPRq;}H{7u(uNtlMs_dB-n z#6lH3w~G*q5ld9Cb4wA+5X)8Y(_;l0_8-J?#0eGb?McKb#Ay{gw`UM%5$9B}`{xlC z5EoUjbC(d85m!W{Waq9Tt|6|gVCQZiZX#}};HkWgxP!Q>f~WEx;y&Vm3U>b?;t}Gp z3ikF1;wj>p3ikG2#B;<86})j@B3>b0t6=xvAl@SWQ^DT8L%c_PP{Gc9M1%xBQ{dnK z5%~J)-=tHrbDstNH1PW$h;S-+Zo?xYAR?;Zxs8N~jEEv46?+>M5e*St1$!F<5fc$h z1$!GC5eE@h1wU`&A>tzvsNkn*LPR1&Vii2uNf1d9$yBiW$q^|KDOChIBs3KwH6o1) zc0Vm59U{F7c0U6mBO;Rup8d>-EQqWsn2&6T?1&sH*!`S{T!`E%*xNjayoh`%c(U^& z3LpxKNX_mSLKH?6QNivPMHEAPp@QeOIHCliqzd-76rwbuj0*PlOGH^jITh?}c|-+7 zMHTEzB}8RJ6&2jmuMkxc)l~3ARY%l7)KtM;uZ5_Ms3Rf`_p~me9-_Vq?r8%=LqsDL zJW-7iO%P30@GfkIXpU&1g1i1Tq9vl03hsJqL>oj~6@d;3ZHH)&=%9k`-u*`r#}I$12z1~#KoI{Rj;r8_Ie|EdIHiKO&}qaO#90;0 z$2r7##03@1$3?^?#AOxC#}&j?#5EPn$92RF#7z~<$1TKd#2ppP$6drd#C;JNn2!gD zhlocicrG6!o*CibNWq9~%63f{P1Ac`YOsNjhzi716At%AE=2Jt1LtP1XW zIYfCx1r^-&iik>x$||_)RS;hxs;b~W1yw^-N7PWkGglK)3sGAIPgEU5T|_+qv_iB-v{AwH(iYJU(Ov~R*8$NH z(Mbh&tTUnuqN@siMs-7UM}(?i)_Wj&B6^9)!X4|4=!591f;-j^(H}8D1(Q4wF$ghO z1@E9Ch@psKD!60A5hD;IRdB~fAx0y{s9=)EBE})UQNbjSM@&FWRKfdW5@Ir9iV9|Z zDqSnM$8wHmEB)} zScq7pg56(?Sb|upf~R2_VmV@k3ifs-VijVw3if3UVl85w3if3^Vgq8M3Z9ouh|P#C zD%h8;h;4}NDtKOYAa){lsbF7rBlaNnipa)2-G|tZIG}>(*y z5fmYT-~12!21wv*;5R@5UqAi3c8(-L@i`(KBD{*gIYUAtAR;0nsYn$Rkr7c4QB|Z3 zifD-Fh!`r^mzaoHh}bH)>v0fq5%E;y30@^WA^{?y3hrqlL}Ek|6-9&RNQy{?NG>Ah zXF-tykrI(gMfjjdjYxw?t0HPpq(h`fWKhA*Wkh5`WL6O`c#bTHtcYwX*xT%g9EhAM zQU%YE3y~XN+3!iN{PtD zzLZ9kL42u#ds-Gz4pCkO_p}0{BBGLtguy#j8BqoCl?tAhs)%Zc>MFRWH4rrswN&sl z)JD`n)K$S1y58bL}x@76}$_(BDx{EtKgXnMf5=QRKa}oLi9%TQNeulMf5}T zSHXM?Knz3-Qo&O>7%>DfR0R_>3^5!rLIuz5NW>__XcbJ*7{pk_I2FvtH;D0w2`bqA ziHJ#v$tu{}DTt|vX)1Vbrz5^ad?z9gyFUXl6ERB#`!X9b2QgO#`!WylJ>mxy-1Q$3 zKOufr!BaUOu>i491<%|f#A3t}72MOMh-HZ7DtP8rAXXw)so<`!Myx@sRl!|fhggr; zpn|);5wQueSwvp$`WD1i#5NT?bK4O+5Ia?{FS`)C5qngyFMARD5c^dy>jx0OAbwTB zUH=X7JK~@U?&%@KABaCya8C~-jv$Vz;5~K>@fYH672NfI5XTWGMC9Y1o$x5T?KEc8;F~TTPk>7ZX@m>?y6wt z?ji0Y9;o0g^$_t0@mK}V+!Mr8#4{D_{=bOlh!-l@+n0z}h}SCExi^Tni2p?7=Z?KY zyhnUc!6bh~garO`S>QK70$)G1TmI4g3ZOBAg2DM|ea8L_`(bk4T8fh$t#} z!$d_yLqr!*fGLcDh>3`$f*FgAh=Yi$f+ss3B0eI43hqZjL?T3D6--bPL{daD6@0hJ z5h)NURq$k|LZn8dQNiw~MWjQdSHY8=0g(}rNd<4q%!n+AtRf0>$Fd=^BXX$VsmzJU zg~+Xf8Owvni^!*fdzv3n08vl{Ggb&u7*Rw8cfBa07~%^RJh#OWB@iW5FsG#ur4eOR zuybD`$|B0C;JGc2sDP-bf=RA~sEnwhg5CcLQ58{51v^(AQ3Fv^L?Pa|wGg!tbyRT2 z>LThP>Z{<6H9#~(G*ZF+XpCrrXsUw0k2OOyN3>AE9s3&5646Qp_oFqU4Wg|I?ngUB zdqf8jg}EOc5uFg7RWQk25M2@7RPc7_jtE8cP{FMCMD#-RR>58GgXoLsr-HlQA29$i zPz7^32r(EjL5tO|a4~dh{K2@Dwv?7h+~MqR4_q*BmO}gSHXOoK%7LJQo($jMw~&MRl(cg z9O69Uf(j<+BH|L_vI^eXR}fbb*HkcL*AX`mH&rlWw-C1xcT_NAcMLIrm{B_b6fwF;j7G>Ei_bSl`F^oR_Ij4HV6nGl%~Syb?|AuA#qBD)IqB?lrW zB9{vGB{w1uBCm=-2mTEpfv=-N^9R1x3@s4&`su#>k0-kzq7b663ZB~{h@yyMD%iO% z5XBKCRPa=mM3h35R>4zQ2Jt1LtccIpmvV^mhzcrr8Y&_xAu6k2U#cL!LR3}3U9X0y zj;Nu6yIvDf3sGAI_p}b8E~1_a?rD8Q14Kg+pYw)kglLRtqJmj(ifD#tu7X)_f%qEH zQU&+46{0nwjSB8*TSPlVdlgJ_2Si6iCl%b&&WJ9Et||f@_s|qJ@B<=XlUT; zr*rx__p}G1C!&{%oFRd8^hWeS^i{!}_CxeX3{b(G4nzz>3|7IM4nYh>3{z1wB=9+g z2mW+a=!n4AnxP{_gkx_rAB1<%|j#Ad`674bp>cWf(S8)CZ(p2{7Forqm3cq(@z_8|7EU_SOC_9G6c2z21z z0225*D)iUD*P5Zf1-^bdAK}^k-w_89hg7ipe<1!u99F^39YGvL98}^IwCPZcx>}?iARzx-tk=Wbph#ZKVD%jgxh}?)gD%jh+hL5xRCP{9OEL`*_V77>NrpMsc*n5Kf=pN{wz@tq2Ge+FVEVwMVa zZZ={LVy+5yZXV)$#1AUixgQZfA%0fDzRX7~KrB?jzAQp4Ml4amzAQy7Lo8RpzN|p3 zM66Q5zN|*9L9A85zN|y6M{H2Rlf4nK39(s3RDL#WL2N~AQ^B*p9kBzkQw6)f3$Yuq zM+LjT7qJhqUj@5=0PzdrR~07r0)HC#{SQPq73^Gi zL4cL94eTgoQPbA+#+Ip7W|}n5P1>#RPavDk0^jBsDiyMgeZ(CqJq6GiYSKo zLIwAuIHCliqzdjwDMV>R85P`*FA-%CWCVMnksnaY9VSP>ZstEtBa_IsIP*%-T=`M(MSbPLt{h}L{k;adNV|GL<<$n z`qzk-h*m0ipSDJ{L9|uD-nK)uM|4oZ-gZQELUdNa-gZHBMRZfa-gZZXB6_G`Z+jwo zA$p65#m@CX^hNYj5jnW~{SgBY169Nfib06Mh#@NC2gOjtFvM^biGpGTVkBaeiljj? z8ZibjRzD=pvsrBNnLOt}jF^LM&FnU0;G&idd#1 zMeuVhN31}sR1x@T9TK_P1^coVu@13bMc&{!HXt@4Hi?K603o595nB*jRYVAi zZHVoN9V(dQorqnC-712Y3EhL(i`b`vyS^WB0P%|o?&+_H-w?m6;GP~t976n|f?59) zaTsw#MaJMKJ&HJn_)7(M{cprSh~p}F8crZiB2KBu6}-x6#2Lg{73|zO#CgO86+BTF z5tk5`Rq#YzL0m;#6A?G?F+xJGBW@sWs^FQsg}9Blqk{Rki@1lluY#xY0pcOzkqVy5 z$A~A0rz)7BXNZ3h&sFeLzCgT0yi&mgy+*u2yj8&j{fBsmc&~z|@&n={A|&vW@6*3Y z$7RO;7x>e_?|%fo27doT1v3`>@8Cv-hDQYd$$yx0Fk=xBkr0tpFdtD6Q4!HZ1pg`( z_q&4I(WfoeK6gJt6}lqYCym6CyJriwbrwDX0KS_L~-2Jt1L ztP0-iPXoqO8;`87H zbwG4PbW*_tbw+eSbXCCwbwhMVgsR}}&;!vE(Mtsr)Em(U(N_f%)DO`gF+c?qG!QWe zF<1p}harfeh+!(2vEhgjh>hL|oQ_>VsVzyE>w4lzT;=Rq+OF$*zU1$#ROF&8mU1#gG%5kDY)RKecY*fLM zy$P`yu|)+tw-vDsv0X%PkV8UuAa){lso<&Hjo5?OtAd@|huDuepn|9J7sRiK-&C-3 zzatJJ4yj=0{y_YRIIM!5JAychIHrPq`3vzk;vW?}QO6M{5GPfzFQ*Wv5oc8J%$-G? zL!4K^&RsxUL|jtAQ+XM21#wjcdwUIW9dScMV)phX;uhkziqC_)e+O|FaZd%ie;@Gx z@lXZ3{|NCI@k9mB?Nh`v#J?)o{pW}mh?gqZ{a1+Bh&L*DC%;Afhj^!g`FM}`fcU6_ zCp#oC1A*WF2z(9v{zu^Jr+-gQ%mjUo2!{x-BG7?<{|6!>B9aQ8{m6(Yh^Q)L`p;|70g&_L>fd|70g&VM0!L96}(|GA~GQ|t6&PVAhIH|sbC7TBXS^es$dFp zA#x+~s9*~7BJv^ft6&NXAPOQ1sbC5VBZ?r3s$dF>A-+HqSHX;xK$Jw3Qo)RsMwCH( zse&0RiztUEuYwt?fT)P5q=Ff%jHrV6N(FDas)%Zc>MEGR8i<;RS|WnCG$gb(q7I_2 z3Z}3gqCTR53Z}3jq7kC83Z}3LqA8-83Z}3*q6Ok>6-;4EL@PvV6-;3pL|a5V6--cj zL3{}C54MPk^j8MUhjYNz>j8?&njX{h>j1!TJ8T$q?9x*`$Gd2-12{Bm( zGd2Y=6){Z(Gd3OZE#f;BOwbI(OvEe|Oweq^9K>7|yvODtzDN9^g7?^uh@TKYt6&P} zBNiYQs$dEiAr>Q+s9*}0B9uV=rPKV!sOJ;{f6p#IGuN2mOZl z9dS?v6Lbji2jWi^OweJ(5yVjyOwcjJUx>d|FhTzyjw4Q}V1iB}P9aXKV1mvd&LYmK zV1mvgE+8(d2y{s3CB$XK6%|a-Rm3&Kbrnp|4a7~vEfq}AZNweKT@_5wJ;Z&)0~Nfb z9wHtg9;@Ij^#t)0@k~VUhJ}Rwi+GNB@qdb!h*yZ$Dwx7Ih_{IUR4|3_5bqHmR4|1f z5h3aM{g1%cPyZ&Jf+_qg@TY;_|3HLO!4!r^L_kDT!4yV9L`FnW!4yVCL_VC#B2qGAnGsnKSyk|+%ZA8~$f1H6%ZbQ^$gP4I%Y(>^$ftrC z%a164D5!!ND}*SFD58QHD~c$F_(BCURvb|RQBnmnRtixXQAPzb_9db$qMQn5tURIu zqN0kxvlJ3q2~inQMFlhV6{0GlnhIvDI-&-mrV3`P7NRzyjtXY1E}|Zyz6xfn0iq$I zkqTz4F`@~gsfbj}STjU(L<<$n*w=`bh*m0?pw@^sh_))2pmvD%hz=^4ppJ-6h|VgQ zpe~56h;AyFpzerJL=P2AP)|fJL~j*LP#;8JL_ZZwP=CY##6T6ir3N7eBZjD8f`%f7 zA%?49f<_=lB1WlTf<`08AjYa-g2o}fL5x?y&&mmiiHJ!mn6b%-DTt{mn6YVy>4xvOQNhj~MI1x?rGlON8}Sd~xC);A6NruBl*euOn_CZmM8!Zy|0Y?ubas-rhyrL)=%v-abG)L_AW#bNd+a1o8C$ z6weU_`kHR85%(af4hi?h=ho&f(eR(h>D1&f(eR_h=GWyf(eR+h>eJ& zf}ag>5%Cc5RWM@-5D5{9R4`+S5lIk9MFhXukkDj^8P)$TFL~Rujg6F7%sEeqlf(fdRXn<&_f(dGb zXpCs0f(dGhXohI6f(dGY_!`kt1ryW?(HhZ41#hXgh<1qfDwwejh>nO(Dwv?oh%Sh( zDtHHVLv%-ks$hb8AbKKtso)*d8_@^RR|PZH578enKm{{45HScbSOqgS1ThpbOhh09 zyFVN;0x?oWgrFFO7>yXCg83MW7>D>q1@kc;F#$1A1@kcpF&QyM1$#RcF%2mz%k1E*xpAbJI=Br@$7a$fQ7O7zO7bBJ+ zma1U)mm!uTR;Xb2S0Yv+R*MJ@a!BYJ#9G8U6%m4BJz@i5qY8F^6Jj%BiwbssD`FdB zy9#!H2Vy5;mkQnvyAgX3dsXmu*oWAUIG}?0_yzGR;x`q{$M1-Ph(jva{XY;+P8N<1fVDh<{WtAIA|V5GPeIAEywf5oc5|A7>Hg5a(6!^W_5KBI1&WOw7k+ z#1+I<75sd;hPaNnp@R9iiMWNht%BXZgSd;hr-I$Tk9dH1sDih{BgA9G6BX?KQ^YgG zzbe@M=ZF`GmnwKWyh6N2yivi=hPR0S5bsp5`|lAS5Fb^r`yqid1%Ce{@HO!JAAzr* z{!{Br?EdG7aES0K*!>8Ih=@okcsoQ!L_tJV!F)tRL`TFBk(v33iHL=Wt%CW8gNTcW zr-J#2k4S(>sDk%LB1B?D5*18PQbaODaurNa3PegoDiypxQX|qJ(yCy_(jn3#GN@q2 zG9oe|GOJ+5vLLb|vZ-LkvLkXJa;o4Rlnap?kw-D0xD%kzX zh$@J$RIs;I5!DdYRj{`;5H%6CRIsu{9qJawD4h<2F5RFx^b4?IU z5zSPvbIlPg5MQfc=UO6KAzG_o=h`6JBHD?_%FeY%bU<`e!OnF;bVhVh!E@Uc(GAgE z1y5xtq6ea<3U;m+qBo+C3ihQhq93Ba3ZA(Eh=GVfDtP7wBZeS`st9yQ=rF`^#0VAa z+(^VI#Ap@l%NWF1#5fh~%QuMehzTOHu`d%5lMs_turE^(QxVft@J^nN_!jY<3U+P= zVkTmi3U+QbVh&=i3ZAHWi0=_Us9@)QMEr#KSq1wtAF%+jP(`3aLKh(xBbKP(nOllj zhFGqGom+ueiCCqApUA5bYY=NyuygAW>k%771TPX2x)HGnu~`K>w*|2ku}uX#w;izq zu~P*{)0Gg1xf}e!<5cd%eRIqao5swg$Rq)I`K|Dn~QxWLE z?|&ekBVMRrUtS_!AzrIsU)~_zBK}jszPv-cM|@DhPr{FgkidVK3Hu91g6B3NA`v373U)sUA}J!73f|hu5h)NURq$k|LZn8dQNfd)7Lg8-UPWNf zLP9ejG9ogmV1hCuvLLdmV1lwCvLkY+V1jZYav^f7U_SC7@*?u7U_SCA3LpxK2wo&4 zv=E{&qKFFSqbQ;n;tLhL9f~7LAWEuWf=VGuBg&{?g1$tQMU+#)1eHfrKvY!0`=b(~ zGNOtKX6!3ORYWxvOi*=14Ma^9Oi(REZA2XvOi*1!Jw*M$*VdUneYSJ)o@#(;h-nn~ z8b&ZfjS)={O;s>M%@EBIEmZK1`5Ms@(Mkm~)EdzS(N+aB)DF=e(LqFRW~d{g6QZ*U zW~d9IE25hUW~e(N6wyNkGt?8&3(;EzGt>vs7tv1zGt?h305MPnGc*V>7%@ZzGc*)2 z3^7~<@2L@pk%&<$n4!^#F^I7$n2&LYZxG{E@D`eYn24C9f(e?8n1Yz9f(e?2n2z{X z1#hA65Hk=nRWM_-5VH|;R4`+65%Un=t6;`{K>UdKNkkrI>}SM$!~zw}*h0i2#9|f9 z*b>B2#4;63&~n5I#7Y&hg73>!h}DQSDiQ?6TEsfUdKHO-Vgq6$Vv~yCuhNHZMr=WB zRT2CZ!_aMr?T8&Jg1_Jqx)ZSrv0Fv(zoHA>gV>AMry|fHq5BaB5WlDh{^c?BSHy3K z-&GV0ii3zlh(A;m35q`vhY?3aD@m58? zp!g5*4)I`&8t%*X!%e;W7=kigf#Z-A&^KEffwBO<6^_ah=AAtI|_ zZ=)chBBH6_$&QYQfrzPsor{HtjfkUyor{ZzhlsC&eMx{wh)AS@eMyW+f=H@@eMyE$ zj!2<`eMyN(g-ES}eMy5zi%2ITKTmdgL5;y1W^=GOa=4t1)?~jgbLpLB@v|%rByIN zWe{H?%Bo<3$|1@lDyRrNMIoUT5tR^?RWM^!5MLpxs$hbuA*v&4s9=I>B5EOOt6+la zAnGFOso{cJ0esC6VwCI6VXcr6Vw~g2hmpr^U)8{A2C1$ z^Dz)H2r*a%@1P-wp@?BBn4sZ^5r~m0n4nRJ(TFiBn4qzUafokJFhS!H6A%+c6l8)X zAtocHs9=JoBBmjxt6)C9MSO>tp@R9CiI|0$t%7&Z9K>A2JQYmP_lO@5KdN9penR|= zn6H9&&;rCl#3B_;&|<_A#8MSZ&@#kw#0nL>gH|F|Ay%tk#?~O#BG##3#?~V?AU3LC z#x@}~Betku#_;3>!5iim#IK0oR4_rm zBMu@CsbD_-K>Ud~tb+MCf;fserh+%jUx>dE|EOSsjw4PWPO4ynP9aVs&ZuC5&LYks z&Z}U8E+8%yt|6`?Zm3{_ZX#|WZmVEE?jY_W?x|ot?js%`9;#qI z9w8nho`e-KL%1JL5zi3+2EMkgAN&ubHy4h%wPESa?UQegof-N(@W+|z2mh1ojcsdh z&z%+eBJc;F_Dktk(yyi8NWb;IIdMkt{Qr5|_1;Op_cs4Q`lED6maw1qf6||M+xb5C zw(E!Uw)ODR5rQ2#cz?F8x;1o3@LPx|B$BtCGP1XwGK#mYN0pA|Z9ckm4C$ECv7}>5 z$MLr7$CZvJ9bYbROxv-gf?c z()pzeNEh_B>lO00^A(mZ;%&aDxA|hyUq~1Cw&P29+wmp6ZM~FqY3VZ3UwYg5%1W1$ zE-zg{x}tO?>B`>rd8R8cytxn%;IuE$Q0Qb)@SCJM1Ou z390XGmuw*2(Az#jBk9J{O{AMjHwYeUS8E=^@^B{7`Q@ewg&|V22&y5kf|K+bKtR+bKs&kC7fLJ51NU{3Pkg-gf*HZ}U^VZGD>bbZ_(DN`L2V$ItLKKhxXRXGzcYHb2MP{9JEa zpC|pjxA`Bu&HpITT=Wyls8Ex2^B+w)LITyQFt} z+wpt6?fAXkw!Tk#f3U-D*#RNHc-tv|^|n*~=56c0OCR($e@OZdZ#(`^>BHW3{1I>S zN2QN>oBvDtZ|Q%ek9*tsPk7t;PD-EhHh)_BjJF+sR{EUudFcz@cK(arcD_r}m!+>r zU-h>0UGui{U6;P$ZT_aW`CHPrrSEv#@prxL_^1m(JjA=gTOa$=lAC+1q>;Z(GkQoz2^Pc5m}Jylp+Fx2@-r&h2eJkGJ`}()pzG zOBe9A^A+^A^A(aV>}|e?xA~&d#iYNGE*|W#k4y<6CB5yErM&HurM+#vjJK_SDP2~& zoOF3_J6{EFJ6}cVO2H2MEmU@5r>G+RmA9R;s&qB!>e4m5?ff;p?R>SQYkQloBVE_q zj;|+OU%G*}ov)#{9p6a0v2+vZrqa!%n@hKl{@U9uu+2C*3~S zVYj@4kdD%wyzL`&_O?rOk?!hkzMHrC?$V*sJ-qGsp3=RfdrSB6w(Irvw)6G#w)Otf z1H8=-^fo`p+tvq55AilX)Z6?pZ(ASkZR;bvZGEKlDCyDCW2DDQkMp+cedBG{8!tUU zdZM=-KgrvUpDaB^daCp^>FLtndfWBBlb+#i*PkgpOM15S9O=2H{?pt1VQ=$Cq>oA;^S0yv^0wpumj1`v{Bh|M z(kG=)NuQQJBYoD}KF>Mn^WOG(E=XULz9fBF`ii$*@2a<*|C+b0U-!248{W2l)7#c> zdE5GJ={w%$?@HhEw&U+hKahSX{Yd(;w_Wdvx1Il~^fPbs|9YE$F8xCKrSvQ5*V1pK z-+J5i|MRx%zw@^B_tGDvKT3yW3%sH}`AvuadE5Cvlm1*foVQ&+ytf@6K{}$h`AFX8 zBTGl|HXl_wnsjt;J6{ZMJ3gkjt;dp%EgeTXu5>)<_|gfa6MEa{NhFqk@q@1@MQr_FvD@a%LHeboxd}VK2ui|a%UwPYl zRc~9b=56cMrE7Sbujy^Rmbb0f_O|sp-nL%X+t%wz*Y`Hxz}tL7=|<9xz3uoW(oLnC zdE5D$OScGi*e(59NK5Hf-u4k%OSkd1OSbhk-%h%{bO-54DONyzTQ04tCgEHblr!>0#dX z5r%u)DMxtQ`bg3ZO4D(ZR_KuCrD59w&N#x+wqgVZGDQjtxuJn z=52nuxA||qZT&lMTc064Q+k%S9Y0%oj`UpVdD7oY{~-ON^iR@1OV9VV?`MJZLg_`) zi@ojpTjFi!Tk37=%cPfkn_m&^up_2=cUr8jun^)`Cj^)^Xw z_BOvodaLv{>Fv@xyzP2Bz3u$Fyls8A^d9NG()*1*Eh`L0Xf@V3u)Q~H+lZRtDScK*BGcD{Sk_oW|5KlHZq zJqmW%SH@!@Po$rE+edgN{jaxO;<@w->6hMizE|FM{A+Jpeuo-sxB2+e38WKx+wqB{69+r&M@-_xE|F9^nRN31{}d^N zq?Ar2o!Z+zWg2h$2x+C$NvD_2Af3_M&Ywv-vvd~etkT)M?ek^#w)5xkw)LFSxq=<` zuICn#$J!YQ|c$*(9Jx=-?>G9rn{t40(r6);GmY(8mpJ%GKoqw9Qtxxy1^>3xW^EN+2dZxD> zKg-+vY;RkiBRyAop0^$Uy|*3zL$Je+z>h+H^0rg_EInU(f%HOeyW}G2#nMZpmr5`5 zw$HQN+s?nj+tyb~uaaKvZO5;XUh8e=TPM9nq{ePs7OP`QFDSgV@&VO3^jJI9ytn@kQ^U@cjFG^pMzU*zE z=Zd#o|Elyg>Fd%rq;GoL`EN0!Ox2^vx{apHm^h@bi-gf@i-gf>s(r=~zlYS@t-rLUq!Q0ON(c9KT0{@neF#p4W zV2AyY@tKg%z3mj?yzLU1@*3rE^H<& z`K1d;7xcC*DCBKhP}tkni%1vsHebx!{1@J~UfkQ(OL*IQN$FD3rM>O=GSXj4m-V*u zmy<3pUBTPVU(wr+uOwYrx{CBy(p9CaNmrMy;ccI%rgSao+TQkg>v-Gob-iu9p0};n zmu}!~zM*s@Z#%xRxA`X0O{JSjH}|&lwUGW=x}|h0Z@YeL={DYW{SJ@J9*oBXK!2YBHh*7d^d0N-Mwu+RJw`UG!V zpXhDtlcXnmo1fxseya2|Z}Zc=&3_y0us_nib7F_g@U}x{O3#v>?QO@;@wVgVdfWQE zV2Aw(-wXM{+fMPLx1HiAZ(IM_+t%lM+xh};TVLpH>x;Z?eX;ZsZ}Uro9rjyT=EP31 z+}jRW;ce?Hy={G!^lIrf-gf+2Z##aSx2><2-r#M1qqq4@-nPD3dW-Z{Z##aQw;jLT z+tzn@+xkv#Ti@kv>$`&;_7?APVu$SYwnO$w@Ao!;K>8PNJO0;ThrQ%)PV5xFOCR*M zQylU(|A+LS-sTTWACW#Peazd=|CjXN-gf?fq>oFVkUlAW%G<7YTKbH)UGJ>7`E%0e zr7w8f@fW@A_)F54rLTC~@mGT#b_=fwx$bS3xFLO0`j+%<={w%`5$;OglfEzgK>DG# zeZEKDcK*lSw*Ex=sq{1Hf2E&GzmR_EZP$M#{aX5sw|&01-gf+d-nRZu`n|XL58mcK zdfR$P;Qs_C%s(BF{!IFF>2T8Fr6YLT^&(0~l8!7LMLMc1`h= zmbb0P_O|sn(s8}b#|w7Yj}Tu-0_lX_c8NsNiM{O-Nu-l{+wsYylS`-Yw)3TwP8IC1 z-$H69c8N67X}#@~>AcOSm(JjAKBKq!OwyUX&1aF$>TSnolg{pK$LEmF>21g7lFlui zM>?-`zF>#Fh54P>M<^g&(Az#jA?d>2c6_;vsq?EUv zqO^1wZ#(6e(q*N~Ntc(dAYDazml%%ZJ)QAbam+((lx#9{I$I8e6^+P zNY|CFCtY8^+uM8}>AuqayzPAbr3Xk4 zlpYl9uv;)#$PnqF(!;#%BM?KDzu|q~nkMXusjFleeZO4BjJzjc( zx1DdI^d#xY(o>|TdfWA zdY$xo=?&5wr8h}$mfj-0ReGEBc5nOqJG|}l@08vpy<2*Z^j_(G()+#b`Uj+c33k|5 zz^_7n^R|!tyYxYCyTl=H^M6SH>23aSu)}_YBSMaP+bNGp|MmZ$;%_1Uc-vKvd)p~b zc-#6(=~Le3PfMTiw&Txwn?L7m>*u8}c$>fIZT^zUzffieN+0Dw|&0b z(s!iqdfVr}Cw)KIVR!w3kcZNbyzL`A_O?qrk$x)u%-fFt*V~SNF8#vW{7dOq(yzVk zd~c-RdfWN_lYS@tUiw3@!``xwPVAB)xdJ5kKQ6HTpSNA|vtWlk#ph1!kZ{uBr6YLT zDI-cp^0xCu_BJ2I+t#Cc+j=zV=+ZH~?f96|vApekvAxa5@wWB2(($C@d)x5|q!UUf zl1?1#u(v#kkfhSdq?1dh@V0LurF1H9`^c%i&8Lx0E1k~Uj!!S0!Q0N4Q96@!W^X%R z7U`_scD`)V*`;#?JM1mZ>BKIP%iB(wTRM+)UT-@-pSK;K-`my;1Uu|UDCoovDdcU3 z6!y0DBGN^ri%EasZRacQZRacDZR;haOG%fOF5_+I`%=2Bx1GP7xB2qY6{IUlSCXzQ zT}ApU>8jGzq^o<|=dIywpSPyBt=E#SEnP>tuD6}9p0}N^zPGJ6kZ$O0zLB^2#?nos zn@TtHw(~Xjw)3^{w)L;QZM~(pt+(>F_150D-bT8uxA}I`?Y-^z4$>W^J9*prI(ysk zU8K8uo9`yw-P?{2^)}x_x~I4KUedj#`*_>=`g+^({k(0xzqhRq@V51V-nKqSda$?o zA<{#=?f7BR!@ceJ5#HuUN{{k3KU#WBu*1F*$Nv9+G)|8C#@nts-rKG+!Q0j+N>B1O zKUsQ;w;ey#+x#?dTc7T2>)(3Y`ghVZq-T2D@w2?`_}S8Pyv@(`Ha}1LdvEhUc$@#x z+tz>bw)LOAZGC>Q!|vJwCw9m}Z#!g>^kV5H-gf*_>1EQ(rB_I=lwRd+pJ#Qj!+r~E zoY+TLE4@y7z4Qj@jnbQ>H%o7k-YUIKdb{)v>7Cy8E${NS?{ByC9&ht|z0L2F-Y=fYdE5Damp&+cNcs=yKcx?Q+x3rl+x3o0ACvw|`fqPL-#^}VzT@7ue!|<< zPfDMXJ}rI5+s=1Z`kc3&|Ge}CZ#({?^d;%b(pRLfdfWA`NniK2>)r4+e^dIF^lj-o z(s#Y>{P(=={P(?W{ekpD=||pn{9|uB{)xA(Kb3wa{jc3Gudr4vXelujg_SUQPxQt4#U$)!_Br<6`5 zomx7LbXsq_|LMH#ey5ktAe~VW^J4tu;w)1!Kw)1uMw)Jk_w%%PjRJw?fB8&wm!z&*2hYZ3wGFB@Qo8Y z#dzrn(i6Sy5|gASd)xV@NKf^)pYCnz-%5WcJwtkCu)}_fvz*u^W=qeJp6hKN zVV<`g|Gl@Z|KM%wKYH8xPu{lvv$w6!mtNp)exdXtZ##amxA`UBw!YNc)|W{y_cp&m zdZo7=ze;+ww;jJmdabt|zfO9+^akmT(wn3=d)xK5c-!^1N^g_iF1^Fs&bQOs&bQ0k z)^|(qk>2ZV$M2KgFMUAz7wKQ6fAhA_^SksxZ@c~>Z}Wdh|LJZ1u=Ek>qteHu|C0V& z`XA}z(kG-(N}rNGEqzA%thast=cLbj+xLIL+x$i8OWx)$dz-%^ebw9iHRnr89Wj`7(N& z&m^5$I*W8xZ#!Q$Z#!RhZ(GkHol`oOw;i9`+m6rUZR>fZ^Ld-k?`^(-bU|EhBQq)U3+=Pe~&+S@*F8E^AndfR$g>2luY%X^!z;BD&_r7L-xuk3BU zinpzQC0$jznsjyP8qzhr?RvGOYkS-E>qys?t|whzx`DS{uc34!Z@XS&=_b-mrJG4N zmu?~bwRB7AR?@A#?en+sw$Izv+t%Ajx0mkVZO3<%?&NLf>+Eg5i?^+JmF_0pT{={{ zhjdSGyIwEp-rjcoKHld0O84_N-{0H(0B>6#C_PAeu=Eh=q26}>VcvHB;nE|dM|#`w zqyArM_W@*Ov26j^r>KaSP%$eyLBy*u%ogX!tUp&~{yg*fGV3ppS%0C-{9yAC^HB3eGW!ja+3#ZWaPuWH z_g^Y=|7GS8=8@*h%~zPOG+!n2{HtZ2e~rxiwdU*0qh#*C-h6}1emBajzsY>F%=%ko z*54{Kf1Aww?K1Os$jskqzRP^Kd9?W+^S$Q#%wx>=n;$SgDD(ag$-Mu==10trnjbSi zZhpf2WSiUn$UbEyPn(}HKU?48IV*YI{DS#K^GoKJWxl0XWWJ?W&9BL%TRBXZ~L1{vTxS|50ZCC-cwdU(CPC?Dw0@e!t7i{~keTl#GrypDA@joKMP&9{)V!E^aq|-9CC$Cf zOPTwamzMcF%gDUnvNH3_nU|MYzk+#1b6@jHGW)MAv)?K*^Q)RyGp{al{~G2sW%gUk zyta8Anf=z4xqm&G`Ss0}%=!&v)^BLuNM`-U=6>c)WcJ%s=Kjsho6D@S--E$`u$|)_m`PJKxY0x^FcD}Tg(T`+~40kz7@; z-2bKdEA!XpZ_MAy?Ejtld-D(GA7$R>Cz<_zHveM&)%=_Jck>_SKV_c(m(273Hvc2D z{$KMXz3~+&{~H8k)=w(4|70@roy?QVte?U>rOf@E%~P4Xn5UN6ziXS@e}$(}36Gdo z=9cNq)0=0IxxbrvM)OQE`_C+MfA==Gzv3)bGONrMvzcc%&taa^JePTH^E~Dr=6Pj4 z!hACC*I;fmH<_Ew^ULhtQ)d4K%)MmRFDSEqA({Dw&5M{9H7_Q!-{R&aWcFWDW_@p& z`K8Q#%uAb>F)wRg&b+*N1@nsLzHM&*6d=YDy*Ob{} zEt&Oe%gnDMGrz8RJ@fkJ%DjQh{u|2dzma)kb3gMYGW%_6-pstY%=5M|Zz;3iRx<0i zmYLtiysddV^Y-Q)%sZNQGVg5O#k{L|H}mf1J1F|!(`SUZazZh{v&19A7wt;e2n>6nf;D4A8$TE z=6NT|+<%hH{K+!&r^w8oY944l&3wAderL$+H%MmwOquz!%x9a=F`sKb&wRf50`rCD z!R8_6q2`Os!^{_(hnp`kUuwS0JiP z*1u|gP3HdB&2O0Bl-chs^V{ZkWcGho=KlB0@5`+J!2F@i{U6D!|5#@J6Z5As>&KeM znOn`Dna9gK?{o7P<}b}($-K|k=5J*7|JMAS%>Cb+e~`KVNApkSpUuCRf0cROZ!-J; zZvMmkr};0L{r;BO?;rEO=1G>qk6`)#EFiPrq%!+WCNtkjW`1&+`6*=Pr!;pqPi5|6 zo?2%AuI6cEo;R(``svKm%dDTl+|4|rc_x|tXO`KoyLlFw^|Q*XpUphG%=$TG*3T(3 zKbLuK^E@*5_mH`NUh{nB26Lm#eoZp_HJj%*_cSjcvtKXsf-?IrB(r{DnfXP`i^{BD zOlJM!GV@EAmy}uGTW0-IZEpWZS|2M}T4sx7%*)Dbxtw`<^9tq_W%loDUP)&EmCdWj z+`p>K`qj*<%dB5RX8oGxwPe<>Ewg?d^Sb8sWbR+zT*>UWfz0|1%^R6FHusa+Zxi#T zGW%~Pvwm}#`7O*_%BKv9?0=m3c$xJl$gDq6X8t6Z`IF73$gDqAX8k~!`P0m&%d9`cJji^e z`7D|J&z9Nm9GUrZ&F7iVm%0A}nfou4nI9}OKSXAJsQDt9^~1~;%iKTQe2L8cmzpn= zxqpPr`jO_#W!7IIv;In%`Kx5+ua=p=MrQt6^L1@*|3~O3m9WM2GF#kWzR`S>`DU5@ zZZY2~v;S=}>u)#TVZPIRm-+5Cw|~UZR&tN|UYS?A&pgI_zxe_4gXV|Y-2O@rTgfBl zN6nAPd`pkZyz&z=^H0jmKP5B&w9Nc7GV{;M%s*#-US|CZGV5P7za+E%WtsJ_$jrZL ze$D*4%>8f3-2bNeEt&Oio8K|NYktrCzRdGJFn?(NNapz;n?I4+?^E+wnfu3?Tie|J zEB2X6*mAtg7N48HkXiqw`786+GW&fabN{z8^WVwLe=jrtgUtMoZEpWaKUvAo^(DVp z$*<<$WS;uF`497-=D%d#@^AA$GW-8)o}>?+s{CgHGV3Rm*?%%~CzE5B`!~wWH<_Ew^P78`7cloSFDUc;g=C(;uz3;lqB8d{ zCUgJd<|Sm-FDbLWx6J%faQ~J*ZL3}JKi@I-zPnpTj2Uu*81n-Sm5rwwDvVfsr50n309ztJK?YGAyD&%poN;c%9i9@qP>|Uu zj$L_YTpI(7IRiW$Rxn2p^_a6swv7PCwbstU>W8-FxZ7@ZocX&b9qYXiZ2wSgcO zL8LG*R2vawO$5o@68T@wr=*dPW)MNJW4dH;GhU3Cw#& zkdF{=Al?QROU7cpt_)CyI6BnNT;~>nz^3aJdmiIx7wlPPLD+pr5b0Lcnahz42 zh{`-PIvBBt`aB|PVvhB4jFIR=U+GNKp-xhsmx_2?5163-HGtmj;&vbYTL3xFWL zNcc6203&`U_i3zbtgICT^%|}68Amp6o4X>wE{wcebdD=U zD(fSab7aenu~HUfHX7E?4(9lv7qOtmhM5*;vKEQzN*k zu^Qdcc%-p8=-{E3>Qb^%b z%k36KXH;T6KJJ|J6ORu)*HIY_v4ZB9qo>_xLPRlAc=k9EL3t3qy}|O!jGu@c@#T-7 z2+aR+oZeK$xS*-BacOPkwx()KQ~3`2APVWY=W9t%)mD0@0_>~UPT7+T0jJNC_F5J zd?)ejSdWp969L-Vim|?+d{>qY+6Ul)Q{&mhZi2gKzZ%)8|s5R^|Ej~`E(?;+ks z%<(o5$A~Xjhn3MP>dQHrW6nF|ttJ#?Wd`|ZC(#;s`Qj>m}pU ziBF`%&!udF*hbzQL5%X%;cdsj&wGw1nyQzI@lI2fV{B6uejYJfkUys#2KedD_4r8# z%f-yb4}LNZvTI;5SF#`$%Qq%~gz`E#f8^D2O2DxM8jYG3SWilR4*a zO9WVqUzqWvxdj1Ma)9NoGLE+8N;bq|#MUj_b{Ir4W*c}wtmONN=LPc>6N)EP8Lcp9fDXZ!@sbhb+sn@dD~MlH zDhgt;{9Nh_qB3i;Au6L`1hJlT9+xX9ixInWeL_Kf+wAZ`trcb)<7+FJG5(ss_@TBQ zGs+@Bajw`#tamt2HpTpHr(pi?M!7e;l>y>7I)fxuVln4fiTVF|vH$LIa#!2|S1qrE zxc)W9Cxtma&BdHl$fKOp$=1To5=*;ui;{Bes^b_2^SVq58uv1qB- zn<)h4u-k(8g%fl9=D{4llwuC%@93#A|Extp%7dde=s#5X5@9lJT~+zyD({ zNA$-^9+cOJpu8vIyuSremW#;@;u(W6Xa77g?~pOX3ZH@zn7) zV#d4bRs2OdUL96qJ+oNpQT)xhdlAQ$oKY5W2AQf4a0e{foQ&I+PfoAm&KroF7UVL>4O+1mnaG=kluYEO^HJT=F}KAU++;1^K%A zHurB?2LF<2K