hikyuu2/hikyuu/house.py

526 lines
18 KiB
C++
Raw Normal View History

2020-08-16 18:32:16 +08:00
#!/usr/bin/env python
# -*- coding: utf8 -*-
# cp936
#
#===============================================================================
# History
# 1. 20200816, Added by fasiondog
#===============================================================================
import os
2020-08-23 01:07:32 +08:00
import stat
import errno
2020-08-17 00:35:01 +08:00
import sys
2020-08-18 00:45:17 +08:00
import shutil
2020-08-23 18:27:09 +08:00
import pathlib
2020-08-18 00:45:17 +08:00
import logging
import importlib
2020-08-16 18:32:16 +08:00
import git
from configparser import ConfigParser
2020-08-22 00:48:20 +08:00
from hikyuu.util.check import checkif
2020-08-22 19:15:31 +08:00
from hikyuu.util.singleton import SingletonType
2020-08-22 00:48:20 +08:00
2020-08-17 00:35:01 +08:00
from sqlalchemy import (create_engine, Sequence, Column, Integer, String, and_, UniqueConstraint)
2020-08-22 19:15:31 +08:00
from sqlalchemy.orm import sessionmaker, scoped_session
2020-08-16 18:32:16 +08:00
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
2020-08-17 00:35:01 +08:00
class ConfigModel(Base):
__tablename__ = 'house_config'
id = Column(Integer, Sequence('config_id_seq'), primary_key=True)
key = Column(String, index=True) #
value = Column(String) #
2020-08-16 18:32:16 +08:00
2020-08-17 00:35:01 +08:00
__table_args__ = (UniqueConstraint('key'), )
def __str__(self):
return "ConfigModel(id={}, key={}, value={})".format(self.id, self.key, self.value)
def __repr__(self):
return "<{}>".format(self.__str__())
class HouseModel(Base):
__tablename__ = 'house_repo'
2020-08-16 18:32:16 +08:00
id = Column(Integer, Sequence('remote_id_seq'), primary_key=True)
name = Column(String, index=True) #
2020-08-19 00:50:10 +08:00
house_type = Column(String) # 'remote' () | 'local'
2020-08-17 00:35:01 +08:00
local = Column(String) #
2020-08-16 18:32:16 +08:00
url = Column(String) # git
branch = Column(String) #
2020-08-17 00:35:01 +08:00
__table_args__ = (UniqueConstraint('name'), )
2020-08-16 18:32:16 +08:00
def __str__(self):
2020-08-19 00:50:10 +08:00
return "HouseModel(id={}, name={}, house_type={}, local={}, url={}, branch={})".format(
self.id, self.name, self.house_type, self.local, self.url, self.branch
)
2020-08-16 18:32:16 +08:00
def __repr__(self):
return "<{}>".format(self.__str__())
2020-08-17 00:35:01 +08:00
class PartModel(Base):
__tablename__ = 'house_part'
id = Column(Integer, Sequence('part_id_seq'), primary_key=True)
2020-08-22 00:48:20 +08:00
house_name = Column(String) #
2020-08-17 00:35:01 +08:00
part = Column(String) #
name = Column(String) #
2020-08-18 00:45:17 +08:00
author = Column(String) #
2020-08-23 18:27:09 +08:00
doc = Column(String) #
2020-08-23 01:07:32 +08:00
module_name = Column(String) #
2020-08-17 00:35:01 +08:00
2020-08-19 00:50:10 +08:00
def __str__(self):
2020-08-22 00:48:20 +08:00
return 'PartModel(id={}, house_name={}, part={}, name={}, author={}, module_name={})'.format(
self.id, self.house_name, self.part, self.name, self.author, self.module_name
2020-08-19 00:50:10 +08:00
)
def __repr__(self):
return '<{}>'.format(self.__str__())
2020-08-17 00:35:01 +08:00
2020-08-22 00:48:20 +08:00
class HouseNameRepeatError(Exception):
def __init__(self, name):
self.name = name
def __str__(self):
return "已存在相同名称的仓库({}),请更换仓库名!".format(self.name)
2020-08-23 18:27:09 +08:00
class HouseNotFoundError(Exception):
def __init__(self, name):
self.name = name
def __str__(self):
return '"{}"'.format(self.name)
2020-08-22 00:48:20 +08:00
class ModuleConflictError(Exception):
2020-08-22 19:15:31 +08:00
def __init__(self, house_name, conflict_module, house_path):
self.house_name = house_name
self.conflict_module = conflict_module
self.house_path = house_path
2020-08-22 00:48:20 +08:00
def __str__(self):
2020-08-22 19:15:31 +08:00
return '{} python "{}""{}"'.format(
self.house_name, self.conflict_module, self.house_path
)
class PartNotFoundError(Exception):
def __init__(self, name, cause):
self.name = name
self.cause = cause
def __str__(self):
return ': "{}", {}!'.format(self.name, self.cause)
2020-08-22 00:48:20 +08:00
2020-08-23 18:27:09 +08:00
class PartNameError(Exception):
def __init__(self, name):
self.name = name
def __str__(self):
return ': "{}"!'.format(self.name)
2020-08-23 01:07:32 +08:00
# Windows下 shutil.rmtree 删除的目录中如有存在只读文件或目录会导致失败,需要此函数辅助处理
# 可参见https://blog.csdn.net/Tri_C/article/details/99862201
def handle_remove_read_only(func, path, exc):
excvalue = exc[1]
if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
func(path)
else:
raise RuntimeError(' "{}"'.format(path))
2020-08-22 19:15:31 +08:00
def dbsession(func):
def wrapfunc(*args, **kwargs):
x = args[0]
old_session = x._session
if x._session is None:
x._session = x._scoped_Session()
result = func(*args, **kwargs)
x._session.commit()
if old_session is not x._session:
x._session.close()
x._session = old_session
return result
return wrapfunc
class HouseManager(metaclass=SingletonType):
2020-08-16 18:32:16 +08:00
"""策略库管理"""
def __init__(self):
2020-08-18 00:45:17 +08:00
self.logger = logging.getLogger(self.__class__.__name__)
2020-08-16 18:32:16 +08:00
usr_dir = os.path.expanduser('~')
# 创建仓库数据库
engine = create_engine("sqlite:///{}/.hikyuu/stockhouse.db".format(usr_dir))
Base.metadata.create_all(engine)
2020-08-22 19:15:31 +08:00
self._scoped_Session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
)
self._session = None
2020-08-16 18:32:16 +08:00
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-20 02:20:19 +08:00
def setup_house(self):
2020-08-22 00:48:20 +08:00
"""初始化 hikyuu 默认策略仓库"""
2020-08-20 02:20:19 +08:00
usr_dir = os.path.expanduser('~')
2020-08-17 00:35:01 +08:00
# 检查并建立远端仓库的本地缓存目录
self.remote_cache_dir = self._session.query(ConfigModel.value
).filter(ConfigModel.key == 'remote_cache_dir'
).first()
if self.remote_cache_dir is None:
2020-08-22 00:48:20 +08:00
self.remote_cache_dir = "{}/.hikyuu/house_cache".format(usr_dir)
2020-08-17 00:35:01 +08:00
record = ConfigModel(key='remote_cache_dir', value=self.remote_cache_dir)
self._session.add(record)
2020-08-16 18:32:16 +08:00
else:
2020-08-17 00:35:01 +08:00
self.remote_cache_dir = self.remote_cache_dir[0]
if not os.path.lexists(self.remote_cache_dir):
os.makedirs(self.remote_cache_dir)
2020-08-18 00:45:17 +08:00
# 将远程仓库本地缓存地址加入系统路径
sys.path.append(self.remote_cache_dir)
2020-08-19 00:50:10 +08:00
# 将所有本地仓库的上层路径加入系统路径
house_models = self._session.query(HouseModel).filter_by(house_type='local').all()
for model in house_models:
2020-08-21 00:07:53 +08:00
sys.path.append(os.path.dirname(model.local))
2020-08-19 00:50:10 +08:00
# 检查并下载 hikyuu 默认策略仓库, hikyuu_house 避免导入时模块和 hikyuu 重名
2020-08-17 00:35:01 +08:00
hikyuu_house_path = self._session.query(HouseModel.local
2020-08-22 00:48:20 +08:00
).filter(HouseModel.name == 'default').first()
2020-08-17 00:35:01 +08:00
if hikyuu_house_path is None:
2020-08-22 19:15:31 +08:00
self.add_remote_house(
'default', 'https://gitee.com/fasiondog/hikyuu_house.git', 'master'
)
def download_remote_house(self, local_dir, url, branch):
print(' hikyuu "{}"'.format(local_dir))
# 如果存在同名缓存目录,则强制删除
if os.path.lexists(local_dir):
2020-08-23 01:07:32 +08:00
shutil.rmtree(local_dir, onerror=handle_remove_read_only)
2020-08-22 19:15:31 +08:00
try:
2020-08-23 01:07:32 +08:00
git.Repo.clone_from(url, local_dir, branch=branch)
2020-08-22 19:15:31 +08:00
except:
raise RuntimeError("请检查网络是否正常或链接地址({})是否正确!".format(url))
print('')
2020-08-17 00:35:01 +08:00
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-17 00:35:01 +08:00
def add_remote_house(self, name, url, branch='master'):
"""增加远程策略仓库
2020-08-22 00:48:20 +08:00
:param str name:
2020-08-17 00:35:01 +08:00
:param str url: git
:param str branch: git
"""
record = self._session.query(HouseModel).filter(HouseModel.name == name).first()
2020-08-22 00:48:20 +08:00
checkif(record is not None, name, HouseNameRepeatError)
2020-08-16 18:32:16 +08:00
2020-08-17 00:35:01 +08:00
record = self._session.query(HouseModel).filter(
and_(HouseModel.url == url, HouseModel.branch == branch)
2020-08-16 18:32:16 +08:00
).first()
2020-08-22 19:15:31 +08:00
# 下载远程仓库
2020-08-17 00:35:01 +08:00
local_dir = "{}/{}".format(self.remote_cache_dir, name)
2020-08-22 19:15:31 +08:00
self.download_remote_house(local_dir, url, branch)
2020-08-17 00:35:01 +08:00
2020-08-20 02:20:19 +08:00
# 导入仓库各部件策略信息
2020-08-17 00:35:01 +08:00
record = HouseModel(name=name, house_type='remote', url=url, branch=branch, local=local_dir)
2020-08-20 02:20:19 +08:00
self.import_part_to_db(record)
# 更新仓库记录
2020-08-16 18:32:16 +08:00
self._session.add(record)
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-22 00:48:20 +08:00
def add_local_house(self, path):
2020-08-21 00:07:53 +08:00
"""增加本地数据仓库
:param str path:
"""
2020-08-22 00:48:20 +08:00
checkif(not os.path.lexists(path), '"{}"'.format(path))
2020-08-21 00:07:53 +08:00
# 获取绝对路径
local_path = os.path.abspath(path)
2020-08-22 00:48:20 +08:00
name = os.path.basename(local_path)
record = self._session.query(HouseModel).filter(HouseModel.name == name).first()
checkif(record is not None, name, HouseNameRepeatError)
#assert record is None, '本地仓库名重复'
2020-08-21 00:07:53 +08:00
# 将本地路径的上一层路径加入系统路径
sys.path.append(os.path.dirname(path))
2020-08-22 00:48:20 +08:00
# 检查仓库目录名称是否与其他 python 模块存在冲突
tmp = importlib.import_module(name)
2020-08-22 19:15:31 +08:00
checkif(
tmp.__path__[0] != local_path,
name,
ModuleConflictError,
conflict_module=tmp.__path__[0],
house_path=local_path
)
2020-08-21 00:07:53 +08:00
# 导入部件信息
house_model = HouseModel(name=name, house_type='local', local=local_path)
self.import_part_to_db(house_model)
# 更新仓库记录
self._session.add(house_model)
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-22 00:48:20 +08:00
def update_house(self, name):
"""更新指定仓库
:param str name:
"""
house_model = self._session.query(HouseModel).filter_by(name=name).first()
checkif(house_model is None, '{}'.format(name))
self._session.query(PartModel).filter_by(house_name=name).delete()
2020-08-22 19:15:31 +08:00
if house_model.house_type == 'remote':
self.download_remote_house(house_model.local, house_model.url, house_model.branch)
2020-08-22 00:48:20 +08:00
self.import_part_to_db(house_model)
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-22 00:48:20 +08:00
def remove_house(self, name):
"""删除指定的仓库
:param str name:
"""
self._session.query(PartModel).filter_by(house_name=name).delete()
self._session.query(HouseModel).filter_by(name=name).delete()
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-18 00:45:17 +08:00
def import_part_to_db(self, house_model):
part_dict = {
'af': 'part/af',
'cn': 'part/cn',
'ev': 'part/ev',
'mm': 'part/mm',
'pg': 'part/pg',
'se': 'part/se',
'sg': 'part/sg',
'sp': 'part/sp',
2020-08-22 00:48:20 +08:00
'st': 'part/st',
2020-08-18 00:45:17 +08:00
'portfolio': 'portfolio',
'system': 'system',
}
# 检查仓库本地目录是否存在,不存在则给出告警信息并直接返回
local_dir = house_model.local
if not os.path.lexists(local_dir):
self.logger.warning(
'The {} house path ("{}") is not exists! Ignored this house!'.format(
house_model.name, house_model.local
)
)
return
base_local = os.path.basename(local_dir)
2020-08-19 00:50:10 +08:00
# 遍历仓库导入部件信息
2020-08-18 00:45:17 +08:00
for part, part_dir in part_dict.items():
path = "{}/{}".format(house_model.local, part_dir)
try:
with os.scandir(path) as it:
for entry in it:
2020-08-22 00:48:20 +08:00
if (not entry.name.startswith('.')
) and entry.is_dir() and (entry.name != "__pycache__"):
2020-08-19 00:50:10 +08:00
# 计算实际的导入模块名
2020-08-21 00:07:53 +08:00
module_name = '{}.part.{}.{}.part'.format(
2020-08-18 00:45:17 +08:00
base_local, part, entry.name
2020-08-19 00:50:10 +08:00
) if part not in (
'portfolio', 'system'
) else '{}.{}.{}.part'.format(base_local, part, entry.name)
# 导入模块
try:
2020-08-21 00:07:53 +08:00
part_module = importlib.import_module(module_name)
2020-08-19 00:50:10 +08:00
except ModuleNotFoundError:
2020-08-21 00:07:53 +08:00
print(module_name)
2020-08-22 19:15:31 +08:00
self.logger.error(' part.py , "{}"'.format(entry.path))
2020-08-19 00:50:10 +08:00
continue
module_vars = vars(part_module)
2020-08-21 00:07:53 +08:00
name = '{}.{}.{}'.format(
2020-08-19 00:50:10 +08:00
house_model.name, part, entry.name
) if part not in (
'portfolio', 'system'
) else '{}.{}.{}'.format(house_model.name, part, entry.name)
2020-08-18 00:45:17 +08:00
part_model = PartModel(
2020-08-22 00:48:20 +08:00
house_name=house_model.name,
2020-08-18 00:45:17 +08:00
part=part,
2020-08-21 00:07:53 +08:00
name=name,
2020-08-18 00:45:17 +08:00
module_name=module_name,
2020-08-23 18:27:09 +08:00
author=part_module.author.strip()
if 'author' in module_vars else 'None',
doc=part_module.doc.strip() if 'doc' in module_vars else 'None',
2020-08-18 00:45:17 +08:00
)
2020-08-19 00:50:10 +08:00
self._session.add(part_model)
2020-08-23 18:27:09 +08:00
2020-08-18 00:45:17 +08:00
except FileNotFoundError:
continue
2020-08-19 00:50:10 +08:00
2020-08-22 19:15:31 +08:00
@dbsession
2020-08-23 18:27:09 +08:00
def get_part(self, name, **kwargs):
2020-08-22 19:15:31 +08:00
"""获取指定策略部件
:param str name:
2020-08-23 18:27:09 +08:00
:param kwargs:
2020-08-22 19:15:31 +08:00
"""
2020-08-23 18:27:09 +08:00
name_parts = name.split('.')
checkif(
len(name_parts) < 2 or (
name_parts[-2] not in
('af', 'cn', 'ev', 'mm', 'pg', 'se', 'sg', 'sp', 'st', 'prtflo', 'sys')
), name, PartNameError
)
if len(name_parts) == 2:
# 未指定仓库名,尝试获取所在仓库名,并重新组装名称
abs_path = os.path.abspath(__file__) #
path_parts = pathlib.Path(abs_path).parts
cause = '"{}"'.format(abs_path)
if name_parts[0] in ('prtflo', 'sys'):
checkif(len(path_parts) < 4, name, PartNotFoundError, cause=cause)
part_name = '{}.{}'.format(path_parts[-4], name)
else:
checkif(len(path_parts) < 5, name, PartNotFoundError, cause=cause)
part_name = '{}.{}'.format(path_parts[-5], name)
else:
part_name = name
part_model = self._session.query(PartModel).filter_by(name=part_name).first()
checkif(part_model is None, part_name, PartNotFoundError, cause='')
2020-08-22 19:15:31 +08:00
try:
part_module = importlib.import_module(part_model.module_name)
except ModuleNotFoundError:
2020-08-23 18:27:09 +08:00
raise PartNotFoundError(part_name, '')
2020-08-19 00:50:10 +08:00
part = part_module.sg.clone()
2020-08-23 18:27:09 +08:00
for k, v in kwargs.items():
part.set_param(k, v)
2020-08-21 00:07:53 +08:00
part.name = part_model.name
2020-08-19 00:50:10 +08:00
return part
2020-08-17 00:35:01 +08:00
2020-08-23 18:27:09 +08:00
@dbsession
def get_part_info(self, name):
"""获取策略部件信息
:param str name:
"""
part_model = self._session.query(PartModel).filter_by(name=name).first()
checkif(part_model is None, name, PartNotFoundError, cause='')
return {
'name': name,
'author': part_model.author,
'doc': part_model.doc,
}
def print_part_info(self, name):
info = self.get_part_info(name)
print('name:', info['name'])
print('author:', info['author'])
print('doc:', info['doc'])
@dbsession
def get_house_path(self, name):
"""获取仓库所在的本地路径
:param str name:
"""
path = self._session.query(HouseModel.local).filter_by(name=name).first()
checkif(path is None, name, HouseNotFoundError)
return path[0]
2020-08-16 18:32:16 +08:00
2020-08-22 00:48:20 +08:00
def add_remote_house(name, url, branch='master'):
"""增加远程策略仓库
:param str name:
:param str url: git
:param str branch: git
"""
HouseManager().add_remote_house()
def add_local_house(path):
"""增加本地数据仓库
:param str path:
"""
HouseManager().add_local_house(path)
def update_house(name):
"""更新指定仓库
:param str name:
"""
HouseManager().update_house(name)
def remove_house(name):
"""删除指定的仓库
:param str name:
"""
HouseManager().remove_house(name)
2020-08-23 18:27:09 +08:00
def get_part(name, **kwargs):
2020-08-22 19:15:31 +08:00
"""获取指定策略部件
:param str name:
2020-08-23 18:27:09 +08:00
:param kwargs:
2020-08-22 19:15:31 +08:00
"""
2020-08-23 18:27:09 +08:00
return HouseManager().get_part(name, **kwargs)
def get_house_path(name):
"""获取仓库所在的本地路径
:param str name:
"""
return HouseManager().get_house_path(name)
def get_part_info(name):
"""获取策略部件信息
:param str name:
"""
return HouseManager().get_part_info(name)
def print_part_info(name):
HouseManager().print_part_info(name)
2020-08-22 19:15:31 +08:00
2020-08-16 18:32:16 +08:00
if __name__ == "__main__":
2020-08-22 19:15:31 +08:00
logging.basicConfig(
level=logging.INFO,
format='%(asctime)-15s [%(levelname)s] - %(message)s [%(name)s::%(funcName)s]'
)
2020-08-20 02:20:19 +08:00
house = HouseManager()
house.setup_house()
2020-08-22 19:15:31 +08:00
#add_local_house('/home/fasiondog/workspace/test1')
#update_house('test1')
2020-08-23 18:27:09 +08:00
#update_house('default')
2020-08-22 19:15:31 +08:00
#remove_house('test1')
2020-08-22 00:48:20 +08:00
remove_house('test')
2020-08-23 18:27:09 +08:00
sg = get_part('default.sg.ama', a=1, b=2)
2020-08-22 19:15:31 +08:00
print(sg)
2020-08-23 18:27:09 +08:00
print_part_info('default.sg.ama')