添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

前言

Python凭借其开发效率高和功能强大的特性,在众多编程语言中脱颖而出,成为大数据时代的分析利器。
据我多年的领悟,编程语言只是一种按照人的意图去实现特定功能的高效工具而已,程序化所实现的核心决策功能依然需要人工智慧来支撑,在量化投资交易领域,投资者所思考的交易逻辑是非常重要,正所谓重剑无锋,大巧不工 (真正的剑技不是要依靠剑锋,而是个人的修行,投资也是如此,投资者的素养最为重要) ,因此应当把80%的时间与精力放到投资模型构建的思考上,20%的时间与精力放到编程实现上。
即将走上量化投资交易的你,工欲善其事,必先利其器,将Python作为量化投资交易的首选语言,无疑是最为明智的,余生很短,请跟我一起用python!

思路

在量化交易方面,通过计算机程序自动实现股票盯盘与找到买卖信号,应该是很多人都比较向往的吧。但九层之台,起于累土,千里之行,始于足下,只有打下坚实的基础,将各个知识点逐一突破后加以综合运用,才能构建自己完整的量化交易体系。
今天就从量化交易最基础的入门知识点讲起,通过Python程序,编写股票价格实时盯盘的机器人,当股价触发一定的涨幅条件时,自动发送电子邮件或短信通知到投资者,这一场景可运用于平时喜欢炒股,但是没有时间盯盘的股民朋友。
通过该文章的学习,读者可以掌握对证券(包括股票和基金)实时价格的获取、电子邮件发送、程序定时运行和程序打包成exe文件等知识点。

盯盘机器人的工作流程图及效果图

为便于让各位读者从全局观了解整个程序运行的逻辑,特将流程图绘制如下。

1. 程序工作流程图

2. 股价监控的效果

例如: 2021年7月19日,所监控的目标股票三峡能源 (证券交易代码:600905) 因某时点的涨跌幅达到监控水平线,自动触发邮件提醒,通过邮件方式告知投资者当前价格,涨跌幅和盈亏情况等数据,效果如下图所示。

代码实现

1. 需要安装的第三方库及简要介绍

这里首先为大家介绍一下,本文需要用到的若干Python库。
  • Tushare: 一个免费、开源的python财经数据接口包,通过该库的get_realtime_quotes(code)的方法(code为目标证券的交易代码,包括股票和ETF基金的交易代码都可以),可以返回股票的当前报价和成交信息,返回值的数据类型为DataFrame,该DataFram包括name(证券名称),open(今日开盘价),pre_close(昨日收盘价),price(当前价格)...time(时间)等,根据本次需求,仅需要部分维度即可,其他的维度,读者可以自行通过print()打印方式查看所有的维度信息。
  • pandas: 数据分析的核心库,因为调用Tushare库的get_realtime_quotes(code)方法返回DataFrame数据类型,所以需要该库对返回数据进行操作。
  • schedule: 在证券交易中的制度中,有交易和休市时间,要实现程序的定时运行,该库必不可少,详见程序部分对该库用法的介绍。
  • smtplib: 该库主要实现电子邮件的发送。
  • sys: 在交易日的15:00以后已经闭市,为避免资源的浪费,此时可以调用sys.exit()方法实现程序的自动退出。
  • pyinstaller: 用该库可以将程序打包成可执行的exe格式文件,便于程序的运行。
以上所需的第三方库,可以使用pip指令完成安装即可。

2. 程序代码实现

① 编写获取当前证券价格信息的方法
def get_now_jiage(code) :
   df = ts.get_realtime_quotes(code)[['name','price','pre_close','date','time']]
   return df
其中参数code为目标股票的交易代码,例如股票名称为 “三峡能源” 的证券交易代码为“600905”。调用Tushare的get_realtime_quotes(‘600905’)方法,即可返回一个DataFrame类型的数据,根据功能需要,我们只需要获取 name(股票名称) price(当前价格) pre_close(昨日收盘价) date(价格对应的日期) time(价格对应的时间) 即可。
编写好该方法后,主需要传递目标股票的交易代码至 get_now_jiage 方法,即可获取需要的数据。
② 编写判断是否在交易时间段内的方法
在每个交易日,股票交易的时间为 09:30-11:30,13:00-15:00 ,早上9:30程序开始监控,可以通过schedule来实现(后面讲解),在11:30-13:00之间的午间休市时间内,为避免造成资源浪费,就不必调用Tushare接口的数据,该时间段我们可以称为 暂停交易时间 。判断是否在暂停交易时间段的方法编写如下:
def pd_ztjytime():#判断是否是交易时间
    now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    now_datetime = datetime.datetime.strptime(now_time, '%Y-%m-%d %H:%M:%S')
    d1 = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y-%m-%d') + ' 11:30:01''%Y-%m-%d %H:%M:%S')
    d2 = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y-%m-%d') + ' 13:00:00''%Y-%m-%d %H:%M:%S')
    delta1 = (now_datetime - d1).total_seconds()
    delta2 = (d2-now_datetime).total_seconds()
    if delta1>0 and delta2>0 : #在暂停交易的时间内
        return True  #在暂停的交易时间范围内,返回 True
    else:
        return False #不在暂停的交易时间范围内,返回 False
③ 编写监控股价的主体运行程序
该模块作为股价监控与计算涨跌幅,判断是否发送通知的核心程序,为了与早间9:30定时运行程序的模块相配合,故该模块写成独立的方法,完整程序如下:
def do_programe(code):
    if pd_ztjytime()==False#判断是否在暂停交易的时间范围内
        info=get_now_jiage(code) #调用方法获取当前的DataFrame
        now_jiage=float(info['price'][0]) #获取现价
        name=info['name'][0#获取证券名称
        pre_close=float(info['pre_close'][0]) #获取昨日收盘价
        riqi=info['date'][0#获取现价对应的日期
        sj=info['time'][0#获取价格对应的时间
        now_zdie=round((now_jiage-pre_close)/pre_close*100,2#计算现在的涨跌幅
        all_zdie=round((now_jiage-cbj)/cbj*100,2)  #计算股票持有期间内总的涨跌幅,其中cbj为购买时候的成本价,需要约定全局变量
        now_shizhi=round(shuliang*now_jiage,2#计算股票现在的市值,其中shuliang为购买股票的数量,需要约定为全局变量
        ykui=round(now_shizhi-cbj*shuliang,2)  #计算股票现在总的盈亏
        if (abs(now_zdie)>=3 and abs(now_zdie)<3.09or (abs(now_zdie)>=6  and abs(now_zdie)<6.05)  or (abs(now_zdie)>=9 and  abs(now_zdie)<9.1) : #判断现在的涨跌幅是否在目标范围内
            email_comment = []
            email_comment.append('')
            email_comment.append('

您好:'

)
            email_comment.append('

根据设置参数,现将监控到'

+name+'('+str(code)+')的证券价格异动消息汇报如下:')
            email_comment.append(' + color_bg_fg + ' style="border-collapse:collapse">')

            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')

            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('')
            email_comment.append('
序号购买单价持股数现价现涨跌幅总涨跌幅现市值盈亏额异动时间
'+str(1)+''+str(cbj) + '' + str(shuliang) + '' + str(now_jiage) +'' + str(now_zdie) + '%' + str(all_zdie) + '%' + str(now_shizhi) + '元' + str(ykui) + '元' + str(riqi) +' '+str(sj) +'
'
)
            email_comment.append('

祝:股市天天红,日日发大财!

'
)
            email_comment.append('')
            send_msg = '\n'.join(email_comment)
            send_Email(email_add[0], send_msg)
在上述程序中,判断是否发送通知的判断语句为:
if (abs(now_zdie)>=3 and abs(now_zdie)<3.1or (abs(now_zdie)>=6  and abs(now_zdie)<6.1)  or (abs(now_zdie)>=9 and  abs(now_zdie)<9.1
上述if判断语句表示现在涨跌幅的绝对值在3%(含)至3.1%(不含)(使用绝对值可以同时兼顾涨和跌的幅度),或6%(含)至6.1%(不含),或9%(含)至9.1%(不含)之间时,便通过发送电子邮件的形式发送通知,具体的涨跌幅触发参数读者可以自行修改。
电子邮件发送的关键程序为:
send_Email(email_add[0], send_msg)
其中,email_add为列表形式,可以存放多个接收通知的电子邮件地址,此例中仅设置一个接收地址,全局变量email_add=['......'],故获取该地址的程序为email_add[0]。send_msg即为要发送的邮件内容,邮件内容使用列表email_comment进行添加,这里发送的邮件格式为html格式,使用html格式是为了方便绘制表格。 html文件的开头应当是,而结尾则是与之配对的,其中绘制表格的标签是及配对的
,表格行的标签是,而列的标签则是。
发送电子邮件send_Email方法的程序如下:
def send_Email(Email_address, email_text):
    from_addr = '*****' #发出电子邮件的地址
    password = '*****'   #发出电子邮件的密码
    title = '股票价格异动监控消息-' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')  #电子邮件的标题
    msg = MIMEText(email_text, 'html''utf-8'#电子邮件的格式是HTML
    msg['From'] = from_addr
    msg['To'] = Email_address
    msg['Subject'] = title

    try:
        server = smtplib.SMTP_SSL('smtp.qq.com'465)
        server.login(from_addr, password)  # 发送邮件
        server.send_message(msg)
        server.quit()

        # print(Email_address+'  send success!')
        #send_info.append(Email_address + '  send success!\n')
    except Exception as e:
        a+1
        # print(e)
        #send_info.append(e + '\n')
        #send_info.append(Email_address + ' send failed!\n')
        # print(Email_address+' send failed!')
from_addr为发件人的邮箱地址,而password则是发件人的授权码,这里需要根据实际情况进行修改和填写。
另外,程序中的:
server = smtplib.SMTP_SSL('smtp.qq.com', 465)
是选择QQ邮箱的SMTP服务器地址smtp.qq.com,默认端口为465,如果是其他邮箱,则应该进行相应的服务器和端口号进行修改。
如何获取发件人的授权码 呢?以QQ邮箱为例说明:
第一步:登录QQ邮箱,单击顶部的“设置”链接,然后单击“账户”标签,如下图所示。

第二步:在 “账户” 选项卡中向下滚动,直到看到如下图所示的选项,单击 “POP3/SMTP服务” 右侧的“开启”链接,如下图所示。

第三步:单击 “开启” 链接后,会有一个验证密保的过程。按照页面中的说明,向指定号码发送指定内容的手机短信,发送完毕后单击页面中的 “我已发送” 按钮,会弹出一个框,里面就包含SMTP授权码,把它复制并存储起来,方便以后调用。

④ 编写调用do_programe(code)的监控程序
为了实现主体程序的调用,编写run()方法入下所示:
def run():
    while True:
        do_programe('600905')
        now_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        d1 = datetime.datetime.strptime(now_time, '%Y-%m-%d %H:%M:%S')
        d2 = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y-%m-%d')+' 15:00:00''%Y-%m-%d %H:%M:%S')
        delta = d2 - d1
        if delta.total_seconds()<=0:
          sys.exit()
        time.sleep(1)
⑤ 编写每天9点30分开始监控的主程序
为了实现每个交易日交易时点开始监控,需要的程序如下所示:
if __name__ == '__main__':
    schedule.every().day.at("09:30").do(run)
    while True:
        schedule.run_pending()
        time.sleep(1)
⑥ 程序打包与自动运行
当编写完程序以后,就需要通过打包的形式把程序转化为exe格式,该格式下的程序可以点击或者设置自动运行,打包的库是pyinstaller ,使用命令 pyinstaller -w -F 程序路径\程序名.py 即可。其中-w表示生成的exe文件运行时不出现黑色的DOS界面,我们只需要该程序 “悄悄” 在后台运行即可。
为了实现程序在电脑开机的时候自动运行,可以将生成的exe文件复制到windwos系统的Startup文件夹下,点击windows的开始菜单-所有程序,找到“启动”或者“Startup”的文件夹,将exe文件复制到该文件夹内,每次开机,电脑就可以自动运行该监控程序。
因为程序运行不出现任何界面,为了查看程序是否在运行,可以用快捷键 “Ctrl Alt Delete” 的快捷键打开任务管理器,在进程里面可以查看到“股票监控.exe” (这里的文件名是作者改的文件名) 的文件,表明程序在监控中。

展望

该程序只是设置了一只股票来作为简单功能实现的案例,仍然有一定的改进空间,说明如下。
一是在实践中,往往都是构建一个股票池 (数只股票) 来动态监测股价和自动判断交易时点 (比如MACD,均线,KDJ指标等) ,往往需要结合数据库技术,才能便于灵活构造股票池。
二是对于发送短信的功能,本文中并未做介绍,仅介绍了电子邮件,其实短信通知的思路和邮件的思路一致。如果要实现免费发短信功能,读者可以在twilio 网站上 (https://www.twilio.com) 上注册和调用相应功能即可,读者可以再网上搜索。
三是关于Tushare数据接口,本文中用的是Tushare老的接口API,目前官方主要维护的是Tushare Pro接口,相应的调用功能要达到一定的积分才可以,但是相比其他收费接口,Tushare是属于业界的良心之作,关于Tushare Pro,参考的网址详见 https://waditu.com/document/2
四是其他商业的量化接口,可以推荐聚宽量化接口,大约有半年左右的免费试用期,但是免费过后,每个月还是有几千元的收费,读者可选择使用聚宽网址 https://www.joinquant.com/view/community/list?listType=1
五是关于爬虫获取证券交易数据,现在证券交易数据比较丰富的网站有东方财富、同花顺、新浪财经以及和讯网等。通过爬虫也可以获取相应的数据,但是应当注意的是,像本文中每个交易日每秒钟调用一次API,如果用爬虫来实现,就不理想,因为调用太频繁可能触发网站的反爬虫机制。
六是该程序设置的是在本地计算机上自动开机运行,在程序不断优化和增加功能后,感兴趣的读者可以了解购买云服务器部署监控程序。


- EOF -

推荐阅读 点击标题可跳转

1、 20 条非常实用的 Python 代码,建议收藏!

2、 Python 里最具代表性的符号,竟如此强大

3、 写好 Python 代码的几条重要技巧


看完 本文有收获?请分享给更多人

推荐关注「Linux 爱好者」,提升Linux技能

点赞和在看就是最大的支持❤️

0) break; outerWidth += parseFloat(parent_style.paddingLeft) + parseFloat(parent_style.paddingRight) + parseFloat(parent_style.marginLeft) + parseFloat(parent_style.marginRight) + parseFloat(parent_style.borderLeftWidth) + parseFloat(parent_style.borderRightWidth); parent = parent.parentNode; return parent_width; var getOuterW=function(dom){ var style=getComputedStyle(dom), if(!!style){ w = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth); return w; var getOuterH =function(dom){ var style=getComputedStyle(dom), if(!!style){ h = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) + parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); return h; var insertAfter = function(dom,afterDom){ var _p = afterDom.parentNode; if(!_p){ return; if(_p.lastChild === afterDom){ _p.appendChild(dom); }else{ _p.insertBefore(dom,afterDom.nextSibling); var getQuery = function(name,url){ var u = arguments[1] || window.location.search, reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)"), r = u.substr(u.indexOf("\?")+1).match(reg); return r!=null?r[2]:""; function setImgSize(item, widthNum, widthUnit, ratio, breakParentWidth) { setTimeout(function () { var img_padding_border = getOuterW(item) || 0; var img_padding_border_top_bottom = getOuterH(item) || 0; if (widthNum > getParentWidth(item) && !breakParentWidth) { widthNum = getParentWidth(item); height = (widthNum - img_padding_border) * ratio + img_padding_border_top_bottom; if (isIE || '0' === '1') { var url = item.getAttribute('data-src'); item.src = url; } else { if(parseFloat(widthNum, 10) > 40 && height > 40 && breakParentWidth) { item.className += ' img_loading'; item.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=="; widthNum !== 'auto' && (item.style.cssText += ";width: " + widthNum + widthUnit + " !important;"); widthNum !== 'auto' && (item.style.cssText += ";height: " + height + widthUnit + " !important;"); }, 10); (function(){ var images = document.getElementsByTagName('img'); var length = images.length; var max_width = getMaxWith(); for (var i = 0; i < length; ++i) { if (window.__second_open__ && images[i].getAttribute('__sec_open_place_holder__')) { continue; var imageItem = images[i]; var src_ = imageItem.getAttribute('data-src'); var realSrc = imageItem.getAttribute('src'); if (!src_ || realSrc) continue; var originWidth = imageItem.getAttribute('data-w'); var ratio_ = 1 * imageItem.getAttribute('data-ratio'); var height = 100; if (ratio_ && ratio_ > 0) { var parent_width = getParentWidth(imageItem) || max_width; var initWidth = imageItem.style.width || imageItem.getAttribute('width') || originWidth || parent_width; initWidth = parseFloat(initWidth, 10) > max_width ? max_width : initWidth; if (initWidth) { imageItem.setAttribute('_width', !isNaN(initWidth * 1) ? initWidth + 'px' : initWidth); if (typeof initWidth === 'string' && initWidth.indexOf('%') !== -1) { initWidth = parseFloat(initWidth.replace('%', ''), 10) / 100 * parent_width; if (initWidth === 'auto') { initWidth = originWidth; var widthNum; var widthUnit; if (initWidth === 'auto') { widthNum = 'auto'; } else { var res = /^(\d+(?:\.\d+)?)([a-zA-Z%]+)?$/.exec(initWidth); widthNum = res && res.length >= 2 ? res[1] : 0; widthUnit = res && res.length >= 3 && res[2] ? res[2] : 'px'; setImgSize(imageItem, widthNum, widthUnit, ratio_, true); (function (item, widthNumber, unit, ratio) { setTimeout(function () { setImgSize(item, widthNumber, unit, ratio, false); })(imageItem, widthNum, widthUnit, ratio_); } else { imageItem.style.cssText += ";visibility: hidden !important;"; })(); window.__videoDefaultRatio=16/9; window.__getVideoWh = function(dom){ var max_width = getMaxWith(), width = max_width, ratio_ = dom.getAttribute('data-ratio')*1, arr = [4/3, 16/9], ret = arr[0], abs = Math.abs(ret - ratio_); if (!ratio_) { if (dom.getAttribute("data-mpvid")) { ratio_ = 16/9; } else { ratio_ = 4/3; } else { for (var j = 1, jl = arr.length; j < jl; j++) { var _abs = Math.abs(arr[j] - ratio_); if (_abs < abs) { abs = _abs; ret = arr[j]; ratio_ = ret; var parent_width = getParentWidth(dom)||max_width, width = width > parent_width ? parent_width : width, outerW = getOuterW(dom)||0, outerH = getOuterH(dom)||0, videoW = width - outerW, videoH = videoW/ratio_, speedDotH = 12, height = videoH + outerH + speedDotH; return {w:Math.ceil(width),h:Math.ceil(height),vh:videoH,vw:videoW,ratio:ratio_,sdh: speedDotH}; (function(){ var iframe = document.getElementsByTagName('iframe'); for (var i=0,il=iframe.length;i = 200 && xhr.status < 400 ){ obj.success && obj.success(xhr.responseText); } else { obj.error && obj.error(xhr); obj.complete && obj.complete(); obj.complete = null; xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.send(data); var mid = "2666556552" || ""|| ""; var biz = "MzAxODI5ODMwOA=="||""; var sessionid = ""||"svr_756cd625229"; var idx = "3"; (function sendReq(parentNode, copyIframe, index) { ajax({ url: '/mp/videoplayer?vid=' + vid + '&mid=' + mid + '&idx=1&__biz=' + biz + '&sessionid=' + sessionid + '&f=json', type: "GET", dataType: 'json', success: function (json) { var ret = JSON.parse(json || '{}'); var ori = ret.ori_status; var hit_biz_headimg = ret.hit_biz_headimg + '/64'; var hit_nickname = ret.hit_nickname; var hit_username = ret.hit_username; var selfUserName = "gh_9f1efcd6f4ab"; if (ori === 2 && selfUserName !== hit_username) { var videoBar = document.createElement('div'); var videoBarHtml = '