Skip to content

Commit 341b4df

Browse files
committed
Support non-invasive follow
1 parent 6bc81b5 commit 341b4df

File tree

6 files changed

+182
-62
lines changed

6 files changed

+182
-62
lines changed

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ Mac/Linux
139139
- 修改 shipane_sdk_config.yaml,升级后需参考 shipane_sdk_config_template.yaml 进行修改。
140140
- 修改策略代码,可参考如下示例:
141141

142-
- examples/joinquant/simple\_strategy.py - 基本跟单用法
143-
- examples/joinquant/simple\_sync\_strategy.py - 基本同步用法
142+
- examples/joinquant/simple\_strategy.py - 基本跟单用法(侵入式设计,不推荐)
143+
- examples/joinquant/advanced\_strategy.py - 高级同步、跟单用法(非侵入式设计,推荐)
144144
- examples/joinquant/new\_stocks\_purchase.py - 新股申购
145145
- examples/joinquant/repo.py - 逆回购
146146

config/joinquant/research/shipane_sdk_config_template.yaml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,20 @@ managers:
125125
# trader-1
126126
- id: trader-1
127127
client: client-1
128+
# 是否开启?
129+
# 正式运行时设置为 true
130+
enabled: true
131+
# 是否排练?排练时不会下单。
132+
# 正式运行时设置为 false
133+
dry-run: true
134+
# 工作模式
135+
# 1. SYNC: 指按模拟交易的持仓进行同步
136+
# 2. FOLLOW:指按模拟交易的下单进行跟单
137+
mode: SYNC
128138
# 同步选项
129139
# 如果该策略无需同步操作,可以省略 sync 配置项
130140
# 注意:该配置在下次 handle_data 调用时生效
131141
sync:
132-
# 是否开启?
133-
# 正式运行时设置为 true
134-
enabled: true
135-
# 是否排练?排练时不会下单。
136-
# 正式运行时设置为 false
137-
dry-run: true
138142
# 同步前是否撤销模拟盘未成交订单
139143
# 如果该选项未启用,并且模拟盘有未成交订单,SDK 将不会做同步
140144
pre-clear-for-sim: false
@@ -160,9 +164,10 @@ managers:
160164
traders:
161165
- id: trader-2
162166
client: client-1
167+
enabled: true
168+
dry-run: true
169+
mode: SYNC
163170
sync:
164-
enabled: true
165-
dry-run: true
166171
pre-clear-for-sim: false
167172
pre-clear-for-live: false
168173
min-order-value: 1%

examples/joinquant/simple_sync_strategy.py renamed to examples/joinquant/advanced_strategy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ def handle_data(context, data):
2323

2424
finally:
2525
# 放在 finally 块中,以防原有代码抛出异常或者 return
26-
# 在函数结尾处加入以下语句,用来将模拟盘同步至实盘
27-
g.__manager.sync()
26+
# 在函数结尾处加入以下语句,用来将模拟盘同步、跟单至实盘
27+
g.__manager.work()

shipane_sdk/base_manager.py

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@ def cancel(self, order):
118118
except:
119119
self._logger.exception('[%s] 撤单失败', trader.id)
120120

121-
def sync(self):
121+
def work(self):
122122
stop_watch = StopWatch()
123123
stop_watch.start()
124-
self._logger.info("[%s] 开始同步", self._id)
124+
self._logger.info("[%s] 开始工作", self._id)
125125
self._refresh()
126126
for id, trader in self._traders.items():
127-
trader.sync()
127+
trader.work()
128128
stop_watch.stop()
129-
self._logger.info("[%s] 结束同步,总耗时[%s]", self._id, stop_watch.short_summary())
129+
self._logger.info("[%s] 结束工作,总耗时[%s]", self._id, stop_watch.short_summary())
130130
self._logger.info(self.THEMATIC_BREAK)
131131

132132
def _refresh(self):
@@ -142,7 +142,7 @@ def __init__(self, logger, config, strategy_context):
142142
self._config = config
143143
self._strategy_context = strategy_context
144144
self._shipane_client = Client(self._logger, **config['client'])
145-
self._order_id_map = {}
145+
self._order_id_to_info_map = {}
146146
self._expire_before = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
147147
self._last_sync_portfolio_fingerprint = None
148148

@@ -158,38 +158,45 @@ def set_config(self, config):
158158
self._config = config
159159

160160
def purchase_new_stocks(self):
161+
if not self._pre_check():
162+
return
163+
161164
self._shipane_client.purchase_new_stocks()
162165

163166
def execute(self, order=None, **kwargs):
167+
if not self._pre_check():
168+
return
169+
164170
if order is None:
165171
common_order = Order.from_e_order(**kwargs)
166172
else:
167173
common_order = self._normalize_order(order)
168174

169-
self._logger.info("[实盘易] 跟单:" + str(common_order))
170-
if not self._should_execute(common_order):
171-
return
172-
173175
try:
174176
actual_order = self._execute(common_order)
175177
return actual_order
176178
except Exception:
177179
self._logger.exception("[实盘易] 下单异常")
178180

179181
def cancel(self, order):
180-
if order is None:
181-
self._logger.info('[实盘易] 委托为空,忽略撤单请求')
182+
if not self._pre_check():
182183
return
183184

184185
try:
185186
self._cancel(order)
186187
except:
187188
self._logger.exception("[实盘易] 撤单异常")
188189

189-
def sync(self):
190+
def work(self):
190191
if not self._pre_check():
191192
return
192193

194+
if self._config['mode'] == 'SYNC':
195+
self._sync()
196+
else:
197+
self._follow()
198+
199+
def _sync(self):
193200
stop_watch = StopWatch()
194201
stop_watch.start()
195202
self._logger.info("[%s] 开始同步", self.id)
@@ -199,7 +206,7 @@ def sync(self):
199206
self._logger.info("[%s] 模拟盘撤销全部订单已完成", self.id)
200207
target_portfolio = self._strategy_context.get_portfolio()
201208
if self._should_sync(target_portfolio):
202-
if self._sync_config['pre-clear-for-live'] and not self._sync_config['dry-run']:
209+
if self._sync_config['pre-clear-for-live'] and not self._config['dry-run']:
203210
self._shipane_client.cancel_all()
204211
time.sleep(self._sync_config['order-interval'] / 1000.0)
205212
self._logger.info("[%s] 实盘撤销全部订单已完成", self.id)
@@ -220,27 +227,81 @@ def sync(self):
220227
stop_watch.stop()
221228
self._logger.info("[%s] 结束同步,耗时[%s]", self.id, stop_watch.short_summary())
222229

230+
def _follow(self):
231+
stop_watch = StopWatch()
232+
stop_watch.start()
233+
self._logger.info("[%s] 开始跟单", self.id)
234+
try:
235+
common_orders = []
236+
all_common_orders = self._strategy_context.get_orders()
237+
for common_order in all_common_orders:
238+
if common_order.add_time >= self._strategy_context.get_current_time():
239+
if common_order.status == OrderStatus.canceled:
240+
origin_order = copy.deepcopy(common_order)
241+
origin_order.status = OrderStatus.open
242+
common_orders.append(origin_order)
243+
else:
244+
common_orders.append(common_order)
245+
if common_order.status == OrderStatus.canceled:
246+
common_orders.append(common_order)
247+
248+
common_orders = sorted(common_orders, key=lambda o: _PrioritizedOrder(o))
249+
for common_order in common_orders:
250+
if common_order.status != OrderStatus.canceled:
251+
try:
252+
self._execute(common_order)
253+
except:
254+
self._logger.exception("[实盘易] 下单异常")
255+
else:
256+
try:
257+
self._cancel(common_order)
258+
except:
259+
self._logger.exception("[实盘易] 撤单异常")
260+
except:
261+
self._logger.exception("[%s] 跟单失败", self.id)
262+
stop_watch.stop()
263+
self._logger.info("[%s] 结束跟单,耗时[%s]", self.id, stop_watch.short_summary())
264+
223265
@property
224266
def _sync_config(self):
225267
return self._config['sync']
226268

227269
def _execute(self, order):
270+
if not self._should_run():
271+
self._logger.info("[%s] %s", self.id, order)
272+
return None
273+
actual_order = self._do_execute(order)
274+
return actual_order
275+
276+
def _cancel(self, order):
277+
if not self._should_run():
278+
self._logger.info("[%s] 撤单 [%s]", self.id, order)
279+
return
280+
self._do_cancel(order)
281+
282+
def _do_execute(self, order):
228283
common_order = self._normalize_order(order)
229284
e_order = common_order.to_e_order()
230285
actual_order = self._shipane_client.execute(**e_order)
231-
self._order_id_map[common_order.id] = actual_order['id']
286+
self._order_id_to_info_map[common_order.id] = {'id': actual_order['id'], 'canceled': False}
232287
return actual_order
233288

234-
def _cancel(self, order):
289+
def _do_cancel(self, order):
290+
if order is None:
291+
self._logger.info('[实盘易] 委托为空,忽略撤单请求')
292+
return
293+
235294
if isinstance(order, int):
236295
quant_order_id = order
237296
else:
238297
common_order = self._normalize_order(order)
239298
quant_order_id = common_order.id
240299

241300
try:
242-
order_id = self._order_id_map.pop(quant_order_id)
243-
self._shipane_client.cancel(order_id=order_id)
301+
order_info = self._order_id_to_info_map[quant_order_id]
302+
if not order_info['canceled']:
303+
order_info['canceled'] = True
304+
self._shipane_client.cancel(order_id=order_info['id'])
244305
except KeyError:
245306
self._logger.warning('[实盘易] 未找到对应的委托编号')
246307

@@ -251,27 +312,21 @@ def _normalize_order(self, order):
251312
common_order = self._strategy_context.convert_order(order)
252313
return common_order
253314

254-
def _should_execute(self, common_order):
255-
if self._strategy_context.is_backtest():
256-
self._logger.info("[实盘易] 当前为回测环境,忽略下单请求")
257-
return False
258-
if common_order is None:
259-
self._logger.info('[实盘易] 委托为空,忽略下单请求')
260-
return False
261-
if self._is_expired(common_order):
262-
self._logger.info('[实盘易] 委托已过期,忽略下单请求')
315+
def _should_run(self):
316+
if self._config['dry-run']:
317+
self._logger.debug("[实盘易] 当前为排练模式,不执行下单、撤单请求")
263318
return False
264319
return True
265320

266321
def _is_expired(self, common_order):
267322
return common_order.add_time < self._expire_before
268323

269324
def _pre_check(self):
270-
if not self._sync_config['enabled']:
271-
self._logger.info("[%s] 同步未启用,不进行同步", self.id)
325+
if not self._config['enabled']:
326+
self._logger.info("[%s] 同步未启用,不执行", self.id)
272327
return False
273328
if self._strategy_context.is_backtest():
274-
self._logger.info("[%s] 当前为回测环境,不进行同步", self.id)
329+
self._logger.info("[%s] 当前为回测环境,不执行", self.id)
275330
return False
276331
return True
277332

@@ -316,21 +371,10 @@ def _log_progress(self, adjustment):
316371
def _execute_adjustment(self, adjustment):
317372
for batch in adjustment.batches:
318373
for order in batch:
319-
self._execute_order(order)
374+
self._execute(order)
320375
time.sleep(self._sync_config['order-interval'] / 1000.0)
321376
time.sleep(self._sync_config['batch-interval'] / 1000.0)
322377

323-
def _execute_order(self, order):
324-
try:
325-
if self._sync_config['dry-run']:
326-
self._logger.info("[%s] %s", self.id, order)
327-
return
328-
329-
e_order = order.to_e_order()
330-
self._shipane_client.execute(**e_order)
331-
except:
332-
self._logger.exception("[%s] 客户端下单失败", self.id)
333-
334378

335379
class StrategyConfig(object):
336380
def __init__(self, strategy_context):
@@ -368,14 +412,10 @@ def _create_proxy_configs(self):
368412

369413
def _create_trader_config(self, raw_trader_config):
370414
client_config = self._create_client_config(raw_trader_config)
371-
sync_config = raw_trader_config['sync']
372-
sync_config['reserved-securities'] = client_config['reserved_securities']
373-
result = {
374-
'id': raw_trader_config['id'],
375-
'client': client_config,
376-
'sync': sync_config
377-
}
378-
return result
415+
trader_config = copy.deepcopy(raw_trader_config)
416+
trader_config['client'] = client_config
417+
trader_config['sync']['reserved-securities'] = client_config.pop('reserved_securities', [])
418+
return trader_config
379419

380420
def _create_client_config(self, raw_trader_config):
381421
client_config = None
@@ -410,3 +450,39 @@ def _create_client_config(self, raw_trader_config):
410450
if client_config is not None:
411451
break
412452
return client_config
453+
454+
455+
class _PrioritizedOrder(object):
456+
def __init__(self, order):
457+
self.order = order
458+
459+
def __lt__(self, other):
460+
x = self.order
461+
y = other.order
462+
if x.add_time != y.add_time:
463+
return x.add_time < y.add_time
464+
if x.status == OrderStatus.canceled:
465+
if y.status == OrderStatus.canceled:
466+
return x.id < y.id
467+
else:
468+
return False
469+
else:
470+
if y.status == OrderStatus.canceled:
471+
return True
472+
else:
473+
return x.id < y.id
474+
475+
def __gt__(self, other):
476+
return other.__lt__(self)
477+
478+
def __eq__(self, other):
479+
return (not self.__lt__(other)) and (not other.__lt__(self))
480+
481+
def __le__(self, other):
482+
return not self.__gt__(other)
483+
484+
def __ge__(self, other):
485+
return not self.__lt__(other)
486+
487+
def __ne__(self, other):
488+
return not self.__eq__(other)

shipane_sdk/joinquant/manager.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class JoinQuantStrategyContext(BaseStrategyContext):
3030
def __init__(self, context):
3131
self._context = context
3232

33+
def get_current_time(self):
34+
return self._context.current_dt
35+
3336
def get_portfolio(self):
3437
quant_portfolio = self._context.portfolio
3538
portfolio = Portfolio()
@@ -50,9 +53,19 @@ def convert_order(self, quant_order):
5053
price=quant_order.limit,
5154
amount=quant_order.amount,
5255
style=(OrderStyle.LIMIT if quant_order.limit > 0 else OrderStyle.MARKET),
56+
status=OrderStatus(quant_order.status.value),
57+
add_time=quant_order.add_time,
5358
)
5459
return common_order
5560

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+
5669
def has_open_orders(self):
5770
return bool(get_open_orders())
5871

0 commit comments

Comments
 (0)