EDIT:今儿市场比较平静,来更新一下。
EDIT2:继续更新。今天看到新闻说2sigma的Kang Gao认罪了,唉。
EDIT3:连Brexit都顶得住,大家的情绪还是很乐观的么!更新一下……
EDIT4:无论谁上台,全球化的趋势都快是要停了的样子,趁着日子还好,更新一下……
EDIT5:每天更新一点的Piece meal确实不太好,我已经尽力抽空写了……很后悔一开始的时候没有用匿名的模式来写,否则我就可以写更多东西出来了。
1. 断线重发指令
FIX连上了,发送第一个交易指令,开始等待-->这时网络断了,没收到确认回执-->等待超时,心跳还没超时-->重发交易指令,却错误地用了新的订单号,网络是好的。
这种概率bug真的很给力。很多新手喜欢用类似quickfix之类的包,一个不小心就出问题了。一般来说这些FIX连接走的都是site-to-site vpn 或者 co-location的光缆,断网的概率不是很高,但遇到潮涌的时候还是会丢包(例如你同时对几千个tickers做市),恰好碰上了就好玩了。
2. FIX夏令时
FIX常用格林威治时间报时。夏令时冬令时切换的那些天总有人迟到早退。下次夏令时切换是2015年3月1日,设定一个备忘吧。
3. Book pressure 0
有些品种在奇怪的时候,例如圣诞节的最后半天,bid 1 - bid 5 都只有价格,量为0(也能是iceberg,规则天天变,举个例子而已)。熊孩子实习生恰好写了个作死的algo看ask size / bid size。回测的时候用的数据都清理过的,没这么奇怪的玩意儿,于是这段代码没有try catch numerical error就上线了。
暂时先加俩,有空再回来写……回来了。
4 迭代次数过多
有些algo需要解几个凸优化或者线性规划问题来决策,这类策略往往是看分钟bar data的。所以每次画一个新的分钟k线,某些函数就会被唤醒起来重新解一下。有时候某些数字比较奇怪,例如penny stocks 太多的时候,求解器就容易陷入迭代的漩涡中不能自拔,连续报错说自己遇到的矩阵非正定什么的。当它想独自安静一下时,下一个bar恰好到了……
两个bar之前往往不会相差太多,毕竟我们也不是每分钟都看得到惊心动魄的走势的。于是这个求解器又陷入一次超过1分钟的沉思。
想象一下,这个过程发生在下午3:57,求解器连续三次迭代次数过多,系统里还有几百个股票需要平仓,最后一分钟风控切进来直接开始mkt order fire sell。
5 数据源断了一半
比速度比到后面,一堆码农就喜欢用奇技淫巧。例如先来十几个线程接受各大交易所数据,弄好了放在一个共享内存里;十几个线程从这个共享内存里面读市场数据,算好决策,单子打过去。
就是有这么一两个交易所总是喜欢出小毛病,恰好它们交易的品种是独占的。
例如direct edge的数据断了,bats的还在,bats的标普500etf在变动,de的es期货没有更新了。如果代码里面没有自动清除过去数据的函数,或者有,但十几个线程又是共享同一个last updated timestamp,bats没断导致这个值一直不触发系统警报……
假如只是简单的股票做市还好,顶多付了spread。如果某个策略做的是vol arb,买卖期权那可就亏大了。
6 Excel的错误代号
我发现还是有蛮多人喜欢用Excel来做前端管理的,给头寸设置各种字体、颜色看着自己爽一些。“现代化”的Excel工具都会直接把绝大多数的工作负荷交给DLL,表格里面的vba只是简单地传递参数和结果而已。VBA里面有个奇怪的函数叫做CVErr,负责把一个错误代号转换成Excel里面的错误,例如CVErr(2015) = #VALUE!,而2015本身又有一个别名,叫做xlErrValue。
在VBA里面,这个xlErrValue是可以参与计算的,你要是把这个玩意儿传到dll里面就有意思了。
print CVErr(2015) Error 2015 print CVErr(xlErrValue) Error 2015 print xlErrValue + 1 2016 print sqr(xlErrValue) 44.8887513749269
明白了吧。
这个页面里面有更多信息:
How to: Access DLLs in Excel7 重码
这个问题完全和代码的逻辑错误关系不大,只和维护人员有关。
我见过某个强大的api,一开始开发的时候考虑的全是美国境内的股票,股票代码只保留了最短的长度,没想到某天扩张开了业务,在其他市场上遇到重码了,CWB既是iShares的可转债etf,也可以是一只加拿大的股票。
老板说:你们把find_instrument的函数重新整理一下,让客户可以手动限制查找的市场和资产类别。
这个活自然就给了实习生咯。实习生看了一眼CWB是可转债啊,重码的又是加拿大西部银行Canadian Western Bank::TSE,行,那CWB的资产类别就是债券,加拿大西部银行的资产类别就是股票。
为了find_instrument函数的向后兼容性,开发人员把这个函数拓展了:
# 之前 def find_instrument(ticker) # 业务需求,拓展了交易所和类别的代码 def find_instrument(ticker, exchanges = "", asset_class = "")
具体里面的逻辑是怎么样的我就不知道了。当时新系统上线第一天,整个村子的人,尤其做etf套利的,活生生把多伦多的这个银行推上了天。
8 爬虫
有些团队喜欢省钱,就不买市场日历的api了,自己动手写一个爬虫,从yahoo finance上抓股票的财务数据发布、股东会日期等数据。
今天Yahoo Finance改版,估计也是害惨了一堆爬虫……富国WFC今天公布盈利数据吧好像是(2016年7月15日),反正我已经听说了好几个抱怨了。
写过爬虫的都知道……有时候页面就是找不到这些元素,返回空集就好了。
返回空集,嗯。
9 微波塔弱弱的payload
这里的细节都不是真的。我只能最大限度地还原一些背景知识。
考虑一个微波塔通信协议,微波塔的消息都比较短,所以每个消息的payload(有效负载)是有限的,现在我们想在一个packet里面塞进整个市场的盘口信息(卖一,量,买一,量,卖二,量,买二,量……),显然不做一点压缩是不可能的。
假设这个是利率市场。
要开始压缩了,盘口的量,超过一千以上的值,例如1342,后面的342没什么意义,显示成1000+也可以。压缩一点。
再看如何把利率的小数点给弄掉?反正交易所要求有一个最小变动单位(Tick Size),例如1/32。把报价除以tick size,得到的肯定是一个长整。
然后考虑利率市场的特性,利率这东西本来交易范围就有限,我们完全不用考虑利率是年化90%的情况,所以完全的一个长整又是用不上的,可以改成8 bit integer。
每天早上微波塔初始化的时候,发送一个包,定义当天的128是什么利率,然后1就是这个利率 - 127 * tick size,255就是这个利率 + 127 * tick size,0留作他用。255个tick size,过去十年,应对任何一天的也是够的。
人呐就是不知道,自己不可以预料。瑞士国家银行宣布取消保底汇率。微波塔卒。
10 硬代码 Hard coded
今天(2016年8月9日)是VXX,跟踪短期波动率指数的ETF,四股合一股的生效日(reverse split),随着这个合股,VXX的cusip码也变了。
你猜我看到了多少尝试用旧cusip码来发送交易指令,被对手方拒绝的单子?
所谓的不好的开发习惯在这种关键的时候就显得特别重要,据我所知是有一些小策略的脚本或者源代码,并不需要同时获知几千个股票的cusip的,那么硬代码写几个能用上的cusip到一个dictionary里面也无可厚非。但假如这时候系统在线上,真是改都没有机会改。
好歹也写在一个文本文件或者json里面吧。
11 轮询
老员工A发掘了一个看上去有潜力的策略,在数据库里面回测发现挺能赚钱的。成功了之后把逻辑和数据库使用的代码交给了实习生,让其整理上线。
实习生写出来的代码小规模上线,实盘表现是亏钱的。老员工觉得不对,自己整理上线了一个版本,实盘表现终于正常,赚钱的效率在预想的范围内。老员工有点郁闷,怎么会这样呢,对着实习生的代码一行行找差异,数学上显然是完全没出入的。老员工下载了实际交割的数据来看,发现实习生的版本,在AA,AAPL,BA这些股票上还是赚钱的,到了JPM附近的ticker就开始不对了,再下去基本就是亏的。
原来问题居然出自这一行:
for i in stocks: process(i)
stocks变量恰好是一个按字母排序的列表。每个处理过程都需要一些时间不可忽略,轮询到后面的时候,快照下来的数据就已经失效了。市场的竞争已经很有效率,一个股票的波动会很快传染到同行业、上下游股票上去。而实习生从A开始一直试到Z,代码效率还不是最高的话,很难赚到钱。
代码改成了一个多线程分发的版本之后就开始对起来了。过了一段时间老员工回来看结果,发现每个ticker上的pnl还是和lexical order有关,老员工觉得这里肯定还是有人为的因素在里面,考虑到还有这么多用excel做前端的手动交易员。
老员工把代码改成了这样:
r_stocks = stocks[::-1] for i in r_stocks: process(i)
开了一个小账户试试手气,他惊奇地发现这么个实现,Sharpe居然比多线程分发的要高0.1。
市场还是很有意思的。
12 规则生成、节假日、新合约,看上去就是个概率bug
背景知识:8月31日起,SPY,也就是著名的标普500指数ETF,开始有周三到期的期权了。以前只有每个周五到期的期权,现在每个周三到期的期权也有了,所以既有9月14日到期的期权,也有9月16日到期的期权。
因为行权价和到期日的繁多,期权合约是不会分配CUSIP之类的序列号的,所以在各种电子化交易的协议里面,对交易合约的定义一般而言有两种方式:
第一种就是明确分离的字典:
{ "action": "BUY", "quantity": 10, "price": "MKT!", "ticker": "SPY", "expiration_month": 9, "expiration_day": 14, "strike": 211.00, "direction": "CALL" }
第二种呢原理也一样,就是把多个字典项目合并成一个超长的字符串:
{ "action": "BUY", "quantity": "10", "price": "MKT!", "contract": "SPY 20160914C00211000" }
一般而言,这里说的到期日周五是不严格的时间概念,周五到期的期权,大多数指的是周五收盘4pm结算。如果周五恰好是节假日,那就是周四4pm结算,如果周五是半天交易日,那就是周五1pm结算。如果周四周五都是节假日,周三4pm结算。
在周三期权推出之前,一个声明自己周五到期的期权,事实上真的可以在周三下午到期。
9月1日,周三期权开始交易。
就目前来看,因为彭博和路透等数据终端的影响,大家都喜欢用一个长字符来解决问题。于是某个非常有名气的券商就在9月1日遇到了如下的bug,客户在平台上交易周三的期权(9/14)时,有一部分单子会顺利成交,另一部分单子会一直显示live (挂在盘面的队列上),这时候客户就算把价格抬高或降低到best bid or offer,已经cross the market了,还是不会成交。撤销后重发,有可能成交。
这个bug大概影响了生产环境三个小时,对系统声誉略有影响。因为是概率bug,除bug的过程极其复杂。最后发现是这个原因:
假设现在你是券商,你拿到了SPY 20160914C00211000这么个长字符,你的翻译器会怎么办?
恰好你的智能交易引擎没停工,为了获得最好的价格,你帮客户把一个单子拆分成好多个,发到了不同的交易所去。使用方式2的交易所没有受到影响,使用方式1的交易所接收到你的交易指令,把你的指令放入了周五到期的期权队列里,周五的期权价格和周三不一样(得差不少钱)。所以你就算调整价格高低一两分钱,也是无法成交的。
撤销单子之后重发,交易指令有一定的概率不需要转译就到达方式2交易所,形成了概率bug的错觉。
回头想想:这个bug到底牵涉了多少不同的部门?当初写出这个规则翻译器的IT员工万万没想到几年之后会出现这么一个神奇的周三期权,当年的节假日豁免匹配规则太懒惰,把客户的输入直接标准化成周五,这么多年相安无事。交易部门的员工知道有周三期权推出,却不懂码农的心,不大可能知道周五日期标准化意味着什么,不会想到原来这地方也能有坑,自然不会督促IT哥们修改翻译器。处理网络connectivity的员工啥都不知道,只能埋头看log,看到一个SPY 20160914C00211000这个字符串,根本不会想到待会儿这字符串会有一个4变成一个6。
Teamwork是一个艺术活。