集合竞价是电子撮合交易中的重要撮合方式,通常用来在开盘或者收盘时产生开盘价或者收盘价,或者对于某些流动性差的产品,通过一段时间集中进行撮合,找出能产生最大成交量的价格的方式,(即市场大多数人认可的价格)防止价格被不小心操纵。
中国大陆市场中,由于人口众多,流动性几乎从不缺乏,所以从一开始就是采用把集合竞价生成开盘价和连续竞价高效撮合组合在一体的方式。具体上,沪深交易所都是以集合竞价来场开盘价和收盘价,在收盘价上,如果集合竞价不能产生收盘价,则采用最后一分钟加权平均价(上交所最开始的收盘价是使用的1分钟均价,后来改成了也采用集合竞价的方式产生)。本文对沪深交易所的开盘集合竞价算法作了简单理解和实现。
法规
开盘集合竞价的规则,在上海证券交易所的网站上能找到,在上海证券交易所交易规则(2023年修订)中,与集合竞价有关的规则有:
3.4.1 本所接受交易参与人竞价交易申报的时间为每个交易日9:15至 9:25、9:30至11:30 、13:00至15:00。
每个交易日9:20至9:25的开盘集合竞价阶段、14:57至15:00的收盘集合竞价阶段,本所交易主机不接受撤单申报;其他接受交易申报的时间内,未成交申报可以撤销。撤销指令经本所交易主机确认方为有效。3.4.1 证券竞价交易采用集合竞价和连续竞价两种方式。
集合竞价是指在规定时间内接受的买卖申报一次性集中撮合的竞价方式。
连续竞价是指对买卖申报逐笔连续撮合的竞价方式。
3.4.2 当前竞价交易阶段未成交的买卖申报,自动进入当日后续竞价交易阶段。3.5.1 证券竞价交易按价格优先、时间优先的原则撮合成交。
成交时价格优先的原则为:较高价格买入申报优先于较低价格买入申报,较低价格卖出申报优先于较高价格卖出申报。
成交时时间优先的原则为:买卖方向、价格相同的,先申报者优先于后申报者。先后顺序按交易主机接受申报的时间确定。
3.5.2 集合竞价时,成交价格的确定原则为:
(一)可实现最大成交量的价格;
(二)高于该价格的买入申报与低于该价格的卖出申报全部成交的价格;
(三)与该价格相同的买方或卖方至少有一方全部成交的价格。
两个以上申报价格符合上述条件的,使未成交量最小的申报价格为成交价格;仍有两个以上使未成交量最小的申报价格符合上述条件的,其中间价为成交价格。
集合竞价的所有交易以同一价格成交。
深交所的规则在其官网上也有深圳证券交易所交易规则(2023年修订),集合竞价相关的规则也类似:
3.4.1 证券竞价交易采用集合竞价和连续竞价两种方式。
集合竞价,是指对一段时间内接受的买卖申报一次性集中撮合的竞价方式。
连续竞价,是指对买卖申报逐笔连续撮合的竞价方式。3.4.2 证券竞价交易按价格优先、时间优先的原则撮合成交。
价格优先的原则为:较高价格买入申报优先于较低价格买入申报,较低价格卖出申报优先于较高价格卖出申报。
时间优先的原则为:买卖方向、价格相同的,先申报者优先于后申报者。先后顺序按交易主机接受申报的时间确定。3.4.3 集合竞价时,成交价的确定原则为:
(一)可实现最大成交量;
(二)高于该价格的买入申报与低于该价格的卖出申报全部成交;
(三)与该价格相同的买方或卖方至少有一方全部成交。两个以上价格符合上述条件的,取在该价格以上的买入申报累计数量与在该价格以下的卖出申报累计数量之差最小的价格为成交价;买卖申报累计数量之差仍存在相等情况的,开盘集合竞价时取最接近即时行情显示的前收盘价的价格为成交价,盘中、收盘集合竞价时取最接近最近成交价的价格为成交价。
集合竞价的所有交易以同一价格成交。3.4.4 连续竞价时,成交价的确定原则为:
(一)最高买入申报与最低卖出申报价格相同,以该价格为成交价;
(二)买入申报价格高于集中申报簿当时最低卖出申报价格时,以集中申报簿当时的最低卖出申报价格为成交价;
(三)卖出申报价格低于集中申报簿当时最高买入申报价格时,以集中申报簿当时的最高买入申报价格为成交价。
比较重要的点就是上交所的3.5.1和3.5.2和深交所的3.4.2和3.4.3。简单来说,就是在开盘集合竞价期间9:15:00~9:25:00(不包含),可以放出委托,其中9:15:00~9:20:00的委托可以撤单,9:20:00~9:25:00之间的委托不可撤单。将所有这些委托(订单薄),按照买卖方向分类,买单按照从高到低排序(较高的买入价格申报优先于较低的买入申报),卖单按照从低到高排序(较低的卖出申报优先于较高的卖出申报),这就是价格优先。如果有多个买单价格相同,则先申报的优先于后申报的,这就是时间优先。
算法
我们这里只讨论集合竞价算法如何产生满足最大交易量的价格,不涉及到具体的订单簿生成,所以对订单簿结构进行了简化(真正的订单簿结构可能是2个有序的LinkList,买卖方各一个,每一个Node都是买卖方向、价格相同的列表,列表里面按照委托时间从早到晚排序)。我这里以2021-09-01日300033这只股票的逐比委托数据为例来说明:
从9:15:00~9:15:11之间,将所有的申报的订单薄信息,按照买卖两个方向,最后按照价格和数量统计后,得到如下买卖方向的委托价格和数量:
真实情况下,买卖方向不止10档,这里只列出来10档。每一档包含价格和数量。
第一步:首先计算每一档价格的总买单和总卖单。
-
- 先看左边BidList,统计每一价格档位的总买单。
138.85的买入总买单有4手
138.84的买入总买单有4+10=14手(以138.85的那4手买入当然同意以更低的价格买)
138的买入总买单有1+14=15手
依次类推。 - 再看右边AskList,统计每一价格档位的总卖单。
92.57的卖出总卖单有3手
109.93的卖出总卖单3+8=11手(以92.57的那3手买单,当然同意一更高的价格卖)
113的卖出总卖单1+11=12手
依次类推。 - 最后得到下面表格:
- 先看左边BidList,统计每一价格档位的总买单。
第二步:以买入价为虚拟成交价,计算匹配量和未匹配量。
-
- 买一大于等于卖一,才可以匹配。否则,无法匹配,直接返回。
- 匹配量的计算方法为,买入一侧:买入档位中某一档价格对应的总数量;卖出一侧为,卖出十档价格里面小于等于买入价格的最大值,对应的总数量。比如以买一价格成交,匹配价格为138.85,买入一方总数量为4;卖出方小于匹配价格138.52的最大档为卖十(可能不止卖10,取小于138.85价格的最大的值,这里简化),对应的总数量为35,所以若以138.85价格匹配成交,则匹配量为Min(4,35)=4,未匹配量为35-4=31,未匹配量在卖一侧。
- 依照该计算方法,统计买一到买十的每档匹配量,得到如下表:
- 可以看到匹配量那一列,理论上这个匹配量应该是先单调递增,然后再单调递减,所以要找到最大匹配量,只需要找到第一个递减的匹配量即可停止。
- 这里需要记录下最大匹配量时的成交价格,匹配数量,未匹配数量。
第三步:按照第二步的规则,以卖出价为虚拟成交价,计算匹配量和未匹配量。
-
- 卖一小于等于买一,才可以匹配,否则无法匹配,直接返回。
- 匹配量的计算方法为,卖出一侧,卖出档位中某一档价格对应的总数量;买入一侧为,买入十档里面价格大于等于卖出价格的最小值。比如以卖五价格成交,匹配卖方价格为115.71,卖出以一方的总数量为16;买入方大于等于匹配价格115.71的最大档为买十,对应的总数量为42,所以若以115.71的价格匹配成交,则匹配量为Min(42,16)=16,未匹配量为42-16=26,未匹配量在买一侧。
- 按照该计算方法,统计卖一到卖十的每档匹配量,得到如下表:
第四步:从所有的匹配量中,找到最大匹配量,并同时满足,以该价格成交时,成交价格一侧完全成交。
-
- 从上表中,可以看到,从买方十档中,可以看到最大匹配量出现在以买8价格116.52成交,最大匹配量为28,未匹配量为2
,在买一侧,表示买8没有完全成交,还剩2手,这里的成交价应该是116.38,因为此时卖七价116.38完全成交。 - 从卖方十档中,可以看到最大匹配量出现在以卖7价格116.38成交,最大匹配量为28,未匹配量为2
,在买一则,表示以卖7价格成交,卖7完全成交,买方一则还有2手未成交。 - 按照规则,存在两个价格116.52和116.38 ,都能实现最大成交量,且剩余未成交量相同,都为2。所以,如果按照上交所的规则,那么成交价就是116.52和116.38两个数取平均值116.45作为集合竞价成交价,在该价位下匹配量为28,未匹配量为2。按照深交所规则(300033确实是深交所股票),要从这些满足相同条件的值中取离昨收价最近的值,查询300033在2021-09-01的昨收,即2021-08-31的收盘价为115.71,这两个数中,116.38相比116.52离115.71更近,所以集合竞价的成交价就是116.38,在该价位下的匹配量为28,未匹配量为2。
- 从上表中,可以看到,从买方十档中,可以看到最大匹配量出现在以买8价格116.52成交,最大匹配量为28,未匹配量为2
第五步:特殊情况的处理,我们还是回到3.6.2中,可以看到,必须要满足三个条件。就是匹配量最大,按照该价格匹配时,买卖至少有一方能够全部成交。如果有多个价格都满足该条件,能使得匹配量最大,且全部成交,那么:
-
- 取未匹配量最小的作为成交价格,比如举个例子,如果买1和卖1都能达到最大匹配量,但是买1达到最大匹配量时,卖方的未匹配量较小,那么就以买1价格成交。
- 如果未匹配量也相同,则取中间价格即平均价作为开盘价(上交所)或者取离昨收最近的价格为开盘价(深交所)。比如假设集合竞价只有买一和卖一这两笔委托,买一报138.85,卖一报92.57,且量一样,那么如果按照买一价138.85和卖一价92.57得到的匹配量和未匹配量都相等,那么就以(138.85+92.57)/2=115.71 的价格作为匹配价格。
第六步:结果处理。
-
- 得到匹配价格、匹配量、未匹配数量、未匹配数量所在的方向之后,就可以拼接数据了。方法是,卖一买一价格和数量都是匹配价格和匹配量,如果未匹配数量在买方一则,那么买二价格为0,买二量为未匹配量,卖二价格和数量都为0;否则卖二价格为0,卖二数量为未匹配量,买二价格和数量都为0。
- 剩下的买三到买十,卖三到卖十,就把原买十档中,价格小于匹配价格的档位往买三和买十填充。原卖十档中,价格大于匹配价格的档位往卖三和卖十填充,最后得到类似这样的图:
实现
按照算法,实现起来也比较简单。首先定义一个两个实体,CallAuctionLv2DetailInfo用来保存第一步中各价位的总买单和总卖单:
public class CallAuctionLv2DetailInfo
{
public double Price { get; set; }
public int Size { get; set; }
public int TotalSize { get; set; }
}
public class CallAuctionMatchInfo
{
public double MatachedPrice { get; set; }//匹配价格
public int MatchedSize { get; set; }//匹配数量,不是必须
public int UnMatchedSize { get; set; }//未匹配数量
public bool MatchedPriceAtBid { get; set; }//匹配价格是否在买方
}
然后定义一个类CallAuctionFastMarketData专门用来计算实时的模拟集合竞价结果,
public class CallAuctionFastMarketData
{
//买卖十档的价格,数量和总数量
private List<CallAuctionLv2DetailInfo> bidList = new List<CallAuctionLv2DetailInfo>();
private List<CallAuctionLv2DetailInfo> askList = new List<CallAuctionLv2DetailInfo>();
//所有满足最大匹配量的匹配信息
private List<CallAuctionMatchInfo> matchedInfos = new List<CallAuctionMatchInfo>();
public MarketData Update(FastMarketData fmd)
{
Init();
//计算总买卖单
CalculateTotalSize(fmd);
//计算最大匹配量
CalculateMaxMatch();
//拼接结果,买卖一档显示匹配价和匹配量,买卖二档显示 未匹配量,剩下的显示当前小于匹配价的买单,和大于匹配价的卖单价格及当前的数量
return FillResult(fmd);
}
private void Init()
{
bidList.Clear();
askList.Clear();
matchedInfos.Clear();
}
}
使用起来很简单,直接实例化一个CallAuctionFastMarketData对象,然后调用Update方法,传入买卖十档的价格和委托数量信息即可。在Update方法中,首先调用初始化方法Init(),将一些集合置为空。
然后就是第一步,根据原始的买卖十档委托价格和数量计算该档位的总买卖数量。CalculateTotalSize方法如下:
private void CalculateTotalSize(FastMarketData fmd)
{
LinkedListNode<Level2DetailInfo> node = fmd.BidList.First;
while (null != node)
{
if (bidList.Count > 0)
{
bidList.Add(new CallAuctionLv2DetailInfo
{
Price = node.Value.Price,
Size = node.Value.Size,
TotalSize = node.Value.Size + bidList[bidList.Count - 1].TotalSize
});
}
else
{
bidList.Add(new CallAuctionLv2DetailInfo { Price = node.Value.Price, Size = node.Value.Size, TotalSize = node.Value.Size });
}
node = node.Next;
}
node = fmd.AskList.First;
while (null != node)
{
if (askList.Count > 0)
{
askList.Add(new CallAuctionLv2DetailInfo
{
Price = node.Value.Price,
Size = node.Value.Size,
TotalSize = node.Value.Size + askList[askList.Count - 1].TotalSize
});
}
else
{
askList.Add(new CallAuctionLv2DetailInfo { Price = node.Value.Price, Size = node.Value.Size, TotalSize = node.Value.Size });
}
node = node.Next;
}
}
非常简单,TotalSize只需要把当前的量加上前一个价格的总量即可,结果保存在了bidList和askList两个有序集合中。注意,这里的两个集合是有顺序的,bidList是按照从大到小排列的,askList是按照从小到大排列,以为传进来的FastMarketData的BidList和AskList就是这个顺序。
第二步就是计算最大匹配量,这一步就是实现算法里面的第二、三、四四个步骤。
private void CalculateMaxMatch()
{
int maxMatchedSize = int.MinValue;
for (int i = 0; i < bidList.Count; i++)
{
CallAuctionLv2DetailInfo bid = bidList[i];
int askMatchIndex = GetLatestMaxValue(bid.Price, askList);
if (askMatchIndex > -1)
{
//匹配数量
int matchedSize = Math.Min(bid.TotalSize, askList[askMatchIndex].TotalSize);
//未匹配数量
int unMatchedSize = Math.Abs((bid.TotalSize - askList[askMatchIndex].TotalSize));
if (matchedSize >= maxMatchedSize)
{
//当前方是否全部成交
if (matchedSize > maxMatchedSize)
{
matchedInfos.Clear();
}
maxMatchedSize = matchedSize;
matchedInfos.Add(new CallAuctionMatchInfo
{
MatachedPrice = bid.Price,
MatchedPriceAtBid = true,
MatchedSize = matchedSize,
UnMatchedSize = unMatchedSize
});
}
else
{
break;
}
}
}
int askMaxMatchedSize = int.MinValue;
for (int i = 0; i < askList.Count; i++)
{
CallAuctionLv2DetailInfo ask = askList[i];
int bidMatchIndex = GetLatestMinValue(ask.Price, bidList);
if (bidMatchIndex > -1)
{
//匹配数量
int matchedSize = Math.Min(ask.TotalSize, bidList[bidMatchIndex].TotalSize);
//未匹配数量
int unMatchedSize = Math.Abs((ask.TotalSize - bidList[bidMatchIndex].TotalSize));
if (matchedSize >= maxMatchedSize)
{
if (matchedSize > maxMatchedSize)
{
matchedInfos.Clear();
}
maxMatchedSize = matchedSize;
matchedInfos.Add(new CallAuctionMatchInfo
{
MatachedPrice = ask.Price,
MatchedPriceAtBid = false,
MatchedSize = matchedSize,
UnMatchedSize = unMatchedSize
});
}
if (matchedSize >= askMaxMatchedSize)
{
askMaxMatchedSize = matchedSize;
}
else
{
break;
}
}
}
}
首先计算买方十档的最大匹配量的匹配价格,匹配量和未匹配量,然后计算卖方的最大匹配量的匹配价格,匹配量和未匹配量。这里有两个优化的点。
- 在找出匹配价格对应的档位的时候,因为数组序列是有序的,所以应用了二分查找法,这提现在GetLatestMaxValue和GetLatestMinValue中,在以买方价格匹配时,要从从小到大的有序数组askList中,找到小于买方价格的最大值,这个方法就是GetLatestMaxValue。在以卖方价格匹配时,要从从大到小的有序数组bidList中,找到大于卖方价格的最小值,这个方法就是GetLatestMinValue,通过二分查找,可以将复杂度从O(n)降为O(log(n))。原先的方法为:
private int GetLatestMaxValue(double target, List<CallAuctionLv2DetailInfo> autions) { int result = 0; double maxValue = 0; bool haveMatched = false; for (int j = 0; j < autions.Count; j++) { if (autions[j].Price < target) { haveMatched = true; if (autions[j].Price > maxValue) { maxValue = autions[result].Price; result = j; } } else if (autions[j].Price == target) { haveMatched = true; maxValue = autions[j].Price; result = j; } } if (!haveMatched) { result = -1; } return result; } /// <summary> /// 从从大到小的数组中,找出大于target的最小值 /// </summary> /// <param name="price"></param> /// <param name="autions"></param> /// <returns></returns> private int GetLatestMinValue(double target, List<CallAuctionLv2DetailInfo> autions) { int result = 0; double minValue = Double.MaxValue; bool haveMatched = false; for (int j = 0; j < autions.Count; j++) { if (autions[j].Price > target) { haveMatched = true; if (autions[j].Price < minValue) { minValue = autions[j].Price; result = j; } } else if (autions[j].Price == target) { haveMatched = true; minValue = autions[j].Price; result = j; } } if (!haveMatched) { result = -1; } return result; }
优化之后为:
/// <summary> /// 从从大到小的数组中,找出大于target的最小值 /// </summary> /// <param name="price"></param> /// <param name="autions"></param> /// <returns></returns> private int GetLatestMinValue(double target, List<CallAuctionLv2DetailInfo> autions) { int l = 0; int r = autions.Count - 1; while (l < r) { int mid = (l + r + 1) / 2; if (autions[mid].Price >= target) { l = mid; } else { r = mid - 1; } } if (r < 0 || autions[r].Price < target) { return -1; } return r; } /// <summary> /// 从从小到大的数组中,找出小于target的最大值 /// </summary> /// <param name="price"></param> /// <param name="autions"></param> /// <returns></returns> private int GetLatestMaxValue(double target, List<CallAuctionLv2DetailInfo> autions) { int l = 0; int r = autions.Count - 1; while (l < r) { int mid = (l + r + 1) / 2; if (autions[mid].Price <= target) { l = mid; } else { r = mid - 1; } } if (r < 0 || autions[l].Price > target) { return -1; } return l; }
- 买卖方档位可能不止十档,通常情况下可能有20多个档位,以买方为例,从买一最高价开始匹配到最后一档最低价,匹配量通常是先单调递增,然后达到最大值后,匹配量单调递减,所以我们这里计算了最大匹配量,如果遇到某个匹配量开始小于最大匹配量,则跳出循环。这个优化减少了循环的长度。
第四步:在获得了完全匹配且匹配量最大的所有匹配档位信息matchedInfos之后,就要按照规则,找出最小未匹配的价格,最终确认匹配价格,最后然后填充结果。FillResult方法如下:
private MarketData FillResult(FastMarketData fmd)
{
MarketData result;
double matchedPrice = GetMatchedPrice();
if (matchedPrice == 0)
{
return null;
}
int bidMatchedIndex = GetLatestMinValue(matchedPrice, bidList);
if (bidMatchedIndex < 0)
{
//匹配价格错误
return null;
}
int askMatchedIndex = GetLatestMaxValue(matchedPrice, askList);
if (askMatchedIndex < 0)
{
return null;
}
int bidMatchedSize = bidList[bidMatchedIndex].TotalSize;
int askMatchedSize = askList[askMatchedIndex].TotalSize;
int matchedSize = Math.Min(bidMatchedSize, askMatchedSize);
int unMatchedSize = Math.Abs(bidMatchedSize - askMatchedSize);
bool unmatchSizeAtBid = bidMatchedSize > askMatchedSize;
result = new MarketData(fmd.Symbol);
result.PreClose = fmd.PreClose;
result.Open = fmd.Open;
result.High = fmd.High;
result.Low = fmd.Low;
result.CurrentPrice = fmd.CurrentPrice;
result.Volume = fmd.Volume;
result.Turnover = fmd.Turnover;
result.Trades = fmd.Trades;
result.HighLimit = fmd.HighLimit;
result.LowLimit = fmd.LowLimit;
result.TotalBidVol = fmd.TotalBidVol;
result.WeightedAvgBidPrice = fmd.WeightedAvgBidPrice;
result.TotalAskVol = fmd.TotalAskVol;
result.WeightedAvgAskPrice = fmd.WeightedAvgAskPrice;
result.IOPV = fmd.IOPV;
result.Exchange = fmd.Exchange;
result.Type = fmd.Type;
result.Status = fmd.Status;
result.Time = fmd.Time;
result.SetBid(0, matchedPrice, matchedSize);
result.SetAsk(0, matchedPrice, matchedSize);
if (unmatchSizeAtBid)
{
result.SetBid(1, 0, unMatchedSize);
result.SetAsk(1, 0, 0);
}
else
{
result.SetBid(1, 0, 0);
result.SetAsk(1, 0, unMatchedSize);
}
int index = 2;
int bidLevel = Math.Min(bidList.Count, bidMatchedIndex + 9);//从MatchedIndex往后最多填充8档
for (int i = bidMatchedIndex + 1; i < bidLevel; i++)
{
result.SetBid(index++, bidList[i].Price, bidList[i].Size);
}
index = 2;
int askLevel = Math.Min(askList.Count, askMatchedIndex + 9);//从MatchedIndex往后最多填充8档
for (int i = askMatchedIndex + 1; i < askLevel; i++)
{
result.SetAsk(index++, askList[i].Price, askList[i].Size);
}
return result;
}
计算匹配价格的方法GetMatchedPrice如下:
private double GetMatchedPrice()
{
double result = 0;
//最小未匹配量
int minUnMatchedSize = int.MaxValue;
foreach (CallAuctionMatchInfo m in matchedInfos)
{
if (m.UnMatchedSize < minUnMatchedSize)
{
minUnMatchedSize = m.UnMatchedSize;
}
}
//匹配价格
int matchedCount = 0;
foreach (CallAuctionMatchInfo m in matchedInfos)
{
if (m.UnMatchedSize == minUnMatchedSize)
{
result += m.MatachedPrice;
matchedCount += 1;
}
}
//这里仅实现了上交所,取均价;深交所是取价格中离昨收最近的价格
//实际上,需要根据股票所属的交易所来根据那种规则获得开盘价
//实际上,只有深交所的开盘集合竞价模拟才有意思,因为上交所在9:15~9:25集合竞价期间,并没有发送委托数据。它是等到9:25再把委托数据全部发过来。
if (matchedCount > 1)
{
result = Math.Round(result / matchedCount, 2);
}
return result;
}
主要的代码逻辑就写完了,在验证以上逻辑时最好使用单元测试的方法,模拟各种情况。
单元测试
单元测试也比较简单,我这里写了几个case:
[TestClass]
public class CallAuctionFastMarketDataTest
{
[TestMethod]
public void NormalMatchedTest()
{
FastMarketData fmd = new FastMarketData("");
fmd.BidList.AddLast(LevelInfo(138.85, 4));
fmd.BidList.AddLast(LevelInfo(138.84, 10));
fmd.BidList.AddLast(LevelInfo(138, 1));
fmd.BidList.AddLast(LevelInfo(122.56, 3));
fmd.BidList.AddLast(LevelInfo(118.02, 3));
fmd.BidList.AddLast(LevelInfo(117, 1));
fmd.BidList.AddLast(LevelInfo(116.6, 5));
fmd.BidList.AddLast(LevelInfo(116.52, 3));
fmd.BidList.AddLast(LevelInfo(116, 7));
fmd.BidList.AddLast(LevelInfo(115.71, 5));
fmd.AskList.AddLast(LevelInfo(92.57, 3));
fmd.AskList.AddLast(LevelInfo(109.93, 8));
fmd.AskList.AddLast(LevelInfo(113, 1));
fmd.AskList.AddLast(LevelInfo(115.7, 3));
fmd.AskList.AddLast(LevelInfo(115.71, 1));
fmd.AskList.AddLast(LevelInfo(116, 1));
fmd.AskList.AddLast(LevelInfo(116.38, 11));
fmd.AskList.AddLast(LevelInfo(116.7, 1));
fmd.AskList.AddLast(LevelInfo(117.44, 5));
fmd.AskList.AddLast(LevelInfo(117.49, 1));
CallAuctionFastMarketData d = new CallAuctionFastMarketData();
MarketData result = d.Update(fmd);
Assert.IsNotNull(result);
Assert.IsTrue(result.BidList[0].Price == 116.38 && result.BidList[0].Size == 28);
Assert.IsTrue(result.BidList[1].Price == 0 && result.BidList[1].Size == 2);
Assert.IsTrue(result.AskList[0].Price == 116.38 && result.AskList[0].Size == 28);
Assert.IsTrue(result.AskList[1].Price == 0 && result.AskList[1].Size == 0);
}
[TestMethod]
public void NormalMatchedTestV2()
{
FastMarketData fmd = new FastMarketData("");
fmd.BidList.AddLast(LevelInfo(122.56, 3));
fmd.BidList.AddLast(LevelInfo(116.52, 3));
fmd.BidList.AddLast(LevelInfo(115.71, 1));
fmd.BidList.AddLast(LevelInfo(114.71, 1));
fmd.BidList.AddLast(LevelInfo(114.5, 1));
fmd.BidList.AddLast(LevelInfo(114.29, 1));
fmd.BidList.AddLast(LevelInfo(114, 2));
fmd.BidList.AddLast(LevelInfo(113.8, 1));
fmd.BidList.AddLast(LevelInfo(113.43, 5));
fmd.BidList.AddLast(LevelInfo(112.88, 1));
fmd.AskList.AddLast(LevelInfo(109.93, 8));
fmd.AskList.AddLast(LevelInfo(117.7, 1));
fmd.AskList.AddLast(LevelInfo(118.64, 4));
fmd.AskList.AddLast(LevelInfo(119.44, 5));
fmd.AskList.AddLast(LevelInfo(119.7, 1));
fmd.AskList.AddLast(LevelInfo(119.99, 1));
fmd.AskList.AddLast(LevelInfo(120, 2));
fmd.AskList.AddLast(LevelInfo(120.05, 1));
fmd.AskList.AddLast(LevelInfo(120.27, 1));
fmd.AskList.AddLast(LevelInfo(121, 2));
CallAuctionFastMarketData d = new CallAuctionFastMarketData();
MarketData result = d.Update(fmd);
Assert.IsNotNull(result);
Assert.IsTrue(result.BidList[0].Price == 114.71 && result.BidList[0].Size == 8);
Assert.IsTrue(result.BidList[1].Price == 0 && result.BidList[1].Size == 0);
Assert.IsTrue(result.AskList[0].Price == 114.71 && result.AskList[0].Size == 8);
Assert.IsTrue(result.AskList[1].Price == 0 && result.AskList[1].Size == 0);
}
[TestMethod]
public void NormalMatchedTestV3()
{
FastMarketData fmd = new FastMarketData("");
fmd.BidList.AddLast(LevelInfo(138.85, 4));
fmd.BidList.AddLast(LevelInfo(122.56, 3));
fmd.BidList.AddLast(LevelInfo(116.52, 3));
fmd.BidList.AddLast(LevelInfo(115.71, 1));
fmd.BidList.AddLast(LevelInfo(115.1, 1));
fmd.BidList.AddLast(LevelInfo(114.71, 1));
fmd.BidList.AddLast(LevelInfo(114.57, 1));
fmd.BidList.AddLast(LevelInfo(114.5, 1));
fmd.BidList.AddLast(LevelInfo(114.29, 1));
fmd.BidList.AddLast(LevelInfo(114, 2));
fmd.AskList.AddLast(LevelInfo(109.93, 8));
fmd.AskList.AddLast(LevelInfo(115.7, 2));
fmd.AskList.AddLast(LevelInfo(116, 1));
fmd.AskList.AddLast(LevelInfo(116.7, 1));
fmd.AskList.AddLast(LevelInfo(117.7, 2));
fmd.AskList.AddLast(LevelInfo(118, 9));
fmd.AskList.AddLast(LevelInfo(118.23, 2));
fmd.AskList.AddLast(LevelInfo(118.64, 4));
fmd.AskList.AddLast(LevelInfo(118.7, 1));
fmd.AskList.AddLast(LevelInfo(118.88, 3));
CallAuctionFastMarketData d = new CallAuctionFastMarketData();
MarketData result = d.Update(fmd);
Assert.IsNotNull(result);
Assert.IsTrue(result.BidList[0].Price == 116.11 && result.BidList[0].Size == 10);
Assert.IsTrue(result.BidList[1].Price == 0 && result.BidList[1].Size == 0);
Assert.IsTrue(result.AskList[0].Price == 116.11 && result.AskList[0].Size == 10);
Assert.IsTrue(result.AskList[1].Price == 0 && result.AskList[1].Size == 1);
}
[TestMethod]
public void SigleBidAskMatchedTest()
{
FastMarketData fmd = new FastMarketData("");
List<CallAuctionLv2DetailInfo> bid = new List<CallAuctionLv2DetailInfo>();
fmd.BidList.AddLast(LevelInfo(138.85, 4));
List<CallAuctionLv2DetailInfo> ask = new List<CallAuctionLv2DetailInfo>();
fmd.AskList.AddLast(LevelInfo(92.57, 4));
CallAuctionFastMarketData d = new CallAuctionFastMarketData();
MarketData result = d.Update(fmd);
Assert.IsNotNull(result);
Assert.IsTrue(result.BidList[0].Price == 115.71 && result.BidList[0].Size == 4);
Assert.IsTrue(result.BidList[1].Price == 0 && result.BidList[1].Size == 0);
Assert.IsTrue(result.AskList[0].Price == 115.71 && result.AskList[0].Size == 4);
Assert.IsTrue(result.AskList[1].Price == 0 && result.AskList[1].Size == 0);
}
[TestMethod]
public void UnMatchedTest()
{
FastMarketData fmd = new FastMarketData("");
List<CallAuctionLv2DetailInfo> bid = new List<CallAuctionLv2DetailInfo>();
fmd.BidList.AddLast(LevelInfo(91.85, 4));
List<CallAuctionLv2DetailInfo> ask = new List<CallAuctionLv2DetailInfo>();
fmd.AskList.AddLast(LevelInfo(92.57, 4));
CallAuctionFastMarketData d = new CallAuctionFastMarketData();
MarketData result = d.Update(fmd);
Assert.IsNull(result);
}
private Level2DetailInfo LevelInfo(double price, int vol)
{
return new Level2DetailInfo(new OrderData("") { Price = price, Volume = vol });
}
}
每次修改或重构完算法之后,跑一遍测试看是否通过,这非常有用。
验证
最后我用落地数据,将该算法算出来的结果与同花顺Lv2收费版本(这个软件必须要是最新版本)的集合竞价对比了一下,发现基本能够匹配的上。
最后需要说明的是,这种根据主笔委托计算开盘集合竞价的方法,虽然理论上适合上交所和深交所。但是由于上交所在集合竞价期间(9:15~9:25)的委托数据,是在集合竞价完成之后即9:25,一股脑的推送过来的,所以在实际操作中,上述方法仅对深交所的股票适用。
参考资料
文中例子算错了吧,成交价应该是116.52而不是116.38。。。
感觉开盘价格应该是116.52,或者是avg(116.38,116.52)。如果是116.38作为开盘价,那开盘后买一价格超过了开盘价格,却没有成交。 这就很奇怪了~
请问落地数据是什么?怎样来处理level2数据的时间戳并把买卖单聚合起来?还有连续竞价的问题,谢谢!