Skip to content

Commit fec46ac

Browse files
committed
Merge branch 'dev' into 'master'
Dev -> master See merge request !33
2 parents 9547011 + cb8f55c commit fec46ac

File tree

8 files changed

+398
-34
lines changed

8 files changed

+398
-34
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='tigeropen',
13-
version='1.1.6',
13+
version='1.1.7',
1414
description='TigerBrokers Open API',
1515
packages=find_packages(exclude=[]),
1616
author='TigerBrokers',

tigeropen/examples/push_client_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def on_position_changed(account, items):
6161
# time.sleep(t)
6262
# else:
6363
# print('reconnect success')
64-
# break
64+
# return
6565
# print('reconnect failed, please check your network')
6666
#
6767

tigeropen/examples/sp500.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import logging
2+
import pandas as pd
3+
import talib as ta
4+
5+
from datetime import datetime, timedelta
6+
from time import sleep
7+
from functools import wraps
8+
9+
from tigeropen.quote.quote_client import QuoteClient
10+
from tigeropen.tiger_open_config import get_client_config
11+
from tigeropen.trade.trade_client import TradeClient
12+
from tigeropen.common.consts import *
13+
from tigeropen.common.util.contract_utils import stock_contract
14+
from tigeropen.common.util.order_utils import limit_order
15+
16+
PRIVATE_KEY_PATH = 'your private key path'
17+
TIGER_ID = 'your tiger_id'
18+
# 下单使用的账户
19+
ACCOUNT = 'your account'
20+
# 查询订单成交的重试次数
21+
MAX_RETRY = 30
22+
# 日志文件的路径
23+
LOG_PATH = 'sp500.log'
24+
25+
# sp500 的成分股
26+
UNIVERSE = ['MMM', 'ABT', 'ABBV', 'ACN', 'ATVI', 'AYI', 'ADBE', 'AMD', 'AAP', 'AES', 'AET', 'AMG', 'AFL', 'A', 'APD',
27+
'AKAM', 'ALK', 'ALB', 'ARE', 'ALXN', 'ALGN', 'ALLE', 'AGN', 'ADS', 'LNT', 'ALL', 'GOOGL', 'GOOG', 'MO',
28+
'AMZN', 'AEE', 'AAL', 'AEP', 'AXP', 'AIG', 'AMT', 'AWK', 'AMP', 'ABC', 'AME', 'AMGN', 'APH', 'APC', 'ADI',
29+
'ANDV', 'ANSS', 'ANTM', 'AON', 'AOS', 'APA', 'AIV', 'AAPL', 'AMAT', 'APTV', 'ADM', 'ARNC', 'AJG', 'AIZ',
30+
'T', 'ADSK', 'ADP', 'AZO', 'AVB', 'AVY', 'BHGE', 'BLL', 'BAC', 'BK', 'BAX', 'BBT', 'BDX', 'BRK.B', 'BBY',
31+
'BIIB', 'BLK', 'HRB', 'BA', 'BKNG', 'BWA', 'BXP', 'BSX', 'BHF', 'BMY', 'AVGO', 'BF.B', 'CHRW', 'CA', 'COG',
32+
'CDNS', 'CPB', 'COF', 'CAH', 'KMX', 'CCL', 'CAT', 'CBOE', 'CBRE', 'CBS', 'CELG', 'CNC', 'CNP', 'CTL',
33+
'CERN', 'CF', 'SCHW', 'CHTR', 'CVX', 'CMG', 'CB', 'CHD', 'CI', 'XEC', 'CINF', 'CTAS', 'CSCO', 'C', 'CFG',
34+
'CTXS', 'CLX', 'CME', 'CMS', 'KO', 'CTSH', 'CL', 'CMCSA', 'CMA', 'CAG', 'CXO', 'COP', 'ED', 'STZ', 'COO',
35+
'GLW', 'COST', 'COTY', 'CCI', 'CSX', 'CMI', 'CVS', 'DHI', 'DHR', 'DRI', 'DVA', 'DE', 'DAL', 'XRAY', 'DVN',
36+
'DLR', 'DFS', 'DISCA', 'DISCK', 'DISH', 'DG', 'DLTR', 'D', 'DOV', 'DWDP', 'DPS', 'DTE', 'DRE', 'DUK',
37+
'DXC', 'ETFC', 'EMN', 'ETN', 'EBAY', 'ECL', 'EIX', 'EW', 'EA', 'EMR', 'ETR', 'EVHC', 'EOG', 'EQT', 'EFX',
38+
'EQIX', 'EQR', 'ESS', 'EL', 'ES', 'RE', 'EXC', 'EXPE', 'EXPD', 'ESRX', 'EXR', 'XOM', 'FFIV', 'FB', 'FAST',
39+
'FRT', 'FDX', 'FIS', 'FITB', 'FE', 'FISV', 'FLIR', 'FLS', 'FLR', 'FMC', 'FL', 'F', 'FTV', 'FBHS', 'BEN',
40+
'FCX', 'GPS', 'GRMN', 'IT', 'GD', 'GE', 'GGP', 'GIS', 'GM', 'GPC', 'GILD', 'GPN', 'GS', 'GT', 'GWW', 'HAL',
41+
'HBI', 'HOG', 'HRS', 'HIG', 'HAS', 'HCA', 'HCP', 'HP', 'HSIC', 'HSY', 'HES', 'HPE', 'HLT', 'HOLX', 'HD',
42+
'HON', 'HRL', 'HST', 'HPQ', 'HUM', 'HBAN', 'HII', 'IDXX', 'INFO', 'ITW', 'ILMN', 'IR', 'INTC', 'ICE',
43+
'IBM', 'INCY', 'IP', 'IPG', 'IFF', 'INTU', 'ISRG', 'IVZ', 'IPGP', 'IQV', 'IRM', 'JEC', 'JBHT', 'SJM',
44+
'JNJ', 'JCI', 'JPM', 'JNPR', 'KSU', 'K', 'KEY', 'KMB', 'KIM', 'KMI', 'KLAC', 'KSS', 'KHC', 'KR', 'LB',
45+
'LLL', 'LH', 'LRCX', 'LEG', 'LEN', 'LUK', 'LLY', 'LNC', 'LKQ', 'LMT', 'L', 'LOW', 'LYB', 'MTB', 'MAC', 'M',
46+
'MRO', 'MPC', 'MAR', 'MMC', 'MLM', 'MAS', 'MA', 'MAT', 'MKC', 'MCD', 'MCK', 'MDT', 'MRK', 'MET', 'MTD',
47+
'MGM', 'KORS', 'MCHP', 'MU', 'MSFT', 'MAA', 'MHK', 'TAP', 'MDLZ', 'MON', 'MNST', 'MCO', 'MS', 'MOS', 'MSI',
48+
'MSCI', 'MYL', 'NDAQ', 'NOV', 'NAVI', 'NKTR', 'NTAP', 'NFLX', 'NWL', 'NFX', 'NEM', 'NWSA', 'NWS', 'NEE',
49+
'NLSN', 'NKE', 'NI', 'NBL', 'JWN', 'NSC', 'NTRS', 'NOC', 'NCLH', 'NRG', 'NUE', 'NVDA', 'ORLY', 'OXY',
50+
'OMC', 'OKE', 'ORCL', 'PCAR', 'PKG', 'PH', 'PAYX', 'PYPL', 'PNR', 'PBCT', 'PEP', 'PKI', 'PRGO', 'PFE',
51+
'PCG', 'PM', 'PSX', 'PNW', 'PXD', 'PNC', 'RL', 'PPG', 'PPL', 'PX', 'PFG', 'PG', 'PGR', 'PLD', 'PRU', 'PEG',
52+
'PSA', 'PHM', 'PVH', 'QRVO', 'PWR', 'QCOM', 'DGX', 'RRC', 'RJF', 'RTN', 'O', 'RHT', 'REG', 'REGN', 'RF',
53+
'RSG', 'RMD', 'RHI', 'ROK', 'COL', 'ROP', 'ROST', 'RCL', 'CRM', 'SBAC', 'SCG', 'SLB', 'STX', 'SEE', 'SRE',
54+
'SHW', 'SPG', 'SWKS', 'SLG', 'SNA', 'SO', 'LUV', 'SPGI', 'SWK', 'SBUX', 'STT', 'SRCL', 'SYK', 'STI',
55+
'SIVB', 'SYMC', 'SYF', 'SNPS', 'SYY', 'TROW', 'TTWO', 'TPR', 'TGT', 'TEL', 'FTI', 'TXN', 'TXT', 'TMO',
56+
'TIF', 'TWX', 'TJX', 'TMK', 'TSS', 'TSCO', 'TDG', 'TRV', 'TRIP', 'FOXA', 'FOX', 'TSN', 'UDR', 'ULTA',
57+
'USB', 'UAA', 'UA', 'UNP', 'UAL', 'UNH', 'UPS', 'URI', 'UTX', 'UHS', 'UNM', 'VFC', 'VLO', 'VAR', 'VTR',
58+
'VRSN', 'VRSK', 'VZ', 'VRTX', 'VIAB', 'V', 'VNO', 'VMC', 'WMT', 'WBA', 'DIS', 'WM', 'WAT', 'WEC', 'WFC',
59+
'WELL', 'WDC', 'WU', 'WRK', 'WY', 'WHR', 'WMB', 'WLTW', 'WYN', 'WYNN', 'XEL', 'XRX', 'XLNX', 'XL', 'XYL',
60+
'YUM', 'ZBH', 'ZION', 'ZTS']
61+
62+
63+
client_config = get_client_config(private_key_path=PRIVATE_KEY_PATH, tiger_id=TIGER_ID, account=ACCOUNT,
64+
sandbox_debug=True)
65+
quote_client = QuoteClient(client_config)
66+
trade_client = TradeClient(client_config)
67+
68+
69+
# 初始化 log 模块
70+
logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s',
71+
filename=LOG_PATH,
72+
filemode='a')
73+
log = logging.getLogger(__name__)
74+
log.setLevel(logging.INFO)
75+
76+
77+
def recorder(func):
78+
@wraps(func)
79+
def do_log(*args, **kwargs):
80+
try:
81+
log.info('func name is %s, args are %s, kwars are %s' % (func.__name__, args, kwargs))
82+
return func(*args, **kwargs)
83+
except Exception as e:
84+
log.warning(e, exc_info=True)
85+
return do_log
86+
87+
88+
@recorder
89+
def get_daily_close(symbols):
90+
"""获取日级收盘价
91+
:param symbols:证券代码列表
92+
"""
93+
i = 0
94+
price = pd.DataFrame()
95+
while i <= len(symbols):
96+
batch = symbols[i:i+50]
97+
i += 50
98+
temp = quote_client.get_bars(symbols=batch)
99+
temp['time'] = temp['time'].map(lambda x: datetime.fromtimestamp(x/1000))
100+
price = price.append(temp)
101+
log.info('batch number is %s' % i)
102+
103+
return price
104+
105+
106+
@recorder
107+
def calculate_indicators(price):
108+
"""计算用于交易的指标: 昨收价对于 EMA 的偏离值
109+
:return:Series,index 是 symbol, value 是指标的值
110+
"""
111+
# 先处理数据结构, index 是时间, column 是股票代码
112+
price = price[['symbol', 'time', 'close']]
113+
price.set_index('time', inplace=True)
114+
close = price.pivot(columns='symbol')
115+
close.columns = close.columns.droplevel()
116+
# 计算调仓使用的技术指标
117+
ema = close.apply(lambda x: ta.EMA(x.values, timeperiod=10))
118+
indicator = 1 - (ema.iloc[-1, :]/close.iloc[-1, :])
119+
return indicator
120+
121+
122+
@recorder
123+
def get_optimal_portfolio(indicator, holding_num):
124+
"""计算市值加权的目标调仓权重
125+
"""
126+
127+
target_stock = indicator.sort_values()[:holding_num]
128+
log.info('target stock is %s, target num is %s' % (target_stock, holding_num))
129+
market_cap = quote_client.get_financial_daily(symbols=list(target_stock.index),
130+
market=Market.US,
131+
fields=[Valuation.market_capitalization],
132+
begin_date=(datetime.now()-timedelta(days=4)).timestamp()*1000,
133+
end_date=datetime.now().timestamp()*1000)
134+
135+
market_cap = market_cap.sort_values('date', ascending=False).drop_duplicates(subset='symbol', keep='first')
136+
log.info('market cap is %s' % market_cap)
137+
market_cap.set_index('symbol', inplace=True)
138+
market_cap = market_cap['value']
139+
return market_cap/market_cap.sum()
140+
141+
142+
@recorder
143+
def batch_get_stock_briefs(symbols):
144+
i = 0
145+
briefs = pd.DataFrame()
146+
while i <= len(symbols):
147+
batch = symbols[i:i+50]
148+
i += 50
149+
temp = quote_client.get_stock_briefs(symbols=batch)
150+
briefs = briefs.append(temp)
151+
log.info('batch number is %s' % i)
152+
153+
return briefs
154+
155+
156+
@recorder
157+
def execute(target_weight):
158+
"""调仓,并返回结果
159+
"""
160+
positions = trade_client.get_positions(account=ACCOUNT, market=Market.US)
161+
# 当前持仓
162+
current_holdings = {}
163+
# 卖单
164+
sell_orders = {}
165+
# 买单
166+
buy_orders = {}
167+
168+
for i in positions:
169+
current_holdings.update({i.contract.symbol: i.quantity})
170+
if len(current_holdings.keys()) != 0:
171+
# 先卖出当前持仓中不在目标调仓列表中的股票
172+
briefs = batch_get_stock_briefs(symbols=list(current_holdings.keys()))
173+
briefs.set_index('symbol', inplace=True)
174+
latest_price = briefs.latest_price
175+
current_holdings = pd.Series(current_holdings)
176+
177+
liquidation_list = set(current_holdings.index) - set(target_weight.index)
178+
log.info('executing sell orders ,latest_price are %s' % latest_price)
179+
180+
for symbol in liquidation_list:
181+
contract = stock_contract(symbol, currency='USD')
182+
quantity = current_holdings[symbol]
183+
limit_price = round(latest_price[symbol], 2)
184+
order = limit_order(account=ACCOUNT,
185+
contract=contract,
186+
action='SELL' if quantity > 0 else 'BUY',
187+
quantity=abs(int(quantity)),
188+
limit_price=limit_price)
189+
try:
190+
trade_client.place_order(order)
191+
sell_orders.update({order.id: order})
192+
except Exception:
193+
log.error('Faild to get result, contract: %s, limit_price: %s' % (contract, limit_price), exc_info=True)
194+
# 轮询卖单的执行情况
195+
retry_time = 0
196+
while retry_time < MAX_RETRY and len(sell_orders.keys()) != 0:
197+
filled_orders = trade_client.get_filled_orders(account=ACCOUNT,
198+
sec_type=SecurityType.STK,
199+
market=Market.US,
200+
start_time=(datetime.now()-timedelta(days=1)).timestamp()*1000,
201+
end_time=datetime.now().timestamp()*1000)
202+
for order in filled_orders:
203+
sell_orders.pop(order.id, None)
204+
205+
retry_time += 1
206+
sleep(20)
207+
log.info('retry time %s, remaining orders %s' % (retry_time, len(sell_orders.keys())))
208+
else:
209+
# 取消所有open 状态的订单
210+
for id in sell_orders.keys():
211+
trade_client.cancel_order(account=ACCOUNT, id=id)
212+
sleep(2)
213+
214+
# 下买单
215+
# 先获取 security Segment 的账户信息(标准与模拟账户都类似, 环球账户不同)
216+
assets = trade_client.get_assets(account=ACCOUNT)[0].segments['S']
217+
# 按照当前的可用现金下单(无杠杆)
218+
cash = assets.cash
219+
if cash < 0:
220+
return
221+
target_value = cash * target_weight
222+
briefs = batch_get_stock_briefs(symbols=list(target_value.index))
223+
briefs.set_index('symbol', inplace=True)
224+
latest_price = briefs.latest_price
225+
log.info('cash is %s, target_value is %s' % (cash, target_value))
226+
for symbol in target_value.index:
227+
contract = stock_contract(symbol, 'USD')
228+
limit_price = round(latest_price[symbol], 2) # 不同合约对下单价格的变动单位不同
229+
order = limit_order(account=ACCOUNT,
230+
contract=contract,
231+
action='BUY',
232+
quantity=int(target_value[symbol]/latest_price[symbol]),
233+
limit_price=limit_price)
234+
try:
235+
trade_client.place_order(order)
236+
buy_orders.update({order.id: order})
237+
except Exception:
238+
log.error('Faild to get result, contract: %s, limit_price: %s' % (contract, limit_price), exc_info=True)
239+
240+
# 检查更新订单的状态
241+
for id in buy_orders.keys():
242+
order = trade_client.get_order(account=ACCOUNT, id=id)
243+
buy_orders[id] = order
244+
log.info('buy order status %s' % buy_orders)
245+
246+
247+
def run():
248+
price = get_daily_close(symbols=UNIVERSE)
249+
indicator = calculate_indicators(price)
250+
optimal_portfolio = get_optimal_portfolio(indicator, 10)
251+
execute(optimal_portfolio)
252+
253+
254+
if __name__ == '__main__':
255+
run()

tigeropen/push/push_client.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,41 @@
2121
}
2222
QUOTE_KEYS_MAPPINGS = {field.value: field.name for field in QuoteChangeKey} # like {'askPrice': 'ask_price'}
2323
QUOTE_KEYS_MAPPINGS.update(HOUR_TRADING_QUOTE_KEYS_MAPPINGS)
24+
PRICE_FIELDS = {'open', 'high', 'low', 'close', 'prev_close', 'ask_price', 'bid_price', 'latest_price'}
2425

2526
ASSET_KEYS_MAPPINGS = {'buyingPower': 'buying_power', 'cashBalance': 'cash',
2627
'grossPositionValue': 'gross_position_value',
2728
'netLiquidation': 'net_liquidation', 'equityWithLoan': 'equity_with_loan',
2829
'initMarginReq': 'initial_margin_requirement',
2930
'maintMarginReq': 'maintenance_margin_requirement',
3031
'availableFunds': 'available_funds', 'excessLiquidity': 'excess_liquidity',
31-
'dayTradesRemaining': 'day_trades_remaining', 'currency': 'currency'}
32+
'dayTradesRemaining': 'day_trades_remaining', 'currency': 'currency', 'segment': 'segment'}
3233

3334
POSITION_KEYS_MAPPINGS = {'averageCost': 'average_cost', 'position': 'quantity', 'latestPrice': 'market_price',
3435
'marketValue': 'market_value', 'orderType': 'order_type', 'realizedPnl': 'realized_pnl',
3536
'unrealizedPnl': 'unrealized_pnl', 'secType': 'sec_type', 'localSymbol': 'local_symbol',
3637
'originSymbol': 'origin_symbol', 'contractId': 'contract_id', 'symbol': 'symbol',
37-
'currency': 'currency', 'strike': 'strike', 'expiry': 'expiry', 'right': 'right'}
38+
'currency': 'currency', 'strike': 'strike', 'expiry': 'expiry', 'right': 'right',
39+
'segment': 'segment'}
3840

3941
ORDER_KEYS_MAPPINGS = {'parentId': 'parent_id', 'orderId': 'order_id', 'orderType': 'order_type',
4042
'limitPrice': 'limit_price', 'auxPrice': 'aux_price', 'avgFillPrice': 'avg_fill_price',
4143
'totalQuantity': 'quantity', 'filledQuantity': 'filled', 'lastFillPrice': 'last_fill_price',
42-
'realizedPnl': 'realized_pnl', 'secType': 'sec_type',
44+
'realizedPnl': 'realized_pnl', 'secType': 'sec_type', 'symbol': 'symbol',
4345
'remark': 'reason', 'localSymbol': 'local_symbol', 'originSymbol': 'origin_symbol',
4446
'outsideRth': 'outside_rth', 'timeInForce': 'time_in_force', 'openTime': 'order_time',
4547
'latestTime': 'trade_time', 'contractId': 'contract_id', 'trailStopPrice': 'trail_stop_price',
4648
'trailingPercent': 'trailing_percent', 'percentOffset': 'percent_offset', 'action': 'action',
47-
'status': 'status', 'currency': 'currency', 'remaining': 'remaining', 'id': 'id'}
49+
'status': 'status', 'currency': 'currency', 'remaining': 'remaining', 'id': 'id',
50+
'segment': 'segment'}
4851

4952
if sys.platform == 'linux' or sys.platform == 'linux2':
5053
KEEPALIVE = True
5154
else:
5255
KEEPALIVE = False
5356

5457

55-
class PushClient(object):
58+
class PushClient(stomp.ConnectionListener):
5659
def __init__(self, host, port, use_ssl=True, connection_timeout=120, auto_reconnect=True,
5760
heartbeats=(30 * 1000, 30 * 1000)):
5861
"""
@@ -155,12 +158,34 @@ def on_message(self, headers, body):
155158
hour_trading = True
156159
if 'symbol' in data:
157160
symbol = data.get('symbol')
161+
offset = data.get('offset', 0)
158162
items = []
159-
for key, value in data.items():
160-
if (key == 'latestTime' or key == 'hourTradingLatestTime') and isinstance(value, six.string_types):
161-
continue
162-
if key in QUOTE_KEYS_MAPPINGS:
163-
items.append((QUOTE_KEYS_MAPPINGS.get(key), value))
163+
# 期货行情推送的价格都乘了 10 的 offset 次方变成了整数, 需要除回去变为正常单位的价格
164+
if offset:
165+
for key, value in data.items():
166+
if (key == 'latestTime' or key == 'hourTradingLatestTime') and \
167+
isinstance(value, six.string_types):
168+
continue
169+
if key in QUOTE_KEYS_MAPPINGS:
170+
key = QUOTE_KEYS_MAPPINGS.get(key)
171+
if key in PRICE_FIELDS:
172+
value /= 10 ** offset
173+
elif key == 'minute':
174+
minute_item = dict()
175+
for m_key, m_value in value.items():
176+
if m_key in {'p', 'h', 'l'}:
177+
m_value /= 10 ** offset
178+
minute_item[m_key] = m_value
179+
value = minute_item
180+
items.append((key, value))
181+
else:
182+
for key, value in data.items():
183+
if (key == 'latestTime' or key == 'hourTradingLatestTime') and \
184+
isinstance(value, six.string_types):
185+
continue
186+
if key in QUOTE_KEYS_MAPPINGS:
187+
key = QUOTE_KEYS_MAPPINGS.get(key)
188+
items.append((key, value))
164189
if items:
165190
self.quote_changed(symbol, items, hour_trading)
166191
elif response_type == str(ResponseType.SUBSCRIBE_ASSET.value):

0 commit comments

Comments
 (0)