├── tests ├── __init__.py ├── strategyease_sdk │ ├── __init__.py │ ├── matchers │ │ ├── __init__.py │ │ └── dataframe_matchers.py │ ├── uqer │ │ ├── __init__.py │ │ ├── test_client.py │ │ └── test_transaction.py │ ├── joinquant │ │ ├── __init__.py │ │ ├── test_client.py │ │ ├── test_transaction.py │ │ └── test_executor.py │ ├── ricequant │ │ ├── __init__.py │ │ ├── test_client.py │ │ └── test_transaction.py │ ├── test_str.py │ ├── test_support.py │ ├── test_dic.py │ ├── test_models.py │ ├── test_config.py │ ├── guorn │ │ └── test_client.py │ ├── test_client_management.py │ ├── test_client.py │ └── test_client_margin_trading.py ├── resources │ └── example-config │ │ ├── .gitignore │ │ ├── a-template.ini │ │ └── b-template.ini ├── config │ ├── sample.ini │ └── config-template.ini ├── sample_data │ ├── rq_client-response.json │ ├── uqer-order-response.json │ └── transactionDetail.json └── test_setup.py ├── examples ├── __init__.py ├── joinquant │ ├── __init__.py │ ├── repo.py │ ├── new_stocks_purchase.py │ ├── convertible_bonds_purchase.py │ ├── simple_strategy.py │ └── advanced_strategy.py ├── ricequant │ ├── __init__.py │ ├── repo.py │ ├── simple_strategy.py │ ├── new_stocks_purchase.py │ ├── convertible_bonds_purchase.py │ └── advanced_strategy.py ├── batch │ └── 2017-05-02.csv └── basic_example.py ├── strategyease_sdk ├── jobs │ ├── __init__.py │ ├── basic_job.py │ ├── new_stock_purchase.py │ ├── convertible_bonds_purchase.py │ ├── repo.py │ ├── batch.py │ ├── online_quant_following.py │ └── online_quant_sync.py ├── uqer │ ├── __init__.py │ ├── transaction.py │ └── client.py ├── guorn │ ├── __init__.py │ └── client.py ├── joinquant │ ├── __init__.py │ ├── transaction.py │ ├── client.py │ └── manager.py ├── ricequant │ ├── __init__.py │ ├── transaction.py │ ├── client.py │ └── manager.py ├── __init__.py ├── ap.py ├── base_quant_client.py ├── market_utils.py ├── stock.py ├── transaction.py ├── support.py ├── scheduler.py ├── client.py ├── models.py └── base_manager.py ├── .gitattributes ├── setup.cfg ├── MANIFEST.in ├── scripts ├── strategyease-scheduler.py ├── dist.bat ├── dist.sh ├── strategyease_sdk_installer.ipynb └── strategyease_sdk_installer.py ├── config ├── logging-template.ini ├── online-quant │ └── research │ │ └── strategyease_sdk_config_template.yaml └── scheduler-template.ini ├── docs ├── scheduler.rst └── online-quant-integration.rst ├── LICENSE.txt ├── .gitignore ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/joinquant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ricequant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategyease_sdk/uqer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf 2 | -------------------------------------------------------------------------------- /strategyease_sdk/guorn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategyease_sdk/joinquant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategyease_sdk/ricequant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/matchers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/uqer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/joinquant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/ricequant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/batch/2017-05-02.csv: -------------------------------------------------------------------------------- 1 | 买卖标志,证券代码,价格,数量 2 | 买入,000001,8.94,100 3 | -------------------------------------------------------------------------------- /tests/resources/example-config/.gitignore: -------------------------------------------------------------------------------- 1 | *.ini 2 | !*-template.ini 3 | -------------------------------------------------------------------------------- /tests/resources/example-config/a-template.ini: -------------------------------------------------------------------------------- 1 | [section1] 2 | key1=value1 3 | -------------------------------------------------------------------------------- /tests/resources/example-config/b-template.ini: -------------------------------------------------------------------------------- 1 | [section2] 2 | key2=value2 3 | -------------------------------------------------------------------------------- /strategyease_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .client import Client 4 | -------------------------------------------------------------------------------- /tests/config/sample.ini: -------------------------------------------------------------------------------- 1 | [section1] 2 | key1=value1 3 | key2= 4 | value21 5 | value22 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | 4 | # Include the config files 5 | recursive-include config * 6 | -------------------------------------------------------------------------------- /scripts/strategyease-scheduler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import strategyease_sdk.scheduler 4 | 5 | strategyease_sdk.scheduler.start() 6 | -------------------------------------------------------------------------------- /scripts/dist.bat: -------------------------------------------------------------------------------- 1 | set CWD=%cd% 2 | set SCRIPT_DIR=%~dp0 3 | set HOME_DIR=%SCRIPT_DIR%.. 4 | 5 | cd %HOME_DIR% 6 | del /s /q dist\* 7 | python setup.py sdist 8 | python setup.py bdist_wheel --universal 9 | twine upload dist\* 10 | cd %CWD% 11 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_str.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import unittest 6 | 7 | 8 | class StrTest(unittest.TestCase): 9 | def test_str(self): 10 | print(str(u'证券代码'.encode('utf-8'))) 11 | -------------------------------------------------------------------------------- /examples/basic_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | import strategyease_sdk 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | client = strategyease_sdk.Client(host='localhost', port=8888, key='') 10 | account_info = client.get_account('title:monijiaoyi') 11 | print(account_info) 12 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from strategyease_sdk.support import Struct 6 | 7 | 8 | class StructTest(unittest.TestCase): 9 | def test_init(self): 10 | config = Struct({"key": "value"}) 11 | self.assertEquals(config.key, "value") 12 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | CWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | SCRIPT_DIR=$(dirname "$0") 3 | HOME_DIR=$SCRIPT_DIR/.. 4 | 5 | cd $HOME_DIR 6 | rm -rf dist/* 7 | python3 setup.py sdist 8 | python3 setup.py bdist_wheel --universal 9 | twine upload --repository-url https://upload.pypi.org/legacy/ dist/* 10 | cd $CWD 11 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_dic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | 6 | class DictTest(unittest.TestCase): 7 | def test_modify_in_loop(self): 8 | data = dict( 9 | a=dict() 10 | ) 11 | for key, value in data.items(): 12 | value['b'] = 1 13 | print(data) 14 | -------------------------------------------------------------------------------- /tests/sample_data/rq_client-response.json: -------------------------------------------------------------------------------- 1 | {"code": 200, "resp": {"name": "SVM大法好", 2 | "trades": [{"order_book_id": "600216.XSHG", 3 | "price": 12.77, 4 | "quantity": -100.0, 5 | "time": "2016-12-23 09:32:00", 6 | "trade_id": "2", 7 | "transaction_cost": 6.28}]}} 8 | -------------------------------------------------------------------------------- /tests/config/config-template.ini: -------------------------------------------------------------------------------- 1 | [StrategyEase] 2 | host=localhost 3 | port=8888 4 | key= 5 | client= 6 | 7 | [JoinQuant] 8 | username= 9 | password= 10 | backtest_id= 11 | arena_id= 12 | 13 | [RiceQuant] 14 | username= 15 | password= 16 | backtestId= 17 | 18 | [Uqer] 19 | username= 20 | password= 21 | strategy= 22 | 23 | [Guorn] 24 | username= 25 | password= 26 | sid= 27 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from strategyease_sdk.models import * 6 | 7 | 8 | class OrderTest(unittest.TestCase): 9 | def test_str(self): 10 | order = Order(action=OrderAction.OPEN, security='000001', amount=100, price=10.1, style=OrderStyle.LIMIT) 11 | print(order) 12 | order.price = 417.48 13 | print(order) 14 | -------------------------------------------------------------------------------- /strategyease_sdk/ap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from apscheduler.triggers.cron import CronTrigger 4 | 5 | 6 | class APCronParser(object): 7 | @classmethod 8 | def parse(cls, expression): 9 | parts = list(reversed(expression.split())) 10 | for i in range(len(parts)): 11 | if parts[i] == '?': 12 | parts[i] = None 13 | 14 | return CronTrigger(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7]) 15 | -------------------------------------------------------------------------------- /examples/joinquant/repo.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:用于回测没有意义,需挂到“我的交易” 6 | 7 | def initialize(context): 8 | # 每天的收盘前5分钟进行逆回购,参数设置见:https://www.joinquant.com/api#定时运行 9 | run_daily(repo, '14:55') 10 | 11 | 12 | def process_initialize(context): 13 | # 创建 StrategyManager 对象 14 | # 参数为配置文件中的 manager id 15 | g.__manager = strategyease_sdk.JoinQuantStrategyManagerFactory(context).create('manager-1') 16 | 17 | 18 | def repo(context): 19 | g.__manager.repo() 20 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import codecs 6 | import unittest 7 | 8 | from strategyease_sdk.support import * 9 | 10 | 11 | class ConfigTest(unittest.TestCase): 12 | def test_parse(self): 13 | filename = "../../config/joinquant/research/strategyease_sdk_config_template.yaml" 14 | with codecs.open(filename, encoding="utf_8_sig") as stream: 15 | config = yaml.load(stream, Loader=OrderedDictYAMLLoader) 16 | print(config) 17 | -------------------------------------------------------------------------------- /examples/joinquant/new_stocks_purchase.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:SDK内部取今日时间来获取新股数据 6 | # 注意:用于回测没有意义,需挂到“我的交易” 7 | 8 | def initialize(context): 9 | # 每天的开市后10分钟进行新股申购,参数设置见:https://www.joinquant.com/api#定时运行 10 | run_daily(purchase_new_stocks, '9:40') 11 | 12 | 13 | def process_initialize(context): 14 | # 创建 StrategyManager 对象 15 | # 参数为配置文件中的 manager id 16 | g.__manager = strategyease_sdk.JoinQuantStrategyManagerFactory(context).create('manager-1') 17 | 18 | 19 | def purchase_new_stocks(context): 20 | g.__manager.purchase_new_stocks() 21 | -------------------------------------------------------------------------------- /config/logging-template.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=stdout,file 6 | 7 | [formatters] 8 | keys=default 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=stdout,file 13 | 14 | [handler_stdout] 15 | class=StreamHandler 16 | level=NOTSET 17 | formatter=default 18 | args=(sys.stdout,) 19 | 20 | [handler_file] 21 | class=strategyease_sdk.scheduler.FileHandler 22 | interval=midnight 23 | backupCount=10 24 | formatter=default 25 | level=DEBUG 26 | args=('scheduler.log',) 27 | 28 | [formatter_default] 29 | format=%(asctime)-15s %(levelname)-7s %(message)s 30 | datefmt= 31 | class=logging.Formatter 32 | -------------------------------------------------------------------------------- /examples/joinquant/convertible_bonds_purchase.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:SDK内部取今日时间来获取新股数据 6 | # 注意:用于回测没有意义,需挂到“我的交易” 7 | 8 | def initialize(context): 9 | # 每天的开市后10分钟进行新股申购,参数设置见:https://www.joinquant.com/api#定时运行 10 | run_daily(purchase_convertible_bonds, '9:40') 11 | 12 | 13 | def process_initialize(context): 14 | # 创建 StrategyManager 对象 15 | # 参数为配置文件中的 manager id 16 | g.__manager = strategyease_sdk.JoinQuantStrategyManagerFactory(context).create('manager-1') 17 | 18 | 19 | def purchase_convertible_bonds(context): 20 | g.__manager.purchase_convertible_bonds() 21 | -------------------------------------------------------------------------------- /examples/ricequant/repo.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:用于回测没有意义,需挂到“我的交易” 6 | 7 | def initialize(context): 8 | # 每天的收盘前5分钟进行逆回购。参数设置见:https://www.ricequant.com/api/python/chn#scheduler 9 | scheduler.run_daily(repo, time_rule=market_close(minute=5)) 10 | 11 | 12 | def process_initialize(context): 13 | # 创建 RiceQuantStrategyManagerFactory 对象 14 | # 参数为 strategyease_sdk_config_template.yaml 中配置的 manager id 15 | context.__manager = strategyease_sdk.RiceQuantStrategyManagerFactory(context).create('manager-1') 16 | 17 | 18 | def repo(context): 19 | context.__manager.repo() 20 | -------------------------------------------------------------------------------- /strategyease_sdk/base_quant_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import date, timedelta, datetime 4 | 5 | 6 | class BaseQuantClient(object): 7 | def __init__(self, name): 8 | self._name = name 9 | self._last_login_time = datetime.now() - timedelta(1) 10 | 11 | @property 12 | def name(self): 13 | return self._name 14 | 15 | def login(self): 16 | self._last_login_time = datetime.now() 17 | 18 | def is_login(self): 19 | return self._last_login_time >= datetime.combine(date.today(), datetime.min.time()) 20 | 21 | def query(self): 22 | return [] 23 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/basic_job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | 6 | class BasicJob(object): 7 | def __init__(self, name=None, schedule=None, is_enabled=False): 8 | self._logger = logging.getLogger() 9 | self._name = name 10 | self._schedule = schedule 11 | self._is_enabled = is_enabled 12 | 13 | def __call__(self): 14 | pass 15 | 16 | @property 17 | def name(self): 18 | return self._name 19 | 20 | @property 21 | def schedule(self): 22 | return self._schedule 23 | 24 | @property 25 | def is_enabled(self): 26 | return self._is_enabled 27 | -------------------------------------------------------------------------------- /strategyease_sdk/uqer/transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | from strategyease_sdk.transaction import Transaction 6 | 7 | 8 | class UqerTransaction(object): 9 | def __init__(self, json): 10 | self.__dict__.update(json) 11 | 12 | def normalize(self): 13 | transaction = Transaction() 14 | transaction.completed_at = datetime.fromtimestamp(self.place_time / 1000.0) 15 | transaction.action = self.side 16 | transaction.symbol = self.ticker 17 | transaction.price = self.execution_avg_price 18 | transaction.amount = abs(self.amount) 19 | return transaction 20 | -------------------------------------------------------------------------------- /examples/ricequant/simple_strategy.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | def init(context): 5 | context.s1 = "000001.XSHE" 6 | 7 | def before_trading(context): 8 | # 创建 RiceQuantStrategyManagerFactory 对象 9 | # 参数为 strategyease_sdk_config_template.yaml 中配置的 manager id 10 | context.__manager = strategyease_sdk.RiceQuantStrategyManagerFactory(context).create('manager-1') 11 | 12 | def handle_bar(context, bar_dict): 13 | # 保存 order 14 | order_ = order_shares(context.s1, 100) 15 | # 策略易依据 order_ 下单 16 | context.__manager.execute(order_) 17 | 18 | order_ = order_shares(context.s1, -100) 19 | context.__manager.execute(order_) 20 | 21 | cancel_order(order_) 22 | context.__manager.cancel(order_) 23 | -------------------------------------------------------------------------------- /examples/ricequant/new_stocks_purchase.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:SDK内部取今日时间来获取新股数据 6 | # 注意:用于回测没有意义,需挂到“模拟交易” 7 | 8 | # 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。 9 | def init(context): 10 | # 每天的开市后10分钟进行新股申购。参数设置见:https://www.ricequant.com/api/python/chn#scheduler 11 | scheduler.run_daily(purchase_new_stocks, time_rule=market_open(minute=10)) 12 | 13 | 14 | def before_trading(context): 15 | # 创建 RiceQuantStrategyManagerFactory 对象 16 | # 参数为 strategyease_sdk_config_template.yaml 中配置的 manager id 17 | context.__manager = strategyease_sdk.RiceQuantStrategyManagerFactory(context).create('manager-1') 18 | 19 | 20 | def purchase_new_stocks(context, bar_dict): 21 | context.__manager.purchase_new_stocks() 22 | -------------------------------------------------------------------------------- /strategyease_sdk/ricequant/transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | from strategyease_sdk.transaction import Transaction 6 | 7 | 8 | class RiceQuantTransaction(object): 9 | def __init__(self, json): 10 | self.__dict__.update(json) 11 | 12 | def normalize(self): 13 | transaction = Transaction() 14 | transaction.completed_at = datetime.strptime(self.time, '%Y-%m-%d %H:%M:%S') 15 | transaction.action = 'BUY' if self.quantity > 0 else 'SELL' 16 | transaction.symbol = self.order_book_id 17 | transaction.type = 'LIMIT' 18 | transaction.priceType = 0 19 | transaction.price = self.price 20 | transaction.amount = abs(self.quantity) 21 | return transaction 22 | -------------------------------------------------------------------------------- /examples/ricequant/convertible_bonds_purchase.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 注意:需将回测调成分钟级别 5 | # 注意:SDK内部取今日时间来获取新股数据 6 | # 注意:用于回测没有意义,需挂到“模拟交易” 7 | 8 | # 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。 9 | def init(context): 10 | # 每天的开市后10分钟进行新股申购。参数设置见:https://www.ricequant.com/api/python/chn#scheduler 11 | scheduler.run_daily(purchase_convertible_bonds, time_rule=market_open(minute=10)) 12 | 13 | 14 | def before_trading(context): 15 | # 创建 RiceQuantStrategyManagerFactory 对象 16 | # 参数为 strategyease_sdk_config_template.yaml 中配置的 manager id 17 | context.__manager = strategyease_sdk.RiceQuantStrategyManagerFactory(context).create('manager-1') 18 | 19 | 20 | def purchase_convertible_bonds(context, bar_dict): 21 | context.__manager.purchase_convertible_bonds() 22 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/new_stock_purchase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from strategyease_sdk.jobs.basic_job import BasicJob 4 | 5 | 6 | class NewStockPurchaseJob(BasicJob): 7 | def __init__(self, client, client_aliases=None, name=None, **kwargs): 8 | super(NewStockPurchaseJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 9 | 10 | self._client = client 11 | self._client_aliases = client_aliases 12 | 13 | def __call__(self): 14 | for client_alias in self._client_aliases: 15 | try: 16 | client = self._client_aliases[client_alias] 17 | self._client.purchase_new_stocks(client) 18 | except: 19 | self._logger.exception('客户端[%s]打新失败', client_alias) 20 | -------------------------------------------------------------------------------- /examples/ricequant/advanced_strategy.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | def init(context): 5 | context.s1 = "000001.XSHE" 6 | 7 | def before_trading(context): 8 | # 创建 RiceQuantStrategyManagerFactory 对象 9 | # 参数为 strategyease_sdk_config_template.yaml 中配置的 manager id 10 | context.__manager = strategyease_sdk.RiceQuantStrategyManagerFactory(context).create('manager-1') 11 | 12 | def handle_bar(context, bar_dict): 13 | try: 14 | order_target_value(context.s1, 0) 15 | order_target_value(context.s1, 500) 16 | 17 | order_ = order_shares(context.s1, 100, style=LimitOrder(bar_dict[context.s1].limit_down)) 18 | cancel_order(order_) 19 | finally: 20 | # 放在 finally 块中,以防原有代码抛出异常或者 return 21 | # 在函数结尾处加入以下语句,用来将模拟盘同步至实盘 22 | context.__manager.work() 23 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/convertible_bonds_purchase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from strategyease_sdk.jobs.basic_job import BasicJob 4 | 5 | 6 | class ConvertibleBondsPurchaseJob(BasicJob): 7 | def __init__(self, client, client_aliases=None, name=None, **kwargs): 8 | super(ConvertibleBondsPurchaseJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 9 | 10 | self._client = client 11 | self._client_aliases = client_aliases 12 | 13 | def __call__(self): 14 | for client_alias in self._client_aliases: 15 | try: 16 | client = self._client_aliases[client_alias] 17 | self._client.purchase_convertible_bonds(client) 18 | except: 19 | self._logger.exception('客户端[%s]申购转债失败', client_alias) 20 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/guorn/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import codecs 4 | import os 5 | import unittest 6 | 7 | from six.moves import configparser 8 | 9 | from strategyease_sdk.guorn.client import GuornClient 10 | 11 | ConfigParser = configparser.RawConfigParser 12 | 13 | 14 | class GuornClientTest(unittest.TestCase): 15 | def setUp(self): 16 | config = ConfigParser() 17 | dir_path = os.path.dirname(os.path.realpath(__file__)) 18 | config.readfp(codecs.open('{}/../../config/config.ini'.format(dir_path), encoding="utf_8_sig")) 19 | self._guorn_client = GuornClient(**dict(config.items('Guorn'))) 20 | self._guorn_client.login() 21 | 22 | def test_query_portfolio(self): 23 | portfolio = self._guorn_client.query_portfolio() 24 | print(portfolio) 25 | -------------------------------------------------------------------------------- /examples/joinquant/simple_strategy.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 初始化函数,设定要操作的股票、基准等等 5 | def initialize(context): 6 | # 定义一个全局变量, 保存要操作的股票 7 | # 000001(股票:平安银行) 8 | g.security = '000001.XSHE' 9 | # 设定沪深300作为基准 10 | set_benchmark('000300.XSHG') 11 | 12 | 13 | def process_initialize(context): 14 | # 创建 StrategyManager 对象 15 | # 参数为配置文件中的 manager id 16 | g.__manager = strategyease_sdk.JoinQuantStrategyManagerFactory(context).create('manager-1') 17 | 18 | 19 | # 每个单位时间(如果按天回测,则每天调用一次,如果按分钟,则每分钟调用一次)调用一次 20 | def handle_data(context, data): 21 | # 保存 order 对象 22 | order_ = order(g.security, 100) 23 | # 策略易依据聚宽的 order 对象下单 24 | g.__manager.execute(order_) 25 | 26 | order_ = order(g.security, -100) 27 | g.__manager.execute(order_) 28 | 29 | # 撤单 30 | g.__manager.cancel(order_) 31 | -------------------------------------------------------------------------------- /tests/sample_data/uqer-order-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "amount": 100, 4 | "execution_avg_price": 9.42, 5 | "execution_time": "09:30:07", 6 | "filled_amount": 100.0, 7 | "id": 10955228, 8 | "mark": "[J149I]:\u8be5\u8ba2\u5355\u5df2\u5168\u90e8\u6210\u4ea4!", 9 | "name": "\u5e73\u5b89\u94f6\u884c", 10 | "place_time": 1487035806610, 11 | "side": "BUY", 12 | "status": "\u5168\u90e8\u6210\u4ea4", 13 | "ticker": "000001" 14 | }, 15 | { 16 | "amount": 100, 17 | "execution_avg_price": 9.41, 18 | "execution_time": "09:30:07", 19 | "filled_amount": 100.0, 20 | "id": 10955229, 21 | "mark": "[J149I]:\u8be5\u8ba2\u5355\u5df2\u5168\u90e8\u6210\u4ea4!", 22 | "name": "\u5e73\u5b89\u94f6\u884c", 23 | "place_time": 1487035806652, 24 | "side": "SELL", 25 | "status": "\u5168\u90e8\u6210\u4ea4", 26 | "ticker": "000001" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/uqer/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import codecs 3 | import os 4 | import unittest 5 | 6 | import six 7 | from six.moves import configparser 8 | 9 | if six.PY2: 10 | ConfigParser = configparser.RawConfigParser 11 | else: 12 | ConfigParser = configparser.ConfigParser 13 | 14 | from strategyease_sdk.uqer.client import UqerClient 15 | 16 | 17 | class UqerClientTest(unittest.TestCase): 18 | def setUp(self): 19 | config = ConfigParser() 20 | dir_path = os.path.dirname(os.path.realpath(__file__)) 21 | config.readfp(codecs.open('{}/../../config/config.ini'.format(dir_path), encoding="utf_8_sig")) 22 | self._uq_client = UqerClient(**dict(config.items('Uqer'))) 23 | 24 | def test_query(self): 25 | self._uq_client.login() 26 | transactions = self._uq_client.query() 27 | self.assertTrue(isinstance(transactions, list)) 28 | -------------------------------------------------------------------------------- /strategyease_sdk/market_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | from datetime import time 5 | 6 | 7 | class MarketUtils(object): 8 | OPEN_TIME = time(9, 30) 9 | CLOSE_TIME = time(15) 10 | MIDDAY_CLOSE_TIME = time(11, 30) 11 | MIDDAY_OPEN_TIME = time(13) 12 | 13 | @classmethod 14 | def is_opening(cls, datetime_=None): 15 | if datetime_ is None: 16 | datetime_ = datetime.now() 17 | 18 | if datetime_.isoweekday() not in range(1, 6): 19 | return False 20 | if datetime_.time() <= cls.OPEN_TIME or datetime_.time() >= cls.CLOSE_TIME: 21 | return False 22 | if cls.MIDDAY_CLOSE_TIME <= datetime_.time() <= cls.MIDDAY_OPEN_TIME: 23 | return False 24 | return True 25 | 26 | @classmethod 27 | def is_closed(cls, datetime_=None): 28 | return not cls.is_opening(datetime_) 29 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/ricequant/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import codecs 3 | import os 4 | import unittest 5 | 6 | import six 7 | from six.moves import configparser 8 | 9 | if six.PY2: 10 | ConfigParser = configparser.RawConfigParser 11 | else: 12 | ConfigParser = configparser.ConfigParser 13 | 14 | from strategyease_sdk.ricequant.client import RiceQuantClient 15 | 16 | 17 | class RiceQuantClientTest(unittest.TestCase): 18 | def setUp(self): 19 | config = ConfigParser() 20 | dir_path = os.path.dirname(os.path.realpath(__file__)) 21 | config.readfp(codecs.open('{}/../../config/config.ini'.format(dir_path), encoding="utf_8_sig")) 22 | self._rq_client = RiceQuantClient(**dict(config.items('RiceQuant'))) 23 | 24 | def test_query(self): 25 | self._rq_client.login() 26 | transactions = self._rq_client.query() 27 | self.assertTrue(isinstance(transactions, list)) 28 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import importlib.util 4 | import os 5 | import unittest 6 | 7 | 8 | class ConfigInstantiatorTest(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | pass 12 | 13 | def test_config_instantiation(self): 14 | setup_module_file_location = os.path.join(os.path.dirname(__file__), '..', 'setup.py') 15 | spec = importlib.util.spec_from_file_location("setup", setup_module_file_location) 16 | setup = importlib.util.module_from_spec(spec) 17 | spec.loader.exec_module(setup) 18 | 19 | path = os.path.join(os.path.dirname(__file__), 'resources', 'example-config') 20 | concrete_file_path = os.path.join(path, 'b.ini') 21 | if os.path.isfile(concrete_file_path): 22 | os.remove(concrete_file_path) 23 | setup.ConfigInstantiator.instantiate(path) 24 | self.assertTrue(os.path.isfile(concrete_file_path)) 25 | -------------------------------------------------------------------------------- /examples/joinquant/advanced_strategy.py: -------------------------------------------------------------------------------- 1 | import strategyease_sdk 2 | 3 | 4 | # 初始化函数,设定要操作的股票、基准等等 5 | def initialize(context): 6 | # 定义一个全局变量, 保存要操作的股票 7 | # 000001(股票:平安银行) 8 | g.security = '000001.XSHE' 9 | # 设定沪深300作为基准 10 | set_benchmark('000300.XSHG') 11 | 12 | 13 | def process_initialize(context): 14 | # 创建 StrategyManager 对象 15 | # 参数为配置文件中的 manager id 16 | g.__manager = strategyease_sdk.JoinQuantStrategyManagerFactory(context).create('manager-1') 17 | 18 | 19 | # 每个单位时间(如果按天回测,则每天调用一次,如果按分钟,则每分钟调用一次)调用一次 20 | def handle_data(context, data): 21 | try: 22 | order_target(g.security, 0) 23 | order_target(g.security, 100) 24 | 25 | current_data = get_current_data() 26 | order_ = order(g.security, 100, LimitOrderStyle(current_data[g.security].low_limit)) 27 | cancel_order(order_) 28 | finally: 29 | # 放在 finally 块中,以防原有代码抛出异常或者 return 30 | # 在函数结尾处加入以下语句,用来将模拟盘同步、跟单至实盘 31 | g.__manager.work() 32 | -------------------------------------------------------------------------------- /docs/scheduler.rst: -------------------------------------------------------------------------------- 1 | 定时任务调度 2 | ======================= 3 | 4 | .. contents:: **目录** 5 | 6 | 功能列表 7 | -------------- 8 | 9 | - 多账号自动申购新股(自动打新) 10 | - 多账号自动申购转债 11 | - 多账号自动逆回购 12 | - 定时批量下单 13 | - 聚宽(JoinQuant) 14 | 15 | - 自动跟单模拟交易(抓取方式) 16 | - 自动同步擂台策略(抓取方式) 17 | 18 | - 米筐(RiceQuant)自动跟单(抓取方式) 19 | - 优矿(Uqer)自动跟单(抓取方式) 20 | 21 | Windows 22 | ~~~~~~~ 23 | 24 | 配置 25 | ^^^^ 26 | 27 | - cmd 中运行::code:`explorer %UserProfile%\.strategyease_sdk\config` 28 | - 修改 scheduler.ini 中的配置(建议使用Notepad++) 29 | 30 | 运行 31 | ^^^^ 32 | 33 | - cmd 下运行::code:`strategyease-scheduler` 34 | 35 | 升级 36 | ^^^^ 37 | 38 | - 参考 scheduler-template.ini 修改 scheduler.ini 39 | 40 | 日志 41 | ^^^^ 42 | 43 | - cmd 中运行::code:`explorer %UserProfile%\AppData\Local\爱股网\策略易` 44 | 45 | Mac/Linux 46 | ~~~~~~~~~ 47 | 48 | 配置 49 | ^^^^ 50 | 51 | - 修改 ~/.strategyease_sdk/config/scheduler.ini 52 | 53 | 运行 54 | ^^^^ 55 | 56 | - terminal 中运行::code:`strategyease-scheduler:code:` 57 | 58 | 升级 59 | ^^^^ 60 | 61 | - 参考 scheduler-template.ini 修改 scheduler.ini 62 | -------------------------------------------------------------------------------- /strategyease_sdk/stock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import lxml.html.soupparser 4 | import pandas as pd 5 | import requests 6 | 7 | 8 | class StockUtils(object): 9 | @staticmethod 10 | def new_stocks(): 11 | url = 'http://vip.stock.finance.sina.com.cn/corp/view/vRPD_NewStockIssue.php?page=1&cngem=0&orderBy=NetDate&orderType=desc' 12 | request = requests.get(url) 13 | doc = lxml.html.soupparser.fromstring(request.content, features='html.parser') 14 | table = doc.cssselect('table#NewStockTable')[0] 15 | table.remove(table.cssselect('thead')[0]) 16 | table_html = lxml.html.etree.tostring(table).decode('utf-8') 17 | df = pd.read_html(table_html, skiprows=[0, 1])[0] 18 | df = df.select(lambda x: x in [0, 1, 2, 3, 7], axis=1) 19 | df.columns = ['code', 'xcode', 'name', 'ipo_date', 'price'] 20 | df['code'] = df['code'].map(lambda x: str(x).zfill(6)) 21 | df['xcode'] = df['xcode'].map(lambda x: str(x).zfill(6)) 22 | return df 23 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/ricequant/test_transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | import unittest 5 | from datetime import datetime 6 | 7 | from strategyease_sdk.ricequant.transaction import RiceQuantTransaction 8 | 9 | 10 | class TransactionTest(unittest.TestCase): 11 | def setUp(self): 12 | dir_path = os.path.dirname(os.path.realpath(__file__)) 13 | with open('{}/../../sample_data/rq_client-response.json'.format(dir_path), encoding='utf_8_sig') as data_file: 14 | self._transaction_detail = json.loads(data_file.read()) 15 | 16 | def test_from_raw(self): 17 | rq_transaction = RiceQuantTransaction(self._transaction_detail['resp']['trades'][0]) 18 | transaction = rq_transaction.normalize() 19 | self.assertEqual(transaction.completed_at, datetime.strptime('2016-12-23 09:32:00', '%Y-%m-%d %H:%M:%S')) 20 | self.assertEqual(transaction.action, 'SELL') 21 | self.assertEqual(transaction.symbol, '600216.XSHG') 22 | self.assertEqual(transaction.price, 12.77) 23 | self.assertEqual(transaction.amount, 100) 24 | -------------------------------------------------------------------------------- /strategyease_sdk/joinquant/transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from datetime import datetime 5 | 6 | from strategyease_sdk.transaction import Transaction 7 | 8 | 9 | class JoinQuantTransaction(object): 10 | def __init__(self, json): 11 | self.__dict__.update(json) 12 | 13 | def normalize(self): 14 | transaction = Transaction() 15 | transaction.completed_at = datetime.strptime('{} {}'.format(self.date, (self.time + ':00')[:8]), '%Y-%m-%d %H:%M:%S') 16 | transaction.action = 'BUY' if self.transaction == u'买' else 'SELL' 17 | transaction.symbol = re.search(".*\\((\\d+)\\..*\\)", self.stock).group(1) 18 | transaction.type = 'LIMIT' if self.type == u'限价单' else 'MARKET' 19 | if transaction.type == 'LIMIT': 20 | transaction.priceType = 0 21 | transaction.price = self.limitPrice 22 | else: 23 | transaction.priceType = 4 24 | transaction.price = self.price 25 | transaction.amount = int(re.search(u".*>[-]*(\\d+).*", self.orderAmount, re.UNICODE).group(1)) 26 | return transaction 27 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/repo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tushare as ts 4 | 5 | from strategyease_sdk.jobs.basic_job import BasicJob 6 | 7 | 8 | class RepoJob(BasicJob): 9 | def __init__(self, client, client_aliases=None, name=None, **kwargs): 10 | super(RepoJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 11 | 12 | self._client = client 13 | self._client_aliases = client_aliases 14 | self._symbol = kwargs.get('security', '131810') 15 | 16 | def __call__(self): 17 | df = ts.get_realtime_quotes(self._symbol) 18 | order = { 19 | 'action': 'SELL', 20 | 'symbol': self._symbol, 21 | 'type': 'LIMIT', 22 | 'price': float(df['bid'][0]), 23 | 'amountProportion': 'ALL' 24 | } 25 | for client_alias in self._client_aliases: 26 | try: 27 | client = self._client_aliases[client_alias] 28 | self._client.execute(client, **order) 29 | except: 30 | self._logger.exception('客户端[%s]逆回购失败', client_alias) 31 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/joinquant/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import codecs 3 | import os 4 | import unittest 5 | 6 | import six 7 | from six.moves import configparser 8 | 9 | if six.PY2: 10 | ConfigParser = configparser.RawConfigParser 11 | else: 12 | ConfigParser = configparser.ConfigParser 13 | 14 | from strategyease_sdk.joinquant.client import JoinQuantClient 15 | 16 | 17 | class JoinQuantClientTest(unittest.TestCase): 18 | def setUp(self): 19 | config = ConfigParser() 20 | dir_path = os.path.dirname(os.path.realpath(__file__)) 21 | config.readfp(codecs.open('{}/../../config/config.ini'.format(dir_path), encoding="utf_8_sig")) 22 | self._jq_client = JoinQuantClient(**dict(config.items('JoinQuant'))) 23 | 24 | def test_query(self): 25 | self._jq_client.login() 26 | transactions = self._jq_client.query() 27 | self.assertTrue(isinstance(transactions, list)) 28 | 29 | def test_query_portfolio(self): 30 | self._jq_client.login() 31 | portfolio = self._jq_client.query_portfolio() 32 | self.assertIsNotNone(portfolio) 33 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/uqer/test_transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | import unittest 5 | from datetime import datetime 6 | 7 | from strategyease_sdk.uqer.transaction import UqerTransaction 8 | 9 | 10 | class TransactionTest(unittest.TestCase): 11 | def setUp(self): 12 | dir_path = os.path.dirname(os.path.realpath(__file__)) 13 | with open('{}/../../sample_data/uqer-order-response.json'.format(dir_path), 'r', 14 | encoding='utf_8_sig') as data_file: 15 | self._transaction_detail = json.loads(data_file.read()) 16 | 17 | def test_from_raw(self): 18 | uq_transaction = UqerTransaction(self._transaction_detail[0]) 19 | transaction = uq_transaction.normalize() 20 | self.assertEqual(transaction.completed_at, 21 | datetime.strptime('2017-02-14 09:30:06.610000', '%Y-%m-%d %H:%M:%S.%f')) 22 | self.assertEqual(transaction.action, 'BUY') 23 | self.assertEqual(transaction.symbol, '000001') 24 | self.assertEqual(transaction.price, 9.42) 25 | self.assertEqual(transaction.amount, 100) 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 sinall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /strategyease_sdk/ricequant/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from rqopen_client import RQOpenClient 4 | 5 | from strategyease_sdk.base_quant_client import BaseQuantClient 6 | from strategyease_sdk.ricequant.transaction import RiceQuantTransaction 7 | 8 | 9 | class RiceQuantClient(BaseQuantClient): 10 | def __init__(self, **kwargs): 11 | super(RiceQuantClient, self).__init__('RiceQuant') 12 | 13 | self._rq_client = RQOpenClient(kwargs.get('username', None), kwargs.get('password', None), 14 | timeout=kwargs.pop('timeout', (5.0, 10.0))) 15 | self._run_id = kwargs.get('run_id', None) 16 | 17 | def login(self): 18 | self._rq_client.login() 19 | super(RiceQuantClient, self).login() 20 | 21 | def query(self): 22 | response = self._rq_client.get_day_trades(self._run_id) 23 | raw_transactions = response['resp']['trades'] 24 | transactions = [] 25 | for raw_transaction in raw_transactions: 26 | transaction = RiceQuantTransaction(raw_transaction).normalize() 27 | transactions.append(transaction) 28 | 29 | return transactions 30 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_client_management.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import os 5 | import unittest 6 | 7 | import six 8 | from requests import HTTPError 9 | from six.moves import configparser 10 | 11 | from strategyease_sdk import Client 12 | 13 | if six.PY2: 14 | ConfigParser = configparser.RawConfigParser 15 | else: 16 | ConfigParser = configparser.ConfigParser 17 | 18 | 19 | class ClientManagementTest(unittest.TestCase): 20 | def setUp(self): 21 | logging.basicConfig(level=logging.DEBUG) 22 | config = ConfigParser() 23 | dir_path = os.path.dirname(os.path.realpath(__file__)) 24 | config.read('{}/../config/config.ini'.format(dir_path)) 25 | self.client = Client(logging.getLogger(), host=config.get('StrategyEase', 'host'), key=config.get('StrategyEase', 'key')) 26 | 27 | def test_start_clients(self): 28 | try: 29 | self.client.start_clients() 30 | except HTTPError as e: 31 | self.fail() 32 | 33 | def test_shutdown_clients(self): 34 | try: 35 | self.client.shutdown_clients() 36 | except HTTPError as e: 37 | self.fail() 38 | -------------------------------------------------------------------------------- /scripts/strategyease_sdk_installer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "\n", 11 | "from six.moves import reload_module\n", 12 | "from six.moves import urllib\n", 13 | "\n", 14 | "# 在线量化平台名称,可选:joinquant、ricequant、uqer\n", 15 | "QUANT_NAME = 'joinquant'\n", 16 | "# 版本,默认 master 即为最新版\n", 17 | "VERSION = 'master'\n", 18 | "# 更新安装文件?\n", 19 | "UPDATE_INSTALLER = False\n", 20 | "\n", 21 | "REPO_URL = 'https://raw.githubusercontent.com/sinall/StrategyEase-Python-SDK'\n", 22 | "INSTALLER_URL = '{}/{}/scripts/strategyease_sdk_installer.py'.format(REPO_URL, VERSION)\n", 23 | "INSTALLER_FILENAME = 'strategyease_sdk_installer.py'\n", 24 | "\n", 25 | "if not os.path.isfile(INSTALLER_FILENAME) or UPDATE_INSTALLER:\n", 26 | " urllib.request.urlretrieve(INSTALLER_URL, INSTALLER_FILENAME)\n", 27 | "\n", 28 | "import strategyease_sdk_installer\n", 29 | "\n", 30 | "reload_module(strategyease_sdk_installer)\n", 31 | "\n", 32 | "strategyease_sdk_installer.main([\n", 33 | " '--quant=' + QUANT_NAME,\n", 34 | " '--version=' + VERSION\n", 35 | "])" 36 | ] 37 | } 38 | ], 39 | "metadata": {}, 40 | "nbformat": 4, 41 | "nbformat_minor": 0 42 | } -------------------------------------------------------------------------------- /strategyease_sdk/jobs/batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import csv 4 | from datetime import date 5 | 6 | from strategyease_sdk.jobs.basic_job import BasicJob 7 | 8 | 9 | class BatchJob(BasicJob): 10 | def __init__(self, client, client_aliases=None, name=None, **kwargs): 11 | super(BatchJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 12 | 13 | self._client = client 14 | self._client_aliases = client_aliases 15 | self._folder = kwargs.get('folder') 16 | 17 | def __call__(self): 18 | file_name = '{}/{}.csv'.format(self._folder, date.today().isoformat()) 19 | with open(file_name, encoding='utf-8-sig') as file: 20 | reader = csv.DictReader(file) 21 | for client_alias in self._client_aliases: 22 | client = self._client_aliases[client_alias] 23 | for row in reader: 24 | order = { 25 | 'action': 'BUY' if row[u'买卖标志'] == u'买入' else 'SELL', 26 | 'symbol': row[u'证券代码'], 27 | 'type': 'LIMIT', 28 | 'price': float(row[u'价格']), 29 | 'amount': int(row[u'数量']) 30 | } 31 | try: 32 | self._client.execute(client, **order) 33 | except Exception as e: 34 | self._logger.exception("下单异常") 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .idea/ 92 | tests/config/config.ini 93 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/matchers/dataframe_matchers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | from hamcrest.core.base_matcher import BaseMatcher 6 | 7 | 8 | class HasColumn(BaseMatcher): 9 | def __init__(self, column): 10 | self._column = column 11 | 12 | def _matches(self, df): 13 | return self._column in df.columns 14 | 15 | def describe_to(self, description): 16 | description.append_text(u'Dataframe doesn\'t have colum [{0}]'.format(self._column)) 17 | 18 | 19 | def has_column(column): 20 | return HasColumn(column) 21 | 22 | 23 | class HasColumnMatches(BaseMatcher): 24 | def __init__(self, column_pattern): 25 | self._column_pattern = re.compile(column_pattern) 26 | 27 | def _matches(self, df): 28 | return len(list(filter(self._column_pattern.match, df.columns.values))) > 0 29 | 30 | def describe_to(self, description): 31 | description.append_text(u'Dataframe doesn\'t have colum matches [{0}]'.format(self._column_pattern)) 32 | 33 | 34 | def has_column_matches(column_pattern): 35 | return HasColumnMatches(column_pattern) 36 | 37 | 38 | class HasRow(BaseMatcher): 39 | def __init__(self, row): 40 | self._row = row 41 | 42 | def _matches(self, df): 43 | return self._row in df.index 44 | 45 | def describe_to(self, description): 46 | description.append_text(u'Dataframe doesn\'t have row [%s]'.format(self._row)) 47 | 48 | 49 | def has_row(row): 50 | return HasRow(row) 51 | -------------------------------------------------------------------------------- /tests/sample_data/transactionDetail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "transaction": [ 4 | { 5 | "date": "2017-06-01", 6 | "time": "14:55", 7 | "security": "股票", 8 | "stock": "平安银行(000001.XSHE)", 9 | "transaction": "买", 10 | "type": "市价单", 11 | "amount": "100股<\/span>", 12 | "price": 9.19, 13 | "total": 919, 14 | "gains": 0, 15 | "commission": 5, 16 | "status": "全部成交", 17 | "orderAmount": "100股<\/span>", 18 | "limitPrice": "-", 19 | "trueOrderAmount": "100股<\/span>", 20 | "trueAmount": "100股<\/span>", 21 | "trueLimitPrice": 0, 22 | "truePrice": 9.19 23 | }, 24 | { 25 | "date": "2017-06-01", 26 | "time": "14:55:00", 27 | "security": "股票", 28 | "stock": "万科A(000002.XSHE)", 29 | "transaction": "买", 30 | "type": "限价单", 31 | "amount": "0股<\/span>", 32 | "price": "--", 33 | "total": 0, 34 | "gains": 0, 35 | "commission": 0, 36 | "status": "打开", 37 | "orderAmount": "100股<\/span>", 38 | "limitPrice": 19.13, 39 | "trueOrderAmount": "100股<\/span>", 40 | "trueAmount": "0股<\/span>", 41 | "trueLimitPrice": 19.13, 42 | "truePrice": 0 43 | } 44 | ] 45 | }, 46 | "status": "0", 47 | "code": "00000", 48 | "msg": "" 49 | } 50 | -------------------------------------------------------------------------------- /strategyease_sdk/transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Transaction(object): 5 | def __init__(self, **kwargs): 6 | self._completed_at = kwargs.get('completed_at') 7 | self._action = kwargs.get('action') 8 | self._symbol = kwargs.get('symbol') 9 | self._price = kwargs.get('price') 10 | self._amount = kwargs.get('amount') 11 | 12 | def __eq__(self, other): 13 | if self.completed_at != other.completed_at: 14 | return False 15 | if self.action != other.action: 16 | return False 17 | if self.symbol != other.symbol: 18 | return False 19 | if self.price != other.price: 20 | return False 21 | if self.amount != other.amount: 22 | return False 23 | return True 24 | 25 | def get_cn_action(self): 26 | return u'买入' if self.action == 'BUY' else u'卖出' 27 | 28 | @property 29 | def completed_at(self): 30 | return self._completed_at 31 | 32 | @completed_at.setter 33 | def completed_at(self, value): 34 | self._completed_at = value 35 | 36 | @property 37 | def action(self): 38 | return self._action 39 | 40 | @action.setter 41 | def action(self, value): 42 | self._action = value 43 | 44 | @property 45 | def symbol(self): 46 | return self._symbol 47 | 48 | @symbol.setter 49 | def symbol(self, value): 50 | self._symbol = value 51 | 52 | @property 53 | def price(self): 54 | return self._price 55 | 56 | @price.setter 57 | def price(self, value): 58 | self._price = value 59 | 60 | @property 61 | def amount(self): 62 | return self._amount 63 | 64 | @amount.setter 65 | def amount(self, value): 66 | self._amount = value 67 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/joinquant/test_transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | import unittest 5 | from datetime import datetime 6 | 7 | from strategyease_sdk.joinquant.transaction import JoinQuantTransaction 8 | 9 | 10 | class TransactionTest(unittest.TestCase): 11 | def setUp(self): 12 | dir_path = os.path.dirname(os.path.realpath(__file__)) 13 | with open('{}/../../sample_data/transactionDetail.json'.format(dir_path), encoding='utf_8_sig') as data_file: 14 | self._transaction_detail = json.loads(data_file.read()) 15 | 16 | def test_from_raw(self): 17 | jq_transaction1 = JoinQuantTransaction(self._transaction_detail['data']['transaction'][0]) 18 | transaction1 = jq_transaction1.normalize() 19 | self.assertEqual(transaction1.completed_at, datetime.strptime('2017-06-01 14:55', '%Y-%m-%d %H:%M')) 20 | self.assertEqual(transaction1.action, 'BUY') 21 | self.assertEqual(transaction1.symbol, '000001') 22 | self.assertEqual(transaction1.type, 'MARKET') 23 | self.assertEqual(transaction1.priceType, 4) 24 | self.assertEqual(transaction1.price, 9.19) 25 | self.assertEqual(transaction1.amount, 100) 26 | 27 | jq_transaction2 = JoinQuantTransaction(self._transaction_detail['data']['transaction'][1]) 28 | transaction2 = jq_transaction2.normalize() 29 | self.assertEqual(transaction2.completed_at, datetime.strptime('2017-06-01 14:55', '%Y-%m-%d %H:%M')) 30 | self.assertEqual(transaction2.action, 'BUY') 31 | self.assertEqual(transaction2.symbol, '000002') 32 | self.assertEqual(transaction2.type, 'LIMIT') 33 | self.assertEqual(transaction2.priceType, 0) 34 | self.assertEqual(transaction2.price, 19.13) 35 | self.assertEqual(transaction2.amount, 100) 36 | -------------------------------------------------------------------------------- /strategyease_sdk/uqer/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from strategyease_sdk.base_quant_client import BaseQuantClient 8 | from strategyease_sdk.uqer.transaction import UqerTransaction 9 | 10 | 11 | class UqerClient(BaseQuantClient): 12 | BASE_URL = 'https://gw.datayes.com' 13 | 14 | def __init__(self, **kwargs): 15 | super(UqerClient, self).__init__('Uqer') 16 | 17 | self._session = requests.Session() 18 | self._username = kwargs.get('username', None) 19 | self._password = kwargs.get('password', None) 20 | self._strategy = kwargs.get('strategy', None) 21 | self._timeout = kwargs.pop('timeout', (5.0, 10.0)) 22 | 23 | def login(self): 24 | self._session.headers = { 25 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 26 | 'Accept-Encoding': 'gzip, deflate, br', 27 | 'Accept-Language': 'en-US,en;q=0.8', 28 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', 29 | 'Referer': '{}/user/login/index'.format(self.BASE_URL), 30 | 'X-Requested-With': 'XMLHttpRequest', 31 | 'Origin': self.BASE_URL, 32 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 33 | } 34 | response = self._session.post('{}/usermaster/authenticate/v1.json'.format(self.BASE_URL), data={ 35 | 'username': self._username, 36 | 'password': self._password, 37 | 'rememberMe': 'false' 38 | }, timeout=self._timeout) 39 | self._session.headers.update({ 40 | 'cookie': response.headers['Set-Cookie'] 41 | }) 42 | 43 | super(UqerClient, self).login() 44 | 45 | def query(self): 46 | today_str = datetime.today().strftime('%Y-%m-%d') 47 | response = self._session.get('{}/mercury_trade/strategy/{}/order'.format(self.BASE_URL, self._strategy), 48 | params={ 49 | 'date': today_str, 50 | }, timeout=self._timeout) 51 | raw_transactions = response.json() 52 | transactions = [] 53 | for raw_transaction in raw_transactions: 54 | transaction = UqerTransaction(raw_transaction).normalize() 55 | transactions.append(transaction) 56 | 57 | return transactions 58 | -------------------------------------------------------------------------------- /strategyease_sdk/support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | from collections import OrderedDict 5 | 6 | import yaml 7 | 8 | 9 | class Struct(object): 10 | def __init__(self, data): 11 | for key, value in data.items(): 12 | key = key.replace('-', '_') 13 | if isinstance(value, tuple): 14 | setattr(self, key, (Struct(x) if isinstance(x, dict) else x for x in value)) 15 | if isinstance(value, list): 16 | setattr(self, key, [Struct(x) if isinstance(x, dict) else x for x in value]) 17 | else: 18 | setattr(self, key, Struct(value) if isinstance(value, dict) else value) 19 | 20 | 21 | class OrderedDictYAMLLoader(yaml.Loader): 22 | def __init__(self, *args, **kwargs): 23 | yaml.Loader.__init__(self, *args, **kwargs) 24 | self.add_constructor(u'tag:yaml.org,2002:omap', type(self).construct_odict) 25 | 26 | def construct_odict(self, node): 27 | omap = OrderedDict() 28 | yield omap 29 | if not isinstance(node, yaml.SequenceNode): 30 | raise yaml.constructor.ConstructorError( 31 | "while constructing an ordered map", 32 | node.start_mark, 33 | "expected a sequence, but found %s" % node.id, node.start_mark 34 | ) 35 | for subnode in node.value: 36 | if not isinstance(subnode, yaml.MappingNode): 37 | raise yaml.constructor.ConstructorError( 38 | "while constructing an ordered map", node.start_mark, 39 | "expected a mapping of length 1, but found %s" % subnode.id, 40 | subnode.start_mark 41 | ) 42 | if len(subnode.value) != 1: 43 | raise yaml.constructor.ConstructorError( 44 | "while constructing an ordered map", node.start_mark, 45 | "expected a single mapping item, but found %d items" % len(subnode.value), 46 | subnode.start_mark 47 | ) 48 | key_node, value_node = subnode.value[0] 49 | key = self.construct_object(key_node) 50 | value = self.construct_object(value_node) 51 | omap[key] = value 52 | 53 | 54 | class StopWatch(object): 55 | def __init__(self): 56 | pass 57 | 58 | def start(self): 59 | self._start_time = datetime.datetime.now() 60 | 61 | def stop(self): 62 | self._end_time = datetime.datetime.now() 63 | return self 64 | 65 | def short_summary(self): 66 | return str(self._end_time - self._start_time) 67 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/joinquant/test_executor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import collections 4 | import datetime 5 | import inspect 6 | import logging 7 | import os 8 | import unittest 9 | 10 | import six 11 | from six.moves import configparser 12 | 13 | if six.PY2: 14 | ConfigParser = configparser.RawConfigParser 15 | else: 16 | ConfigParser = configparser.ConfigParser 17 | 18 | from strategyease_sdk import JoinQuantExecutor 19 | 20 | 21 | @unittest.skip("integration class") 22 | class JoinQuantExecutorTest(unittest.TestCase): 23 | def setUp(self): 24 | logging.basicConfig(level=logging.INFO) 25 | 26 | config = ConfigParser() 27 | dir_path = os.path.dirname(os.path.realpath(__file__)) 28 | config.read('{}/../../config/config.ini'.format(dir_path)) 29 | self.Order = collections.namedtuple('Order', ['is_buy', 'security', 'price', 'amount']) 30 | self.executor = JoinQuantExecutor(host=config.get('StrategyEase', 'host')) 31 | 32 | def test_buy_stock(self): 33 | mock_order = self.Order 34 | mock_order.is_buy = True 35 | mock_order.order_id = 1 36 | mock_order.security = '000001.XSHE' 37 | mock_order.price = 11.11 38 | mock_order.amount = 100 39 | mock_order.limit = 11.11 40 | mock_order.add_time = datetime.datetime.now() 41 | response = self.executor.execute(mock_order) 42 | print(inspect.stack()[0][3] + ' - ' + response.text) 43 | json = response.json(); 44 | if response.status_code == 200: 45 | self.assertTrue(json['id']) 46 | elif response.status_code == 400: 47 | self.assertTrue(json['message']) 48 | else: 49 | self.fail() 50 | 51 | def test_sell_stock(self): 52 | mock_order = self.Order 53 | mock_order.is_buy = False 54 | mock_order.order_id = 2 55 | mock_order.security = '000001.XSHE' 56 | mock_order.price = 11.11 57 | mock_order.amount = 100 58 | mock_order.limit = 11.11 59 | mock_order.add_time = datetime.datetime.now() 60 | response = self.executor.execute(mock_order) 61 | print(inspect.stack()[0][3] + ' - ' + response.text) 62 | json = response.json(); 63 | if response.status_code == 200: 64 | self.assertTrue(json['id']) 65 | elif response.status_code == 400: 66 | self.assertTrue(json['message']) 67 | else: 68 | self.fail() 69 | 70 | def test_cancel(self): 71 | mock_order = self.Order 72 | mock_order.is_buy = True 73 | mock_order.order_id = 3 74 | mock_order.security = '000001.XSHE' 75 | mock_order.price = 9.01 76 | mock_order.amount = 100 77 | mock_order.limit = 9.01 78 | mock_order.add_time = datetime.datetime.now() 79 | self.executor.execute(mock_order) 80 | 81 | response = self.executor.cancel(mock_order) 82 | print(inspect.stack()[0][3] + ' - ' + response.text) 83 | if response.status_code != 200: 84 | self.fail() 85 | -------------------------------------------------------------------------------- /strategyease_sdk/guorn/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | 5 | import pandas as pd 6 | import requests 7 | 8 | from strategyease_sdk.base_quant_client import BaseQuantClient 9 | from strategyease_sdk.models import * 10 | 11 | 12 | class GuornClient(BaseQuantClient): 13 | BASE_URL = 'https://guorn.com' 14 | 15 | def __init__(self, **kwargs): 16 | super(GuornClient, self).__init__('Guorn') 17 | 18 | self._session = requests.Session() 19 | self._username = kwargs.get('username', None) 20 | self._password = kwargs.get('password', None) 21 | self._sid = kwargs.get('sid', None) 22 | self._timeout = kwargs.pop('timeout', (5.0, 10.0)) 23 | 24 | def login(self): 25 | self._session.headers = { 26 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 27 | 'Accept-Encoding': 'gzip, deflate, br', 28 | 'Accept-Language': 'en-US,en;q=0.8', 29 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', 30 | 'Referer': '{}'.format(self.BASE_URL), 31 | 'X-Requested-With': 'XMLHttpRequest', 32 | 'Origin': self.BASE_URL, 33 | 'Content-Type': 'application/json; charset=UTF-8', 34 | } 35 | self._session.get(self.BASE_URL, timeout=self._timeout) 36 | response = self._session.post('{}/user/login'.format(self.BASE_URL), json={ 37 | 'account': self._username, 38 | 'passwd': self._password, 39 | 'keep_login': 'true' 40 | }, timeout=self._timeout) 41 | self._session.headers.update({ 42 | 'cookie': response.headers['Set-Cookie'] 43 | }) 44 | 45 | super(GuornClient, self).login() 46 | 47 | def query_portfolio(self): 48 | response = self._session.get('{}/stock/instruction'.format(self.BASE_URL), params={ 49 | 'fmt': 'json', 50 | 'amount': 1000000, 51 | 'sid': self._sid, 52 | '_': time.time() 53 | }, timeout=self._timeout) 54 | instruction = response.json() 55 | 56 | status = instruction['status'] 57 | data = instruction['data'] 58 | if status == 'failed': 59 | if isinstance(data, str): 60 | raise Exception(data) 61 | raise Exception("获取调仓指令数据失败") 62 | 63 | df = pd.DataFrame() 64 | sheet_data = instruction['data']['sheet_data'] 65 | if sheet_data is not None: 66 | for row in sheet_data['row']: 67 | df[row['name']] = pd.Series(row['data'][1]) 68 | meas_data = sheet_data['meas_data'] 69 | for index, col in enumerate(sheet_data['col']): 70 | df[col['name']] = pd.Series(meas_data[index]) 71 | 72 | portfolio = Portfolio(total_value=1.0) 73 | for index, row in df.iterrows(): 74 | security = row.get(u'股票代码') or row.get(u'基金代码') 75 | value = row[u'目标仓位'] 76 | price = row[u'参考价'] 77 | amount = value / price 78 | position = Position(security, price, amount, amount) 79 | portfolio.add_position(position) 80 | portfolio.rebalance() 81 | 82 | return portfolio 83 | -------------------------------------------------------------------------------- /docs/online-quant-integration.rst: -------------------------------------------------------------------------------- 1 | 策略集成 2 | ================== 3 | 4 | .. contents:: **目录** 5 | 6 | 聚宽(JoinQuant)集成 7 | --------------------- 8 | 9 | 一. 推送方式 10 | ~~~~~~~~~~~~ 11 | 12 | 适用于云服务器环境,例如阿里云;特点是稳定、高效,集成简单。 13 | 14 | 准备工作 15 | ^^^^^^^^ 16 | 17 | - 部署策略易。 18 | - 本地测试通过。 19 | - 远程测试通过。 20 | 21 | 步骤 22 | ^^^^ 23 | 24 | - 下载 `scripts/strategyease_sdk_installer.ipynb`_ 并上传至“投资研究”根目录。 25 | - 打开该文件,设置参数:QUANT_NAME = 'joinquant' 26 | - 查看其它参数并根据需要进行修改。 27 | - 点击工具栏中的右箭头运行该文件,并检查窗口中打印的日志。 28 | - 修改 strategyease_sdk_config.yaml,升级后需参考 strategyease_sdk_config_template.yaml 进行修改。 29 | - 修改策略代码,可参考如下示例: 30 | 31 | - examples/joinquant/simple\_strategy.py - 基本跟单用法(侵入式设计,不推荐) 32 | - examples/joinquant/advanced\_strategy.py - 高级同步、跟单用法(非侵入式设计,推荐) 33 | - examples/joinquant/new\_stocks\_purchase.py - 新股申购 34 | - examples/joinquant/convertible\_bonds\_purchase.py - 转债申购 35 | - examples/joinquant/repo.py - 逆回购 36 | 37 | 同步操作注意事项: 38 | 39 | - 同步操作根据模拟盘持仓比例对实盘进行调整。 40 | - 同步操作依赖于“可用”资金。请留意配置文件中“撤销全部订单”相关选项。 41 | - “新股申购”不影响“可用”资金,并且不可被撤销,因此不影响同步功能。 42 | - 同步操作依赖于策略易 API /adjustments;因此也依赖于“查询投资组合”API,使用前请先做好测试及配置。 43 | - 同步操作使用“市价单”。 44 | - 如遇到策略报错“ImportError: No module named strategyease_sdk”,请稍后重试。 45 | - 量化平台模拟交易运行中升级 SDK,需重启生效。 46 | 47 | 二. 抓取方式 48 | ~~~~~~~~~~~~ 49 | 50 | 无需云服务器,采用定时轮询的方式,实时性不如"推送方式"。 51 | 52 | 准备工作 53 | ^^^^^^^^ 54 | 55 | - 部署策略易。 56 | - 测试通过。 57 | 58 | 步骤 59 | ^^^^ 60 | 61 | 见 `定时任务调度 <#定时任务调度>`__ 62 | 63 | 米筐(RiceQuant)集成 64 | --------------------- 65 | 66 | 一. 推送方式 67 | ~~~~~~~~~~~~ 68 | 69 | 适用于云服务器环境,例如阿里云;特点是稳定、高效,集成简单。 70 | 71 | 准备工作 72 | ^^^^^^^^ 73 | 74 | - 部署策略易。 75 | - 本地测试通过。 76 | - 远程测试通过。 77 | 78 | 步骤 79 | ^^^^ 80 | 81 | - 下载 `scripts/strategyease_sdk_installer.ipynb`_ 并上传至“策略研究”根目录。 82 | - 打开该文件,设置参数:QUANT_NAME = 'ricequant' 83 | - 查看其它参数并根据需要进行修改。 84 | - 点击工具栏中的右箭头运行该文件,并检查窗口中打印的日志。 85 | - 修改策略代码,可参考如下示例: 86 | 87 | - examples/ricequant/simple\_strategy.py - 基本用法 88 | - examples/ricequant/advanced\_strategy.py - 高级同步用法(非侵入式设计,推荐) 89 | - examples/ricequant/new\_stocks\_purchase.py - 新股申购 90 | - examples/ricequant/convertible\_bonds\_purchase.py - 转债申购 91 | - examples/ricequant/repo.py - 逆回购 92 | 93 | 二. 抓取方式 94 | ~~~~~~~~~~~~ 95 | 96 | 采用定时轮询的方式。 97 | 98 | 准备工作 99 | ^^^^^^^^ 100 | 101 | - 部署策略易。 102 | - 测试通过。 103 | 104 | 步骤 105 | ^^^^ 106 | 107 | 见 `定时任务调度 <#定时任务调度>`__ 108 | 109 | 优矿(Uqer)集成 110 | --------------------- 111 | 112 | 一. 推送方式 113 | ~~~~~~~~~~~~ 114 | 115 | | 适用于云服务器环境,例如阿里云;特点是稳定、高效,集成简单。 116 | | 开发中,暂不支持。 117 | 118 | 二. 抓取方式 119 | ~~~~~~~~~~~~ 120 | 121 | 采用定时轮询的方式。 122 | 123 | 准备工作 124 | ^^^^^^^^ 125 | 126 | - 部署策略易。 127 | - 测试通过。 128 | 129 | 步骤 130 | ^^^^ 131 | 132 | 见 `定时任务调度 <#定时任务调度>`__ 133 | 134 | 果仁(Guorn)集成 135 | --------------------- 136 | 137 | 一. 推送方式 138 | ~~~~~~~~~~~~ 139 | 140 | | 不支持。 141 | 142 | 二. 抓取方式 143 | ~~~~~~~~~~~~ 144 | 145 | 采用定时轮询的方式。 146 | 147 | 准备工作 148 | ^^^^^^^^ 149 | 150 | - 部署策略易。 151 | - 测试通过。 152 | 153 | 步骤 154 | ^^^^ 155 | 156 | 见 `定时任务调度 <#定时任务调度>`__ 157 | 158 | 字段要求 159 | ^^^^^^^^ 160 | 161 | 见策略易《用户手册.txt》的“查询投资组合”章节,可通过策略易菜单“帮助>查看帮助”访问。 162 | 163 | 164 | .. _scripts/strategyease_sdk_installer.ipynb: https://raw.githubusercontent.com/sinall/StrategyEase-Python-SDK/master/scripts/strategyease_sdk_installer.ipynb 165 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/online_quant_following.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | from requests import HTTPError 6 | 7 | from strategyease_sdk.jobs.basic_job import BasicJob 8 | from strategyease_sdk.market_utils import MarketUtils 9 | 10 | 11 | class OnlineQuantFollowingJob(BasicJob): 12 | def __init__(self, strategyease_client, quant_client, client_aliases=None, name=None, **kwargs): 13 | super(OnlineQuantFollowingJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 14 | 15 | self._strategyease_client = strategyease_client 16 | self._quant_client = quant_client 17 | self._client_aliases = client_aliases 18 | self._name = name 19 | self._start_datatime = datetime.now() 20 | self._processed_transactions = [] 21 | 22 | def __call__(self): 23 | if MarketUtils.is_closed(): 24 | self._logger.warning("********** 休市期间不跟单 **********") 25 | if self._processed_transactions: 26 | del self._processed_transactions[:] 27 | return 28 | 29 | if not self._quant_client.is_login(): 30 | self._logger.info("登录 %s", self._quant_client.name) 31 | self._quant_client.login() 32 | 33 | self._logger.info("********** 开始跟单 **********") 34 | try: 35 | all_transactions = self._quant_client.query() 36 | self._logger.info("获取到 %d 条委托", len(all_transactions)) 37 | 38 | transactions = [] 39 | for transaction in all_transactions: 40 | if self._is_expired(transaction): 41 | continue 42 | transactions.append(transaction) 43 | self._logger.info("获取到 %d 条有效委托", len(transactions)) 44 | 45 | for client_alias in self._client_aliases: 46 | client = self._client_aliases[client_alias] 47 | for tx in transactions: 48 | try: 49 | self._processed_transactions.append(tx) 50 | self._logger.info("开始在[%s(%s)]以 %f元 %s %d股 %s", 51 | client_alias, client, tx.price, tx.get_cn_action(), tx.amount, tx.symbol) 52 | self._strategyease_client.execute(client, 53 | action=tx.action, 54 | symbol=tx.symbol, 55 | type=tx.type, 56 | priceType=tx.priceType, 57 | price=tx.price, 58 | amount=tx.amount) 59 | except HTTPError as e: 60 | self._logger.exception("下单异常") 61 | except Exception as e: 62 | self._logger.exception("跟单异常") 63 | self._logger.info("********** 结束跟单 **********\n") 64 | 65 | @property 66 | def name(self): 67 | return self._name 68 | 69 | def _is_expired(self, transaction): 70 | if transaction.completed_at < self._start_datatime: 71 | return True 72 | if transaction in self._processed_transactions: 73 | return True 74 | return False 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | import shutil 6 | from codecs import open 7 | 8 | from setuptools import setup, find_packages 9 | from setuptools.command.install import install 10 | 11 | 12 | class CustomInstallCommand(install): 13 | def run(self): 14 | install.run(self) 15 | config_path = os.path.join(os.path.expanduser('~'), '.strategyease_sdk', 'config') 16 | ConfigInstantiator.instantiate(config_path) 17 | 18 | 19 | class ConfigInstantiator: 20 | @staticmethod 21 | def instantiate(path): 22 | for filename in os.listdir(path): 23 | match = re.search("(.*)-template\\.(.*)", filename) 24 | if match is None: 25 | continue 26 | concrete_filename = '{}.{}'.format(match.group(1), match.group(2)) 27 | template_file_path = os.path.join(path, filename) 28 | concrete_file_path = os.path.join(path, concrete_filename) 29 | if os.path.isfile(concrete_file_path): 30 | continue 31 | shutil.copyfile(template_file_path, concrete_file_path) 32 | 33 | 34 | def main(): 35 | here = os.path.abspath(os.path.dirname(__file__)) 36 | 37 | with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 38 | long_description = f.read() 39 | 40 | setup( 41 | name='strategyease_sdk', 42 | 43 | version='2.1.1', 44 | 45 | description=u'策略易(StrategyEase)Python SDK,策略自动化交易 API。', 46 | long_description=long_description, 47 | 48 | url='https://github.com/sinall/StrategyEase-Python-SDK', 49 | 50 | author='sinall', 51 | author_email='gaoruinan@163.com', 52 | 53 | license='MIT', 54 | 55 | classifiers=[ 56 | 'Development Status :: 3 - Alpha', 57 | 58 | 'Intended Audience :: Developers', 59 | 'Intended Audience :: Financial and Insurance Industry', 60 | 'Topic :: Office/Business :: Financial :: Investment', 61 | 62 | 'License :: OSI Approved :: MIT License', 63 | 64 | 'Programming Language :: Python :: 2.7', 65 | 'Programming Language :: Python :: 3', 66 | 'Programming Language :: Python :: 3.3', 67 | 'Programming Language :: Python :: 3.4', 68 | 'Programming Language :: Python :: 3.5', 69 | 'Programming Language :: Python :: 3.6', 70 | 'Programming Language :: Python :: 3.7', 71 | ], 72 | 73 | keywords='StrategyEase SDK', 74 | 75 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 76 | 77 | install_requires=['requests', 'six', 'apscheduler', 'lxml', 'cssselect', 'bs4', 'html5lib', 'pandas', 78 | 'tushare', 'pyyaml'], 79 | 80 | extras_require={ 81 | 'dev': [], 82 | 'test': [], 83 | }, 84 | 85 | package_data={ 86 | }, 87 | 88 | data_files=[(os.path.join(os.path.expanduser('~'), '.strategyease_sdk', 'config'), [ 89 | 'config/scheduler-template.ini', 90 | 'config/logging-template.ini', 91 | ])], 92 | 93 | scripts=['scripts/strategyease-scheduler.py'], 94 | 95 | entry_points={ 96 | 'console_scripts': [ 97 | 'strategyease-scheduler = strategyease_sdk.scheduler:start', 98 | ], 99 | }, 100 | 101 | cmdclass={ 102 | 'install': CustomInstallCommand, 103 | }, 104 | ) 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | StrategyEase-Python-SDK 2 | ======================= 3 | 4 | 策略易(StrategyEase)Python SDK。 5 | 6 | | 策略易是\ `爱股网 `__\ 旗下的策略自动化解决方案;提供基于 HTTP 协议的 RESTFul Service,并管理交易客户端。 7 | | 详情见:http://www.iguuu.com/e 8 | | 交流QQ群:115279569 |策略交流| 9 | | 10 | 11 | .. contents:: **目录** 12 | 13 | 原理概述 14 | -------- 15 | - 策略易通过调用 WINDOWS API 对交易客户端进行操作。 16 | - 策略易提供基于 HTTP 协议的 RESTFul Service/API。 17 | - SDK 对 API 进行了封装(由 strategyease_sdk/client.py 中的 Client 类实现)。 18 | - 本地策略或量化交易平台(目前支持聚宽、米筐、优矿)的模拟交易通过调用 SDK 实现自动下单。 19 | 20 | 功能介绍 21 | -------- 22 | 23 | - 简单的策略易 HTTP API 封装,见 strategyease_sdk/client.py 24 | - 定时任务 25 | 26 | - 多账号自动新股申购(自动打新) 27 | - 多账号自动逆回购 28 | - 定时批量下单 29 | 30 | - 策略集成 31 | 32 | - 聚宽(JoinQuant)集成 33 | - 米筐(RiceQuant)集成 34 | - 优矿(Uqer)集成 35 | - 果仁(Guorn)集成 36 | 37 | 安装 38 | -------------- 39 | 40 | - 安装 Python 3.5(建议安装 `Anaconda3-4.2.0 `_) 41 | - 命令行中运行 42 | 43 | +--------+-------------------------------------------------------------------------+ 44 | | 正式版 | :code:`pip install --no-binary strategyease_sdk strategyease_sdk` | 45 | +--------+-------------------------------------------------------------------------+ 46 | | 测试版 | :code:`pip install --pre --no-binary strategyease_sdk strategyease_sdk` | 47 | +--------+-------------------------------------------------------------------------+ 48 | 49 | 升级 50 | -------------- 51 | 52 | - 命令行中运行 53 | 54 | +--------+---------------------------------------------------------------------------------------------+ 55 | | 正式版 | :code:`pip install --upgrade --no-deps --no-binary strategyease_sdk strategyease_sdk` | 56 | +--------+---------------------------------------------------------------------------------------------+ 57 | | 测试版 | :code:`pip install --upgrade --pre --no-deps --no-binary strategyease_sdk strategyease_sdk` | 58 | +--------+---------------------------------------------------------------------------------------------+ 59 | 60 | 基本用法 61 | -------------- 62 | 63 | .. code:: python 64 | 65 | import logging 66 | 67 | import strategyease_sdk 68 | 69 | logging.basicConfig(level=logging.DEBUG) 70 | 71 | client = strategyease_sdk.Client(host='localhost', port=8888, key='') 72 | account_info = client.get_account('title:monijiaoyi') 73 | print(account_info) 74 | 75 | 详见:examples/basic_example.py 76 | 77 | 测试用例 78 | -------------- 79 | 80 | 策略易 HTTP API 封装对应的测试用例见: 81 | 82 | +------------+------------------------------------------------------+ 83 | | 查询及下单 | tests/strategyease_sdk/test_client.py | 84 | +------------+------------------------------------------------------+ 85 | | 客户端管理 | tests/strategyease_sdk/test_client_management.py | 86 | +------------+------------------------------------------------------+ 87 | | 融资融券 | tests/strategyease_sdk/test_client_margin_trading.py | 88 | +------------+------------------------------------------------------+ 89 | | 其他 | tests/strategyease_sdk/... | 90 | +------------+------------------------------------------------------+ 91 | 92 | 定时任务调度 93 | -------------- 94 | 见《`定时任务调度说明 `_》 95 | 96 | 策略集成 97 | --------------------- 98 | 见《`策略集成说明 `_》 99 | 100 | 其他语言 SDK 101 | ------------ 102 | 103 | C# SDK 104 | ~~~~~~ 105 | 106 | | 由网友 @YBO(QQ:259219140)开发。 107 | | 见 `ShiPanETradingSDK `_ 108 | 109 | .. |策略交流| image:: http://pub.idqqimg.com/wpa/images/group.png 110 | :target: http://shang.qq.com/wpa/qunwpa?idkey=1ce867356702f5f7c56d07d5c694e37a3b9a523efce199bb0f6ff30410c6185d%22 111 | -------------------------------------------------------------------------------- /strategyease_sdk/joinquant/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | import requests 6 | from bs4 import BeautifulSoup 7 | 8 | from strategyease_sdk.base_quant_client import BaseQuantClient 9 | from strategyease_sdk.joinquant.transaction import JoinQuantTransaction 10 | from strategyease_sdk.models import Portfolio, Position 11 | 12 | 13 | class JoinQuantClient(BaseQuantClient): 14 | BASE_URL = 'https://www.joinquant.com' 15 | 16 | def __init__(self, **kwargs): 17 | super(JoinQuantClient, self).__init__('JoinQuant') 18 | 19 | self._session = requests.Session() 20 | self._username = kwargs.get('username', None) 21 | self._password = kwargs.get('password', None) 22 | self._backtest_id = kwargs.get('backtest_id', None) 23 | self._arena_id = kwargs.get('arena_id', None) 24 | self._timeout = kwargs.pop('timeout', (5.0, 10.0)) 25 | 26 | def login(self): 27 | self._session.headers = { 28 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 29 | 'Accept-Encoding': 'gzip, deflate, br', 30 | 'Accept-Language': 'en-US,en;q=0.8', 31 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', 32 | 'Referer': '{}/user/login/index'.format(self.BASE_URL), 33 | 'X-Requested-With': 'XMLHttpRequest', 34 | 'Origin': self.BASE_URL, 35 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 36 | } 37 | self._session.get(self.BASE_URL, timeout=self._timeout) 38 | response = self._session.post('{}/user/login/doLogin?ajax=1'.format(self.BASE_URL), data={ 39 | 'CyLoginForm[username]': self._username, 40 | 'CyLoginForm[pwd]': self._password, 41 | 'ajax': 1 42 | }, timeout=self._timeout) 43 | self._session.headers.update({ 44 | 'cookie': response.headers['Set-Cookie'] 45 | }) 46 | 47 | super(JoinQuantClient, self).login() 48 | 49 | def query(self): 50 | today_str = datetime.today().strftime('%Y-%m-%d') 51 | response = self._session.get('{}/algorithm/live/transactionDetail'.format(self.BASE_URL), params={ 52 | 'backtestId': self._backtest_id, 53 | 'date': today_str, 54 | 'ajax': 1 55 | }, timeout=self._timeout) 56 | transaction_detail = response.json() 57 | raw_transactions = transaction_detail['data']['transaction'] 58 | transactions = [] 59 | for raw_transaction in raw_transactions: 60 | transaction = JoinQuantTransaction(raw_transaction).normalize() 61 | transactions.append(transaction) 62 | 63 | return transactions 64 | 65 | def query_portfolio(self): 66 | today_str = datetime.today().strftime('%Y-%m-%d') 67 | strategy_res = self._session.get('{}/post/{}'.format(self.BASE_URL, self._arena_id)) 68 | strategy_soup = BeautifulSoup(strategy_res.content, "lxml") 69 | backtest_id = strategy_soup.findAll('input', id="backtestId").pop().get('value') 70 | total_value = float(strategy_soup.findAll('div', class_="inline-block num f18 red").pop().text) 71 | position_res = self._session.get('{}/algorithm/live/sharePosition'.format(self.BASE_URL), params={ 72 | 'isAjax': 1, 73 | 'backtestId': backtest_id, 74 | 'date': today_str, 75 | 'isMobile': 0, 76 | 'isForward': 1, 77 | 'ajax': 1}) 78 | position_soup = BeautifulSoup(position_res.json()['data']['html'], "lxml") 79 | trs = position_soup.findAll('tr', class_="border_bo position_tr") 80 | portfolio = Portfolio() 81 | portfolio.total_value = total_value 82 | for tr in trs: 83 | position = self.__tr_to_position(tr) 84 | portfolio.add_position(position) 85 | portfolio.rebalance() 86 | return portfolio 87 | 88 | def __tr_to_position(self, tr): 89 | tds = tr.findAll('td') 90 | position = Position() 91 | position.security = tds[0].text.split()[1] 92 | position.price = float(tds[7].text) 93 | position.total_amount = int(tds[2].text.replace(u'股', '')) 94 | position.value = float(tds[4].text) 95 | return position 96 | -------------------------------------------------------------------------------- /strategyease_sdk/joinquant/manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Begin of __future__ module 4 | # End of __future__ module 5 | 6 | # Begin of external module 7 | # End of external module 8 | 9 | import traceback 10 | 11 | from kuanke.user_space_api import * 12 | 13 | from strategyease_sdk.base_manager import * 14 | from strategyease_sdk.models import * 15 | 16 | 17 | class JoinQuantStrategyManagerFactory(BaseStrategyManagerFactory): 18 | def __init__(self, context): 19 | self._strategy_context = JoinQuantStrategyContext(context) 20 | super(JoinQuantStrategyManagerFactory, self).__init__() 21 | 22 | def _get_context(self): 23 | return self._strategy_context 24 | 25 | def _create_logger(self): 26 | return JoinQuantLogger() 27 | 28 | 29 | class JoinQuantStrategyContext(BaseStrategyContext): 30 | def __init__(self, context): 31 | self._context = context 32 | 33 | def get_current_time(self): 34 | return self._context.current_dt 35 | 36 | def get_portfolio(self): 37 | quant_portfolio = self._context.portfolio 38 | portfolio = Portfolio() 39 | portfolio.available_cash = quant_portfolio.available_cash 40 | portfolio.total_value = quant_portfolio.total_value 41 | positions = dict() 42 | for security, quant_position in quant_portfolio.positions.items(): 43 | position = self._convert_position(quant_position) 44 | positions[position.security] = position 45 | portfolio.positions = positions 46 | return portfolio 47 | 48 | def convert_order(self, quant_order): 49 | common_order = Order( 50 | id=quant_order.order_id, 51 | action=(OrderAction.OPEN if quant_order.is_buy else OrderAction.CLOSE), 52 | security=quant_order.security, 53 | price=quant_order.limit, 54 | amount=quant_order.amount, 55 | style=(OrderStyle.LIMIT if quant_order.limit > 0 else OrderStyle.MARKET), 56 | status=self._convert_status(quant_order.status), 57 | add_time=quant_order.add_time, 58 | ) 59 | return common_order 60 | 61 | def get_orders(self): 62 | orders = get_orders() 63 | common_orders = [] 64 | for order in orders.values(): 65 | common_order = self.convert_order(order) 66 | common_orders.append(common_order) 67 | return common_orders 68 | 69 | def has_open_orders(self): 70 | return bool(get_open_orders()) 71 | 72 | def cancel_open_orders(self): 73 | open_orders = get_open_orders() 74 | for open_order in open_orders.values(): 75 | self.cancel_order(open_order) 76 | 77 | def cancel_order(self, open_order): 78 | return cancel_order(open_order) 79 | 80 | def read_file(self, path): 81 | return read_file(path) 82 | 83 | def is_sim_trade(self): 84 | return self._context.run_params.type == 'sim_trade' 85 | 86 | def is_backtest(self): 87 | return not self.is_sim_trade() 88 | 89 | def is_read_file_allowed(self): 90 | return True 91 | 92 | @staticmethod 93 | def _convert_position(quant_position): 94 | position = Position() 95 | position.security = quant_position.security 96 | position.price = quant_position.price 97 | position.total_amount = quant_position.total_amount + quant_position.locked_amount 98 | position.closeable_amount = quant_position.closeable_amount 99 | position.value = quant_position.value 100 | return position 101 | 102 | @staticmethod 103 | def _convert_status(quant_order_status): 104 | try: 105 | return OrderStatus(quant_order_status.value) 106 | except ValueError: 107 | return OrderStatus.open 108 | 109 | 110 | class JoinQuantLogger(BaseLogger): 111 | def debug(self, msg, *args, **kwargs): 112 | log.debug(msg, *args, **kwargs) 113 | 114 | def info(self, msg, *args, **kwargs): 115 | log.info(msg, *args, **kwargs) 116 | 117 | def warning(self, msg, *args, **kwargs): 118 | log.warn(msg, *args, **kwargs) 119 | 120 | def error(self, msg, *args, **kwargs): 121 | log.error(msg, *args, **kwargs) 122 | 123 | def exception(self, msg, *args, **kwargs): 124 | msg += "\n%s" 125 | args += (traceback.format_exc(),) 126 | log.error(msg, *args, **kwargs) 127 | -------------------------------------------------------------------------------- /strategyease_sdk/ricequant/manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Begin of __future__ module 4 | # End of __future__ module 5 | 6 | # Begin of external module 7 | # End of external module 8 | 9 | import traceback 10 | 11 | from strategyease_sdk.base_manager import * 12 | from strategyease_sdk.models import * 13 | 14 | 15 | class RiceQuantStrategyManagerFactory(BaseStrategyManagerFactory): 16 | def __init__(self, context): 17 | self._strategy_context = RiceQuantStrategyContext(context) 18 | super(RiceQuantStrategyManagerFactory, self).__init__() 19 | 20 | def _get_context(self): 21 | return self._strategy_context 22 | 23 | def _create_logger(self): 24 | return RiceQuantLogger() 25 | 26 | 27 | class RiceQuantStrategyContext(BaseStrategyContext): 28 | def __init__(self, context): 29 | self._context = context 30 | 31 | def get_current_time(self): 32 | return self._context.now 33 | 34 | def get_portfolio(self): 35 | quant_portfolio = self._context.portfolio 36 | portfolio = Portfolio() 37 | portfolio.available_cash = quant_portfolio.cash 38 | portfolio.total_value = quant_portfolio.total_value 39 | positions = dict() 40 | for order_book_id, quant_position in quant_portfolio.positions.items(): 41 | position = self._convert_position(quant_position) 42 | positions[position.security] = position 43 | portfolio.positions = positions 44 | return portfolio 45 | 46 | def convert_order(self, quant_order): 47 | status = { 48 | ORDER_STATUS.PENDING_NEW: OrderStatus.open, 49 | ORDER_STATUS.ACTIVE: OrderStatus.open, 50 | ORDER_STATUS.FILLED: OrderStatus.filled, 51 | ORDER_STATUS.CANCELLED: OrderStatus.canceled, 52 | ORDER_STATUS.REJECTED: OrderStatus.rejected, 53 | }.get(quant_order.ORDER_STATUS) 54 | common_order = Order( 55 | id=quant_order.order_id, 56 | action=(OrderAction.OPEN if quant_order.side == SIDE.BUY else OrderAction.CLOSE), 57 | security=quant_order.order_book_id, 58 | price=quant_order.price, 59 | amount=quant_order.quantity, 60 | style=(OrderStyle.LIMIT if quant_order.price > 0 else OrderStyle.MARKET), 61 | status=status, 62 | add_time=quant_order.datetime, 63 | ) 64 | return common_order 65 | 66 | def get_orders(self): 67 | pass 68 | 69 | def has_open_orders(self): 70 | return bool(get_open_orders()) 71 | 72 | def cancel_open_orders(self): 73 | open_orders = get_open_orders() 74 | for open_order in open_orders.values(): 75 | self.cancel_order(open_order) 76 | 77 | def cancel_order(self, open_order): 78 | return cancel_order(open_order) 79 | 80 | def read_file(self, path): 81 | return get_file(path) 82 | 83 | def is_sim_trade(self): 84 | return self._context.run_info.run_type == RUN_TYPE.PAPER_TRADING 85 | 86 | def is_backtest(self): 87 | return not self.is_sim_trade() 88 | 89 | def is_read_file_allowed(self): 90 | return False 91 | 92 | @staticmethod 93 | def _convert_position(quant_position): 94 | position = Position() 95 | position.security = quant_position.order_book_id 96 | position.price = quant_position.avg_price 97 | position.total_amount = quant_position.quantity 98 | position.closeable_amount = quant_position.sellable 99 | position.value = quant_position.market_value 100 | return position 101 | 102 | 103 | class RiceQuantLogger(BaseLogger): 104 | def debug(self, msg, *args, **kwargs): 105 | if not args: 106 | logger.debug(msg) 107 | else: 108 | logger.debug(msg % args) 109 | 110 | def info(self, msg, *args, **kwargs): 111 | if not args: 112 | logger.info(msg) 113 | else: 114 | logger.info(msg % args) 115 | 116 | def warning(self, msg, *args, **kwargs): 117 | if not args: 118 | logger.warning(msg) 119 | else: 120 | logger.warning(msg % args) 121 | 122 | def error(self, msg, *args, **kwargs): 123 | if not args: 124 | logger.error(msg) 125 | else: 126 | logger.error(msg % args) 127 | 128 | def exception(self, msg, *args, **kwargs): 129 | msg += "\n%s" 130 | args += (traceback.format_exc(),) 131 | logger.error(msg % args) 132 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import os 5 | import unittest 6 | 7 | import six 8 | from hamcrest import * 9 | from requests import HTTPError 10 | from six.moves import configparser 11 | 12 | from strategyease_sdk import Client 13 | from strategyease_sdk.client import MediaType 14 | from tests.strategyease_sdk.matchers.dataframe_matchers import * 15 | 16 | if six.PY2: 17 | ConfigParser = configparser.RawConfigParser 18 | else: 19 | ConfigParser = configparser.ConfigParser 20 | 21 | 22 | class ClientTest(unittest.TestCase): 23 | @classmethod 24 | def setUpClass(cls): 25 | logging.basicConfig(level=logging.DEBUG) 26 | config = ConfigParser() 27 | dir_path = os.path.dirname(os.path.realpath(__file__)) 28 | config.read('{}/../config/config.ini'.format(dir_path)) 29 | cls.client = Client(logging.getLogger(), **dict(config.items('StrategyEase'))) 30 | cls.client_param = config.get('StrategyEase', 'client') 31 | 32 | def test_get_account(self): 33 | try: 34 | self.client.get_account(self.client_param) 35 | except HTTPError as e: 36 | self.fail() 37 | 38 | def test_get_portfolios(self): 39 | try: 40 | data = self.client.get_portfolio(self.client_param) 41 | assert_that(data['sub_accounts'], has_row(u'人民币')) 42 | assert_that(data['positions'], has_column(u'证券代码')) 43 | except HTTPError as e: 44 | self.fail() 45 | 46 | def test_get_positions_in_jq_format(self): 47 | try: 48 | data = self.client.get_positions(self.client_param, media_type=MediaType.JOIN_QUANT) 49 | self.assertIsNotNone(data['availableCash']) 50 | except HTTPError as e: 51 | self.fail() 52 | 53 | def test_get_orders(self): 54 | try: 55 | df = self.client.get_orders(self.client_param) 56 | assert_that(df, has_column_matches(u"(委托|合同)编号")) 57 | except HTTPError as e: 58 | self.fail() 59 | 60 | def test_get_open_orders(self): 61 | try: 62 | df = self.client.get_orders(self.client_param, 'open') 63 | assert_that(df, has_column_matches(u"(委托|合同)编号")) 64 | except HTTPError as e: 65 | self.fail() 66 | 67 | def test_get_filled_orders(self): 68 | try: 69 | df = self.client.get_orders(self.client_param, 'filled') 70 | assert_that(df, has_column_matches(u"(委托|合同)编号")) 71 | except HTTPError as e: 72 | self.fail() 73 | 74 | def test_buy_stock(self): 75 | try: 76 | order = self.client.buy(self.client_param, symbol='000001', price=9, amount=100) 77 | self.assertIsNotNone(order['id']) 78 | except HTTPError as e: 79 | result = e.response.json() 80 | self.assertNotEqual(result['source'], "策略易") 81 | 82 | def test_sell_stock(self): 83 | try: 84 | order = self.client.sell(self.client_param, symbol='000001', price=9.5, amount=100) 85 | self.assertIsNotNone(order['id']) 86 | except HTTPError as e: 87 | result = e.response.json() 88 | self.assertNotEqual(result['source'], "策略易") 89 | 90 | def test_buy_stock_at_market_price(self): 91 | try: 92 | order = self.client.buy(self.client_param, symbol='000001', type='MARKET', priceType=4, amount=100) 93 | self.assertIsNotNone(order['id']) 94 | except HTTPError as e: 95 | result = e.response.json() 96 | self.assertNotEqual(result['source'], "策略易") 97 | 98 | def test_sell_stock_at_market_price(self): 99 | try: 100 | order = self.client.sell(self.client_param, symbol='000001', type='MARKET', priceType=4, amount=100) 101 | self.assertIsNotNone(order['id']) 102 | except HTTPError as e: 103 | result = e.response.json() 104 | self.assertNotEqual(result['source'], "策略易") 105 | 106 | def test_cancel_all(self): 107 | try: 108 | self.client.cancel_all(self.client_param) 109 | except HTTPError as e: 110 | self.fail() 111 | 112 | def test_query_by_type(self): 113 | try: 114 | df = self.client.query(self.client_param, type='FUND') 115 | assert_that(df, has_column(u'币种')) 116 | except HTTPError as e: 117 | self.fail() 118 | 119 | def test_query_by_navigation(self): 120 | try: 121 | df = self.client.query(self.client_param, navigation='查询>资金股份') 122 | assert_that(df, has_column(u'证券代码')) 123 | except HTTPError as e: 124 | self.fail() 125 | 126 | def test_query_new_stocks(self): 127 | df = self.client.query_new_stocks() 128 | self.assertTrue((df.columns == ['code', 'xcode', 'name', 'ipo_date', 'price']).all()) 129 | 130 | def test_query_convertible_bonds(self): 131 | df = self.client.query_convertible_bonds() 132 | assert_that(df, has_column('ipo_date')) 133 | assert_that(df, has_column('xcode')) 134 | -------------------------------------------------------------------------------- /tests/strategyease_sdk/test_client_margin_trading.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import os 5 | import unittest 6 | 7 | import six 8 | from hamcrest import * 9 | from requests import HTTPError 10 | from six.moves import configparser 11 | 12 | from strategyease_sdk import Client 13 | from tests.strategyease_sdk.matchers.dataframe_matchers import * 14 | 15 | if six.PY2: 16 | ConfigParser = configparser.RawConfigParser 17 | else: 18 | ConfigParser = configparser.ConfigParser 19 | 20 | 21 | class ClientMarginTradingTest(unittest.TestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | logging.basicConfig(level=logging.DEBUG) 25 | config = ConfigParser() 26 | dir_path = os.path.dirname(os.path.realpath(__file__)) 27 | config.read('{}/../config/config.ini'.format(dir_path)) 28 | cls.client = Client(logging.getLogger(), **dict(config.items('StrategyEase'))) 29 | cls.client_param = config.get('StrategyEase', 'client') 30 | 31 | def test_query(self): 32 | try: 33 | df = self.client.query(self.client_param, '融券卖出') 34 | assert_that(df, has_column(u'证券代码')) 35 | except HTTPError as e: 36 | self.fail() 37 | 38 | def test_buy_on_margin(self): 39 | try: 40 | order = self.client.execute( 41 | self.client_param, 42 | action='BUY_ON_MARGIN', symbol='000001', type='LIMIT', price=9, amount=100 43 | ) 44 | self.assertIsNotNone(order['id']) 45 | except HTTPError as e: 46 | result = e.response.json() 47 | self.assertNotEqual(result['source'], "策略易") 48 | 49 | def test_buy_on_margin_at_market_price(self): 50 | try: 51 | order = self.client.execute( 52 | self.client_param, 53 | action='BUY_ON_MARGIN', symbol='000001', type='MARKET', priceType=4, amount=100 54 | ) 55 | self.assertIsNotNone(order['id']) 56 | except HTTPError as e: 57 | result = e.response.json() 58 | self.assertNotEqual(result['source'], "策略易") 59 | 60 | def test_sell_then_repay(self): 61 | try: 62 | order = self.client.execute( 63 | self.client_param, 64 | action='SELL_THEN_REPAY', symbol='000001', type='LIMIT', price=9.5, amount=100 65 | ) 66 | self.assertIsNotNone(order['id']) 67 | except HTTPError as e: 68 | result = e.response.json() 69 | self.assertNotEqual(result['source'], "策略易") 70 | 71 | def test_sell_then_repay_at_market_price(self): 72 | try: 73 | order = self.client.execute( 74 | self.client_param, 75 | action='SELL_THEN_REPAY', symbol='000001', type='MARKET', priceType=4, amount=100 76 | ) 77 | self.assertIsNotNone(order['id']) 78 | except HTTPError as e: 79 | result = e.response.json() 80 | self.assertNotEqual(result['source'], "策略易") 81 | 82 | def test_sell_on_margin(self): 83 | try: 84 | order = self.client.execute( 85 | self.client_param, 86 | action='SELL_ON_MARGIN', symbol='000003', type='LIMIT', price=9.5, amount=100 87 | ) 88 | self.assertIsNotNone(order['id']) 89 | except HTTPError as e: 90 | result = e.response.json() 91 | self.assertNotEqual(result['source'], "策略易") 92 | 93 | def test_sell_on_margin_at_market_price(self): 94 | try: 95 | order = self.client.execute( 96 | self.client_param, 97 | action='SELL_ON_MARGIN', symbol='000001', type='MARKET', priceType=4, amount=100 98 | ) 99 | self.assertIsNotNone(order['id']) 100 | except HTTPError as e: 101 | result = e.response.json() 102 | self.assertNotEqual(result['source'], "策略易") 103 | 104 | def test_buy_then_repay(self): 105 | try: 106 | order = self.client.execute( 107 | self.client_param, 108 | action='BUY_THEN_REPAY', symbol='000001', type='LIMIT', price=8.9, amount=100 109 | ) 110 | self.assertIsNotNone(order['id']) 111 | except HTTPError as e: 112 | result = e.response.json() 113 | self.assertNotEqual(result['source'], "策略易") 114 | 115 | def test_buy_then_repay_at_market_price(self): 116 | try: 117 | order = self.client.execute( 118 | self.client_param, 119 | action='BUY_THEN_REPAY', symbol='000001', type='MARKET', priceType=4, amount=100 120 | ) 121 | self.assertIsNotNone(order['id']) 122 | except HTTPError as e: 123 | result = e.response.json() 124 | self.assertNotEqual(result['source'], "策略易") 125 | 126 | def test_repay_cash(self): 127 | try: 128 | order = self.client.execute( 129 | self.client_param, 130 | action='REPAY_CASH', symbol='', type='LIMIT', price=1.0, amount=1 131 | ) 132 | self.assertIsNotNone(order['id']) 133 | except HTTPError as e: 134 | result = e.response.json() 135 | self.assertNotEqual(result['source'], "策略易") 136 | 137 | def test_repay_sec(self): 138 | try: 139 | order = self.client.execute( 140 | self.client_param, 141 | action='REPAY_SEC', symbol='000001', type='LIMIT', price=0.0, amount=100 142 | ) 143 | self.assertIsNotNone(order['id']) 144 | except HTTPError as e: 145 | result = e.response.json() 146 | self.assertNotEqual(result['source'], "策略易") 147 | -------------------------------------------------------------------------------- /strategyease_sdk/jobs/online_quant_sync.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import distutils 6 | import time 7 | 8 | from strategyease_sdk.jobs.basic_job import BasicJob 9 | from strategyease_sdk.market_utils import MarketUtils 10 | from strategyease_sdk.models import * 11 | 12 | 13 | class OnlineQuantSyncJob(BasicJob): 14 | def __init__(self, strategyease_client, quant_client, client_aliases=None, name=None, **kwargs): 15 | super(OnlineQuantSyncJob, self).__init__(name, kwargs.get('schedule', None), kwargs.get('enabled', False)) 16 | 17 | self._config = PortfolioSyncConfig(**kwargs) 18 | self._strategyease_client = strategyease_client 19 | self._quant_client = quant_client 20 | self._client_aliases = client_aliases 21 | self._name = name 22 | 23 | def __call__(self): 24 | if MarketUtils.is_closed() and not self._config.dry_run: 25 | self._logger.warning("********** 休市期间不同步 **********") 26 | return 27 | 28 | if not self._quant_client.is_login(): 29 | self._logger.info("登录 %s", self._quant_client.name) 30 | self._quant_client.login() 31 | 32 | self._logger.info("********** 开始同步 **********") 33 | try: 34 | target_portfolio = self._get_target_portfolio() 35 | for client_alias in self._client_aliases: 36 | client = self._client_aliases[client_alias] 37 | self._sync(target_portfolio, client) 38 | except Exception as e: 39 | self._logger.exception("同步异常") 40 | self._logger.info("********** 结束同步 **********\n") 41 | 42 | @property 43 | def name(self): 44 | return self._name 45 | 46 | def _sync(self, target_portfolio, client): 47 | if self._config.pre_clear: 48 | if not self._config.dry_run: 49 | self._strategyease_client.cancel_all(client) 50 | time.sleep(self._config.order_interval) 51 | 52 | for i in range(0, 2 + self._config.extra_rounds): 53 | is_sync = self._sync_once(target_portfolio, client) 54 | if is_sync: 55 | self._logger.info("已同步") 56 | return 57 | time.sleep(self._config.round_interval) 58 | 59 | def _sync_once(self, target_portfolio, client): 60 | adjustment = self._create_adjustment(target_portfolio, client) 61 | self._log_progress(adjustment) 62 | is_sync = adjustment.empty() 63 | if not is_sync: 64 | self._execute_adjustment(adjustment, client) 65 | return is_sync 66 | 67 | def _get_target_portfolio(self): 68 | portfolio = self._quant_client.query_portfolio() 69 | return portfolio 70 | 71 | def _execute_adjustment(self, adjustment, client): 72 | for batch in adjustment.batches: 73 | for order in batch: 74 | self._execute_order(order, client) 75 | time.sleep(self._config.order_interval) 76 | time.sleep(self._config.batch_interval) 77 | 78 | def _execute_order(self, order, client): 79 | try: 80 | if self._config.dry_run: 81 | self._logger.info(order) 82 | return 83 | e_order = order.to_e_order() 84 | self._strategyease_client.execute(client=client, **e_order) 85 | except Exception as e: 86 | self._logger.error('客户端[%s]下单失败\n%s', client, e) 87 | 88 | def _create_adjustment(self, target_portfolio, client): 89 | request = self._create_adjustment_request(target_portfolio) 90 | request_json = Adjustment.to_json(request) 91 | response_json = self._strategyease_client.create_adjustment(client=client, request_json=request_json) 92 | adjustment = Adjustment.from_json(response_json) 93 | return adjustment 94 | 95 | def _create_adjustment_request(self, target_portfolio): 96 | schema = AdjustmentSchema(self._config.reserved_securities, 97 | self._config.min_order_value, 98 | self._config.max_order_value) 99 | request = Adjustment() 100 | request.source_portfolio = Portfolio( 101 | other_value=self._config.other_value, 102 | total_value_deviation_rate=self._config.total_value_deviation_rate 103 | ) 104 | request.target_portfolio = target_portfolio 105 | request.schema = schema 106 | return request 107 | 108 | def _log_progress(self, adjustment): 109 | self._logger.info(adjustment.progress) 110 | 111 | 112 | class PortfolioSyncConfig(object): 113 | def __init__(self, **kwargs): 114 | self._dry_run = distutils.util.strtobool(kwargs.get('dry_run', 'false')) 115 | self._pre_clear = distutils.util.strtobool(kwargs.get('pre_clear', 'false')) 116 | self._other_value = float(kwargs.get('other_value', 0.0)) 117 | self._total_value_deviation_rate = float(kwargs.get('total_value_deviation_rate', 0.001)) 118 | self._reserved_securities = list(filter(None, kwargs.get('reserved_securities').split('\n'))) 119 | self._min_order_value = kwargs.get('min_order_value', '0') 120 | self._max_order_value = float(kwargs.get('max_order_value', '1000000')) 121 | self._round_interval = int(kwargs.get('round_interval', '5')) 122 | self._batch_interval = int(kwargs.get('batch_interval', '5')) 123 | self._order_interval = int(kwargs.get('order_interval', '1')) 124 | self._extra_rounds = int(kwargs.get('extra_rounds', '0')) 125 | 126 | @property 127 | def dry_run(self): 128 | return self._dry_run 129 | 130 | @property 131 | def pre_clear(self): 132 | return self._pre_clear 133 | 134 | @property 135 | def other_value(self): 136 | return self._other_value 137 | 138 | @property 139 | def total_value_deviation_rate(self): 140 | return self._total_value_deviation_rate 141 | 142 | @property 143 | def reserved_securities(self): 144 | return self._reserved_securities 145 | 146 | @property 147 | def min_order_value(self): 148 | return self._min_order_value 149 | 150 | @property 151 | def max_order_value(self): 152 | return self._max_order_value 153 | 154 | @property 155 | def round_interval(self): 156 | return self._round_interval 157 | 158 | @property 159 | def batch_interval(self): 160 | return self._batch_interval 161 | 162 | @property 163 | def order_interval(self): 164 | return self._order_interval 165 | 166 | @property 167 | def extra_rounds(self): 168 | return self._extra_rounds 169 | -------------------------------------------------------------------------------- /config/online-quant/research/strategyease_sdk_config_template.yaml: -------------------------------------------------------------------------------- 1 | # ********************************************************* 2 | # 策略易 SDK 配置 3 | # 如无特别说明,配置项修改后,将在策略重启后生效 4 | # 注意: 5 | # - 请勿在策略运行期间修改结构,比如 id 等关键信息 6 | # - 配置项冒号后需保留一个空格 7 | # - 为必选项,[xxx] 为可选项;需要将括号移除 8 | # - 为多选一项,使用其中一项即可 9 | # ********************************************************* 10 | 11 | # ********************************************************* 12 | # 代理配置 13 | # ********************************************************* 14 | proxies: 15 | - id: default 16 | base-url: http://www.iguuu.com/proxy/trade 17 | # 爱股网用户名 18 | username: 19 | # 爱股网密码 20 | password: 21 | 22 | # ********************************************************* 23 | # 策略易配置 24 | # ********************************************************* 25 | gateways: 26 | # 策略易-1 配置 27 | - id: gateway-1 28 | # 连接方式 29 | # DIRECT:直连,适用于有公网 IP 的环境 30 | # PROXY: 通过爱股网代理连接 31 | connection-method: 32 | # IP 地址 33 | host: xxx.xxx.xxx.xxx 34 | # 端口 35 | port: 8888 36 | # 代理 ID 37 | # 连接方式为“代理”时需要设置 38 | proxy: default 39 | # 实例 ID,即运行策略易的计算机名 40 | # 连接方式为“代理”时需要设置 41 | instance-id: 42 | # 密钥 43 | key: [key] 44 | # 超时 45 | timeout: 46 | # 连接超时 47 | connect: 5.0 48 | # 读取超时 49 | read: 10.0 50 | # 交易客户端 51 | clients: 52 | # 客户端-1 53 | # 注意:id 需全局唯一 54 | - id: client-1 55 | # 查询串,对应于 API 的 client 参数 56 | # 其中 xxxx 为交易账号或交易账号后半段 57 | query: account:xxxx 58 | # 是否默认? 59 | # 1 个策略易只允许设置 1 个交易客户端为默认 60 | default: true 61 | # 其他资产价值 62 | # 基金及其他非场内资产价值,该项配置用于校验账户 63 | other-value: 0 64 | # 总资产价值偏差率 65 | # 该项配置用于校验账户 66 | total-value-deviation-rate: 0.001 67 | # 保留名单,每行一个 68 | # 股票代码,注意使用 str 标签 69 | # 例如:!!str 000001 70 | # 注意:该配置在下次 handle_data 调用时生效 71 | reserved-securities: 72 | # 含有非数字的代码 73 | - \D 74 | # B股代码 75 | - ^[92] 76 | # 港股代码 77 | - ^[\d]{5}$ 78 | # 逆回购代码 79 | - ^(204|131) 80 | # 新标准券代码 81 | - !!str 888880 82 | # 客户端-2 83 | - id: client-2 84 | query: account:xxxx 85 | other-value: 0 86 | total-value-deviation-rate: 0.001 87 | reserved-securities: 88 | - \D 89 | - ^[92] 90 | - ^[\d]{5}$ 91 | - ^(204|131) 92 | - !!str 888880 93 | # 策略易-2 配置 94 | - id: gateway-2 95 | # 连接方式 96 | connection-method: DIRECT 97 | host: xxx.xxx.xxx.xxx 98 | port: 8888 99 | key: 100 | timeout: 101 | connect: 5.0 102 | read: 10.0 103 | clients: 104 | - id: client-3 105 | query: title:monijiaoyi 106 | default: true 107 | other-value: 0 108 | total-value-deviation-rate: 0.001 109 | reserved-securities: 110 | - \D 111 | - ^[92] 112 | - ^[\d]{5}$ 113 | - ^(204|131) 114 | - !!str 888880 115 | - id: client-4 116 | query: title:xxx,account:xxx 117 | other-value: 0 118 | total-value-deviation-rate: 0.001 119 | reserved-securities: 120 | - \D 121 | - ^[92] 122 | - ^[\d]{5}$ 123 | - ^(204|131) 124 | - !!str 888880 125 | 126 | # ********************************************************* 127 | # 策略配置 128 | # 实体关系 129 | # 130 | # manager 1 ---- N trader 1 ---- 1 交易客户端(client) 131 | # 132 | # ********************************************************* 133 | managers: 134 | # manager-1 配置 135 | - id: manager-1 136 | traders: 137 | # trader-1 138 | - id: trader-1 139 | client: client-1 140 | # 是否开启? 141 | # 正式运行时设置为 true 142 | enabled: true 143 | # 在回测中是否开启? 144 | enabled-in-backtest: false 145 | # 是否排练?排练时不会下单。 146 | # 正式运行时设置为 false 147 | dry-run: true 148 | # 工作模式 149 | # 1. SYNC: 指按模拟交易的持仓进行同步 150 | # 2. FOLLOW:指按模拟交易的下单进行跟单 151 | # 目前米筐只支持 SYNC 模式 152 | mode: SYNC 153 | # 同步选项 154 | # 如果该策略无需同步操作,可以省略 sync 配置项 155 | # 注意:该配置在下次 handle_data 调用时生效 156 | sync: 157 | # 同步前是否撤销模拟盘未成交订单 158 | # 如果该选项未启用,并且模拟盘有未成交订单,SDK 将不会做同步 159 | pre-clear-for-sim: false 160 | # 同步前是否撤销实盘未成交订单 161 | pre-clear-for-live: false 162 | # 最小订单金额,低于该值的订单将被忽略,以防因为价格波动导致的频繁调仓 163 | # 取值可以为数值,或者百分比 164 | min-order-value: 1% 165 | # 最大订单金额,用于分单 166 | # 取值为数值 167 | max-order-value: 200000 168 | # 轮次间隔时间,单位为毫秒 169 | # 建议不小于 5 秒,以防交易软件持仓刷新过慢 170 | round-interval: 5000 171 | # 批次间隔时间,单位为毫秒 172 | batch-interval: 1000 173 | # 下单间隔时间,单位为毫秒 174 | order-interval: 1000 175 | # 默认为 2 轮,该选项用于增加额外轮次 176 | # 额外轮次 177 | extra-rounds: 0 178 | - id: manager-2 179 | traders: 180 | - id: trader-2 181 | client: client-1 182 | enabled: true 183 | dry-run: true 184 | mode: SYNC 185 | sync: 186 | pre-clear-for-sim: false 187 | pre-clear-for-live: false 188 | min-order-value: 1% 189 | max-order-value: 200000 190 | round-interval: 5000 191 | batch-interval: 1000 192 | order-interval: 1000 193 | extra-rounds: 0 194 | -------------------------------------------------------------------------------- /scripts/strategyease_sdk_installer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import codecs 6 | import errno 7 | import logging 8 | import os 9 | import re 10 | import shutil 11 | import sys 12 | from datetime import datetime 13 | from enum import Enum 14 | 15 | import requests 16 | import six 17 | 18 | GIT_BASE_URL = "https://raw.githubusercontent.com/sinall/StrategyEase-Python-SDK" 19 | WORK_DIR = '.' 20 | 21 | 22 | class SourceLocation(Enum): 23 | LOCAL = "local" 24 | GITHUB = "github" 25 | 26 | 27 | class SdkInstaller: 28 | def __init__(self, quant, output_dir, version, source_location): 29 | self._logger = logging.getLogger(__name__) 30 | self._logger.setLevel(logging.DEBUG) 31 | self._quant = quant 32 | self._output_dir = output_dir 33 | self._version = version 34 | self._source_location = source_location 35 | 36 | def install(self): 37 | self._install_sdk() 38 | try: 39 | self._install_config() 40 | except: 41 | self._logger.error("无法安装配置文件") 42 | 43 | def _install_sdk(self): 44 | buffer = [] 45 | module_statements = [] 46 | main_module = "strategyease_sdk.{0}.manager".format(self._quant) 47 | self._import_sdk_module(main_module, buffer, module_statements) 48 | 49 | output_file_path = os.path.join(self._output_dir, 'strategyease_sdk.py') 50 | self._mkdir_p(os.path.dirname(output_file_path)) 51 | self._backup(output_file_path) 52 | self._write_file(buffer, output_file_path) 53 | self._logger.info(u"生成文件[%s]成功", output_file_path) 54 | 55 | def _install_config(self): 56 | file_path = "config/online-quant/research/strategyease_sdk_config_template.yaml" 57 | source_file = self._get_file(file_path) 58 | 59 | tpl_output_file_path = os.path.join(self._output_dir, 'strategyease_sdk_config_template.yaml') 60 | self._backup(tpl_output_file_path) 61 | self._write_file(list(source_file), tpl_output_file_path) 62 | self._logger.info(u"生成文件[%s]成功", tpl_output_file_path) 63 | 64 | output_file_path = os.path.join(self._output_dir, 'strategyease_sdk_config.yaml') 65 | if not os.path.isfile(output_file_path): 66 | shutil.copyfile(tpl_output_file_path, output_file_path) 67 | 68 | def _import_sdk_module(self, module, buffer, module_statements): 69 | path = module.replace('.', '/') + '.py' 70 | lines = list(self._get_file(path)) 71 | if buffer and re.search("^#.*coding:", lines[0]): 72 | lines.pop(0) 73 | index = next(i for i, line in enumerate(lines) if line and not line.isspace()) 74 | for line in lines[index:]: 75 | match = re.search("^from .* import .*", line) or re.search("^import .*", line) 76 | if match: 77 | if line in module_statements: 78 | continue 79 | self._import_module(line, buffer, module_statements) 80 | module_statements.append(line) 81 | else: 82 | buffer.append(line) 83 | 84 | def _import_module(self, statement, buffer, module_statements): 85 | match = re.search("^from __future__ import .*", statement) 86 | if match: 87 | index = next(i for i, line in enumerate(buffer) if line.startswith("# End of __future__ module")) 88 | buffer.insert(index, statement) 89 | return 90 | match = re.search("^from (?!strategyease_sdk).* import .*", statement) or re.search("^import .*", statement) 91 | if match: 92 | index = next(i for i, line in enumerate(buffer) if line.startswith("# End of external module")) 93 | buffer.insert(index, statement) 94 | return 95 | match = re.search("^from (strategyease_sdk\\..*) import .*", statement) 96 | if match: 97 | module = match.group(1) 98 | self._import_sdk_module(module, buffer, module_statements) 99 | return 100 | 101 | def _get_file(self, path): 102 | if self._source_location is SourceLocation.LOCAL: 103 | file = self._get_local_file(path) 104 | else: 105 | file = self._get_url(path) 106 | return file 107 | 108 | def _get_local_file(self, path): 109 | script_dir = os.path.dirname(os.path.realpath(__file__)) 110 | source_root_dir = os.path.join(script_dir, "..") 111 | filename = os.path.join(source_root_dir, path) 112 | file = codecs.open(filename, encoding="utf-8") 113 | return file 114 | 115 | def _get_url(self, path): 116 | url = "{0}/{1}/{2}".format(GIT_BASE_URL, self._version, path) 117 | response = requests.get(url) 118 | response.raise_for_status() 119 | file = six.StringIO(response.text) 120 | return file 121 | 122 | def _backup(self, filename): 123 | backup_dir = os.path.join(self._output_dir, 'backup') 124 | self._mkdir_p(backup_dir) 125 | 126 | if not os.path.isfile(filename): 127 | return 128 | 129 | filename_parts = os.path.splitext(os.path.basename(filename)) 130 | backup_filename = "{0}/{1}.{2}.{3}".format( 131 | backup_dir, filename_parts[0], datetime.now().strftime("%Y_%m_%d_%H_%M_%S"), filename_parts[1] 132 | ) 133 | shutil.copyfile(filename, backup_filename) 134 | self._logger.info(u"备份文件[%s]至[%s]成功", filename, backup_filename) 135 | 136 | def _mkdir_p(self, path): 137 | try: 138 | os.makedirs(path) 139 | except OSError as e: 140 | if e.errno == errno.EEXIST and os.path.isdir(path): 141 | pass 142 | else: 143 | raise 144 | 145 | def _write_file(self, buffer, path): 146 | with codecs.open(path, "w+", "utf-8") as file: 147 | for line in buffer: 148 | file.write(line) 149 | 150 | 151 | def main(args): 152 | logging.basicConfig(level=logging.DEBUG) 153 | 154 | parser = argparse.ArgumentParser(description='Process some integers.') 155 | parser.add_argument('--quant', help='在线量化平台名称,可选:joinquant、ricequant、uqer') 156 | parser.add_argument('--output', help='生成文件存储位置', default=WORK_DIR) 157 | parser.add_argument('--version', help='版本,如:v1.1.0.a6,默认为最新版本', default='master') 158 | parser.add_argument('--source-location', help='源代码位置,可选:local、github;默认为 github', default='github') 159 | args = parser.parse_known_args(args) 160 | 161 | sdk_installer = SdkInstaller( 162 | quant=args[0].quant, 163 | output_dir=args[0].output, 164 | version=args[0].version, 165 | source_location=SourceLocation(args[0].source_location) 166 | ) 167 | sdk_installer.install() 168 | 169 | 170 | if __name__ == "__main__": 171 | main(sys.argv[1:]) 172 | -------------------------------------------------------------------------------- /strategyease_sdk/scheduler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import codecs 4 | import collections 5 | import distutils.util 6 | import errno 7 | import logging 8 | import logging.config 9 | import logging.handlers 10 | import os 11 | import os.path 12 | import time 13 | 14 | from apscheduler.schedulers.background import BackgroundScheduler 15 | from six.moves import configparser 16 | 17 | from strategyease_sdk import Client 18 | from strategyease_sdk.ap import APCronParser 19 | from strategyease_sdk.guorn.client import GuornClient 20 | from strategyease_sdk.jobs.batch import BatchJob 21 | from strategyease_sdk.jobs.convertible_bonds_purchase import ConvertibleBondsPurchaseJob 22 | from strategyease_sdk.jobs.new_stock_purchase import NewStockPurchaseJob 23 | from strategyease_sdk.jobs.online_quant_following import OnlineQuantFollowingJob 24 | from strategyease_sdk.jobs.online_quant_sync import OnlineQuantSyncJob 25 | from strategyease_sdk.jobs.repo import RepoJob 26 | from strategyease_sdk.joinquant.client import JoinQuantClient 27 | from strategyease_sdk.ricequant.client import RiceQuantClient 28 | from strategyease_sdk.uqer.client import UqerClient 29 | 30 | 31 | class Scheduler(object): 32 | def __init__(self): 33 | self._logger = logging.getLogger() 34 | 35 | config_path = os.path.join(os.path.expanduser('~'), '.strategyease_sdk', 'config', 'scheduler.ini') 36 | self._logger.info('Config path: %s', config_path) 37 | self._config = configparser.RawConfigParser() 38 | self._config.readfp(codecs.open(config_path, encoding="utf_8_sig"), ) 39 | 40 | self._scheduler = BackgroundScheduler() 41 | self._client = Client(self._logger, **dict(self._config.items('StrategyEase'))) 42 | 43 | def start(self): 44 | for section in self._config.sections(): 45 | if not self.__is_job(section): 46 | continue 47 | job = self.__create_job(section) 48 | if job is not None: 49 | self.__add_job(job) 50 | else: 51 | self._logger.warning("[{}] is not a valid job", section) 52 | 53 | self._scheduler.start() 54 | print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C')) 55 | 56 | try: 57 | while True: 58 | time.sleep(1) 59 | except (KeyboardInterrupt, SystemExit): 60 | self._scheduler.shutdown() 61 | 62 | def __add_job(self, job): 63 | if job.is_enabled: 64 | self._scheduler.add_job(job, APCronParser.parse(job.schedule), name=job.name, misfire_grace_time=None) 65 | else: 66 | self._logger.warning('{} is not enabled'.format(job.name)) 67 | 68 | def __create_job(self, section): 69 | job_type = self._config.get(section, 'type') 70 | job = None 71 | if job_type == 'NewStocks': 72 | job = self.__create_new_stock_purchase_job(section) 73 | elif job_type == 'ConvertibleBonds': 74 | job = self.__create_convertible_bonds_job(section) 75 | elif job_type == 'Repo': 76 | job = self.__create_repo_job(section) 77 | elif job_type == 'Batch': 78 | job = self.__create_batch_job(section) 79 | elif job_type == 'JoinQuant': 80 | job = self.__create_join_quant_following_job(section) 81 | elif job_type == 'RiceQuant': 82 | job = self.__create_rice_quant_following_job(section) 83 | elif job_type == 'Uqer': 84 | job = self.__create_uqer_following_job(section) 85 | elif job_type == 'Guorn': 86 | job = self.__create_guorn_sync_job(section) 87 | elif job_type == 'JoinQuantArena': 88 | job = self.__create_join_quant_sync_job(section) 89 | return job 90 | 91 | def __create_new_stock_purchase_job(self, section): 92 | options = self.__build_options(section) 93 | client_aliases = self.__filter_client_aliases(section) 94 | return NewStockPurchaseJob(self._client, client_aliases, '{}-Job'.format(section), **options) 95 | 96 | def __create_convertible_bonds_job(self, section): 97 | options = self.__build_options(section) 98 | client_aliases = self.__filter_client_aliases(section) 99 | return ConvertibleBondsPurchaseJob(self._client, client_aliases, '{}-Job'.format(section), **options) 100 | 101 | def __create_repo_job(self, section): 102 | options = self.__build_options(section) 103 | client_aliases = self.__filter_client_aliases(section) 104 | return RepoJob(self._client, client_aliases, '{}-Job'.format(section), **options) 105 | 106 | def __create_batch_job(self, section): 107 | options = self.__build_options(section) 108 | client_aliases = self.__filter_client_aliases(section) 109 | return BatchJob(self._client, client_aliases, '{}-Job'.format(section), **options) 110 | 111 | def __create_join_quant_following_job(self, section): 112 | options = self.__build_options(section) 113 | client_aliases = self.__filter_client_aliases(section) 114 | quant_client = JoinQuantClient(**options) 115 | return OnlineQuantFollowingJob(self._client, quant_client, client_aliases, '{}-FollowingJob'.format(section), 116 | **options) 117 | 118 | def __create_rice_quant_following_job(self, section): 119 | options = self.__build_options(section) 120 | client_aliases = self.__filter_client_aliases(section) 121 | quant_client = RiceQuantClient(**options) 122 | return OnlineQuantFollowingJob(self._client, quant_client, client_aliases, '{}-FollowingJob'.format(section), 123 | **options) 124 | 125 | def __create_uqer_following_job(self, section): 126 | options = self.__build_options(section) 127 | client_aliases = self.__filter_client_aliases(section) 128 | quant_client = UqerClient(**options) 129 | return OnlineQuantFollowingJob(self._client, quant_client, client_aliases, '{}-FollowingJob'.format(section), 130 | **options) 131 | 132 | def __create_guorn_sync_job(self, section): 133 | options = self.__build_options(section) 134 | client_aliases = self.__filter_client_aliases(section) 135 | quant_client = GuornClient(**options) 136 | return OnlineQuantSyncJob(self._client, quant_client, client_aliases, '{}-SyncJob'.format(section), 137 | **options) 138 | 139 | def __create_join_quant_sync_job(self, section): 140 | options = self.__build_options(section) 141 | client_aliases = self.__filter_client_aliases(section) 142 | quant_client = JoinQuantClient(**options) 143 | return OnlineQuantSyncJob(self._client, quant_client, client_aliases, '{}-SyncJob'.format(section), 144 | **options) 145 | 146 | def __is_job(self, section): 147 | return self._config.has_option(section, 'type') 148 | 149 | def __build_options(self, section): 150 | if not self._config.has_section(section): 151 | return dict() 152 | 153 | options = dict(self._config.items(section)) 154 | options['enabled'] = bool(distutils.util.strtobool(options['enabled'])) 155 | return options 156 | 157 | def __filter_client_aliases(self, section): 158 | if not self._config.has_section(section): 159 | return dict() 160 | 161 | all_client_aliases = dict(self._config.items('ClientAliases')) 162 | client_aliases = [client_alias.strip() for client_alias in 163 | filter(None, self._config.get(section, 'clients').split(','))] 164 | return collections.OrderedDict( 165 | (client_alias, all_client_aliases[client_alias]) for client_alias in client_aliases) 166 | 167 | 168 | class FileHandler(logging.handlers.TimedRotatingFileHandler): 169 | def __init__(self, fileName): 170 | path = os.path.join(os.path.expanduser('~'), 'AppData', 'Local', '爱股网', '策略易') 171 | try: 172 | os.makedirs(path) 173 | except OSError as e: 174 | if e.errno == errno.EEXIST and os.path.isdir(path): 175 | pass 176 | else: 177 | raise 178 | super(FileHandler, self).__init__(path + "/" + fileName) 179 | 180 | 181 | def start(): 182 | logging.config.fileConfig(os.path.join(os.path.expanduser('~'), '.strategyease_sdk', 'config', 'logging.ini')) 183 | 184 | Scheduler().start() 185 | -------------------------------------------------------------------------------- /config/scheduler-template.ini: -------------------------------------------------------------------------------- 1 | ; ********************************************************* 2 | ; 计划任务类型 3 | ; ********************************************************* 4 | ; 5 | ; NewStocks: 新股申购 6 | ; ConvertibleBonds: 转债申购 7 | ; Repo: 逆回购 8 | ; Batch: 批量下单 9 | ; JoinQuant: 聚宽跟单 10 | ; JoinQuantArena: 聚宽擂台(商城)同步 11 | ; RiceQuant: 米筐策略跟单 12 | ; Uquer: 优矿策略跟单 13 | ; Guorn: 果仁策略同步 14 | ; 15 | ; ********************************************************* 16 | ; schedule 参数 17 | ; ********************************************************* 18 | ; 19 | ; 类似于 cron 表达式 20 | ; 格式为:[秒(0-59)] [分(0-59)] [时(0-23)] [星期几(0-6 或英文缩写)] [星期(1-53)] [日(1-31)] [月(1-12)] [年(四位数字)] 21 | ; (星期几英文缩写:mon,tue,wed,thu,fri,sat,sun) 22 | ; 23 | ; 字段支持表达式: 24 | ; - * 为任意单位时间触发 25 | ; - */a 为每 a 个单位时间触发 26 | ; - a-b 为 a 到 b 的区间触发 27 | ; - a-b/c 为 a 到 b 的区间每 c 个单位时间触发 28 | ; 29 | ; 详见:https://apscheduler.readthedocs.io/en/v2.1.2/cronschedule.html 30 | ; 31 | 32 | 33 | ; ********************************************************* 34 | ; 策略易配置 35 | ; ********************************************************* 36 | [StrategyEase] 37 | host=localhost 38 | port=8888 39 | key= 40 | 41 | 42 | ; ********************************************************* 43 | ; 交易客户端 client 参数别名列表 44 | ; ********************************************************* 45 | [ClientAliases] 46 | ; client 参数别名定义 47 | ; 下面的各个任务可能会用到相同的客户端,使用 client 参数别名可以简化配置 48 | ; 取值可以参见策略易帮助(帮助>查看帮助) 49 | client1=account:45678 50 | client2=account:12345 51 | 52 | 53 | ; ********************************************************* 54 | ; 以下为计划任务设置 55 | ; ********************************************************* 56 | 57 | ; ********************************************************* 58 | ; 新股申购 59 | ; ********************************************************* 60 | [NewStocks] 61 | type=NewStocks 62 | 63 | ; 是否启用? 64 | enabled=false 65 | 66 | ; 默认设置为:星期一至星期五 11:35 67 | schedule=0 35 11 mon-fri * * * * 68 | 69 | ; 需要自动新股申购的交易客户端列表,以,(半角逗号)分割 70 | ; clients=client1,client2 71 | clients=client1 72 | 73 | 74 | ; ********************************************************* 75 | ; 转债申购 76 | ; ********************************************************* 77 | [ConvertibleBonds] 78 | type=ConvertibleBonds 79 | 80 | ; 是否启用? 81 | enabled=false 82 | 83 | ; 默认设置为:星期一至星期五 11:40 84 | schedule=0 40 11 mon-fri * * * * 85 | 86 | ; 需要自动新股申购的交易客户端列表,以,(半角逗号)分割 87 | ; clients=client1,client2 88 | clients=client1 89 | 90 | 91 | ; ********************************************************* 92 | ; 逆回购 93 | ; ********************************************************* 94 | [Repo] 95 | type=Repo 96 | 97 | ; 是否启用? 98 | enabled=false 99 | 100 | ; 默认设置为:星期一至星期五 14:55 101 | schedule=0 55 14 mon-fri * * * * 102 | 103 | ; 需要自动逆回购的交易客户端列表,以,(半角逗号)分割 104 | ; clients=client1,client2 105 | clients=client1 106 | 107 | security=131810 108 | 109 | 110 | ; ********************************************************* 111 | ; 批量下单 112 | ; ********************************************************* 113 | [Batch] 114 | type=Batch 115 | 116 | ; 是否启用? 117 | enabled=false 118 | 119 | ; 默认设置为:每天晚20点 120 | schedule=0 0 20 * * * * * 121 | 122 | ; 需要自动新股申购的交易客户端列表,以,(半角逗号)分割 123 | ; clients=client1,client2 124 | clients=client1 125 | 126 | folder=C:\\batch-orders 127 | 128 | 129 | ; ********************************************************* 130 | ; 聚宽策略跟单 131 | ; ********************************************************* 132 | [JoinQuant-1] 133 | type=JoinQuant 134 | 135 | username= 136 | password= 137 | 138 | ; 模拟交易 URL 中 backtestId= 后面的那串字符 139 | ; 例如,模拟交易 URL 为:https://www.joinquant.com/algorithm/live/index?backtestId=c215e5e57b30a65df4139bfff8c90e99 140 | ; 聚宽的 backtestId 会经常改变,但是指向的都是同一个模拟交易,该配置无需跟着修改 141 | ; 则此处填写:c215e5e57b30a65df4139bfff8c90e99 142 | backtest_id= 143 | 144 | ; 是否启用? 145 | enabled=false 146 | 147 | ; 148 | ; 默认设置为:星期一至星期五 9:00 到 15:00 每分钟的第 30 秒 149 | schedule=30 */1 9-15 mon-fri * * * * 150 | 151 | ; 需要跟单的交易客户端列表,以,(半角逗号)分割 152 | ; clients=client1,client2 153 | clients=client1 154 | 155 | 156 | ; ********************************************************* 157 | ; 聚宽擂台(商城)同步 158 | ; ********************************************************* 159 | [JoinQuantArena-1] 160 | type=JoinQuantArena 161 | 162 | ; 聚宽账号密码 163 | username= 164 | password= 165 | 166 | ; 擂台策略 URL 中 post后面字符 167 | ; 例如,擂台策略 URL 为:https://www.joinquant.com/post/4805?f=sharelist&m=list 168 | ; 则此处填写:4805 169 | ; 注意:若填写商城策略,该账户需有权限,否则无法抓取 170 | arena_id= 171 | 172 | ; 是否启用? 173 | enabled=false 174 | 175 | ; 排练模式? 176 | ; 排练模式不会下单,可用于在收盘阶段进行测试 177 | dry_run=false 178 | 179 | ; 默认设置为:星期一至星期五 9:00 到 15:00 每10分钟同步一次 180 | schedule=0 1/10 9-14 mon-fri * * * * 181 | 182 | ; 同步前是否撤销未成交订单? 183 | ; 由于未成交买单会占用可用资金,导致调仓计算不准确。撤销未成交订单可以有效避免该问题。 184 | ; 如果实盘账户无人工干预的情况,可以设置为 false。 185 | pre_clear=false 186 | 187 | ; 需要同步的交易客户端列表,以,(半角逗号)分割 188 | ; clients=client1,client2 189 | clients=client1 190 | 191 | ; 其他资产价值 192 | ; 基金及其他非场内资产价值,该项配置用于校验账户 193 | other_value: 0.0 194 | 195 | ; 总资产价值偏差率 196 | ; 该项配置用于校验账户 197 | total_value_deviation_rate: 0.001 198 | 199 | ; 保留名单,每行一个代码或正则表达式 200 | ; 比如:B股、港股、逆回购、新股、货币基金等保留的证券代码 201 | ; 其中: 202 | ; 含有非数字的代码:\D 203 | ; B股代码:^[92] 204 | ; 港股代码:^[\d]{5}$ 205 | ; 逆回购代码:^(204|131) 206 | ; 新标准券代码:888880 207 | reserved_securities= 208 | \D 209 | ^[92] 210 | ^[\d]{5}$ 211 | ^(204|131) 212 | 888880 213 | 214 | ; 最小订单金额,低于该值的订单将被忽略,以防因为价格波动导致的频繁调仓 215 | ; 取值可以为数值,或者百分比 216 | min_order_value=1% 217 | 218 | ; 最大订单金额,用于分单 219 | ; 取值为数值 220 | max_order_value=200000 221 | 222 | ; 轮次间隔时间,单位为秒 223 | round_interval=10 224 | 225 | ; 批次间隔时间,单位为秒 226 | batch_interval=1 227 | 228 | ; 下单间隔时间,单位为秒 229 | order_interval=1 230 | 231 | ; 额外轮次 232 | extra_rounds=0 233 | 234 | 235 | ; ********************************************************* 236 | ; 米筐策略跟单 237 | ; ********************************************************* 238 | [RiceQuant-1] 239 | type=RiceQuant 240 | 241 | ; 手机号码前需加上 +86,例如:+8615012345678 242 | username= 243 | password= 244 | 245 | ; 模拟交易 URL 中 最后一个 / 后面的那串字符 246 | ; 例如,模拟交易 URL 为:https://www.ricequant.com/pt/454879/1226010 247 | ; 则此处填写:1226010 248 | run_id= 249 | 250 | ; 是否启用? 251 | enabled=false 252 | 253 | ; 254 | ; 默认设置为:星期一至星期五 9:00 到 15:00 每分钟的第 30 秒 255 | schedule=30 */1 9-15 mon-fri * * * * 256 | 257 | ; 需要跟单的交易客户端列表,以,(半角逗号)分割 258 | ; clients=client1,client2 259 | clients=client1 260 | 261 | 262 | ; ********************************************************* 263 | ; 优矿策略跟单 264 | ; ********************************************************* 265 | [Uqer-1] 266 | type=Uqer 267 | 268 | username= 269 | password= 270 | 271 | ; 模拟交易 URL 中 strategy/ 后面的那串字符(不包含 /overview) 272 | ; 例如,模拟交易 URL 为:https://uqer.io/trade/strategy/18551/overview 273 | ; 则此处填写:18551 274 | strategy= 275 | 276 | ; 是否启用? 277 | enabled=false 278 | 279 | ; 280 | ; 默认设置为:星期一至星期五 9:00 到 15:00 每分钟的第 30 秒 281 | schedule=30 */1 9-15 mon-fri * * * * 282 | 283 | ; 需要跟单的交易客户端列表,以,(半角逗号)分割 284 | ; clients=client1,client2 285 | clients=client1 286 | 287 | 288 | ; ********************************************************* 289 | ; 果仁策略同步 290 | ; ********************************************************* 291 | ; 自动同步采用多个轮次,典型的轮次如下: 292 | ; 1. 卖单及可用资金可以满足的买单 293 | ; 2. 前一次未成交的部分卖单及可用资金可以满足的买单 294 | ; 3. 额外轮次将重复步骤 2 295 | ; 296 | [Guorn-1] 297 | type=Guorn 298 | 299 | username= 300 | password= 301 | 302 | ; 策略 URL 中 sid= 后面的那串字符 303 | ; 例如,策略 URL 为:https://guorn.com/stock/strategy?sid=7379.R.73780198158364 304 | ; 则此处填写:7379.R.73780198158364 305 | sid= 306 | 307 | ; 是否启用? 308 | enabled=false 309 | 310 | ; 排练模式? 311 | ; 排练模式不会下单,可用于在收盘阶段进行测试 312 | dry_run=false 313 | 314 | ; 默认设置为:星期一至星期五 9:30 到 15:00 每小时第 40 分 315 | schedule=0 40 9-14 mon-fri * * * * 316 | 317 | ; 同步前是否撤销未成交订单? 318 | ; 由于未成交买单会占用可用资金,导致调仓计算不准确。撤销未成交订单可以有效避免该问题。 319 | ; 如果实盘账户无人工干预的情况,可以设置为 false。 320 | pre_clear=false 321 | 322 | ; 需要同步的交易客户端列表,以,(半角逗号)分割 323 | ; clients=client1,client2 324 | clients=client1 325 | 326 | ; 其他资产价值 327 | ; 基金及其他非场内资产价值,该项配置用于校验账户 328 | other_value: 0.0 329 | 330 | ; 总资产价值偏差率 331 | ; 该项配置用于校验账户 332 | total_value_deviation_rate: 0.001 333 | 334 | ; 保留名单,每行一个代码或正则表达式 335 | ; 比如:B股、港股、逆回购、新股、货币基金等保留的证券代码 336 | ; 其中: 337 | ; 含有非数字的代码:\D 338 | ; B股代码:^[92] 339 | ; 港股代码:^[\d]{5}$ 340 | ; 逆回购代码:^(204|131) 341 | ; 新标准券代码:888880 342 | reserved_securities= 343 | \D 344 | ^[92] 345 | ^[\d]{5}$ 346 | ^(204|131) 347 | 888880 348 | 349 | ; 最小订单金额,低于该值的订单将被忽略,以防因为价格波动导致的频繁调仓 350 | ; 取值可以为数值,或者百分比 351 | min_order_value=1% 352 | 353 | ; 最大订单金额,用于分单 354 | ; 取值为数值 355 | max_order_value=200000 356 | 357 | ; 轮次间隔时间,单位为秒 358 | round_interval=10 359 | 360 | ; 批次间隔时间,单位为秒 361 | batch_interval=1 362 | 363 | ; 下单间隔时间,单位为秒 364 | order_interval=1 365 | 366 | ; 额外轮次 367 | extra_rounds=0 368 | -------------------------------------------------------------------------------- /strategyease_sdk/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import re 5 | from enum import Enum 6 | 7 | import lxml.html 8 | import pandas as pd 9 | import requests 10 | import six 11 | import tushare as ts 12 | from lxml import etree 13 | from requests import Request 14 | from requests.auth import HTTPBasicAuth 15 | from six import StringIO 16 | from six.moves.urllib.parse import urlencode 17 | 18 | 19 | class MediaType(Enum): 20 | DEFAULT = 'application/json' 21 | UNIFIED = 'application/vnd.quant.unified+json' 22 | JOIN_QUANT = 'application/vnd.joinquant+json' 23 | 24 | 25 | class ConnectionMethod(Enum): 26 | DIRECT = 'DIRECT' 27 | PROXY = 'PROXY' 28 | 29 | 30 | class Client(object): 31 | VERSION = 'v1.0' 32 | KEY_REGEX = r'key=([^&]*)' 33 | 34 | def __init__(self, logger=None, **kwargs): 35 | if logger is not None: 36 | self._logger = logger 37 | else: 38 | import logging 39 | self._logger = logging.getLogger(__name__) 40 | self._connection_method = ConnectionMethod[kwargs.pop('connection_method', 'DIRECT')] 41 | if self._connection_method is ConnectionMethod.DIRECT: 42 | self._host = kwargs.pop('host', 'localhost') 43 | self._port = kwargs.pop('port', 8888) 44 | else: 45 | self._proxy_base_url = kwargs.pop('proxy_base_url') 46 | self._proxy_username = kwargs.pop('proxy_username') 47 | self._proxy_password = kwargs.pop('proxy_password') 48 | self._instance_id = kwargs.pop('instance_id') 49 | self._base_url = self.__create_base_url() 50 | self._key = kwargs.pop('key', '') 51 | self._client = kwargs.pop('client', '') 52 | self._timeout = kwargs.pop('timeout', (5.0, 10.0)) 53 | 54 | @property 55 | def host(self): 56 | return self._host 57 | 58 | @host.setter 59 | def host(self, value): 60 | self._host = value 61 | 62 | @property 63 | def port(self): 64 | return self._port 65 | 66 | @port.setter 67 | def port(self, value): 68 | self._port = value 69 | 70 | @property 71 | def key(self): 72 | return self._key 73 | 74 | @key.setter 75 | def key(self, value): 76 | self._key = value 77 | 78 | @property 79 | def timeout(self): 80 | return self._timeout 81 | 82 | @timeout.setter 83 | def timeout(self, value): 84 | self._timeout = value 85 | 86 | def get_statuses(self, timeout=None): 87 | request = Request('GET', self.__create_url(None, 'statuses')) 88 | response = self.__send_request(request, timeout) 89 | return response.json() 90 | 91 | def get_account(self, client=None, timeout=None): 92 | request = Request('GET', self.__create_url(client, 'accounts')) 93 | response = self.__send_request(request, timeout) 94 | return response.json() 95 | 96 | # You should use get_portfolio 97 | def get_positions(self, client=None, media_type=MediaType.DEFAULT, timeout=None): 98 | request = Request('GET', self.__create_url(client, 'positions')) 99 | request.headers['Accept'] = media_type.value 100 | response = self.__send_request(request, timeout) 101 | json = response.json() 102 | if media_type == MediaType.DEFAULT: 103 | sub_accounts = pd.DataFrame(json['subAccounts']).T 104 | positions = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns']) 105 | portfolio = {'sub_accounts': sub_accounts, 'positions': positions} 106 | return portfolio 107 | return json 108 | 109 | def get_portfolio(self, client=None, media_type=MediaType.DEFAULT, timeout=None): 110 | request = Request('GET', self.__create_url(client, 'portfolios')) 111 | request.headers['Accept'] = media_type.value 112 | response = self.__send_request(request, timeout) 113 | json = response.json() 114 | if media_type == MediaType.DEFAULT: 115 | sub_accounts = pd.DataFrame(json['subAccounts']).T 116 | positions = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns']) 117 | portfolio = {'sub_accounts': sub_accounts, 'positions': positions} 118 | return portfolio 119 | return json 120 | 121 | def get_orders(self, client=None, status="", timeout=None): 122 | request = Request('GET', self.__create_url(client, 'orders', status=status)) 123 | response = self.__send_request(request, timeout) 124 | json = response.json() 125 | df = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns']) 126 | return df 127 | 128 | def buy(self, client=None, timeout=None, **kwargs): 129 | kwargs['action'] = 'BUY' 130 | return self.__execute(client, timeout, **kwargs) 131 | 132 | def sell(self, client=None, timeout=None, **kwargs): 133 | kwargs['action'] = 'SELL' 134 | return self.__execute(client, timeout, **kwargs) 135 | 136 | def ipo(self, client=None, timeout=None, **kwargs): 137 | kwargs['action'] = 'IPO' 138 | return self.__execute(client, timeout, **kwargs) 139 | 140 | def execute(self, client=None, timeout=None, **kwargs): 141 | return self.__execute(client, timeout, **kwargs) 142 | 143 | def cancel(self, client=None, order_id=None, symbol=None, timeout=None): 144 | request = Request('DELETE', self.__create_order_url(client, order_id, symbol=symbol)) 145 | self.__send_request(request, timeout) 146 | 147 | def cancel_all(self, client=None, timeout=None): 148 | request = Request('DELETE', self.__create_order_url(client)) 149 | self.__send_request(request, timeout) 150 | 151 | def query(self, client=None, type=None, navigation=None, timeout=None): 152 | request = Request('GET', self.__create_url(client, 'reports', type=type, navigation=navigation)) 153 | response = self.__send_request(request, timeout) 154 | json = response.json() 155 | df = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns']) 156 | return df 157 | 158 | def query_new_stocks(self): 159 | return self.__query_new_stocks() 160 | 161 | def query_convertible_bonds(self): 162 | return self.__query_convertible_bonds() 163 | 164 | def purchase_new_stocks(self, client=None, timeout=None): 165 | today = datetime.datetime.strftime(datetime.datetime.today(), '%Y-%m-%d') 166 | df = self.query_new_stocks() 167 | df = df[(df.ipo_date == today)] 168 | self._logger.info('今日有[{}]支可申购新股'.format(len(df))) 169 | for index, row in df.iterrows(): 170 | try: 171 | order = { 172 | 'symbol': row['xcode'], 173 | 'price': row['price'], 174 | 'amountProportion': 'ALL' 175 | } 176 | self._logger.info('申购新股:{}'.format(order)) 177 | self.ipo(client, timeout, **order) 178 | except Exception as e: 179 | self._logger.error( 180 | '客户端[{}]申购新股[{}({})]失败\n{}'.format((client or self._client), row['name'], row['code'], e)) 181 | 182 | def purchase_convertible_bonds(self, client=None, timeout=None): 183 | today = datetime.datetime.strftime(datetime.datetime.today(), '%Y-%m-%d') 184 | df = self.query_convertible_bonds() 185 | df = df[(df.ipo_date == today)] 186 | self._logger.info('今日有[{}]支可申购转债'.format(len(df))) 187 | for index, row in df.iterrows(): 188 | try: 189 | order = { 190 | 'symbol': row['xcode'], 191 | 'price': 100, 192 | 'amountProportion': 'ALL' 193 | } 194 | self._logger.info('申购转债:{}'.format(order)) 195 | self.buy(client, timeout, **order) 196 | except Exception as e: 197 | self._logger.error( 198 | '客户端[{}]申购转债[{}({})]失败\n{}'.format((client or self._client), row['bname'], row['xcode'], e)) 199 | 200 | def create_adjustment(self, client=None, request_json=None, timeout=None): 201 | request = Request('POST', self.__create_url(client, 'adjustments'), json=request_json) 202 | request.headers['Content-Type'] = MediaType.UNIFIED.value 203 | response = self.__send_request(request, timeout) 204 | json = response.json() 205 | return json 206 | 207 | def start_clients(self, timeout=None): 208 | self.__change_clients_status('LOGGED') 209 | 210 | def shutdown_clients(self, timeout=None): 211 | self.__change_clients_status('STOPPED') 212 | 213 | def __execute(self, client=None, timeout=None, **kwargs): 214 | if not kwargs.get('type'): 215 | kwargs['type'] = 'LIMIT' 216 | request = Request('POST', self.__create_order_url(client), json=kwargs) 217 | response = self.__send_request(request) 218 | return response.json() 219 | 220 | def __change_clients_status(self, status, timeout=None): 221 | request = Request('PATCH', self.__create_url(None, 'clients'), json={ 222 | 'status': status 223 | }) 224 | self.__send_request(request, timeout) 225 | 226 | def __query_new_stocks(self): 227 | DATA_URL = 'http://vip.stock.finance.sina.com.cn/corp/view/vRPD_NewStockIssue.php?page=1&cngem=0&orderBy=NetDate&orderType=desc' 228 | html = lxml.html.parse(DATA_URL) 229 | res = html.xpath('//table[@id=\"NewStockTable\"]/tr') 230 | if six.PY2: 231 | sarr = [etree.tostring(node) for node in res] 232 | else: 233 | sarr = [etree.tostring(node).decode('utf-8') for node in res] 234 | sarr = ''.join(sarr) 235 | sarr = sarr.replace('*', '') 236 | sarr = '%s
' % sarr 237 | df = pd.read_html(StringIO(sarr), skiprows=[0, 1])[0] 238 | df = df.select(lambda x: x in [0, 1, 2, 3, 7], axis=1) 239 | df.columns = ['code', 'xcode', 'name', 'ipo_date', 'price'] 240 | df['code'] = df['code'].map(lambda x: str(x).zfill(6)) 241 | df['xcode'] = df['xcode'].map(lambda x: str(x).zfill(6)) 242 | return df 243 | 244 | def __query_convertible_bonds(self): 245 | df = ts.new_cbonds() 246 | return df 247 | 248 | def __create_order_url(self, client=None, order_id=None, **params): 249 | return self.__create_url(client, 'orders', order_id, **params) 250 | 251 | def __create_url(self, client, resource, resource_id=None, **params): 252 | all_params = dict((k, v) for k, v in params.items() if v is not None) 253 | all_params.update(client=(client or self._client)) 254 | all_params.update(key=(self._key or '')) 255 | if resource_id is None: 256 | path = '/{}'.format(resource) 257 | else: 258 | path = '/{}/{}'.format(resource, resource_id) 259 | url = '{}/api/{}{}?{}'.format(self._base_url, self.VERSION, path, urlencode(all_params)) 260 | return url 261 | 262 | def __create_base_url(self): 263 | if self._connection_method is ConnectionMethod.DIRECT: 264 | return 'http://{}:{}'.format(self._host, self._port) 265 | else: 266 | return self._proxy_base_url 267 | 268 | def __send_request(self, request, timeout=None): 269 | if self._connection_method is ConnectionMethod.PROXY: 270 | request.auth = HTTPBasicAuth(self._proxy_username, self._proxy_password) 271 | request.headers['X-Instance-ID'] = self._instance_id 272 | prepared_request = request.prepare() 273 | self.__log_request(prepared_request) 274 | with requests.sessions.Session() as session: 275 | response = session.send(prepared_request, timeout=(timeout or self._timeout)) 276 | self.__log_response(response) 277 | response.raise_for_status() 278 | return response 279 | 280 | def __log_request(self, prepared_request): 281 | url = self.__eliminate_privacy(prepared_request.path_url) 282 | if prepared_request.body is None: 283 | self._logger.info('Request:\n{} {}'.format(prepared_request.method, url)) 284 | else: 285 | self._logger.info('Request:\n{} {}\n{}'.format(prepared_request.method, url, prepared_request.body)) 286 | 287 | def __log_response(self, response): 288 | message = u'Response:\n{} {}\n{}'.format(response.status_code, response.reason, response.text) 289 | if response.status_code == 200: 290 | self._logger.info(message) 291 | else: 292 | self._logger.error(message) 293 | 294 | @classmethod 295 | def __eliminate_privacy(cls, url): 296 | match = re.search(cls.KEY_REGEX, url) 297 | if match is None: 298 | return url 299 | key = match.group(1) 300 | masked_key = '*' * len(key) 301 | url = re.sub(cls.KEY_REGEX, "key={}".format(masked_key), url) 302 | return url 303 | -------------------------------------------------------------------------------- /strategyease_sdk/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import division 4 | 5 | from enum import Enum 6 | 7 | 8 | class Adjustment(object): 9 | @staticmethod 10 | def to_json(instance): 11 | json = { 12 | 'sourcePortfolio': Portfolio.to_json(instance.source_portfolio), 13 | 'targetPortfolio': Portfolio.to_json(instance.target_portfolio), 14 | 'schema': AdjustmentSchema.to_json(instance.schema) 15 | } 16 | return json 17 | 18 | @classmethod 19 | def from_json(cls, json): 20 | instance = Adjustment() 21 | instance.id = json.get('id', None) 22 | instance.status = json.get('status', None) 23 | batches = [] 24 | for batch_json in json['batches']: 25 | batch = [] 26 | for order_json in batch_json: 27 | batch.append(Order.from_json(order_json)) 28 | batches.append(batch) 29 | instance.batches = batches 30 | instance._progress = AdjustmentProgressGroup.from_json(json['progress']) 31 | return instance 32 | 33 | def empty(self): 34 | return not self.batches 35 | 36 | @property 37 | def id(self): 38 | return self._id 39 | 40 | @id.setter 41 | def id(self, value): 42 | self._id = value 43 | 44 | @property 45 | def status(self): 46 | return self._status 47 | 48 | @status.setter 49 | def status(self, value): 50 | self._status = value 51 | 52 | @property 53 | def target_portfolio(self): 54 | return self._target_portfolio 55 | 56 | @target_portfolio.setter 57 | def target_portfolio(self, value): 58 | self._target_portfolio = value 59 | 60 | @property 61 | def schema(self): 62 | return self._schema 63 | 64 | @schema.setter 65 | def schema(self, value): 66 | self._schema = value 67 | 68 | @property 69 | def batches(self): 70 | return self._batches 71 | 72 | @batches.setter 73 | def batches(self, value): 74 | self._batches = value 75 | 76 | @property 77 | def progress(self): 78 | return self._progress 79 | 80 | @progress.setter 81 | def progress(self, value): 82 | self._progress = value 83 | 84 | 85 | class AdjustmentSchema(object): 86 | @staticmethod 87 | def to_json(instance): 88 | json = { 89 | 'minOrderValue': instance.min_order_value, 90 | 'maxOrderValue': instance.max_order_value, 91 | 'reservedSecurities': instance.reserved_securities, 92 | } 93 | return json 94 | 95 | def __init__(self, reserved_securities, min_order_value, max_order_value): 96 | self._reserved_securities = reserved_securities 97 | self._min_order_value = min_order_value 98 | self._max_order_value = max_order_value 99 | 100 | @property 101 | def reserved_securities(self): 102 | return self._reserved_securities 103 | 104 | @reserved_securities.setter 105 | def reserved_securities(self, value): 106 | self._reserved_securities = value 107 | 108 | @property 109 | def min_order_value(self): 110 | return self._min_order_value 111 | 112 | @min_order_value.setter 113 | def min_order_value(self, value): 114 | self._min_order_value = value 115 | 116 | @property 117 | def max_order_value(self): 118 | return self._max_order_value 119 | 120 | @max_order_value.setter 121 | def max_order_value(self, value): 122 | self._max_order_value = value 123 | 124 | 125 | class AdjustmentProgressGroup(object): 126 | @staticmethod 127 | def from_json(json): 128 | instance = AdjustmentProgressGroup() 129 | instance._today = AdjustmentProgress.from_json(json['today']) 130 | instance._overall = AdjustmentProgress.from_json(json['overall']) 131 | return instance 132 | 133 | def __str__(self): 134 | str = "今日进度:{0:>.0f}% -> {1:>.0f}%;总进度:{2:>.0f}% -> {3:>.0f}%".format( 135 | self.today.before * 100, self.today.after * 100, 136 | self.overall.before * 100, self.overall.after * 100 137 | ) 138 | return str 139 | 140 | @property 141 | def today(self): 142 | return self._today 143 | 144 | @today.setter 145 | def today(self, value): 146 | self._today = value 147 | 148 | @property 149 | def overall(self): 150 | return self._overall 151 | 152 | @overall.setter 153 | def overall(self, value): 154 | self._overall = value 155 | 156 | 157 | class AdjustmentProgress(object): 158 | @staticmethod 159 | def from_json(json): 160 | instance = AdjustmentProgress() 161 | instance.before = json['before'] 162 | instance.after = json['after'] 163 | return instance 164 | 165 | @property 166 | def before(self): 167 | return self._before 168 | 169 | @before.setter 170 | def before(self, value): 171 | self._before = value 172 | 173 | @property 174 | def after(self): 175 | return self._after 176 | 177 | @after.setter 178 | def after(self, value): 179 | self._after = value 180 | 181 | 182 | class Portfolio(object): 183 | @staticmethod 184 | def to_json(instance): 185 | positions_json = [] 186 | for security, position in instance.positions.items(): 187 | positions_json.append(Position.to_json(position)) 188 | json = { 189 | 'summary': { 190 | 'availableCash': instance.available_cash, 191 | 'totalValue': instance.total_value, 192 | 'otherValue': instance.other_value, 193 | 'totalValueDeviationRate': instance.total_value_deviation_rate, 194 | 'positionsValue': instance.positions_value, 195 | }, 196 | 'positions': positions_json 197 | } 198 | return json 199 | 200 | def __init__(self, available_cash=None, total_value=None, other_value=None, total_value_deviation_rate=None): 201 | self._available_cash = available_cash 202 | self._total_value = total_value 203 | self._other_value = other_value 204 | self._total_value_deviation_rate = total_value_deviation_rate 205 | self._positions_value = 0 206 | self._positions = dict() 207 | 208 | def __getitem__(self, security): 209 | return self._positions[security] 210 | 211 | def __setitem__(self, security, position): 212 | self._positions[security] = position 213 | 214 | @property 215 | def fingerprint(self): 216 | result = dict((security, position.total_amount) for security, position in self._positions.items()) 217 | return result 218 | 219 | @property 220 | def available_cash(self): 221 | return self._available_cash 222 | 223 | @available_cash.setter 224 | def available_cash(self, value): 225 | self._available_cash = value 226 | 227 | @property 228 | def total_value(self): 229 | return self._total_value 230 | 231 | @total_value.setter 232 | def total_value(self, value): 233 | self._total_value = value 234 | 235 | @property 236 | def other_value(self): 237 | return self._other_value 238 | 239 | @other_value.setter 240 | def other_value(self, value): 241 | self._other_value = value 242 | 243 | @property 244 | def total_value_deviation_rate(self): 245 | return self._total_value_deviation_rate 246 | 247 | @total_value_deviation_rate.setter 248 | def total_value_deviation_rate(self, value): 249 | self._total_value_deviation_rate = value 250 | 251 | @property 252 | def positions_value(self): 253 | return self._positions_value 254 | 255 | @positions_value.setter 256 | def positions_value(self, value): 257 | self._positions_value = value 258 | 259 | @property 260 | def positions(self): 261 | return self._positions 262 | 263 | @positions.setter 264 | def positions(self, value): 265 | self._positions = value 266 | 267 | def add_position(self, position): 268 | self._positions_value += position.value 269 | self._positions[position.security] = position 270 | 271 | def rebalance(self): 272 | if self._available_cash is None: 273 | self._available_cash = self._total_value - self._positions_value 274 | elif self._total_value is None: 275 | self._total_value = self._available_cash + self.positions_value 276 | if self._available_cash < 0: 277 | self._available_cash = 0 278 | self._total_value = self._positions_value 279 | 280 | 281 | class Position(object): 282 | @staticmethod 283 | def to_json(instance): 284 | json = { 285 | 'security': instance.security, 286 | 'price': instance.price, 287 | 'totalAmount': instance.total_amount, 288 | 'closeableAmount': instance.closeable_amount, 289 | } 290 | return json 291 | 292 | def __init__(self, security=None, price=None, total_amount=0, closeable_amount=0): 293 | self._security = self._normalize_security(security) 294 | self._price = price 295 | self._total_amount = total_amount 296 | self._closeable_amount = total_amount if closeable_amount is None else closeable_amount 297 | if price is not None and total_amount is not None: 298 | self._value = price * total_amount 299 | 300 | @property 301 | def security(self): 302 | return self._security 303 | 304 | @security.setter 305 | def security(self, value): 306 | self._security = self._normalize_security(value) 307 | 308 | @property 309 | def price(self): 310 | return self._price 311 | 312 | @price.setter 313 | def price(self, value): 314 | self._price = value 315 | 316 | @property 317 | def total_amount(self): 318 | return self._total_amount 319 | 320 | @total_amount.setter 321 | def total_amount(self, value): 322 | self._total_amount = value 323 | 324 | @property 325 | def closeable_amount(self): 326 | return self._closeable_amount 327 | 328 | @closeable_amount.setter 329 | def closeable_amount(self, value): 330 | self._closeable_amount = value 331 | 332 | @property 333 | def value(self): 334 | return self._value 335 | 336 | @value.setter 337 | def value(self, value): 338 | self._value = value 339 | if self._price != 0: 340 | self._total_amount = self._value / self._price 341 | 342 | def _normalize_security(self, security): 343 | return security.split('.')[0] if security else None 344 | 345 | 346 | class OrderAction(Enum): 347 | OPEN = 'OPEN' 348 | CLOSE = 'CLOSE' 349 | 350 | 351 | class OrderStyle(Enum): 352 | LIMIT = 'LIMIT' 353 | MARKET = 'MARKET' 354 | 355 | 356 | class OrderStatus(Enum): 357 | open = 0 358 | filled = 1 359 | canceled = 2 360 | rejected = 3 361 | held = 4 362 | 363 | 364 | class Order(object): 365 | @staticmethod 366 | def from_json(json): 367 | order = Order() 368 | order.action = OrderAction.OPEN if json['action'] == 'BUY' else OrderAction.CLOSE 369 | order.security = json['symbol'] 370 | order.style = OrderStyle(json['type']) 371 | order.price = json['price'] 372 | order.amount = json['amount'] 373 | order.amountProportion = json.get('amountProportion', '') 374 | return order 375 | 376 | @staticmethod 377 | def from_e_order(**kwargs): 378 | order = Order() 379 | order.action = OrderAction.OPEN if kwargs['action'] == 'BUY' else OrderAction.CLOSE 380 | order.security = kwargs['symbol'] 381 | order.style = OrderStyle(kwargs['type']) 382 | order.price = kwargs['price'] 383 | order.amount = kwargs.get('amount', 0) 384 | order.amountProportion = kwargs.get('amountProportion', '') 385 | return order 386 | 387 | def __init__(self, id=None, action=None, security=None, amount=None, amountProportion=None, price=None, style=None, 388 | status=OrderStatus.open, add_time=None): 389 | self._id = id 390 | self._action = action 391 | self._security = security 392 | self._amount = amount 393 | self._amountProportion = amountProportion 394 | self._price = price 395 | self._style = style 396 | self._status = status 397 | self._add_time = add_time 398 | 399 | def __str__(self): 400 | str = "以 {0:>7.3f}元 {1}{2} {3:>5} {4}".format( 401 | self.price, 402 | '限价' if self.style == OrderStyle.LIMIT else '市价', 403 | '买入' if self.action == OrderAction.OPEN else '卖出', 404 | self.amount, 405 | self.security 406 | ) 407 | return str 408 | 409 | def to_e_order(self): 410 | e_order = dict( 411 | action=('BUY' if self._action == OrderAction.OPEN else 'SELL'), 412 | symbol=self._security, 413 | type=self._style.name, 414 | priceType=(0 if self._style == OrderStyle.LIMIT else 4), 415 | price=self._price, 416 | amount=self._amount, 417 | amountProportion=self._amountProportion or '' 418 | ) 419 | return e_order 420 | 421 | @property 422 | def value(self): 423 | return self._amount * self._price 424 | 425 | @property 426 | def id(self): 427 | return self._id 428 | 429 | @id.setter 430 | def id(self, value): 431 | self._id = value 432 | 433 | @property 434 | def action(self): 435 | return self._action 436 | 437 | @action.setter 438 | def action(self, value): 439 | self._action = value 440 | 441 | @property 442 | def security(self): 443 | return self._security 444 | 445 | @security.setter 446 | def security(self, value): 447 | self._security = value 448 | 449 | @property 450 | def amount(self): 451 | return self._amount 452 | 453 | @amount.setter 454 | def amount(self, value): 455 | self._amount = value 456 | 457 | @property 458 | def amountProportion(self): 459 | return self._amountProportion 460 | 461 | @amountProportion.setter 462 | def amountProportion(self, value): 463 | self._amountProportion = value 464 | 465 | @property 466 | def price(self): 467 | return self._price 468 | 469 | @price.setter 470 | def price(self, value): 471 | self._price = value 472 | 473 | @property 474 | def style(self): 475 | return self._style 476 | 477 | @style.setter 478 | def style(self, value): 479 | self._style = value 480 | 481 | @property 482 | def status(self): 483 | return self._status 484 | 485 | @status.setter 486 | def status(self, value): 487 | self._status = value 488 | 489 | @property 490 | def add_time(self): 491 | return self._add_time 492 | 493 | @add_time.setter 494 | def add_time(self, value): 495 | self._add_time = value 496 | -------------------------------------------------------------------------------- /strategyease_sdk/base_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import copy 4 | import time 5 | 6 | import tushare as ts 7 | 8 | from strategyease_sdk.client import * 9 | from strategyease_sdk.models import * 10 | from strategyease_sdk.support import * 11 | 12 | 13 | class BaseStrategyManagerFactory(object): 14 | def __init__(self): 15 | self._config = self._create_config() 16 | 17 | def create(self, id): 18 | traders = self._create_traders(id) 19 | return StrategyManager(id, self._create_logger(), self._config, traders, self._get_context()) 20 | 21 | def _get_context(self): 22 | pass 23 | 24 | def _create_traders(self, id): 25 | traders = OrderedDict() 26 | for trader_id, trader_config in self._config.build_trader_configs(id).items(): 27 | trader = self._create_trader(trader_config) 28 | traders[trader_id] = trader 29 | return traders 30 | 31 | def _create_trader(self, trader_config): 32 | return StrategyTrader(self._create_logger(), trader_config, self._get_context()) 33 | 34 | def _create_logger(self): 35 | pass 36 | 37 | def _create_config(self): 38 | return StrategyConfig(self._get_context()) 39 | 40 | 41 | class BaseStrategyContext(object): 42 | def get_portfolio(self): 43 | pass 44 | 45 | def convert_order(self, quant_order): 46 | pass 47 | 48 | def has_open_orders(self): 49 | pass 50 | 51 | def cancel_open_orders(self): 52 | pass 53 | 54 | def cancel_order(self, quant_order): 55 | pass 56 | 57 | def read_file(self, path): 58 | pass 59 | 60 | def is_sim_trade(self): 61 | pass 62 | 63 | def is_backtest(self): 64 | pass 65 | 66 | def is_read_file_allowed(self): 67 | return False 68 | 69 | 70 | class BaseLogger(object): 71 | def debug(self, msg, *args, **kwargs): 72 | pass 73 | 74 | def info(self, msg, *args, **kwargs): 75 | pass 76 | 77 | def warning(self, msg, *args, **kwargs): 78 | pass 79 | 80 | def error(self, msg, *args, **kwargs): 81 | pass 82 | 83 | def exception(self, msg, *args, **kwargs): 84 | pass 85 | 86 | 87 | class StrategyManager(object): 88 | THEMATIC_BREAK = '-' * 50 89 | 90 | def __init__(self, id, logger, config, traders, strategy_context): 91 | self._id = id 92 | self._logger = logger 93 | self._config = config 94 | self._traders = traders 95 | self._strategy_context = strategy_context 96 | 97 | @property 98 | def id(self): 99 | return self._id 100 | 101 | @property 102 | def traders(self): 103 | return self._traders 104 | 105 | def purchase_new_stocks(self): 106 | for trader in self._traders.values(): 107 | try: 108 | trader.purchase_new_stocks() 109 | except: 110 | self._logger.exception('[%s] 打新失败', trader.id) 111 | 112 | def repo(self): 113 | try: 114 | security = '131810' 115 | quote_df = ts.get_realtime_quotes(security) 116 | order = { 117 | 'action': 'SELL', 118 | 'symbol': security, 119 | 'type': 'LIMIT', 120 | 'price': float(quote_df['bid'][0]), 121 | 'amountProportion': 'ALL' 122 | } 123 | for trader in self._traders.values(): 124 | try: 125 | trader.execute(**order) 126 | except: 127 | self._logger.exception('[%s] 逆回购失败', trader.id) 128 | except: 129 | self._logger.exception('逆回购失败') 130 | 131 | def purchase_convertible_bonds(self): 132 | for trader in self._traders.values(): 133 | try: 134 | trader.purchase_convertible_bonds() 135 | except: 136 | self._logger.exception('[%s] 申购转债失败', trader.id) 137 | 138 | def execute(self, order=None, **kwargs): 139 | if order is None and not kwargs: 140 | return 141 | for trader in self._traders.values(): 142 | try: 143 | trader.execute(order, **kwargs) 144 | except: 145 | self._logger.exception('[%s] 下单失败', trader.id) 146 | 147 | def cancel(self, order): 148 | for trader in self._traders.values(): 149 | try: 150 | trader.cancel(order) 151 | except: 152 | self._logger.exception('[%s] 撤单失败', trader.id) 153 | 154 | def work(self): 155 | stop_watch = StopWatch() 156 | stop_watch.start() 157 | self._logger.info("[%s] 开始工作", self._id) 158 | self._refresh() 159 | for id, trader in self._traders.items(): 160 | trader.work() 161 | stop_watch.stop() 162 | self._logger.info("[%s] 结束工作,总耗时[%s]", self._id, stop_watch.short_summary()) 163 | self._logger.info(self.THEMATIC_BREAK) 164 | 165 | def _refresh(self): 166 | if not self._strategy_context.is_read_file_allowed(): 167 | return 168 | self._config.reload() 169 | trader_configs = self._config.build_trader_configs(self._id) 170 | for id, trader in self._traders.items(): 171 | trader.set_config(trader_configs[id]) 172 | 173 | 174 | class StrategyTrader(object): 175 | def __init__(self, logger, config, strategy_context): 176 | self._logger = logger 177 | self._config = config 178 | self._strategy_context = strategy_context 179 | self._strategyease_client = Client(self._logger, **config['client']) 180 | self._order_id_to_info_map = {} 181 | self._expire_before = datetime.datetime.combine(datetime.date.today(), datetime.time.min) 182 | self._last_sync_portfolio_fingerprint = None 183 | 184 | @property 185 | def id(self): 186 | return self._config['id'] 187 | 188 | @property 189 | def client(self): 190 | return self._strategyease_client 191 | 192 | def set_config(self, config): 193 | self._config = config 194 | 195 | def purchase_new_stocks(self): 196 | if not self._pre_check(): 197 | return 198 | 199 | self._strategyease_client.purchase_new_stocks() 200 | 201 | def purchase_convertible_bonds(self): 202 | if not self._pre_check(): 203 | return 204 | 205 | self._strategyease_client.purchase_convertible_bonds() 206 | 207 | def execute(self, order=None, **kwargs): 208 | if not self._pre_check(): 209 | return 210 | 211 | if order is None: 212 | common_order = Order.from_e_order(**kwargs) 213 | else: 214 | common_order = self._normalize_order(order) 215 | 216 | try: 217 | actual_order = self._execute(common_order) 218 | return actual_order 219 | except Exception: 220 | self._logger.exception("[策略易] 下单异常") 221 | 222 | def cancel(self, order): 223 | if not self._pre_check(): 224 | return 225 | 226 | try: 227 | self._cancel(order) 228 | except: 229 | self._logger.exception("[策略易] 撤单异常") 230 | 231 | def work(self): 232 | if not self._pre_check(): 233 | return 234 | 235 | if self._config['mode'] == 'SYNC': 236 | self._sync() 237 | else: 238 | self._follow() 239 | 240 | def _sync(self): 241 | stop_watch = StopWatch() 242 | stop_watch.start() 243 | self._logger.info("[%s] 开始同步", self.id) 244 | try: 245 | if self._sync_config['pre-clear-for-sim']: 246 | self._cancel_all_for_sim() 247 | self._logger.info("[%s] 模拟盘撤销全部订单已完成", self.id) 248 | target_portfolio = self._strategy_context.get_portfolio() 249 | if self._should_sync(target_portfolio): 250 | if self._sync_config['pre-clear-for-live'] and not self._config['dry-run']: 251 | self._strategyease_client.cancel_all() 252 | time.sleep(self._sync_config['order-interval'] / 1000.0) 253 | self._logger.info("[%s] 实盘撤销全部订单已完成", self.id) 254 | 255 | is_sync = False 256 | for i in range(0, 2 + self._sync_config['extra-rounds']): 257 | self._logger.info("[%s] 开始第[%d]轮同步", self.id, i + 1) 258 | is_sync = self._sync_once(target_portfolio) 259 | self._logger.info("[%s] 结束第[%d]轮同步", self.id, i + 1) 260 | if is_sync: 261 | self._last_sync_portfolio_fingerprint = target_portfolio.fingerprint 262 | self._logger.info("[%s] 实盘已与模拟盘同步", self.id) 263 | break 264 | time.sleep(self._sync_config['round-interval'] / 1000.0) 265 | self._logger.info(u"[%s] 结束同步,状态:%s", self.id, "已完成" if is_sync else "未完成") 266 | except: 267 | self._logger.exception("[%s] 同步失败", self.id) 268 | stop_watch.stop() 269 | self._logger.info("[%s] 结束同步,耗时[%s]", self.id, stop_watch.short_summary()) 270 | 271 | def _follow(self): 272 | stop_watch = StopWatch() 273 | stop_watch.start() 274 | self._logger.info("[%s] 开始跟单", self.id) 275 | try: 276 | common_orders = [] 277 | all_common_orders = self._strategy_context.get_orders() 278 | for common_order in all_common_orders: 279 | if common_order.add_time >= self._strategy_context.get_current_time(): 280 | if common_order.status == OrderStatus.canceled: 281 | origin_order = copy.deepcopy(common_order) 282 | origin_order.status = OrderStatus.open 283 | common_orders.append(origin_order) 284 | else: 285 | common_orders.append(common_order) 286 | if common_order.status == OrderStatus.canceled: 287 | common_orders.append(common_order) 288 | 289 | common_orders = sorted(common_orders, key=lambda o: _PrioritizedOrder(o)) 290 | for common_order in common_orders: 291 | if common_order.status != OrderStatus.canceled: 292 | try: 293 | self._execute(common_order) 294 | except: 295 | self._logger.exception("[策略易] 下单异常") 296 | else: 297 | try: 298 | self._cancel(common_order) 299 | except: 300 | self._logger.exception("[策略易] 撤单异常") 301 | except: 302 | self._logger.exception("[%s] 跟单失败", self.id) 303 | stop_watch.stop() 304 | self._logger.info("[%s] 结束跟单,耗时[%s]", self.id, stop_watch.short_summary()) 305 | 306 | @property 307 | def _sync_config(self): 308 | return self._config['sync'] 309 | 310 | def _execute(self, order): 311 | if not self._should_run(): 312 | self._logger.info("[%s] %s", self.id, order) 313 | return None 314 | actual_order = self._do_execute(order) 315 | return actual_order 316 | 317 | def _cancel(self, order): 318 | if not self._should_run(): 319 | self._logger.info("[%s] 撤单 [%s]", self.id, order) 320 | return 321 | self._do_cancel(order) 322 | 323 | def _do_execute(self, order): 324 | common_order = self._normalize_order(order) 325 | e_order = common_order.to_e_order() 326 | actual_order = self._strategyease_client.execute(**e_order) 327 | self._order_id_to_info_map[common_order.id] = {'id': actual_order['id'], 'canceled': False} 328 | return actual_order 329 | 330 | def _do_cancel(self, order): 331 | if order is None: 332 | self._logger.info('[策略易] 委托为空,忽略撤单请求') 333 | return 334 | 335 | if isinstance(order, int): 336 | quant_order_id = order 337 | else: 338 | common_order = self._normalize_order(order) 339 | quant_order_id = common_order.id 340 | 341 | try: 342 | order_info = self._order_id_to_info_map[quant_order_id] 343 | if not order_info['canceled']: 344 | order_info['canceled'] = True 345 | self._strategyease_client.cancel(order_id=order_info['id']) 346 | except KeyError: 347 | self._logger.warning('[策略易] 未找到对应的委托编号') 348 | self._order_id_to_info_map[quant_order_id] = {'id': None, 'canceled': True} 349 | 350 | def _normalize_order(self, order): 351 | if isinstance(order, Order): 352 | common_order = order 353 | else: 354 | common_order = self._strategy_context.convert_order(order) 355 | return common_order 356 | 357 | def _should_run(self): 358 | if self._config['dry-run']: 359 | self._logger.debug("[策略易] 当前为排练模式,不执行下单、撤单请求") 360 | return False 361 | return True 362 | 363 | def _is_expired(self, common_order): 364 | return common_order.add_time < self._expire_before 365 | 366 | def _pre_check(self): 367 | if not self._config['enabled']: 368 | self._logger.info("[%s] 交易未启用,不执行", self.id) 369 | return False 370 | if not self._config['enabled-in-backtest']: 371 | if self._strategy_context.is_backtest(): 372 | self._logger.info("[%s] 当前为回测环境,不执行", self.id) 373 | return False 374 | return True 375 | 376 | def _should_sync(self, target_portfolio): 377 | if self._strategy_context.has_open_orders(): 378 | self._logger.info("[%s] 有未完成订单,不进行同步", self.id) 379 | return False 380 | is_changed = target_portfolio.fingerprint != self._last_sync_portfolio_fingerprint 381 | if not is_changed: 382 | self._logger.info("[%s] 模拟持仓未改变,不进行同步", self.id) 383 | return is_changed 384 | 385 | def _cancel_all_for_sim(self): 386 | self._strategy_context.cancel_open_orders() 387 | 388 | def _sync_once(self, target_portfolio): 389 | adjustment = self._create_adjustment(target_portfolio) 390 | self._log_progress(adjustment) 391 | self._execute_adjustment(adjustment) 392 | is_sync = adjustment.empty() 393 | return is_sync 394 | 395 | def _create_adjustment(self, target_portfolio): 396 | request = self._create_adjustment_request(target_portfolio) 397 | request_json = Adjustment.to_json(request) 398 | response_json = self._strategyease_client.create_adjustment(request_json=request_json) 399 | adjustment = Adjustment.from_json(response_json) 400 | return adjustment 401 | 402 | def _create_adjustment_request(self, target_portfolio): 403 | request = Adjustment() 404 | request.source_portfolio = Portfolio( 405 | other_value=self._sync_config['other-value'], 406 | total_value_deviation_rate=self._sync_config['total-value-deviation-rate'], 407 | ) 408 | request.target_portfolio = target_portfolio 409 | request.schema = AdjustmentSchema(self._sync_config['reserved-securities'], 410 | self._sync_config['min-order-value'], 411 | self._sync_config['max-order-value']) 412 | return request 413 | 414 | def _log_progress(self, adjustment): 415 | self._logger.info("[%s] %s", self.id, adjustment.progress) 416 | 417 | def _execute_adjustment(self, adjustment): 418 | for batch in adjustment.batches: 419 | for order in batch: 420 | self._execute(order) 421 | time.sleep(self._sync_config['order-interval'] / 1000.0) 422 | time.sleep(self._sync_config['batch-interval'] / 1000.0) 423 | 424 | 425 | class StrategyConfig(object): 426 | def __init__(self, strategy_context): 427 | self._strategy_context = strategy_context 428 | self._data = dict() 429 | self.reload() 430 | 431 | @property 432 | def data(self): 433 | return self._data 434 | 435 | def reload(self): 436 | content = self._strategy_context.read_file('strategyease_sdk_config.yaml') 437 | stream = six.BytesIO(content) 438 | self._data = yaml.load(stream, Loader=OrderedDictYAMLLoader) 439 | self._proxies = self._create_proxy_configs() 440 | stream.close() 441 | 442 | def build_trader_configs(self, id): 443 | trader_configs = OrderedDict() 444 | for raw_manager_config in self._data['managers']: 445 | if raw_manager_config['id'] == id: 446 | for raw_trader_config in raw_manager_config['traders']: 447 | trader_config = self._create_trader_config(raw_trader_config) 448 | trader_configs[raw_trader_config['id']] = trader_config 449 | break 450 | return trader_configs 451 | 452 | def _create_proxy_configs(self): 453 | proxies = {} 454 | for raw_proxy_config in self._data['proxies']: 455 | id = raw_proxy_config['id'] 456 | proxies[id] = raw_proxy_config 457 | return proxies 458 | 459 | def _create_trader_config(self, raw_trader_config): 460 | client_config = self._create_client_config(raw_trader_config) 461 | trader_config = copy.deepcopy(raw_trader_config) 462 | trader_config['client'] = client_config 463 | if 'sync' in trader_config: 464 | trader_config['sync']['reserved-securities'] = client_config.pop('reserved_securities', []) 465 | trader_config['sync']['other-value'] = client_config.pop('other_value', []) 466 | trader_config['sync']['total-value-deviation-rate'] = client_config.pop('total_value_deviation_rate', []) 467 | return trader_config 468 | 469 | def _create_client_config(self, raw_trader_config): 470 | client_config = None 471 | for raw_gateway_config in self._data['gateways']: 472 | for raw_client_config in raw_gateway_config['clients']: 473 | if raw_client_config['id'] == raw_trader_config['client']: 474 | connection_method = raw_gateway_config['connection-method'] 475 | client_config = { 476 | 'connection_method': connection_method, 477 | 'key': raw_gateway_config['key'], 478 | 'timeout': tuple([ 479 | raw_gateway_config['timeout']['connect'], 480 | raw_gateway_config['timeout']['read'], 481 | ]), 482 | 'client': raw_client_config['query'], 483 | 'reserved_securities': raw_client_config['reserved-securities'], 484 | 'other_value': raw_client_config['other-value'], 485 | 'total_value_deviation_rate': raw_client_config['total-value-deviation-rate'], 486 | } 487 | if connection_method == 'DIRECT': 488 | client_config.update({ 489 | 'host': raw_gateway_config['host'], 490 | 'port': raw_gateway_config['port'], 491 | }) 492 | else: 493 | proxy_config = self._proxies[raw_gateway_config['proxy']] 494 | client_config.update({ 495 | 'proxy_base_url': proxy_config['base-url'], 496 | 'proxy_username': proxy_config['username'], 497 | 'proxy_password': proxy_config['password'], 498 | 'instance_id': raw_gateway_config['instance-id'], 499 | }) 500 | break 501 | if client_config is not None: 502 | break 503 | return client_config 504 | 505 | 506 | class _PrioritizedOrder(object): 507 | def __init__(self, order): 508 | self.order = order 509 | 510 | def __lt__(self, other): 511 | x = self.order 512 | y = other.order 513 | if x.add_time != y.add_time: 514 | return x.add_time < y.add_time 515 | if x.status == OrderStatus.canceled: 516 | if y.status == OrderStatus.canceled: 517 | return x.id < y.id 518 | else: 519 | return False 520 | else: 521 | if y.status == OrderStatus.canceled: 522 | return True 523 | else: 524 | return x.id < y.id 525 | 526 | def __gt__(self, other): 527 | return other.__lt__(self) 528 | 529 | def __eq__(self, other): 530 | return (not self.__lt__(other)) and (not other.__lt__(self)) 531 | 532 | def __le__(self, other): 533 | return not self.__gt__(other) 534 | 535 | def __ge__(self, other): 536 | return not self.__lt__(other) 537 | 538 | def __ne__(self, other): 539 | return not self.__eq__(other) 540 | --------------------------------------------------------------------------------