├── BuySell.py ├── EzQmt ├── __init__.py └── smy.py ├── NormFunc.py ├── README.md ├── Reblance.py ├── Summary.py ├── launch.py ├── setup.py └── upload_pypi.sh /BuySell.py: -------------------------------------------------------------------------------- 1 | # encoding:gbk 2 | import datetime, re, time, os, copy 3 | import numpy as np 4 | import pandas as pd 5 | 6 | ####################################################################################################### 7 | ############################# 自定义参数 ########################################### 8 | ####################################################################################################### 9 | 10 | # 1. 根据策略文件买入; 11 | # 2. 卖出买入仓位。 12 | 13 | # 基本设置 14 | ACCOUNT = '66666666' # 填写您的账号 15 | account_type = 'STOCK' 16 | multiples = 10 # 可转债每手十张 17 | strategy_name = 'strat' # 策略名称 18 | logloc = '' # 您的日志文件位置 19 | logfile = logloc + ACCOUNT + '-' + strategy_name + '.txt' 20 | statfile = '' + ACCOUNT + '/' + strategy_name + '.txt' # 策略状态文件位置 21 | 22 | # 策略输入 23 | stratfile_loc = '' # 策略文件存储位置 24 | stratfile_name = strategy_name # 按照 strat_file_loc+日期(20240102)-strat_file_name 格式输入策略文件 25 | buy_num = 0 # 此排名内等权买入(如果是0则按策略文件权重买入) 26 | 27 | # 交易设置 28 | strat_cap = 100e4 # 全部篮子标的目标市值 29 | buy_start_time = '093000' # 开始交易时点 30 | buy_dur_time = 300 # 最长交易时间(秒) 31 | sell_start_time = '145000' # 开始交易时点 32 | sell_dur_time = 300 # 最长交易时间(秒) 33 | interval = 30 # 每隔interval秒挂单一次 34 | wait_dur = interval*0.8 # 订单等待wait_dur秒后未成交撤单 35 | tolerance = 0.01 # 买卖挂单超价(%) 36 | delta_min = 3000 # 最小挂单金额 37 | delta_max = 30000 # 最大挂单金额 38 | max_upndown = 0.2 # 涨跌幅限制,转债20% 39 | 40 | 41 | buy_prepare_time = datetime.datetime.strptime(buy_start_time, "%H%M%S")\ 42 | + datetime.timedelta(seconds=-30) # 交易前30s读取篮子文件 43 | buy_prepare_time = buy_prepare_time.strftime("%H%M%S") 44 | buy_end_time = datetime.datetime.strptime(buy_start_time, "%H%M%S")\ 45 | + datetime.timedelta(seconds=buy_dur_time) 46 | buy_end_time = buy_end_time.strftime("%H%M%S") 47 | buy_summary_time = datetime.datetime.strptime(buy_end_time, "%H%M%S")\ 48 | + datetime.timedelta(seconds=interval) # 交易结束后interval总结交易结果 49 | buy_summary_time = buy_summary_time.strftime("%H%M%S") 50 | sell_prepare_time = datetime.datetime.strptime(sell_start_time, "%H%M%S")\ 51 | + datetime.timedelta(seconds=-30) # 交易前30s读取篮子文件 52 | sell_prepare_time = sell_prepare_time.strftime("%H%M%S") 53 | sell_end_time = datetime.datetime.strptime(sell_start_time, "%H%M%S")\ 54 | + datetime.timedelta(seconds=sell_dur_time) 55 | sell_end_time = sell_end_time.strftime("%H%M%S") 56 | sell_summary_time = datetime.datetime.strptime(sell_end_time, "%H%M%S")\ 57 | + datetime.timedelta(seconds=interval) # 交易结束后interval总结交易结果 58 | sell_summary_time = sell_summary_time.strftime("%H%M%S") 59 | 60 | 61 | ####################################################################################################### 62 | ############################# 常用函数模块 ########################################### 63 | ####################################################################################################### 64 | 65 | 66 | 67 | ########################################### 日常io运行 ################################################### 68 | 69 | # log 函数 70 | def log(*txt): 71 | try: 72 | f = open(logfile,'a+', encoding='gbk') 73 | f.write('%s'%datetime.datetime.now()+' '*6) # 时间戳 74 | if type(txt[0])==pd.Series: 75 | f.write('name: %s\n'%txt[0].name) 76 | for i,v in txt[0].items(): 77 | f.write(' '*32+str(i)+', '+str(v)+'\n') 78 | elif type(txt[0])==pd.DataFrame: 79 | f.write(' '.join([str(i) for i in txt[0].columns])+'\n') 80 | for i,r in txt[0].iterrows(): 81 | f.write(' '*29+str(i)+': ') 82 | f.write(', '.join([str(j) for j in r.values])+'\n') 83 | else: 84 | write_str = ('\n'+' '*32).join([str(i) for i in txt]) 85 | f.write('%s\n' %write_str) 86 | f.close() 87 | except PermissionError as e: 88 | print(f"Error: {e}. You don't have permission to access the specified file.") 89 | 90 | ########################################### 行情数据 ################################################### 91 | 92 | # 获取行情快照数据 DataFrame, index:code 93 | SHmul = 10 94 | SZmul = 10 95 | def get_snapshot(C, code_list): 96 | # 获取标的快照数据 97 | df = C.get_full_tick(code_list) 98 | df = pd.DataFrame.from_dict(df, dtype='float').T.reset_index().rename(columns={'index': 'code'}) 99 | # 盘口 100 | bidPrice_columns = ['bidp1','bidp2','bidp3','bidp4','bidp5'] 101 | askPrice_columns = ['askp1','askp2','askp3','askp4','askp5'] 102 | df[bidPrice_columns] = df['bidPrice'].apply(pd.Series, index=bidPrice_columns) 103 | df[askPrice_columns] = df['askPrice'].apply(pd.Series, index=askPrice_columns) 104 | bidVol_columns = ['bidv1','bidv2','bidv3','bidv4','bidv5'] 105 | askVol_columns = ['askv1','askv2','askv3','askv4','askv5'] 106 | df[bidVol_columns] = df['bidVol'].apply(pd.Series, index=bidVol_columns) 107 | df[askVol_columns] = df['askVol'].apply(pd.Series, index=askVol_columns) 108 | # 中间价 109 | df['mid'] = (df['bidp1'] + df['askp1'])/2 110 | # 涨跌停则bid/askprice为0 111 | df.loc[(df['bidp1'] == 0) | (df['askp1'] == 0),'mid'] = df['bidp1'] + df['askp1'] # 涨跌停修正 112 | ## 展示列 最新价,当日成交额、成交量(手)、最高价、最低价、开盘价 113 | # 盘口 askp\askv*/bid* 买卖5档, 昨收 114 | ## 中间价 askp\askv*/bid* 买卖5档,需要使用券商行情 115 | display_columns = ['code', 'lastPrice', 'amount', 'volume', 'high', 'low', 'open', 'lastClose',\ 116 | 'mid', 'askp1', 'askp2', 'askp3', 'askp4', 'askp5', \ 117 | 'bidp1', 'bidp2', 'bidp3', 'bidp4', 'bidp5', \ 118 | 'askv1', 'askv2', 'askv3', 'askv4', 'askv5',\ 119 | 'bidv1', 'bidv2', 'bidv3', 'bidv4', 'bidv5'] 120 | df = df[display_columns].rename(columns={'volume':'vol'}) 121 | df = df.set_index('code') 122 | # 有时,沪市转债单位是手,沪市需要乘一个沪市转化因子 123 | df['vol'] = df['vol']*df.index.map(lambda x: SHmul if 'SH' in x else SZmul if 'SZ' in x else 1) 124 | return df 125 | 126 | ########################################### 账户状态 ################################################### 127 | 128 | # 获取账户状态 净值,现金 129 | def get_account(): 130 | acct_info = get_trade_detail_data(ACCOUNT, account_type, 'account')[0] 131 | return {'net':acct_info.m_dBalance, 'cash':acct_info.m_dAvailable} 132 | # 获取持仓数据 DataFrame index:code, cash 如果没有持仓返回空表(但是有columns) 133 | def get_pos(): 134 | position_to_dict = lambda pos: { 135 | 'code': pos.m_strInstrumentID + '.' + pos.m_strExchangeID, # 证券代码 000001.SZ 136 | 'name': pos.m_strInstrumentName, # 证券名称 137 | 'vol': pos.m_nVolume, # 当前拥股,持仓量 138 | 'AvailableVol': pos.m_nCanUseVolume, # 可用余额,可用持仓,期货不用这个字段,股票的可用数量 139 | 'MarketValue': pos.m_dMarketValue, # 市值,合约价值 140 | 'PositionCost': pos.m_dPositionCost, # 持仓成本, 141 | } 142 | position_info = get_trade_detail_data(ACCOUNT, account_type, 'position') 143 | pos = pd.DataFrame(list(map(position_to_dict, position_info))) 144 | if pos.empty: 145 | return pd.DataFrame(columns=['name', 'vol', 'AvailabelVol', 'MarketValue', 'PositionCost']) 146 | pos = pos.set_index('code') 147 | extract_names = ['新标准券', '国标准券'] 148 | pos = pos[(pos['vol']!=0)&(~pos['name'].isin(extract_names))].copy() # 已清仓不看,去掉逆回购重复输出 149 | return pos 150 | # 忽略逆回购订单、交割单 151 | status_extract_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 152 | '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 153 | '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 154 | '204014.SH', '204028.SH', '204091.SH', '204182.SH'] 155 | # 获取订单状态 当日没有订单返回空表(但是有columns) 当天订单 156 | def get_order(): 157 | order_info = get_trade_detail_data(ACCOUNT, account_type, 'ORDER') 158 | order_to_dict = lambda o:{ 159 | 'id':o.m_strOrderSysID, 160 | 'date': o.m_strInsertDate, 161 | 'code': o.m_strInstrumentID+'.'+o.m_strExchangeID, 162 | 'sub_time': o.m_strInsertTime, # 例如 str:095620 163 | 'trade_type': o.m_nOffsetFlag, # 48 买入/开仓;49 卖出/平仓 164 | 'price': o.m_dLimitPrice, # 挂单价 165 | 'sub_vol': o.m_nVolumeTotalOriginal, 166 | 'dealt_vol': o.m_nVolumeTraded, 167 | 'remain_vol': o.m_nVolumeTotal, 168 | # 48 未报, 49 待报, 50 已报, 51 已报待撤,52 部成待撤, 53 部撤(部成撤单), 169 | # 54 已撤, 55 部成, 56 已成, 57 废单(算法单执行完毕之后为废单), 86 已确认, 255 未知 170 | 'status':o.m_nOrderStatus, 171 | 'frozen':o.m_dFrozenMargin+o.m_dFrozenCommission, # 冻结金额/保证金+手续费 172 | 'remark':o.m_strRemark # 订单备注 173 | } 174 | order = pd.DataFrame(list(map(order_to_dict, order_info))) 175 | if order.empty: 176 | return pd.DataFrame(columns=['id', 'date', 'code', 'sub_time', 'trade_type',\ 177 | 'price', 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']) 178 | order = order[(order['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 179 | (~order['code'].isin(status_extract_codes))].copy() 180 | order = order.set_index('id') 181 | return order[['date', 'code', 'sub_time', 'trade_type', 'price',\ 182 | 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']] 183 | # 获取成交数据 184 | def get_deal(): 185 | deal_info = get_trade_detail_data(ACCOUNT, account_type, 'DEAL') 186 | deal_to_dict = lambda d:{ 187 | 'order_id':d.m_nRef, # 订单编号 188 | 'id':d.m_strOrderSysID, # 合同编号 189 | 'code': d.m_strInstrumentID + '.' + d.m_strExchangeID, 190 | 'date':d.m_strTradeDate, 191 | 'deal_time':d.m_strTradeTime, # 成交时间 192 | # 48 买入/开仓 49卖出/平仓 50 强平 51 平今 52 平昨 53 强减 193 | 'trade_type':d.m_nOffsetFlag, 194 | 'price':d.m_dPrice, 195 | 'vol': d.m_nVolume, 196 | 'amount': d.m_dTradeAmount, 197 | 'remark': d.m_strRemark 198 | } 199 | deal = pd.DataFrame(list(map(deal_to_dict, deal_info))) 200 | if deal.empty: 201 | return pd.DataFrame(columns=['id', 'order_id', 'code', 'date', 'deal_time',\ 202 | 'trade_type', 'price', 'vol', 'amount', 'remark']) 203 | deal = deal[(deal['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 204 | (~deal['code'].isin(status_extract_codes))].copy() 205 | return deal[['id', 'order_id', 'code', 'date', 'deal_time',\ 206 | 'trade_type', 'price', 'vol', 'amount', 'remark']] 207 | 208 | ########################################### 买卖挂单 ################################################### 209 | 210 | # 撤单 超过wait_dur s的订单取消 211 | def cancel_order(C, wait_dur, stratname=None): 212 | order = get_order() 213 | # 全部可撤订单 214 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 215 | if order.empty: 216 | return 217 | # 属于该策略 218 | if stratname!=None: 219 | order = order[order['remark']==stratname].copy() 220 | if not order.empty: 221 | # 超过等待时间撤单 insert_time 为1900年 222 | order['sub_time'] = order['sub_time'].map(lambda x: datetime.datetime.strptime(x, "%H%M%S")) 223 | order = order[order['sub_time'].map(lambda x: (datetime.datetime.now()-x).seconds>wait_dur)] 224 | for orderid in order.index: 225 | cancel(orderid, ACCOUNT, account_type, C) 226 | # 撤单 属于策略strat的挂单价超过最新价(1+r)或低于最新价(1-r)的订单取消 227 | def cancel_order_price(C, r, stratname=None): 228 | order = get_order() 229 | # 全部可撤订单 230 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 231 | if order.empty: 232 | return 233 | # 属于该策略 234 | if stratname!=None: 235 | order = order[order['remark']==stratname].copy() 236 | if not order.empty: 237 | # 最新价格 238 | codes = list(set(order['code'])) 239 | snapshot = get_snapshot(C, codes) 240 | lastPrice = snapshot[['lastPrice', 'lastClose']].apply(lambda x: \ 241 | x['lastPrice'] if x['lastPrice']!=0 else x['lastClose'], axis=1) 242 | lastPrice = order['code'].map(lambda x: lastPrice[x]) 243 | if not order.empty: 244 | delta = abs((order['price']-lastPrice)/lastPrice) 245 | delta = delta[delta>r] 246 | for orderid in delta.index: 247 | cancel(orderid, ACCOUNT, account_type, C) 248 | #strategy_name = 'craft' 249 | #multiples = 10 250 | #卖出 251 | def sell(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 252 | vol = int((vol//multiples)*multiples) 253 | if vol==0: 254 | print('too less vol to sub') 255 | return 256 | # 卖出,单标的,账号, 代码,限价单,价格,量,策略名,立即触发下单,备注 257 | if account_type=='STOCK': 258 | passorder(24, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 259 | elif account_type=='CREDIT': 260 | passorder(34, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 261 | #买入 262 | def buy(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 263 | vol = int((vol//multiples)*multiples) 264 | if vol==0: 265 | print('too less vol to sub') 266 | return 267 | if account_type=='STOCK': 268 | passorder(23, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 269 | elif account_type=='CREDIT': 270 | passorder(33, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 271 | 272 | ########################################### 其他 ################################################### 273 | 274 | # 存储全局变量 275 | class a(): 276 | pass 277 | 278 | 279 | 280 | ####################################################################################################### 281 | ############################# 策略主代码 ########################################### 282 | ####################################################################################################### 283 | 284 | 285 | # 初始化准备 286 | def buy_prepare(C): 287 | strat_files = [f for f in os.listdir(stratfile_loc) \ 288 | if f.split('.')[-2].split('-')[-1]==stratfile_name] 289 | strat_files = sorted(strat_files) 290 | strat_file = stratfile_loc + strat_files[-1] 291 | df = pd.read_csv(strat_file, encoding='utf-8') 292 | log('读取策略文件', strat_file) 293 | print('读取策略文件', strat_file) 294 | sorted_codes = (df['代码'].astype('str')+'.'+df['市场']).values # 策略标的按打分排序 295 | if buy_num==0: 296 | print(df[['代码', '市场', 'name', 'weight']]) 297 | weights = pd.Series(df['weight'].values, index=sorted_codes) 298 | else: 299 | print(df[['代码', '市场', 'name']]) 300 | weights = pd.Series(1, index=list(sorted_codes[:buy_num])) 301 | weights = weights/weights.sum() 302 | buy_cap = strat_cap*weights 303 | log('目标买入额') 304 | log(buy_cap) 305 | buy_cap.loc[buy_cap0] 308 | snapshot = get_snapshot(C, list(buy_cap.index)) 309 | mid_snapshot = snapshot['mid'] 310 | trade_vol = buy_cap/mid_snapshot 311 | log('目标交易张数,金额,即时价格') 312 | log(pd.concat([trade_vol, buy_cap, mid_snapshot], axis=1)) 313 | # trader运行所需参数 314 | A.trade_vol = trade_vol 315 | A.traded_vol = pd.Series(0, A.trade_vol.index) # 已成交张数 316 | A.remain_times = int(buy_dur_time/interval-1) # 剩余挂单轮数 317 | A.start_time = buy_start_time 318 | A.end_time = buy_end_time 319 | A.dur_time = buy_dur_time 320 | A.interval = interval 321 | A.summary_time = buy_summary_time 322 | 323 | # 卖出 324 | def sell_prepare(C): 325 | with open(statfile, 'r') as f: 326 | r = f.readlines() 327 | bought = {i.split(',')[0]:int(i.split(',')[1][:-1]) for i in r} 328 | bought = pd.Series(bought) 329 | log('昨夜买入:') 330 | log(bought) 331 | print('昨夜买入:') 332 | print(bought) 333 | # trader运行所需参数 334 | A.trade_vol = -bought 335 | A.traded_vol = pd.Series(0, bought.index) # 已成交张数 336 | A.remain_times = int(sell_dur_time/interval-1) # 剩余挂单轮数 337 | A.start_time = sell_start_time 338 | A.end_time = sell_end_time 339 | A.dur_time = sell_dur_time 340 | A.interval = interval 341 | A.summary_time = sell_summary_time 342 | 343 | # 挂单员 344 | def trader(C): 345 | if A.remain_times==0: 346 | return 347 | log('第%s/%s轮挂单'%(int(A.dur_time/A.interval)-A.remain_times+1, int(A.dur_time/A.interval))) 348 | snapshot = get_snapshot(C, list(A.trade_vol.index)) 349 | mid_snapshot = snapshot['mid'] 350 | bidPrice_snapshot = snapshot['bidp1'] 351 | askPrice_snapshot = snapshot['askp1'] 352 | lastClose_snapshot = snapshot['lastClose'] 353 | # 该trader已成交数量(备注为strategy_name, sub_time从start_time到end_time) 354 | trader_orders = get_order().reset_index() 355 | trader_orders = trader_orders[(trader_orders['remark']==strategy_name)&\ 356 | (trader_orders['sub_time']>A.start_time)&(trader_orders['sub_time']'145700' else 0)*\ 358 | trader_orders['code'].map(lambda x: 1 if 'SZ' in x else 0) 359 | trader_orders['dealt_vol'] = trader_orders['dealt_vol']+undeal # SZ 交易所,1457之后未成交全部归为成交 360 | trader_orders['dealt_vol'] = trader_orders['dealt_vol']*trader_orders['trade_type'].map(lambda x: 1 if x==48 else -1) 361 | A.traded_vol = (trader_orders.groupby('code')['dealt_vol'].sum()).reindex(A.traded_vol.index).fillna(0) 362 | log('策略已成交:') 363 | log(A.traded_vol) 364 | # 涨停不卖出、不排板 365 | limitup_codes = askPrice_snapshot[askPrice_snapshot.isna()|(askPrice_snapshot==0)] 366 | limitdown_codes = bidPrice_snapshot[bidPrice_snapshot.isna()|(bidPrice_snapshot==0)] 367 | should_trade_vol = (A.trade_vol - A.traded_vol).sort_values() # 先卖再买 368 | for code, delta_vol in should_trade_vol.items(): 369 | log('处理 %s %s'%(code, delta_vol)) 370 | if code in limitup_codes.index: 371 | print('涨停不卖出,不排版') 372 | log('涨停不卖出,不排板') 373 | continue 374 | mean_vol = abs(delta_vol)/A.remain_times # 平均每次需要交易张数绝对值 375 | price = mid_snapshot.loc[code] 376 | if (abs(delta_vol*price)0): 377 | log('买入与目标市值差距小于挂单金额,不交易') 378 | continue 379 | else: 380 | if mean_vol*price0: 389 | # 跌停不买入 390 | if code in limitdown_codes.index: 391 | print('跌停不买入', code) 392 | log('跌停不买入', code) 393 | else: 394 | # 不能超过涨停价 395 | price = min(lastClose_snapshot[code]*(1+max_upndown), bidPrice_snapshot[code]*(tolerance+1)) 396 | buy(C, code, price, vol) 397 | log('buy %s %s %s'%(code, price, vol)) 398 | #print('buy', code, price, vol) 399 | else: 400 | price = min(lastClose_snapshot[code]*(1-max_upndown), askPrice_snapshot[code]*(1-tolerance)) 401 | sell(C, code, price, vol) 402 | log('sell %s %s %s'%(code, price, vol)) 403 | #print('sell', code, price, vol) 404 | A.remain_times = A.remain_times-1 405 | 406 | # 交易情况总结 407 | def summary(C): 408 | trader_orders = get_order().reset_index() 409 | trader_orders = trader_orders[(trader_orders['remark']==strategy_name)&\ 410 | (trader_orders['sub_time']>A.start_time)&(trader_orders['sub_time']buy_end_time)&\ 438 | (datetime.datetime.now().strftime("%H%M%S")sell_end_time: 444 | A.start_time = sell_start_time 445 | A.summary_time = sell_summary_time 446 | summary(C) 447 | else: 448 | if datetime.datetime.now().strftime("%H%M%S")sell_end_time)&\ 451 | (datetime.datetime.now().strftime("%H%M%S")buy_end_time: 457 | A.start_time = buy_start_time 458 | A.summary_time = buy_summary_time 459 | summary(C) 460 | # 交易时间运行装饰器 交易日start到start+dur 461 | def buy_trader_time(func): 462 | def wrapper(*args, **kwargs): 463 | today = datetime.datetime.now().date().strftime("%Y%m%d") 464 | now = datetime.datetime.now().time() 465 | if C.get_trading_dates('SH', today, today, 1, '1d'): 466 | if (datetime.time(int(buy_start_time[:2]), int(buy_start_time[2:4]), int(buy_start_time[4:6])) \ 467 | <= now <= datetime.time(int(buy_end_time[:2]), int(buy_end_time[2:4]), int(buy_end_time[4:6]))): 468 | return func(*args, **kwargs) 469 | else: 470 | pass 471 | else: 472 | pass 473 | return wrapper 474 | def sell_trader_time(func): 475 | def wrapper(*args, **kwargs): 476 | today = datetime.datetime.now().date().strftime("%Y%m%d") 477 | now = datetime.datetime.now().time() 478 | if C.get_trading_dates('SH', today, today, 1, '1d'): 479 | if (datetime.time(int(sell_start_time[:2]), int(sell_start_time[2:4]), int(sell_start_time[4:6])) \ 480 | <= now <= datetime.time(int(sell_end_time[:2]), int(sell_end_time[2:4]), int(sell_end_time[4:6]))): 481 | return func(*args, **kwargs) 482 | else: 483 | pass 484 | else: 485 | pass 486 | return wrapper 487 | def trade_time(func): 488 | def wrapper(*args, **kwargs): 489 | today = datetime.datetime.now().date().strftime("%Y%m%d") 490 | now = datetime.datetime.now().time() 491 | if C.get_trading_dates('SH', today, today, 1, '1d'): 492 | if (datetime.time(9, 30) <= now <= datetime.time(11, 30)) or \ 493 | (datetime.time(13, 00) <= now <= datetime.time(15, 0, 0)): 494 | return func(*args, **kwargs) 495 | else: 496 | pass 497 | #print('非交易时间,不运行') 498 | else: 499 | pass 500 | #print('非交易日,不运行') 501 | return wrapper 502 | def trade_date(func): 503 | def wrapper(*args, **kwargs): 504 | today = datetime.datetime.now().date().strftime("%Y%m%d") 505 | now = datetime.datetime.now().time() 506 | if C.get_trading_dates('SH', today, today, 1, '1d'): 507 | return func(*args, **kwargs) 508 | else: 509 | pass 510 | return wrapper 511 | # 挂载定时函数 512 | global f0, f1, f2, f_summary 513 | f0 = trade_time(order_canceler) # 定时、按条件撤单程序 514 | C.run_time('f0', "1nSecond", "2022-08-01 09:15:00", "SH") # 每秒检查撤单 515 | C.run_time('buy_prepare', "1d", "2022-08-01 %s:%s:%s"%(buy_prepare_time[:2], buy_prepare_time[2:4], buy_prepare_time[4:6]),\ 516 | "SH") # 买入初始化函数 517 | f1 = buy_trader_time(trader) 518 | C.run_time('f1', "%snSecond"%interval, "2022-08-01 09:15:00", "SH") # 交易员 519 | f_summary = trade_date(summary) 520 | C.run_time('f_summary', "1d", "2022-08-01 %s:%s:%s"%(buy_summary_time[:2], buy_summary_time[2:4], buy_summary_time[4:6]),\ 521 | "SH") # 记录买入数量 522 | C.run_time('sell_prepare', "1d", "2022-08-01 %s:%s:%s"%(sell_prepare_time[:2], sell_prepare_time[2:4], sell_prepare_time[4:6]),\ 523 | "SH") # 买入初始化函数 524 | f2 = sell_trader_time(trader) 525 | C.run_time('f2', "%snSecond"%interval, "2022-08-01 09:15:00", "SH") # 交易员 526 | C.run_time('f_summary', "1d", "2022-08-01 %s:%s:%s"%(sell_summary_time[:2], sell_summary_time[2:4], sell_summary_time[4:6]),\ 527 | "SH") # 记录卖出数量 528 | # 读取图形界面传入的ACCOUNT 529 | global ACCOUNT 530 | ACCOUNT = account if 'account' in globals() else ACCOUNT 531 | 532 | -------------------------------------------------------------------------------- /EzQmt/__init__.py: -------------------------------------------------------------------------------- 1 | from EzQmt import smy -------------------------------------------------------------------------------- /EzQmt/smy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import FreeBack as FB 4 | import os,datetime 5 | 6 | class account(): 7 | # 总结文件根目录,进出资金记录[('yyyy-mm-dd', 进出金额),..],起止日期,业绩基准,转股信息({转债代码:(股票代码,转股价)},是否隐藏具体金额,策略合并 8 | def __init__(self, summary_loc, outcash_list=[], start_date=None, end_date=None, benchmark=None, conv_stk={}, if_hide=False,\ 9 | renamestrat={}, accnum=8888888888, initstratpos=True, ifout=False): 10 | self.summary_loc = summary_loc 11 | self.outcash_list = outcash_list 12 | net_file = sorted([f for f in os.listdir(self.summary_loc) if ('acct' in f)]) 13 | if start_date==None: 14 | self.start_date = net_file[0].split('-')[1].split('.')[0] 15 | else: 16 | self.start_date = pd.to_datetime(start_date).strftime("%Y%m%d") 17 | if end_date==None: 18 | self.end_date = net_file[-1].split('-')[1].split('.')[0] 19 | else: 20 | self.end_date = pd.to_datetime(end_date).strftime("%Y%m%d") 21 | #if type(benchmark)==type(None): 22 | # daterange = [pd.to_datetime(f.split('.')[0].split('-')[1]) for f in net_file] 23 | # self.benchmark = pd.DataFrame({0:0}, index=daterange) 24 | #else: 25 | self.benchmark = benchmark 26 | self.conv_stk = conv_stk 27 | self.if_hide = if_hide 28 | self.renamestrat = renamestrat 29 | self.accnum = accnum 30 | self.initstratpos = initstratpos 31 | self.ifout = ifout 32 | self.get_acct() 33 | self.get_pos() 34 | self.get_deal() 35 | self.cal_stratpos() 36 | self.cal_contri() 37 | # net=现金+证券持仓(包括逆回购) 38 | def get_acct(self): 39 | net_file = sorted([f for f in os.listdir(self.summary_loc) if ('acct' in f) and \ 40 | (f.split('-')[1].split('.')[0]>=self.start_date) and (f.split('-')[1].split('.')[0]<=self.end_date) ]) 41 | net = [] 42 | for f in net_file: 43 | date = f[-12:-4] 44 | onedaynet = pd.read_csv(self.summary_loc+'/'+f, header=None).set_index(0).transpose() 45 | onedaynet['date'] = pd.to_datetime(date) 46 | net.append(onedaynet) 47 | net = pd.concat(net).set_index('date').sort_index() 48 | # 每日收益率 发生资金转入转出时需要对returns进行修正 49 | returns = (net['net']/net['net'].shift()-1).fillna(0) 50 | outcash = pd.Series({pd.to_datetime(i[0]):i[1] for i in self.outcash_list}, index=net.index).fillna(0) 51 | replace_returns = (net['net']/(net['net']+outcash.shift(-1)).shift()-1).loc[outcash!=0].loc[returns.index[0]:] 52 | returns.loc[replace_returns.index] = replace_returns 53 | net['returns'] = returns 54 | net.fillna(0) 55 | self.net = net 56 | # net=现金(包括逆回购)+证券持仓 57 | # 持仓按账号类型显示,而acct按账号全部持仓显示(包含股票、期权、港股通等),对于现金+逆回购之外部分记为unknown 58 | def get_pos(self): 59 | pos_file = sorted([f for f in os.listdir(self.summary_loc) if ('position' in f) and \ 60 | (f.split('-')[1].split('.')[0]>=self.start_date) and (f.split('-')[1].split('.')[0]<=self.end_date)]) 61 | pos = [] 62 | for f in pos_file: 63 | date = f[-12:-4] 64 | onedaypos = pd.read_csv(self.summary_loc+'/'+f) 65 | onedaypos['date'] = pd.to_datetime(date) 66 | pos.append(onedaypos) 67 | pos = pd.concat(pos).set_index(['date', 'code']) 68 | yichang = pos[~(pos['MarketValue']<999999)].index # 新债申购持仓 69 | pos.loc[yichang, 'MarketValue'] = pos.loc[yichang, 'PositionCost'] 70 | # 普通持仓的价格代表收盘价,逆回购代表利率 71 | pos['price'] = pos['MarketValue']/pos['vol'] 72 | # 修正逆回购持仓MarketValue,并且添加至net中新增一列为total_cash 73 | self.nihuigou_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 74 | '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 75 | '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 76 | '204014.SH', '204028.SH', '204091.SH', '204182.SH'] # 深市、沪市逆回购代码 77 | nihuigou = pos[pos.index.get_level_values(1).isin(self.nihuigou_codes)].copy() 78 | nihuigou['MarketValue'] = nihuigou['vol']*100 79 | nihuigou['name'] = '逆回购' 80 | paichunihuigou = pos[~pos.index.get_level_values(1).isin(self.nihuigou_codes)].copy() 81 | pos = pd.concat([nihuigou, paichunihuigou]).sort_index() 82 | # 如果总资产-现金-持仓 大于1,则表明有未知持仓(例如港股通) 如果ifout为False则认为总资产数据错误,进行修改 83 | unknown_value = self.net['net']-self.net['cash']-pos['MarketValue'].groupby('date').sum() 84 | if self.ifout: 85 | if unknown_value.max()>1: 86 | unknown_value = unknown_value.reset_index().rename(columns={0:'MarketValue'}) 87 | unknown_value['code'] = 'unknown' 88 | pos = pd.concat([pos, unknown_value.set_index(['date', 'code'])]).sort_index() 89 | pos.loc[pos.index.get_level_values(1)=='unknown', 'PositionCost'] = 0 90 | pos.loc[pos.index.get_level_values(1)=='unknown', 'name'] = '港股通等持仓' 91 | pos.loc[pos.index.get_level_values(1)=='unknown', 'price'] = 1 92 | pos.loc[pos.index.get_level_values(1)=='unknown', 'vol'] = \ 93 | pos.loc[pos.index.get_level_values(1)=='unknown', 'MarketValue'] 94 | pos.loc[pos.index.get_level_values(1)=='unknown', 'AvailableVol'] = \ 95 | pos.loc[pos.index.get_level_values(1)=='unknown', 'vol'] 96 | pos = pos[pos['vol']>1].copy() 97 | else: 98 | print(unknown_value) 99 | # 将逆回购计为现金,添加总现金字段 100 | self.net['total_cash'] = self.net['cash'].add(\ 101 | pos[pos['name']=='逆回购'].groupby('date')['MarketValue'].sum(), fill_value=0) 102 | self.pos = pos 103 | self.code2name = dict(zip(self.pos.index.get_level_values(1), self.pos['name'])) 104 | # 成交数据 105 | def get_deal(self): 106 | deal_file = sorted([f for f in os.listdir(self.summary_loc) if ('deal' in f) and \ 107 | (f.split('-')[1].split('.')[0]>=self.start_date) and (f.split('-')[1].split('.')[0]<=self.end_date)]) 108 | deal = [] 109 | for f in deal_file: 110 | date = f[-12:-4] 111 | onedaydeal = pd.read_csv(self.summary_loc+'/'+'deal-%s.csv'%date) 112 | deal.append(onedaydeal) 113 | deal = pd.concat(deal) 114 | # 以时间戳为唯一index 115 | deal['date'] = deal.apply(lambda x: pd.to_datetime(str(x['date'])+\ 116 | '%06d'%x['deal_time']), axis=1) 117 | deal = deal.rename(columns={'remark':'strat'}) 118 | deal['strat'] = deal['strat'].fillna('craft') 119 | deal = deal.sort_values(by='date').reset_index()[['date', 'code', 'trade_type', 'price', 'vol', 'amount', 'strat']].sort_values(by='date').copy() 120 | deal['vol'] = deal['vol']*pd.Series(np.where(deal['trade_type']==48, 1, -1), deal.index) 121 | deal['amount'] = deal['amount']*pd.Series(np.where(deal['trade_type']==49, 1, -1), deal.index) # 48为买入,49为卖出。 122 | deal['time'] = deal['date'] + deal.index.map(lambda x: datetime.timedelta(microseconds=x%100)) # 避免时间戳重复 123 | deal = deal.set_index('time').sort_index() 124 | deal['date'] = deal.index.map(lambda x :pd.to_datetime(x.date())) 125 | deal.loc[deal['strat'].isna(), 'strat'] = 'craft' # 未备注标记为craft 126 | # 将转股委托变为成交订单 次日9:15卖出转债,买入股票 127 | #if self.conv_stk != {}: 128 | order_file = sorted([f for f in os.listdir(self.summary_loc) if ('order' in f) and \ 129 | (f.split('-')[1].split('.')[0]>=self.start_date) and (f.split('-')[1].split('.')[0]<=self.end_date)]) 130 | order = [] 131 | for f in order_file: 132 | date = f[-12:-4] 133 | onedayorder = pd.read_csv(self.summary_loc+'/'+'order-%s.csv'%date) 134 | order.append(onedayorder) 135 | order = pd.concat(order) 136 | # 48 未报, 49 待报, 50 已报, 51 已报待撤, 52 部成待撤, 53 部撤(剩余已撤单), 54 已撤, 55 部成, 56 已成, 57 废单, 227 未知 137 | conv_order = order[(order['price']==0)&(order['status']==50)].rename(columns={'remark':'strat'}) 138 | if not conv_order.empty: 139 | conv_order['strat'] = 'craft' 140 | conv_order['price'] = conv_order.apply(lambda x: self.pos.loc[str(x['date']), x['code']]['price'], axis=1) # 收盘价结算 141 | conv_order['vol'] = conv_order['sub_vol'] 142 | stk_vol = conv_order['vol']*(100/conv_order['code'].map(lambda x: self.conv_stk[x][1])) 143 | amount = conv_order['price']*conv_order['vol'] 144 | conv_order['amount'] = amount 145 | conv_order['date'] = conv_order['date'].map(lambda x: \ 146 | self.net.index[np.searchsorted(self.net.index, pd.to_datetime(str(x)), 'left')+1]) # 规定转股在T+1日9:15发生 147 | conv_order['time'] = conv_order['date']+datetime.timedelta(hours=9, minutes=15) 148 | conv_order = conv_order[['time', 'date', 'code', 'trade_type', 'price', 'vol', 'amount', 'strat']] 149 | # 转股委托转化为卖出转债同时买入正股的成交订单 150 | conv_order_sell = conv_order.copy() 151 | conv_order_sell['vol'] = -conv_order_sell['vol'] 152 | conv_order_buy = conv_order.copy() 153 | conv_order_buy['trade_type'] = 48 154 | conv_order_buy['vol'] = stk_vol.astype('int') 155 | conv_order_buy['amount'] = conv_order['code'].map(lambda x: self.conv_stk[x][1])*(stk_vol-stk_vol.astype('int'))\ 156 | - amount # 小数股直接转为现金 157 | conv_order_buy['price'] = -conv_order_buy['amount']/conv_order_buy['vol'] 158 | conv_order_buy['code'] = conv_order_buy['code'].map(lambda x: self.conv_stk[x][0]) 159 | self.conv_deal = pd.concat([conv_order_sell, conv_order_buy]).set_index('time') 160 | deal = pd.concat([deal, self.conv_deal]) 161 | # 逆回购持仓转化为成交订单 162 | # 申购转为当日15:30现金换券,次日8:00券换现金 163 | nihuigou_pos = self.pos[self.pos['name']=='逆回购'].reset_index() 164 | nihuigou_pos['strat'] = '逆回购' 165 | nihuigou_pos['trade_type'] = 48 166 | nihuigou_pos['amount'] = -nihuigou_pos['MarketValue'] 167 | nihuigou_pos = nihuigou_pos[['date', 'code', 'trade_type', 'price', 'vol', 'amount', 'strat']] 168 | nihuigou_buy = nihuigou_pos.copy() 169 | nihuigou_buy['time'] = nihuigou_buy['date'] + datetime.timedelta(hours=15, minutes=30) 170 | nihuigou_sell = nihuigou_pos[nihuigou_pos['date']=len(self.net.index) \ 176 | else self.net.index[np.searchsorted(self.net.index, x, 'left')+1]) 177 | nihuigou_sell = nihuigou_sell.dropna(subset=['date']) 178 | nihuigou_sell['time'] = nihuigou_sell['date'] + datetime.timedelta(hours=8) # T+1日8:00,逆回购变为现金 179 | self.nihuigou_deal = pd.concat([nihuigou_buy.set_index('time'), nihuigou_sell.set_index('time')]) 180 | deal = pd.concat([deal, self.nihuigou_deal]) 181 | # 账户总资产和订单结算结果差异转化为申购新股新债或账号外持仓 182 | # 申购(**发债)转为当日16:00买入,代码转变(**发债改为**)当日9:00卖出 183 | unstackpos = self.pos['vol'].unstack().fillna(0) 184 | deltapos = (unstackpos-unstackpos.shift()).stack() 185 | deltapos = deltapos[deltapos!=0].copy() # 持仓变化 186 | net_deal = deal.groupby(['date', 'code'])['vol'].sum() 187 | net_deal = net_deal.loc[deltapos.index[0][0]:] 188 | net_deal = net_deal[net_deal!=0].copy() # 成交 189 | subscrible_deal = deltapos.sub(net_deal, fill_value=0) 190 | subscrible_deal = subscrible_deal[subscrible_deal!=0] 191 | subscrible_deal = subscrible_deal.reset_index().rename(columns={0:'vol'}) 192 | if not subscrible_deal.empty: 193 | if (set(subscrible_deal['code'])-set(['unknown']))!=set(): 194 | print('当前交割记录和持仓不匹配标的:', set(subscrible_deal['code'])-set(['unknown']), \ 195 | '按申购新股/新债处理') 196 | try: 197 | subscrible_deal['price'] = subscrible_deal['code'].map(lambda x: self.pos.loc[:, x, :]['price'].iloc[0]) 198 | except: 199 | print('请检查可转债转股信息是否添加', subscrible_deal['code'].unique()) 200 | return 201 | subscrible_deal['trade_type'] = subscrible_deal['vol'].map(lambda x: 48 if x>0 else 49) 202 | subscrible_deal['time'] = subscrible_deal['date'].map(lambda x: x + datetime.timedelta(hours=9)) 203 | subscrible_deal['amount'] = -subscrible_deal['vol']*subscrible_deal['price'] 204 | subscrible_deal['time'] = subscrible_deal['date'] + \ 205 | subscrible_deal['vol'].map(lambda x: datetime.timedelta(hours=16 if x>0 else 9)) 206 | subscrible_deal['strat'] = 'craft' 207 | self.subscrible_deal = subscrible_deal.set_index('time') 208 | # 主账号外持仓,如果有的话需要 209 | out_deal = self.subscrible_deal[self.subscrible_deal['code']=='unknown'].copy() 210 | if not out_deal.empty: 211 | print('存在Stock账号外持仓(港股通等)') 212 | self.subscrible_deal = self.subscrible_deal[self.subscrible_deal['code']!='unknown'].copy() 213 | out_deal['strat'] = '港股通等持仓' 214 | if self.net.index[0] in self.pos[self.pos['name']=='港股通等持仓'].index.get_level_values(0): 215 | init_out_deal = self.pos[self.pos['name']=='港股通等持仓'].loc[[self.net.index[0]], :].reset_index() 216 | init_out_deal['time'] = init_out_deal['date']+datetime.timedelta(hours=16) 217 | init_out_deal['trade_type'] = 48 218 | init_out_deal['amount'] = -init_out_deal['MarketValue'] 219 | init_out_deal['strat'] = '港股通等持仓' 220 | init_out_deal = init_out_deal.set_index('time')\ 221 | [['date', 'code', 'trade_type', 'price', 'vol', 'amount', 'strat']] 222 | out_deal = pd.concat([init_out_deal, out_deal]) 223 | deal = pd.concat([deal, out_deal]) 224 | # 全部订单 225 | deal = pd.concat([deal, self.subscrible_deal]) 226 | self.deal = deal.sort_index() 227 | # 策略改名 228 | self.deal['strat'] = self.deal['strat'].map(lambda x: \ 229 | x if x not in self.renamestrat.keys() else self.renamestrat[x]) 230 | self.strats = list(self.deal['strat'].unique()) # 运行中策略 231 | print('当前运行中策略:%s'%(','.join(self.strats))) 232 | # 订单数据(包含转股信息) 233 | def get_order(self): 234 | pass 235 | def cal_stratpos(self): 236 | stratpos = [] 237 | for date in self.net.index: 238 | # 当日成交 239 | deal = self.deal[self.deal['date']==date].copy() 240 | # 确定首日持仓 241 | if date==self.net.index[0]: 242 | if self.initstratpos: 243 | initstratpos = pd.read_csv(self.summary_loc+'init_stratpos.csv', header=None\ 244 | ).rename(columns={0:'strat', 1:'code', 2:'vol'}).ffill() # 所属策略空缺按上一行补齐 245 | initcodevol = initstratpos.groupby('code')['vol'].sum() 246 | posinitvol = self.pos.loc[self.pos.index[0][0]].groupby('code')['vol'].sum() 247 | deltavol = posinitvol.sub(initcodevol, fill_value=0) 248 | deltavol = deltavol[deltavol!=0] 249 | deltavol = deltavol.reset_index() 250 | deltavol['strat'] = 'craft' # 未标记持仓默认为craft 251 | initstratpos = pd.concat([initstratpos, deltavol]) 252 | if deltavol['vol'].min()<0: 253 | print('error 策略持仓大于总持仓') 254 | return 255 | # 首日策略持仓 256 | todaystratpos = initstratpos.set_index(['strat', 'code'])['vol'] 257 | else: 258 | todaystratpos = self.pos.loc[date]['vol'] 259 | net_deal = deal.groupby('code')['vol'].sum() 260 | sell_deal = deal[deal['vol']<0].groupby('code')['vol'].sum() 261 | buy_deal = deal[deal['vol']>0].groupby(['strat', 'code'])['vol'].sum() 262 | # 推测前日持仓 263 | prestratpos = todaystratpos.add(-net_deal, fill_value=0) 264 | prestratpos = prestratpos[prestratpos!=0].copy() 265 | # 当日先卖出后持仓全为craft 266 | todaystratpos0 = prestratpos.add(sell_deal, fill_value=0) 267 | todaystratpos0 = todaystratpos0.reset_index() 268 | todaystratpos0['strat'] = 'craft' 269 | todaystratpos0 = todaystratpos0.set_index(['strat', 'code'])['vol'] 270 | todaystratpos0 = todaystratpos0[todaystratpos0!=0].copy() 271 | # 首日策略持仓 272 | todaystratpos = todaystratpos0.add(buy_deal, fill_value=0) 273 | else: 274 | prestratpos = pd.read_csv(self.summary_loc+'/stratpos-'+\ 275 | self.net.index[np.searchsorted(self.net.index, date, side='left')-1].strftime("%Y%m%d")+'.csv').\ 276 | set_index(['strat', 'code'])['vol'] 277 | net_deal = deal.groupby(['strat', 'code'])['vol'].sum() 278 | todaystratpos = prestratpos.add(net_deal, fill_value=0) 279 | # 如果有策略的某标的持仓为负, 280 | negpos = todaystratpos[todaystratpos<0].copy() 281 | while not negpos.empty: 282 | #print('%s,策略持仓为负:'%date) 283 | #print(negpos) 284 | todaystratpos = todaystratpos[todaystratpos>0].copy() 285 | # 归为持仓该标的数量最多的策略 286 | code2strat = todaystratpos.sort_values(ascending=False).reset_index().drop_duplicates(subset=['code']) 287 | code2strat = dict(zip(code2strat['code'], code2strat['strat'])) 288 | # 忽略负持仓所属策略 289 | negpos = negpos.groupby('code').sum().reset_index() 290 | negpos['strat'] = negpos['code'].map(lambda x: 'craft' if x not in code2strat.keys() else code2strat[x]) 291 | #print('该部分持仓归为:') 292 | #print(todaystratpos) 293 | todaystratpos = todaystratpos.add(negpos.set_index(['strat', 'code'])['vol'], fill_value=0) 294 | negpos = todaystratpos[todaystratpos<0].copy() 295 | todaystratpos = todaystratpos[todaystratpos!=0] 296 | todaystratpos.to_csv(self.summary_loc+'/stratpos-'+date.strftime("%Y%m%d")+'.csv') 297 | todaystratpos = todaystratpos.reset_index() 298 | todaystratpos['date'] = date 299 | todaystratpos = todaystratpos.set_index(['date', 'strat', 'code']) 300 | stratpos.append(todaystratpos) 301 | stratpos = pd.concat(stratpos) 302 | # 修正pos,逆回购价格改为100 303 | correctpos = self.pos.copy() 304 | correctpos['price'] = correctpos.apply(lambda x: 100 if x['name']=='逆回购' else x['price'], axis=1) 305 | stratpos = stratpos.reset_index().merge(correctpos.reset_index()[['code', 'date', 'price', 'name']], on=['date', 'code']) 306 | stratpos['MarketValue'] = stratpos['price']*stratpos['vol'] 307 | self.stratpos = stratpos.set_index(['date', 'strat', 'code']) 308 | # 策略持仓/成交分仓/净资产/收益率 309 | self.split_strats = {} 310 | for strat in self.strats: 311 | try: 312 | self.split_strats[strat] = [self.stratpos.loc[:, strat, :], self.deal[self.deal['strat']==strat], 0, np.nan] 313 | except: 314 | self.split_strats[strat] = [self.stratpos.loc[[]], self.deal[self.deal['strat']==strat], 0, np.nan] 315 | # 获取所有策略按持仓归因收益 316 | def cal_contri(self): 317 | self.df_contri = {} 318 | self.contri = {} 319 | for strat in self.strats+['all']: 320 | if strat=='all': 321 | pos_ = self.pos 322 | deal_ = self.deal[self.deal['strat']!='港股通等持仓'] # 计算收益时忽略港股通等持仓订单 323 | else: 324 | pos_ = self.split_strats[strat][0] 325 | #if strat=='港股通等持仓': 326 | # deal_ = self.deal[[]] 327 | #else: 328 | deal_ = self.split_strats[strat][1] 329 | all_tradedates = list(pos_.index.get_level_values(0))+list(deal_['date'].values) 330 | # T日相对T-1日策略持仓市值变动 331 | if not pos_.empty: # 有策略始终无隔夜持仓 332 | pos_unstack = pos_[['vol', 'MarketValue']].unstack().fillna(0).\ 333 | reindex(self.net.index).loc[min(all_tradedates):max(all_tradedates)].fillna(0) 334 | pos_unstack = (pos_unstack-pos_unstack.shift().fillna(0)) 335 | pos_delta = pos_unstack['MarketValue'] 336 | pos_delta_vol = pos_unstack['vol'] 337 | # T日交易净流水 338 | deal_ = deal_.groupby(['date', 'code'])[['vol', 'amount']].sum().unstack().fillna(0) 339 | deal_net = deal_['amount'] 340 | deal_net_vol = deal_['vol'] 341 | # T日交易净张数和持仓变化不相等的部分是策略订单操作了不属于自己策略的持仓造成的,需将此部分划转造成的资金变动归还 342 | # 负值表示该策略卖出了其他策略持仓,正值表示其他策略卖出了该策略持仓 343 | if not pos_.empty: 344 | lost = deal_net_vol.sub(pos_delta_vol, fill_value=0) 345 | else: 346 | lost = deal_net_vol 347 | lost = lost.stack()[lost.stack()!=0] 348 | amountlend2others = (lost*self.pos['price'].unstack().shift().stack()).dropna() # 对于前日有持仓标的按照前收价其他策略划转市值,按前收计价 349 | VWAP = self.deal.copy() 350 | VWAP['vol'] = abs(VWAP['vol']) 351 | VWAP['amount'] = abs(VWAP['amount']) 352 | VWAP = VWAP.groupby(['date', 'code'])[['vol', 'amount']].sum() 353 | VWAP = VWAP['amount']/VWAP['vol'] 354 | amountlend2othersT0 = lost.drop(amountlend2others.index) 355 | amountlend2othersT0 = (amountlend2othersT0*VWAP).dropna() # 前日策略无持仓标的按照成交均价计价 356 | deal_otherstrats = pd.concat([amountlend2others, amountlend2othersT0]).sort_index() 357 | deal_otherstrats = deal_otherstrats.unstack().fillna(0) 358 | if 'unknown' in deal_otherstrats.columns: 359 | deal_otherstrats.loc[:, 'unknown'] = 0 360 | # 收益分解 361 | if strat=='港股通等持仓': 362 | contri_strat = pos_delta.fillna(0) 363 | elif not pos_.empty: 364 | contri_strat = pos_delta.add(deal_net, fill_value=0).fillna(0).add(deal_otherstrats, fill_value=0) 365 | else: 366 | contri_strat = deal_net.fillna(0).add(deal_otherstrats, fill_value=0) 367 | if self.net.index[0] in contri_strat.index: 368 | contri_strat.loc[self.net.index[0], :] = 0 # 如果策略从开始日期开始则设置当天策略盈亏为0 369 | self.df_contri[strat] = contri_strat 370 | # 标的总盈亏 371 | pnl_total = contri_strat.sum() 372 | pnl_total.name = '总盈亏' 373 | # 简称/平均仓位/持仓金额 374 | #pos_name = pd.Series(dict(zip(pos_.index.get_level_values(1), pos_.values))) 375 | pos_name = pd.Series(pnl_total.index.map(lambda x: np.nan if x not in self.code2name.keys() else self.code2name[x]), index=pnl_total.index) 376 | pos_name.name = '标的简称' 377 | pos_ratio = 100*(pos_['MarketValue']/pos_['MarketValue'].groupby('date').sum()).groupby('code').mean() 378 | pos_ratio.name = '平均仓位(%)' 379 | pos_amount = pos_['MarketValue'].groupby('code').mean() 380 | pos_amount.name = '平均持仓金额(元)' 381 | self.contri[strat] = pd.concat([pos_name, pos_amount, pos_ratio, pnl_total], axis=1).sort_values(by='总盈亏') 382 | # 计算交易滑点(按照开盘价/收盘价/开盘收盘平均价/VWAP四种基准),需提供分钟线数据。 383 | def cal_deal_comm(self, min_data, deal0): 384 | deal0['date'] = deal0['date'] + deal0.index.map(lambda x: \ 385 | datetime.timedelta(hours=15, minutes=0) if ((x.hour==15)&(x.minute==0))|((x.hour==14)&(x.minute==59)) \ 386 | else datetime.timedelta(hours=x.hour, minutes=x.minute+1)) 387 | deal0 = deal0.set_index(['date', 'code']) 388 | # 正为买入,负为卖出 389 | bought_deal = deal0[deal0['trade_type']==48].copy() 390 | sold_deal = deal0[deal0['trade_type']==49].copy() 391 | bought_vol = bought_deal.groupby(['date', 'code'])['vol'].sum() 392 | bought_vol.name = 'myvol' 393 | bought_amount = -bought_deal.groupby(['date', 'code'])['amount'].sum() 394 | bought_amount.name = 'myamount' 395 | bought = pd.concat([bought_vol, bought_amount], axis=1) 396 | bought['type'] = 'buy' 397 | bought['price'] = bought['myamount']/bought['myvol'] 398 | bought = bought.join(min_data).dropna() 399 | sold_vol = -sold_deal.groupby(['date', 'code'])['vol'].sum() 400 | sold_vol.name = 'myvol' 401 | sold_amount = sold_deal.groupby(['date', 'code'])['amount'].sum() 402 | sold_amount.name = 'myamount' 403 | sold = pd.concat([sold_vol, sold_amount], axis=1) 404 | sold['type'] = 'sell' 405 | sold['price'] = sold['myamount']/sold['myvol'] 406 | sold = sold.join(min_data).dropna() 407 | # 滑点 408 | bought['comm_close'] = 1e4*(bought['price']-bought['close'])/bought['close'] 409 | sold['comm_close'] = 1e4*(sold['close']-sold['price'])/sold['close'] 410 | bought['comm_open'] = 1e4*(bought['price']-bought['open'])/bought['open'] 411 | sold['comm_open'] = 1e4*(sold['open']-sold['price'])/sold['open'] 412 | # open close 平均价 413 | bought['comm_mco'] = 1e4*(bought['price']-(bought['close']+bought['open'])/2)/((bought['close']+bought['open'])/2) 414 | sold['comm_mco'] = 1e4*((sold['close']+sold['open'])/2-sold['price'])/((sold['close']+sold['open'])/2) 415 | bought['comm_avg'] = 1e4*(bought['price']-bought['avg'])/bought['avg'] 416 | sold['comm_avg'] = 1e4*(sold['avg']-sold['price'])/sold['avg'] 417 | deal_comm = pd.concat([bought, sold]).sort_index() 418 | return deal_comm 419 | # 净值 default 默认值, None 0基准 420 | def pnl(self, strat='all', benchmark='default'): 421 | start_date = self.df_contri[strat].index[0] 422 | end_date = self.df_contri[strat].index[-1] 423 | if type(benchmark)==str: 424 | if benchmark=='default': 425 | benchmark = self.benchmark 426 | if strat=='all': 427 | equity = self.net['net'] 428 | plot_returns = self.net['returns'] 429 | else: 430 | equity = self.split_strats[strat][0].groupby('date')['MarketValue'].sum().\ 431 | reindex(self.df_contri[strat].index).ffill() 432 | plot_returns = self.df_contri[strat].sum(axis=1)/equity 433 | self.split_strats[strat][2] = equity 434 | self.split_strats[strat][3] = plot_returns 435 | self.post0 = FB.post.ReturnsPost(plot_returns, benchmark) 436 | plt, fig, ax = FB.display.matplot() 437 | lb = [] 438 | # 策略走势 439 | l, = ax.plot((plot_returns+1).cumprod(), c='C3', linewidth=2) 440 | lb.append(l) 441 | # 如果基准是0就不绘制了 442 | if not type(benchmark)==type(None): 443 | # 基准走势 444 | colors = ['C0', 'C1', 'C2', 'C4', 'C5', 'C6', 'C7', 'C8'] 445 | for i in range(len(benchmark.columns)): 446 | benchmark_returns = benchmark.iloc[:, i].loc[self.net.index[0]:self.net.index[-1]] 447 | l, = ax.plot((benchmark_returns+1).cumprod(), colors[i]) 448 | lb.append(l) 449 | # 策略净资产 450 | ax2 = ax.twinx() 451 | l, = ax2.plot(equity/1e4, c='C3', alpha=0.5, ls='--') 452 | lb.append(l) 453 | plt.legend(lb, [('组合' if strat=='all' else '策略') + '收益',]+ \ 454 | ([] if type(benchmark)==type(None) else list(benchmark.columns))+\ 455 | [('组合总' if strat=='all' else '策略') + '资产(右)'] , bbox_to_anchor=(0.9, -0.2), ncol=3) 456 | ax.set_title('资金账号:%s****%s'%(str(self.accnum)[:4], str(self.accnum)[-4:]) if strat=='all' else\ 457 | '策略:%s'%strat) 458 | ax2.set_ylabel('(万元)') 459 | if self.if_hide: 460 | ax2.set_yticks(ax2.get_yticks(), ['*.*' for i in ax2.get_yticks()]) 461 | ax.set_ylabel('净值走势') 462 | ax.set_xlim(start_date, end_date) 463 | if strat=='all': 464 | ax.text(0, -0.25, '1、右轴为组合总资产', fontsize=10, transform=ax.transAxes) 465 | else: 466 | ax.text(0, -0.25, '1、右轴为策略总资产\n2、无隔夜持仓策略总资产为0', fontsize=10, transform=ax.transAxes) 467 | plt.gcf().autofmt_xdate() 468 | FB.post.check_output() 469 | plt.savefig('output/%s.png'%self.accnum, bbox_inches='tight') 470 | plt.show() 471 | # 月度收益 472 | def pnl_monthly(self, strat='all'): 473 | if strat=='all': 474 | plot_returns = self.net['returns'] 475 | else: 476 | equity = self.split_strats[strat][0].groupby('date')['MarketValue'].sum().\ 477 | reindex(self.df_contri[strat].index).ffill() 478 | plot_returns = self.df_contri[strat].sum(axis=1)/equity 479 | self.post0 = FB.post.ReturnsPost(plot_returns, self.benchmark, show=False) 480 | self.post0.pnl_monthly() 481 | # 多策略仓位 482 | def displaystrats_pos(self, ratio=True): 483 | split_strats_pos = {} 484 | for strat in self.split_strats.keys(): 485 | split_strats_pos[strat] = self.split_strats[strat][0].groupby('date')['MarketValue'].sum() 486 | split_strats_pos = pd.DataFrame(split_strats_pos).fillna(0) 487 | split_strats_pos['现金'] = self.net['cash'] 488 | 489 | plt, fig, ax = FB.display.matplot() 490 | if ratio: 491 | split_strats_pos = 100*split_strats_pos.div(split_strats_pos.sum(axis=1), axis=0) 492 | #split_strats_pos = split_strats_pos.drop(columns='现金') 493 | ax.stackplot(split_strats_pos.index, split_strats_pos.values.T, \ 494 | labels=split_strats_pos.columns, alpha=0.8) 495 | if (~ratio) & self.if_hide: 496 | ax.set_yticks(ax.get_yticks(), ['*.*' for i in ax.get_yticks()]) 497 | ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3) 498 | if ratio: 499 | ax.set_title('各策略及现金仓位') 500 | ax.set_ylabel('(%)') 501 | else: 502 | ax.set_title('各策略及现金市值') 503 | ax.set_ylabel('(元)') 504 | FB.post.check_output() 505 | plt.savefig('output/%s_strats_pos.png'%self.accnum, bbox_inches='tight') 506 | plt.show() 507 | # 多策略盈亏 508 | def displaystrats_pnl(self, ratio=True): 509 | plt, fig, ax = FB.display.matplot() 510 | df_net = [] 511 | for stratname in self.strats: 512 | if ratio: 513 | net = (1+self.df_contri[stratname].sum(axis=1)/\ 514 | self.split_strats[stratname][0].groupby('date')['MarketValue'].sum().\ 515 | reindex(self.df_contri[stratname].index).ffill()).cumprod() 516 | else: 517 | net = self.df_contri[stratname].sum(axis=1).cumsum() 518 | net.name = stratname 519 | df_net.append(net) 520 | df_net = pd.concat(df_net, axis=1) 521 | for stratname in df_net.columns: 522 | ax.plot(df_net[stratname].dropna(), label=stratname) 523 | if ratio: 524 | ax.set_ylabel('策略归一化净值') 525 | else: 526 | ax.set_ylabel('(元)') 527 | if (~ratio) & self.if_hide: 528 | ax.set_yticks(ax.get_yticks(), ['*.*' for i in ax.get_yticks()]) 529 | ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3) 530 | FB.post.check_output() 531 | plt.savefig('output/%s_strats_pnl.png'%self.accnum, bbox_inches='tight') 532 | plt.show() 533 | -------------------------------------------------------------------------------- /NormFunc.py: -------------------------------------------------------------------------------- 1 | ####################################################################################################### 2 | ############################# 常用函数模块 ########################################### 3 | ####################################################################################################### 4 | 5 | 6 | 7 | ########################################### 日常io运行 ################################################### 8 | 9 | # log 函数 10 | def log(*txt): 11 | try: 12 | f = open(logfile,'a+', encoding='gbk') 13 | f.write('%s'%datetime.datetime.now()+' '*6) # 时间戳 14 | if type(txt[0])==pd.Series: 15 | f.write('name: %s\n'%txt[0].name) 16 | for i,v in txt[0].items(): 17 | f.write(' '*32+str(i)+', '+str(v)+'\n') 18 | elif type(txt[0])==pd.DataFrame: 19 | f.write(' '.join([str(i) for i in txt[0].columns])+'\n') 20 | for i,r in txt[0].iterrows(): 21 | f.write(' '*29+str(i)+': ') 22 | f.write(', '.join([str(j) for j in r.values])+'\n') 23 | else: 24 | write_str = ('\n'+' '*32).join([str(i) for i in txt]) 25 | f.write('%s\n' %write_str) 26 | f.close() 27 | except PermissionError as e: 28 | print(f"Error: {e}. You don't have permission to access the specified file.") 29 | 30 | ########################################### 行情数据 ################################################### 31 | 32 | # 获取行情快照数据 DataFrame, index:code 33 | SHmul = 10 34 | SZmul = 10 35 | def get_snapshot(C, code_list): 36 | # 获取标的快照数据 37 | df = C.get_full_tick(code_list) 38 | df = pd.DataFrame.from_dict(df, dtype='float').T.reset_index().rename(columns={'index': 'code'}) 39 | # 盘口 40 | bidPrice_columns = ['bidp1','bidp2','bidp3','bidp4','bidp5'] 41 | askPrice_columns = ['askp1','askp2','askp3','askp4','askp5'] 42 | df[bidPrice_columns] = df['bidPrice'].apply(pd.Series, index=bidPrice_columns) 43 | df[askPrice_columns] = df['askPrice'].apply(pd.Series, index=askPrice_columns) 44 | bidVol_columns = ['bidv1','bidv2','bidv3','bidv4','bidv5'] 45 | askVol_columns = ['askv1','askv2','askv3','askv4','askv5'] 46 | df[bidVol_columns] = df['bidVol'].apply(pd.Series, index=bidVol_columns) 47 | df[askVol_columns] = df['askVol'].apply(pd.Series, index=askVol_columns) 48 | # 中间价 49 | df['mid'] = (df['bidp1'] + df['askp1'])/2 50 | # 涨跌停则bid/askprice为0 51 | df.loc[(df['bidp1'] == 0) | (df['askp1'] == 0),'mid'] = df['bidp1'] + df['askp1'] # 涨跌停修正 52 | ## 展示列 最新价,当日成交额、成交量(手)、最高价、最低价、开盘价 53 | # 盘口 askp\askv*/bid* 买卖5档, 昨收 54 | ## 中间价 askp\askv*/bid* 买卖5档,需要使用券商行情 55 | display_columns = ['code', 'lastPrice', 'amount', 'volume', 'high', 'low', 'open', 'lastClose',\ 56 | 'mid', 'askp1', 'askp2', 'askp3', 'askp4', 'askp5', \ 57 | 'bidp1', 'bidp2', 'bidp3', 'bidp4', 'bidp5', \ 58 | 'askv1', 'askv2', 'askv3', 'askv4', 'askv5',\ 59 | 'bidv1', 'bidv2', 'bidv3', 'bidv4', 'bidv5'] 60 | df = df[display_columns].rename(columns={'volume':'vol'}) 61 | df = df.set_index('code') 62 | # 有时,沪市转债单位是手,沪市需要乘一个沪市转化因子 63 | df['vol'] = df['vol']*df.index.map(lambda x: SHmul if 'SH' in x else SZmul if 'SZ' in x else 1) 64 | return df 65 | 66 | ########################################### 账户状态 ################################################### 67 | 68 | # 获取账户状态 净值,现金 69 | def get_account(): 70 | acct_info = get_trade_detail_data(ACCOUNT, account_type, 'account')[0] 71 | return {'net':acct_info.m_dBalance, 'cash':acct_info.m_dAvailable} 72 | # 获取持仓数据 DataFrame index:code, cash 如果没有持仓返回空表(但是有columns) 73 | def get_pos(): 74 | position_to_dict = lambda pos: { 75 | 'code': pos.m_strInstrumentID + '.' + pos.m_strExchangeID, # 证券代码 000001.SZ 76 | 'name': pos.m_strInstrumentName, # 证券名称 77 | 'vol': pos.m_nVolume, # 当前拥股,持仓量 78 | 'AvailableVol': pos.m_nCanUseVolume, # 可用余额,可用持仓,期货不用这个字段,股票的可用数量 79 | 'MarketValue': pos.m_dMarketValue, # 市值,合约价值 80 | 'PositionCost': pos.m_dPositionCost, # 持仓成本, 81 | } 82 | position_info = get_trade_detail_data(ACCOUNT, account_type, 'position') 83 | pos = pd.DataFrame(list(map(position_to_dict, position_info))) 84 | if pos.empty: 85 | return pd.DataFrame(columns=['name', 'vol', 'AvailabelVol', 'MarketValue', 'PositionCost']) 86 | pos = pos.set_index('code') 87 | extract_names = ['新标准券', '国标准券'] 88 | pos = pos[(pos['vol']!=0)&(~pos['name'].isin(extract_names))].copy() # 已清仓不看,去掉逆回购重复输出 89 | return pos 90 | # 忽略逆回购订单、交割单 91 | status_extract_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 92 | '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 93 | '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 94 | '204014.SH', '204028.SH', '204091.SH', '204182.SH'] 95 | # 获取订单状态 当日没有订单返回空表(但是有columns) 当天订单 96 | def get_order(): 97 | order_info = get_trade_detail_data(ACCOUNT, account_type, 'ORDER') 98 | order_to_dict = lambda o:{ 99 | 'id':o.m_strOrderSysID, 100 | 'date': o.m_strInsertDate, 101 | 'code': o.m_strInstrumentID+'.'+o.m_strExchangeID, 102 | 'sub_time': o.m_strInsertTime, # 例如 str:095620 103 | 'trade_type': o.m_nOffsetFlag, # 48 买入/开仓;49 卖出/平仓 104 | 'price': o.m_dLimitPrice, # 挂单价 105 | 'sub_vol': o.m_nVolumeTotalOriginal, 106 | 'dealt_vol': o.m_nVolumeTraded, 107 | 'remain_vol': o.m_nVolumeTotal, 108 | # 48 未报, 49 待报, 50 已报, 51 已报待撤,52 部成待撤, 53 部撤(部成撤单), 109 | # 54 已撤, 55 部成, 56 已成, 57 废单(算法单执行完毕之后为废单), 86 已确认, 255 未知 110 | 'status':o.m_nOrderStatus, 111 | 'frozen':o.m_dFrozenMargin+o.m_dFrozenCommission, # 冻结金额/保证金+手续费 112 | 'remark':o.m_strRemark # 订单备注 113 | } 114 | order = pd.DataFrame(list(map(order_to_dict, order_info))) 115 | if order.empty: 116 | return pd.DataFrame(columns=['id', 'date', 'code', 'sub_time', 'trade_type',\ 117 | 'price', 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']) 118 | order = order[(order['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 119 | (~order['code'].isin(status_extract_codes))].copy() 120 | order = order.set_index('id') 121 | return order[['date', 'code', 'sub_time', 'trade_type', 'price',\ 122 | 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']] 123 | # 获取成交数据 124 | def get_deal(): 125 | deal_info = get_trade_detail_data(ACCOUNT, account_type, 'DEAL') 126 | deal_to_dict = lambda d:{ 127 | 'order_id':d.m_nRef, # 订单编号 128 | 'id':d.m_strOrderSysID, # 合同编号 129 | 'code': d.m_strInstrumentID + '.' + d.m_strExchangeID, 130 | 'date':d.m_strTradeDate, 131 | 'deal_time':d.m_strTradeTime, # 成交时间 132 | # 48 买入/开仓 49卖出/平仓 50 强平 51 平今 52 平昨 53 强减 133 | 'trade_type':d.m_nOffsetFlag, 134 | 'price':d.m_dPrice, 135 | 'vol': d.m_nVolume, 136 | 'amount': d.m_dTradeAmount, 137 | 'remark': d.m_strRemark 138 | } 139 | deal = pd.DataFrame(list(map(deal_to_dict, deal_info))) 140 | if deal.empty: 141 | return pd.DataFrame(columns=['id', 'order_id', 'code', 'date', 'deal_time',\ 142 | 'trade_type', 'price', 'vol', 'amount', 'remark']) 143 | deal = deal[(deal['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 144 | (~deal['code'].isin(status_extract_codes))].copy() 145 | return deal[['id', 'order_id', 'code', 'date', 'deal_time',\ 146 | 'trade_type', 'price', 'vol', 'amount', 'remark']] 147 | 148 | ########################################### 买卖挂单 ################################################### 149 | 150 | # 撤单 超过wait_dur s的订单取消 151 | def cancel_order(C, wait_dur=0.1, stratname=None): 152 | order = get_order() 153 | # 全部可撤订单 154 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 155 | # 属于该策略 156 | if stratname!=None: 157 | order = order[order['remark']==stratname].copy() 158 | if not order.empty: 159 | # 超过等待时间撤单 insert_time 为1900年 160 | order['sub_time'] = order['sub_time'].map(lambda x: datetime.datetime.strptime(x, "%H%M%S")) 161 | order = order[order['sub_time'].map(lambda x: (datetime.datetime.now()-x).seconds>wait_dur)] 162 | for orderid in order.index: 163 | cancel(orderid, ACCOUNT, account_type, C) 164 | # 撤单 属于策略strat的挂单价超过最新价(1+r)或低于最新价(1-r)的订单取消 165 | def cancel_order_price(C, r, stratname=None): 166 | order = get_order() 167 | # 全部可撤订单 168 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 169 | # 属于该策略 170 | if stratname!=None: 171 | order = order[order['remark']==stratname].copy() 172 | if not order.empty: 173 | # 最新价格 174 | codes = list(set(order['code'])) 175 | snapshot = get_snapshot(C, codes) 176 | lastPrice = snapshot[['lastPrice', 'lastClose']].apply(lambda x: \ 177 | x['lastPrice'] if x['lastPrice']!=0 else x['lastClose'], axis=1) 178 | lastPrice = order['code'].map(lambda x: lastPrice[x]) 179 | if not order.empty: 180 | delta = abs((order['price']-lastPrice)/lastPrice) 181 | delta = delta[delta>r] 182 | for orderid in delta.index: 183 | cancel(orderid, ACCOUNT, account_type, C) 184 | #strategy_name = 'craft' 185 | #multiples = 10 186 | #卖出 187 | def sell(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 188 | vol = int((vol//multiples)*multiples) 189 | if vol==0: 190 | print('too less vol to sub') 191 | return 192 | # 卖出,单标的,账号, 代码,限价单,价格,量,策略名,立即触发下单,备注 193 | if account_type=='STOCK': 194 | passorder(24, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 195 | elif account_type=='CREDIT': 196 | passorder(34, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 197 | #买入 198 | def buy(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 199 | vol = int((vol//multiples)*multiples) 200 | if vol==0: 201 | print('too less vol to sub') 202 | return 203 | if account_type=='STOCK': 204 | passorder(23, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 205 | elif account_type=='CREDIT': 206 | passorder(33, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 207 | 208 | ########################################### 其他 ################################################### 209 | 210 | # 存储全局变量 211 | class a(): 212 | pass 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | ## 成交情况 231 | #def get_dealt(): 232 | # order = get_order().reset_index() 233 | # # 所有已撤、部成、已成订单 234 | # dealt_total = order[order['status'].map(lambda x: x in [53, 54, 55, 56])].copy() 235 | # # 买入\卖出 236 | # bought_vol = dealt_total[dealt_total['trade_type']==48][['dealt_vol', 'code']].groupby('code').sum() 237 | # sold_vol = -dealt_total[dealt_total['trade_type']==49][['dealt_vol', 'code']].groupby('code').sum() 238 | # # 完成量 239 | # dealt_vol = pd.concat([bought_vol['dealt_vol'], sold_vol['dealt_vol']]) 240 | # dealt_vol = dealt_vol[abs(dealt_vol).sort_values(ascending=False).index] 241 | # extract_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 242 | # '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 243 | # '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 244 | # '204014.SH', '204028.SH', '204091.SH', '204182.SH'] # 深市、沪市逆回购代码 245 | # dealt_vol = dealt_vol[~dealt_vol['code'].isin(extract_codes)].copy() 246 | # return dealt_vol 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QMT自动交易及监控脚本 2 | 3 | # 快速开始 4 | 5 | ## 账户分析(需配置Summary.py导出策略运行文件) 6 | 7 | 根据持仓/交割单备注,分析策略持仓,各策略盈亏,分标的盈亏情况。 8 | 9 | ### 初始化配置 10 | 11 | import EzQmt as qmt 12 | 13 | ''' 14 | summary_loc: 策略运行文件目录, outcash_list: 外部转入转出资金情况(格式:[('20250124', -10000), ]),start_date/end_date: 开始/结束时间,benchmark: 业绩比较基准 15 | conv_stk: 转债转股条款(格式:{转债代码:(股票代码,转股价), ...}),if_hide: 是否隐藏具体金额 16 | renamestrat: 策略合并({订单备注1:合并后策略名称a,...}), accnum: 资金账号 17 | ''' 18 | 19 | acct0 = qmt.smy.account(summary_loc, outcash_list=[], start_date=None, end_date=None, benchmark=None, 20 | conv_stk={}, if_hide=True, renamestrat={}, accnum='') 21 | 22 | ### 总账户 23 | 24 | ''' 25 | 总组合净值,月度收益,收益的标的贡献 26 | ''' 27 | 28 | acct0.pnl() 29 | 30 | ![image](https://github.com/user-attachments/assets/f044754e-8e49-4145-8e16-c9f683650a2f) 31 | 32 | ![image](https://github.com/user-attachments/assets/5377d28a-f8be-4584-8677-fa6d7b5d5761) 33 | 34 | acct0.pnl_monthly() 35 | 36 | ![image](https://github.com/user-attachments/assets/5ee0e60e-8ea2-476d-8079-ba9b2f088155) 37 | 38 | acct0.contri['all'] 39 | 40 | ![image](https://github.com/user-attachments/assets/04c8617a-4990-4e0a-a0cb-a5b2f706db57) 41 | 42 | 43 | ### 分策略 44 | 45 | ''' 46 | 策略仓位,各策略表现 47 | ''' 48 | 49 | acct0.displaystrats_pos() 50 | 51 | acct0.displaystrats_pnl() 52 | 53 | ![image](https://github.com/user-attachments/assets/385a996c-8d64-4ce6-b488-298251ae8d03) 54 | ![image](https://github.com/user-attachments/assets/b25c20d3-9071-4a10-8468-593d1258f9c6) 55 | ''' 56 | 查看具体某一策略 57 | ''' 58 | 59 | strat = '策略0' 60 | 61 | acct0.pnl(strat, benchmark=None) 62 | 63 | acct0.contri[strat] 64 | 65 | ![image](https://github.com/user-attachments/assets/ed40a08e-5a8b-471c-8961-5680fbabba2e) 66 | 67 | ![image](https://github.com/user-attachments/assets/9041d466-2ddd-412f-9461-9d4ef0fc4b02) 68 | 69 | ![image](https://github.com/user-attachments/assets/71897e1e-6c0e-4f7f-b885-1752600a6431) 70 | 71 | ### 交易滑点分析(单边) 72 | ''' 73 | 需提供分钟线数据(开盘集合竞价时间戳为9:30,9:30~9:31时间戳为9:31) 74 | ''' 75 | 76 | deal_comm = acct0.cal_deal_comm(min_data, acct0.deal[acct0.deal['strat']=='策略1'].copy()) 77 | 78 | deal_comm[['comm_close', 'comm_open', 'comm_mco', 'comm_avg']].mean() 79 | 80 | ![image](https://github.com/user-attachments/assets/2675f059-20b8-4efd-8550-ac9ac52d1fdb) 81 | 82 | 83 | ## 策略 84 | ### Rebalance.py 仓位再平衡策略 85 | 86 | 自动拆单、挂撤单,将持仓市值占比调整至目标值。 87 | 88 | 输入为lude(禄得)格式的策略篮子文件,支持阈值调仓。 89 | 90 | 91 | # 安装/配置 92 | 93 | ## python库安装 94 | 95 | pip install FreeBack 96 | 97 | pip install EzQmt 98 | 99 | ## QMT 客户端 配置方法 100 | ![1731993271869](https://github.com/user-attachments/assets/d7852645-305f-4b93-ba9c-87d1a0643e9d) 101 | 102 | 在模型研究界面,使用策略文件中内容替换图示代码框中全部代码,调整代码中自定义参数,新建策略。 103 | ![1731993357372](https://github.com/user-attachments/assets/d7a5f601-cd73-4daa-a150-55ff65418a2f) 104 | ![1731993389345](https://github.com/user-attachments/assets/0481d6f8-5814-4b2a-b9b8-50345dd450f3) 105 | 106 | 在模型交易界面,找到刚刚新建的策略,新建策略交易,选择自己的账号和账号类型,运行。 107 | 108 | # 备注 109 | 110 | 111 | # -------------------- 联系作者 --------------------- 112 | 对于个性化程序交易策略代码需求,可以联系作者。 113 | ![cde0c826807b3836377d0e13cf4bbf4](https://github.com/user-attachments/assets/3954cec9-8d4e-481c-a014-2ec971ab7cb4) 114 | 115 | -------------------------------------------------------------------------------- /Reblance.py: -------------------------------------------------------------------------------- 1 | # encoding:gbk 2 | import datetime, re, time, os, copy 3 | import numpy as np 4 | import pandas as pd 5 | 6 | ####################################################################################################### 7 | ############################# 自定义参数 ########################################### 8 | ####################################################################################################### 9 | 10 | # 1. 每天start_time前1分钟读取strat_file文件,获取目标调仓目标权重; 11 | # 2. start_time计算当前各标的持仓市值与目标市值所需要交易金额,制定交易计划; 12 | # 3. start_time开始交易,根据策略市值和调仓目标权重交易,多卖少买,在end_time前结束交易; 13 | # 4. summary_time总结交易结果(该时点策略市值与持仓权重)。 14 | 15 | # 基本设置 16 | ACCOUNT = '' # 填写您的账号 17 | account_type = 'STOCK' 18 | multiples = 10 # 可转债每手十张 19 | strategy_name = 'rebalancing' # 策略名称 20 | logloc = 'D:/cloud/monitor/QMT/LogRunning/' # 您的日志文件位置 21 | logfile = logloc + ACCOUNT + '-' + strategy_name + '.txt' 22 | 23 | # 策略输入 24 | stratfile_loc = 'D:/cloud/monitor/strat/' # 您的lude篮子文件存储位置 25 | stratfile_name = 'basket' # 按照 strat_file_loc+日期(20240102)-strat_file_name 格式输入策略文件 26 | extract_codes = [] # !!!此篮子外的标的可能被卖出!!! 27 | buy_num = 5 # 此排名内买入 28 | holding_num = 6 # 此排名内不卖出 29 | 30 | # 交易设置 31 | strat_cap = 100e4 # 全部篮子标的目标市值 32 | start_time = '143000' # 开始交易时点 33 | interval = 30 # 每隔interval秒挂单一次 34 | dur_time = 300 # 最长交易时间(秒) 35 | wait_dur = interval*0.8 # 订单等待wait_dur秒后未成交撤单 36 | tolerance = 0.01 # 买卖挂单超价(%) 37 | delta_min = 3000 # 最小挂单金额 38 | delta_max = 30000 # 最大挂单金额 39 | max_upndown = 0.2 # 涨跌幅限制,转债20% 40 | 41 | 42 | prepare_time = datetime.datetime.strptime(start_time, "%H%M%S")\ 43 | + datetime.timedelta(seconds=-60) # 交易前一分钟读取篮子文件 44 | prepare_time = prepare_time.strftime("%H%M%S") 45 | end_time = datetime.datetime.strptime(start_time, "%H%M%S")\ 46 | + datetime.timedelta(seconds=dur_time) 47 | end_time = end_time.strftime("%H%M%S") 48 | summary_time = datetime.datetime.strptime(end_time, "%H%M%S")\ 49 | + datetime.timedelta(seconds=60) # 交易结束后一分钟总结交易结果 50 | summary_time = summary_time.strftime("%H%M%S") 51 | 52 | 53 | 54 | ####################################################################################################### 55 | ############################# 常用函数模块 ########################################### 56 | ####################################################################################################### 57 | 58 | 59 | 60 | ########################################### 日常io运行 ################################################### 61 | 62 | # log 函数 63 | def log(*txt): 64 | try: 65 | f = open(logfile,'a+', encoding='gbk') 66 | f.write('%s'%datetime.datetime.now()+' '*6) # 时间戳 67 | if type(txt[0])==pd.Series: 68 | f.write('name: %s\n'%txt[0].name) 69 | for i,v in txt[0].items(): 70 | f.write(' '*32+str(i)+', '+str(v)+'\n') 71 | elif type(txt[0])==pd.DataFrame: 72 | f.write(' '.join([str(i) for i in txt[0].columns])+'\n') 73 | for i,r in txt[0].iterrows(): 74 | f.write(' '*29+str(i)+': ') 75 | f.write(', '.join([str(j) for j in r.values])+'\n') 76 | else: 77 | write_str = ('\n'+' '*32).join([str(i) for i in txt]) 78 | f.write('%s\n' %write_str) 79 | f.close() 80 | except PermissionError as e: 81 | print(f"Error: {e}. You don't have permission to access the specified file.") 82 | 83 | ########################################### 行情数据 ################################################### 84 | 85 | # 获取行情快照数据 DataFrame, index:code 86 | SHmul = 10 87 | SZmul = 10 88 | def get_snapshot(C, code_list): 89 | # 获取标的快照数据 90 | df = C.get_full_tick(code_list) 91 | df = pd.DataFrame.from_dict(df, dtype='float').T.reset_index().rename(columns={'index': 'code'}) 92 | # 盘口 93 | bidPrice_columns = ['bidp1','bidp2','bidp3','bidp4','bidp5'] 94 | askPrice_columns = ['askp1','askp2','askp3','askp4','askp5'] 95 | df[bidPrice_columns] = df['bidPrice'].apply(pd.Series, index=bidPrice_columns) 96 | df[askPrice_columns] = df['askPrice'].apply(pd.Series, index=askPrice_columns) 97 | bidVol_columns = ['bidv1','bidv2','bidv3','bidv4','bidv5'] 98 | askVol_columns = ['askv1','askv2','askv3','askv4','askv5'] 99 | df[bidVol_columns] = df['bidVol'].apply(pd.Series, index=bidVol_columns) 100 | df[askVol_columns] = df['askVol'].apply(pd.Series, index=askVol_columns) 101 | # 中间价 102 | df['mid'] = (df['bidp1'] + df['askp1'])/2 103 | # 涨跌停则bid/askprice为0 104 | df.loc[(df['bidp1'] == 0) | (df['askp1'] == 0),'mid'] = df['bidp1'] + df['askp1'] # 涨跌停修正 105 | ## 展示列 最新价,当日成交额、成交量(手)、最高价、最低价、开盘价 106 | # 盘口 askp\askv*/bid* 买卖5档, 昨收 107 | ## 中间价 askp\askv*/bid* 买卖5档,需要使用券商行情 108 | display_columns = ['code', 'lastPrice', 'amount', 'volume', 'high', 'low', 'open', 'lastClose',\ 109 | 'mid', 'askp1', 'askp2', 'askp3', 'askp4', 'askp5', \ 110 | 'bidp1', 'bidp2', 'bidp3', 'bidp4', 'bidp5', \ 111 | 'askv1', 'askv2', 'askv3', 'askv4', 'askv5',\ 112 | 'bidv1', 'bidv2', 'bidv3', 'bidv4', 'bidv5'] 113 | df = df[display_columns].rename(columns={'volume':'vol'}) 114 | df = df.set_index('code') 115 | # 有时,沪市转债单位是手,沪市需要乘一个沪市转化因子 116 | df['vol'] = df['vol']*df.index.map(lambda x: SHmul if 'SH' in x else SZmul if 'SZ' in x else 1) 117 | return df 118 | 119 | ########################################### 账户状态 ################################################### 120 | 121 | # 获取账户状态 净值,现金 122 | def get_account(): 123 | acct_info = get_trade_detail_data(ACCOUNT, account_type, 'account')[0] 124 | return {'net':acct_info.m_dBalance, 'cash':acct_info.m_dAvailable} 125 | # 获取持仓数据 DataFrame index:code, cash 如果没有持仓返回空表(但是有columns) 126 | def get_pos(): 127 | position_to_dict = lambda pos: { 128 | 'code': pos.m_strInstrumentID + '.' + pos.m_strExchangeID, # 证券代码 000001.SZ 129 | 'name': pos.m_strInstrumentName, # 证券名称 130 | 'vol': pos.m_nVolume, # 当前拥股,持仓量 131 | 'AvailableVol': pos.m_nCanUseVolume, # 可用余额,可用持仓,期货不用这个字段,股票的可用数量 132 | 'MarketValue': pos.m_dMarketValue, # 市值,合约价值 133 | 'PositionCost': pos.m_dPositionCost, # 持仓成本, 134 | } 135 | position_info = get_trade_detail_data(ACCOUNT, account_type, 'position') 136 | pos = pd.DataFrame(list(map(position_to_dict, position_info))) 137 | if pos.empty: 138 | return pd.DataFrame(columns=['name', 'vol', 'AvailabelVol', 'MarketValue', 'PositionCost']) 139 | pos = pos.set_index('code') 140 | extract_names = ['新标准券', '国标准券'] 141 | pos = pos[(pos['vol']!=0)&(~pos['name'].isin(extract_names))].copy() # 已清仓不看,去掉逆回购重复输出 142 | return pos 143 | # 忽略逆回购订单、交割单 144 | status_extract_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 145 | '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 146 | '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 147 | '204014.SH', '204028.SH', '204091.SH', '204182.SH'] 148 | # 获取订单状态 当日没有订单返回空表(但是有columns) 当天订单 149 | def get_order(): 150 | order_info = get_trade_detail_data(ACCOUNT, account_type, 'ORDER') 151 | order_to_dict = lambda o:{ 152 | 'id':o.m_strOrderSysID, 153 | 'date': o.m_strInsertDate, 154 | 'code': o.m_strInstrumentID+'.'+o.m_strExchangeID, 155 | 'sub_time': o.m_strInsertTime, # 例如 str:095620 156 | 'trade_type': o.m_nOffsetFlag, # 48 买入/开仓;49 卖出/平仓 157 | 'price': o.m_dLimitPrice, # 挂单价 158 | 'sub_vol': o.m_nVolumeTotalOriginal, 159 | 'dealt_vol': o.m_nVolumeTraded, 160 | 'remain_vol': o.m_nVolumeTotal, 161 | # 48 未报, 49 待报, 50 已报, 51 已报待撤,52 部成待撤, 53 部撤(部成撤单), 162 | # 54 已撤, 55 部成, 56 已成, 57 废单(算法单执行完毕之后为废单), 86 已确认, 255 未知 163 | 'status':o.m_nOrderStatus, 164 | 'frozen':o.m_dFrozenMargin+o.m_dFrozenCommission, # 冻结金额/保证金+手续费 165 | 'remark':o.m_strRemark # 订单备注 166 | } 167 | order = pd.DataFrame(list(map(order_to_dict, order_info))) 168 | if order.empty: 169 | return pd.DataFrame(columns=['id', 'date', 'code', 'sub_time', 'trade_type',\ 170 | 'price', 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']) 171 | order = order[(order['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 172 | (~order['code'].isin(status_extract_codes))].copy() 173 | order = order.set_index('id') 174 | return order[['date', 'code', 'sub_time', 'trade_type', 'price',\ 175 | 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']] 176 | # 获取成交数据 177 | def get_deal(): 178 | deal_info = get_trade_detail_data(ACCOUNT, account_type, 'DEAL') 179 | deal_to_dict = lambda d:{ 180 | 'order_id':d.m_nRef, # 订单编号 181 | 'id':d.m_strOrderSysID, # 合同编号 182 | 'code': d.m_strInstrumentID + '.' + d.m_strExchangeID, 183 | 'date':d.m_strTradeDate, 184 | 'deal_time':d.m_strTradeTime, # 成交时间 185 | # 48 买入/开仓 49卖出/平仓 50 强平 51 平今 52 平昨 53 强减 186 | 'trade_type':d.m_nOffsetFlag, 187 | 'price':d.m_dPrice, 188 | 'vol': d.m_nVolume, 189 | 'amount': d.m_dTradeAmount, 190 | 'remark': d.m_strRemark 191 | } 192 | deal = pd.DataFrame(list(map(deal_to_dict, deal_info))) 193 | if deal.empty: 194 | return pd.DataFrame(columns=['id', 'order_id', 'code', 'date', 'deal_time',\ 195 | 'trade_type', 'price', 'vol', 'amount', 'remark']) 196 | deal = deal[(deal['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 197 | (~deal['code'].isin(status_extract_codes))].copy() 198 | return deal[['id', 'order_id', 'code', 'date', 'deal_time',\ 199 | 'trade_type', 'price', 'vol', 'amount', 'remark']] 200 | 201 | ########################################### 买卖挂单 ################################################### 202 | 203 | # 撤单 超过wait_dur s的订单取消 204 | def cancel_order(C, wait_dur, stratname=None): 205 | order = get_order() 206 | # 全部可撤订单 207 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 208 | # 属于该策略 209 | if stratname!=None: 210 | order = order[order['remark']==stratname].copy() 211 | if not order.empty: 212 | # 超过等待时间撤单 insert_time 为1900年 213 | order['sub_time'] = order['sub_time'].map(lambda x: datetime.datetime.strptime(x, "%H%M%S")) 214 | order = order[order['sub_time'].map(lambda x: (datetime.datetime.now()-x).seconds>wait_dur)] 215 | for orderid in order.index: 216 | cancel(orderid, ACCOUNT, account_type, C) 217 | # 撤单 属于策略strat的挂单价超过最新价(1+r)或低于最新价(1-r)的订单取消 218 | def cancel_order_price(C, r, stratname=None): 219 | order = get_order() 220 | # 全部可撤订单 221 | order = order[order['status'].map(lambda x:(x!=53)&(x!=54)&(x!=56)&(x!=57))].copy() 222 | # 属于该策略 223 | if stratname!=None: 224 | order = order[order['remark']==stratname].copy() 225 | if not order.empty: 226 | # 最新价格 227 | codes = list(set(order['code'])) 228 | snapshot = get_snapshot(C, codes) 229 | lastPrice = snapshot[['lastPrice', 'lastClose']].apply(lambda x: \ 230 | x['lastPrice'] if x['lastPrice']!=0 else x['lastClose'], axis=1) 231 | lastPrice = order['code'].map(lambda x: lastPrice[x]) 232 | if not order.empty: 233 | delta = abs((order['price']-lastPrice)/lastPrice) 234 | delta = delta[delta>r] 235 | for orderid in delta.index: 236 | cancel(orderid, ACCOUNT, account_type, C) 237 | #strategy_name = 'craft' 238 | #multiples = 10 239 | #卖出 240 | def sell(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 241 | vol = int((vol//multiples)*multiples) 242 | if vol==0: 243 | print('too less vol to sub') 244 | return 245 | # 卖出,单标的,账号, 代码,限价单,价格,量,策略名,立即触发下单,备注 246 | if account_type=='STOCK': 247 | passorder(24, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 248 | elif account_type=='CREDIT': 249 | passorder(34, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 250 | #买入 251 | def buy(C, code, price, vol, strategyName=strategy_name, remark=strategy_name): 252 | vol = int((vol//multiples)*multiples) 253 | if vol==0: 254 | print('too less vol to sub') 255 | return 256 | if account_type=='STOCK': 257 | passorder(23, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 258 | elif account_type=='CREDIT': 259 | passorder(33, 1101, ACCOUNT, code, 11, price, vol, strategyName, 2, remark, C) # 下单 260 | 261 | ########################################### 其他 ################################################### 262 | 263 | # 存储全局变量 264 | class a(): 265 | pass 266 | 267 | 268 | 269 | ####################################################################################################### 270 | ############################# 策略主代码 ########################################### 271 | ####################################################################################################### 272 | 273 | 274 | # 初始化准备 275 | def prepare(C): 276 | strat_files = [f for f in os.listdir(stratfile_loc) \ 277 | if f.split('.')[-2].split('-')[-1]==stratfile_name] 278 | strat_files = sorted(strat_files) 279 | strat_file = stratfile_loc + strat_files[-1] 280 | df = pd.read_csv(strat_file, encoding='gbk') 281 | log('读取策略文件', strat_file) 282 | print('读取策略文件', strat_file) 283 | pos_init = get_pos() # 初始持仓 284 | init_cap = pos_init['MarketValue'] 285 | init_vol = pos_init['vol'] 286 | sorted_codes = df['代码'].astype('str')+'.'+df['市场'] # 策略标的按打分排序 287 | holding_codes = set(init_cap.index)&set(sorted_codes[:holding_num].values) # 排名holding_num之内的标的继续持有 288 | buy_codes = sorted_codes[~sorted_codes.isin(holding_codes)] # 如果继续持有标的不足buy_num只,则需要新买入,按打分排序填充 289 | buy_codes = buy_codes.values[:max(buy_num - len(holding_codes), 0)] 290 | target_codes = holding_codes|set(buy_codes)-set(extract_codes) # 目标市值为 继续持有+新买入 标的等权*策略市值,去掉黑名单标的 291 | target_weights = pd.Series(1, index=list(target_codes)) 292 | target_weights = target_weights/target_weights.sum() 293 | trade_codes = list((set(target_codes)|set(init_cap.index))-set(extract_codes)) # 交易涉及标的 294 | target_cap = (strat_cap*target_weights).reindex(trade_codes).fillna(0) 295 | log('目标市值') 296 | log(target_cap) 297 | init_cap = init_cap.reindex(trade_codes).fillna(0) 298 | init_vol = init_vol.reindex(trade_codes).fillna(0) # 选中但非持仓转债持仓张数和市值为0 299 | log('初始持仓') 300 | log(pd.concat([init_vol, init_cap], axis=1)) 301 | snapshot = get_snapshot(C, trade_codes) 302 | mid_snapshot = snapshot['mid'] 303 | trade_cap = target_cap-init_cap 304 | trade_cap.loc[abs(trade_cap)0: 350 | # 跌停不买入 351 | if code in limitdown_codes.index: 352 | print('跌停不买入', code) 353 | log('跌停不买入', code) 354 | else: 355 | # 不能超过涨停价 356 | price = min(lastClose_snapshot[code]*(1+max_upndown), bidPrice_snapshot[code]*(tolerance+1)) 357 | buy(C, code, price, vol) 358 | log('buy %s %s %s'%(code, price, vol)) 359 | #print('buy', code, price, vol) 360 | A.traded_vol.loc[code] += int((vol//multiples)*multiples) 361 | else: 362 | price = min(lastClose_snapshot[code]*(1-max_upndown), askPrice_snapshot[code]*(1-tolerance)) 363 | sell(C, code, price, vol) 364 | log('sell %s %s %s'%(code, price, vol)) 365 | #print('sell', code, price, vol) 366 | A.traded_vol.loc[code] -= int((vol//multiples)*multiples) 367 | A.remain_times = A.remain_times-1 368 | 369 | # 大概率不成交的单子进行撤单 370 | def order_canceler(C): 371 | cancel_order(C, wait_dur, strategy_name) 372 | cancel_order_price(C, 0.01, strategy_name) 373 | 374 | # 交易情况总结 375 | def summary(C): 376 | log('总成交张数') # 已成交张数 377 | log(A.traded_vol) 378 | end_cap = get_pos()['MarketValue'] # 处理全部持仓 379 | log('结束时持仓') 380 | log(end_cap.loc[list(A.trade_vol.index)].fillna(0)) 381 | 382 | 383 | 384 | 385 | # 初始化函数 主程序 386 | def init(C): 387 | # 存储全局变量 388 | global A 389 | A = a() 390 | # 初始化时读取一次,之后每日8:30读取 391 | prepare(C) 392 | # 交易时间运行装饰器 交易日start到start+dur 393 | def trader_time(func): 394 | def wrapper(*args, **kwargs): 395 | today = datetime.datetime.now().date().strftime("%Y%m%d") 396 | now = datetime.datetime.now().time() 397 | if C.get_trading_dates('SH', today, today, 1, '1d'): 398 | if (datetime.time(int(start_time[:2]), int(start_time[2:4]), int(start_time[4:6])) \ 399 | <= now <= datetime.time(int(end_time[:2]), int(end_time[2:4]), int(end_time[4:6]))): 400 | return func(*args, **kwargs) 401 | else: 402 | pass 403 | else: 404 | pass 405 | return wrapper 406 | # 挂载定时函数 407 | global f0, f1, f2, f3 408 | f0 = trader_time(order_canceler) # 定时、按条件撤单程序 409 | C.run_time('f0', "1nSecond", "2022-08-01 09:15:00", "SH") # 每秒检查撤单 410 | C.run_time('prepare', "1d", "2022-08-01 %s:%s:%s"%(prepare_time[:2], prepare_time[2:4], prepare_time[4:6]),\ 411 | "SH") # 每天初始化函数 412 | # trader 413 | f1 = trader_time(trader) 414 | C.run_time('f1', "%snSecond"%interval, "2022-08-01 09:15:00", "SH") # 交易员 415 | C.run_time('summary', "1d", "2022-08-01 %s:%s:%s"%(summary_time[:2], summary_time[2:4], summary_time[4:6]),\ 416 | "SH") # 每天初始化函数 417 | # 读取图形界面传入的ACCOUNT 418 | global ACCOUNT 419 | ACCOUNT = account if 'account' in globals() else ACCOUNT 420 | -------------------------------------------------------------------------------- /Summary.py: -------------------------------------------------------------------------------- 1 | # encoding:gbk 2 | import datetime, re, os, time 3 | import numpy as np 4 | import pandas as pd 5 | 6 | # 每交易日summary_time(16:20)输出账户当日市值/现金、持仓情况、交易结算情况、申报委托单等信息。 7 | 8 | ############### 请根据账户和本地配置修改以下部分 ##################### 9 | ACCOUNT = '**********' # 填写您的资金账号 10 | account_type = 'STOCK' 11 | strategy_name = 'summary' 12 | 13 | logfile = 'D:/cloud/monitor/QMT/LogRunning/' # 填写您的日志文件保存位置 14 | logfile = logfile + ACCOUNT + '-' + strategy_name + '.txt' 15 | save_loc = 'D:/cloud/monitor/QMT/summary/' # 填写您的结算文件(本策略输出结果)保存位置 16 | save_loc = save_loc + ACCOUNT + '/' + account_type + '/' 17 | summary_time = '162000' 18 | 19 | 20 | #################################### 以下不可修改 ################################### 21 | 22 | ####################################################################################################### 23 | ############################# 常用函数模块 ########################################### 24 | ####################################################################################################### 25 | 26 | 27 | 28 | ########################################### 日常io运行 ################################################### 29 | 30 | # log 函数 31 | def log(*txt): 32 | try: 33 | f = open(logfile,'a+', encoding='gbk') 34 | f.write('%s'%datetime.datetime.now()+' '*6) # 时间戳 35 | if type(txt[0])==pd.Series: 36 | f.write('name: %s\n'%txt[0].name) 37 | for i,v in txt[0].items(): 38 | f.write(' '*32+str(i)+', '+str(v)+'\n') 39 | elif type(txt[0])==pd.DataFrame: 40 | f.write(' '.join([str(i) for i in txt[0].columns])+'\n') 41 | for i,r in txt[0].iterrows(): 42 | f.write(' '*29+str(i)+': ') 43 | f.write(', '.join([str(j) for j in r.values])+'\n') 44 | else: 45 | write_str = ('\n'+' '*32).join([str(i) for i in txt]) 46 | f.write('%s\n' %write_str) 47 | f.close() 48 | except PermissionError as e: 49 | print(f"Error: {e}. You don't have permission to access the specified file.") 50 | 51 | ########################################### 账户状态 ################################################### 52 | 53 | # 获取账户状态 净值,现金 54 | def get_account(): 55 | acct_info = get_trade_detail_data(ACCOUNT, account_type, 'account')[0] 56 | return {'net':acct_info.m_dBalance, 'cash':acct_info.m_dAvailable} 57 | # 获取持仓数据 DataFrame index:code, cash 如果没有持仓返回空表(但是有columns) 58 | def get_pos(): 59 | position_to_dict = lambda pos: { 60 | 'code': pos.m_strInstrumentID + '.' + pos.m_strExchangeID, # 证券代码 000001.SZ 61 | 'name': pos.m_strInstrumentName, # 证券名称 62 | 'vol': pos.m_nVolume, # 当前拥股,持仓量 63 | 'AvailableVol': pos.m_nCanUseVolume, # 可用余额,可用持仓,期货不用这个字段,股票的可用数量 64 | 'MarketValue': pos.m_dMarketValue, # 市值,合约价值 65 | 'PositionCost': pos.m_dPositionCost, # 持仓成本, 66 | } 67 | position_info = get_trade_detail_data(ACCOUNT, account_type, 'position') 68 | pos = pd.DataFrame(list(map(position_to_dict, position_info))) 69 | if pos.empty: 70 | return pd.DataFrame(columns=['name', 'vol', 'AvailabelVol', 'MarketValue', 'PositionCost']) 71 | pos = pos.set_index('code') 72 | extract_names = ['新标准券', '国标准券'] 73 | pos = pos[(pos['vol']!=0)&(~pos['name'].isin(extract_names))].copy() # 已清仓不看,去掉逆回购重复输出 74 | return pos 75 | # 忽略逆回购订单、交割单 76 | status_extract_codes = ['131810.SZ', '131811.SZ', '131800.SZ', '131809.SZ', '131801.SZ',\ 77 | '131802.SZ', '131803.SZ', '131805.SZ', '131806.SZ',\ 78 | '204001.SH', '204002.SH', '204003.SH', '204004.SH', '204007.SH',\ 79 | '204014.SH', '204028.SH', '204091.SH', '204182.SH'] 80 | # 获取订单状态 当日没有订单返回空表(但是有columns) 当天订单 81 | def get_order(): 82 | order_info = get_trade_detail_data(ACCOUNT, account_type, 'ORDER') 83 | order_to_dict = lambda o:{ 84 | 'id':o.m_strOrderSysID, 85 | 'date': o.m_strInsertDate, 86 | 'code': o.m_strInstrumentID+'.'+o.m_strExchangeID, 87 | 'sub_time': o.m_strInsertTime, # 例如 str:095620 88 | 'trade_type': o.m_nOffsetFlag, # 48 买入/开仓;49 卖出/平仓 89 | 'price': o.m_dLimitPrice, # 挂单价 90 | 'sub_vol': o.m_nVolumeTotalOriginal, 91 | 'dealt_vol': o.m_nVolumeTraded, 92 | 'remain_vol': o.m_nVolumeTotal, 93 | # 48 未报, 49 待报, 50 已报, 51 已报待撤,52 部成待撤, 53 部撤(部成撤单), 94 | # 54 已撤, 55 部成, 56 已成, 57 废单(算法单执行完毕之后为废单), 86 已确认, 255 未知 95 | 'status':o.m_nOrderStatus, 96 | 'frozen':o.m_dFrozenMargin+o.m_dFrozenCommission, # 冻结金额/保证金+手续费 97 | 'remark':o.m_strRemark # 订单备注 98 | } 99 | order = pd.DataFrame(list(map(order_to_dict, order_info))) 100 | if order.empty: 101 | return pd.DataFrame(columns=['id', 'date', 'code', 'sub_time', 'trade_type',\ 102 | 'price', 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']) 103 | order = order[(order['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 104 | (~order['code'].isin(status_extract_codes))].copy() 105 | order = order.set_index('id') 106 | return order[['date', 'code', 'sub_time', 'trade_type', 'price',\ 107 | 'sub_vol', 'dealt_vol', 'remain_vol', 'status', 'frozen', 'remark']] 108 | # 获取成交数据 109 | def get_deal(): 110 | deal_info = get_trade_detail_data(ACCOUNT, account_type, 'DEAL') 111 | deal_to_dict = lambda d:{ 112 | 'order_id':d.m_nRef, # 订单编号 113 | 'id':d.m_strOrderSysID, # 合同编号 114 | 'code': d.m_strInstrumentID + '.' + d.m_strExchangeID, 115 | 'date':d.m_strTradeDate, 116 | 'deal_time':d.m_strTradeTime, # 成交时间 117 | # 48 买入/开仓 49卖出/平仓 50 强平 51 平今 52 平昨 53 强减 118 | 'trade_type':d.m_nOffsetFlag, 119 | 'price':d.m_dPrice, 120 | 'vol': d.m_nVolume, 121 | 'amount': d.m_dTradeAmount, 122 | 'remark': d.m_strRemark 123 | } 124 | deal = pd.DataFrame(list(map(deal_to_dict, deal_info))) 125 | if deal.empty: 126 | return pd.DataFrame(columns=['id', 'order_id', 'code', 'date', 'deal_time',\ 127 | 'trade_type', 'price', 'vol', 'amount', 'remark']) 128 | deal = deal[(deal['date']==datetime.datetime.today().strftime("%Y%m%d"))&\ 129 | (~deal['code'].isin(status_extract_codes))].copy() 130 | return deal[['id', 'order_id', 'code', 'date', 'deal_time',\ 131 | 'trade_type', 'price', 'vol', 'amount', 'remark']] 132 | 133 | ########################################### 其他 ################################################### 134 | 135 | # 存储全局变量 136 | class a(): 137 | pass 138 | 139 | ############################################################################################## 140 | ################################### 策略 ################################################# 141 | ############################################################################################## 142 | 143 | 144 | def summary(C): 145 | today = datetime.datetime.now().date().strftime("%Y%m%d") 146 | # 如果有当日状态文件则删除 147 | def delete_file(file_path): 148 | # 检查文件是否存在 149 | if os.path.exists(file_path): 150 | os.remove(file_path) # 删除文件 151 | else: 152 | pass 153 | summary_nams = {'acct':save_loc+'acct-'+today+'.csv',\ 154 | 'pos':save_loc+'position-'+today+'.csv',\ 155 | 'order':save_loc+'order-'+today+'.csv',\ 156 | 'deal':save_loc+'deal-'+today+'.csv',\ 157 | 'strat_pos':save_loc+'/stratpos-'+today+'.csv'} 158 | for f in summary_nams.keys(): 159 | delete_file(summary_nams[f]) 160 | # 账户 161 | acct = get_account() 162 | pd.Series(acct).to_csv(summary_nams['acct']) 163 | # 当日持仓记录 164 | pos = get_pos() 165 | pos.to_csv(summary_nams['pos'], encoding='utf_8_sig') 166 | # 当日委托单 167 | order = get_order() 168 | order.to_csv(summary_nams['order']) 169 | # 当日成交 170 | deal = get_deal() 171 | deal.to_csv(summary_nams['deal'], index=False) 172 | log('summary success') 173 | 174 | 175 | # 初始化函数 主程序 176 | def init(C): 177 | #summary(C) 178 | # 存储全局变量 179 | global A 180 | A = a() 181 | # 初始化时检查文件夹,如果没有的话则创建 182 | if not os.path.exists(save_loc): 183 | os.makedirs(save_loc) 184 | # 交易日 185 | def trade_time(func): 186 | def wrapper(*args, **kwargs): 187 | today = datetime.datetime.now().date().strftime("%Y%m%d") 188 | now = datetime.datetime.now().time() 189 | if C.get_trading_dates('SH', today, today, 1, '1d'): 190 | return func(*args, **kwargs) 191 | else: 192 | pass 193 | return wrapper 194 | # 每日定时定点summary函数 195 | global f 196 | f = trade_time(summary) 197 | C.run_time('f', "1d", "2024-01-01 %s:%s:%s"%(summary_time[:2], summary_time[2:4], \ 198 | summary_time[4:6]), "SH") # 输出今日委托 199 | # 读取图形界面传入的ACCOUNT 200 | global ACCOUNT 201 | ACCOUNT = account if 'account' in globals() else ACCOUNT 202 | -------------------------------------------------------------------------------- /launch.py: -------------------------------------------------------------------------------- 1 | # 自动启动国金qmt客户端 2 | import os 3 | import time 4 | import win32gui, win32api, win32con 5 | 6 | # 适用于国金qmt,登录需要时间,如自动运行多个qmt请间隔足够长时间 7 | 8 | user=' ' # 填入您的qmt用户名 9 | password=' ' # 填入您的qmt密码 10 | cmdstr="start D:\\国金证券QMT交易端111\\bin.x64\\XtItClient.exe " # 填入您的qmt路径 11 | 12 | 13 | def input_content(hwd, content): 14 | mylist=[int(ord(item)) for item in str(content)] 15 | for item in mylist: 16 | click_keys(hwd,item) 17 | time.sleep(0.1) 18 | 19 | def get_my_child_window(parent): 20 | hwndChildList = [] 21 | win32gui.EnumChildWindows( 22 | parent, lambda hwnd, param: param.append((hwnd,win32gui.GetWindowText(hwnd),win32gui.GetClassName(hwnd),win32gui.GetWindowRect(hwnd))), hwndChildList) 23 | for i in range(len(hwndChildList)): 24 | item=hwndChildList[i] 25 | #print item[0],item[1],item[2],i 26 | return hwndChildList 27 | 28 | def find_child_window(parent_handle,winstr,classname=""): 29 | result=[] 30 | handlelist=get_my_child_window(parent_handle) 31 | for item in handlelist: 32 | if item[1].strip().startswith(winstr): 33 | if classname=="": 34 | result.append(item[0]) 35 | else: 36 | if str(item[2])==classname: 37 | result.append(item[0]) 38 | return result 39 | 40 | def click_keys(hwd, mykey): 41 | win32api.SendMessage(hwd, win32con.WM_KEYDOWN, mykey, 0) 42 | win32api.SendMessage(hwd, win32con.WM_KEYUP, mykey, 0) 43 | 44 | # 进入qmt文件夹 45 | os.system(cmdstr) 46 | # 获取qmt句柄 47 | qmt_handle=find_child_window(0,u"国金证券QMT交易端","Qt5QWindowIcon")[0] 48 | time.sleep(2) 49 | # 后台输入账号 50 | input_content(qmt_handle,str(user)) 51 | time.sleep(2) 52 | click_keys(qmt_handle,win32con.VK_RETURN) 53 | time.sleep(2) 54 | # 后台输入密码 55 | input_content(qmt_handle,str(password)) 56 | time.sleep(2) 57 | click_keys(qmt_handle,win32con.VK_RETURN) 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | 4 | with open("README.md","r",encoding='utf-8') as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name="EzQmt", 9 | # 版本号: 第几次模块增加,第几次函数增加,第几次函数功能修改 10 | # (每次高级别序号增加后,低级别序号归0) 11 | # alpha为调试版,beta为测试版,没有后缀为稳定版 12 | version="2.1.1", 13 | author="LH.Li", 14 | author_email="lh98lee@zju.edu.cn", 15 | description='Package for QMT', 16 | long_description=long_description, 17 | # 描述文件为md格式 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/LHanLi/EzQmt", 20 | packages=find_packages(), 21 | install_requires = [ 22 | #'pandas', 23 | #'scipy', 24 | #'statsmodels', 25 | #'seaborn', 26 | #'plottable', 27 | #'pyecharts', 28 | #'numpy_ext', 29 | #'xlsxwriter' 30 | ], 31 | classifiers=[ 32 | # 该软件包仅与Python3兼容 33 | "Programming Language :: Python :: 3", 34 | # 根据GPL 3.0许可证开源 35 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 36 | # 与操作系统无关 37 | "Operating System :: OS Independent", 38 | ], 39 | ) 40 | 41 | # python3 setup.py install --u 42 | -------------------------------------------------------------------------------- /upload_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mypython=/Users/h1nlee/miniconda3/envs/gitenv/bin/python 3 | 4 | # Upload project to pypi 5 | 6 | rm -rf ./build 7 | rm -rf ./dist 8 | rm -rf ./EzQmt.egg-info 9 | 10 | $mypython setup.py sdist bdist_wheel 11 | twine check dist/* 12 | twine upload dist/* 13 | # (-u __token__ -p password) 14 | --------------------------------------------------------------------------------