Skip to content

Commit f7e2ce0

Browse files
committed
tick push
1 parent 8f50893 commit f7e2ce0

File tree

8 files changed

+230
-5
lines changed

8 files changed

+230
-5
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
name='tigeropen',
1717
version=__VERSION__,
1818
description='TigerBrokers Open API',
19-
packages=find_packages(exclude=[]),
19+
packages=find_packages(exclude=["tests"]),
2020
author='TigerBrokers',
2121
author_email='[email protected]',
2222
license='Apache License v2',

tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# @Date : 2022/6/24
4+
# @Author : sukai

tests/test_push_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# @Date : 2022/6/24
4+
# @Author : sukai
5+
import unittest
6+
7+
from tigeropen.push.push_client import PushClient
8+
9+
10+
class TestPushClient(unittest.TestCase):
11+
12+
def test_tick_convert(self):
13+
body = '{"symbol":"QQQ","tickType":"*****","serverTimestamp":1656062042242,"priceOffset":2,' \
14+
'"volumes":[99,10,10,10,800],"partCode":["t","p","p","p","t"],"cond":"IIIIT","type":"TradeTick",' \
15+
'"times":[1656062084833,11,0,0,31],"quoteLevel":"usStockQuote","priceBase":28770,"sn":878,' \
16+
'"prices":[6,3,2,2,0],"timestamp":1656062085570}'
17+
18+
expected = 'QQQ', [{'tick_type': '*', 'price': 287.76, 'volume': 99, 'part_code': 'NSDQ',
19+
'part_code_name': 'NASDAQ Stock Market, LLC (NASDAQ)', 'cond': 'US_ODD_LOT_TRADE',
20+
'time': 1656062084833, 'server_timestamp': 1656062042242, 'type': 'TradeTick',
21+
'quote_level': 'usStockQuote', 'sn': 878, 'timestamp': 1656062085570},
22+
{'tick_type': '*', 'price': 287.73, 'volume': 10, 'part_code': 'ARCA',
23+
'part_code_name': 'NYSE Arca, Inc. (NYSE Arca)', 'cond': 'US_ODD_LOT_TRADE',
24+
'time': 1656062084844, 'server_timestamp': 1656062042242, 'type': 'TradeTick',
25+
'quote_level': 'usStockQuote', 'sn': 878, 'timestamp': 1656062085570},
26+
{'tick_type': '*', 'price': 287.72, 'volume': 10, 'part_code': 'ARCA',
27+
'part_code_name': 'NYSE Arca, Inc. (NYSE Arca)', 'cond': 'US_ODD_LOT_TRADE',
28+
'time': 1656062084844, 'server_timestamp': 1656062042242, 'type': 'TradeTick',
29+
'quote_level': 'usStockQuote', 'sn': 878, 'timestamp': 1656062085570},
30+
{'tick_type': '*', 'price': 287.72, 'volume': 10, 'part_code': 'ARCA',
31+
'part_code_name': 'NYSE Arca, Inc. (NYSE Arca)', 'cond': 'US_ODD_LOT_TRADE',
32+
'time': 1656062084844, 'server_timestamp': 1656062042242, 'type': 'TradeTick',
33+
'quote_level': 'usStockQuote', 'sn': 878, 'timestamp': 1656062085570},
34+
{'tick_type': '*', 'price': 287.7, 'volume': 800, 'part_code': 'NSDQ',
35+
'part_code_name': 'NASDAQ Stock Market, LLC (NASDAQ)', 'cond': 'US_FORM_T',
36+
'time': 1656062084875, 'server_timestamp': 1656062042242, 'type': 'TradeTick',
37+
'quote_level': 'usStockQuote', 'sn': 878, 'timestamp': 1656062085570}]
38+
self.assertEqual(expected, PushClient._convert_tick(body))

tigeropen/common/consts/push_destinations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
QUOTE_DEPTH = 'quotedepth'
55
QUOTE_FUTURE = 'future'
66
QUOTE_OPTION = 'option'
7+
QUOTE_TICK = 'tradetick'
78

89
TRADE_ASSET = 'trade/asset'
910
TRADE_POSITION = 'trade/position'

tigeropen/common/consts/push_subscriptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
SUBSCRIPTION_QUOTE_DEPTH = 'QuoteDepth'
55
SUBSCRIPTION_QUOTE_FUTURE = 'Future'
66
SUBSCRIPTION_QUOTE_OPTION = 'Option'
7+
SUBSCRIPTION_QUOTE_TICK = 'TradeTick'
78

89
SUBSCRIPTION_TRADE_ASSET = 'Asset'
910
SUBSCRIPTION_TRADE_POSITION = 'Position'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# @Date : 2022/6/23
4+
# @Author : sukai
5+
6+
HK_QUOTE_LEVEL_PREFIX = "hk"
7+
US_QUOTE_LEVEL_PREFIX = "us"
8+
9+
PART_CODE_NAME_MAP = {
10+
"a": "NYSE American, LLC (NYSE American)",
11+
"b": "NASDAQ OMX BX, Inc. (NASDAQ OMX BX)",
12+
"c": "NYSE National, Inc. (NYSE National)",
13+
"d": "FINRA Alternative Display Facility (ADF)",
14+
"h": "MIAX Pearl Exchange, LLC (MIAX)",
15+
"i": "International Securities Exchange, LLC (ISE)",
16+
"j": "Cboe EDGA Exchange, Inc. (Cboe EDGA)",
17+
"k": "Cboe EDGX Exchange, Inc. (Cboe EDGX)",
18+
"l": "Long-Term Stock Exchange, Inc. (LTSE)",
19+
"m": "NYSE Chicago, Inc. (NYSE Chicago)",
20+
"n": "New York Stock Exchange, LLC (NYSE)",
21+
"p": "NYSE Arca, Inc. (NYSE Arca)",
22+
"s": "Consolidated Tape System (CTS)",
23+
"t": "NASDAQ Stock Market, LLC (NASDAQ)",
24+
"u": "Members Exchange, LLC (MEMX)",
25+
"v": "Investors’ Exchange, LLC. (IEX)",
26+
"w": "CBOE Stock Exchange, Inc. (CBSX)",
27+
"x": "NASDAQ OMX PSX, Inc. (NASDAQ OMX PSX)",
28+
"y": "Cboe BYX Exchange, Inc. (Cboe BYX)",
29+
"z": "Cboe BZX Exchange, Inc. (Cboe BZX)",
30+
}
31+
32+
PART_CODE_MAP = {
33+
"a": "AMEX",
34+
"b": "BX",
35+
"c": "NSX",
36+
"d": "ADF",
37+
"h": "MIAX",
38+
"i": "ISE",
39+
"j": "EDGA",
40+
"k": "EDGX",
41+
"l": "LTSE",
42+
"m": "CHO",
43+
"n": "NYSE",
44+
"p": "ARCA",
45+
"s": "CTS",
46+
"t": "NSDQ",
47+
"u": "MEMX",
48+
"v": "IEX",
49+
"w": "CBSX",
50+
"x": "PSX",
51+
"y": "BYX",
52+
"z": "BZX",
53+
}
54+
55+
US_TRADE_COND_MAP = {
56+
" ": "US_REGULAR_SALE", # 自动对盘
57+
"B": "US_BUNCHED_TRADE", # 批量交易
58+
"C": "US_CASH_TRADE", # 现金交易
59+
"F": "US_INTERMARKET_SWEEP", # 跨市场交易
60+
"G": "US_BUNCHED_SOLD_TRADE", # 批量卖出
61+
"H": "US_PRICE_VARIATION_TRADE", # 离价交易
62+
"I": "US_ODD_LOT_TRADE", # 碎股交易
63+
"K": "US_RULE_127_OR_155_TRADE", # 纽交所 第127条交易 或 第155条交易
64+
"L": "US_SOLD_LAST", # 延迟交易
65+
"M": "US_MARKET_CENTER_CLOSE_PRICE", # 中央收市价
66+
"N": "US_NEXT_DAY_TRADE", # 隔日交易
67+
"O": "US_MARKET_CENTER_OPENING_TRADE", # 中央开盘价交易
68+
"P": "US_PRIOR_REFERENCE_PRICE", # 前参考价
69+
"Q": "US_MARKET_CENTER_OPEN_PRICE", # 中央开盘价
70+
"R": "US_SELLER", # 卖方
71+
"T": "US_FORM_T", # 盘前盘后交易
72+
"U": "US_EXTENDED_TRADING_HOURS", # 延长交易时段
73+
"V": "US_CONTINGENT_TRADE", # 合单交易
74+
"W": "US_AVERAGE_PRICE_TRADE", # 均价交易
75+
"X": "US_CROSS_TRADE", #
76+
"Z": "US_SOLD_OUT_OF_SEQUENCE", # 场外售出
77+
"0": "US_ODD_LOST_CROSS_TRADE", # 碎股跨市场交易
78+
"4": "US_DERIVATIVELY_PRICED", # 衍生工具定价
79+
"5": "US_MARKET_CENTER_RE_OPENING_TRADE", # 再开盘定价
80+
"6": "US_MARKET_CENTER_CLOSING_TRADE", # 收盘定价
81+
"7": "US_QUALIFIED_CONTINGENT_TRADE", # 合单交易
82+
"9": "US_CONSOLIDATED_LAST_PRICE_PER_LISTING_PACKET", # 综合延迟价格
83+
}
84+
85+
HK_TRADE_COND_MAP = {
86+
" ": "HK_AUTOMATCH_NORMAL", # 自动对盘
87+
"D": "HK_ODD_LOT_TRADE", # 碎股交易
88+
"U": "HK_AUCTION_TRADE", # 竞价交易
89+
"*": "HK_OVERSEAS_TRADE", # 场外交易
90+
"P": "HK_LATE_TRADE_OFF_EXCHG", # 开市前成交
91+
"M": "HK_NON_DIRECT_OFF_EXCHG_TRADE", # 非自动对盘
92+
"X": "HK_DIRECT_OFF_EXCHG_TRADE", # 同券商自动对盘
93+
"Y": "HK_AUTOMATIC_INTERNALIZED", # 同券商非自动对盘
94+
}

tigeropen/common/util/tick_util.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# @Date : 2022/6/24
4+
# @Author : sukai
5+
6+
from tigeropen.common.consts.tick_constants import PART_CODE_MAP, PART_CODE_NAME_MAP, HK_QUOTE_LEVEL_PREFIX, \
7+
HK_TRADE_COND_MAP, US_TRADE_COND_MAP
8+
9+
10+
def get_part_code(code):
11+
return PART_CODE_MAP.get(code)
12+
13+
14+
def get_part_code_name(code):
15+
return PART_CODE_NAME_MAP.get(code)
16+
17+
18+
def get_trade_condition_map(quote_level):
19+
if quote_level.lower().startswith(HK_QUOTE_LEVEL_PREFIX):
20+
return HK_TRADE_COND_MAP
21+
return US_TRADE_COND_MAP
22+
23+
24+
def get_trade_condition(cond, cond_map):
25+
return cond_map.get(cond)
26+
27+

tigeropen/push/push_client.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import json
88
import logging
99
import sys
10-
import os
1110
from collections import defaultdict
11+
from itertools import accumulate, zip_longest
1212

1313
import stomp
1414
from stomp.exception import ConnectFailedException
@@ -17,17 +17,19 @@
1717
from tigeropen.common.consts import OrderStatus
1818
from tigeropen.common.consts.params import P_SDK_VERSION, P_SDK_VERSION_PREFIX
1919
from tigeropen.common.consts.push_destinations import QUOTE, QUOTE_DEPTH, QUOTE_FUTURE, QUOTE_OPTION, TRADE_ASSET, \
20-
TRADE_ORDER, TRADE_POSITION
20+
TRADE_ORDER, TRADE_POSITION, QUOTE_TICK
2121
from tigeropen.common.consts.push_subscriptions import SUBSCRIPTION_QUOTE, SUBSCRIPTION_QUOTE_DEPTH, \
2222
SUBSCRIPTION_QUOTE_OPTION, SUBSCRIPTION_QUOTE_FUTURE, SUBSCRIPTION_TRADE_ASSET, SUBSCRIPTION_TRADE_POSITION, \
23-
SUBSCRIPTION_TRADE_ORDER
23+
SUBSCRIPTION_TRADE_ORDER, SUBSCRIPTION_QUOTE_TICK
2424
from tigeropen.common.consts.push_types import RequestType, ResponseType
2525
from tigeropen.common.consts.quote_keys import QuoteChangeKey, QuoteKeyType
2626
from tigeropen.common.exceptions import ApiException
27-
from tigeropen.common.util.string_utils import camel_to_underline
2827
from tigeropen.common.util.common_utils import get_enum_value
2928
from tigeropen.common.util.order_utils import get_order_status
3029
from tigeropen.common.util.signature_utils import sign_with_rsa
30+
from tigeropen.common.util.string_utils import camel_to_underline, camel_to_underline_obj
31+
from tigeropen.common.util.tick_util import get_part_code, get_part_code_name, get_trade_condition_map, \
32+
get_trade_condition
3133
from tigeropen.push import _patch_ssl
3234

3335
HOUR_TRADING_QUOTE_KEYS_MAPPINGS = {'hourTradingLatestPrice': 'latest_price', 'hourTradingPreClose': 'pre_close',
@@ -89,6 +91,7 @@ def __init__(self, host, port, use_ssl=True, connection_timeout=120, heartbeats=
8991

9092
self.subscribed_symbols = None
9193
self.quote_changed = None
94+
self.tick_changed = None
9295
self.asset_changed = None
9396
self.position_changed = None
9497
self.order_changed = None
@@ -204,6 +207,11 @@ def on_message(self, frame):
204207
items.append((camel_to_underline(key), value))
205208
if items:
206209
self.quote_changed(symbol, items, hour_trading)
210+
elif response_type == str(ResponseType.GET_TRADING_TICK_END.value):
211+
if self.tick_changed:
212+
symbol, items = self._convert_tick(body)
213+
self.tick_changed(symbol, items)
214+
207215
elif response_type == str(ResponseType.SUBSCRIBE_ASSET.value):
208216
if self.asset_changed:
209217
data = json.loads(body)
@@ -342,6 +350,15 @@ def subscribe_quote(self, symbols, quote_key_type=QuoteKeyType.TRADE, focus_keys
342350
return self._handle_quote_subscribe(destination=QUOTE, subscription=SUBSCRIPTION_QUOTE, symbols=symbols,
343351
extra_headers=extra_headers)
344352

353+
def subscribe_tick(self, symbols):
354+
"""
355+
subscribe trade tick
356+
:param symbols: symbol列表
357+
:return:
358+
"""
359+
return self._handle_quote_subscribe(destination=QUOTE_TICK, subscription=SUBSCRIPTION_QUOTE_TICK,
360+
symbols=symbols)
361+
345362
def subscribe_depth_quote(self, symbols):
346363
"""
347364
订阅深度行情
@@ -383,6 +400,14 @@ def unsubscribe_quote(self, symbols=None, id=None):
383400
"""
384401
self._handle_quote_unsubscribe(destination=QUOTE, subscription=SUBSCRIPTION_QUOTE, sub_id=id, symbols=symbols)
385402

403+
def unsubscribe_tick(self, symbols=None, id=None):
404+
"""
405+
退订行情更新
406+
:return:
407+
"""
408+
self._handle_quote_unsubscribe(destination=QUOTE_TICK, subscription=SUBSCRIPTION_QUOTE_TICK, sub_id=id,
409+
symbols=symbols)
410+
386411
def unsubscribe_depth_quote(self, symbols=None, id=None):
387412
"""
388413
退订深度行情更新
@@ -440,3 +465,38 @@ def _generate_headers(self, extra_headers=None):
440465
headers.update(extra_headers)
441466
return headers
442467

468+
@staticmethod
469+
def _convert_tick(tick):
470+
data = json.loads(tick)
471+
symbol = data.pop('symbol')
472+
data = camel_to_underline_obj(data)
473+
price_offset = 10 ** data.pop('price_offset')
474+
price_base = data.pop('price_base')
475+
# The latter time is equal to the sum of all previous values
476+
price_items = [('price', (item + price_base) / price_offset) for item in data.pop('prices')]
477+
time_items = [('time', item) for item in accumulate(data.pop('times'))]
478+
volumes = [('volume', item) for item in data.pop('volumes')]
479+
tick_type_items = [('tick_type', item) for item in data.pop('tick_type')]
480+
part_codes = data.pop('part_code')
481+
if part_codes:
482+
part_code_items = [('part_code', get_part_code(item)) for item in part_codes]
483+
part_code_name_items = [('part_code_name', get_part_code_name(item)) for item in part_codes]
484+
else:
485+
part_code_items = [('part_code', None) for _ in range(len(time_items))]
486+
part_code_name_items = [('part_code_name', None) for _ in range(len(time_items))]
487+
488+
conds = data.pop('cond')
489+
cond_map = get_trade_condition_map(data.get('quote_level'))
490+
if conds:
491+
cond_items = [('cond', get_trade_condition(item, cond_map)) for item in conds]
492+
else:
493+
cond_items = [('cond', None) for _ in range(len(time_items))]
494+
tick_data = zip_longest(tick_type_items, price_items, volumes, part_code_items,
495+
part_code_name_items, cond_items, time_items)
496+
items = []
497+
for item in tick_data:
498+
item_dict = dict(item)
499+
item_dict.update(data)
500+
items.append(item_dict)
501+
return symbol, items
502+

0 commit comments

Comments
 (0)