集合竞价是电子撮合交易中的重要撮合方式,通常用来在开盘或者收盘时产生开盘价或者收盘价,或者对于某些流动性差的产品,通过一段时间集中进行撮合,找出能产生最大成交量的价格的方式,(即市场大多数人认可的价格)防止价格被不小心操纵。
    中国大陆市场中,由于人口众多,流动性几乎从不缺乏,所以从一开始就是采用把集合竞价生成开盘价和连续竞价高效撮合组合在一体的方式。具体上,沪深交易所都是以集合竞价来场开盘价和收盘价,在收盘价上,如果集合竞价不能产生收盘价,则采用最后一分钟加权平均价(上交所最开始的收盘价是使用的1分钟均价,后来改成了也采用集合竞价的方式产生)。本文对沪深交易所的开盘集合竞价算法作了简单理解和实现。

法规


     开盘集合竞价的规则,在上海证券交易所的网站上能找到,在上海证券交易所交易规则(2020年第二次修订)中,与集合竞价有关的规则有:

3.4.1 本所接受交易参与人竞价交易申报的时间为每个交易日9:15至 9:25、9:30至11:30 、13:00至15:00。
  每个交易日9:20至9:25的开盘集合竞价阶段、14:57至15:00的收盘集合竞价阶段,本所交易主机不接受撤单申报;其他接受交易申报的时间内,未成交申报可以撤销。撤销指令经本所交易主机确认方为有效。

3.5.1 证券竞价交易采用集合竞价和连续竞价两种方式。
  集合竞价是指在规定时间内接受的买卖申报一次性集中撮合的竞价方式。
  连续竞价是指对买卖申报逐笔连续撮合的竞价方式。
3.5.2 当前竞价交易阶段未成交的买卖申报,自动进入当日后续竞价交易阶段。

3.6.1 证券竞价交易按价格优先、时间优先的原则撮合成交。
  成交时价格优先的原则为:较高价格买入申报优先于较低价格买入申报,较低价格卖出申报优先于较高价格卖出申报。
  成交时时间优先的原则为:买卖方向、价格相同的,先申报者优先于后申报者。先后顺序按交易主机接受申报的时间确定。
3.6.2 集合竞价时,成交价格的确定原则为:
  (一)可实现最大成交量的价格;
  (二)高于该价格的买入申报与低于该价格的卖出申报全部成交的价格;
  (三)与该价格相同的买方或卖方至少有一方全部成交的价格。
  两个以上申报价格符合上述条件的,使未成交量最小的申报价格为成交价格;仍有两个以上使未成交量最小的申报价格符合上述条件的,其中间价为成交价格。
  集合竞价的所有交易以同一价格成交。

4.1.1 证券的开盘价为当日该证券的第一笔成交价格。
4.1.2 证券的开盘价通过集合竞价方式产生,不能产生开盘价的,以连续竞价方式产生。
4.1.3 除本规则另有规定外,证券的收盘价通过集合竞价的方式产生。收盘集合竞价不能产生收盘价或未进行收盘集合竞价的,以当日该证券最后一笔交易前一分钟所有交易的成交量加权平均价(含最后一笔交易)为收盘价。

    比较重要的点就是3.6.1和3.6.2。简单来说,就是在开盘集合竞价期间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档。每一档包含价格和数量。

    第一步:首先计算每一档价格的总买单和总卖单。

    •  先看左边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手
      依次类推。
    • 最后得到下面表格:

     第二步:以买入价为虚拟成交价,计算匹配量和未匹配量。

    • 买一大于等于卖一,才可以匹配。否则,无法匹配,直接返回。
    • 匹配量的计算方法为,买入一侧:买入档位中某一档价格对应的总数量;卖出一侧为,卖出十档价格里面小于等于买入价格的最大值,对应的总数量。比如以买一价格成交,匹配价格为138.85,买入一方总数量为4;卖出方小于匹配价格138.52的最大档为卖十,对应的总数量为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手未成交。
    • 按照规则,虽然买卖十档中,以买8和卖7成交,最大匹配量都是28,未匹配量都是2,但是以卖7成交,卖方是全部成交的,所以该例子中,最终成交价格为卖7,价格为116.38,未成交2手,在买方。

     第五步:特殊情况的处理,我们还是回到3.6.2中,可以看到,必须要满足三个条件。就是匹配量最大,按照该价格匹配时,能够全部成交(如果是买单,则大于等于该价格的全部成交,如果是卖单,则小于等于该价格的全部成交)。这也是为什么第四部中是以卖7,而不是买8成交。如果有多个价格都满足该条件,能使得匹配量最大,且全部成交,那么:

    • 取未匹配量最小的作为成交价格,比如举个例子,如果买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)
            {
                //当前方是否全部成交
                bool isFullMatch = bid.TotalSize <= askList[askMatchIndex].TotalSize;
                if (isFullMatch)
                {
                    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)
            {
                bool isFullMatch = ask.TotalSize <= bidList[bidMatchIndex].TotalSize;
                if (isFullMatch)
                {
                    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;
            }
        }
    }
}

      首先计算买方十档的最大匹配量的匹配价格,匹配量和未匹配量,然后计算卖方的最大匹配量的匹配价格,匹配量和未匹配量。这里有两个优化的点。

  1. 在找出匹配价格对应的档位的时候,因为数组序列是有序的,所以应用了二分查找法,这提现在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;
    }
  2. 买卖方档位可能不止十档,通常情况下可能有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;
        }
    }
    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收费版本(这个软件必须要是最新版本)的集合竞价对比了一下,发现基本能够匹配的上。

参考资料