Skip to content

Commit 784f398

Browse files
committed
Support Guorn sync
1 parent 8cd7e5f commit 784f398

File tree

15 files changed

+808
-22
lines changed

15 files changed

+808
-22
lines changed

README.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ShiPanE-Python-SDK
2222
- 聚宽(JoinQuant)集成
2323
- `米筐(RiceQuant)`_ 集成
2424
- 优矿(Uqer)集成
25+
- `果仁(Guorn)集成 <#果仁guorn集成>`__
2526

2627
基本用法
2728
--------------
@@ -210,6 +211,59 @@ Mac/Linux
210211

211212
见 `定时任务调度 <#定时任务调度>`__
212213

214+
果仁(Guorn)集成
215+
---------------------
216+
217+
一. 推送方式
218+
~~~~~~~~~~~~
219+
220+
| 不支持。
221+
222+
二. 抓取方式
223+
~~~~~~~~~~~~
224+
225+
采用定时轮询的方式。
226+
227+
准备工作
228+
^^^^^^^^
229+
230+
- 部署实盘易。
231+
- 测试通过。
232+
233+
步骤
234+
^^^^
235+
236+
见 `定时任务调度 <#定时任务调度>`__
237+
238+
字段要求
239+
^^^^^^^^
240+
241+
**资金** 包含以下字段
242+
243+
- 可用
244+
245+
**股份** 包含以下字段
246+
247+
- 证券数量
248+
249+
正则::code:`(证券|股份)(数量|余额)`
250+
251+
示例:证券数量、股份余额
252+
253+
- 可卖数量
254+
255+
正则::code:`(可卖|可用)(数量|余额)`
256+
257+
示例:可卖数量、可用数量
258+
259+
- 当前价
260+
261+
正则::code:`(当前|最新|参考市)价`
262+
263+
示例:当前价、最新价
264+
265+
字段映射可通过实盘易进行设置。详见:`实盘易3.6.0.0自定义字段映射使用说明 <http://www.iguuu.com/discuz/thread-7885-1-1.html>`_
266+
213267
其他语言 SDK
214268
------------
215269

config/scheduler-example.ini

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,65 @@ schedule=30 */1 9-15 mon-fri * * * *
102102
; 需要跟单的交易客户端列表,以,(半角逗号)分割
103103
; clients=client1,client2
104104
clients=client1
105+
106+
;
107+
; 果仁自动同步设置
108+
; 自动同步采用分批下单,典型的下单步骤如下:
109+
; 1. 卖单及可用资金可以满足的买单
110+
; 2. 前一次未成交的部分卖单及可用资金可以满足的买单
111+
; 3. 额外下单批次将重复步骤 2
112+
;
113+
[Guorn]
114+
username=
115+
password=
116+
117+
; 策略 URL 中 sid= 后面的那串字符
118+
; 例如,策略 URL 为:https://guorn.com/stock/strategy?sid=7379.R.73780198158364
119+
; 则此处填写:7379.R.73780198158364
120+
sid=
121+
122+
; 是否启用?
123+
enabled=false
124+
125+
; 试运行?
126+
; 试运行不会真实下单
127+
dry_run=false
128+
129+
; 调试模式?
130+
; 调试模式可用于在收盘阶段进行测试
131+
debug=false
132+
133+
; 默认设置为:星期一至星期五 9:30 到 15:00 每小时第 40 分
134+
schedule=0 40 9-14 mon-fri * * * *
135+
136+
; 需要同步的交易客户端列表,以,(半角逗号)分割
137+
; clients=client1,client2
138+
clients=client1
139+
140+
; 保留名单,以;(半角分号)分割
141+
; 比如:B股、港股、逆回购、新股、货币基金等保留的证券代码
142+
; 其中:
143+
; B股代码:^[92]
144+
; 港股代码:^[\d]{5}$
145+
; 逆回购代码:^(204|131)
146+
reserved_securities=^[92];^[\d]{5}$;^(204|131)
147+
148+
; 最小订单金额,低于该值的订单将被忽略,以防因为价格波动导致的频繁调仓
149+
; 取值可以为数值,或者百分比
150+
min_order_value=1%
151+
152+
; 最大订单金额,用于分单
153+
; 取值为数值
154+
max_order_value=1000000
155+
156+
; 循环间隔时间,单位为秒
157+
loop_interval=10
158+
159+
; 批次间隔时间,单位为秒
160+
batch_interval=1
161+
162+
; 下单间隔时间,单位为秒
163+
order_interval=1
164+
165+
; 额外循环次数
166+
extra_loops=0

setup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
22

3-
from setuptools import setup, find_packages
3+
import os.path
44
from codecs import open
55
from os import path
6-
import os.path
6+
7+
from setuptools import setup, find_packages
78

89
here = path.abspath(path.dirname(__file__))
910

@@ -45,7 +46,8 @@
4546

4647
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
4748

48-
install_requires=['requests', 'six', 'apscheduler', 'lxml', 'cssselect', 'bs4', 'html5lib', 'pandas', 'rqopen-client'],
49+
install_requires=['requests', 'six', 'apscheduler', 'lxml', 'cssselect', 'bs4', 'html5lib', 'pandas',
50+
'rqopen-client', 'tushare'],
4951

5052
extras_require={
5153
'dev': [],

shipane_sdk/client.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import copy
44
import datetime
55
import re
6+
from enum import Enum
67

78
import lxml.html
89
import pandas as pd
@@ -14,6 +15,11 @@
1415
from six.moves.urllib.parse import urlencode
1516

1617

18+
class MediaType(Enum):
19+
DEFAULT = 'application/json'
20+
JOIN_QUANT = 'application/vnd.joinquant+json'
21+
22+
1723
class Client(object):
1824
KEY_REGEX = r'key=([^&]*)'
1925

@@ -69,13 +75,17 @@ def get_account(self, client=None, timeout=None):
6975
response = self.__send_request(request, timeout)
7076
return response.json()
7177

72-
def get_positions(self, client=None, timeout=None):
78+
def get_positions(self, client=None, media_type=MediaType.DEFAULT, timeout=None):
7379
request = Request('GET', self.__create_url(client, 'positions'))
80+
request.headers['Accept'] = media_type.value
7481
response = self.__send_request(request, timeout)
7582
json = response.json()
76-
sub_accounts = pd.DataFrame(json['subAccounts']).T
77-
positions = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns'])
78-
return {'sub_accounts': sub_accounts, 'positions': positions}
83+
if media_type == MediaType.DEFAULT:
84+
sub_accounts = pd.DataFrame(json['subAccounts']).T
85+
positions = pd.DataFrame(json['dataTable']['rows'], columns=json['dataTable']['columns'])
86+
portfolio = {'sub_accounts': sub_accounts, 'positions': positions}
87+
return portfolio
88+
return json
7989

8090
def get_orders(self, client=None, status="", timeout=None):
8191
request = Request('GET', self.__create_url(client, 'orders', status=status))
@@ -128,6 +138,13 @@ def purchase_new_stocks(self, client=None, timeout=None):
128138
except Exception as e:
129139
self._logger.error('客户端[%s]申购新股[%s(%s)]失败\n%s', client, row['name'], row['code'], e)
130140

141+
def create_adjustment(self, client=None, request_json=None, timeout=None):
142+
request = Request('POST', self.__create_url(client, 'adjustments'), json=request_json)
143+
request.headers['Content-Type'] = MediaType.JOIN_QUANT.value
144+
response = self.__send_request(request, timeout)
145+
json = response.json()
146+
return json
147+
131148
def start_clients(self, timeout=None):
132149
request = Request('PUT', self.__create_url(None, 'clients'))
133150
self.__send_request(request, timeout)
@@ -173,8 +190,8 @@ def __create_url(self, client, resource, resource_id=None, **params):
173190
path = '/{}'.format(resource)
174191
else:
175192
path = '/{}/{}'.format(resource, resource_id)
176-
177-
return '{}{}?{}'.format(self.__create_base_url(), path, urlencode(all_params))
193+
url = '{}{}?{}'.format(self.__create_base_url(), path, urlencode(all_params))
194+
return url
178195

179196
def __create_base_url(self):
180197
return 'http://' + self._host + ':' + str(self._port)
@@ -197,7 +214,7 @@ def __log_request(self, prepared_request):
197214
self._logger.info('Request:\n%s %s\n%s', prepared_request.method, url, prepared_request.body)
198215

199216
def __log_response(self, response):
200-
message = 'Response:\n{} {}\n{}'.format(response.status_code, response.reason, response.text)
217+
message = u'Response:\n{} {}\n{}'.format(response.status_code, response.reason, response.text)
201218
if response.status_code == 200:
202219
self._logger.info(message)
203220
else:

shipane_sdk/guorn/__init__.py

Whitespace-only changes.

shipane_sdk/guorn/client.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import time
4+
5+
import pandas as pd
6+
import requests
7+
8+
from shipane_sdk.base_quant_client import BaseQuantClient
9+
from shipane_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+
data = instruction['data']
57+
position = data['position']
58+
df = pd.DataFrame()
59+
sheet_data = instruction['data']['sheet_data']
60+
for row in sheet_data['row']:
61+
df[row['name']] = pd.Series(row['data'][1])
62+
meas_data = sheet_data['meas_data']
63+
for index, col in enumerate(sheet_data['col']):
64+
df[col['name']] = pd.Series(meas_data[index])
65+
66+
portfolio = Portfolio(1 - position)
67+
for index, row in df.iterrows():
68+
security = row[u'股票代码']
69+
value = row[u'目标仓位']
70+
price = row[u'参考价']
71+
amount = value / price
72+
position = Position(security, price, amount, amount)
73+
portfolio.add_position(position)
74+
75+
return portfolio

0 commit comments

Comments
 (0)