├── tests ├── datasource │ ├── __init__.py │ ├── test_csv.py │ ├── test_sqlite.py │ └── test_cache.py ├── trading │ ├── __init__.py │ └── stock_util.py ├── data │ ├── digger.db │ ├── CONTRACTS.csv │ └── 1DAY │ │ └── SH │ │ └── 600522.csv ├── backup │ ├── test_cache.py │ └── test_custom_datasource.py ├── sql_main.py ├── test_rabbitmq_speed.py ├── work │ └── contracts.txt ├── csv_main.py ├── infras │ └── test_function.py ├── test_zmq_speed.py ├── import_data.py ├── interaction │ ├── test_backend.py │ ├── test_shell.py │ └── test_window_gate.py └── test_eventengine.py ├── quantdigger ├── digger │ ├── __init__.py │ ├── finance.py │ ├── plotting.py │ └── sugar.py ├── engine │ ├── __init__.py │ ├── context │ │ ├── __init__.py │ │ ├── plotter.py │ │ └── context.py │ ├── strategy.py │ ├── orderedset.py │ ├── api.py │ ├── exchange.py │ ├── profile.py │ └── series.py ├── infras │ ├── __init__.py │ ├── function.py │ ├── object.py │ └── ioc.py ├── widgets │ ├── __init__.py │ ├── qtwidgets │ │ ├── __init__.py │ │ └── techwidget.py │ ├── mplotwidgets │ │ ├── __init__.py │ │ ├── mainwindow.py │ │ └── mplots.py │ └── plotter.py ├── datasource │ ├── __init__.py │ ├── impl │ │ ├── __init__.py │ │ ├── tushare_source.py │ │ ├── mongodb_source.py │ │ ├── localfs_cache.py │ │ ├── csv_source.py │ │ └── sqlite_source.py │ ├── dsutil.py │ ├── source.py │ ├── data.py │ └── cache.py ├── technicals │ ├── __init__.py │ ├── techutil.py │ └── common.py ├── event │ ├── __init__.py │ ├── rpc_client.py │ ├── event.py │ └── rpc.py ├── interaction │ ├── __init__.py │ ├── ipython_config.py │ ├── serialize.py │ ├── shell.py │ ├── windowgate.py │ └── backend.py ├── configutil.py ├── config.py ├── __init__.py ├── util │ └── __init__.py └── errors.py ├── install.sh ├── demo ├── data │ └── digger.db ├── tools │ ├── import_stocks.py │ ├── import_mongo.py │ ├── transform.py │ ├── import_data.py │ ├── import_contracts.py │ └── sql.py ├── plot_ma.py ├── main.py ├── mplot_demo.py ├── work │ ├── _djtrend2_IF000.csv │ ├── signal_IF000.csv │ └── _djtrend2_IF000_wave.txt ├── stock_search.py ├── profile_strategy.py ├── manytomany.py ├── plot_strategy.py └── mongotest.py ├── doc └── images │ ├── plot.png │ ├── quant.png │ ├── pyquant.png │ ├── qd_zmq2.png │ ├── quant_ui.png │ ├── qd_event2.png │ ├── figure_money.png │ └── figure_networth.png ├── .gitignore ├── requirements ├── tests.txt └── requirements.txt ├── setup.py └── README.rst /tests/datasource/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/trading/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/digger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/engine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/infras/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/widgets/qtwidgets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdigger/widgets/mplotwidgets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python setup.py install 4 | -------------------------------------------------------------------------------- /demo/data/digger.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/demo/data/digger.db -------------------------------------------------------------------------------- /doc/images/plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/plot.png -------------------------------------------------------------------------------- /doc/images/quant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/quant.png -------------------------------------------------------------------------------- /quantdigger/datasource/__init__.py: -------------------------------------------------------------------------------- 1 | from .datautil import * 2 | from . import impl as ds_impl 3 | -------------------------------------------------------------------------------- /quantdigger/technicals/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | from .base import TechnicalBase 3 | -------------------------------------------------------------------------------- /tests/data/digger.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/tests/data/digger.db -------------------------------------------------------------------------------- /doc/images/pyquant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/pyquant.png -------------------------------------------------------------------------------- /doc/images/qd_zmq2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/qd_zmq2.png -------------------------------------------------------------------------------- /doc/images/quant_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/quant_ui.png -------------------------------------------------------------------------------- /doc/images/qd_event2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/qd_event2.png -------------------------------------------------------------------------------- /doc/images/figure_money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/figure_money.png -------------------------------------------------------------------------------- /quantdigger/event/__init__.py: -------------------------------------------------------------------------------- 1 | from .event import * 2 | from .eventengine import * 3 | from .rpc import * 4 | 5 | -------------------------------------------------------------------------------- /doc/images/figure_networth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantFans/quantdigger/HEAD/doc/images/figure_networth.png -------------------------------------------------------------------------------- /quantdigger/engine/context/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_context import OriginalData 2 | from .context import Context 3 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/__init__.py: -------------------------------------------------------------------------------- 1 | from . import csv_source 2 | from . import tushare_source 3 | from . import mongodb_source 4 | from . import sqlite_source 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.DS_Store 4 | 5 | 6 | .idea/ 7 | dist/ 8 | *.egg-info/ 9 | build/ 10 | demo/work/* 11 | tests/work/ 12 | 13 | _local_* 14 | demo/log 15 | __pycache__ 16 | -------------------------------------------------------------------------------- /quantdigger/interaction/__init__.py: -------------------------------------------------------------------------------- 1 | from quantdigger.interaction.windowgate import WindowGate 2 | from quantdigger.interaction.backend import Backend 3 | from quantdigger.interaction.interface import BackendInterface, UIInterface 4 | -------------------------------------------------------------------------------- /demo/tools/import_stocks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from quantdigger import locd, set_config 3 | from quantdigger.datasource.datautil import import_tdx_stock 4 | set_config({ 'data_path': '../data' }) 5 | import_tdx_stock('../work/stock', locd) 6 | -------------------------------------------------------------------------------- /quantdigger/interaction/ipython_config.py: -------------------------------------------------------------------------------- 1 | 2 | c = get_config() 3 | #c.InteractiveShellApp.exec_files = [ 4 | #'shell.py' 5 | #] 6 | c.InteractiveShellApp.exec_lines = [ 7 | 'from quantdigger.interaction.shell import shell as qd' 8 | ] 9 | -------------------------------------------------------------------------------- /tests/backup/test_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 缓存的单元测试 4 | 5 | import unittest 6 | 7 | class TestCache(unittest.TestCase): 8 | pass # TODO 9 | 10 | 11 | if __name__ == '__main__': 12 | unittest.main() 13 | -------------------------------------------------------------------------------- /demo/tools/import_mongo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import quantdigger.datasource as qdd 4 | 5 | 6 | ms = qdd.source.MongoSource() 7 | qdd.datautil.import_data([ 8 | './work/BB.SHFE-1.Minute.csv', 9 | './work/AA.SHFE-1.Minute.csv' 10 | ], ms) 11 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | pylint 3 | pep8 4 | flake8>=2.5.1 5 | pylint-common 6 | 7 | # http://plugincompat.herokuapp.com/ 8 | pytest 9 | # Running tests in parallel 10 | pytest-xdist 11 | pytest-django 12 | pytest-catchlog 13 | pytest-cov 14 | pytest-timeout 15 | pytest-pylint -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.18.1 2 | BeautifulSoup4>=4.6 3 | lxml>=3.8 4 | tushare>=0.8.2 5 | pyzmq>=4.1.5 6 | six 7 | logbook>=0.12.5 8 | numpy>=1.13.1 9 | pandas>=0.20.2 10 | pymongo>=3.1.1 11 | matplotlib>=1.5.1 12 | python-dateutil>=2.4.2 13 | progressbar2>=3.6.2 14 | TA-Lib>=0.4.8 -------------------------------------------------------------------------------- /quantdigger/technicals/techutil.py: -------------------------------------------------------------------------------- 1 | from quantdigger.infras.ioc import * 2 | 3 | _tech_container = IoCContainer() 4 | 5 | 6 | register_tech = register_to(_tech_container) 7 | resolve_datasource = resolve_from(_tech_container) 8 | 9 | 10 | def get_techs(): 11 | return _tech_container.keys() 12 | -------------------------------------------------------------------------------- /tests/sql_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from test_data import * 3 | from test_engine import * 4 | from trading.test_future import * 5 | from trading.test_stock import * 6 | from quantdigger import locd, set_config 7 | 8 | if __name__ == '__main__': 9 | set_config({ 'source': 'sqlite' }) 10 | unittest.main() 11 | assert locd.source == 'sqlite' 12 | # 这里的代码不会被运行 13 | -------------------------------------------------------------------------------- /tests/test_rabbitmq_speed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | 5 | from timeit import timeit 6 | 7 | from nameko.standalone.rpc import ClusterRpcProxy 8 | with ClusterRpcProxy({"AMQP_URI": "pyamqp://guest:guest@localhost"}) as rpc: 9 | 10 | def func(): 11 | result = rpc.backend_service.say_hello("world") 12 | 13 | t = timeit('func()', 'from __main__ import func', number=100) 14 | print(t) 15 | -------------------------------------------------------------------------------- /tests/work/contracts.txt: -------------------------------------------------------------------------------- 1 | name,code,exchange,spell,long_margin_ratio,short_margin_ratio,price_tick,volume_multiple 2 | stock,stock,TEST,stock,1,1,0,1 3 | 600521,600521,SH,stock,1,1,0,1 4 | 600522,600522,SH,stock,1,1,0,1 5 | future,future,TEST,future,0.4,0.4,0.2,3 6 | future2,future2,TEST,future,0.4,0.4,0.2,3 7 | AA,AA,TEST,AA,1,1,0,1 8 | BB,BB,TEST,BB,1,1,0,1 9 | CC,CC,TEST,CC,1,1,0,1 10 | oneday,oneday,TEST,oneday,1,1,0,1 11 | TWODAY,TWODAY,TEST,TWODAY,1,1,0,1 12 | -------------------------------------------------------------------------------- /quantdigger/configutil.py: -------------------------------------------------------------------------------- 1 | from quantdigger.infras.function import overload_setter 2 | from quantdigger.config import settings 3 | 4 | 5 | class ConfigUtil(object): 6 | @staticmethod 7 | @overload_setter 8 | def set(key, val): 9 | settings[key] = val 10 | 11 | @staticmethod 12 | def get(key, d=KeyError): 13 | if d == KeyError: 14 | return settings[key] 15 | else: 16 | return settings.get(key, d) 17 | -------------------------------------------------------------------------------- /tests/csv_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from datasource.test_csv import * 4 | from test_engine import * 5 | from trading.test_future import * 6 | from trading.test_stock import * 7 | from quantdigger import ConfigUtil 8 | 9 | if __name__ == '__main__': 10 | # 默认为csv 11 | assert(ConfigUtil.get('source') == 'csv') 12 | unittest.main() 13 | assert(ConfigUtil.get('source') == 'csv') 14 | # 这里的代码不会被运行 15 | # code 16 | -------------------------------------------------------------------------------- /quantdigger/infras/function.py: -------------------------------------------------------------------------------- 1 | import six 2 | def overload_setter(setter): 3 | 4 | def multiple_set(d): 5 | for k, v in six.iteritems(d): 6 | setter(k, v) 7 | 8 | def new_setter(*args, **kwargs): 9 | # args 10 | if len(args) == 1: 11 | multiple_set(args[0]) 12 | elif len(args) >= 2: 13 | setter(args[0], args[1]) 14 | # kwargs 15 | multiple_set(kwargs) 16 | 17 | return new_setter 18 | -------------------------------------------------------------------------------- /demo/tools/transform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sqlite3 4 | import time 5 | import pandas as pd 6 | from quantdigger.datasource import datautil 7 | 8 | import timeit 9 | import datetime 10 | 11 | df = pd.read_csv('./TWODAY.SHFE-1.Minute.csv', parse_dates={'datetime': ['date', 'time']}) 12 | df['volume'] = df.vol 13 | 14 | df.to_csv('temp.csv', index=False, 15 | columns = ['datetime', 'open', 'close', 'high', 'low', 'volume']) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /quantdigger/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | settings = { 4 | 'source': 'csv', 5 | #'source': 'mongodb', 6 | 'data_path': './data', 7 | 'stock_commission': 3 / 10000.0, 8 | 'future_commission': 1 / 10000.0, 9 | 'tick_test': False, 10 | } 11 | 12 | 13 | class ConfigLog(object): 14 | log_level = 'INFO' 15 | log_to_file = True 16 | log_to_console = True 17 | log_path = './log' 18 | 19 | 20 | __all__ = ['settings', 'ConfigLog'] 21 | -------------------------------------------------------------------------------- /tests/data/CONTRACTS.csv: -------------------------------------------------------------------------------- 1 | code,exchange,name,spell,long_margin_ratio,short_margin_ratio,price_tick,volume_multiple 2 | stock,TEST,stock,stock,1.0,1.0,0.0,1 3 | 600521,SH,600521,stock,1.0,1.0,0.0,1 4 | 600522,SH,600522,stock,1.0,1.0,0.0,1 5 | future,TEST,future,future,0.4,0.4,0.2,3 6 | future2,TEST,future2,future,0.4,0.4,0.2,3 7 | AA,TEST,AA,AA,1.0,1.0,0.0,1 8 | BB,TEST,BB,BB,1.0,1.0,0.0,1 9 | CC,TEST,CC,CC,1.0,1.0,0.0,1 10 | oneday,TEST,oneday,oneday,1.0,1.0,0.0,1 11 | TWODAY,TEST,TWODAY,TWODAY,1.0,1.0,0.0,1 12 | -------------------------------------------------------------------------------- /quantdigger/infras/object.py: -------------------------------------------------------------------------------- 1 | import six 2 | class _HashObjectImpl(object): 3 | def __str__(self): 4 | keys = filter(lambda k: not k.startswith('__'), dir(self)) 5 | d = {} 6 | for k in keys: 7 | d[k] = getattr(self, k) 8 | return str(d) 9 | 10 | 11 | class HashObject(object): 12 | @staticmethod 13 | def new(**kwargs): 14 | obj = _HashObjectImpl() 15 | for k, v in six.iteritems(kwargs): 16 | setattr(obj, k, v) 17 | return obj 18 | -------------------------------------------------------------------------------- /quantdigger/widgets/qtwidgets/techwidget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg 3 | from quantdigger.widgets.mplotwidgets.widgets import MultiWidgets 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | class TechWidget(MultiWidgets, FigureCanvasQTAgg): 8 | def __init__(self, parent=None, *args): 9 | self.fig = plt.figure() 10 | FigureCanvasQTAgg.__init__(self, self.fig) 11 | MultiWidgets.__init__(self, self.fig, *args) 12 | self.setParent(parent) 13 | -------------------------------------------------------------------------------- /demo/plot_ma.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import matplotlib 3 | 4 | matplotlib.use('TkAgg') 5 | 6 | import matplotlib.pyplot as plt 7 | import pandas as pd 8 | from quantdigger.technicals.common import MA 9 | 10 | # 创建画布 11 | fig, ax = plt.subplots() 12 | # 加载数据 13 | price_data = pd.read_csv("work/IF000.SHFE-10.Minute.csv", 14 | index_col=0, parse_dates=True) 15 | # 创建平均线 16 | ma10 = MA(price_data.close, 10, 'MA10', 'y', 2) 17 | ma20 = MA(price_data.close, 60, 'MA10', 'b', 2) 18 | # 绘制指标 19 | ma10.plot(ax) 20 | ma20.plot(ax) 21 | plt.show() 22 | -------------------------------------------------------------------------------- /demo/tools/import_data.py: -------------------------------------------------------------------------------- 1 | from quantdigger import ConfigUtil 2 | from quantdigger.datasource import import_data, ds_impl 3 | 4 | csv_ds = ds_impl.csv_source.CsvSource('../data') 5 | import_data(['../work/AA.SHFE-1.Minute.csv', 6 | '../work/BB.SHFE-1.Minute.csv', 7 | '../work/BB.SHFE-1.Day.csv'], 8 | csv_ds) 9 | 10 | sqlite_ds = ds_impl.sqlite_source.SqliteSource('../data/digger.db') 11 | import_data(['../work/AA.SHFE-1.Minute.csv', 12 | '../work/BB.SHFE-1.Minute.csv', 13 | '../work/BB.SHFE-1.Day.csv'], 14 | sqlite_ds) 15 | -------------------------------------------------------------------------------- /quantdigger/event/rpc_client.py: -------------------------------------------------------------------------------- 1 | import six 2 | from six.moves import range 3 | from rpc import EventRPCClient 4 | from eventengine import ZMQEventEngine 5 | import time 6 | import sys 7 | 8 | 9 | 10 | client_engine = ZMQEventEngine('test') 11 | client_engine.start() 12 | client = EventRPCClient('test', client_engine, 'test') 13 | for i in range(0, 5): 14 | six.print_("sync_call: print_hello ") 15 | six.print_("return: ", client.sync_call("print_hello", 16 | { 'data': 'from_rpc_client' })) 17 | time.sleep(1) 18 | six.print_("***************" ) 19 | try: 20 | while True: 21 | time.sleep(1) 22 | except KeyboardInterrupt: 23 | client_engine.stop() 24 | sys.exit(0) 25 | -------------------------------------------------------------------------------- /quantdigger/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | 3 | from quantdigger.engine.strategy import * 4 | from quantdigger.config import settings 5 | from quantdigger.configutil import ConfigUtil 6 | from quantdigger.engine.series import NumberSeries, DateTimeSeries 7 | from quantdigger.engine.profile import Profile 8 | from quantdigger.technicals.common import * 9 | from quantdigger.util import log, deprecated 10 | 11 | __version__ = '0.6.1' 12 | 13 | 14 | @deprecated 15 | def set_config(cfg): 16 | """""" 17 | # from quantdigger.datasource.data import locd 18 | # if 'source' in cfg: 19 | # cfg['source'] = cfg['source'].lower() 20 | # assert(cfg['source'] in ['sqlite', 'csv', 'mongodb']) 21 | settings.update(cfg) 22 | # locd.set_source(settings) 23 | 24 | 25 | # set_config(settings) 26 | -------------------------------------------------------------------------------- /tests/infras/test_function.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from quantdigger.infras.function import * 4 | 5 | 6 | class TestFunction(unittest.TestCase): 7 | 8 | def test_overload_setter(self): 9 | d = {} 10 | 11 | @overload_setter 12 | def setter(k, v): 13 | d[k] = v 14 | 15 | setter('k1', 'v1') 16 | self.assertEqual(d['k1'], 'v1') 17 | setter({'k2': 'v2', 'k1': 'v1_1'}) 18 | self.assertEqual(d['k1'], 'v1_1') 19 | self.assertEqual(d['k2'], 'v2') 20 | setter(k1='v1_2', k2='v2_2') 21 | self.assertEqual(d['k1'], 'v1_2') 22 | self.assertEqual(d['k2'], 'v2_2') 23 | setter('k1', 'v1_3', k2='v2_3') 24 | self.assertEqual(d['k1'], 'v1_3') 25 | self.assertEqual(d['k2'], 'v2_3') 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /quantdigger/datasource/dsutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | from quantdigger.configutil import ConfigUtil 5 | from quantdigger.infras.ioc import * 6 | from quantdigger.util import log 7 | 8 | _ds_container = IoCContainer() 9 | 10 | 11 | class _DatasourceTrunk(IoCTrunk): 12 | 13 | def __init__(self, cls, args, kwargs): 14 | super(_DatasourceTrunk, self).__init__(cls, args, kwargs) 15 | 16 | def on_register(self, name): 17 | log.info('register datasource: {0} => {1}'.format(self.cls, name)) 18 | 19 | def construct(self): 20 | a = [ConfigUtil.get(k, None) for k in self.args] 21 | ka = {k: ConfigUtil.get(name, None) for k, name in six.iteritems(self.kwargs)} 22 | return self.cls(*a, **ka) 23 | 24 | 25 | register_datasource = register_to(_ds_container, _DatasourceTrunk) 26 | resolve_datasource = resolve_from(_ds_container) 27 | 28 | 29 | def get_setting_datasource(): 30 | ds_type = ConfigUtil.get('source') 31 | return resolve_datasource(ds_type), ds_type 32 | -------------------------------------------------------------------------------- /quantdigger/datasource/source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class SourceWrapper(object): 3 | """ 数据源包装器,使相关数据源支持逐步读取操作 """ 4 | 5 | def __init__(self, pcontract, data, max_length): 6 | self.data = data 7 | self.curbar = -1 8 | self.pcontract = pcontract 9 | self._max_length = max_length 10 | 11 | def __len__(self): 12 | return self._max_length 13 | 14 | def rolling_forward(self): 15 | """ 读取下一个数据""" 16 | self.curbar += 1 17 | if self.curbar == self._max_length: 18 | return False, self.curbar 19 | else: 20 | return True, self.curbar 21 | 22 | 23 | class DatasourceAbstract(object): 24 | '''数据源抽象基类''' 25 | 26 | def get_bars(self, pcontract, dt_start, dt_end): 27 | raise NotImplementedError 28 | 29 | def get_last_bars(self, pcontract, n): 30 | raise NotImplementedError 31 | 32 | def get_contracts(self): 33 | raise NotImplementedError 34 | 35 | def get_code2strpcon(self): 36 | raise NotImplementedError 37 | -------------------------------------------------------------------------------- /quantdigger/util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import six 4 | import datetime 5 | import os 6 | import time 7 | import sys 8 | 9 | from .log import gen_log as log 10 | MAX_DATETIME = datetime.datetime(2100, 1, 1) 11 | 12 | project_dir = os.path.abspath(os.path.dirname(os.path.abspath(os.path.dirname(os.path.abspath(os.path.dirname(__file__)))))) 13 | source_dir = os.path.abspath(os.path.dirname(os.path.abspath(os.path.dirname(__file__)))) 14 | 15 | 16 | if sys.version_info >= (3,): 17 | py = 3 18 | else: 19 | py = 2 20 | 21 | def deprecated(f): 22 | def ff(*args, **kwargs): 23 | six.print_('{0} is deprecated!'.format(str(f))) 24 | return f(*args, **kwargs) 25 | return ff 26 | 27 | #def api(method): 28 | #def wrapper(*args, **kwargs): 29 | #rst = method(*args, **kwargs) 30 | #return rst 31 | #return wrapper 32 | 33 | 34 | def time2int(t): 35 | """ datetime转化为unix毫秒时间。 """ 36 | epoch = int(time.mktime(t.timetuple()) * 1000) 37 | return epoch 38 | 39 | 40 | def int2time(tf): 41 | return datetime.datetime.fromtimestamp(float(tf) / 1000) 42 | -------------------------------------------------------------------------------- /demo/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from six.moves import input 4 | import subprocess 5 | import time 6 | import os 7 | from quantdigger.util import gen_log as log 8 | from quantdigger.util import project_dir 9 | 10 | ui_path = os.path.join(project_dir, "quantdigger", "widgets", "mplotwidgets", "mainwindow.py" ) 11 | shell_path = os.path.join(project_dir, "quantdigger", "interaction", "ipython_config.py") 12 | backend_path = os.path.join(project_dir, "quantdigger", "interaction", "backend.py") 13 | 14 | log.info("启动后台..") 15 | backend = subprocess.Popen('python %s' % backend_path, shell=True) 16 | time.sleep(1) 17 | 18 | log.info("启动主窗口..") 19 | #mainwindow = subprocess.Popen('python %s' % ui_path, shell=True) 20 | mainwindow = subprocess.Popen('python %s > log' % ui_path, shell=True) 21 | time.sleep(1) 22 | 23 | log.info("启动ipython..") 24 | shell = subprocess.call('ipython --config=%s' % shell_path, shell=True) 25 | ###notebook = subprocess.call('jupyter notebook --config=shell.py', shell=True) 26 | ###six.print_(mainwindow.pid) 27 | 28 | input("Any key to quit quantdigger.") 29 | subprocess.Popen.kill(mainwindow) 30 | subprocess.Popen.kill(backend) 31 | -------------------------------------------------------------------------------- /tests/test_zmq_speed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file test_interactive.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.1 7 | # @date 2016-08-07 8 | 9 | 10 | import subprocess 11 | import os 12 | import time 13 | from timeit import timeit 14 | 15 | from quantdigger.util import gen_log as log 16 | from quantdigger.util import project_dir 17 | from quantdigger.event.rpc import EventRPCClient 18 | from quantdigger.event.eventengine import ZMQEventEngine 19 | from quantdigger.interaction.backend import Backend 20 | 21 | 22 | backend_path = os.path.join(project_dir, "quantdigger", "interaction", "backend.py") 23 | 24 | log.info("启动后台..") 25 | backend = subprocess.Popen('python %s' % backend_path, shell=True) 26 | time.sleep(1) 27 | 28 | 29 | engine = ZMQEventEngine('WindowGate') 30 | engine.start() 31 | shell = EventRPCClient('test_shell', engine, Backend.SERVER_FOR_SHELL) 32 | 33 | 34 | def func(): 35 | shell.sync_call("test_speed") 36 | return 37 | 38 | 39 | t = timeit('func()', 'from __main__ import func', number=100) 40 | print(t) 41 | 42 | subprocess.Popen.kill(backend) 43 | a = raw_input("Any key to quit quantdigger.") 44 | -------------------------------------------------------------------------------- /quantdigger/engine/strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from quantdigger.engine.execute_unit import ExecuteUnit 3 | 4 | # 系统角色 5 | 6 | 7 | def add_strategies(symbols, strategies): 8 | """ 一个组合中的策略 9 | 10 | Args: 11 | algos (list): 一个策略组合 12 | 13 | Returns: 14 | Profile. 组合结果 15 | """ 16 | simulator = ExecuteUnit(symbols, "1980-1-1", "2100-1-1", None, {}) 17 | profiles = list(simulator.add_strategies(strategies)) 18 | simulator.run() 19 | 20 | return profiles 21 | 22 | 23 | class Strategy(object): 24 | """ 策略基类""" 25 | def __init__(self, name): 26 | self.name = name 27 | 28 | def on_init(self, ctx): 29 | """初始化数据""" 30 | return 31 | 32 | def on_symbol_init(self, ctx): 33 | """初始化数据""" 34 | return 35 | 36 | def on_symbol_step(self, ctx): 37 | """ 逐合约逐根k线运行 """ 38 | return 39 | 40 | def on_symbol(self, ctx): 41 | """ 逐合约逐根k线运行 """ 42 | return 43 | 44 | def on_bar(self, ctx): 45 | """ 逐根k线运行 """ 46 | # 停在最后一个合约处 47 | return 48 | 49 | def on_exit(self, ctx): 50 | """ 策略结束前运行 """ 51 | # 停在最后一根bar 52 | return 53 | 54 | __all__ = ['add_strategies', 'Strategy'] 55 | -------------------------------------------------------------------------------- /tests/import_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | import os 4 | 5 | import pandas as pd 6 | 7 | from quantdigger import locd, set_config 8 | from quantdigger.datasource import import_data 9 | 10 | 11 | def import_contracts(): 12 | """ 从文件导入合约到数据库""" 13 | df = pd.read_csv('./work/contracts.txt') 14 | df['key'] = df['code'] + '.' + df['exchange'] 15 | return df 16 | 17 | 18 | six.print_("import contracts info..") 19 | contracts = import_contracts() 20 | 21 | set_config({'source': 'csv'}) 22 | locd.import_contracts(contracts) 23 | six.print_("import bars..") 24 | fpaths = [] 25 | for path, dirs, files in os.walk('./work'): 26 | for file in files: 27 | filepath = path + os.sep + file 28 | if filepath.endswith(".csv") or filepath.endswith(".CSV"): 29 | fpaths.append(filepath) 30 | import_data(fpaths, locd) 31 | 32 | set_config({'source': 'sqlite'}) 33 | locd.import_contracts(contracts) 34 | 35 | six.print_("import bars..") 36 | fpaths = [] 37 | for path, dirs, files in os.walk('./work'): 38 | for file in files: 39 | filepath = path + os.sep + file 40 | if filepath.endswith(".csv") or filepath.endswith(".CSV"): 41 | fpaths.append(filepath) 42 | import_data(fpaths, locd) 43 | -------------------------------------------------------------------------------- /demo/mplot_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import matplotlib 3 | import pandas as pd 4 | 5 | from quantdigger.widgets.mplotwidgets import widgets 6 | from quantdigger.widgets.mplotwidgets.mplots import Candles 7 | from quantdigger.technicals.common import MA, Volume 8 | 9 | matplotlib.use('TkAgg') 10 | import matplotlib.pyplot as plt 11 | 12 | price_data = pd.read_csv('data/IF000.csv', index_col=0, parse_dates=True) 13 | fig = plt.figure() 14 | 15 | frame = widgets.TechnicalWidget(fig, price_data) 16 | axes = frame.init_layout(50, # 窗口显示k线数量。 17 | 4, 18 | 1) # 两个1:1大小的窗口 19 | 20 | candle_widget = widgets.FrameWidget(axes[0], "candle_widget", 100, 50) 21 | candles = Candles(price_data, None, 'candles') 22 | ma30 = MA(price_data.close, 30, 'MA30', 'b', 2) 23 | ma20 = MA(price_data.close, 20, 'MA20', 'y', 2) 24 | candle_widget.add_plotter(candles, False) 25 | candle_widget.add_plotter(ma30, False) 26 | candle_widget.add_plotter(ma20, False) 27 | 28 | volume_widget = widgets.FrameWidget(axes[1], "volume_widget ", 100, 50) 29 | volume_plotter = Volume(price_data.open, price_data.close, price_data.vol) 30 | volume_widget.add_plotter(volume_plotter, False) 31 | 32 | frame.add_widget(0, candle_widget, True) 33 | frame.add_widget(1, volume_widget, True) 34 | frame.draw_widgets() 35 | plt.show() 36 | -------------------------------------------------------------------------------- /quantdigger/datasource/data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file data.py 4 | # @brief 数据控制模块 5 | # @author skabyy 6 | # @version 0.3 7 | # @date 2016-05-26 8 | 9 | from .dsutil import get_setting_datasource 10 | from quantdigger.datastruct import PContract, Contract 11 | from quantdigger.util import log 12 | 13 | 14 | class DataManager(object): 15 | """ 16 | 数据代理 17 | """ 18 | 19 | DEFAULT_DT_START = '1980-1-1' 20 | DEFAULT_DT_END = '2100-1-1' 21 | 22 | def __init__(self): 23 | self._src, type_ = get_setting_datasource() 24 | if Contract.source_type and Contract.source_type != type_: 25 | log.warn("数据源发生了切换!之前可能以另外一个数据源调用Contract.xxx") 26 | Contract.info = self._src.get_contracts() 27 | Contract.source_type = type_ 28 | 29 | def get_bars(self, strpcon, 30 | dt_start=DEFAULT_DT_START, dt_end=DEFAULT_DT_END): 31 | pcontract = PContract.from_string(strpcon) 32 | return self._src.get_bars(pcontract, dt_start, dt_end) 33 | 34 | def get_last_bars(self, strpcon, n): 35 | pcontract = PContract.from_string(strpcon) 36 | return self._src.get_last_bars(pcontract, n) 37 | 38 | def get_code2strpcon(self): 39 | return self._src.get_code2strpcon() 40 | 41 | def get_contracts(self): 42 | return self._src.get_contracts() 43 | 44 | -------------------------------------------------------------------------------- /quantdigger/engine/context/plotter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class PlotterDelegator(object): 5 | """docstring for PlotterDele""" 6 | def __init__(self): 7 | self.marks = [{}, {}] 8 | 9 | def plot_line(self, name, ith_window, x, y, 10 | styles, lw=1, ms=10, twinx=False): 11 | """ 绘制曲线 12 | 13 | Args: 14 | name (str): 标志名称 15 | ith_window (int): 在第几个窗口显示,从1开始。 16 | x (datetime): 时间坐标 17 | y (float): y坐标 18 | styles (str): 控制颜色,线的风格,点的风格 19 | lw (int): 线宽 20 | ms (int): 点的大小 21 | """ 22 | mark = self.marks[0].setdefault(name, []) 23 | mark.append((ith_window - 1, twinx, x - 1, float(y), styles, lw, ms)) 24 | 25 | def plot_text(self, name, ith_window, x, y, text, 26 | color='black', size=15, rotation=0): 27 | 28 | """ 绘制文本 29 | 30 | Args: 31 | name (str): 标志名称 32 | ith_window (int): 在第几个窗口显示,从1开始。 33 | x (float): x坐标 34 | y (float): y坐标 35 | text (str): 文本内容 36 | color (str): 颜色 37 | size (int): 字体大小 38 | rotation (float): 旋转角度 39 | """ 40 | mark = self.marks[1].setdefault(name, []) 41 | mark.append((ith_window - 1, x - 1, float(y), text, color, size, rotation)) 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from codecs import open 3 | 4 | from setuptools import setup, find_packages 5 | 6 | with open("README.rst", "r", "utf-8") as f: 7 | readme = f.read() 8 | 9 | setup( 10 | name="QuantDigger", 11 | version="0.6.0", 12 | description="量化交易Python回测系统", 13 | long_description=readme, 14 | author="QuantFans", 15 | author_email="dingjie.wang@foxmail.com", 16 | license="MIT", 17 | url="https://github.com/QuantFans/quantdigger", 18 | packages=find_packages(exclude=['tests', 'demo', "requirements", "images"]), 19 | include_package_data=True, 20 | install_requires=[ 21 | "tushare>=0.8.2", 22 | "logbook>=0.12.5", 23 | "ta-lib>=0.4.8", 24 | "progressbar2>=3.6.2", 25 | "matplotlib>=1.5.1", 26 | "pandas>=0.20.2", 27 | "python-dateutil>=2.4.2", 28 | "numpy>=1.10.4", 29 | "pymongo>=3.1.1", 30 | "pyzmq>=4.1.5", 31 | "lxml>=3.5.0", 32 | "six" 33 | #"cython>=0.23.4", 34 | ], 35 | classifiers=[ 36 | #'Environment :: Finance', 37 | "Development Status :: 3 - Alpha", 38 | "Intended Audience :: Developers", 39 | "License :: OSI Approved :: MIT License", 40 | 'Topic :: Software Development :: Libraries :: Python Modules', 41 | "Operating System :: OS Independent", 42 | 'Programming Language :: Python', 43 | ], 44 | zip_safe=False, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/datasource/test_csv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pandas as pd 3 | import os 4 | import unittest 5 | from logbook import Logger 6 | from quantdigger import ConfigUtil 7 | from quantdigger.datasource.data import DataManager 8 | 9 | logger = Logger('test') 10 | _DT_START = '1980-1-1' 11 | _DT_END = '2100-1-1' 12 | 13 | 14 | class TestCsvSource(unittest.TestCase): 15 | 16 | def test_csv_source(self): 17 | source_bak = ConfigUtil.get('source') 18 | logger.info('***** 数据测试开始 *****') 19 | ConfigUtil.set(source='csv') 20 | data_manager = DataManager() 21 | target = data_manager.get_bars( 22 | 'BB.TEST-1.Minute', _DT_START, _DT_END) 23 | fname = os.path.join(os.getcwd(), 'data', '1MINUTE', 'TEST', 'CC.csv') 24 | logger.info('-- CSV数据路径: ' + fname + ' --') 25 | source = pd.read_csv( 26 | fname, parse_dates=['datetime'], index_col='datetime') 27 | self.assertFalse(source.equals(target), '本地数据接口负测试失败!') 28 | fname = os.path.join(os.getcwd(), 'data', '1MINUTE', 'TEST', 'BB.csv') 29 | source = pd.read_csv( 30 | fname, parse_dates=['datetime'], index_col='datetime') 31 | self.assertTrue(source.equals(target), '本地数据接口正测试失败!') 32 | logger.info('-- 本地数据接口测试成功 --') 33 | ConfigUtil.set(source=source_bak) 34 | logger.info('***** 数据测试结束 *****\n') 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /tests/datasource/test_sqlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pandas as pd 3 | import os 4 | import unittest 5 | from logbook import Logger 6 | from quantdigger import ConfigUtil 7 | from quantdigger.datasource.data import DataManager 8 | 9 | logger = Logger('test') 10 | _DT_START = '1980-1-1' 11 | _DT_END = '2100-1-1' 12 | 13 | 14 | class TestSqliteSource(unittest.TestCase): 15 | def test_local_data2(self): 16 | old_source = ConfigUtil.get('source') 17 | if old_source == 'csv': 18 | return 19 | logger.info('***** 数据测试开始 *****') 20 | ConfigUtil.set(source='sqlite') 21 | data_manager = DataManager() 22 | target = data_manager.get_bars( 23 | 'BB.TEST-1.Minute', _DT_START, _DT_END).data 24 | fname = os.path.join(os.getcwd(), 'data', '1MINUTE', 'TEST', 'CC.csv') 25 | source = pd.read_csv( 26 | fname, parse_dates='datetime', index_col='datetime') 27 | self.assertFalse(source.equals(target), '本地数据接口负测试失败!') 28 | fname = os.path.join(os.getcwd(), 'data', '1MINUTE', 'TEST', 'BB.csv') 29 | source = pd.read_csv( 30 | fname, parse_dates='datetime', index_col='datetime') 31 | self.assertTrue(source.equals(target), '本地数据接口正测试失败!') 32 | logger.info('-- 本地数据接口测试成功 --') 33 | logger.info('***** 数据测试结束 *****\n') 34 | ConfigUtil.set(source=old_source) 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /tests/interaction/test_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file test_interactive.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.1 7 | # @date 2016-08-07 8 | 9 | import six 10 | import json 11 | import time, sys 12 | import unittest 13 | 14 | from quantdigger.event.rpc import EventRPCClient 15 | from quantdigger.interaction.backend import Backend 16 | 17 | backend = Backend() 18 | 19 | class TestBackend(unittest.TestCase): 20 | 21 | ui = EventRPCClient('test_ui', backend._engine, 22 | backend.SERVER_FOR_UI) 23 | shell = EventRPCClient('test_shell', backend._engine, 24 | backend.SERVER_FOR_SHELL) 25 | 26 | 27 | def test_get_all_contracts(self): 28 | ret = self.ui.sync_call("get_all_contracts") 29 | ret = self.shell.sync_call("get_all_contracts") 30 | six.print_("***********" ) 31 | six.print_(ret) 32 | six.print_("***********" ) 33 | 34 | def test_get_pcontract(self): 35 | ret = self.ui.sync_call("get_pcontract", { 36 | 'str_pcontract': 'BB.TEST-1.MINUTE' 37 | }) 38 | ret = self.shell.sync_call("get_pcontract", { 39 | 'str_pcontract': 'BB.TEST-1.MINUTE' 40 | }) 41 | six.print_(json.loads(ret).keys()) 42 | return 43 | 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | try: 49 | while True: 50 | time.sleep(1) 51 | except KeyboardInterrupt: 52 | ## @BUG 如果异常退出,系统可能遗留连接,导致多次回调。 53 | backend.stop() 54 | sys.exit(0) 55 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/tushare_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pandas as pd 4 | import tushare as ts 5 | 6 | from quantdigger.datasource.cache import CachedDatasource 7 | from quantdigger.datasource.dsutil import register_datasource 8 | from quantdigger.datasource.source import SourceWrapper, DatasourceAbstract 9 | from quantdigger.datasource.impl.localfs_cache import LocalFsCache 10 | 11 | 12 | def _process_ts_dt(dt): 13 | return str(pd.to_datetime(dt)) 14 | 15 | 16 | @register_datasource('tushare') 17 | class TuShareSource(DatasourceAbstract): 18 | '''TuShare数据源''' 19 | 20 | def __init__(self): 21 | pass 22 | 23 | def get_bars(self, pcontract, dt_start, dt_end): 24 | # TODO: 判断pcontract是不是股票,或者tushare支持期货? 25 | data = self._load_data(pcontract.contract.code, dt_start, dt_end) 26 | assert data.index.is_unique 27 | return SourceWrapper(pcontract, data, len(data)) 28 | 29 | def get_last_bars(self, pcontract, n): 30 | # TODO 31 | pass 32 | 33 | def _load_data(self, code, dt_start, dt_end): 34 | dts = _process_ts_dt(dt_start) 35 | dte = _process_ts_dt(dt_end) 36 | data = ts.get_k_data(code, start=dts, end=dte) 37 | data.set_index('date',drop=True,inplace=True) 38 | data.index.names = ['datetime'] 39 | return data.iloc[::-1] 40 | 41 | def get_contracts(self): 42 | # TODO 43 | return pd.DataFrame() 44 | 45 | 46 | @register_datasource('cached-tushare', 'cache_path') 47 | def CachedTuShareSource(cache_path): 48 | return CachedDatasource(TuShareSource(), LocalFsCache(cache_path)) 49 | -------------------------------------------------------------------------------- /quantdigger/interaction/serialize.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @file serialize.py 3 | # @brief 4 | # @author Wells 5 | # @version 0.5 6 | # @date 2016-08-07 7 | 8 | from datetime import datetime 9 | import pandas as pd 10 | from quantdigger.datastruct import PContract, Contract 11 | 12 | import json 13 | from json import JSONEncoder 14 | 15 | 16 | class DataStructCoder(JSONEncoder): 17 | def default(self, o): 18 | return o.__dict__ 19 | 20 | 21 | def serialize_pcontract_bars(str_pcontract, bars): 22 | data = { 23 | 'pcontract': str_pcontract, 24 | 'datetime': list(map(lambda x: str(x), bars.index)), 25 | 'open': bars.open.tolist(), 26 | 'close': bars.close.tolist(), 27 | 'high': bars.high.tolist(), 28 | 'low': bars.low.tolist(), 29 | 'vol': bars.volume.tolist(), 30 | } 31 | return json.dumps(data) 32 | 33 | 34 | def deserialize_pcontract_bars(data): 35 | data = json.loads(data) 36 | dt = list(map(lambda x: datetime.strptime(x, "%Y-%m-%d %H:%M:%S"), data['datetime'])) 37 | # datetime.datetime.strptime(string_date, "%Y-%m-%d %H:%M:%S.%f") 38 | pcon = data['pcontract'] 39 | del data['pcontract'] 40 | del data['datetime'] 41 | return pcon, pd.DataFrame(data, index=dt) 42 | 43 | 44 | def serialize_all_pcontracts(pcontracts): 45 | return [str(pcontract) for pcontract in pcontracts] 46 | 47 | 48 | def serialize_all_contracts(contracts): 49 | return [str(contract) for contract in contracts] 50 | 51 | 52 | def deserialize_all_pcontracts(pcontracts): 53 | return [PContract.from_string(strpcon) for strpcon in pcontracts] 54 | 55 | 56 | def deserialize_all_contracts(contracts): 57 | return [Contract.from_string(strcon) for strcon in contracts] 58 | -------------------------------------------------------------------------------- /quantdigger/interaction/shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from quantdigger.event.rpc import EventRPCClient 4 | from quantdigger.event.eventengine import ZMQEventEngine 5 | from quantdigger.interaction import ( 6 | Backend, 7 | WindowGate, 8 | ) 9 | from quantdigger.util import mlogger as log 10 | 11 | 12 | class Shell: 13 | """ 终端接口类,可通过它在python终端上操作界面和后台代码。 """ 14 | def __init__(self): 15 | log.info("Init Shell..") 16 | self._engine = ZMQEventEngine('Shell') 17 | self._engine.start() 18 | self.gate = EventRPCClient('Shell', 19 | self._engine, 20 | WindowGate.SERVER_FOR_SHELL) 21 | 22 | self._backend = EventRPCClient('test_shell', 23 | self._engine, 24 | Backend.SERVER_FOR_SHELL) 25 | 26 | def get_all_pcontracts(self): 27 | pass 28 | 29 | def get_all_contracts(self): 30 | ret = self._backend.sync_call("get_all_contracts") 31 | print(ret) 32 | 33 | def show_data(self, strpcontract): 34 | return self.gate.sync_call("show_data", { 35 | 'pcontract': strpcontract 36 | }) 37 | 38 | def get_pcontract(self, pcontract): 39 | """docstring for get_data""" 40 | pass 41 | 42 | def run_strategy(self, name): 43 | """""" 44 | return 45 | 46 | def run_technical(self, name): 47 | return 48 | 49 | def get_technicals(self): 50 | """ 获取系统的所有指标。 """ 51 | return 52 | 53 | def get_strategies(self): 54 | return 'hello' 55 | 56 | def plot(self): 57 | """docstring for plo""" 58 | print("plot") 59 | 60 | 61 | shell = Shell() 62 | -------------------------------------------------------------------------------- /tests/interaction/test_shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file test_shell.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.1 7 | # @date 2016-08-07 8 | 9 | import six 10 | import sys 11 | import unittest 12 | 13 | from quantdigger.interaction.backend import Backend 14 | from quantdigger.interaction.shell import shell 15 | 16 | 17 | backend = Backend() 18 | class TestShell(object): 19 | """ WindowGate call backend """ 20 | def __init__(self): 21 | pass 22 | #self._engine = ZMQEventEngine('TestShell') 23 | #self._engine.start() 24 | #self._gate = EventRPCClient('TestShell', self._engine, 25 | #ConfigInteraction.ui_server_for_shell) 26 | #self._backend = EventRPCClient('TestShell', self._engine, 27 | #ConfigInteraction.backend_server_for_shell) 28 | 29 | 30 | def test_get_all_contracts(self): 31 | six.print_(shell.get_all_contracts()) 32 | 33 | 34 | 35 | #def test_get_pcontract(self): 36 | #ret = self.shell.sync_call("get_pcontract", { 37 | #'str_pcontract': 'BB.TEST-1.MINUTE' 38 | #}) 39 | #six.print_(json.loads(ret).keys()) 40 | #return 41 | 42 | 43 | 44 | if __name__ == '__main__': 45 | #unittest.main() 46 | #try: 47 | #while True: 48 | #time.sleep(1) 49 | #except KeyboardInterrupt: 50 | ### @BUG 如果异常退出,系统可能遗留连接,导致多次回调。 51 | #backend.stop() 52 | #sys.exit(0) 53 | import time 54 | t = TestShell() 55 | t.test_get_all_contracts() 56 | try: 57 | while True: 58 | time.sleep(1) 59 | except KeyboardInterrupt: 60 | ## @BUG 如果异常退出,系统可能遗留连接,导致多次回调。 61 | backend.stop() 62 | sys.exit(0) 63 | -------------------------------------------------------------------------------- /demo/work/_djtrend2_IF000.csv: -------------------------------------------------------------------------------- 1 | entry_datetime,entry_price,exit_datetime,exit_price 2 | 2010-05-24 11:00:00,2932,2010-05-25 13:45:00,2836 3 | 2010-07-09 14:00:00,2668,2010-07-15 09:45:00,2646 4 | 2010-07-19 14:45:00,2710,2010-08-05 14:45:00,2842 5 | 2010-08-16 14:45:00,2950,2010-08-20 10:45:00,2950 6 | 2010-09-06 13:45:00,2988,2010-09-09 13:00:00,2954 7 | 2010-10-08 09:15:00,2982,2010-11-12 13:30:00,3412 8 | 2010-12-13 13:30:00,3252,2010-12-20 11:00:00,3256 9 | 2011-01-31 11:15:00,3092,2011-02-09 09:30:00,3076 10 | 2011-02-14 10:45:00,3198,2011-02-22 11:15:00,3192 11 | 2011-03-07 09:30:00,3318,2011-03-10 09:45:00,3334 12 | 2011-03-23 13:00:00,3280,2011-03-29 14:15:00,3286 13 | 2011-04-08 13:30:00,3364,2011-04-12 14:15:00,3340 14 | 2011-06-24 13:30:00,3038,2011-07-12 14:45:00,3068 15 | 2011-08-26 09:15:00,2908,2011-08-30 14:15:00,2836 16 | 2011-09-21 13:30:00,2772,2011-09-22 13:00:00,2718 17 | 2011-10-26 13:15:00,2678,2011-11-02 09:30:00,2670 18 | 2011-11-03 09:45:00,2772,2011-11-09 11:15:00,2718 19 | 2012-01-09 14:45:00,2378,2012-01-13 13:00:00,2402 20 | 2012-01-18 09:30:00,2504,2012-03-23 13:00:00,2580 21 | 2012-04-12 14:15:00,2564,2012-04-17 11:00:00,2564 22 | 2012-04-20 14:45:00,2644,2012-04-24 09:30:00,2612 23 | 2012-05-02 13:15:00,2700,2012-05-04 10:45:00,2690 24 | 2012-05-29 11:15:00,2620,2012-06-04 09:30:00,2584 25 | 2012-09-07 13:15:00,2350,2012-09-11 10:15:00,2312 26 | 2012-09-27 14:30:00,2280,2012-10-25 13:00:00,2318 27 | 2012-12-05 11:00:00,2214,2013-02-21 13:15:00,2614 28 | 2013-03-20 14:00:00,2608,2013-03-26 09:30:00,2602 29 | 2013-04-19 13:15:00,2520,2013-04-23 09:45:00,2504 30 | 2013-05-17 14:00:00,2570,2013-05-24 13:15:00,2566 31 | 2013-05-28 14:45:00,2624,2013-06-04 10:15:00,2570 32 | 2013-07-05 13:30:00,2198,2013-07-08 09:30:00,2130 33 | 2013-07-11 09:45:00,2220,2013-07-12 14:15:00,2268 34 | 2013-08-02 09:30:00,2264,2013-08-16 10:00:00,2304 35 | 2013-09-09 09:30:00,2372,2013-09-17 14:45:00,2442 36 | -------------------------------------------------------------------------------- /quantdigger/engine/orderedset.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | 4 | class OrderedSet(collections.MutableSet): 5 | 6 | def __init__(self, iterable=None): 7 | self.end = end = [] 8 | end += [None, end, end] # sentinel node for doubly linked list 9 | self.map = {} # key --> [key, prev, next] 10 | if iterable is not None: 11 | self |= iterable 12 | 13 | def __len__(self): 14 | return len(self.map) 15 | 16 | def __contains__(self, key): 17 | return key in self.map 18 | 19 | def add(self, key): 20 | if key not in self.map: 21 | end = self.end 22 | curr = end[1] 23 | curr[2] = end[1] = self.map[key] = [key, curr, end] 24 | 25 | def discard(self, key): 26 | if key in self.map: 27 | key, prev, next = self.map.pop(key) 28 | prev[2] = next 29 | next[1] = prev 30 | 31 | def __iter__(self): 32 | end = self.end 33 | curr = end[2] 34 | while curr is not end: 35 | yield curr[0] 36 | curr = curr[2] 37 | 38 | def __reversed__(self): 39 | end = self.end 40 | curr = end[1] 41 | while curr is not end: 42 | yield curr[0] 43 | curr = curr[1] 44 | 45 | def pop(self, last=True): 46 | if not self: 47 | raise KeyError('set is empty') 48 | key = self.end[1][0] if last else self.end[2][0] 49 | self.discard(key) 50 | return key 51 | 52 | def __repr__(self): 53 | if not self: 54 | return '%s()' % (self.__class__.__name__,) 55 | return '%s(%r)' % (self.__class__.__name__, list(self)) 56 | 57 | def __eq__(self, other): 58 | if isinstance(other, OrderedSet): 59 | return len(self) == len(other) and list(self) == list(other) 60 | return set(self) == set(other) 61 | -------------------------------------------------------------------------------- /demo/stock_search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @file stock_search.py 4 | # @brief 选股的例子 5 | # @author wondereamer 6 | # @version 0.2 7 | # @date 2015-12-09 8 | 9 | from quantdigger import * 10 | 11 | class DemoStrategy(Strategy): 12 | """ 策略A1 """ 13 | def __init__(self, name): 14 | super(DemoStrategy, self).__init__(name) 15 | self.candicates = [] 16 | self.to_sell = [] 17 | 18 | def on_init(self, ctx): 19 | """初始化数据""" 20 | ctx.ma10 = MA(ctx.close, 10, 'ma10', 'y', 2) 21 | ctx.ma20 = MA(ctx.close, 20, 'ma20', 'b', 2) 22 | 23 | def on_symbol(self, ctx): 24 | if ctx.curbar > 20: 25 | if ctx.ma10[1] < ctx.ma20[1] and ctx.ma10 > ctx.ma20: 26 | self.candicates.append(ctx.symbol) 27 | elif ctx.ma10[1] < ctx.ma20[1]: 28 | self.to_sell.append(ctx.symbol) 29 | 30 | def on_bar(self, ctx): 31 | for symbol in self.to_sell: 32 | if ctx.pos('long', symbol) > 0: 33 | ctx.sell(ctx[symbol].close, 1, symbol) 34 | 35 | for symbol in self.candicates: 36 | if ctx.pos('long', symbol) == 0: 37 | ctx.buy(ctx[symbol].close, 1, symbol) 38 | 39 | 40 | self.candicates = [] 41 | self.to_sell = [] 42 | return 43 | 44 | def on_exit(self, ctx): 45 | print("策略运行结束.") 46 | return 47 | 48 | 49 | 50 | if __name__ == '__main__': 51 | # 52 | profiles = add_strategies(['*.SH'], [ 53 | { 54 | 'strategy': DemoStrategy('A1'), 55 | 'capital': 500000000.0 56 | } 57 | ]) 58 | 59 | 60 | from quantdigger.digger import finance, plotting 61 | curve = finance.create_equity_curve(Profile.all_holdings_sum(profiles)) 62 | #plotting.plot_strategy(profile.data('AA.SHFE-1.Minute'), profile.technicals(0), 63 | #profile.deals(0), curve.equity.values) 64 | ## 绘制净值曲线 65 | plotting.plot_curves([curve.networth]) 66 | -------------------------------------------------------------------------------- /quantdigger/datasource/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from quantdigger.datasource.source import DatasourceAbstract 3 | from quantdigger.util import log 4 | from quantdigger.infras.object import HashObject 5 | 6 | 7 | class CachedDatasource(DatasourceAbstract): 8 | '''带缓存的数据源''' 9 | 10 | def __init__(self, datasource, cache): 11 | self.datasource = datasource 12 | self.cache = cache 13 | 14 | def get_bars(self, pcontract, dt_start, dt_end): 15 | try: 16 | log.info('trying to load from cache') 17 | return self.cache.get_bars(pcontract, dt_start, dt_end) 18 | except LoadCacheException as e: 19 | log.info('updating cache') 20 | missing_range = e.missing_range 21 | log.info('missing range: {0}', missing_range) 22 | missing_data = [] 23 | for start, end in missing_range: 24 | wrapper = self.datasource.get_bars(pcontract, start, end) 25 | missing_data.append(HashObject.new(data=wrapper.data, 26 | start=start, 27 | end=end)) 28 | self.cache.save_data(missing_data, pcontract) 29 | log.info('loading cache') 30 | return self.cache.get_bars(pcontract, dt_start, dt_end) 31 | 32 | def get_last_bars(self, pcontract, n): 33 | raise NotImplementedError 34 | 35 | def get_contracts(self): 36 | # TODO: 37 | return self.datasource.get_contracts() 38 | 39 | 40 | class CacheAbstract(DatasourceAbstract): 41 | '''缓存抽象类''' 42 | 43 | def save_data(self, data, pcontract): 44 | raise NotImplementedError 45 | 46 | 47 | class LoadCacheException(Exception): 48 | def __init__(self, missing_range, cached_data=None): 49 | assert(missing_range) 50 | self.cached_data = cached_data 51 | self.missing_range = missing_range 52 | 53 | def __str__(self): 54 | return str(self.missing_range) 55 | -------------------------------------------------------------------------------- /tests/interaction/test_window_gate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file test_window_gate.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.1 7 | # @date 2016-08-07 8 | 9 | import six 10 | import json 11 | import time, sys 12 | import unittest 13 | 14 | from quantdigger.event.eventengine import ZMQEventEngine 15 | from quantdigger.interaction.backend import Backend 16 | from quantdigger.widgets.mplotwidgets.mainwindow import mainwindow 17 | 18 | 19 | backend = Backend() 20 | class TestWindowGateCallBackend(object): 21 | """ WindowGate call backend """ 22 | 23 | def __init__(self): 24 | self.engine = ZMQEventEngine('TestWindowGate') 25 | self.engine.start() 26 | self.gate = mainwindow._gate 27 | 28 | #def test_get_all_contracts(self): 29 | #ret = self.gate.sync_call("get_all_contracts") 30 | #six.print_("***********" ) 31 | ##six.print_(json.loads(ret)) 32 | #six.print_(ret) 33 | #six.print_("***********" ) 34 | 35 | def test_get_all_contracts(self): 36 | ret = self.gate.get_all_contracts() 37 | six.print_("***********" ) 38 | six.print_(ret) 39 | six.print_("***********" ) 40 | 41 | def test_get_all_pcontracts(self): 42 | ret = self.gate.get_all_pcontracts() 43 | six.print_("***********" ) 44 | six.print_(ret) 45 | six.print_("***********" ) 46 | 47 | #def test_get_pcontract(self): 48 | #ret = self.shell.sync_call("get_pcontract", { 49 | #'str_pcontract': 'BB.TEST-1.MINUTE' 50 | #}) 51 | #six.print_(json.loads(ret).keys()) 52 | #return 53 | 54 | 55 | 56 | if __name__ == '__main__': 57 | #unittest.main() 58 | #try: 59 | #while True: 60 | #time.sleep(1) 61 | #except KeyboardInterrupt: 62 | ### @BUG 如果异常退出,系统可能遗留连接,导致多次回调。 63 | #backend.stop() 64 | #sys.exit(0) 65 | t = TestWindowGateCallBackend() 66 | t.test_get_all_contracts() 67 | t.test_get_all_pcontracts() 68 | -------------------------------------------------------------------------------- /demo/work/signal_IF000.csv: -------------------------------------------------------------------------------- 1 | entry_date,entry_price,exit_datetime,exit_price,islong 2 | 2010-05-24 11:00:00,2932,2010-05-25 13:45:00,2836,True 3 | 2010-07-09 14:00:00,2668,2010-07-15 09:45:00,2646,True 4 | 2010-07-19 14:45:00,2710,2010-08-05 14:45:00,2842,True 5 | 2010-08-16 14:45:00,2950,2010-08-20 10:45:00,2950,True 6 | 2010-09-06 13:45:00,2988,2010-09-09 13:00:00,2954,True 7 | 2010-10-08 09:15:00,2982,2010-11-12 13:30:00,3412,True 8 | 2010-12-13 13:30:00,3252,2010-12-20 11:00:00,3256,True 9 | 2011-01-31 11:15:00,3092,2011-02-09 09:30:00,3076,True 10 | 2011-02-14 10:45:00,3198,2011-02-22 11:15:00,3192,True 11 | 2011-03-07 09:30:00,3318,2011-03-10 09:45:00,3334,True 12 | 2011-03-23 13:00:00,3280,2011-03-29 14:15:00,3286,True 13 | 2011-04-08 13:30:00,3364,2011-04-12 14:15:00,3340,True 14 | 2011-06-24 13:30:00,3038,2011-07-12 14:45:00,3068,True 15 | 2011-08-26 09:15:00,2908,2011-08-30 14:15:00,2836,True 16 | 2011-09-21 13:30:00,2772,2011-09-22 13:00:00,2718,True 17 | 2011-10-26 13:15:00,2678,2011-11-02 09:30:00,2670,True 18 | 2011-11-03 09:45:00,2772,2011-11-09 11:15:00,2718,True 19 | 2012-01-09 14:45:00,2378,2012-01-13 13:00:00,2402,True 20 | 2012-01-18 09:30:00,2504,2012-03-23 13:00:00,2580,True 21 | 2012-04-12 14:15:00,2564,2012-04-17 11:00:00,2564,True 22 | 2012-04-20 14:45:00,2644,2012-04-24 09:30:00,2612,True 23 | 2012-05-02 13:15:00,2700,2012-05-04 10:45:00,2690,True 24 | 2012-05-29 11:15:00,2620,2012-06-04 09:30:00,2584,True 25 | 2012-09-07 13:15:00,2350,2012-09-11 10:15:00,2312,True 26 | 2012-09-27 14:30:00,2280,2012-10-25 13:00:00,2318,True 27 | 2012-12-05 11:00:00,2214,2013-02-21 13:15:00,2614,True 28 | 2013-03-20 14:00:00,2608,2013-03-26 09:30:00,2602,True 29 | 2013-04-19 13:15:00,2520,2013-04-23 09:45:00,2504,True 30 | 2013-05-17 14:00:00,2570,2013-05-24 13:15:00,2566,True 31 | 2013-05-28 14:45:00,2624,2013-06-04 10:15:00,2570,True 32 | 2013-07-05 13:30:00,2198,2013-07-08 09:30:00,2130,True 33 | 2013-07-11 09:45:00,2220,2013-07-12 14:15:00,2268,True 34 | 2013-08-02 09:30:00,2264,2013-08-16 10:00:00,2304,True 35 | 2013-09-09 09:30:00,2372,2013-09-17 14:45:00,2442,True 36 | -------------------------------------------------------------------------------- /demo/profile_strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @file profile_strategy.py 4 | # @brief 策略运行和图表展示 5 | # @author wondereamer 6 | # @version 0.2 7 | # @date 2015-12-09 8 | 9 | import six 10 | #from quantdigger.engine.series import NumberSeries 11 | #from quantdigger.indicators.common import MA 12 | #from quantdigger.util import pcontract 13 | from quantdigger import * 14 | 15 | boll = { 16 | 'upper': [], 17 | 'middler': [], 18 | 'lower': [] 19 | } 20 | 21 | class DemoStrategy(Strategy): 22 | """ 策略A2 """ 23 | 24 | def on_init(self, ctx): 25 | """初始化数据""" 26 | ctx.ma5 = MA(ctx.close, 5, 'ma5', 'y', 2) #, 'ma200', 'b', '1') 27 | ctx.ma30 = MA(ctx.close, 30, 'ma30', 'black', 2) #, 'ma200', 'b', '1') 28 | 29 | def on_symbol(self, ctx): 30 | pass 31 | 32 | def on_bar(self, ctx): 33 | if ctx.curbar > 30: 34 | if ctx.ma5[2] < ctx.ma30[2] and ctx.ma5[1] > ctx.ma30[1]: 35 | if ctx.pos('long') == 0: 36 | ctx.buy(ctx.close, 1) 37 | if ctx.pos('short') > 0: 38 | ctx.cover(ctx.close, ctx.pos('short')) 39 | elif ctx.ma5[2] > ctx.ma30[2] and ctx.ma5[1] < ctx.ma30[1]: 40 | if ctx.pos('short') == 0: 41 | ctx.short(ctx.close, 1) 42 | if ctx.pos('long') > 0: 43 | ctx.sell(ctx.close, ctx.pos('long')) 44 | 45 | return 46 | 47 | def on_exit(self, ctx): 48 | return 49 | 50 | 51 | if __name__ == '__main__': 52 | import timeit 53 | from quantdigger.digger.analyze import AnalyzeFrame 54 | import matplotlib.pyplot as plt 55 | start = timeit.default_timer() 56 | set_config({ 'source': 'csv' }) 57 | profiles = add_strategies(['BB.SHFE-1.Day'], [ 58 | { 59 | 'strategy': DemoStrategy('A1'), 60 | 'capital': 5000000.0 61 | } 62 | ]) 63 | stop = timeit.default_timer() 64 | six.print_("运行耗时: %d秒" % ((stop - start ))) 65 | AnalyzeFrame(profiles[0]) 66 | plt.show() 67 | -------------------------------------------------------------------------------- /quantdigger/infras/ioc.py: -------------------------------------------------------------------------------- 1 | class IoCContainer(object): 2 | 3 | def __init__(self): 4 | self._container = {} 5 | 6 | def register(self, key, obj): 7 | if key in self._container: 8 | msg = 'IoC Error: %s already exists!\n\t' % key 9 | raise Exception(msg + str(self)) 10 | self._container[key] = obj 11 | 12 | def resolve(self, key): 13 | if key not in self._container: 14 | msg = 'IoC Error: %s does not exist!\n\t' % key 15 | raise Exception(msg + str(self)) 16 | return self._container[key] 17 | 18 | def set(self, key, obj): 19 | if key not in self._container: 20 | msg = 'IoC Error: %s does not exist!\n\t' % key 21 | raise Exception(msg + str(self)) 22 | self._container[key] = obj 23 | 24 | def keys(self): 25 | return self._container.keys() 26 | 27 | def __str__(self): 28 | return 'IoC: %s' % str(self._container) 29 | 30 | 31 | class IoCTrunk(object): 32 | 33 | def __init__(self, cls, args, kwargs): 34 | self.cls = cls 35 | self.args = args 36 | self.kwargs = kwargs 37 | 38 | def on_register(self, name): 39 | pass 40 | 41 | def construct(self): 42 | raise NotImplementedError 43 | 44 | 45 | def register_to(ioc_container, trunk_cls=None): 46 | def register(name, *args, **kwargs): 47 | def wrapper(obj): 48 | if trunk_cls is not None: 49 | # obj is a class 50 | trunk = trunk_cls(obj, args, kwargs) 51 | trunk.on_register(name) 52 | ioc_container.register(name, trunk) 53 | else: 54 | # obj is an instance 55 | ioc_container.register(name, obj) 56 | return obj 57 | return wrapper 58 | return register 59 | 60 | 61 | def resolve_from(ioc_container): 62 | def resolve(name): 63 | obj = ioc_container.resolve(name) 64 | if isinstance(obj, IoCTrunk): 65 | instance = obj.construct() 66 | ioc_container.set(name, instance) 67 | return instance 68 | else: 69 | return obj 70 | return resolve 71 | -------------------------------------------------------------------------------- /quantdigger/digger/finance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file finance.py 4 | # @brief 常见的金融工具函数 5 | # @author wondereamer 6 | # @version 0.2 7 | # @date 2015-12-09 8 | 9 | from six.moves import range 10 | import pandas as pd 11 | import numpy as np 12 | 13 | 14 | def sharpe_ratio(returns, periods=252): 15 | """ 16 | Create the Sharpe ratio for the strategy, based on a 17 | benchmark of zero (i.e. no risk-free rate information). 18 | 19 | Args: 20 | returns (list, Series) - A pandas Series representing 21 | period percentage returns. 22 | 23 | periods (int.) Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc. 24 | 25 | Returns: 26 | float. The result 27 | """ 28 | return np.sqrt(periods) * (np.mean(returns)) / np.std(returns) 29 | 30 | 31 | def max_drawdown(networth): 32 | """ 统计最大回测和相应的回测周期。 33 | 34 | networth: 历史净值 35 | """ 36 | hwm = [0] # 历史最大值序列变量 37 | eq_idx = networth.index 38 | drawdown = pd.Series(index=eq_idx) 39 | duration = pd.Series(index=eq_idx) 40 | 41 | for t in range(1, len(eq_idx)): 42 | cur_hwm = max(hwm[t-1], networth[t]) 43 | hwm.append(cur_hwm) 44 | drawdown[t] = hwm[t] - networth[t] 45 | # <=0 新高,计数0 46 | duration[t] = 0 if drawdown[t] <= 0 else duration[t-1] + 1 47 | return drawdown.max(), duration.max() 48 | 49 | 50 | def create_equity_curve(all_holdings): 51 | """ 创建资金曲线, 历史回报率对象。 52 | 53 | Args: 54 | all_holdings (list): 账号资金历史。 55 | 56 | Returns: 57 | pd.DataFrame 数据列 { 'cash', 'commission', 58 | 'equity', 'returns', 'networth' } 59 | """ 60 | curve = pd.DataFrame(all_holdings) 61 | curve.set_index('datetime', inplace=True) 62 | curve['returns'] = curve['equity'].pct_change() 63 | curve.iloc[0, 3] = 0 # reset first value as 0 instead nan 64 | curve['networth'] = (1.0+curve['returns']).cumprod() 65 | return curve 66 | 67 | 68 | def summary_stats(curve, periods): 69 | """ 70 | 策略统计。 71 | periods - Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc. 72 | """ 73 | total_return = curve['networth'][-1] 74 | returns = curve['returns'] 75 | pnl = curve['networth'] 76 | 77 | sharpe = sharpe_ratio(returns, periods) 78 | max_dd, dd_duration = max_drawdown(pnl) 79 | 80 | stats = [("Total Return", "%0.2f%%" % ((total_return - 1.0) * 100.0)), 81 | ("Sharpe Ratio", "%0.2f" % sharpe), 82 | ("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)), 83 | ("Drawdown Duration", "%d" % dd_duration)] 84 | return stats 85 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/mongodb_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pandas as pd 4 | import pymongo 5 | from pymongo import MongoClient 6 | 7 | from quantdigger.datasource import datautil 8 | from quantdigger.datasource.dsutil import * 9 | from quantdigger.datasource.source import SourceWrapper, DatasourceAbstract 10 | 11 | 12 | @register_datasource('mongodb', 'address', 'port', 'dbname') 13 | class MongoDBSource(DatasourceAbstract): 14 | '''MongoDBs数据源''' 15 | 16 | def __init__(self, address, port, dbname): 17 | # TODO address, port 18 | self._client = MongoClient() 19 | self._db = self._client[dbname] 20 | 21 | def _get_collection_name(self, period, exchange, code): 22 | return '{period}.{exchange}.{code}'.format( 23 | period=str(period).replace('.', ''), 24 | exchange=exchange, 25 | code=code) 26 | 27 | def _parse_collection_name(self, collection_name): 28 | return collection_name.split('.') 29 | 30 | def get_bars(self, pcontract, dt_start, dt_end): 31 | dt_start = pd.to_datetime(dt_start) 32 | dt_end = pd.to_datetime(dt_end) 33 | id_start, _ = datautil.encode2id(pcontract.period, dt_start) 34 | id_end, _ = datautil.encode2id(pcontract.period, dt_end) 35 | colname = self._get_collection_name( 36 | pcontract.period, 37 | pcontract.contract.exchange, 38 | pcontract.contract.code) 39 | cursor = self._db[colname].find({ 40 | 'id': { 41 | '$gt': id_start, 42 | '$lt': id_end 43 | } 44 | }).sort('id', pymongo.ASCENDING) 45 | data = pd.DataFrame(list(cursor)).set_index('datetime') 46 | return SourceWrapper(pcontract, data, len(data)) 47 | 48 | def get_last_bars(self, pcontract, n): 49 | raise NotImplementedError 50 | 51 | def get_contracts(self): 52 | colname = 'contract' 53 | cursor = self._db[colname].find() 54 | return pd.DataFrame(list(cursor)) 55 | 56 | def get_code2strpcon(self): 57 | symbols = {} 58 | period_exchange2strpcon = {} 59 | names = self._db.collection_names() 60 | symbols = {} 61 | period_exchange2strpcon = {} 62 | for name in filter(lambda n: n == 'system.indexes', names): 63 | period, exch, code = self._parse_collection_names(name) 64 | period_exch = '%s-%s' % (exch, period) 65 | strpcon = '%s.%s' % (code, period_exch) 66 | lst = symbols.setdefault(code, []) 67 | lst.append(strpcon) 68 | lst = period_exchange2strpcon(period_exch, []) 69 | lst.append(strpcon) 70 | return symbols, period_exchange2strpcon 71 | -------------------------------------------------------------------------------- /quantdigger/interaction/windowgate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file windowgate.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.5 7 | # @date 2016-07-10 8 | 9 | from quantdigger.datastruct import PContract 10 | from quantdigger.event.rpc import EventRPCClient, EventRPCServer 11 | from quantdigger.event.eventengine import ZMQEventEngine 12 | from quantdigger.util import gen_logger as log 13 | from quantdigger.interaction.backend import Backend 14 | 15 | from quantdigger.interaction.serialize import ( 16 | deserialize_all_pcontracts, 17 | deserialize_all_contracts, 18 | deserialize_pcontract_bars 19 | ) 20 | 21 | 22 | class WindowGate: 23 | SERVER_FOR_SHELL = "ui4shell" 24 | 25 | def __init__(self, widget): 26 | log.info("Init WindowGate..") 27 | self._engine = ZMQEventEngine('WindowGate') 28 | self._engine.start() 29 | self._backend = EventRPCClient('WindowGate', self._engine, Backend.SERVER_FOR_UI) 30 | self._shell_srv = EventRPCServer(self._engine, self.SERVER_FOR_SHELL) 31 | self._register_functions(self._shell_srv) 32 | self._period = None 33 | self._contract = None 34 | self._widget = widget 35 | 36 | def _register_functions(self, server): 37 | server.register('get_all_contracts', self.get_all_contracts) 38 | server.register('get_all_pcontracts', self.get_all_pcontracts) 39 | server.register('get_pcontract', self.get_pcontract) 40 | server.register('show_data', self.show_data) 41 | 42 | def add_widget(self, ith, type_): 43 | self._widget.add_widget 44 | 45 | def add_technical(self, ith, technical): 46 | """""" 47 | # @TODO compute technical with backend, 48 | # display result from backend 49 | return 50 | 51 | @property 52 | def pcontract(self): 53 | return PContract(self._contract, self._period) 54 | 55 | def stop(self): 56 | self._engine.stop() 57 | 58 | def get_all_contracts(self): 59 | ret = self._backend.sync_call('get_all_contracts') 60 | return deserialize_all_contracts(ret) 61 | 62 | def get_all_pcontracts(self): 63 | ret = self._backend.sync_call('get_all_pcontracts') 64 | return deserialize_all_pcontracts(ret) 65 | 66 | def get_pcontract(self, str_pcontract): 67 | ret = self._backend.sync_call('get_pcontract', { 68 | 'str_pcontract': str_pcontract 69 | }) 70 | return deserialize_pcontract_bars(ret) 71 | 72 | def run_strategy(self, name): 73 | """""" 74 | return 75 | 76 | def run_technical(self, name): 77 | return 78 | 79 | def get_technicals(self): 80 | """ 获取系统的所有指标。 """ 81 | return 82 | 83 | def get_strategies(self): 84 | """ 获取系统的所有策略。 """ 85 | return 86 | 87 | def show_data(self, pcontract): 88 | self._widget.show_data(pcontract) 89 | -------------------------------------------------------------------------------- /tests/backup/test_custom_datasource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | from quantdigger.engine.execute_unit import ExecuteUnit 4 | from quantdigger.indicators.common import MA, BOLL 5 | from quantdigger.engine.strategy import TradingStrategy 6 | from quantdigger.util import pcontract, stock 7 | from quantdigger.digger import deals 8 | import plotting 9 | import random 10 | import pdb 11 | 12 | import ds163 13 | 14 | code = '600096' 15 | 16 | class DemoStrategy(TradingStrategy): 17 | """ 策略实例 """ 18 | def __init__(self, exe): 19 | super(DemoStrategy, self).__init__(exe) 20 | six.print_('start: ', self.datetime[0]) 21 | 22 | #self.mabase = MA(self, self.close, 100,'mabase', 'r', '1') 23 | self.mabig = MA(self, self.close, 20,'mabig', 'b', '1') 24 | self.masmall = MA(self, self.close, 5,'masmall', 'y', '1') 25 | self.b_upper, self.b_middler, self.b_lower = BOLL(self, self.close, 10,'boll10', 'y', '1') 26 | self.num_cont = 0 27 | self.num_win = 0 28 | 29 | def __determine_position(self): 30 | P = 10 # 最少交易股数,这里假设是10 31 | quantity = int(self.cash() / self.open / P) * P 32 | return quantity 33 | 34 | def on_bar(self): 35 | """ 策略函数,对每根Bar运行一次。""" 36 | if self.volume == 0: return # 这天没有成交量,跳过 37 | if self.position() == 0 and self.masmall[1] <= self.mabig[1] and self.masmall > self.mabig: 38 | quantity = self.__determine_position() 39 | if quantity > 0: 40 | price = self.close[0] 41 | self.buy('long', price, quantity, contract = code) 42 | self.buy_price = price 43 | self.num_cont += 1 44 | #six.print_('buy', self.datetime[0].date(), price, quantity) 45 | elif self.position() > 0 and self.masmall < self.mabig: 46 | price = self.close[0] 47 | self.sell('long', price, self.position()) 48 | #six.print_('sel', self.datetime[0].date(), price, self.position()) 49 | #six.print_('---') 50 | if price > self.buy_price: 51 | self.num_win += 1 52 | 53 | if __name__ == '__main__': 54 | pcon = stock(code) 55 | simulator = ExecuteUnit([pcon], None, #'2015-08-02', 56 | # 使用自定义的数据源 57 | datasource=ds163.CachedStock163Source('163cache')) 58 | algo = DemoStrategy(simulator) 59 | simulator.run() 60 | #six.print_('close: ', algo.close.data) 61 | #six.print_('close length: ', algo.close.length_history) 62 | six.print_('total: %s, win: %s' % (algo.num_cont, algo.num_win)) 63 | 64 | # 显示回测结果 65 | a = {} 66 | b = [] 67 | try: 68 | for trans in algo.blotter.transactions: 69 | deals.update_positions(a, b, trans); 70 | except Exception, e: 71 | six.print_(e) 72 | plotting.plot_result(simulator.data[pcon], algo._indicators, b, algo.blotter) 73 | 74 | -------------------------------------------------------------------------------- /demo/manytomany.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file manytomany.py 4 | # @brief 两个组合, 每个组合有两个包含两个策略。 5 | # @author wondereamer 6 | # @version 0.2 7 | # @date 2015-12-09 8 | 9 | import six 10 | from quantdigger import * 11 | 12 | class DemoStrategy(Strategy): 13 | """ 策略A1 """ 14 | 15 | def on_init(self, ctx): 16 | """初始化数据""" 17 | ctx.ma10 = MA(ctx.close, 10, 'ma10', 'y', 2) 18 | ctx.ma20 = MA(ctx.close, 20, 'ma20', 'b', 2) 19 | 20 | def on_bar(self, ctx): 21 | if ctx.curbar > 20: 22 | if ctx.ma10[1] < ctx.ma20[1] and ctx.ma10 > ctx.ma20: 23 | ctx.buy(ctx.close, 1) 24 | six.print_('策略%s, 买入%s'%(ctx.strategy, ctx.symbol)) 25 | elif ctx.position() is not None and ctx.ma10[1] > ctx.ma20[1] and \ 26 | ctx.ma10 < ctx.ma20: 27 | ctx.sell(ctx.close, 1) 28 | six.print_('策略%s, 卖出%s'%(ctx.strategy, ctx.symbol)) 29 | 30 | def on_symbol(self, ctx): 31 | return 32 | 33 | def on_exit(self, ctx): 34 | return 35 | 36 | 37 | class DemoStrategy2(Strategy): 38 | """ 策略A2 """ 39 | 40 | def on_init(self, ctx): 41 | """初始化数据""" 42 | ctx.ma5 = MA(ctx.close, 5, 'ma5', 'y', 2) 43 | ctx.ma10 = MA(ctx.close, 10, 'ma10', 'black', 2) 44 | 45 | def on_bar(self, ctx): 46 | if ctx.curbar > 10: 47 | if ctx.ma5[1] < ctx.ma10[1] and ctx.ma5 > ctx.ma10: 48 | ctx.buy(ctx.close, 1) 49 | six.print_('策略%s, 买入%s'%(ctx.strategy, ctx.symbol)) 50 | elif ctx.position() is not None and ctx.ma5[1] > ctx.ma10[1] and \ 51 | ctx.ma5 < ctx.ma10: 52 | ctx.sell(ctx.close, 1) 53 | six.print_('策略%s, 卖出%s'%(ctx.strategy, ctx.symbol)) 54 | 55 | def on_symbol(self, ctx): 56 | return 57 | 58 | def on_exit(self, ctx): 59 | return 60 | 61 | if __name__ == '__main__': 62 | from quantdigger.digger import finance 63 | 64 | comb1 = add_strategies(['BB.SHFE-1.Minute'], [ 65 | { 66 | 'strategy': DemoStrategy('A1'), 67 | 'capital': 10000000.0 * 0.5, 68 | }, 69 | { 70 | 'strategy': DemoStrategy('A2'), 71 | 'capital': 10000000.0 * 0.5, 72 | } 73 | ]) 74 | # 打印组合1的统计信息 75 | curve1 = finance.create_equity_curve(Profile.all_holdings_sum(comb1)) 76 | six.print_('组合A', finance.summary_stats(curve1, 252*4*60)) 77 | 78 | comb2 = add_strategies(['BB.SHFE-1.Minute'], [ 79 | { 80 | 'strategy': DemoStrategy('B1'), 81 | 'capital': 20000000 * 0.4, 82 | }, 83 | { 84 | 'strategy': DemoStrategy('B2'), 85 | 'capital': 20000000 * 0.6, 86 | } 87 | ]) 88 | # 打印组合2的统计信息 89 | curve2 = finance.create_equity_curve(Profile.all_holdings_sum(comb2)) 90 | six.print_('组合B', finance.summary_stats(curve2, 252*4*60)) 91 | -------------------------------------------------------------------------------- /tests/datasource/test_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pandas as pd 4 | import shutil 5 | import unittest 6 | 7 | from quantdigger.infras.object import HashObject 8 | from quantdigger.datasource.cache import CachedDatasource 9 | from quantdigger.datasource.impl.localfs_cache import LocalFsCache 10 | from quantdigger.datasource.source import DatasourceAbstract, SourceWrapper 11 | from quantdigger.datastruct import PContract 12 | 13 | 14 | class _MockSource(DatasourceAbstract): 15 | def __init__(self): 16 | self.log = [] 17 | 18 | def get_bars(self, pcontract, dt_start, dt_end): 19 | self.log.append(HashObject.new(start=dt_start, end=dt_end)) 20 | dt_start = pd.to_datetime(dt_start) 21 | dt_end = pd.to_datetime(dt_end) 22 | dt_curr = dt_start 23 | arr = [] 24 | while dt_curr <= dt_end: 25 | arr.append({'datetime': dt_curr}) 26 | dt_curr += pcontract.period.to_timedelta() 27 | data = pd.DataFrame([{'datetime': pd.to_datetime('2010-1-1')}])\ 28 | .set_index('datetime') 29 | return SourceWrapper(pcontract, data, len(data)) 30 | 31 | 32 | def dt_eq(dt1, dt2): 33 | return pd.to_datetime(dt1) == pd.to_datetime(dt2) 34 | 35 | 36 | class TestCache(unittest.TestCase): 37 | 38 | CACHE_PATH = '_local_test_cache' 39 | 40 | def setUp(self): 41 | cache = LocalFsCache(TestCache.CACHE_PATH) 42 | self.src = _MockSource() 43 | self.ds = CachedDatasource(self.src, cache) 44 | self.pcontract = PContract.from_string('000001.SH-1.DAY') 45 | 46 | def tearDown(self): 47 | shutil.rmtree(TestCache.CACHE_PATH) 48 | 49 | def test1(self): 50 | self.ds.get_bars(self.pcontract, '2010-3-1', '2010-6-1') 51 | self.assertTrue(len(self.src.log) > 0, '没有访问数据源!') 52 | rec = self.src.log[-1] 53 | self.assertTrue(dt_eq(rec.start, '2010-3-1') and 54 | dt_eq(rec.end, '2010-6-1'), 55 | '访问的时间范围不正确!') 56 | self.src.log = [] 57 | 58 | self.ds.get_bars(self.pcontract, '2010-3-1', '2010-6-1') 59 | self.assertTrue(len(self.src.log) == 0, '访问缓存失败!') 60 | 61 | self.ds.get_bars(self.pcontract, '2010-2-15', '2010-6-1') 62 | self.assertTrue(len(self.src.log) > 0 and 63 | dt_eq(self.src.log[-1].start, '2010-2-15') and 64 | dt_eq(self.src.log[-1].end, '2010-2-28'), 65 | '访问数据源的时间范围不正确!') 66 | self.src.log = [] 67 | 68 | self.ds.get_bars(self.pcontract, '2010-3-1', '2010-7-1') 69 | self.assertTrue(len(self.src.log) > 0 and 70 | dt_eq(self.src.log[-1].start, '2010-6-2') and 71 | dt_eq(self.src.log[-1].end, '2010-7-1'), 72 | '访问数据源的时间范围不正确!') 73 | self.src.log = [] 74 | 75 | # TODO: 两边 76 | 77 | 78 | if __name__ == '__main__': 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /quantdigger/event/event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # from flufl.enum import Enum 3 | import json 4 | import six 5 | 6 | from quantdigger.util import py 7 | 8 | 9 | # @TODO REMOVE EventsPool 10 | class EventsPool(object): 11 | """ 事件池,每个策略有一个。""" 12 | _pool = [] 13 | 14 | def __init__(self, container=None): 15 | """ container决定是否是线程安全的。 16 | 17 | Args: 18 | container (list or Queue): 事件容器 19 | """ 20 | if container: 21 | self._pool = container 22 | 23 | def put(self, item): 24 | self._pool.append(item) 25 | 26 | def get(self): 27 | return self._pool.pop(0) 28 | 29 | 30 | class Event(object): 31 | """ 事件类型。 32 | 33 | :ivar MARKET: 市场数据事件, 目前未用到。 34 | :ivar SIGNAL: 交易信号事件, 由策略函数产生。 35 | :ivar ORDER: 委托下单事件, 由下单控制器产生。 36 | :ivar FILL: 订单成交事件, 由交易模拟器产生。 37 | """ 38 | MARKET = 'MARKET' 39 | SIGNAL = 'SIGNAL' 40 | ORDER = 'ORDER' 41 | FILL = 'FILL' 42 | ONCE = 'ONCE' 43 | TIMER = 'TIMER' 44 | 45 | def __init__(self, route, args): 46 | self.data = { 47 | 'route': route, 48 | 'data': args 49 | } 50 | 51 | def __str__(self): 52 | return "route: %s\nargs: %s" % (self.data['route'], self.data['data']) 53 | 54 | @property 55 | def route(self): 56 | return self.data['route'] 57 | 58 | @property 59 | def args(self): 60 | """""" 61 | return self.data['data'] 62 | 63 | @classmethod 64 | def message_to_event(self, message): 65 | if py == 3: 66 | message = message.decode('utf8') 67 | route, args = message.split('&') 68 | route = route[1:] 69 | return Event(route=route, args=json.loads(args)) 70 | 71 | @classmethod 72 | def event_to_message(self, event): 73 | # 消息头 + json字符串 74 | return '<%s&' % event.route + json.dumps(event.args) 75 | 76 | @classmethod 77 | def message_header(self, route): 78 | return b'<%s&' % six.b(route) 79 | 80 | 81 | class SignalEvent(Event): 82 | """ 由策略函数产生的交易信号事件。 """ 83 | 84 | def __init__(self, orders): 85 | super(SignalEvent, self).__init__(Event.SIGNAL, orders) 86 | 87 | @property 88 | def orders(self): 89 | return self.data['data'] 90 | 91 | 92 | class OrderEvent(Event): 93 | """ 委托下单事件。 """ 94 | 95 | def __init__(self, order): 96 | super(OrderEvent, self).__init__(Event.ORDER, order) 97 | 98 | @property 99 | def order(self): 100 | return self.data['data'] 101 | 102 | 103 | class OnceEvent(Event): 104 | 105 | def __init__(self): 106 | super(OnceEvent, self).__init__(Event.ONCE, None) 107 | 108 | 109 | class FillEvent(Event): 110 | """ 委托下单事件。 """ 111 | 112 | def __init__(self, transaction): 113 | super(FillEvent, self).__init__(Event.FILL, transaction) 114 | 115 | @property 116 | def transaction(self): 117 | return self.data['data'] 118 | -------------------------------------------------------------------------------- /quantdigger/interaction/backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from quantdigger.event.rpc import EventRPCServer 5 | from quantdigger.event.eventengine import ZMQEventEngine 6 | from quantdigger.util import mlogger as log 7 | from quantdigger.datasource.data import DataManager 8 | from quantdigger.datastruct import Contract, PContract 9 | from quantdigger.interaction.serialize import ( 10 | serialize_pcontract_bars, 11 | serialize_all_pcontracts, 12 | serialize_all_contracts, 13 | ) 14 | 15 | 16 | class Backend: 17 | SERVER_FOR_UI = 'backend4ui' 18 | SERVER_FOR_SHELL = "backend4shell" 19 | 20 | def __init__(self): 21 | log.info("Init Backend..") 22 | self._dm = DataManager() 23 | self._engine = ZMQEventEngine('Backend') 24 | self._engine.start() 25 | 26 | self._shell_srv = EventRPCServer(self._engine, 27 | self.SERVER_FOR_SHELL) 28 | self._ui_srv = EventRPCServer(self._engine, 29 | self.SERVER_FOR_UI) 30 | self.register_functions(self._shell_srv) 31 | self.register_functions(self._ui_srv) 32 | 33 | def register_functions(self, server): 34 | server.register('get_all_contracts', self.get_all_contracts) 35 | server.register('get_all_pcontracts', self.get_all_pcontracts) 36 | server.register('get_pcontract', self.get_pcontract) 37 | server.register('get_strategies', self.get_strategies) 38 | server.register('run_strategy', self.run_strategy) 39 | server.register('run_technical', self.run_technical) 40 | server.register('test_speed', self.test_speed) 41 | 42 | def stop(self): 43 | log.info('Backend stopped.') 44 | self._engine.stop() 45 | 46 | def get_all_contracts(self): 47 | def _mk_contract(code, exchage): 48 | s = '%s.%s' % (code, exchage) 49 | return Contract(s) 50 | # 模拟接口 51 | df = self._dm.get_contracts() 52 | contracts = [str(_mk_contract(row['code'], row['exchange'])) for _, row in df.iterrows()] 53 | return serialize_all_contracts(contracts) 54 | 55 | def get_all_pcontracts(self): 56 | # 模拟接口 57 | data = ['CC.SHFE-1.MINUTE', 'BB.SHFE-1.MINUTE'] 58 | pcontracts = [PContract.from_string(d) for d in data] 59 | return serialize_all_pcontracts(pcontracts) 60 | 61 | def get_pcontract(self, str_pcontract): 62 | da = self._dm.get_bars(str_pcontract) 63 | return serialize_pcontract_bars(str_pcontract, da.data) 64 | 65 | def run_strategy(self, name): 66 | """""" 67 | return 68 | 69 | def run_technical(self, name): 70 | return 71 | 72 | def get_technicals(self): 73 | """ 获取系统的所有指标。 """ 74 | from quantdigger.technicals import get_techs 75 | return get_techs() 76 | 77 | def get_strategies(self): 78 | return 'hello' 79 | 80 | def test_speed(self): 81 | return 'hello world!' 82 | 83 | 84 | if __name__ == '__main__': 85 | backend = Backend() 86 | import time 87 | import sys 88 | try: 89 | while True: 90 | time.sleep(1) 91 | except KeyboardInterrupt: 92 | backend.stop() 93 | sys.exit(0) 94 | -------------------------------------------------------------------------------- /demo/plot_strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from quantdigger import ( 4 | Strategy, 5 | MA, 6 | DateTimeSeries, 7 | NumberSeries, 8 | set_config, 9 | add_strategies, 10 | Profile 11 | ) 12 | 13 | 14 | class DemoStrategy(Strategy): 15 | """ 策略A1 """ 16 | 17 | def on_init(self, ctx): 18 | """初始化数据""" 19 | ctx.ma10 = MA(ctx.close, 10, 'ma10', 'y', 1) 20 | ctx.ma20 = MA(ctx.close, 20, 'ma20', 'b', 1) 21 | ctx.dt = DateTimeSeries() 22 | ctx.month_price = NumberSeries() 23 | 24 | def on_bar(self, ctx): 25 | ctx.dt.update(ctx.datetime) 26 | if ctx.dt[1].month != ctx.dt[0].month: 27 | ctx.month_price.update(ctx.close) 28 | if ctx.curbar > 20: 29 | if ctx.pos() == 0 and ctx.ma10[2] < ctx.ma20[2] and ctx.ma10[1] > ctx.ma20[1]: 30 | ctx.buy(ctx.close, 1) 31 | ctx.plot_text("buy", 1, ctx.curbar, ctx.close, "buy", 'black', 15) 32 | elif ctx.pos() > 0 and ctx.ma10[2] > ctx.ma20[2] and \ 33 | ctx.ma10[1] < ctx.ma20[1]: 34 | ctx.plot_text("sell", 1, ctx.curbar, ctx.close, "sell", 'blue', 15) 35 | ctx.sell(ctx.close, ctx.pos()) 36 | ctx.plot_line("month_price", 1, ctx.curbar, ctx.month_price, 'y--', lw=2) 37 | return 38 | 39 | def on_exit(self, ctx): 40 | return 41 | 42 | 43 | class DemoStrategy2(Strategy): 44 | """ 策略A2 """ 45 | 46 | def on_init(self, ctx): 47 | """初始化数据""" 48 | ctx.ma50 = MA(ctx.close, 50, 'ma50', 'y', 2) 49 | ctx.ma100 = MA(ctx.close, 100, 'ma100', 'black', 2) 50 | 51 | def on_symbol(self, ctx): 52 | pass 53 | 54 | def on_bar(self, ctx): 55 | if ctx.curbar > 100: 56 | if ctx.pos() == 0 and ctx.ma50[2] < ctx.ma100[2] and ctx.ma50[1] > ctx.ma100[1]: 57 | ctx.buy(ctx.close, 1) 58 | elif ctx.pos() > 0 and ctx.ma50[2] > ctx.ma100[2] and \ 59 | ctx.ma50[1] < ctx.ma100[1]: 60 | ctx.sell(ctx.close, ctx.pos()) 61 | 62 | return 63 | 64 | def on_exit(self, ctx): 65 | return 66 | 67 | 68 | if __name__ == '__main__': 69 | import timeit 70 | start = timeit.default_timer() 71 | set_config({'source': 'csv'}) 72 | profiles = add_strategies(['BB.SHFE-1.Day'], [ 73 | { 74 | 'strategy': DemoStrategy('A1'), 75 | 'capital': 50000.0 * 0.5, 76 | }, 77 | { 78 | 'strategy': DemoStrategy2('A2'), 79 | 'capital': 50000.0 * 0.5, 80 | } 81 | ]) 82 | stop = timeit.default_timer() 83 | print("运行耗时: %d秒" % ((stop - start))) 84 | 85 | # 绘制k线,交易信号线 86 | from quantdigger.digger import finance, plotting 87 | s = 0 88 | # 绘制策略A1, 策略A2, 组合的资金曲线 89 | curve0 = finance.create_equity_curve(profiles[0].all_holdings()) 90 | curve1 = finance.create_equity_curve(profiles[1].all_holdings()) 91 | curve = finance.create_equity_curve(Profile.all_holdings_sum(profiles)) 92 | plotting.plot_strategy(profiles[0].data(), profiles[0].technicals(), 93 | profiles[0].deals(), curve0.equity.values, 94 | profiles[0].marks()) 95 | # 绘制净值曲线 96 | plotting.plot_curves([curve.networth]) 97 | # 打印统计信息 98 | print(finance.summary_stats(curve, 252)) 99 | -------------------------------------------------------------------------------- /demo/tools/import_contracts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | import pandas as pd 4 | from quantdigger import ConfigUtil 5 | from quantdigger.datasource import ds_impl 6 | 7 | 8 | def import_contracts(decode=False): 9 | """ 从文件导入合约到数据库 """ 10 | df = pd.read_csv('./contracts.txt') 11 | contracts = [] 12 | codes = set() 13 | for idx, row in df.iterrows(): 14 | data = None 15 | if row['isStock']: 16 | if row['exchangeId'] == 'SZSE': 17 | row['exchangeId'] = 'SZ' 18 | else: 19 | row['exchangeId'] = 'SH' 20 | if decode: 21 | data = (row['code']+'.'+row['exchangeId'], 22 | row['code'], 23 | row['exchangeId'], 24 | row['name'].decode('utf8'), row['pinyin'], 1, 1, 0, 1) 25 | else: 26 | data = (row['code']+'.'+row['exchangeId'], 27 | row['code'], 28 | row['exchangeId'], 29 | row['name'], row['pinyin'], 1, 1, 0, 1) 30 | contracts.append(data) 31 | else: 32 | data = (row['code']+'.'+row['exchangeId'], 33 | row['code'], 34 | row['exchangeId'], 35 | row['code'], row['code'], row['long_margin_ratio'], 36 | row['short_margin_ratio'], 37 | row['price_tick'], 38 | row['volume_multiple']) 39 | contracts.append(data) 40 | # 修正ctp部分合约只有3位日期。 41 | if not row['code'][-4].isdigit(): 42 | row['code'] = row['code'][0:-3] + '1' + row['code'][-3:] 43 | # 支持两种日期 44 | data = (row['code']+'.'+row['exchangeId'], 45 | row['code'], 46 | row['exchangeId'], 47 | row['code'], row['code'], row['long_margin_ratio'], 48 | row['short_margin_ratio'], 49 | row['price_tick'], 50 | row['volume_multiple']) 51 | contracts.append(data) 52 | # 无日期指定的期货合约 53 | code = row['code'][0:-4] 54 | if code not in codes: 55 | t = (code+'.'+row['exchangeId'], code, row['exchangeId'], 56 | code, code, row['long_margin_ratio'], 57 | row['short_margin_ratio'], row['price_tick'], 58 | row['volume_multiple']) 59 | contracts.append(t) 60 | codes.add(code) 61 | contracts = zip(*contracts) 62 | rst = { 63 | 'key': contracts[0], 64 | 'code': contracts[1], 65 | 'exchange': contracts[2], 66 | 'name': contracts[3], 67 | 'spell': contracts[4], 68 | 'long_margin_ratio': contracts[5], 69 | 'short_margin_ratio': contracts[6], 70 | 'price_tick': contracts[7], 71 | 'volume_multiple': contracts[8], 72 | } 73 | return rst 74 | 75 | six.print_("import contracts..") 76 | contracts = import_contracts() 77 | csv_ds = ds_impl.csv_source.CsvSource('../data') 78 | csv_ds.import_contracts(contracts) 79 | 80 | contracts = import_contracts(True) 81 | sqlite_ds = ds_impl.sqlite_source.SqliteSource('../data/digger.db') 82 | sqlite_ds.import_contracts(contracts) 83 | -------------------------------------------------------------------------------- /demo/tools/sql.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | import os 4 | import sqlite3 5 | import time 6 | import pandas as pd 7 | from quantdigger.datasource import datautil 8 | 9 | import timeit 10 | import datetime 11 | 12 | db = sqlite3.connect('digger.db', 13 | detect_types = sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES 14 | ) 15 | 16 | def convert_datetime(tf): 17 | # 取 18 | return datetime.datetime.fromtimestamp(float(tf)/1000) 19 | 20 | sqlite3.register_converter('timestamp', convert_datetime) 21 | #db = sqlite3.connect(':memory:', 22 | c = db.cursor() 23 | 24 | 25 | def read_csv(path): 26 | """ 导入路径path下所有csv数据文件到sqlite中,每个文件对应数据库中的一周表格。 27 | 28 | DateFrame(index, open, close, low, high, vol) 29 | 30 | >>> csv2sqlite(os.getcwd()) 31 | """ 32 | def df2sqlite(df, tbname): 33 | sql = '''CREATE TABLE {tb} 34 | (id int primary key, 35 | datetime timestamp, 36 | open real, 37 | close real, 38 | high real, 39 | low real, 40 | volume int)'''.format(tb=tbname) 41 | c.execute(sql) 42 | data = [] 43 | for index, row in df.iterrows(): 44 | id, utime = datautil.encode2id('1.Minute', index) 45 | data.append((id, utime, row['open'], row['close'], row['high'], row['low'], row['volume'])) 46 | sql = "INSERT INTO %s VALUES (?,?,?,?,?,?,?)" % (tbname) 47 | c.executemany(sql, data) 48 | db.commit() 49 | 50 | for path, dirs, files in os.walk(path): 51 | for file in files: 52 | filepath = path + os.sep + file 53 | if filepath.endswith(".csv"): 54 | fname = file.split('-')[0] 55 | six.print_("import: ", fname) 56 | #df = pd.read_csv(filepath, parse_dates={'datetime': ['date', 'time']}, 57 | df = pd.read_csv(filepath, parse_dates='datetime', 58 | index_col='datetime') 59 | fname = fname.replace('.', '_') 60 | df2sqlite(df, fname) 61 | 62 | 63 | 64 | def get_tables(c): 65 | """ 返回数据库所有的表格""" 66 | c.execute("select name from sqlite_master where type='table'") 67 | return c.fetchall() 68 | 69 | def table_field(c, tb): 70 | """ 返回表格的字段""" 71 | c.execute("select * from %s LIMIT 1" % tb) 72 | field_names = [r[0] for r in c.description] 73 | return field_names 74 | 75 | def sql2csv(db, cursor): 76 | """ 77 | 导出sqlite中的所有表格数据。 78 | """ 79 | cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") 80 | tables = get_tables(cursor) 81 | for table_name in tables: 82 | table_name = table_name[0] 83 | table = pd.read_sql_query("SELECT * from %s" % table_name, db) 84 | table.to_csv(table_name + '.txt', index_label='index') 85 | 86 | 87 | start = timeit.default_timer() 88 | read_csv(os.getcwd()) 89 | stop = timeit.default_timer() 90 | six.print_((stop - start ) * 1000) 91 | six.print_("---------") 92 | db.commit() 93 | 94 | start = timeit.default_timer() 95 | open = close = high = low = [] 96 | for row in c.execute('SELECT id, datetime, open FROM AA_SHFE'): 97 | six.print_(row) 98 | six.print_(get_tables(c)) 99 | 100 | stop = timeit.default_timer() 101 | six.print_((stop - start ) * 1000) 102 | 103 | get_tables(c) 104 | db.commit() 105 | db.close() 106 | -------------------------------------------------------------------------------- /demo/mongotest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @file plot_strategy.py 4 | # @brief 策略运行和图表展示 5 | # @author wondereamer 6 | # @version 0.2 7 | # @date 2015-12-09 8 | 9 | import six 10 | #from quantdigger.engine.series import NumberSeries 11 | #from quantdigger.indicators.common import MA 12 | #from quantdigger.util import pcontract 13 | from quantdigger import * 14 | 15 | boll = { 16 | 'upper': [], 17 | 'middler': [], 18 | 'lower': [] 19 | } 20 | 21 | class DemoStrategy(Strategy): 22 | """ 策略A1 """ 23 | 24 | def on_init(self, ctx): 25 | """初始化数据""" 26 | ctx.ma100 = MA(ctx.close, 100, 'ma100', 'y', 2) #, 'ma200', 'b', '1') 27 | ctx.ma200 = MA(ctx.close, 200, 'ma200', 'b', 2) #, 'ma200', 'b', '1') 28 | ctx.boll = BOLL(ctx.close, 20) 29 | ctx.ma2 = NumberSeries() 30 | 31 | #def on_symbol(self, ctx): 32 | #pass 33 | 34 | def on_bar(self, ctx): 35 | if ctx.curbar > 1: 36 | ctx.ma2.update((ctx.close + ctx.close[1])/2) 37 | if ctx.curbar > 200: 38 | if ctx.pos() == 0 and ctx.ma100[2] < ctx.ma200[2] and ctx.ma100[1] > ctx.ma200[1]: 39 | ctx.buy(ctx.close, 1) 40 | elif ctx.pos() > 0 and ctx.ma100[2] > ctx.ma200[2] and \ 41 | ctx.ma100[1] < ctx.ma200[1]: 42 | ctx.sell(ctx.close, ctx.pos()) 43 | 44 | boll['upper'].append(ctx.boll['upper'][0]) 45 | boll['middler'].append(ctx.boll['middler'][0]) 46 | boll['lower'].append(ctx.boll['lower'][0]) 47 | return 48 | 49 | def on_exit(self, ctx): 50 | return 51 | 52 | 53 | class DemoStrategy2(Strategy): 54 | """ 策略A2 """ 55 | 56 | def on_init(self, ctx): 57 | """初始化数据""" 58 | ctx.ma50 = MA(ctx.close, 50, 'ma50', 'y', 2) #, 'ma200', 'b', '1') 59 | ctx.ma100 = MA(ctx.close, 100, 'ma100', 'black', 2) #, 'ma200', 'b', '1') 60 | 61 | def on_symbol(self, ctx): 62 | pass 63 | 64 | def on_bar(self, ctx): 65 | if ctx.curbar > 100: 66 | if ctx.pos() == 0 and ctx.ma50[2] < ctx.ma100[2] and ctx.ma50[1] > ctx.ma100[1]: 67 | ctx.buy(ctx.close, 1) 68 | elif ctx.pos() > 0 and ctx.ma50[2] > ctx.ma100[2] and \ 69 | ctx.ma50[1] < ctx.ma100[1]: 70 | ctx.sell(ctx.close, ctx.pos()) 71 | return 72 | 73 | def on_exit(self, ctx): 74 | return 75 | 76 | if __name__ == '__main__': 77 | import timeit 78 | ConfigUtil.set({ 79 | 'source': 'mongodb', 80 | 'dbname': 'quantdigger' 81 | }) 82 | start = timeit.default_timer() 83 | set_symbols(['BB.SHFE-1.Minute']) 84 | #set_symbols(['BB.SHFE']) 85 | #set_symbols(['BB']) 86 | profile = add_strategy([DemoStrategy('A1'), DemoStrategy2('A2')], 87 | { 'capital': 50000.0, 'ratio': [0.5, 0.5] }) 88 | 89 | run() 90 | stop = timeit.default_timer() 91 | six.print_("运行耗时: %d秒" % ((stop - start ))) 92 | 93 | # 绘制k线,交易信号线 94 | from quantdigger.digger import finance, plotting 95 | s = 0 96 | # 绘制策略A1, 策略A2, 组合的资金曲线 97 | curve0 = finance.create_equity_curve(profile.all_holdings(0)) 98 | curve1 = finance.create_equity_curve(profile.all_holdings(1)) 99 | curve = finance.create_equity_curve(profile.all_holdings()) 100 | plotting.plot_strategy(profile.data(0), profile.technicals(0), 101 | profile.deals(0), curve.equity) 102 | plotting.plot_curves([curve0.equity, curve1.equity, curve.equity], 103 | colors=['r', 'g', 'b'], 104 | names=[profile.name(0), profile.name(1), 'A0']) 105 | # 绘制净值曲线 106 | plotting.plot_curves([curve.networth]) 107 | # 打印统计信息 108 | six.print_(finance.summary_stats(curve, 252*4*60)) -------------------------------------------------------------------------------- /quantdigger/engine/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from quantdigger.event import OrderEvent 3 | from abc import ABCMeta, abstractmethod 4 | 5 | 6 | class Trader(object): 7 | __metaclass__ = ABCMeta 8 | """ 交易类,包含了回调注册等高级封装。 """ 9 | 10 | def __init__(self, arg): 11 | pass 12 | 13 | @abstractmethod 14 | def connect(self): 15 | """ 连接器 """ 16 | pass 17 | 18 | @abstractmethod 19 | def query_contract(self, contract, sync=False): 20 | """ 合约查询 """ 21 | pass 22 | 23 | @abstractmethod 24 | def query_tick(self, contract, sync=False): 25 | """ 深度行情数据 """ 26 | pass 27 | 28 | @abstractmethod 29 | def query_captital(self, sync=False): 30 | """ 查询资金账户 """ 31 | pass 32 | 33 | @abstractmethod 34 | def query_position(self, sync=False): 35 | """ 查询投资者持仓""" 36 | pass 37 | 38 | @abstractmethod 39 | def order(self, order, sync=False): 40 | """ 下单请求 41 | 42 | :param Order order: 委托订单。 43 | """ 44 | pass 45 | 46 | @abstractmethod 47 | def cancel_order(self, orderid, sync=False): 48 | """ 撤单操作请求 """ 49 | pass 50 | 51 | def on_transaction(self, trans): 52 | """ 委托成交回调 """ 53 | pass 54 | 55 | def on_tick(self, tick): 56 | """ tick数据回调 """ 57 | pass 58 | 59 | def on_capital(self, tick): 60 | """ 资金查询回调 """ 61 | pass 62 | 63 | def on_position(self, tick): 64 | """ 持仓查询回调 """ 65 | pass 66 | 67 | 68 | class CtpTraderAPI(object): 69 | """ Ctp交易类 """ 70 | def __init__(self): 71 | pass 72 | 73 | def connect(self): 74 | """ 连接""" 75 | pass 76 | 77 | def query_contract(self, contract): 78 | """ 合约查询 """ 79 | pass 80 | 81 | def query_tick(self, contract): 82 | """ 深度行情数据 """ 83 | pass 84 | 85 | def query_captital(self): 86 | """ 查询资金账户 """ 87 | pass 88 | 89 | def query_position(self): 90 | """ 查询投资者持仓""" 91 | pass 92 | 93 | def order(self, order): 94 | """ 下单请求 """ 95 | pass 96 | 97 | def cancel_order(self, orderid): 98 | """ 撤单操作请求 """ 99 | pass 100 | 101 | def on_transaction(self, trans): 102 | """ 委托成交回调 """ 103 | pass 104 | 105 | def on_tick(self, tick): 106 | """ tick数据回调 """ 107 | pass 108 | 109 | def on_capital(self, tick): 110 | """ 资金查询回调 """ 111 | pass 112 | 113 | def on_position(self, tick): 114 | """ 持仓查询回调 """ 115 | pass 116 | 117 | 118 | class SimulateTraderAPI(Trader): 119 | """ 模拟交易下单接口 """ 120 | def __init__(self, blotter, events_pool): 121 | self._blotter = blotter 122 | self._events = events_pool 123 | 124 | def connect(self): 125 | """ 连接""" 126 | pass 127 | 128 | def query_contract(self, contract): 129 | """ 合约查询 """ 130 | pass 131 | 132 | def query_tick(self, contract): 133 | """ 深度行情数据 """ 134 | pass 135 | 136 | def query_captital(self): 137 | """ 查询资金账户 """ 138 | pass 139 | 140 | def query_position(self): 141 | """ 查询投资者持仓""" 142 | pass 143 | 144 | def order(self, order): 145 | """ 模拟下单 """ 146 | self._events.put(OrderEvent(order)) 147 | 148 | def cancel_order(self, orderid): 149 | """ 撤单操作请求 """ 150 | pass 151 | 152 | def on_transaction(self, trans): 153 | """ 模拟委托成交回调 """ 154 | self._blotter.update_fill(trans) 155 | 156 | def on_tick(self, tick): 157 | """ tick数据回调 """ 158 | pass 159 | 160 | def on_capital(self, tick): 161 | """ 资金查询回调 """ 162 | pass 163 | 164 | def on_position(self, tick): 165 | """ 持仓查询回调 """ 166 | pass 167 | -------------------------------------------------------------------------------- /tests/test_eventengine.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | ## 3 | # @file eventenvine.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.1 7 | # @date 2016-05-25 8 | 9 | import six 10 | import unittest 11 | from quantdigger.event.event import Event 12 | from quantdigger.event.rpc import EventRPCClient, EventRPCServer 13 | from quantdigger.event.eventengine import QueueEventEngine, ZMQEventEngine, Timer 14 | 15 | 16 | def test_zmq_eventengine(): 17 | """测试函数""" 18 | import sys 19 | from datetime import datetime 20 | import time 21 | 22 | def simpletest(event): 23 | six.print_(str(datetime.now()), event.route) 24 | 25 | six.print_('test_zmq_eventengine.....' ) 26 | ee = ZMQEventEngine('test_zmq_eventengine') 27 | ee.register(Event.TIMER, simpletest) 28 | timer = Timer(ee) 29 | ee.start() 30 | timer.start() 31 | event = Event(route=Event.TIMER) 32 | ee.emit(event) 33 | ## @TODO test_unregister 34 | 35 | #time.sleep(2) 36 | #ee.stop_timer() 37 | #time.sleep(2) 38 | #ee.resume_timer() 39 | try: 40 | while True: 41 | time.sleep(1) 42 | except KeyboardInterrupt: 43 | timer.stop() 44 | ee.stop() 45 | sys.exit(0) 46 | 47 | 48 | def test_eventengine(): 49 | """测试函数""" 50 | import sys 51 | from datetime import datetime 52 | import time 53 | 54 | def simpletest(event): 55 | six.print_(str(datetime.now()), event.route) 56 | 57 | ee = QueueEventEngine() 58 | timer = Timer(ee) 59 | ee.register(Event.TIMER, simpletest) 60 | ee.start() 61 | timer.start() 62 | 63 | time.sleep(2) 64 | timer.stop() 65 | time.sleep(2) 66 | timer.resume() 67 | try: 68 | while True: 69 | time.sleep(1) 70 | except KeyboardInterrupt: 71 | ee.stop() 72 | timer.stop() 73 | sys.exit(0) 74 | 75 | 76 | def test_rpc(): 77 | import time 78 | import sys 79 | """""" 80 | def server_print_hello(args): 81 | time.sleep(4) # 4秒处理时间 82 | six.print_("server_print_hello") 83 | six.print_("args: ", args) 84 | return 'data_sever_print_hello' 85 | 86 | def client_print_hello(args): 87 | six.print_("client_print_hello") 88 | six.print_("args: ", args) 89 | 90 | # ------------------ 91 | def test_call(): 92 | ee = QueueEventEngine() 93 | client = EventRPCClient(ee, 'test') 94 | server = EventRPCServer(ee, 'test') 95 | server.register("server_print_hello", server_print_hello) 96 | ee.start() 97 | client.call("server_print_hello", { 'msg': 'parral_client'}, client_print_hello) 98 | return ee 99 | 100 | def test_sync_call(timeout): 101 | ee = QueueEventEngine() 102 | client = EventRPCClient(ee, 'test') 103 | server = EventRPCServer(ee, 'test') 104 | server.register("server_print_hello", server_print_hello) 105 | ee.start() 106 | six.print_(client.sync_call("server_print_hello", { 'msg': 'sync_client'}, timeout), "**" ) 107 | ee.stop() 108 | return 109 | 110 | test_sync_call(1) 111 | six.print_("*****************" ) 112 | test_sync_call(10) 113 | six.print_("*****************" ) 114 | ee = test_call() 115 | six.print_("********************" ) 116 | 117 | 118 | try: 119 | while True: 120 | time.sleep(1) 121 | except KeyboardInterrupt: 122 | ee.stop() 123 | sys.exit(0) 124 | return 125 | 126 | 127 | # 直接运行脚本可以进行测试 128 | if __name__ == '__main__': 129 | test_zmq_eventengine() 130 | #test_eventengine() 131 | #test_rpc() 132 | 133 | 134 | #class TestEventEngine(unittest.TestCase): 135 | #def test_get_qichats(self): 136 | #ret = get_qichats() 137 | #six.print_(ret) 138 | #self.assertTrue(ret) 139 | 140 | 141 | 142 | #if __name__ == '__main__': 143 | #unittest.main() 144 | -------------------------------------------------------------------------------- /quantdigger/engine/context/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | import datetime 4 | 5 | from quantdigger.engine.series import SeriesBase 6 | from quantdigger.util import MAX_DATETIME 7 | from quantdigger.engine.series import DateTimeSeries 8 | 9 | from .data_context import DataRef 10 | from .plotter import PlotterDelegator 11 | from .trading import TradingDelegator 12 | 13 | 14 | class Context(PlotterDelegator, TradingDelegator): 15 | """ 上下文""" 16 | def __init__(self, data: "dict((s_pcon, DataFrame))", 17 | name, settings, strategy, max_window): 18 | TradingDelegator.__init__(self, name, settings) 19 | PlotterDelegator.__init__(self) 20 | 21 | self.dt_series = DateTimeSeries([MAX_DATETIME] * max_window, 22 | 'universal_time') 23 | self.aligned_dt = datetime.datetime(2100, 1, 1) 24 | self.aligned_bar_index = 0 25 | self.strategy_name = name 26 | self.on_bar = False 27 | self.strategy = strategy 28 | self.data_ref = DataRef(data) 29 | 30 | def process_trading_events(self, at_baropen): 31 | super().update_environment( 32 | self.aligned_dt, self.data_ref.ticks, self.data_ref.bars) 33 | super().process_trading_events(at_baropen) 34 | 35 | def update_datetime(self, next_dt): 36 | self.dt_series.curbar = self.aligned_bar_index 37 | self.aligned_dt = min(self.data_ref.original.next_datetime, 38 | self.aligned_dt) 39 | try: 40 | self.dt_series.data[self.aligned_bar_index] = self.aligned_dt 41 | except IndexError: 42 | self.dt_series.data.append(self.aligned_dt) 43 | 44 | def __getitem__(self, strpcon): 45 | """ 获取跨品种合约 """ 46 | return self.data_ref._all_pcontract_data[strpcon.upper()].original 47 | 48 | def __getattr__(self, name): 49 | original = self.data_ref.original 50 | derived = self.data_ref._pcontract_data.derived 51 | if hasattr(original, name): 52 | return getattr(original, name) 53 | elif hasattr(derived, name): 54 | return getattr(derived, name) 55 | 56 | def __setattr__(self, name, value): 57 | if name in [ 58 | 'dt_series', 'strategy', 59 | '_trading', 'on_bar', 'aligned_bar_index', 'aligned_dt', 60 | 'marks', 'blotter', 'exchange', '_new_orders', '_datetime', 61 | '_cancel_now', 'events_pool', 'data_ref', 62 | 'strategy_name' 63 | ]: 64 | super(Context, self).__setattr__(name, value) 65 | else: 66 | if isinstance(value, SeriesBase): 67 | value.reset_data([], self.data_ref.original.size) 68 | self.data_ref.add_item(name, value) 69 | 70 | @property 71 | def pcontract(self): 72 | """ 当前周期合约 """ 73 | return self.data_ref.original.pcontract 74 | 75 | @property 76 | def contract(self): 77 | """ 当前合约 """ 78 | return self.data_ref.original.pcontract.contract 79 | 80 | @property 81 | def symbol(self): 82 | """ 当前合约 """ 83 | return str(self.contract) 84 | 85 | @property 86 | def curbar(self): 87 | """ 当前是第几根k线, 从1开始 """ 88 | if self.on_bar: 89 | return self.aligned_bar_index + 1 90 | else: 91 | return self.data_ref.original.curbar 92 | 93 | @property 94 | def open(self): 95 | """ k线开盘价序列 """ 96 | return self.data_ref.original.open 97 | 98 | @property 99 | def close(self): 100 | """ k线收盘价序列 """ 101 | return self.data_ref.original.close 102 | 103 | @property 104 | def high(self): 105 | """ k线最高价序列 """ 106 | return self.data_ref.original.high 107 | 108 | @property 109 | def low(self): 110 | """ k线最低价序列 """ 111 | return self.data_ref.original.low 112 | 113 | @property 114 | def volume(self): 115 | """ k线成交量序列 """ 116 | return self.data_ref.original.volume 117 | 118 | @property 119 | def datetime(self): 120 | """ k线时间序列 """ 121 | if self.on_bar: 122 | return self.dt_series 123 | else: 124 | return self.data_ref.original.datetime 125 | 126 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/localfs_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pickle 4 | import pandas as pd 5 | from datetime import datetime, timedelta 6 | 7 | from quantdigger.datasource.cache import CacheAbstract, LoadCacheException 8 | from quantdigger.datasource.source import SourceWrapper 9 | 10 | 11 | def _merge_data(arr): 12 | return pd.concat(arr)\ 13 | .reset_index()\ 14 | .drop_duplicates('datetime', keep='last')\ 15 | .set_index('datetime').sort_index() 16 | 17 | 18 | def _missing_range(delta, dt_start, dt_end, cached_start, cached_end): 19 | result = [] 20 | if cached_start is not None: 21 | if dt_start is None or dt_start < cached_start: 22 | result.append((dt_start, cached_start - delta)) 23 | if dt_end > cached_end: 24 | result.append((cached_end + delta, dt_end)) 25 | return result 26 | 27 | 28 | def _filter_by_datetime_range(data, start, end): 29 | start = pd.to_datetime(start) 30 | end = pd.to_datetime(end) 31 | if start is None: 32 | if end is None: 33 | return data 34 | else: 35 | return data[data.index <= end] 36 | else: 37 | if end is None: 38 | return data[data.index >= start] 39 | else: 40 | return data[(data.index >= start) & (data.index <= end)] 41 | 42 | 43 | class LocalFsCache(CacheAbstract): 44 | '''本地文件系统缓存''' 45 | 46 | def __init__(self, base_path): 47 | self._base_path = base_path 48 | self._load_meta() 49 | 50 | def get_bars(self, pcontract, dt_start, dt_end): 51 | key = self._to_key(pcontract) 52 | dt_start = pd.to_datetime(dt_start) 53 | dt_end = pd.to_datetime(dt_end) 54 | try: 55 | cached_start, cached_end = self._meta[key] 56 | missing_range = _missing_range( 57 | pcontract.period.to_timedelta(), 58 | dt_start, dt_end, cached_start, cached_end) 59 | data = self._load_data_from_path(self._key_to_path(key)) 60 | if missing_range: 61 | raise LoadCacheException(missing_range, data) 62 | data = _filter_by_datetime_range(data, dt_start, dt_end) 63 | return SourceWrapper(pcontract, data, len(data)) 64 | except (KeyError, IOError): 65 | raise LoadCacheException([(dt_start, dt_end)]) 66 | 67 | def save_data(self, missing_data, pcontract): 68 | key = self._to_key(pcontract) 69 | path = self._key_to_path(key) 70 | data_arr = map(lambda t: t.data, missing_data) 71 | try: 72 | old_data = self._load_data_from_path(path) 73 | data_arr.insert(0, old_data) 74 | except IOError: 75 | pass 76 | data = _merge_data(data_arr) 77 | self._save_data_to_path(data, path) 78 | self._update_meta(key, map(lambda t: (t.start, t.end), missing_data)) 79 | self._save_meta() 80 | 81 | def _check_base_path(self): 82 | if not os.path.isdir(self._base_path): 83 | os.makedirs(self._base_path) 84 | 85 | def _load_data_from_path(self, path): 86 | return pd.read_csv(path, index_col=0, parse_dates=True) 87 | 88 | def _save_data_to_path(self, data, path): 89 | self._check_base_path() 90 | data.to_csv(path) 91 | 92 | def _meta_path(self): 93 | return os.path.join(self._base_path, '_meta') 94 | 95 | def _load_meta(self): 96 | try: 97 | with open(self._meta_path(), 'rb') as f: 98 | self._meta = pickle.load(f) 99 | except IOError: 100 | self._meta = {} 101 | 102 | def _save_meta(self): 103 | self._check_base_path() 104 | with open(self._meta_path(), 'wb') as f: 105 | pickle.dump(self._meta, f, protocol=2) 106 | 107 | def _update_meta(self, key, range_lst): 108 | starts, ends = map(lambda t: list(t), zip(*range_lst)) 109 | try: 110 | cached_start, cached_end = self._meta[key] 111 | starts.append(cached_start) 112 | ends.append(cached_end) 113 | except KeyError: 114 | pass 115 | new_start = None if any(map(lambda d: d is None, starts)) \ 116 | else min(starts) 117 | new_end = max(ends) 118 | self._meta[key] = new_start, new_end 119 | 120 | def _to_key(self, pcontract): 121 | return str(pcontract) 122 | 123 | def _key_to_path(self, key): 124 | path = os.path.join(self._base_path, key + '.csv') 125 | return path 126 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/csv_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import pandas as pd 5 | 6 | from quantdigger.datasource.dsutil import * 7 | from quantdigger.datasource.source import SourceWrapper, DatasourceAbstract 8 | from quantdigger.errors import FileDoesNotExist 9 | 10 | 11 | @register_datasource('csv', 'data_path') 12 | class CsvSource(DatasourceAbstract): 13 | '''CSV数据源''' 14 | 15 | def __init__(self, root): 16 | self._root = root 17 | 18 | def get_bars(self, pcontract, dt_start, dt_end): 19 | data = self._load_bars(pcontract) 20 | dt_start = pd.to_datetime(dt_start) 21 | dt_end = pd.to_datetime(dt_end) 22 | data = data[(dt_start <= data.index) & (data.index <= dt_end)] 23 | assert data.index.is_unique 24 | return data 25 | 26 | def get_last_bars(self, pcontract, n): 27 | data = self._load_bars(pcontract) 28 | data = data[-n:] 29 | assert data.index.is_unique 30 | return data 31 | 32 | def get_contracts(self): 33 | """ 获取所有合约的基本信息 34 | 35 | Returns: 36 | pd.DataFrame 37 | """ 38 | fname = os.path.join(self._root, "CONTRACTS.csv") 39 | df = pd.read_csv(fname) 40 | df.index = df['code'] + '.' + df['exchange'] 41 | df.index = map(lambda x: x.upper(), df.index) 42 | return df 43 | 44 | def _load_bars(self, pcontract): 45 | # TODO: 不要字符串转来转去的 46 | strpcon = str(pcontract).upper() 47 | contract, period = tuple(strpcon.split('-')) 48 | code, exch = tuple(contract.split('.')) 49 | period = period.replace('.', '') 50 | fname = os.path.join(self._root, period, exch, code + ".csv") 51 | try: 52 | data = pd.read_csv(fname, index_col=0, parse_dates=True) 53 | except IOError: 54 | raise FileDoesNotExist(file=fname) 55 | else: 56 | return data 57 | 58 | def import_bars(self, tbdata, pcontract): 59 | """ 导入交易数据 60 | 61 | Args: 62 | tbdata (dict): {'datetime', 'open', 'close', 63 | 'high', 'low', 'volume'} 64 | pcontract (PContract): 周期合约 65 | """ 66 | strpcon = str(pcontract).upper() 67 | contract, period = tuple(strpcon.split('-')) 68 | code, exch = tuple(contract.split('.')) 69 | period = period.replace('.', '') 70 | try: 71 | os.makedirs(os.path.join(self._root, period, exch)) 72 | except OSError: 73 | pass 74 | fname = os.path.join(self._root, period, exch, code+'.csv') 75 | df = pd.DataFrame(tbdata) 76 | df.to_csv(fname, columns=[ 77 | 'datetime', 'open', 'close', 'high', 'low', 'volume' 78 | ], index=False) 79 | 80 | def import_contracts(self, data): 81 | """ 导入合约的基本信息。 82 | 83 | Args: 84 | data (dict): {key, code, exchange, name, spell, 85 | long_margin_ratio, short_margin_ratio, price_tick, volume_multiple} 86 | 87 | """ 88 | fname = os.path.join(self._root, "CONTRACTS.csv") 89 | df = pd.DataFrame(data) 90 | df.to_csv(fname, columns=[ 91 | 'code', 'exchange', 'name', 'spell', 92 | 'long_margin_ratio', 'short_margin_ratio', 'price_tick', 93 | 'volume_multiple' 94 | ], index=False) 95 | 96 | def get_code2strpcon(self): 97 | symbols = {} # code -> string pcontracts, 所有周期 98 | period_exchange2strpcon = {} # exchange.period -> string pcontracts , 所有合约 99 | for parent, dirs, files in os.walk(self._root): 100 | if dirs == []: 101 | t = parent.split(os.sep) 102 | period, exch = t[-2], t[-1] 103 | for i, a in enumerate(period): 104 | if not a.isdigit(): 105 | sepi = i 106 | break 107 | count = period[0:sepi] 108 | unit = period[sepi:] 109 | period = '.'.join([count, unit]) 110 | strpcons = period_exchange2strpcon.setdefault( 111 | ''.join([exch, '-', period]), []) 112 | for file_ in files: 113 | if file_.endswith('csv'): 114 | code = file_.split('.')[0] 115 | t = symbols.setdefault(code, []) 116 | rst = ''.join([code, '.', exch, '-', period]) 117 | t.append(rst) 118 | strpcons.append(rst) 119 | return symbols, period_exchange2strpcon 120 | -------------------------------------------------------------------------------- /quantdigger/datasource/impl/sqlite_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sqlite3 4 | import pandas as pd 5 | 6 | from quantdigger.datasource import datautil 7 | from quantdigger.datasource.dsutil import * 8 | from quantdigger.datasource.source import SourceWrapper, DatasourceAbstract 9 | from quantdigger.util import int2time 10 | 11 | 12 | @register_datasource('sqlite', 'data_path') 13 | class SqliteSource(DatasourceAbstract): 14 | '''Sqlite数据源''' 15 | 16 | def __init__(self, path): 17 | self._db = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES) 18 | sqlite3.register_converter('timestamp', int2time) 19 | self._cursor = self._db.cursor() 20 | 21 | def get_bars(self, pcontract, dt_start, dt_end): 22 | id_start, u = datautil.encode2id(pcontract.period, dt_start) 23 | id_end, u = datautil.encode2id(pcontract.period, dt_end) 24 | table = '_'.join([pcontract.contract.exchange, 25 | pcontract.contract.code]) 26 | sql = "SELECT datetime, open, close, high, low, volume FROM {tb} \ 27 | WHERE {start}<=id AND id<={end}".format( 28 | tb=table, start=id_start, end=id_end) 29 | data = pd.read_sql_query(sql, self._db, index_col='datetime') 30 | return SourceWrapper(pcontract, data, len(data)) 31 | 32 | def get_last_bars(self, pcontract, n): 33 | raise NotImplementedError 34 | 35 | def get_contracts(self): 36 | """ 获取所有合约的基本信息 37 | 38 | Returns: 39 | pd.DataFrame. 40 | """ 41 | self._cursor.execute("select * from contract") 42 | data = self._cursor.fetchall() 43 | data = zip(*data) 44 | df = pd.DataFrame({ 45 | 'code': data[1], 46 | 'exchange': data[2], 47 | 'name': data[3], 48 | 'spell': data[4], 49 | 'long_margin_ratio': data[5], 50 | 'short_margin_ratio': data[6], 51 | 'price_tick': data[7], 52 | 'volume_multiple': data[8] 53 | }, index=data[0]) 54 | return df 55 | 56 | def import_bars(self, tbdata, pcontract): 57 | """ 导入交易数据 58 | 59 | Args: 60 | tbdata (dict): {'datetime', 'open', 'close', 61 | 'high', 'low', 'volume'} 62 | pcontract (PContract): 周期合约 63 | """ 64 | strpcon = str(pcontract).upper() 65 | data = [] 66 | ids, utimes = [], [] 67 | strdt = strpcon.split('-')[1].upper() 68 | tbname = strpcon.split('-')[0].split('.') 69 | tbname = "_".join([tbname[1], tbname[0]]) 70 | for dt in tbdata['datetime']: 71 | id, utime = datautil.encode2id(strdt, dt) 72 | ids.append(id) 73 | utimes.append(utime) 74 | data = zip(ids, utimes, tbdata['open'], 75 | tbdata['close'], tbdata['high'], 76 | tbdata['low'], tbdata['volume']) 77 | try: 78 | self._cursor.execute('''CREATE TABLE {tb} 79 | (id int primary key, 80 | datetime timestamp, 81 | open real, 82 | close real, 83 | high real, 84 | low real, 85 | volume int)'''.format(tb=tbname)) 86 | self._db.commit() 87 | except sqlite3.OperationalError: 88 | pass 89 | finally: 90 | sql = "INSERT INTO %s VALUES (?,?,?,?,?,?,?)" % tbname 91 | self._cursor.executemany(sql, data) 92 | self._db.commit() 93 | 94 | def import_contracts(self, data): 95 | """ 导入合约的基本信息。 96 | 97 | Args: 98 | data (dict): {key, code, exchange, name, spell, 99 | long_margin_ratio, short_margin_ratio, price_tick, volume_multiple} 100 | 101 | """ 102 | 103 | tbname = 'contract' 104 | data['key'] = map(lambda x: x.upper(), data['key']) 105 | data = zip(data['key'], data['code'], data['exchange'], data['name'], 106 | data['spell'], data['long_margin_ratio'], 107 | data['short_margin_ratio'], 108 | data['price_tick'], data['volume_multiple']) 109 | sql = '''CREATE TABLE {tb} 110 | (key text primary key, 111 | code text not null, 112 | exchange text not null, 113 | name text not null, 114 | spell text not null, 115 | long_margin_ratio real not null, 116 | short_margin_ratio real not null, 117 | price_tick real not null, 118 | volume_multiple real not null 119 | )'''.format(tb=tbname) 120 | self._cursor.execute(sql) 121 | sql = "INSERT INTO %s VALUES (?,?,?,?,?,?,?,?,?)" % (tbname) 122 | self._cursor.executemany(sql, data) 123 | self._db.commit() 124 | 125 | def get_code2strpcon(self): 126 | raise NotImplementedError 127 | -------------------------------------------------------------------------------- /quantdigger/widgets/mplotwidgets/mainwindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | import matplotlib 5 | matplotlib.use('TkAgg') 6 | import matplotlib.pyplot as plt 7 | from matplotlib.widgets import Button 8 | 9 | from quantdigger.util import gen_logger as log 10 | from quantdigger.interaction.windowgate import WindowGate 11 | from quantdigger.widgets.mplotwidgets import widgets 12 | from quantdigger.widgets.mplotwidgets.mplots import Candles 13 | 14 | import pandas as pd 15 | price_data = pd.read_csv('../demo/data/1DAY/SHFE/BB.csv', index_col=0, parse_dates=True) 16 | #price_data = pd.read_csv('../demo/data/IF000.csv', index_col=0, parse_dates=True) 17 | 18 | class SubWindowData(object): 19 | """ 子窗口的数据。""" 20 | def __init__(self): 21 | self.curpcontract = None 22 | self.technicals = { } 23 | self.strategies = { } 24 | 25 | 26 | class MainWindow(object): 27 | """ 主界面,负责界面的创建,ui信号和WindowGate函数的对接。 28 | WindowGate是界面和其它模块交互的入口。 29 | """ 30 | DEFAULT_PERIOD = 0 31 | def __init__(self): 32 | super(MainWindow, self).__init__() 33 | self._fig = plt.figure() 34 | self._gate = WindowGate(self) 35 | self._cur_contract_index = 0 36 | self._pcontracts_of_contract = {} # {[], []} 37 | self._subwindows = [] 38 | self._cur_period = 0 39 | 40 | self._create_toolbar() 41 | self._create_technical_window() 42 | self._connect_signal() 43 | 44 | pcons = self._gate.get_all_pcontracts() 45 | for pcon in pcons: 46 | d = self._pcontracts_of_contract.get(pcon.contract, []) 47 | d.append(pcon) 48 | self._pcontracts_of_contract[pcon.contract] = d 49 | 50 | def show_data(self, str_pcontract): 51 | """""" 52 | pcon, data = self._gate.get_pcontract(str_pcontract) 53 | self.candle_widget.plot_with_plotter('candles', data) 54 | self._frame.load_data(data) 55 | self.frame.draw_widgets() 56 | return 57 | 58 | def _create_toolbar(self): 59 | axprev = self._fig.add_axes([0.1, 0.92, 0.07, 0.075]) 60 | axnext = self._fig.add_axes([0.2, 0.92, 0.07, 0.075]) 61 | self.btn_next = Button(axnext, '1Day') 62 | self.btn_prev = Button(axprev, '1Min') 63 | 64 | def _create_technical_window(self): 65 | self.frame = widgets.TechnicalWidget(self._fig, price_data, height=0.85) 66 | axes = self.frame.init_layout(50, 4, 1) 67 | ax_volume = axes[1] 68 | # at most 5 ticks, pruning the upper and lower so they don't overlap 69 | # with other ticks 70 | ax_volume.yaxis.set_major_locator(widgets.MyLocator(5, prune='both')) 71 | 72 | # 添加k线和交易信号。 73 | subwidget1 = widgets.FrameWidget(axes[0], "subwidget1", 100, 50) 74 | candles = Candles(price_data, None, 'candles') 75 | subwidget1.add_plotter(candles, False) 76 | self.candle_widget = self.frame.add_widget(0, subwidget1, True) 77 | ## 添加指标 78 | #self.frame.add_technical(0, MA(price_data.close, 20, 'MA20', 'y', 2)) 79 | #self.frame.add_technical(0, MA(price_data.close, 30, 'MA30', 'b', 2)) 80 | #self.frame.add_technical(1, Volume(price_data.open, price_data.close, price_data.vol)) 81 | self.frame.draw_widgets() 82 | 83 | def _connect_signal(self): 84 | self.btn_next.on_clicked(self.on_next_contract) 85 | self.btn_prev.on_clicked(self.on_previous_contract) 86 | self._fig.canvas.mpl_connect('key_release_event', self.on_keyrelease) 87 | 88 | def on_keyrelease(self, event): 89 | log.debug(event.key) 90 | if event.key == u"super+up": 91 | self.on_previous_contract 92 | elif event.key == u"super+down": 93 | self.on_next_contract() 94 | 95 | def show(self): 96 | plt.show() 97 | 98 | def close(self): 99 | self._gate.stop() 100 | 101 | def on_next_contract(self, event): 102 | if self._cur_contract_index + 1 < len(self._pcontracts_of_contract.keys()): 103 | self._cur_contract_index += 1 104 | contract = list(self._pcontracts_of_contract.keys())[self._cur_contract_index] 105 | 106 | pcon = self._pcontracts_of_contract[contract][self._cur_period] 107 | pcon, data = self._gate.get_pcontract(str(pcon)) 108 | self.frame.load_data(data) 109 | self.candle_widget.plot_with_plotter('candles', data) 110 | self.frame.draw_widgets() 111 | six.print_("next" , str(pcon), "**" ) 112 | else: 113 | six.print_("stop_next" ) 114 | 115 | def on_previous_contract(self, event): 116 | if self._cur_contract_index - 1 >= 0: 117 | self._cur_contract_index -= 1 118 | contract = list(self._pcontracts_of_contract.keys())[self._cur_contract_index] 119 | 120 | pcon = self._pcontracts_of_contract[contract][self._cur_period] 121 | pcon, data = self._gate.get_pcontract(str(pcon)) 122 | self.frame.load_data(data) 123 | self.candle_widget.plot_with_plotter('candles', data) 124 | self.frame.draw_widgets() 125 | six.print_("prev" , str(pcon), "**" ) 126 | else: 127 | six.print_("stop_pre" ) 128 | 129 | 130 | 131 | mainwindow = MainWindow() 132 | 133 | if __name__ == '__main__': 134 | mainwindow.show() 135 | import time, sys 136 | try: 137 | while True: 138 | time.sleep(1) 139 | except KeyboardInterrupt: 140 | mainwindow.close() 141 | sys.exit(0) 142 | -------------------------------------------------------------------------------- /quantdigger/engine/exchange.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from quantdigger.datastruct import Transaction, PriceType, TradeSide, Direction 3 | from quantdigger.event import FillEvent 4 | from quantdigger.util import log 5 | 6 | 7 | class Exchange(object): 8 | """ 模拟交易所。 9 | 10 | :ivar events: 事件池。 11 | :ivar name: 策略名,用于代码跟踪。 12 | """ 13 | def __init__(self, name, events_pool, slippage=None, strict=True): 14 | self.events = events_pool 15 | self.name = name 16 | self._slippage = slippage 17 | self._open_orders = set() 18 | # strict 为False表示只关注信号源的可视化,而非实际成交情况。 19 | self._strict = strict 20 | self._datetime = None 21 | 22 | def make_market(self, bars, at_baropen): 23 | """ 价格撮合""" 24 | if len(self._open_orders) == 0: 25 | return 26 | fill_orders = set() 27 | for order in self._open_orders: 28 | if order.side == TradeSide.CANCEL: 29 | fill_orders.add(order) 30 | transact = Transaction(order) 31 | self.events.put(FillEvent(transact)) 32 | continue 33 | try: 34 | bar = bars[order.contract] 35 | except KeyError: 36 | log.error('所交易的合约[%s]数据不存在' % order.contract) 37 | continue 38 | transact = Transaction(order) 39 | if self._strict: 40 | if at_baropen: 41 | if order.price_type == PriceType.LMT: 42 | price = bar.open 43 | if (order.side == TradeSide.OPEN and \ 44 | (order.direction == Direction.LONG and order.price >= price or \ 45 | order.direction == Direction.SHORT and order.price <= price)) or \ 46 | (order.side == TradeSide.CLOSE and \ 47 | (order.direction == Direction.LONG and order.price <= price or \ 48 | order.direction == Direction.SHORT and order.price >= price)): 49 | transact.price = order.price 50 | transact.datetime = bar.datetime 51 | fill_orders.add(order) 52 | self.events.put(FillEvent(transact)) 53 | elif order.price_type == PriceType.MKT: 54 | transact.price = bar.open 55 | transact.datetime = bar.datetime 56 | # recompute commission when price changed 57 | transact.compute_commission() 58 | fill_orders.add(order) 59 | self.events.put(FillEvent(transact)) 60 | else: 61 | if order.price_type == PriceType.LMT: 62 | # 限价单以最高和最低价格为成交的判断条件. 63 | if (order.side == TradeSide.OPEN and \ 64 | (order.direction == Direction.LONG and order.price >= bar.low or \ 65 | order.direction == Direction.SHORT and order.price <= bar.high)) or \ 66 | (order.side == TradeSide.CLOSE and \ 67 | (order.direction == Direction.LONG and order.price <= bar.high or \ 68 | order.direction == Direction.SHORT and order.price >= bar.low)): 69 | transact.price = order.price 70 | # Bar的结束时间做为交易成交时间. 71 | transact.datetime = bar.datetime 72 | fill_orders.add(order) 73 | self.events.put(FillEvent(transact)) 74 | elif order.price_type == PriceType.MKT: 75 | # 市价单以最高或最低价格为成交价格. 76 | if order.side == TradeSide.OPEN: 77 | if order.direction == Direction.LONG: 78 | transact.price = bar.high 79 | else: 80 | transact.price = bar.low 81 | elif order.side == TradeSide.CLOSE: 82 | if order.direction == Direction.LONG: 83 | transact.price = bar.low 84 | else: 85 | transact.price = bar.high 86 | transact.datetime = bar.datetime 87 | # recompute commission when price changed 88 | transact.compute_commission() 89 | fill_orders.add(order) 90 | self.events.put(FillEvent(transact)) 91 | else: 92 | transact.datetime = bar.datetime 93 | fill_orders.add(order) 94 | # 95 | self.events.put(FillEvent(transact)) 96 | # end of for 97 | if fill_orders: 98 | self._open_orders -= fill_orders 99 | 100 | def insert_order(self, event): 101 | """ 102 | 模拟交易所收到订单。 103 | """ 104 | ## @TODO 105 | if event.order in self._open_orders: 106 | # 撤单处理, set不会自动覆盖。 107 | self._open_orders.remove(event.order) 108 | self._open_orders.add(event.order) 109 | 110 | def update_datetime(self, dt): 111 | if self._datetime: 112 | if self._datetime.date() != dt.date(): 113 | self._open_orders.clear() 114 | self._datetime = dt 115 | -------------------------------------------------------------------------------- /quantdigger/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class QError(Exception): 5 | msg = None 6 | 7 | def __init__(self, *args, **kwargs): 8 | self.args = args 9 | self.kwargs = kwargs 10 | self.message = str(self) 11 | 12 | def __str__(self): 13 | msg = self.msg.format(**self.kwargs) 14 | return msg 15 | 16 | __unicode__ = __str__ 17 | __repr__ = __str__ 18 | 19 | 20 | class InvalidRPCClientArguments(QError): 21 | msg = "RPC远程调用的参数类型必须是dict, 但传入的参数类型是{argtype}" 22 | 23 | 24 | class TradingError(QError): 25 | """ 26 | """ 27 | msg = "交易警告:{err}" 28 | 29 | 30 | class DataFormatError(QError): 31 | """ 32 | """ 33 | msg = "{type}--错误的数据格式!" 34 | 35 | 36 | class DataFieldError(QError): 37 | """ 38 | """ 39 | msg = "错误的数据字段: {error_fields}\n正确的字段为: {right_fields} " 40 | 41 | 42 | class FileDoesNotExist(QError): 43 | """ 44 | 当本地文件不存在的时候触发。 45 | """ 46 | msg = "不存在文件:{file}" 47 | 48 | 49 | class PeriodTypeError(QError): 50 | msg = "不存在该周期! -- {period}" 51 | 52 | 53 | class DataAlignError(QError): 54 | msg = "数据没有对齐!" 55 | 56 | 57 | class SeriesIndexError(QError): 58 | msg = "序列变量索引越界!" 59 | 60 | 61 | class BreakConstError(QError): 62 | msg = "不能对常量赋值!" 63 | 64 | 65 | class ArgumentError(QError): 66 | msg = "参数错误!" 67 | 68 | 69 | class WrongDataForTransform(QError): 70 | """ 71 | Raised whenever a rolling transform is called on an event that 72 | does not have the necessary properties. 73 | """ 74 | msg = "{transform} requires {fields}. Event cannot be processed." 75 | 76 | 77 | class UnsupportedSlippageModel(QError): 78 | """ 79 | Raised if a user script calls the override_slippage magic 80 | with a slipage object that isn't a VolumeShareSlippage or 81 | FixedSlipapge 82 | """ 83 | msg = """ 84 | You attempted to override slippage with an unsupported class. \ 85 | Please use VolumeShareSlippage or FixedSlippage. 86 | """.strip() 87 | 88 | 89 | class OverrideSlippagePostInit(QError): 90 | # Raised if a users script calls override_slippage magic 91 | # after the initialize method has returned. 92 | msg = """ 93 | You attempted to override slippage outside of `initialize`. \ 94 | You may only call override_slippage in your initialize method. 95 | """.strip() 96 | 97 | 98 | class RegisterTradingControlPostInit(QError): 99 | # Raised if a user's script register's a trading control after initialize 100 | # has been run. 101 | msg = """ 102 | You attempted to set a trading control outside of `initialize`. \ 103 | Trading controls may only be set in your initialize method. 104 | """.strip() 105 | 106 | 107 | class UnsupportedCommissionModel(QError): 108 | """ 109 | Raised if a user script calls the override_commission magic 110 | with a commission object that isn't a PerShare, PerTrade or 111 | PerDollar commission 112 | """ 113 | msg = """ 114 | You attempted to override commission with an unsupported class. \ 115 | Please use PerShare or PerTrade. 116 | """.strip() 117 | 118 | 119 | class OverrideCommissionPostInit(QError): 120 | """ 121 | Raised if a users script calls override_commission magic 122 | after the initialize method has returned. 123 | """ 124 | msg = """ 125 | You attempted to override commission outside of `initialize`. \ 126 | You may only call override_commission in your initialize method. 127 | """.strip() 128 | 129 | 130 | class TransactionWithNoVolume(QError): 131 | """ 132 | Raised if a transact call returns a transaction with zero volume. 133 | """ 134 | msg = """ 135 | Transaction {txn} has a volume of zero. 136 | """.strip() 137 | 138 | 139 | class TransactionWithWrongDirection(QError): 140 | """ 141 | Raised if a transact call returns a transaction with a direction that 142 | does not match the order. 143 | """ 144 | msg = """ 145 | Transaction {txn} not in same direction as corresponding order {order}. 146 | """.strip() 147 | 148 | 149 | class TransactionWithNoAmount(QError): 150 | """ 151 | Raised if a transact call returns a transaction with zero amount. 152 | """ 153 | msg = """ 154 | Transaction {txn} has an amount of zero. 155 | """.strip() 156 | 157 | 158 | class TransactionVolumeExceedsOrder(QError): 159 | """ 160 | Raised if a transact call returns a transaction with a volume greater than 161 | the corresponding order. 162 | """ 163 | msg = """ 164 | Transaction volume of {txn} exceeds the order volume of {order}. 165 | """.strip() 166 | 167 | 168 | class UnsupportedOrderParameters(QError): 169 | """ 170 | Raised if a set of mutually exclusive parameters are passed to an order 171 | call. 172 | """ 173 | msg = "{msg}" 174 | 175 | 176 | class BadOrderParameters(QError): 177 | """ 178 | Raised if any impossible parameters (nan, negative limit/stop) 179 | are passed to an order call. 180 | """ 181 | msg = "{msg}" 182 | 183 | 184 | class OrderDuringInitialize(QError): 185 | """ 186 | Raised if order is called during initialize() 187 | """ 188 | msg = "{msg}" 189 | 190 | 191 | class TradingControlViolation(QError): 192 | """ 193 | Raised if an order would violate a constraint set by a TradingControl. 194 | """ 195 | msg = """ 196 | Order for {amount} shares of {sid} violates trading constraint {constraint}. 197 | """.strip() 198 | 199 | 200 | class IncompatibleHistoryFrequency(QError): 201 | """ 202 | Raised when a frequency is given to history which is not supported. 203 | At least, not yet. 204 | """ 205 | msg = """ 206 | Requested history at frequency '{frequency}' cannot be created with data 207 | at frequency '{data_frequency}'. 208 | """.strip() 209 | -------------------------------------------------------------------------------- /quantdigger/digger/plotting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | from six.moves import range 4 | import matplotlib 5 | matplotlib.use('TkAgg') 6 | import matplotlib.pyplot as plt 7 | from matplotlib.ticker import Formatter 8 | from quantdigger.widgets.mplotwidgets import widgets, mplots 9 | from quantdigger.widgets.mplotwidgets.mplots import Candles 10 | from quantdigger.technicals.common import Line, LineWithX, Volume 11 | 12 | 13 | def xticks_to_display(data_length): 14 | # six.print_(r.index[0].weekday()) 15 | interval = data_length / 5 16 | v = 0 17 | xticks = [] 18 | for i in range(0, 6): 19 | xticks.append(v) 20 | v += interval 21 | return xticks 22 | 23 | 24 | def plot_strategy(price_data, technicals={}, deals=[], curve=[], marks=[]): 25 | """ 26 | 显示回测结果。 27 | """ 28 | six.print_("plotting..") 29 | fig = plt.figure() 30 | frame = widgets.TechnicalWidget(fig, price_data) 31 | axes = frame.init_layout( 32 | 50, # 窗口显示k线数量。 33 | 4, 1 # 两个1:1大小的窗口 34 | ) 35 | 36 | # 绘制第一个窗口 37 | # 添加k线 38 | subwidget1 = widgets.FrameWidget(axes[0], "subwidget1", 100, 50) 39 | candles = Candles(price_data, None, 'candles') 40 | subwidget1.add_plotter(candles, False) 41 | #subwidget1.plot(price_data) 42 | # 交易信号。 43 | if deals: 44 | signal = mplots.TradingSignalPos(price_data, deals, lw=2) 45 | subwidget1.add_plotter(signal, False) 46 | if len(curve) > 0: 47 | curve = Line(curve) 48 | subwidget1.add_plotter(curve, True) 49 | # 添加指标 50 | for name, indic in six.iteritems(technicals): 51 | subwidget1.add_plotter(indic, False) 52 | 53 | # 绘制第2个窗口 54 | subwidget2 = widgets.FrameWidget(axes[1], "subwidget2", 100, 50) 55 | volume_plotter = Volume(price_data.open, price_data.close, price_data.volume) 56 | subwidget2.add_plotter(volume_plotter, False) 57 | 58 | subwidgets = [subwidget1, subwidget2] 59 | 60 | ### 绘制标志 61 | if marks: 62 | if marks[0]: 63 | # plot lines 64 | for name, values in six.iteritems(marks[0]): 65 | v = values[0] 66 | ith_ax = v[0] 67 | twinx = v[1] 68 | line_pieces = [[v[2]], [v[3]], v[4], v[5], v[6]] 69 | line = [] 70 | for v in values[1: ]: 71 | ## @TODO 如果是带“点”的,以点的特征聚类,会减少绘图对象的数目 72 | x, y, style, lw, ms = v[2], v[3], v[4], v[5], v[6] 73 | if style != line_pieces[2] or lw != line_pieces[3] or ms != line_pieces[4]: 74 | line.append(line_pieces) 75 | line_pieces = [[x], [y], style, lw, ms] 76 | else: 77 | line_pieces[0].append(x) 78 | line_pieces[1].append(y) 79 | line.append(line_pieces) 80 | for v in line: 81 | ## @TODO 这里的sytle明确指出有点奇怪,不一致。 82 | x, y, style, lw, marksize = v[0], v[1], v[2], v[3], v[4] 83 | curve = LineWithX(x, y, style=style, lw=lw, ms=marksize) 84 | subwidgets[ith_ax].add_plotter(curve, twinx) 85 | if marks[1]: 86 | # plot texts 87 | for name, values in six.iteritems(marks[1]): 88 | for v in values: 89 | ith_ax, x, y, text = v[0], v[1], v[2], v[3] 90 | color, size, rotation = v[4], v[5], v[6] 91 | ## @TODO move to text plotter 92 | frame.plot_text(name, ith_ax, x, y, text, color, size, rotation) 93 | 94 | frame.add_widget(0, subwidget1, True) 95 | frame.add_widget(1, subwidget2, True) 96 | frame.draw_widgets() 97 | plt.show() 98 | 99 | 100 | def plot_curves(data, colors=[], lws =[], names=[]): 101 | """ 画资金曲线 102 | 103 | Args: 104 | data (list): [pd.Series] 105 | 106 | colors (list): [str] 107 | 108 | lws (list): [int.] 109 | """ 110 | assert(len(data) > 0) 111 | if colors: 112 | assert(len(data) == len(colors)) 113 | else: 114 | colors = ['b'] * len(data) 115 | if lws: 116 | assert(len(data) == len(lws)) 117 | else: 118 | lws = [1] * len(data) 119 | if names: 120 | assert(len(data) == len(names)) 121 | # 画资金曲线 122 | # six.print_(curve.equity) 123 | fig2 = plt.figure() 124 | lns = [] 125 | ax = fig2.add_axes((0.1, 0.1, 0.8, 0.8)) 126 | ax.xaxis.set_major_formatter(TimeFormatter(data[0].index, '%Y-%m-%d')) 127 | ax.get_yaxis().get_major_formatter().set_useOffset(False) 128 | # ax.get_yaxis().get_major_formatter().set_scientific(False) 129 | ax.set_xticks(xticks_to_display(len(data[0]))) 130 | lns = ax.plot(data[0], c=colors[0]) 131 | for tl in ax.get_yticklabels(): 132 | tl.set_color(colors[0]) 133 | if len(data) > 1: 134 | for i in range(1, len(data)): 135 | new_ax = ax.twinx() 136 | lns += new_ax.plot(data[i], c=colors[i]) 137 | # new_ax.set_ylabel('sin', color=colors[i]) 138 | for tl in new_ax.get_yticklabels(): 139 | tl.set_color(colors[i]) 140 | # new_ax.set_yticks 141 | # ax.legend(lns, ['aaa', 'bbbb', 'ccc']) 142 | if names: 143 | ax.legend(lns, names, loc='upper left').get_frame().set_alpha(0.5) 144 | plt.show() 145 | 146 | 147 | class TimeFormatter(Formatter): 148 | # def __init__(self, dates, fmt='%Y-%m-%d'): 149 | # 分类 --format 150 | def __init__(self, dates, fmt='%Y-%m-%d %H:%M'): 151 | self.dates = dates 152 | self.fmt = fmt 153 | 154 | def __call__(self, x, pos=0): 155 | 'Return the label for time x at position pos' 156 | ind = int(round(x)) 157 | if ind >= len(self.dates) or ind < 0: 158 | return '' 159 | return self.dates[ind].strftime(self.fmt) 160 | -------------------------------------------------------------------------------- /quantdigger/engine/profile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file profile.py 4 | # @brief 5 | # @author wondereamer 6 | # @version 0.4 7 | # @date 2016-12-18 8 | 9 | from six.moves import range 10 | import copy 11 | import pandas as pd 12 | 13 | from quantdigger.datastruct import ( 14 | OneDeal, 15 | PositionKey, 16 | TradeSide, 17 | ) 18 | 19 | 20 | class Profile(object): 21 | """ 组合结果 """ 22 | def __init__(self, marks, blotter, data_ref): 23 | """ 24 | """ 25 | self._marks = marks 26 | self._blotter = blotter 27 | self._data_ref = data_ref 28 | 29 | def name(self): 30 | return self._blotter.name 31 | 32 | def transactions(self) -> "[Transaction]": 33 | """ 策略的所有成交明细 34 | """ 35 | return self._blotter.transactions 36 | 37 | def deals(self): 38 | """ 策略的每笔交易(一开一平)。 39 | 40 | Returns: 41 | list. [OneDeal, ..] 42 | """ 43 | positions = {} 44 | deals = [] 45 | for trans in self.transactions(): 46 | self._update_positions(positions, deals, trans) 47 | return deals 48 | 49 | def all_holdings(self): 50 | """ 策略账号资金的历史。 51 | 52 | Returns: 53 | list. [{'cash', 'commission', 'equity', 'datetime'}, ..] 54 | """ 55 | return self._blotter.all_holdings 56 | 57 | @staticmethod 58 | def all_holdings_sum(profiles): 59 | """ 60 | Returns: 61 | list. [{'cash', 'commission', 'equity', 'datetime'}, ..] 62 | """ 63 | all_holdings = copy.deepcopy(profiles[0].all_holdings()) 64 | for i, hd in enumerate(all_holdings): 65 | for profile in profiles[1:]: 66 | try: 67 | rhd = profile.all_holdings()[i] 68 | except IndexError: 69 | rhd = rhd[-2] # 是否强平导致长度不一 70 | hd['cash'] += rhd['cash'] 71 | hd['commission'] += rhd['commission'] 72 | hd['equity'] += rhd['equity'] 73 | return all_holdings 74 | 75 | 76 | def holding(self): 77 | """ 当前账号情况 78 | Returns: 79 | dict. {'cash', 'commission', 'history_profit', 'equity' } 80 | """ 81 | return self._blotter.holding 82 | 83 | def marks(self): 84 | return self._marks 85 | 86 | def technicals(self, strpcon=None): 87 | if not strpcon: 88 | strpcon = self._data_ref.default_pcontract 89 | strpcon = strpcon.upper() 90 | return self._data_ref.get_technicals(strpcon) 91 | 92 | def data(self, strpcon=None): 93 | """ 周期合约数据, 只有向量运行才有意义。 94 | 95 | Args: 96 | strpcon (str): 周期合约,如'BB.SHFE-1.Minute' 97 | 98 | Returns: 99 | pd.DataFrame. 数据 100 | """ 101 | if not strpcon: 102 | strpcon = self._data_ref.default_pcontract 103 | strpcon = strpcon.upper() 104 | original = self._data_ref.get_data(strpcon).original 105 | df = pd.DataFrame({ 106 | 'open': original.open.data, 107 | 'close': original.close.data, 108 | 'high': original.high.data, 109 | 'low': original.low.data, 110 | 'volume': original.volume.data 111 | }, index=original.datetime.data) 112 | return df 113 | 114 | def _update_positions(self, current_positions, deal_positions, trans): 115 | """ 根据交易明细计算开平仓对。 """ 116 | class PositionsDetail(object): 117 | """ 当前相同合约持仓集合(可能不同时间段下单)。 118 | 119 | :ivar cost: 持仓成本。 120 | :ivar total: 持仓总数。 121 | :ivar positions: 持仓集合。 122 | :vartype positions: list 123 | """ 124 | def __init__(self): 125 | self.total = 0 126 | self.positions = [] 127 | self.cost = 0 128 | assert trans.quantity > 0 129 | poskey = PositionKey(trans.contract, trans.direction) 130 | p = current_positions.setdefault(poskey, PositionsDetail()) 131 | if trans.side == TradeSide.OPEN: 132 | # 开仓 133 | p.positions.append(trans) 134 | p.total += trans.quantity 135 | 136 | elif trans.side == TradeSide.CLOSE: 137 | # 平仓 138 | assert(len(p.positions) > 0 and '所平合约没有持仓') 139 | left_vol = trans.quantity 140 | last_index = 0 141 | search_index = 0 142 | p.total -= trans.quantity 143 | if trans.contract.is_stock: 144 | for position in reversed(p.positions): 145 | # 开仓日期小于平仓时间 146 | if position.datetime.date() < trans.datetime.date(): 147 | break 148 | search_index -= 1 149 | if search_index != 0: 150 | positions = p.positions[:search_index] 151 | left_positions = p.positions[search_index:] 152 | else: 153 | positions = p.positions 154 | for position in reversed(positions): 155 | if position.quantity < left_vol: 156 | # 还需从之前的仓位中平。 157 | left_vol -= position.quantity 158 | last_index -= 1 159 | deal_positions.append( 160 | OneDeal(position, trans, position.quantity)) 161 | elif position.quantity == left_vol: 162 | left_vol -= position.quantity 163 | last_index -= 1 164 | deal_positions.append( 165 | OneDeal(position, trans, position.quantity)) 166 | break 167 | else: 168 | position.quantity -= left_vol 169 | left_vol = 0 170 | deal_positions.append(OneDeal(position, trans, left_vol)) 171 | break 172 | if last_index != 0 and search_index != 0: 173 | p.positions = positions[0:last_index] + left_positions 174 | elif last_index != 0: 175 | p.positions = positions[0:last_index] 176 | # last_index == 0, 表示没找到可平的的开仓对,最后一根强平 177 | # 可以被catch捕获 AssertError 178 | assert(left_vol == 0 or last_index == 0) 179 | -------------------------------------------------------------------------------- /quantdigger/widgets/plotter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ## 3 | # @file plotting.py 4 | # @brief 统一绘图接口, 帮助指标类的绘图。 5 | # @author wondereamer 6 | # @version 0.15 7 | # @date 2015-06-13 8 | 9 | import six 10 | import inspect 11 | from matplotlib.axes import Axes 12 | import numpy as np 13 | 14 | def plot_init(method): 15 | """ 根据被修饰函数的参数构造属性。 16 | 并且触发绘图范围计算。 17 | """ 18 | def wrapper(self, *args, **kwargs): 19 | magic = inspect.getargspec(method) 20 | arg_names = magic.args[1:] 21 | # 默认参数 22 | default = dict( 23 | (x, y) for x, y in zip(magic.args[-len(magic.defaults):], 24 | magic.defaults)) 25 | # 调用参数 26 | method_args = {} 27 | for i, arg in enumerate(args): 28 | method_args[arg_names[i]] = arg 29 | method_args.update(kwargs) 30 | # 31 | default.update(method_args) 32 | # 属性创建 33 | for key, value in six.iteritems(default): 34 | setattr(self, key, value) 35 | # 运行构造函数 36 | rst = method(self, *args, **kwargs) 37 | self._init_bound() 38 | return rst 39 | return wrapper 40 | 41 | import bisect 42 | def sub_interval(start, end, array): 43 | """ 寻找满足区间[start, end]的array值 44 | 45 | Args: 46 | start (int): 区间左侧 47 | end (int): 区间右侧 48 | array (list): 有序数组 49 | 50 | >>> array = [0,1,3, 4, 5, 6, 8] 51 | >>> rst = sub_interval(2, 5, array) 52 | >>> six.print_(array[rst[0]: rst[1]]) 53 | """ 54 | i = bisect.bisect_left(array, start) 55 | if i != len(array): 56 | t_start = i 57 | else: 58 | raise ValueError 59 | i = bisect.bisect_right(array, end) 60 | if i: 61 | t_end = i 62 | else: 63 | raise ValueError 64 | return (t_start, t_end) 65 | 66 | 67 | class AxWidget(object): 68 | """ matplotlib绘图容器 """ 69 | def __init__(self, name): 70 | self.name = name 71 | 72 | def plot_line(self, widget, ydata, style, lw, ms): 73 | widget.plot(ydata, style, lw=lw, ms=ms, label=self.name) 74 | 75 | def plot_line_withx(self, widget, _xdata, ydata, style, lw, ms): 76 | widget.plot(_xdata, ydata, style, lw=lw, ms=ms, label=self.name) 77 | 78 | 79 | class QtWidget(object): 80 | """ pyqt绘图容器 """ 81 | def __init__(self, name): 82 | self.name = name 83 | 84 | def plot_line(self, widget, ydata, style, lw, ms): 85 | raise NotImplementedError 86 | 87 | def plot_line_withx(self, widget, _xdata, ydata, style, lw, ms): 88 | raise NotImplementedError 89 | 90 | 91 | class Plotter(object): 92 | """ 93 | 系统绘图基类。 94 | 95 | :ivar _upper: 坐标上界(绘图用) 96 | :vartype _upper: float 97 | :ivar lower: 坐标上界(绘图用) 98 | :vartype lower: float 99 | :ivar widget: 绘图容器,暂定Axes 100 | """ 101 | def __init__(self, name, widget): 102 | self.ax_widget = AxWidget(name) 103 | self.qt_widget = QtWidget(name) 104 | self.widget = widget 105 | self._upper = self._lower = None 106 | self._xdata = None 107 | 108 | def plot_line(self, *args, **kwargs): 109 | """ 画线 110 | 111 | Args: 112 | *args (tuple): [_xdata], ydata, style 113 | **kwargs (dict): lw, ms 114 | """ 115 | # 区分向量绘图和逐步绘图。 116 | lw = kwargs.get('lw', 1) 117 | ms = kwargs.get('ms', 10) 118 | if len(args[0]) > 0: 119 | if len(args) == 2: 120 | ydata = args[0] 121 | style = args[1] 122 | # 区分绘图容器。 123 | if isinstance(self.widget, Axes): 124 | self.ax_widget.plot_line(self.widget, ydata, style, lw, ms) 125 | else: 126 | self.qt_widget.plot_line(self.widget, ydata, style, lw, ms) 127 | elif len(args) == 3: 128 | _xdata = args[0] 129 | ydata = args[1] 130 | style = args[2] 131 | # 区分绘图容器。 132 | if isinstance(self.widget, Axes): 133 | self.ax_widget.plot_line_withx(self.widget, _xdata, ydata, style, lw, ms) 134 | else: 135 | self.qt_widget.plot_line_withx(self.widget, _xdata, ydata, style, lw, ms) 136 | 137 | def plot(self, widget): 138 | """ 如需绘制指标,则需重载此函数。 """ 139 | # @todo 把plot_line等绘图函数分离到widget类中。 140 | raise NotImplementedError 141 | 142 | def stick_yrange(self, y_range): 143 | """ 固定纵坐标范围。如RSI指标。 144 | 145 | :ivar y_range: 纵坐标范围。 146 | :vartype y_range: list 147 | """ 148 | self._lower = y_range 149 | self._upper = y_range 150 | 151 | def y_interval(self, w_left, w_right): 152 | """ 可视区域[w_left, w_right]移动时候重新计算纵坐标范围。 """ 153 | # @todo 只存储上下界, 每次缩放的时候计算一次, 在移动时候无需计算。 154 | if len(self._upper) == 2: 155 | # 就两个值,分别代表上下界。 156 | return max(self._upper), min(self._lower) 157 | try: 158 | if self._xdata: 159 | w_left, w_right = sub_interval(w_left, w_right, self._xdata) 160 | except ValueError: 161 | # 标志不在可视区间,确保不会被采纳。 162 | return -1000000, 1000000 163 | else: 164 | ymax = np.max(self._upper[w_left: w_right]) 165 | ymin = np.min(self._lower[w_left: w_right]) 166 | return ymax, ymin 167 | 168 | def _init_bound(self): 169 | # 绘图中的y轴范围未被设置,使用默认值。 170 | if not self._upper: 171 | self._upper = self._lower = [] 172 | if isinstance(self.values, dict): 173 | # 多值指标 174 | values = zip(*six.itervalues(self.values)) 175 | self._upper = [max(value) for value in values] 176 | self._lower = [min(value) for value in values] 177 | else: 178 | self._upper = self.values 179 | self._lower = self.values 180 | if self._xdata: 181 | # 用户使用plot_line接口的时候触发这里 182 | # @NOTE 重排,强制绘图点是按x有序的。 183 | temp = zip(self._xdata, self.values) 184 | sdata = sorted(temp, key=lambda x: x[0]) 185 | temp = zip(*sdata) 186 | l_temp = list(temp) 187 | self._xdata = l_temp[0] 188 | self.values = l_temp[1] 189 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | QuantDigger 0.6.0 2 | ================== 3 | 4 | QuantDigger是一个基于python的量化回测框架。它借鉴了主流商业软件(比如TB, 金字塔)简洁的策略语法,同时 5 | 避免了它们内置编程语言的局限性,使用通用语言python做为策略开发工具。和 zipline_ , pyalgotrade_ 相比, 6 | QuantDigger的策略语法更接近策略开发人员的习惯。目前的功能包括:股票回测,期货回测。 支持选股,套利,择时, 组合策略。自带了一个基于matplotlib编写的简单的策略和k线显示界面,能满足广大量化爱好者 基本的回测需求。设计上也兼顾了实盘交易,未来如果有时间,也会加入交易接口。 7 | 8 | 9 | **由于个人时间和工作的关系,本项目不再维护。** 10 | 11 | 12 | 13 | 文档 14 | ----- 15 | wiki文档_ 16 | 17 | 18 | 依赖库 19 | ------- 20 | * matplotlib 21 | * numpy 22 | * logbook 23 | * pandas 24 | * progressbar2 25 | * zmq 26 | * BeautifulSoup4 (tushare需要) 27 | * lxml (tushare需要) 28 | * tushare_ (一个非常强大的股票信息抓取工具) 29 | * python-dateutil(可选) 30 | * IPython 31 | * TA-Lib 32 | 33 | * 可以用pip安装依赖库: 34 | >>> pip install -r requirements/requirements.txt 35 | * 如果出现pypi源超时情况: 36 | >>> pip install -r requirements/requirements.txt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com 37 | 38 | * TA-Lib 通过pip直接安装可能会出错, 39 | * 到 http://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib 下载相应版本然后通过命令安装,如 40 | >>> pip install TA_Lib-0.4.10-cp36-cp36m-win_amd64.whl 41 | * Anaconda用户可以用 42 | >>> conda install -c quantopian ta-lib 43 | 44 | * finance依赖 45 | * 安装 https://github.com/matplotlib/mpl_finance 46 | 47 | 48 | 策略组合DEMO 49 | ----------- 50 | 51 | 源码 52 | ~~~~ 53 | 54 | .. code:: py 55 | 56 | 57 | from quantdigger import ( 58 | Strategy, 59 | MA, 60 | DateTimeSeries, 61 | NumberSeries, 62 | set_config, 63 | add_strategies, 64 | Profile 65 | ) 66 | 67 | 68 | class DemoStrategy(Strategy): 69 | """ 策略A1 """ 70 | 71 | def on_init(self, ctx): 72 | """初始化数据""" 73 | ctx.ma10 = MA(ctx.close, 10, 'ma10', 'y', 1) 74 | ctx.ma20 = MA(ctx.close, 20, 'ma20', 'b', 1) 75 | ctx.dt = DateTimeSeries() 76 | ctx.month_price = NumberSeries() 77 | 78 | def on_bar(self, ctx): 79 | ctx.dt.update(ctx.datetime) 80 | if ctx.dt[1].month != ctx.dt[0].month: 81 | ctx.month_price.update(ctx.close) 82 | if ctx.curbar > 20: 83 | if ctx.pos() == 0 and ctx.ma10[2] < ctx.ma20[2] and ctx.ma10[1] > ctx.ma20[1]: 84 | ctx.buy(ctx.close, 1) 85 | ctx.plot_text("buy", 1, ctx.curbar, ctx.close, "buy", 'black', 15) 86 | elif ctx.pos() > 0 and ctx.ma10[2] > ctx.ma20[2] and \ 87 | ctx.ma10[1] < ctx.ma20[1]: 88 | ctx.plot_text("sell", 1, ctx.curbar, ctx.close, "sell", 'blue', 15) 89 | ctx.sell(ctx.close, ctx.pos()) 90 | ctx.plot_line("month_price", 1, ctx.curbar, ctx.month_price, 'y--', lw=2) 91 | return 92 | 93 | def on_exit(self, ctx): 94 | return 95 | 96 | 97 | class DemoStrategy2(Strategy): 98 | """ 策略A2 """ 99 | 100 | def on_init(self, ctx): 101 | """初始化数据""" 102 | ctx.ma50 = MA(ctx.close, 50, 'ma50', 'y', 2) 103 | ctx.ma100 = MA(ctx.close, 100, 'ma100', 'black', 2) 104 | 105 | def on_symbol(self, ctx): 106 | pass 107 | 108 | def on_bar(self, ctx): 109 | if ctx.curbar > 100: 110 | if ctx.pos() == 0 and ctx.ma50[2] < ctx.ma100[2] and ctx.ma50[1] > ctx.ma100[1]: 111 | ctx.buy(ctx.close, 1) 112 | elif ctx.pos() > 0 and ctx.ma50[2] > ctx.ma100[2] and \ 113 | ctx.ma50[1] < ctx.ma100[1]: 114 | ctx.sell(ctx.close, ctx.pos()) 115 | 116 | return 117 | 118 | def on_exit(self, ctx): 119 | return 120 | 121 | 122 | if __name__ == '__main__': 123 | import timeit 124 | start = timeit.default_timer() 125 | set_config({'source': 'csv'}) 126 | profiles = add_strategies(['BB.SHFE-1.Day'], [ 127 | { 128 | 'strategy': DemoStrategy('A1'), 129 | 'capital': 50000.0 * 0.5, 130 | }, 131 | { 132 | 'strategy': DemoStrategy2('A2'), 133 | 'capital': 50000.0 * 0.5, 134 | } 135 | ]) 136 | stop = timeit.default_timer() 137 | print("运行耗时: %d秒" % ((stop - start))) 138 | 139 | # 绘制k线,交易信号线 140 | from quantdigger.digger import finance, plotting 141 | s = 0 142 | # 绘制策略A1, 策略A2, 组合的资金曲线 143 | curve0 = finance.create_equity_curve(profiles[0].all_holdings()) 144 | curve1 = finance.create_equity_curve(profiles[1].all_holdings()) 145 | curve = finance.create_equity_curve(Profile.all_holdings_sum(profiles)) 146 | plotting.plot_strategy(profiles[0].data(), profiles[0].technicals(), 147 | profiles[0].deals(), curve0.equity.values, 148 | profiles[0].marks()) 149 | # 绘制净值曲线 150 | plotting.plot_curves([curve.networth]) 151 | # 打印统计信息 152 | print(finance.summary_stats(curve, 252)) 153 | 154 | 155 | 策略结果 156 | ~~~~~~~ 157 | 158 | * k线和信号线 159 | 160 | k线显示使用了系统自带的一个联动窗口控件,由蓝色的滑块控制显示区域,可以通过鼠标拖拽改变显示区域。 161 | `上下方向键` 来进行缩放。 162 | 163 | .. image:: doc/images/plot.png 164 | :width: 500px 165 | 166 | * 2个策略和组合的资金曲线。 167 | 168 | .. image:: doc/images/figure_money.png 169 | :width: 500px 170 | 171 | * 组合的历史净值 172 | 173 | .. image:: doc/images/figure_networth.png 174 | :width: 500px 175 | 176 | * 统计结果 177 | 178 | :: 179 | 180 | >>> [('Total Return', '-0.99%'), ('Sharpe Ratio', '-5.10'), ('Max Drawdown', '1.72%'), ('Drawdown Duration', '3568')] 181 | 182 | 183 | .. _TeaEra: https://github.com/TeaEra 184 | .. _deepfish: https://github.com/deepfish 185 | .. _wondereamer: https://github.com/wondereamer 186 | .. _HonePhy: https://github.com/HonePhy 187 | .. _tushare: https://github.com/waditu/tushare 188 | .. _Jimmy: https://github.com/jimmysoa 189 | .. _vodkabuaa: https://github.com/vodkabuaa 190 | .. _ongbe: https://github.com/ongbe 191 | .. _pyalgotrade: https://github.com/gbeced/pyalgotrade 192 | .. _zipline: https://github.com/quantopian/zipline 193 | .. _wiki文档: https://github.com/QuantFans/quantdigger/wiki 194 | 195 | 196 | 版本 197 | ~~~~ 198 | 199 | **0.6.0 版本 2019-05-28** 200 | 201 | * 重构回测引擎,使其设计更合理和简洁。 202 | 203 | **0.5.1 版本 2017-07-13** 204 | 205 | * 在原来0.5.0版的基础上改为支持Python3.6 206 | 207 | **0.5.0 版本 2017-01-08** 208 | 209 | * 完善文档 210 | * 数据源可配置 211 | * 添加shell, 界面,回测引擎三则间的交互框架 212 | 213 | **0.3.0 版本 2015-12-09** 214 | 215 | * 重新设计回测引擎, 支持组合回测,选股 216 | * 重构数据模块 217 | 218 | **0.2.0 版本 2015-08-18** 219 | 220 | * 修复股票回测的破产bug 221 | * 修复回测权益计算bug 222 | * 交易信号对的计算从回测代码中分离 223 | * 把回测金融指标移到digger/finace 224 | * 添加部分数据结构,添加部分数据结构字段 225 | * 添加几个mongodb相关的函数 226 | 227 | **0.1.0 版本 2015-06-16** 228 | 229 | * 夸品种的策略回测功能 230 | * 简单的交互 231 | * 指标,k线绘制 232 | -------------------------------------------------------------------------------- /tests/trading/stock_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import datetime 5 | import six 6 | from quantdigger import settings 7 | 8 | window_size = 0 9 | OFFSET = 0.6 10 | capital = 20000000 11 | bt1 = datetime.datetime.strptime("09:01:00", "%H:%M:%S").time() 12 | bt2 = datetime.datetime.strptime("09:02:00", "%H:%M:%S").time() 13 | bt3 = datetime.datetime.strptime("09:03:00", "%H:%M:%S").time() 14 | st1 = datetime.datetime.strptime("14:57:00", "%H:%M:%S").time() 15 | st2 = datetime.datetime.strptime("14:58:00", "%H:%M:%S").time() 16 | st3 = datetime.datetime.strptime("15:00:00", "%H:%M:%S").time() 17 | 18 | 19 | def trade_closed_curbar(data, capital, long_margin, short_margin, volume_multiple, direction): 20 | """ 策略: 多头限价开仓且当根bar成交 21 | 买入点: [bt1, bt2, bt3] 22 | 当天卖出点: [st1, st2] 23 | 24 | 保证不能当天卖。 25 | """ 26 | assert(volume_multiple == 1 and long_margin == 1) 27 | UNIT = 1 28 | date_quantity= {} 29 | poscost = 0 30 | close_profit = 0 31 | equities = [] 32 | dts = [] 33 | cashes = [] 34 | 35 | open_poscost = 0 36 | open_cashes = [] 37 | open_equities = [] 38 | open_close_profit = 0 39 | open_quantity = 0 40 | num = 0 41 | for curdt, curprice in data.close.iteritems(): 42 | open_price = data.open[curdt] 43 | curtime = curdt.time() 44 | curdate = curdt.date() 45 | if curtime in [bt1, bt2, bt3]: 46 | # 开仓 47 | open_close_profit = close_profit 48 | open_quantity = sum(date_quantity.values()) 49 | open_poscost = poscost 50 | 51 | date_quantity.setdefault(curdate, 0) 52 | quantity = sum(date_quantity.values()) 53 | poscost = (poscost * quantity + curprice * 54 | (1 + direction * settings['stock_commission']) * UNIT) / (quantity + UNIT) 55 | date_quantity[curdate] += UNIT 56 | elif curtime == st1: 57 | for posdate, quantity in six.iteritems(date_quantity): 58 | if posdate < curdate and quantity > 0: # 隔日交易 59 | open_close_profit = close_profit 60 | open_quantity = sum(date_quantity.values()) 61 | close_profit += direction * (curprice * (1 - direction * 62 | settings['stock_commission']) - poscost) * 2 * UNIT * volume_multiple 63 | date_quantity[posdate] -= 2 * UNIT 64 | elif posdate > curdate: 65 | assert(False) 66 | elif curtime == st2: 67 | for posdate, quantity in six.iteritems(date_quantity): 68 | if posdate < curdate and quantity > 0: 69 | open_close_profit = close_profit 70 | open_quantity = sum(date_quantity.values()) 71 | close_profit += direction * (curprice * (1 - direction * 72 | settings['stock_commission']) - poscost) * UNIT * volume_multiple 73 | date_quantity[posdate] -= UNIT 74 | assert(date_quantity[posdate] == 0) 75 | elif posdate > curdate: 76 | assert(False) 77 | else: 78 | open_quantity = sum(date_quantity.values()) 79 | open_poscost = poscost 80 | open_close_profit = close_profit 81 | 82 | if curdt == data.index[-1]: 83 | # 强平现有持仓 84 | open_close_profit = close_profit 85 | open_quantity = sum(date_quantity.values()) 86 | quantity = sum(date_quantity.values()) 87 | close_profit += direction * (curprice * (1 - direction * settings['stock_commission']) - 88 | poscost) * quantity * volume_multiple 89 | date_quantity.clear() 90 | 91 | quantity = sum(date_quantity.values()) 92 | pos_profit = direction * (curprice - poscost) * quantity * volume_multiple 93 | posmargin = curprice * quantity * volume_multiple * long_margin 94 | open_pos_profit = direction * (open_price - open_poscost) * open_quantity * volume_multiple 95 | open_posmargin = open_price * open_quantity * volume_multiple * long_margin 96 | 97 | equities.append(capital + close_profit + pos_profit) 98 | cashes.append(equities[-1] - posmargin) 99 | open_equities.append(capital + open_close_profit + open_pos_profit) 100 | open_cashes.append(open_equities[-1] - open_posmargin) 101 | dts.append(curdt) 102 | num += 1 103 | return equities, cashes, open_equities, open_cashes, dts 104 | 105 | 106 | def buy_monday_sell_friday(data, capital, long_margin, volume_multiple): 107 | """ 策略: 多头限价开仓且当根bar成交 108 | 周一买,周五卖 109 | """ 110 | assert(volume_multiple == 1 and long_margin == 1) 111 | UNIT = 1 112 | poscost = 0 113 | quantity = 0 114 | close_profit = 0 115 | equities = {} 116 | dts = [] 117 | cashes = {} 118 | 119 | open_poscost = 0 120 | open_cashes = {} 121 | open_equities = {} 122 | open_close_profit = 0 123 | open_quantity = 0 124 | num = 0 125 | for curdt, curprice in data.close.iteritems(): 126 | pos_profit = 0 127 | open_price = data.open[curdt] 128 | weekday = curdt.weekday() 129 | if weekday == 0: 130 | open_poscost = poscost 131 | open_quantity = quantity 132 | open_close_profit = close_profit 133 | poscost = (poscost * quantity + curprice * (1 + settings['stock_commission']) * UNIT)\ 134 | / (quantity + UNIT) 135 | quantity += UNIT 136 | elif weekday == 4 and quantity > 0: 137 | open_close_profit = close_profit 138 | open_quantity = quantity 139 | close_profit += (curprice * (1 - settings['stock_commission']) - poscost) * quantity * volume_multiple 140 | quantity = 0 141 | else: 142 | open_close_profit = close_profit 143 | open_poscost = poscost 144 | open_quantity = quantity 145 | 146 | pos_profit = (curprice - poscost) * quantity * volume_multiple 147 | equities[curdt] = capital + close_profit + pos_profit 148 | posmargin = curprice * quantity * volume_multiple * long_margin 149 | cashes[curdt] = capital + close_profit + pos_profit - posmargin 150 | dts.append(curdt) 151 | 152 | open_pos_profit = (open_price - open_poscost) * open_quantity * volume_multiple 153 | open_equities[curdt] = capital + open_close_profit + open_pos_profit 154 | 155 | open_posmargin = open_price * open_quantity * volume_multiple * long_margin 156 | open_cashes[curdt] = capital + open_close_profit + open_pos_profit - open_posmargin 157 | num += 1 158 | return equities, cashes, open_equities, open_cashes, dts 159 | -------------------------------------------------------------------------------- /demo/work/_djtrend2_IF000_wave.txt: -------------------------------------------------------------------------------- 1 | 2010-04-21 14:30:00 2 | 2010-05-12 13:00:00 3 | 2010-05-13 14:15:00 4 | 2010-05-17 14:45:00 5 | 2010-05-19 13:15:00 6 | 2010-05-21 09:15:00 7 | 2010-05-24 10:45:00 8 | 2010-05-27 09:45:00 9 | 2010-05-28 09:15:00 10 | 2010-06-02 13:45:00 11 | 2010-06-03 11:15:00 12 | 2010-06-07 13:15:00 13 | 2010-06-17 09:15:00 14 | 2010-06-18 14:45:00 15 | 2010-06-21 14:15:00 16 | 2010-07-02 13:30:00 17 | 2010-07-12 11:00:00 18 | 2010-07-13 11:00:00 19 | 2010-07-15 10:00:00 20 | 2010-07-16 13:00:00 21 | 2010-07-26 09:45:00 22 | 2010-07-26 11:15:00 23 | 2010-08-02 13:00:00 24 | 2010-08-05 14:30:00 25 | 2010-08-09 10:45:00 26 | 2010-08-13 11:00:00 27 | 2010-08-19 14:00:00 28 | 2010-08-27 13:15:00 29 | 2010-09-01 10:45:00 30 | 2010-09-01 14:15:00 31 | 2010-09-07 15:00:00 32 | 2010-09-10 11:00:00 33 | 2010-09-14 09:45:00 34 | 2010-09-16 14:15:00 35 | 2010-09-20 11:15:00 36 | 2010-09-21 13:00:00 37 | 2010-09-27 09:15:00 38 | 2010-09-27 10:00:00 39 | 2010-10-26 09:15:00 40 | 2010-10-29 14:30:00 41 | 2010-11-01 14:45:00 42 | 2010-11-03 14:45:00 43 | 2010-11-05 09:15:00 44 | 2010-11-10 13:30:00 45 | 2010-11-11 14:15:00 46 | 2010-11-19 13:30:00 47 | 2010-11-19 15:00:00 48 | 2010-11-23 13:00:00 49 | 2010-11-25 14:00:00 50 | 2010-11-30 13:30:00 51 | 2010-12-02 09:15:00 52 | 2010-12-07 09:30:00 53 | 2010-12-07 14:30:00 54 | 2010-12-10 09:15:00 55 | 2010-12-15 13:45:00 56 | 2010-12-20 11:15:00 57 | 2010-12-21 15:00:00 58 | 2010-12-30 11:00:00 59 | 2011-01-06 09:30:00 60 | 2011-01-11 10:45:00 61 | 2011-01-13 09:15:00 62 | 2011-01-18 09:45:00 63 | 2011-01-19 15:00:00 64 | 2011-01-25 14:30:00 65 | 2011-02-01 09:15:00 66 | 2011-02-10 09:30:00 67 | 2011-02-17 09:15:00 68 | 2011-02-21 09:15:00 69 | 2011-02-21 15:00:00 70 | 2011-02-23 14:15:00 71 | 2011-03-01 13:15:00 72 | 2011-03-02 10:30:00 73 | 2011-03-03 09:45:00 74 | 2011-03-03 15:00:00 75 | 2011-03-09 09:15:00 76 | 2011-03-15 10:45:00 77 | 2011-03-21 09:15:00 78 | 2011-03-22 10:15:00 79 | 2011-03-28 10:15:00 80 | 2011-03-31 13:00:00 81 | 2011-04-11 09:15:00 82 | 2011-04-12 14:15:00 83 | 2011-04-13 15:00:00 84 | 2011-04-20 13:15:00 85 | 2011-04-21 10:45:00 86 | 2011-05-06 09:15:00 87 | 2011-05-11 11:15:00 88 | 2011-05-17 10:30:00 89 | 2011-05-19 10:15:00 90 | 2011-05-30 09:30:00 91 | 2011-05-31 14:30:00 92 | 2011-06-02 13:00:00 93 | 2011-06-08 14:30:00 94 | 2011-06-13 13:00:00 95 | 2011-06-14 14:00:00 96 | 2011-06-20 14:00:00 97 | 2011-06-27 10:30:00 98 | 2011-06-29 14:45:00 99 | 2011-07-07 09:15:00 100 | 2011-07-07 09:45:00 101 | 2011-07-07 13:00:00 102 | 2011-07-12 15:00:00 103 | 2011-07-18 09:30:00 104 | 2011-07-27 09:15:00 105 | 2011-07-29 10:45:00 106 | 2011-07-29 14:00:00 107 | 2011-08-02 13:00:00 108 | 2011-08-04 09:45:00 109 | 2011-08-09 09:30:00 110 | 2011-08-16 10:00:00 111 | 2011-08-22 14:30:00 112 | 2011-08-25 15:00:00 113 | 2011-09-06 14:30:00 114 | 2011-09-09 10:00:00 115 | 2011-09-14 13:30:00 116 | 2011-09-16 09:15:00 117 | 2011-09-19 15:00:00 118 | 2011-09-21 14:00:00 119 | 2011-09-26 14:30:00 120 | 2011-09-28 10:15:00 121 | 2011-10-12 09:15:00 122 | 2011-10-17 09:45:00 123 | 2011-10-20 14:00:00 124 | 2011-11-01 10:30:00 125 | 2011-11-02 09:45:00 126 | 2011-11-04 09:15:00 127 | 2011-11-11 15:00:00 128 | 2011-11-14 10:45:00 129 | 2011-11-24 09:30:00 130 | 2011-11-29 14:15:00 131 | 2011-11-30 14:15:00 132 | 2011-12-01 11:00:00 133 | 2011-12-08 10:15:00 134 | 2011-12-08 11:15:00 135 | 2011-12-19 11:15:00 136 | 2011-12-20 09:30:00 137 | 2011-12-22 10:45:00 138 | 2011-12-23 11:15:00 139 | 2011-12-28 13:15:00 140 | 2012-01-04 09:15:00 141 | 2012-01-06 13:15:00 142 | 2012-01-10 15:00:00 143 | 2012-01-17 10:15:00 144 | 2012-01-20 13:30:00 145 | 2012-02-01 14:30:00 146 | 2012-02-06 09:30:00 147 | 2012-02-07 14:00:00 148 | 2012-02-10 11:00:00 149 | 2012-02-13 09:15:00 150 | 2012-02-15 13:00:00 151 | 2012-02-16 14:00:00 152 | 2012-02-20 10:30:00 153 | 2012-02-21 10:45:00 154 | 2012-02-27 14:00:00 155 | 2012-03-01 15:00:00 156 | 2012-03-05 09:15:00 157 | 2012-03-07 15:00:00 158 | 2012-03-14 10:15:00 159 | 2012-03-16 13:00:00 160 | 2012-03-19 14:45:00 161 | 2012-04-05 09:15:00 162 | 2012-04-06 10:45:00 163 | 2012-04-10 11:15:00 164 | 2012-04-13 13:45:00 165 | 2012-04-17 14:45:00 166 | 2012-04-24 10:00:00 167 | 2012-04-24 11:15:00 168 | 2012-05-08 09:15:00 169 | 2012-05-16 14:45:00 170 | 2012-05-17 14:15:00 171 | 2012-05-18 14:30:00 172 | 2012-05-22 14:45:00 173 | 2012-05-28 10:00:00 174 | 2012-05-29 13:30:00 175 | 2012-06-08 15:00:00 176 | 2012-06-11 13:30:00 177 | 2012-06-12 10:45:00 178 | 2012-06-18 11:15:00 179 | 2012-06-29 09:15:00 180 | 2012-07-03 13:00:00 181 | 2012-07-05 13:30:00 182 | 2012-07-06 14:15:00 183 | 2012-07-12 09:45:00 184 | 2012-07-13 10:00:00 185 | 2012-07-18 13:30:00 186 | 2012-07-19 13:30:00 187 | 2012-07-31 14:15:00 188 | 2012-08-01 10:45:00 189 | 2012-08-02 13:15:00 190 | 2012-08-10 09:30:00 191 | 2012-08-20 09:45:00 192 | 2012-08-21 11:00:00 193 | 2012-09-03 09:15:00 194 | 2012-09-03 11:00:00 195 | 2012-09-05 10:15:00 196 | 2012-09-07 11:15:00 197 | 2012-09-11 13:00:00 198 | 2012-09-12 09:15:00 199 | 2012-09-12 11:15:00 200 | 2012-09-14 09:15:00 201 | 2012-09-14 11:00:00 202 | 2012-09-14 13:45:00 203 | 2012-09-24 09:45:00 204 | 2012-09-24 14:15:00 205 | 2012-09-26 14:30:00 206 | 2012-10-09 11:00:00 207 | 2012-10-15 14:00:00 208 | 2012-10-22 14:15:00 209 | 2012-10-29 14:15:00 210 | 2012-11-05 09:30:00 211 | 2012-11-19 13:45:00 212 | 2012-11-23 10:00:00 213 | 2012-11-29 15:00:00 214 | 2012-12-03 09:45:00 215 | 2012-12-04 11:00:00 216 | 2012-12-12 10:00:00 217 | 2012-12-13 14:15:00 218 | 2012-12-18 13:15:00 219 | 2012-12-20 13:00:00 220 | 2013-01-04 09:15:00 221 | 2013-01-04 11:00:00 222 | 2013-01-08 11:15:00 223 | 2013-01-09 14:00:00 224 | 2013-01-10 10:15:00 225 | 2013-01-11 14:15:00 226 | 2013-01-15 14:15:00 227 | 2013-01-16 13:15:00 228 | 2013-01-18 13:15:00 229 | 2013-01-22 09:45:00 230 | 2013-01-24 10:30:00 231 | 2013-01-24 14:15:00 232 | 2013-02-08 13:30:00 233 | 2013-02-26 15:00:00 234 | 2013-02-28 15:00:00 235 | 2013-03-04 14:30:00 236 | 2013-03-06 13:45:00 237 | 2013-03-14 09:15:00 238 | 2013-03-15 11:15:00 239 | 2013-03-19 11:00:00 240 | 2013-03-25 09:15:00 241 | 2013-04-02 11:00:00 242 | 2013-04-08 10:00:00 243 | 2013-04-11 09:15:00 244 | 2013-04-16 09:15:00 245 | 2013-04-19 14:15:00 246 | 2013-04-23 14:30:00 247 | 2013-04-24 13:15:00 248 | 2013-05-02 09:30:00 249 | 2013-05-08 09:45:00 250 | 2013-05-09 14:00:00 251 | 2013-05-13 09:15:00 252 | 2013-05-14 11:15:00 253 | 2013-05-23 11:15:00 254 | 2013-05-24 13:00:00 255 | 2013-05-29 11:00:00 256 | 2013-06-13 10:30:00 257 | 2013-06-17 09:15:00 258 | 2013-06-25 13:00:00 259 | 2013-06-28 10:30:00 260 | 2013-07-03 10:45:00 261 | 2013-07-04 13:30:00 262 | 2013-07-09 15:00:00 263 | 2013-07-11 13:45:00 264 | 2013-07-12 14:45:00 265 | 2013-07-15 11:15:00 266 | 2013-07-22 09:15:00 267 | 2013-07-23 13:15:00 268 | 2013-07-29 13:30:00 269 | 2013-08-07 09:30:00 270 | 2013-08-07 15:00:00 271 | 2013-08-09 09:15:00 272 | 2013-08-09 11:15:00 273 | 2013-08-14 09:45:00 274 | 2013-08-16 15:00:00 275 | 2013-08-20 11:00:00 276 | 2013-08-23 13:30:00 277 | 2013-08-27 10:00:00 278 | 2013-08-30 14:30:00 279 | 2013-09-03 14:00:00 280 | 2013-09-05 10:30:00 281 | 2013-09-12 14:15:00 282 | 2013-09-18 11:00:00 283 | 2013-09-23 14:30:00 284 | 2013-09-27 13:30:00 285 | 2013-10-14 09:15:00 286 | 2013-10-17 15:00:00 287 | 2013-10-21 14:15:00 288 | -------------------------------------------------------------------------------- /quantdigger/widgets/mplotwidgets/mplots.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | from six.moves import range 5 | import numpy as np 6 | import inspect 7 | from matplotlib.colors import colorConverter 8 | from matplotlib.collections import LineCollection, PolyCollection 9 | 10 | 11 | def override_attributes(method): 12 | # 如果plot函数不带绘图参数,则使用属性值做为参数。 13 | # 如果带参数,者指标中的plot函数参数能够覆盖本身的属性。 14 | def wrapper(self, widget, *args, **kwargs): 15 | self.widget = widget 16 | # 用函数中的参数覆盖属性。 17 | arg_names = inspect.getargspec(method).args[2:] 18 | method_args = {} 19 | obj_attrs = {} 20 | for i, arg in enumerate(args): 21 | method_args[arg_names[i]] = arg 22 | method_args.update(kwargs) 23 | 24 | try: 25 | for attr in arg_names: 26 | obj_attrs[attr] = getattr(self, attr) 27 | except Exception as e: 28 | six.print_(e) 29 | six.print_("构造函数和绘图函数的绘图属性参数不匹配。") 30 | obj_attrs.update(method_args) 31 | return method(self, widget, **obj_attrs) 32 | return wrapper 33 | 34 | 35 | class Candles(object): 36 | """ 37 | 画蜡烛线。 38 | """ 39 | def __init__(self, data, tracker, name='candle', 40 | width=0.6, colorup='r', colordown='g', 41 | lc='k', alpha=1): 42 | """ Represent the open, close as a bar line and high low range as a 43 | vertical line. 44 | 45 | 46 | ax : an Axes instance to plot to 47 | 48 | width : the bar width in points 49 | 50 | colorup : the color of the lines where close >= open 51 | 52 | colordown : the color of the lines where close < open 53 | 54 | alpha : bar transparency 55 | 56 | return value is lineCollection, barCollection 57 | """ 58 | # super(Candles, self).__init__(tracker, name) 59 | self.data = data 60 | self.name = name 61 | self.width = width 62 | self.colorup = colorup 63 | self.colordown = colordown 64 | self.lc = lc 65 | self.alpha = alpha 66 | self.lineCollection = [] 67 | self.barCollection = [] 68 | 69 | # note this code assumes if any value open, close, low, high is 70 | # missing they all are missing 71 | @override_attributes 72 | def plot(self, widget, data, width=0.6, 73 | colorup='r', colordown='g', lc='k', alpha=1): 74 | 75 | if self.lineCollection: 76 | self.lineCollection.remove() 77 | if self.barCollection: 78 | self.barCollection.remove() 79 | 80 | self.set_yrange(data.low.values, data.high.values) 81 | self.data = data 82 | """docstring for plot""" 83 | delta = self.width / 2. 84 | barVerts = [((i - delta, open), 85 | (i - delta, close), 86 | (i + delta, close), 87 | (i + delta, open)) 88 | for i, open, close in zip(range(len(self.data)), 89 | self.data.open, 90 | self.data.close) 91 | if open != -1 and close != -1] 92 | rangeSegments = [((i, low), (i, high)) 93 | for i, low, high in zip(range(len(self.data)), 94 | self.data.low, 95 | self.data.high) 96 | if low != -1] 97 | r, g, b = colorConverter.to_rgb(self.colorup) 98 | colorup = r, g, b, self.alpha 99 | r, g, b = colorConverter.to_rgb(self.colordown) 100 | colordown = r, g, b, self.alpha 101 | colord = { 102 | True: colorup, 103 | False: colordown, 104 | } 105 | colors = [colord[open < close] 106 | for open, close in zip(self.data.open, self.data.close) 107 | if open != -1 and close != -1] 108 | assert(len(barVerts) == len(rangeSegments)) 109 | useAA = 0, # use tuple here 110 | lw = 0.5, # and here 111 | r, g, b = colorConverter.to_rgb(self.lc) 112 | linecolor = r, g, b, self.alpha 113 | self.lineCollection = LineCollection(rangeSegments, 114 | colors=(linecolor,), 115 | linewidths=lw, 116 | antialiaseds=useAA, 117 | zorder=0) 118 | 119 | self.barCollection = PolyCollection(barVerts, 120 | facecolors=colors, 121 | edgecolors=colors, 122 | antialiaseds=useAA, 123 | linewidths=lw, 124 | zorder=1) 125 | widget.autoscale_view() 126 | # add these last 127 | widget.add_collection(self.barCollection) 128 | widget.add_collection(self.lineCollection) 129 | return self.lineCollection, self.barCollection 130 | 131 | def set_yrange(self, lower, upper=[]): 132 | self.upper = upper if len(upper) > 0 else lower 133 | self.lower = lower 134 | 135 | def y_interval(self, w_left, w_right): 136 | if len(self.upper) == 2: 137 | return max(self.upper), min(self.lower) 138 | ymax = np.max(self.upper[w_left: w_right]) 139 | ymin = np.min(self.lower[w_left: w_right]) 140 | return ymax, ymin 141 | 142 | 143 | class TradingSignal(object): 144 | """ 从信号坐标(时间, 价格)中绘制交易信号。 """ 145 | def __init__(self, signal, name="Signal", c=None, lw=2): 146 | self.signal = signal 147 | self.name = name 148 | 149 | def plot(self, widget, c=None, lw=2): 150 | useAA = 0, # use tuple here 151 | signal = LineCollection(self.signal, colors=c, linewidths=lw, 152 | antialiaseds=useAA) 153 | widget.add_collection(signal) 154 | 155 | def y_interval(self, w_left, w_right): 156 | return 0, 100000000 157 | 158 | 159 | class TradingSignalPos(object): 160 | """ 从价格和持仓数据中绘制交易信号图。 """ 161 | def __init__(self, price_data, deals, name="Signal", c=None, lw=2): 162 | self.signal = [] 163 | self.colors = [] 164 | price_data['row'] = [i for i in range(0, len(price_data))] 165 | for deal in deals: 166 | # ((x0, y0), (x1, y1)) 167 | p = ((price_data.row[deal.open_datetime], deal.open_price), 168 | (price_data.row[deal.close_datetime], deal.close_price)) 169 | self.signal.append(p) 170 | self.colors.append( 171 | (1, 0, 0, 1) if deal.profit() > 0 else (0, 1, 0, 1)) 172 | self.name = name 173 | 174 | def plot(self, widget, lw=2): 175 | useAA = 0, # use tuple here 176 | signal = LineCollection(self.signal, colors=self.colors, linewidths=lw, 177 | antialiaseds=useAA) 178 | widget.add_collection(signal) 179 | 180 | def y_interval(self, w_left, w_right): 181 | # @todo signal interval 182 | return 0, 100000000 183 | -------------------------------------------------------------------------------- /quantdigger/engine/series.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from quantdigger.errors import SeriesIndexError 4 | import datetime 5 | 6 | 7 | class SeriesBase(object): 8 | def __init__(self, data=[], name='series', indic=None, default=None): 9 | """ 序列变量的基类。 10 | name用来跟踪 11 | """ 12 | self.curbar = 0 13 | self._window_size = len(data) 14 | self._indic = indic 15 | self._default = default 16 | self._realtime = False 17 | self.name = name 18 | if len(data) == 0: 19 | self.data = np.array([self._default] * self._window_size) 20 | else: 21 | self.data = data 22 | 23 | def reset_data(self, data, wsize): 24 | """ 初始化值和窗口大小 25 | 26 | Args: 27 | data (list|ndarray|pd.Series): 数据,类型为支持索引的数据结构 28 | wwsize (int): 窗口大小 29 | """ 30 | self._window_size = wsize 31 | if len(data) == 0: 32 | self.data = np.array([self._default] * self._window_size) 33 | else: 34 | # 序列系统变量 35 | self.data = data 36 | 37 | def update_curbar(self, curbar): 38 | """ 更新当前Bar索引 """ 39 | self.curbar = curbar 40 | 41 | def update(self, v): 42 | """ 更新最后一个值 """ 43 | if isinstance(v, SeriesBase): 44 | self.data[self.curbar] = v[0] 45 | else: 46 | self.data[self.curbar] = v 47 | 48 | def __len__(self): 49 | return len(self.data) 50 | 51 | def duplicate_last_element(self): 52 | """ 53 | 非指标序列变量才会运行 54 | """ 55 | try: 56 | pre_elem = self.data[(self.curbar-1) % self._window_size] 57 | except KeyError: 58 | return 59 | else: 60 | self.data[self.curbar] = pre_elem 61 | 62 | def __str__(self): 63 | return str(self[0]) 64 | 65 | def __getitem__(self, index): 66 | raise NotImplementedError 67 | 68 | #def __call__(self, *args): 69 | #length = len(args) 70 | #if length == 0: 71 | #return float(self) 72 | #elif length == 1: 73 | #return self.data[self.curbar - args[0]] 74 | 75 | 76 | class NumberSeries(SeriesBase): 77 | """ 数字序列变量""" 78 | DEFAULT_VALUE = 0.0 79 | value_type = float 80 | 81 | def __init__(self, data=[], name='NumberSeries', indic=None, default=0.0): 82 | super(NumberSeries, self).__init__(data, name, indic, default) 83 | return 84 | 85 | def __float__(self): 86 | return self[0] 87 | 88 | # 89 | def __eq__(self, r): 90 | return self[0] == float(r) 91 | 92 | def __lt__(self, r): 93 | return self[0] < float(r) 94 | 95 | def __le__(self, r): 96 | return self[0] <= float(r) 97 | 98 | def __ne__(self, r): 99 | return self[0] != float(r) 100 | 101 | def __gt__(self, r): 102 | return self[0] > float(r) 103 | 104 | def __ge__(self, r): 105 | return self[0] >= float(r) 106 | 107 | # 108 | def __iadd__(self, r): 109 | self[0] += float(r) 110 | return self 111 | 112 | def __isub__(self, r): 113 | self[0] -= float(r) 114 | return self 115 | 116 | def __imul__(self, r): 117 | self[0] *= float(r) 118 | return self 119 | 120 | def __idiv__(self, r): 121 | self[0] /= float(r) 122 | return self 123 | 124 | def __ifloordiv__(self, r): 125 | self[0] %= float(r) 126 | return self 127 | 128 | # 129 | def __add__(self, r): 130 | return self[0] + float(r) 131 | 132 | def __sub__(self, r): 133 | return self[0] - float(r) 134 | 135 | def __mul__(self, r): 136 | return self[0] * float(r) 137 | 138 | def __div__(self, r): 139 | return self[0] / float(r) 140 | 141 | def __mod__(self, r): 142 | return self[0] % float(r) 143 | 144 | def __pow__(self, r): 145 | return self[0] ** float(r) 146 | 147 | # 148 | def __radd__(self, r): 149 | return float(r) + self[0] 150 | 151 | def __rsub__(self, r): 152 | return float(r) - self[0] 153 | 154 | def __rmul__(self, r): 155 | return float(r) * self[0] 156 | 157 | def __rdiv__(self, r): 158 | return float(r) / self[0] 159 | 160 | def __rmod__(self, r): 161 | return float(r) % self[0] 162 | 163 | def __rpow__(self, r): 164 | return float(r) ** self[0] 165 | 166 | def __getitem__(self, index): 167 | try: 168 | if self._realtime: 169 | #if index<0: 170 | #return self._default 171 | #if self._shift: 172 | ## 输入 173 | #return self.data[self._index-index] 174 | ## 输出 175 | #i = self.curbar - index 176 | #if self._indic: 177 | ## 延迟计算 178 | #self._indic.compute_element(i%self._window_size, index) 179 | #return self.data[i%self._window_size] 180 | assert(False) 181 | else: 182 | i = self.curbar - index 183 | if i < 0 or index < 0: 184 | return self._default 185 | else: 186 | return float(self.data[i]) 187 | except SeriesIndexError: 188 | raise SeriesIndexError 189 | 190 | def __call__(self, index): 191 | return self[index] 192 | 193 | 194 | class DateTimeSeries(SeriesBase): 195 | """ 时间序列变量 """ 196 | DEFAULT_VALUE = datetime.datetime(1980, 1, 1) 197 | value_type = datetime.datetime 198 | 199 | def __init__(self, data=[], name='DateTimeSeries'): 200 | super(DateTimeSeries, self).__init__(data, name, 201 | default=self.DEFAULT_VALUE) 202 | return 203 | 204 | def __getitem__(self, index): 205 | try: 206 | i = self.curbar - index 207 | if i < 0 or index < 0: 208 | return self._default 209 | #return datetime.fromtimestamp(self.data[i%self._window_size]/1000) 210 | return self.data[i] 211 | except SeriesIndexError: 212 | raise SeriesIndexError 213 | 214 | def __str__(self): 215 | return str(self.data[self.curbar]) 216 | 217 | def __eq__(self, r): 218 | if isinstance(r, DateTimeSeries): 219 | return self[0] == r[0] 220 | return self[0] == r 221 | 222 | def __lt__(self, r): 223 | if isinstance(r, DateTimeSeries): 224 | return self[0] < r[0] 225 | return self[0] < r 226 | 227 | def __le__(self, r): 228 | if isinstance(r, DateTimeSeries): 229 | return self[0] <= r[0] 230 | return self[0] <= r 231 | 232 | def __ne__(self, r): 233 | if isinstance(r, DateTimeSeries): 234 | return self[0] != r[0] 235 | return self[0] != r 236 | 237 | def __gt__(self, r): 238 | if isinstance(r, DateTimeSeries): 239 | return self[0] > r[0] 240 | return self[0] > r 241 | 242 | def __ge__(self, r): 243 | if isinstance(r, DateTimeSeries): 244 | return self[0] >= r[0] 245 | return self[0] >= r 246 | -------------------------------------------------------------------------------- /quantdigger/technicals/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import talib 4 | 5 | from quantdigger.technicals.base import ( 6 | TechnicalBase, 7 | ndarray, 8 | tech_init 9 | ) 10 | from quantdigger.technicals.techutil import register_tech 11 | from quantdigger.widgets.plotter import Plotter, plot_init 12 | 13 | 14 | @register_tech('MA') 15 | class MA(TechnicalBase): 16 | """ 移动平均线指标。 """ 17 | @tech_init 18 | def __init__(self, data, n, name='MA', 19 | style='y', lw=1): 20 | """ data (NumberSeries/np.ndarray/list) """ 21 | super(MA, self).__init__(name) 22 | # 必须的函数参数 23 | self._args = [ndarray(data), n] 24 | 25 | def _rolling_algo(self, data, n, i): 26 | """ 逐步运行函数。""" 27 | return (talib.SMA(data, n)[i], ) 28 | 29 | def _vector_algo(self, data, n): 30 | """向量化运行, 结果必须赋值给self.values。 31 | 32 | Args: 33 | data (np.ndarray): 数据 34 | n (int): 时间窗口大小 35 | """ 36 | ## @NOTE self.values为保留字段! 37 | # 绘图和指标基类都会用到self.values 38 | self.values = talib.SMA(data, n) 39 | 40 | def plot(self, widget): 41 | """ 绘图,参数可由UI调整。 """ 42 | self.widget = widget 43 | self.plot_line(self.values, self.style, lw=self.lw) 44 | 45 | 46 | @register_tech('BOLL') 47 | class BOLL(TechnicalBase): 48 | """ 布林带指标。 """ 49 | @tech_init 50 | def __init__(self, data, n, name='BOLL', 51 | styles=('y', 'b', 'g'), lw=1): 52 | super(BOLL, self).__init__(name) 53 | ### @TODO 只有在逐步运算中需给self.values先赋值, 54 | ## 去掉逐步运算后删除 55 | #self.values = OrderedDict([ 56 | #('upper', []), 57 | #('middler', []), 58 | #('lower', []) 59 | #]) 60 | self._args = [ndarray(data), n, 2, 2] 61 | 62 | def _rolling_algo(self, data, n, a1, a2, i): 63 | """ 逐步运行函数。""" 64 | upper, middle, lower = talib.BBANDS(data, n, a1, a2) 65 | return (upper[i], middle[i], lower[i]) 66 | 67 | def _vector_algo(self, data, n, a1, a2): 68 | """向量化运行""" 69 | u, m, l = talib.BBANDS(data, n, a1, a2) 70 | self.values = { 71 | 'upper': u, 72 | 'middler': m, 73 | 'lower': l 74 | } 75 | 76 | def plot(self, widget): 77 | """ 绘图,参数可由UI调整。 """ 78 | self.widget = widget 79 | self.plot_line(self.values['upper'], self.styles[0], lw=self.lw) 80 | self.plot_line(self.values['middler'], self.styles[1], lw=self.lw) 81 | self.plot_line(self.values['lower'], self.styles[2], lw=self.lw) 82 | 83 | 84 | #class RSI(TechnicalBase): 85 | #@create_attributes 86 | #def __init__(self, tracker, prices, n=14, name="RSI", fillcolor='b'): 87 | #super(RSI, self).__init__(tracker, name) 88 | #self.values = self._relative_strength(prices, n) 89 | ### @todo 一种绘图风格 90 | ## 固定的y范围 91 | #self.stick_yrange([0, 100]) 92 | 93 | #def _relative_strength(self, prices, n=14): 94 | #deltas = np.diff(prices) 95 | #seed = deltas[:n+1] 96 | #up = seed[seed>=0].sum()/n 97 | #down = -seed[seed<0].sum()/n 98 | #rs = up/down 99 | #rsi = np.zeros_like(prices) 100 | #rsi[:n] = 100. - 100./(1.+rs) 101 | 102 | #for i in range(n, len(prices)): 103 | #delta = deltas[i-1] # cause the diff is 1 shorter 104 | #if delta>0: 105 | #upval = delta 106 | #downval = 0. 107 | #else: 108 | #upval = 0. 109 | #downval = -delta 110 | 111 | #up = (up*(n-1) + upval)/n 112 | #down = (down*(n-1) + downval)/n 113 | 114 | #rs = up/down 115 | #rsi[i] = 100. - 100./(1.+rs) 116 | #return rsi 117 | 118 | #def plot(self, widget): 119 | #textsize = 9 120 | #widget.plot(self.values, color=self.fillcolor, lw=2) 121 | #widget.axhline(70, color=self.fillcolor, linestyle='-') 122 | #widget.axhline(30, color=self.fillcolor, linestyle='-') 123 | #widget.fill_between(self.values, 70, where=(self.values>=70), 124 | #facecolor=self.fillcolor, edgecolor=self.fillcolor) 125 | #widget.fill_between(self.values, 30, where=(self.values<=30), 126 | #facecolor=self.fillcolor, edgecolor=self.fillcolor) 127 | #widget.text(0.6, 0.9, '>70 = overbought', va='top', transform=widget.transAxes, fontsize=textsize, color = 'k') 128 | #widget.text(0.6, 0.1, '<30 = oversold', transform=widget.transAxes, fontsize=textsize, color = 'k') 129 | #widget.set_ylim(0, 100) 130 | #widget.set_yticks([30,70]) 131 | #widget.text(0.025, 0.95, 'rsi (14)', va='top', transform=widget.transAxes, fontsize=textsize) 132 | 133 | 134 | #class MACD(TechnicalBase): 135 | #@create_attributes 136 | #def __init__(self, tracker, prices, nslow, nfast, name='MACD'): 137 | #super(MACD, self).__init__(tracker, name) 138 | #self.emaslow, self.emafast, self.macd = self._moving_average_convergence(prices, nslow=nslow, nfast=nfast) 139 | #self.values = (self.emaslow, self.emafast, self.macd) 140 | 141 | #def _moving_average_convergence(x, nslow=26, nfast=12): 142 | #""" 143 | #compute the MACD (Moving Average Convergence/Divergence) using a fast and slow exponential moving avg' 144 | #return value is emaslow, emafast, macd which are len(x) arrays 145 | #""" 146 | #emaslow = MA(x, nslow, type='exponential').value 147 | #emafast = MA(x, nfast, type='exponential').value 148 | #return emaslow, emafast, emafast - emaslow 149 | 150 | #def plot(self, widget): 151 | #self.widget = widget 152 | #fillcolor = 'darkslategrey' 153 | #nema = 9 154 | #ema9 = MA(self.macd, nema, type='exponential').value 155 | #widget.plot(self.macd, color='black', lw=2) 156 | #widget.plot(self.ema9, color='blue', lw=1) 157 | #widget.fill_between(self.macd-ema9, 0, alpha=0.5, facecolor=fillcolor, edgecolor=fillcolor) 158 | 159 | #def _qtplot(self, widget, fillcolor): 160 | #raise NotImplementedError 161 | 162 | 163 | class Volume(Plotter): 164 | ## @TODO 改成技术指标 165 | """ 柱状图。 """ 166 | @plot_init 167 | def __init__(self, open, close, volume, name='volume', 168 | colorup='r', colordown='b', width=1): 169 | super(Volume, self).__init__(name, None) 170 | self.values = ndarray(volume) 171 | 172 | def plot(self, widget): 173 | import mpl_finance as finance 174 | self.widget = widget 175 | finance.volume_overlay(widget, self.open, self.close, self.volume, 176 | self.colorup, self.colordown, self.width) 177 | 178 | ## @TODO merge Line and LineWithX and move to plotting module 179 | class Line(Plotter): 180 | """ 画线 """ 181 | @plot_init 182 | def __init__(self, ydata, name='Line', style='black', lw=1): 183 | super(Line, self).__init__(name, None) 184 | self.values = ydata 185 | 186 | def plot(self, widget): 187 | self.widget = widget 188 | self.plot_line(self.values, self.style, lw=self.lw) 189 | 190 | 191 | class LineWithX(Plotter): 192 | """ 画线 """ 193 | @plot_init 194 | def __init__(self, xdata, ydata, name='LineWithX', style='black', lw=1, ms=1): 195 | super(LineWithX, self).__init__(name, None) 196 | self.values = ydata 197 | self._xdata = xdata 198 | 199 | def plot(self, widget): 200 | self.widget = widget 201 | self.plot_line(self.xdata, self.values, self.style, lw=self.lw, ms=self.ms) 202 | 203 | 204 | __all__ = ['MA', 'BOLL', 'Volume', 'Line', 'LineWithX'] 205 | -------------------------------------------------------------------------------- /quantdigger/digger/sugar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | from six.moves import range 5 | import pandas as pd 6 | import os 7 | import datetime as dt 8 | 9 | 10 | def max_return(nbarprice, islong): 11 | ''' ''' 12 | high = -1000000 13 | low = 1000000 14 | maxdiffs = [] 15 | if islong: 16 | for ith_price in nbarprice: 17 | if ith_price > high: 18 | high = ith_price 19 | low = 1000000 20 | elif ith_price < low: 21 | low = ith_price 22 | maxdiffs.append(high-low) 23 | #six.print_(low) 24 | return max(maxdiffs) if maxdiffs else 0 25 | else: 26 | for ith_price in nbarprice: 27 | if ith_price < low: 28 | low = ith_price 29 | high = -1000000 30 | #six.print_(low) 31 | elif ith_price > high: 32 | high = ith_price 33 | maxdiffs.append(high-low) 34 | return max(maxdiffs) if maxdiffs else 0 35 | 36 | 37 | def process_signal(signal, price_data, n=10, intraday=False): 38 | ## @todo split function 39 | PRICE = 'close' 40 | data = pd.DataFrame(signal) 41 | high_profits = [] 42 | low_profits = [] 43 | exit_profits = [] 44 | 45 | periods = [] 46 | entry_nbar_bests = [] 47 | entry_nbar_worsts = [] 48 | exit_nbar_bests = [] 49 | exit_nbar_worsts = [] 50 | islongs = [] 51 | returns = [] 52 | entry_Nlist = [] 53 | exit_Nlist = [] 54 | for i in range(len(data)): 55 | startt = signal.index[i] 56 | startpos = price_data.index.searchsorted(startt) 57 | endt = signal.loc[i, ['exit_datetime']][0] 58 | endpos = price_data.index.searchsorted(endt) 59 | tradingdf = price_data.truncate(before=startt, after = endt) # 当笔交易间的价格数据 60 | 61 | onetrade = signal.loc[i, :] 62 | # high/low 63 | if len(tradingdf) > 1: 64 | hp = tradingdf.loc[:-1, :][PRICE].max() 65 | lp = tradingdf.loc[:-1, :][PRICE].min() 66 | t = tradingdf.loc[:-1, :][PRICE].tolist() 67 | t.append(float(onetrade['exit_price'])) 68 | returns.append(max_return(t, onetrade['islong'])) # 当笔交易区间内的最大回测 69 | else: 70 | hp = tradingdf.loc[:, :][PRICE].max() 71 | lp = tradingdf.loc[:, :][PRICE].min() 72 | if onetrade['islong']: 73 | returns.append(max(onetrade['entry_price']-onetrade['exit_price'], 0)) # 同一根bar上买和卖 74 | else: 75 | returns.append(max(onetrade['exit_price']-onetrade['entry_price'], 0)) 76 | hp = onetrade['exit_price'] if onetrade['exit_price'] > hp else hp # 算入交易价格 77 | hp = onetrade['entry_price'] if onetrade['entry_price'] > hp else hp 78 | lp = onetrade['exit_price'] if onetrade['exit_price'] < lp else lp 79 | lp = onetrade['entry_price'] if onetrade['entry_price'] < lp else lp 80 | hp = hp - onetrade['entry_price'] 81 | lp = lp - onetrade['entry_price'] 82 | high_profits.append(hp if onetrade['islong'] else 0-hp) # 理论最高利润 83 | low_profits.append(lp if onetrade['islong'] else 0-lp) # 理论最低利润 84 | # exit 85 | ep = onetrade['exit_price'] - onetrade['entry_price'] 86 | exit_profits.append(ep if onetrade['islong'] else 0-ep) # 实际利润 87 | # period 88 | periods.append(endpos - startpos + 1) # 持仓周期 89 | 90 | # 入场或出场后n根bar的最优和最差收益 91 | entry_begin = startpos 92 | exit_begin = endpos + 1 93 | if intraday: 94 | day_entry_end = price_data.index.searchsorted((pd.to_datetime(startt)+dt.timedelta(days=1)).strftime("%Y-%m-%d")) 95 | day_exit_end = price_data.index.searchsorted((pd.to_datetime(endt)+dt.timedelta(days=1)).strftime("%Y-%m-%d")) 96 | # 不隔夜 97 | entry_end = min(startpos+n+1, day_entry_end) 98 | exit_end = min(endpos+1+n, day_exit_end) 99 | else: 100 | entry_end = startpos + n + 1 101 | exit_end = endpos + 1 + n 102 | entry_Nlist.append(entry_end - entry_begin) 103 | exit_Nlist.append(exit_end - exit_begin) 104 | islongs.append(onetrade['islong']) 105 | 106 | position_prices = price_data.loc[entry_begin: entry_end, PRICE] 107 | exit_prices = price_data.loc[exit_begin: exit_end, PRICE] 108 | if onetrade['islong']: 109 | entry_nbar_bests.append(position_prices.max() - onetrade['entry_price']) 110 | entry_nbar_worsts.append(position_prices.min() - onetrade['entry_price']) 111 | exit_nbar_bests.append(exit_prices.max() - onetrade['entry_price']) 112 | exit_nbar_worsts.append(exit_prices.min() - onetrade['entry_price']) 113 | else: 114 | entry_nbar_bests.append(onetrade['entry_price'] - position_prices.min()) 115 | entry_nbar_worsts.append(onetrade['entry_price'] - position_prices.max()) 116 | exit_nbar_bests.append(onetrade['entry_price'] - exit_prices.min()) 117 | exit_nbar_worsts.append(onetrade['entry_price'] - exit_prices.max()) 118 | 119 | data['high_profit'] = high_profits 120 | data['low_profit'] = low_profits 121 | data['exit_profit'] = exit_profits 122 | data['period'] = periods 123 | data['return'] = returns 124 | data['entry_nbar_best'] = entry_nbar_bests 125 | data['entry_nbar_worst'] = entry_nbar_worsts 126 | data['exit_nbar_best'] = exit_nbar_bests 127 | data['exit_nbar_worst'] = exit_nbar_worsts 128 | data['islong'] = islongs 129 | data['entry_n'] = entry_Nlist 130 | data['exit_n'] = exit_Nlist 131 | six.print_("Data Preprocessing Done!") 132 | return data 133 | 134 | 135 | def load_datas(n, intraday, signal_fname, price_fname): 136 | # 一次可加载多个数据 137 | 138 | signal = pd.read_csv(signal_fname, index_col=0, parse_dates=True).sort_index() 139 | price_data = pd.read_csv(price_fname, index_col=0, parse_dates=True).sort_index() 140 | return process_signal(signal, price_data, n, intraday) 141 | #six.print_(data) 142 | #assert(False) 143 | 144 | 145 | def load_wavedata(*fnames): 146 | ''' 147 | dj1, dj2 = load_wavedata("namea", "nameb") 148 | djx --- (wave, wave_r_entry) 149 | return: 150 | ((wave_timestamp, DataFrame('pre','after'))) 151 | ''' 152 | def fnameparse(fname): 153 | ''' 154 | return entry_wave_info and wave 155 | ''' 156 | home = os.getcwd() + "/data/" 157 | return "".join([home, "trace/", fname,"_trade_wave.txt"]), "".join([home, "trace/", fname, "_wave.txt"]) 158 | 159 | def process_session(data): 160 | '''''' 161 | index = data[0] 162 | pre_en_wave = [] 163 | after_en_wave = [] 164 | ispre = True 165 | for line in data[1:]: 166 | issep = line.startswith("=") 167 | if ispre and not issep: 168 | pre_en_wave.append(line) 169 | if issep: 170 | ispre = False 171 | if not ispre and not issep: 172 | after_en_wave.append(line) 173 | return [index, pre_en_wave, after_en_wave] 174 | 175 | rst = [] 176 | for fname in fnames: 177 | # code... 178 | tw_name, w_name = fnameparse(fname) 179 | wave_ts = [] 180 | ses = [] 181 | entroinfo = [] 182 | for line in open(w_name): 183 | wave_ts.append(line.rstrip("\n")) 184 | for line in open(tw_name): 185 | line = line.rstrip("\n") 186 | ses.append(line) 187 | if line.startswith('-'): 188 | # session begin 189 | ses.pop() 190 | if ses: 191 | entroinfo.append(process_session(ses)) 192 | ses = [] 193 | entroinfo.append(process_session(ses)) 194 | d = zip(*entroinfo) 195 | rst.append((wave_ts, pd.DataFrame({'pre':d[1], 'after':d[2]}, index=d[0]))) 196 | return tuple(rst) 197 | -------------------------------------------------------------------------------- /quantdigger/event/rpc.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | import six 4 | import time 5 | from datetime import datetime 6 | from threading import Thread, Condition, Lock 7 | from quantdigger.util import log 8 | from quantdigger.errors import InvalidRPCClientArguments 9 | from quantdigger.event import Event 10 | 11 | 12 | class EventRPCClient(object): 13 | def __init__(self, name, event_engine, service, event_client=None, event_server=None): 14 | self.EVENT_FROM_CLIENT = event_client if event_client else "EVENT_FROM_%s_CLIENT" % service.upper() 15 | self.EVENT_FROM_SERVER = event_server if event_server else "EVENT_FROM_%s_SERVER" % service.upper() 16 | self.rid = 0 17 | self._handlers = {} 18 | self._name = name 19 | self._handlers_lock = Lock() 20 | self._event_engine = event_engine 21 | self._event_engine.register(self.EVENT_FROM_SERVER, self._process_apiback) 22 | self._pause_condition = Condition() 23 | self._sync_ret = None 24 | self._timeout = 0 25 | self._timer_sleep = 1 26 | self._sync_call_time_lock = Lock() 27 | self._sync_call_time = datetime.now() 28 | timer = Thread(target=self._run_timer) 29 | timer.daemon = True 30 | timer.start() 31 | 32 | def _run_timer(self): 33 | # @TODO 用python自带的Event替代。 34 | while True: 35 | if not self._timeout == 0: 36 | with self._sync_call_time_lock: 37 | mtime = self._sync_call_time 38 | delta = (datetime.now() - mtime).seconds 39 | if delta >= self._timeout: 40 | # print("timeout", self._timeout, delta) 41 | # 不可重入,保证self.rid就是超时的那个 42 | with self._handlers_lock: 43 | del self._handlers[self.rid] 44 | log.debug("[RPCClient._runtimer] 处理超时, delete rid; %s" % self.rid) 45 | self._timeout = 0 46 | self._notify_server_data() 47 | time.sleep(self._timer_sleep) 48 | 49 | def _process_apiback(self, event): 50 | assert(event.route == self.EVENT_FROM_SERVER) 51 | self._timeout = 0 52 | rid = event.args['rid'] 53 | try: 54 | with self._handlers_lock: 55 | handler = self._handlers[rid] 56 | except KeyError: 57 | log.info('[RPCClient._process_apiback] 放弃超时任务的返回结果') 58 | else: 59 | try: 60 | if handler: 61 | # 异步 62 | handler(event.args['ret']) 63 | else: 64 | # 同步 65 | self._sync_ret = event.args['ret'] 66 | self._notify_server_data() 67 | except Exception as e: 68 | log.error(e) 69 | log.debug("[RPCClient._process_apiback] 删除已经完成的任务 rid; %s" % rid) 70 | with self._handlers_lock: 71 | del self._handlers[rid] 72 | 73 | def call(self, apiname, args, handler): 74 | """ 给定参数args,异步调用RPCServer的apiname服务, 75 | 返回结果做为回调函数handler的参数。 76 | 77 | Args: 78 | apiname (str): 服务API名称。 79 | args (dict): 给服务API的参数。 80 | handler (function): 回调函数。 81 | """ 82 | if not isinstance(args, dict): 83 | raise InvalidRPCClientArguments(argtype=type(args)) 84 | assert(handler is not None) 85 | log.debug('RPCClient [%s] sync_call: %s' % (self._name, apiname)) 86 | self.rid += 1 87 | args['apiname'] = apiname 88 | args['rid'] = self.rid 89 | self._event_engine.emit(Event(self.EVENT_FROM_CLIENT, args)) 90 | with self._handlers_lock: 91 | self._handlers[self.rid] = handler 92 | 93 | def sync_call(self, apiname, args={}, timeout=5): 94 | """ 给定参数args,同步调用RPCServer的apiname服务, 95 | 返回该服务的处理结果。如果超时,返回None。 96 | 97 | Args: 98 | apiname (str): 服务API名称。 99 | args (dict): 给服务API的参数。 100 | handler (function): 回调函数。 101 | """ 102 | log.debug('RPCClient [%s] sync_call: %s' % (self._name, apiname)) 103 | if not isinstance(args, dict): 104 | self._timeout = 0 105 | self._sync_ret = None 106 | raise InvalidRPCClientArguments(argtype=type(args)) 107 | self.rid += 1 108 | args['apiname'] = apiname 109 | args['rid'] = self.rid 110 | with self._sync_call_time_lock: 111 | self._sync_call_time = datetime.now() 112 | self._timeout = timeout 113 | with self._handlers_lock: 114 | self._handlers[self.rid] = None 115 | self._event_engine.emit(Event(self.EVENT_FROM_CLIENT, args)) 116 | self._waiting_server_data() 117 | ret = self._sync_ret 118 | return ret 119 | 120 | def _waiting_server_data(self): 121 | with self._pause_condition: 122 | self._pause_condition.wait() 123 | 124 | def _notify_server_data(self): 125 | with self._pause_condition: 126 | self._pause_condition.notify() 127 | 128 | 129 | class EventRPCServer(object): 130 | def __init__(self, event_engine, service, event_client=None, event_server=None): 131 | super(EventRPCServer, self).__init__() 132 | self._routes = {} 133 | self._routes_lock = Lock() 134 | # server监听的client事件 135 | self.EVENT_FROM_CLIENT = event_client if event_client else "EVENT_FROM_%s_CLIENT" % service.upper() 136 | # client监听的server事件 137 | self.EVENT_FROM_SERVER = event_server if event_server else "EVENT_FROM_%s_SERVER" % service.upper() 138 | self._event_engine = event_engine 139 | self._event_engine.register(self.EVENT_FROM_CLIENT, self._process_request) 140 | log.info("[Create RPCServer %s]" % self.EVENT_FROM_CLIENT) 141 | self._name = service.upper() 142 | 143 | def register(self, route, handler): 144 | """ 注册服务函数。 145 | 146 | Args: 147 | route (str): 服务名 148 | handler (function): 回调函数 149 | 150 | Returns: 151 | Bool. 是否注册成功。 152 | """ 153 | if route in self._routes: 154 | return False 155 | with self._routes_lock: 156 | self._routes[route] = handler 157 | return True 158 | 159 | def unregister(self, route): 160 | """ 注销服务函数 """ 161 | with self._routes_lock: 162 | if route in self._routes: 163 | del self._routes[route] 164 | 165 | def _process_request(self, event): 166 | args = event.args 167 | rid = args['rid'] 168 | apiname = args['apiname'] 169 | del args['rid'] 170 | del args['apiname'] 171 | log.debug('RPCServer [%s] process: %s' % (self._name, apiname)) 172 | try: 173 | with self._routes_lock: 174 | handler = self._routes[apiname] 175 | # @TODO async 176 | ret = handler(**args) 177 | except Exception as e: 178 | log.exception(e) 179 | else: 180 | args = {'ret': ret, 181 | 'rid': rid 182 | } 183 | log.debug('RPCServer [%s] emit %s' % (self._name, 184 | str(self.EVENT_FROM_SERVER))) 185 | self._event_engine.emit(Event(self.EVENT_FROM_SERVER, args)) 186 | 187 | 188 | if __name__ == '__main__': 189 | 190 | from eventengine import ZMQEventEngine 191 | import sys 192 | 193 | def print_hello(data): 194 | """""" 195 | six.print_("***************") 196 | six.print_("print_hello") 197 | six.print_("args: ", data) 198 | six.print_("return: ", 123) 199 | return "123" 200 | server_engine = ZMQEventEngine('test') 201 | server_engine.start() 202 | server = EventRPCServer(server_engine, 'test') 203 | server.register("print_hello", print_hello) 204 | 205 | try: 206 | while True: 207 | time.sleep(1) 208 | except KeyboardInterrupt: 209 | server_engine.stop() 210 | sys.exit(0) 211 | -------------------------------------------------------------------------------- /tests/data/1DAY/SH/600522.csv: -------------------------------------------------------------------------------- 1 | datetime,open,close,high,low,volume 2 | 2015-01-05 15:00:00,15.32,15.54,15.59,15.09,15339408.0 3 | 2015-01-06 15:00:00,15.43,15.91,15.94,15.29,17749438.0 4 | 2015-01-07 15:00:00,15.84,15.57,15.84,15.4,12607149.0 5 | 2015-01-08 15:00:00,15.57,16.18,16.19,15.5,28891433.0 6 | 2015-01-09 15:00:00,16.19,15.83,16.37,15.79,22396375.0 7 | 2015-01-12 15:00:00,15.69,15.38,15.69,15.3,13209157.0 8 | 2015-01-13 15:00:00,15.39,15.83,16.36,15.29,14717122.0 9 | 2015-01-14 15:00:00,15.81,15.59,15.92,15.49,9972289.0 10 | 2015-01-15 15:00:00,15.56,15.87,15.89,15.55,10921354.0 11 | 2015-01-16 15:00:00,15.91,16.09,16.14,15.81,17486243.0 12 | 2015-01-19 15:00:00,15.69,15.43,16.13,15.22,21432799.0 13 | 2015-01-20 15:00:00,15.62,16.18,16.21,15.47,22034300.0 14 | 2015-01-21 15:00:00,16.21,16.5,16.55,16.11,28948844.0 15 | 2015-01-22 15:00:00,16.51,16.5,16.71,16.25,19104506.0 16 | 2015-01-23 15:00:00,16.51,16.64,16.77,16.31,21348166.0 17 | 2015-01-26 15:00:00,16.62,16.8,16.99,16.62,20245309.0 18 | 2015-01-27 15:00:00,16.77,16.93,17.15,16.44,24698249.0 19 | 2015-01-28 15:00:00,16.89,16.79,17.25,16.7,24996540.0 20 | 2015-01-29 15:00:00,16.59,16.65,16.92,16.52,14628265.0 21 | 2015-01-30 15:00:00,16.65,17.03,17.39,16.65,29669561.0 22 | 2015-02-02 15:00:00,16.91,17.57,17.82,16.79,34767523.0 23 | 2015-02-03 15:00:00,17.57,17.67,17.8,17.41,23612819.0 24 | 2015-02-04 15:00:00,17.89,17.55,18.03,17.45,23837734.0 25 | 2015-02-05 15:00:00,17.74,18.23,18.6,17.62,42581694.0 26 | 2015-02-06 15:00:00,18.19,18.03,18.34,17.97,25108240.0 27 | 2015-02-09 15:00:00,17.97,17.23,18.03,17.14,23112060.0 28 | 2015-02-10 15:00:00,17.24,17.51,17.53,17.08,13059413.0 29 | 2015-02-11 15:00:00,17.53,17.88,17.91,17.4,12269663.0 30 | 2015-02-12 15:00:00,17.99,17.86,18.07,17.69,10430980.0 31 | 2015-02-13 15:00:00,17.88,17.59,18.15,17.54,19941542.0 32 | 2015-02-16 15:00:00,17.59,18.13,18.15,17.32,25358738.0 33 | 2015-02-17 15:00:00,18.15,18.17,18.34,18.0,18456505.0 34 | 2015-02-25 15:00:00,18.17,18.79,19.08,17.98,28118569.0 35 | 2015-02-26 15:00:00,18.69,18.61,18.87,18.54,15784164.0 36 | 2015-02-27 15:00:00,18.66,18.79,19.13,18.58,21560072.0 37 | 2015-03-02 15:00:00,18.79,19.34,19.59,18.74,23258956.0 38 | 2015-03-03 15:00:00,19.23,18.61,19.37,18.52,29457120.0 39 | 2015-03-04 15:00:00,18.69,18.84,18.89,18.51,17168451.0 40 | 2015-03-05 15:00:00,19.08,18.6,19.08,18.49,18594506.0 41 | 2015-03-06 15:00:00,18.6,18.08,18.77,18.01,21839739.0 42 | 2015-03-09 15:00:00,18.6,18.08,18.77,18.01,21839739.0 43 | 2015-05-25 15:00:00,19.9,19.9,19.9,19.9,253467.0 44 | 2015-05-26 15:00:00,21.9,21.9,21.9,21.9,250107.0 45 | 2015-05-27 15:00:00,24.1,24.1,24.1,24.1,146363.0 46 | 2015-05-28 15:00:00,26.52,26.52,26.52,26.52,714854.0 47 | 2015-05-29 15:00:00,29.18,29.18,29.18,28.21,57918844.0 48 | 2015-06-01 15:00:00,31.89,32.11,32.11,30.36,39143186.0 49 | 2015-06-02 15:00:00,34.39,32.3,35.29,31.61,98212024.0 50 | 2015-06-03 15:00:00,32.94,34.39,34.77,32.3,69062248.0 51 | 2015-06-04 15:00:00,33.68,32.51,33.68,31.4,48898504.0 52 | 2015-06-05 15:00:00,32.99,32.44,33.48,31.69,42896019.0 53 | 2015-06-08 15:00:00,32.18,30.94,32.18,30.6,38492383.0 54 | 2015-06-09 15:00:00,30.87,31.84,32.28,30.79,28351409.0 55 | 2015-06-10 15:00:00,31.39,31.46,31.76,30.89,30619376.0 56 | 2015-06-11 15:00:00,31.47,31.72,32.44,31.37,28752190.0 57 | 2015-06-12 15:00:00,32.0,32.83,33.69,31.58,43439997.0 58 | 2015-06-15 15:00:00,32.99,31.98,33.54,31.88,31251788.0 59 | 2015-06-16 15:00:00,31.08,29.01,31.5,28.78,38798310.0 60 | 2015-06-17 15:00:00,29.01,29.42,29.8,27.89,26132626.0 61 | 2015-06-18 15:00:00,29.39,28.59,29.65,28.51,19783361.0 62 | 2015-06-19 15:00:00,27.66,26.16,28.87,25.73,21773883.0 63 | 2015-06-23 15:00:00,25.9,25.93,26.6,24.0,21852862.0 64 | 2015-06-24 15:00:00,26.28,26.88,27.04,26.11,23155253.0 65 | 2015-06-25 15:00:00,27.02,25.51,27.39,25.23,27592143.0 66 | 2015-06-26 15:00:00,24.5,22.96,24.96,22.96,27712562.0 67 | 2015-06-29 15:00:00,23.8,20.66,23.85,20.66,38229751.0 68 | 2015-06-30 15:00:00,21.0,22.73,22.73,19.91,47341209.0 69 | 2015-07-01 15:00:00,22.8,22.93,24.9,22.51,60929155.0 70 | 2015-07-02 15:00:00,23.7,22.5,24.0,22.0,44794350.0 71 | 2015-07-03 15:00:00,22.26,22.64,24.0,20.25,49444231.0 72 | 2015-07-06 15:00:00,24.9,24.11,24.9,21.67,56464370.0 73 | 2015-07-07 15:00:00,23.11,21.7,24.0,21.7,36683839.0 74 | 2015-07-08 15:00:00,19.53,19.53,20.82,19.53,110540058.0 75 | 2015-07-13 15:00:00,21.48,21.48,21.48,21.48,571638.0 76 | 2015-07-14 15:00:00,23.63,23.63,23.63,23.63,856005.0 77 | 2015-07-15 15:00:00,25.99,23.02,25.99,22.03,60009017.0 78 | 2015-07-16 15:00:00,23.02,23.97,24.5,22.1,38661683.0 79 | 2015-07-17 15:00:00,24.1,25.27,25.75,23.97,45903674.0 80 | 2015-07-20 15:00:00,25.45,25.26,26.28,24.78,40490765.0 81 | 2015-07-21 15:00:00,24.79,25.11,25.5,24.52,26719999.0 82 | 2015-07-22 15:00:00,25.2,25.3,25.7,24.58,25484281.0 83 | 2015-07-23 15:00:00,25.44,25.79,25.97,24.82,35786089.0 84 | 2015-07-24 15:00:00,25.7,26.8,27.55,25.69,51020869.0 85 | 2015-07-27 15:00:00,26.15,24.12,26.98,24.12,48136210.0 86 | 2015-07-28 15:00:00,23.63,23.72,24.38,21.81,40228504.0 87 | 2015-07-29 15:00:00,24.18,24.67,24.69,22.35,27041896.0 88 | 2015-07-30 15:00:00,24.49,24.07,25.65,23.93,30314621.0 89 | 2015-07-31 15:00:00,23.66,25.15,25.25,23.65,32411218.0 90 | 2015-08-03 15:00:00,24.83,25.22,25.49,24.39,25212581.0 91 | 2015-08-04 15:00:00,25.4,27.45,27.7,25.25,37046119.0 92 | 2015-08-05 15:00:00,27.09,26.32,27.37,26.02,26473344.0 93 | 2015-08-06 15:00:00,25.78,26.52,27.24,25.5,19934990.0 94 | 2015-08-07 15:00:00,26.88,26.98,27.5,26.55,21541403.0 95 | 2015-08-10 15:00:00,27.0,27.89,28.18,26.7,33839504.0 96 | 2015-08-11 15:00:00,27.5,26.34,27.88,26.0,58248425.0 97 | 2015-08-12 15:00:00,25.7,25.56,26.22,25.5,24824957.0 98 | 2015-08-13 15:00:00,25.48,26.46,26.5,25.48,17472256.0 99 | 2015-08-14 15:00:00,26.55,26.26,26.84,25.9,21652974.0 100 | 2015-08-17 15:00:00,26.0,26.0,26.2,25.38,16288422.0 101 | 2015-08-18 15:00:00,26.18,23.4,26.25,23.4,24770609.0 102 | 2015-08-19 15:00:00,22.99,23.54,23.83,21.36,23156491.0 103 | 2015-08-20 15:00:00,23.7,22.87,24.25,22.82,19080516.0 104 | 2015-08-21 15:00:00,22.5,22.07,23.19,21.65,15391296.0 105 | 2015-08-24 15:00:00,20.88,19.86,21.24,19.86,32592474.0 106 | 2015-08-25 15:00:00,18.59,17.87,19.95,17.87,32265088.0 107 | 2015-08-26 15:00:00,18.5,17.82,19.01,17.55,24546008.0 108 | 2015-08-27 15:00:00,18.48,18.58,18.97,17.82,16971137.0 109 | 2015-08-28 15:00:00,19.06,19.74,19.78,18.58,29083303.0 110 | 2015-08-31 15:00:00,19.3,18.23,19.52,18.19,20648587.0 111 | 2015-09-01 15:00:00,18.1,16.41,18.11,16.41,21540287.0 112 | 2015-09-02 15:00:00,15.45,16.36,16.83,15.3,20506738.0 113 | 2015-09-07 15:00:00,16.74,16.78,17.55,16.73,14368136.0 114 | 2015-09-08 15:00:00,16.79,17.69,17.78,16.5,12688832.0 115 | 2015-09-09 15:00:00,17.89,18.37,18.5,17.61,19262205.0 116 | 2015-09-10 15:00:00,18.0,17.71,18.24,17.7,9843209.0 117 | 2015-09-11 15:00:00,17.68,17.85,18.04,17.4,7250865.0 118 | 2015-09-14 15:00:00,17.89,16.07,18.05,16.07,12149111.0 119 | 2015-09-15 15:00:00,15.6,14.67,16.0,14.57,13424463.0 120 | 2015-09-16 15:00:00,14.89,16.03,16.14,14.78,13691965.0 121 | 2015-09-17 15:00:00,15.9,15.6,16.6,15.3,15004657.0 122 | 2015-09-18 15:00:00,15.7,15.76,15.99,15.36,8393764.0 123 | 2015-09-21 15:00:00,15.77,16.4,16.49,15.52,11839879.0 124 | 2015-09-22 15:00:00,16.48,16.4,16.6,16.02,13330904.0 125 | 2015-09-30 15:00:00,16.75,16.26,16.98,16.13,16286103.0 126 | 2015-10-08 15:00:00,16.79,17.89,17.89,16.6,29061921.0 127 | 2015-10-09 15:00:00,18.0,17.9,18.18,17.68,38592209.0 128 | 2015-10-12 15:00:00,17.95,18.43,19.0,17.69,42678469.0 129 | 2015-10-13 15:00:00,18.3,18.51,18.55,18.02,23663098.0 130 | 2015-10-14 15:00:00,18.38,18.77,19.49,18.1,37461757.0 131 | 2015-10-15 15:00:00,19.2,19.33,19.47,19.1,36206699.0 132 | 2015-10-16 15:00:00,19.39,19.18,19.39,18.79,39821688.0 133 | 2015-10-19 15:00:00,19.22,18.75,19.28,18.6,25589704.0 134 | 2015-10-20 15:00:00,18.75,19.41,19.58,18.7,28741625.0 135 | 2015-10-21 15:00:00,19.81,18.39,20.8,18.3,50541361.0 136 | 2015-10-22 15:00:00,18.68,19.29,19.44,18.5,30137355.0 137 | 2015-10-23 15:00:00,19.49,19.55,19.71,18.99,32861367.0 138 | 2015-10-26 15:00:00,19.83,19.78,20.18,19.31,29106854.0 139 | 2015-10-27 15:00:00,20.02,20.26,20.48,19.08,36896231.0 140 | 2015-10-28 15:00:00,20.18,19.51,20.4,19.38,23729218.0 141 | 2015-10-29 15:00:00,20.36,20.03,20.53,19.91,32768119.0 142 | 2015-10-30 15:00:00,19.88,19.99,20.29,19.58,23438232.0 143 | 2015-11-10 15:00:00,21.13,21.13,21.13,21.13,0.0 144 | --------------------------------------------------------------------------------