最近工作需要,编写了一个程序,实现了使用python读取交割单信息,自动计算每日账户资产。可以为账户收益率计算节省大量时间。
本次变动:1 将对账单文件从csv改为xls,2 增加了命令行方式。其他大致和上次一样。
程序计算量比较小,计算结果直接显示即可。
实现原理:
将交割单数据从后向前一条一条读取。
交割日期大于指定日期的不显示,直接跳过。
交割日期小于等于指定日期的记录,第一条记录中的资金余额为账户资金余额。
每次新碰到一个证券代码,对应的证券余额是持仓证券的数量,如果不是新的证券代码,说明前面已经显示过了,这样重复证券代码的记录直接忽略。
证券数量大于0的记录,显示证券代码,证券名称和证券数量。证券数量为零的证券说明已经清仓了,不需要显示。
根据证券代码和指定日期,查询到对应的收盘价格。根据持仓数量和收盘价格可以算出证券市值。
持仓市值加上资金余额即账户收盘资产。
关于对账单文件:
对账单文件是Excel 97-2003的格式。
交割单中的证券余额和资金余额要准确。如果有误,需要手工调制。尤其是有红股和分红数据对应的证券余额和资金余额。
某些券商的对账单中有对比对应的资金冻结和解冻。直接删掉这些记录,就按照资金未冻结处理。
如果对账单文件中包括基金交易的数据,也可以使用此程序。
查询日期:
查询日期如果不是交易日,则显示上一个交易日的信息。
证券价格:
A股当前交易的股票查对应询日期的价格可以通过baostock获得。其他日期只能手工按照固定格式列出;港股和基金的价格也只能手工给出。程序会读取对应文件,从而获得指定日期的价格。
以下为代码。
'''
Created on 2021年8月26日
@author: yangs
此程序用于计算证券账户指定日期的资产总值。
第一步:读取对账单到dataframe中。
注意对账单的格式要正确,并且需要自己验证个股票代码后面对应的证券余额的正确性。
查询日期按照年月日写作 XXXX-XX-XX
新股中签后的代码和数量需要自己确保正确。从券商系统导出的数据一般有下列问题,需要手动处理正确后本程序才能正确显示资产值。
1 股息分红的记录中,股票代码后面的证券数量可能不正确,需要手工修正
2 红股入账时的证券数量可能不正确,需要手工修正。
3 冻结和解冻资金会导致剩余金额有变动,所以这个资金变动可以忽略。需要删掉冻结和解冻。然后修正资金余额
4 新股新债申购中签后,会有中签记录,冻结资金,新股债入账,到最后的卖出。对应的股票代码可能变了好几次。需要手工调制为一个代码,并且修正扣款和持股的逻辑关系。
如果记录扣款了,就要记录对应的持股。
5 新股新债的持股可能早于交易日期。因此需要在股票价格对应的文件中补全交易日之前的价格。
6 冻结的资金,如果一直没有解冻。就直接按照已解冻修正资金余额。
7 目前支持的港股就三个 福寿园,腾讯控股,京东。超出这三个的需要自己修改程序。
第二步:取指定日期之前的记录,重复的证券代码的,只显示最后一条记录。数量为0的也不显示。
第三步:根据证券代码和指定日期,获得收盘价格。股票的收盘价格从baostock中获取,并从手动表中补充其他的价格
注意,港股需要特殊处理
第四步:计算出收盘后的账户资产值
注意:本版本程序对应的 数据源文件是 Excel 97-2003版本
'''
import pandas as pd
from pandas import read_excel
from datetime import datetime
import baostock as bs
from sqlparse.sql import Begin
import sys
class MyTools(object):
"""
工具类
"""
def __init__(self):
pass
def para_input(self):
if(len(sys.argv ) == 1):
check_date = "2020-12-31"
input_files = ["2006-2015gtja_check_sheetV2.xls","2014-2022gfgx_check_sheetV2.xls","2020-2022ggpa_check_sheet.xls"]
elif( len(sys.argv ) == 2):
check_date = sys.argv[1]
input_files = ["2006-2015gtja_check_sheetV2.xls","2014-2022gfgx_check_sheetV2.xls","2020-2022ggpa_check_sheet.xls"]
elif(len(sys.argv) ==3 ):
check_date = sys.argv[1]
input_file = sys.argv[2]
input_files=[]
input_files.append(input_file)
return check_date, input_files
def myshow(self, data1,data2):
"""
显示账户信息
"""
pd.set_option('display.max_rows',None)
print("\n------------------------------------------------------------")
i = 0
maxi = data1.shape[0]
j=0
maxj = data1.shape[1]
while(i< maxi):
j=0
while(j<maxj):
# print("L82 i,3:\t",data1.iloc[i,3],"\t i,4:\t",data1.iloc[i,4] )
if(float(data1.iloc[i,3]) != 0.0):
print(data1.iloc[i,j],end="\t")
j=j+1
if(float(data1.iloc[i,3]) > 0.0):
print(" ")
i=i+1
# print("\nL92 data2 \n",data2)
print("\nL94 data2")
print(data2.iloc[0,0],end="\t")
print(data2.iloc[0,1],end="\t")
print(data2.iloc[0,2],end="\t")
print(data2.iloc[0,3],end="\t")
print(data2.iloc[0,4],end="\t")
print(data2.iloc[0,5],end="\t")
print(data2.iloc[0,6],end="\n")
# print("\nL47 data1 \n",data1[data1.number >0.0])
print("------------------------------------------------------------\n")
def check_trade_date(self,query_date):
"""
核实query_date是否为交易日。如果为交易日,返回query_date。如果不是交易,返回该日期的前一个交易日
query_date 格式要求:必须是 2015-12-01 的格式。共十位,用-分开。前后不能有空格
"""
file_name = "D:\\temp\\收益率计算\\股票价格\\"+"000300"+"收盘价格.csv"
file_content = self.read_text_file(file_name)
tmp_lines = file_content.splitlines(False)
tmp_date = "2000-01-01"
for item in tmp_lines:
# print("L48: ",item,"\t",item[7:17])
if( query_date == item[7:17]):
return query_date
elif(query_date > item[7:17]):
tmp_date = item[7:17]
else:
break
if(tmp_date < query_date):
return tmp_date
else:
return query_date
def read_tradeFile(self, full_file_name):
'''
读取对账单文件。
返回格式: dataframe
对账单为xls文件,格式如下。
其中要用到的数据包括交收日期,证券代码,证券余额,资金余额。其他列数据暂时不用
序号,交收日期,证券代码,证券名称,交易类别,成交价格,成交数量,证券余额,成交金额,资金发生数,资金余额,,流水序号,业务标志,业务名称,发生金额,后资金额,货币类别,备注,,,,,,,,,,,,,费用合计,净佣金,规费,印花税,过户费,币种,合同号,资金账号,股东代码
1,2014-06-09,,,银行转存,,,,5475,5475.00 ,5475.00 ,,9,2041,银行转存,5475,5475,人民币,银行返回码[0000]返回信息[[0000]交易成功]|转账成功,,,,,,,,,,,,,,,,,,,,,
2,2014-06-16,,,银行转取,,,,-5475,-5475.00 ,0.00 ,,7,2042,银行转取,-5475,0,人民币,银行返回码[0000]返回信息[[0000]交易成功]|转账成功,,,,,,,,,,,,,,,,,,,,
'''
# tmpdata = pd.read_csv( full_file_name ) #读取csv文件
tmpdata = read_excel(full_file_name,sheet_name=0) #读取xls文件
return tmpdata
def read_text_file(self, full_file_name ):
"""
读取文本文件,返回文件内容。
返回格式:字符串
"""
try:
with open(full_file_name, encoding = "utf-8", mode = "r") as f:
text_file_content = f.read()
return text_file_content
except:
print("L168 打开文件出错!")
return ""
def get_hkd_exchange_rate(self, check_date):
"""
根据检查日期给出港股通汇率数值
"""
full_file_name = "D:\\temp\\收益率计算\\股票价格\\深交所结算汇兑比率.csv"
tmpdata = pd.read_csv( full_file_name )
i=0
maxi = len(tmpdata)
while(i<maxi):
if(check_date > tmpdata.iloc[i,0]):
target_value = tmpdata.iloc[i,2]
target_date = tmpdata.iloc[i,0]
else:
break
i=i+1
# print("L188 ", "查询日期 ", check_date, " 实际日期 ", target_date," 查询值 ", target_value)
return target_value
def get_curr_price_manual(self, stock_code, check_date):
"""
从csv文件中读取证券代码指定日期的收盘价格
返回格式:字符串
csv文件格式如下:日期必须是 2016-06-14 的格式。
code,date,price,name
159902,2016-06-14,3.0970 ,中小板
159902,2016-05-26,3.0490 ,中小板
"""
try:
file_name = "D:\\temp\\收益率计算\\股票价格\\"+stock_code+"收盘价格.csv"
with open(file_name, encoding = "utf-8", mode = "r") as f:
manual_data = f.read()
find_str = stock_code+","+check_date
position = manual_data.find(find_str)
start_position = position+len(find_str)+1
end_position = manual_data.find(",",start_position )-1
close_price = manual_data[start_position : end_position ]
close_price = close_price.lstrip().rstrip()
close_price = float(close_price)
return close_price
except:
return 0
def get_spec_date_closeprice(self, stock_code, curr_date):
"""
从baostock上获取沪深A股 stock_code 指定日期 curr_date 的收盘价格
返回值:收盘价格,如果找不到收盘价格,就返回数值0
"""
if(stock_code in ['00700','01448','09618']):
return 0
stock_code1 = int( stock_code ) #证券代码转为整数
stock_code2 = "{:0>6d}".format(stock_code1) # 整数转为6位前补0的字串
first_char = stock_code2[0:1]
if(first_char == "6"):
full_stock_code = "sh."+stock_code2
else:
full_stock_code = "sz."+stock_code2
rs = bs.query_history_k_data_plus(full_stock_code,
"date,code,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,isST",
start_date=curr_date, end_date=curr_date,
frequency="d", adjustflag="3")
#### 打印结果集 ####
data_list = []
while (rs.error_code == '0') & rs.next():
# 获取一条记录,将记录合并在一起
data_list.append(rs.get_row_data())
result = pd.DataFrame(data_list, columns=rs.fields)
#### 结果集输出到csv文件 ####
tmpdata = 0
if( len(result)>0 ):
tmpdata = result.iloc[0,2]
return float(tmpdata)
class InvestAccount(object):
'''
投资账户类
'''
def __init__(self, account_name, trade_log_file):
'''
初始化账户
'''
self.account_name = account_name # 账户名称
self.notes = "" # 账户备注信息
# self.creat_date = create_date # 账户设立日期
# self.close_date = close_date # 账户关闭日期
self.source_data = pd.DataFrame() #账户交易数据
self.total_asset = 0 #账户资产
self.cash = 0 #账户现金
self.net_asset_value = 0 #净值
self.return_value = 0 #账户收益值
self.invest_item = []
my_tools = MyTools()
self.source_data = my_tools.read_tradeFile( trade_log_file )
def show_tradelog(self):
"""
显示读取的历史交易信息
"""
print("\n显示历史交易记录\n")
print(self.source_data)
print(" =================== 历史交易记录显示完毕 =================== ")
def show_account_reverse(self,check_date):
"""
显示账户指定日期的净值。指定日期必须是交易日期。如果不是交易日期,程序会出现异常报错或者数据计算错误。
"""
tmpdata = self.source_data
first_date = tmpdata.iloc[0,1]
last_date = tmpdata.iloc[-1,1]
curr_check_date = datetime.strptime(check_date, '%Y-%m-%d') #将字串转为日期格式
# print("L359 ",creat_date,close_date)
if(curr_check_date < first_date):
print("L198: 账户",self.account_name,"当前未开户,或者查询日期小于第一个日期")
return 1
if(curr_check_date > last_date):
print("L200: 账户",self.account_name,"当前已销户,或者查询日期大于最后一个日期")
return 1
print("\n当前账户名称:\t",self.account_name, end="\t")
print("检查日期点是:\t", check_date)
my_tools = MyTools()
# 注意,以下的显示是从后向前检查的
i=tmpdata.shape[0] #数组总长度
inv_item = [] #【显示列表】或者投资清单列表
total_asset = 0 #账户总资产
firstdisplay = 1 #首次出现的标志
HK_stock_num = 0
hkd_exchange_rate = 1
while(i>0): #从后先前显示
i=i-1
# tmp1 = datetime.strptime(tmpdata.iloc[i,1], '%Y-%m-%d') #将字串转为日期格式
tmp1 = tmpdata.iloc[i,1]
if(tmp1 > curr_check_date ): #日期之后的部分不显示,直接跳过
continue
else:
stock_code = tmpdata.iloc[i,2]
if(stock_code not in inv_item): #不在【已显示列表】中的显示,在的就不显示
inv_item.append(stock_code) #将要显示的代码添加到【已显示列表】中,后续就不再显示了
#以下为第一次显示
if(firstdisplay == 1): #是第一个显示,取账户资金余额的数值
firstdisplay = 0
print("账户资金余额:\t",tmpdata.iloc[i,10])
total_asset = total_asset + float(tmpdata.iloc[i,10] )
print("当前持仓信息:")
print("序号\t 证券代码 \t 证券名称 \t 持仓数量 \t 收盘价 \t 参考市值")
#以下为非第一次显示
if(tmpdata.iloc[i,7] > 0.0): # 证券代码数量不为0才显示。数量为0的直接忽略
stock_code = int(tmpdata.iloc[i,2]) #缺省是浮点数
stock_code = "{:0>6d}".format(stock_code) # 整数转为6位前补0的字串
if(stock_code in ['000700','001448','009618']): #如果是港股,调整为5位数字
stock_code = stock_code[1:len(stock_code)]
stock_name = tmpdata.iloc[i,3]
stock_hold_number = tmpdata.iloc[i,7]
print(i+1,"\t",stock_code,"\t",stock_name,"\t",stock_hold_number, end="\t")
# 获取价格
curr_price = my_tools.get_spec_date_closeprice(stock_code,check_date)
if(curr_price ==0):#如果网络上获取的价格为0,则从手工表中获取股票或者基金的价格
curr_price = my_tools.get_curr_price_manual(stock_code,check_date)
#显示价格
print(curr_price,end="\t")
# print("\n")
# print("L369 check date is: ", check_date)
# print("L369 获得的港币卖出汇率为:", my_tools.get_hkd_exchange_rate(check_date))
if(stock_code in ['00700','01448','09618']):
hkd_exchange_rate = my_tools.get_hkd_exchange_rate(check_date)
item_asset = float(stock_hold_number) * float(curr_price) * hkd_exchange_rate
HK_stock_num = 1
else:
item_asset = float(stock_hold_number) * float(curr_price)
print('%.2f' % ( item_asset ) ,end="\t")
total_asset = total_asset + item_asset
print('%.2f' % total_asset,end="\t" )
print(" ")
print("\nL458 账户名称:\t",self.account_name,"\t 日期:\t", check_date, end=" \t 账户资产合计:\t")
if(HK_stock_num ==1 ):
print('%.2f' % total_asset , "\t港股通汇率\t", hkd_exchange_rate)
else:
print('%.2f' % total_asset)
print("L463 ========================= 账户信息显示完毕 ======================== \n\n")
return total_asset
#主程序开始处
if __name__ == '__main__':
"""
主程序...
"""
lg = bs.login()
my_tools = MyTools()
check_date, input_files = my_tools.para_input()
for item in input_files:
account_name = item
trade_log_file = item
# create_date = ""
# close_date = my_tools.get_last_date(input_files)
curr_account = InvestAccount(account_name,trade_log_file)
valid_date = my_tools.check_trade_date(check_date) #如果查询日期不是交易日,转换为前一个交易日。
# print("L391 ",item,valid_date )
if(valid_date != check_date):
print("L731: 查询开始 日期",check_date,"不是交易日。因此后续显示前一个交易日",valid_date,"的数据 --------")
else:
print("L713: 查询开始 --------------------------------------------------------")
# print("L733 显示",valid_date,"的账户信息")
curr_account.show_account_reverse( valid_date )
print("L738: 查询完毕 ==================================================================================\n")
bs.logout()
print("L741 恭喜恭喜,程序运行正常结束。")