主题：金融投资讨论
问题：在实盘交易中写的哪类代码 bug 产生了严重亏损？
回答：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期权在十几个交易所都能有交易。部分交易所的对接协议要求券商使用第一种方式定义交易指令，部分交易所要求第二种方式券商在自己的服务器上写了一个翻译器这个翻译器的生成规则没有被更新假设现在你是券商，你拿到了SPY 20160914C00211000这么个长字符，你的翻译器会怎么办？当然先把spy提取出来，赋值给ticker。把日期按照yyyymmdd分开，发现是个周三，直接假设周四周五是假日，按照传统填写周五的日期： expiration_month=9, expiration_day=16处理剩下的C=CALL和00211000 = 211.00恰好你的智能交易引擎没停工，为了获得最好的价格，你帮客户把一个单子拆分成好多个，发到了不同的交易所去。使用方式2的交易所没有受到影响，使用方式1的交易所接收到你的交易指令，把你的指令放入了周五到期的期权队列里，周五的期权价格和周三不一样（得差不少钱）。所以你就算调整价格高低一两分钱，也是无法成交的。撤销单子之后重发，交易指令有一定的概率不需要转译就到达方式2交易所，形成了概率bug的错觉。回头想想：这个bug到底牵涉了多少不