Skip to content

Commit 9f7fc79

Browse files
authored
Merge pull request #45 from tigerfintech/feature_api_order_transactions
Add api TradeClient.get_transaction, QuoteClient.get_quoe_permissions;
2 parents 56e3b9c + 6f98dda commit 9f7fc79

25 files changed

+795
-160
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## 2.0.5 (2022-01-10)
2+
### New
3+
- 查询行情权限接口 QuoteClient.get_quote_permission
4+
- 订单综合账户成交记录接口 TradeClient.get_transactions
5+
- 增加一个完整的策略示例
6+
7+
### Changed
8+
- 方法枚举参数优化,使用枚举参数的方法也可以直接使用该枚举对应值
9+
- TradeClient.place_order 去除返回数据中 Order.order_id 属性的校验
10+
- 将 SDK 内部的日志级别由 INFO 调整为 DEBUG, 防止默认情况下输出 SDK 的日志
11+
- 去除 pandas 固定版本号, 方便安装时灵活指定版本
12+
13+
### Breaking
14+
- 行情权限抢占接口 QuoteClient.grab_quote_permission 返回的数据项中,'expireAt' 字段格式转换为 'expire_at'
15+
116
## 2.0.4 (2021-12-08)
217
### New
318
- 综合/模拟账户查询资产接口 TradeClient.get_prime_assets

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
simplejson==3.17.3
22
delorean==1.0.0
3-
pandas==1.1.5
3+
pandas
44
python-dateutil==2.8.2
55
pytz==2021.1
66
pyasn1==0.4.8

tigeropen/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
55
@author: gaoan
66
"""
7-
__VERSION__ = '2.0.4'
7+
__VERSION__ = '2.0.5'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
3+
SUBSCRIPTION_QUOTE = 'Quote'
4+
SUBSCRIPTION_QUOTE_DEPTH = 'QuoteDepth'
5+
SUBSCRIPTION_QUOTE_FUTURE = 'Future'
6+
SUBSCRIPTION_QUOTE_OPTION = 'Option'
7+
8+
SUBSCRIPTION_TRADE_ASSET = 'Asset'
9+
SUBSCRIPTION_TRADE_POSITION = 'Position'
10+
SUBSCRIPTION_TRADE_ORDER = 'OrderStatus'

tigeropen/common/consts/service_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ACTIVE_ORDERS = "active_orders" # 待成交订单
2323
INACTIVE_ORDERS = "inactive_orders" # 已撤销订单
2424
FILLED_ORDERS = "filled_orders" # 已成交订单
25+
ORDER_TRANSACTIONS = "order_transactions" # 订单成交记录
2526

2627
"""
2728
合约
@@ -46,6 +47,7 @@
4647
QUOTE_STOCK_TRADE = "quote_stock_trade"
4748
QUOTE_DEPTH = "quote_depth" # level2 深度行情
4849
GRAB_QUOTE_PERMISSION = "grab_quote_permission" # 抢占行情
50+
GET_QUOTE_PERMISSION = "get_quote_permission"
4951

5052
# 期权行情
5153
OPTION_EXPIRATION = "option_expiration"

tigeropen/common/util/common_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
@author: gaoan
66
"""
7+
from enum import Enum
8+
79
import pytz
810

911
eastern = pytz.timezone('US/Eastern')
@@ -19,3 +21,9 @@ def has_value(m, key):
1921
if not m[key]:
2022
return False
2123
return True
24+
25+
26+
def get_enum_value(e, enum_type=None):
27+
if enum_type is None:
28+
return e.value if isinstance(e, Enum) else e
29+
return e.value if isinstance(e, enum_type) else e

tigeropen/examples/nasdaq100.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import datetime
2+
import logging
3+
import sys
4+
import time
5+
6+
import pandas as pd
7+
8+
from tigeropen.common.consts import BarPeriod, SecurityType, Market, Currency
9+
from tigeropen.common.util.contract_utils import stock_contract
10+
from tigeropen.common.util.order_utils import limit_order
11+
from tigeropen.quote.quote_client import QuoteClient
12+
from tigeropen.tiger_open_config import get_client_config
13+
from tigeropen.trade.trade_client import TradeClient
14+
15+
client_logger = logging.getLogger('client')
16+
client_logger.setLevel(logging.WARNING)
17+
client_logger.addHandler(logging.StreamHandler(sys.stdout))
18+
logger = logging.getLogger(__name__)
19+
logger.setLevel(logging.INFO)
20+
logger.addHandler(logging.StreamHandler(sys.stdout))
21+
22+
pd.set_option('display.max_columns', 500)
23+
pd.set_option('display.max_rows', 100)
24+
pd.set_option('display.width', 1000)
25+
26+
# 纳斯达克100指数成分股 Components of Nasdaq-100. Up to 2021/12/20
27+
UNIVERSE_NDX = ["AAPL", "ADBE", "ADI", "ADP", "ADSK", "AEP", "ALGN", "AMAT", "AMD", "AMGN", "AMZN", "ANSS", "ASML",
28+
"ATVI", "AVGO", "BIDU", "BIIB", "BKNG", "CDNS", "CDW", "CERN", "CHKP", "CHTR", "CMCSA", "COST", "CPRT",
29+
"CRWD", "CSCO", "CSX", "CTAS", "CTSH", "DLTR", "DOCU", "DXCM", "EA", "EBAY", "EXC", "FAST", "FB",
30+
"FISV", "FOX", "GILD", "GOOG", "HON", "IDXX", "ILMN", "INCY", "INTC", "INTU", "ISRG",
31+
"JD", "KDP", "KHC", "KLAC", "LRCX", "LULU", "MAR", "MCHP", "MDLZ", "MELI", "MNST", "MRNA", "MRVL",
32+
"MSFT", "MTCH", "MU", "NFLX", "NTES", "NVDA", "NXPI", "OKTA", "ORLY", "PAYX", "PCAR", "PDD", "PEP",
33+
"PTON", "PYPL", "QCOM", "REGN", "ROST", "SBUX", "SGEN", "SIRI", "SNPS", "SPLK", "SWKS", "TCOM", "TEAM",
34+
"TMUS", "TSLA", "TXN", "VRSK", "VRSN", "VRTX", "WBA", "WDAY", "XEL", "XLNX", "ZM"]
35+
36+
# 恒生科技指数成分股. Up to 2021/12/20
37+
UNIVERSE_HSTECH = ["00241", "00268", "00285", "00522", "00700", "00772", "00780", "00909", "00981", "00992", "01024",
38+
"01347", "01810", "01833", "02013", "02018", "02382", "02518", "03690", "03888", "06060", "06618",
39+
"06690", "09618", "09626", "09698", "09888", "09961", "09988", "09999"]
40+
41+
# 持仓股票个数
42+
HOLDING_NUM = 5
43+
# 订单检查次数
44+
ORDERS_CHECK_MAX_TIMES = 10
45+
# 获取行情每次请求symbol个数
46+
REQUEST_SIZE = 50
47+
TARGET_QUANTITY = "target_quantity"
48+
PRE_CLOSE = "pre_close"
49+
LATEST_PRICE = "latest_price"
50+
MARKET_CAPITAL = "market_capital"
51+
SYMBOL = "symbol"
52+
WEIGHT = "weight"
53+
TIME = "time"
54+
CLOSE = "close"
55+
DATE = "date"
56+
LOT_SIZE = "lot_size"
57+
58+
PRIVATE_KEY_PATH = "your private key path"
59+
TIGER_ID = "your tiger id"
60+
ACCOUNT = "your account"
61+
client_config = get_client_config(private_key_path=PRIVATE_KEY_PATH, tiger_id=TIGER_ID, account=ACCOUNT)
62+
quote_client = QuoteClient(client_config, logger=client_logger)
63+
trade_client = TradeClient(client_config, logger=client_logger)
64+
65+
66+
def request(symbols, method, **kwargs):
67+
"""
68+
:param symbols:
69+
:param method:
70+
:param kwargs:
71+
:return:
72+
"""
73+
symbols = list(symbols)
74+
result = pd.DataFrame()
75+
for i in range(0, len(symbols), REQUEST_SIZE):
76+
part = symbols[i:i + REQUEST_SIZE]
77+
quote = method(part, **kwargs)
78+
result = result.append(quote)
79+
# for rate limit
80+
time.sleep(0.5)
81+
return result
82+
83+
84+
def get_quote(symbols):
85+
quote = request(symbols, quote_client.get_stock_briefs)
86+
return quote.set_index(SYMBOL)
87+
88+
89+
def get_trade_meta(symbols):
90+
metas = request(symbols, quote_client.get_trade_metas)
91+
return metas.set_index(SYMBOL)
92+
93+
94+
def get_history(symbols, total=200, batch_size=50) -> pd.DataFrame:
95+
"""
96+
97+
:param symbols:
98+
:param total:
99+
:param batch_size:
100+
:return:
101+
time open high low close volume
102+
date symbol
103+
2021-03-05 00:00:00-05:00 AAPL 1614920400000 120.9800 121.935 117.5700 121.42 153766601
104+
ADBE 1614920400000 444.8800 444.950 423.7101 440.83 4614971
105+
ADI 1614920400000 149.0000 149.620 143.3900 148.88 4040153
106+
ADP 1614920400000 171.8300 179.000 171.5003 178.26 2535893
107+
ADSK 1614920400000 270.3300 270.330 255.0200 267.39 1835526
108+
... ... ... ... ... ... ...
109+
2021-12-16 00:00:00-05:00 WBA 1639630800000 48.5035 50.150 48.5000 49.26 5551852
110+
WDAY 1639630800000 277.3300 278.365 269.2600 272.23 1206784
111+
XEL 1639630800000 68.5800 69.570 68.3100 68.95 3774564
112+
XLNX 1639630800000 217.3700 218.080 198.5100 199.78 4299386
113+
ZM 1639630800000 183.7900 185.720 177.0000 182.40 4224447
114+
"""
115+
end = int(datetime.datetime.today().timestamp() * 1000)
116+
history = pd.DataFrame()
117+
for i in range(0, total, batch_size):
118+
if i + batch_size <= total:
119+
limit = batch_size
120+
else:
121+
limit = i + batch_size - total
122+
logger.info(f'query history, end_time:{end}, limit:{limit}')
123+
part = request(symbols, quote_client.get_bars, period=BarPeriod.DAY, end_time=end, limit=limit)
124+
part[DATE] = pd.to_datetime(part[TIME], unit='ms').dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
125+
end = min(part[TIME])
126+
history = history.append(part)
127+
history.set_index([DATE, SYMBOL], inplace=True)
128+
history.sort_index(inplace=True)
129+
return history
130+
131+
132+
class Strategy:
133+
134+
def __init__(self):
135+
self.market = Market.US
136+
self.currency = Currency.USD
137+
self.universe = UNIVERSE_NDX
138+
# self.market = Market.HK
139+
# self.currency = Currency.HKD
140+
# self.universe = UNIVERSE_HSTECH
141+
142+
self.selected_symbols = list()
143+
# 计算动量的时间周期
144+
self.momentum_period = 30
145+
# 持仓股票个数
146+
self.holding_num = HOLDING_NUM
147+
# 调仓后隔夜剩余流动性目标占比,剩余流动性占比越高,风控状态越安全,如果隔夜剩余流动性占比过低(比如小于5%), 则存在被强平的风险。
148+
self.target_overnight_liquidation_ratio = 0.6
149+
150+
def screen_stocks(self):
151+
"""screen stock by price momentum
152+
按动量筛选股票. 选取周期内涨幅最高的若干股票
153+
:return:
154+
"""
155+
history = get_history(self.universe)
156+
close_data = history[CLOSE].unstack()
157+
momentum = close_data.pct_change(periods=self.momentum_period).iloc[-1]
158+
self.selected_symbols = momentum.nlargest(self.holding_num).index.values.tolist()
159+
return self.selected_symbols
160+
161+
def rebalance_portfolio(self):
162+
"""
163+
调仓。先将本次选股未选中但在持仓中的股票进行平仓,然后将选中的股票按照等股数权重买入
164+
:return:
165+
"""
166+
position_list = trade_client.get_positions(sec_type=SecurityType.STK, market=self.market)
167+
positions = dict()
168+
for pos in position_list:
169+
positions[pos.contract.symbol] = pos.quantity
170+
171+
need_close_symbols = set(positions.keys()) - set(self.selected_symbols)
172+
173+
# 如果不是美股,需要获取股票的每手股数,每次下单的股数只能使用每手股数的整数倍
174+
lot_size = get_trade_meta(set(positions.keys()).union(self.selected_symbols))[LOT_SIZE]
175+
latest_price = get_quote(need_close_symbols)[LATEST_PRICE]
176+
orders = list()
177+
for symbol in need_close_symbols:
178+
contract = stock_contract(symbol, currency=self.currency.name)
179+
# 将下单股数处理为每手股数的整数倍
180+
quantity = int(positions[symbol] // lot_size[symbol] * lot_size[symbol])
181+
if quantity == 0:
182+
logger.warning(f'can not place order with this quantity, symbol:{symbol}, lot_size:{lot_size[symbol]},'
183+
f'quantity:{positions[symbol]}')
184+
continue
185+
limit_price = latest_price[symbol]
186+
order = limit_order(account=ACCOUNT,
187+
contract=contract,
188+
action='SELL' if quantity > 0 else 'BUY',
189+
quantity=abs(quantity),
190+
limit_price=limit_price)
191+
orders.append(order)
192+
self.execute_orders(orders)
193+
194+
# 环球账户 global account
195+
# asset = trade_client.get_assets(account=ACCOUNT, segment=True)[0].segments['S']
196+
# target_overnight_liquidation = asset.equity_with_loan * self.target_overnight_liquidation_ratio
197+
# adjust_value = asset.sma - target_overnight_liquidation
198+
199+
# 综合/模拟账户 prime/paper account
200+
asset = trade_client.get_prime_assets(account=ACCOUNT).segments['S']
201+
# 调仓后目标隔夜剩余流动性(隔夜剩余流动性 overnight_liquidation = 含贷款价值总权益 equity_with_loan - 隔夜保证金 overnight_margin)
202+
# 隔夜剩余流动性比例 = 隔夜剩余流动性 overnight_liquidation / 含贷款价值总权益 equity_with_loan
203+
target_overnight_liquidation = asset.equity_with_loan * self.target_overnight_liquidation_ratio
204+
# 如果流动性充足,需要买入的金额
205+
adjust_value = asset.overnight_liquidation - target_overnight_liquidation
206+
if adjust_value <= 0:
207+
logger.info('no enough liquidation')
208+
return
209+
quote = get_quote(self.selected_symbols)
210+
# 按持股数量等权重持仓,equal weight
211+
quote[WEIGHT] = 1 / len(self.selected_symbols)
212+
quote[TARGET_QUANTITY] = (adjust_value * quote[WEIGHT] / quote[LATEST_PRICE]).astype(int)
213+
214+
orders = list()
215+
for symbol in quote.index:
216+
contract = stock_contract(symbol, self.currency.name)
217+
quantity = int(quote[TARGET_QUANTITY][symbol] // lot_size[symbol] * lot_size[symbol])
218+
# 该检查主要针对非美股。如果目前下单股数不是log_size的整数倍,则不能下单,只能通过app进行碎股卖出
219+
if quantity == 0:
220+
logger.warning(f'can not place order with this quantity, symbol:{symbol}, lot_size:{lot_size[symbol]},'
221+
f'quantity:{quote[TARGET_QUANTITY][symbol]}')
222+
continue
223+
order = limit_order(account=ACCOUNT,
224+
contract=contract,
225+
action='BUY',
226+
quantity=quantity,
227+
limit_price=quote[LATEST_PRICE][symbol])
228+
order.time_in_force = 'GTC' # 'DAY' 日内有效 / 'GTC' 撤销前有效
229+
orders.append(order)
230+
self.execute_orders(orders)
231+
232+
def execute_orders(self, orders):
233+
local_orders = dict()
234+
for order in orders:
235+
try:
236+
trade_client.place_order(order)
237+
logger.info(f'place order, {order.action} {order.contract.symbol} {order.quantity} {order.limit_price}')
238+
local_orders[order.id] = order
239+
except Exception as e:
240+
logger.error(f'place order error:{order}')
241+
logger.error(e, exc_info=True)
242+
243+
time.sleep(20)
244+
i = 0
245+
while i <= ORDERS_CHECK_MAX_TIMES:
246+
logger.info(f'check {i} times')
247+
history_open_orders = trade_client.get_open_orders(account=ACCOUNT, sec_type=SecurityType.STK,
248+
market=self.market,
249+
start_time=self.get_time_from_now(
250+
datetime.timedelta(days=1)),
251+
end_time=self.get_time_from_now())
252+
if not history_open_orders:
253+
break
254+
255+
# 检查一定次数后如果还未成交, 进行一次改单, 修改限价为最新价格
256+
if i == ORDERS_CHECK_MAX_TIMES // 2:
257+
for open_order in history_open_orders:
258+
latest_price = get_quote([open_order.contract.symbol])[LATEST_PRICE][open_order.contract.symbol]
259+
try:
260+
trade_client.modify_order(open_order, limit_price=latest_price)
261+
logger.info(f'modify order, id:{open_order.id}, symbol:{open_order.contract.symbol},'
262+
f' old_price:{open_order.limit_price}, new_price:{latest_price}')
263+
except Exception as e:
264+
logger.error(f'modify order error:{open_order.id}')
265+
logger.error(e)
266+
# 如果达到最大检查次数还未成交,则进行撤单
267+
if i >= ORDERS_CHECK_MAX_TIMES:
268+
for order in history_open_orders:
269+
logger.info(f'the order was not filled, now cancel it: {order}')
270+
try:
271+
trade_client.cancel_order(ACCOUNT, id=order.id)
272+
except Exception as e:
273+
logger.error(f'cancel order error: {order}')
274+
logger.error(e, exc_info=True)
275+
i += 1
276+
time.sleep(10)
277+
278+
# 打印已成交订单信息
279+
filled_orders = trade_client.get_filled_orders(account=ACCOUNT,
280+
sec_type=SecurityType.STK,
281+
market=self.market,
282+
start_time=self.get_time_from_now(datetime.timedelta(days=1)),
283+
end_time=self.get_time_from_now())
284+
order_infos = [(str(order.id) + ':' + order.contract.symbol + ':' + order.action + ':' + str(order.filled)
285+
+ ':' + str(order.avg_fill_price)) for order in filled_orders]
286+
logger.info(f'recently filled orders:{order_infos}')
287+
288+
# 打印未成交订单信息
289+
unfilled_order_ids = set(local_orders.keys()) - set(order.id for order in filled_orders)
290+
for order_id in unfilled_order_ids:
291+
order = trade_client.get_order(ACCOUNT, id=order_id)
292+
logger.info(f'order was cancelled, id:{order.id}, status:{order.status}, reason:{order.reason}')
293+
294+
@staticmethod
295+
def get_time_from_now(delta=None):
296+
if not delta:
297+
return int(datetime.datetime.now().timestamp()) * 1000
298+
return int((datetime.datetime.now() - delta).timestamp()) * 1000
299+
300+
def run(self):
301+
perms = quote_client.grab_quote_permission()
302+
logger.info(perms)
303+
self.screen_stocks()
304+
self.rebalance_portfolio()
305+
306+
307+
if __name__ == '__main__':
308+
strategy = Strategy()
309+
strategy.run()

0 commit comments

Comments
 (0)