有个C#程序每天实时落地了一些股票行情数据,为节省存储空间,数据是以二进制文件的方式存储的。现在要将这些数据提供给研究人员的MATLAB使用,需要编写一个C#的dll供MATLAB调用。

     先说一下实时行情的格式。目前只需要用到存储数据里的,股票代码,涨停价,跌停价,最新价,时间,成交量,成交额,买一至买十的价格和数量,卖一到卖十的价格和数量这些信息,行情从9:25~15:00大概每3秒一条记录。

↑ 典型的行情界面,上图里盘口只有5档,因为10档要钱😂

  在C#里面,可以定义一个MarketData对象,表示某一时刻的行情,行情里面包含股票代码,买卖十档的价格和数量等等,MarketData 如下图,只列出了部分字段:

public class MarketData
{
    public string Symbol;//代码
    public DateTime Time;//时间
    public double CurrentPrice;//当前价
    public Int64 Volume;//成交量
    public double Turnover;//成交额
    public double HighLimit;//涨停价
    public double LowLimit;//跌停价
    public List<Level2Info> BidList;//买一到买十价格和数量
    public List<Level2Info> AskList;//卖一到卖十价格和数量
}

public class Level2Info
{
    public double Price = 0;

    public Int32 Size = 0;
    public Level2Info(double price = 0, int size = 0)
    {
        this.Price = price;
        this.Size = size;
    }
}

   要让C#代码能在MATLAB里面调用,其实非常简单。只需要新建一个类,然后里面定义公共的静态方法即可,比如在这里我们定义一个ReadMarketData类,参数为本地的二进制文件,方法返回List<MarketData>对象。

public class ReadTool
{
    public static List<MarketData> ReadMarketData(string selectFileName)
    {
        List<MarketData> list = new List<MarketData>();
        try
        {
            using (FileStream fs = new FileStream(selectFileName, FileMode.Open, FileAccess.Read))
            {
                using (BinaryReader br = new BinaryReader(fs))
                {
                    list = LoadMarketData(br);
                }
            }
        }
        catch (Exception err)
        {
            throw err;
        }
        return list;
    }
}

程序集命名为了ReadTASMDDLL.dll,需要注意的是,编译时平台要选择正确,因为我是64位系统,并且MATLAB安装的是64位,所以编译dll的时候平台一定要选择x64,不然到时候MATLAB里调用会报错。

↑ MATLAB版本,64-bit

打开MATLAB之后,首先要把dll通过NET.addAssembly加载进来,然后再调用C#方法,方法需要全路径,包括命名空间(很尴尬的是,目前MATLAB并没有提供方法卸载程序集,要更新dll,只有重启MATLAB,然而MATLAB重启比较慢😂,MATLAB和.NET互操作的限制可以查看limitations-to-net-support)。

>> NET.addAssembly('D:\Dev_Work\Tools\ReadTASMDDLL.dll');
>> marketData=ReadTASMDDLL.ReadTool.ReadMarketData('E:\用户目录\桌面\MD20200611')

marketData = 

  List<ReadTASMDDLL*MarketData> (具有属性):

    Capacity: 8192
       Count: 4876

>> 

可以看到,现在方法已经被正确调用,并且返回值类型显示是C#里面的类型,List对象里有4876个元素(9:25~15:00每3秒一条行情数据)。这个marketData对象目前还不能直接使用,因为他是C#里面的对象,无法在MATLAB里发挥矩阵运算的作用,首先第一步就是解析里面的数据。我对MATLAB不熟悉,所以这里就直接在命令行里演示如何读取,真正的做法是写个.m文件处理。

>> marketData.Count

ans =

int32

4876

>> 
>> firstRecord=marketData.Item(0)

firstRecord = 

MarketData(具有属性) :

BidLevel: 10
AskLevel: 10
FullCode: [1×1 System.String]
BestBid: 13.3700
BestAsk: 13.3800
Gap: 0.0100
Rise: -0.0082
Exchange: SZ
Symbol: [1×1 System.String]
PreClose: 13.4900
Open: 13.3800
High: 13.3800
Low: 13.3800
CurrentPrice: 13.3800
Volume: 493800
Turnover: 6607044
Trades: 0
HighLimit: 14.8400
LowLimit: 12.1400
BidList: [1×1 System.Collections.Generic.List<ReadTASMDDLL*Level2Info>]
AskList: [1×1 System.Collections.Generic.List<ReadTASMDDLL*Level2Info>]
TotalBidVol: 0
WeightedAvgBidPrice: 0
TotalAskVol: 0
WeightedAvgAskPrice: 0
IOPV: 0
Time: [1×1 System.DateTime]
Type: None
Status: Rest
PreSettle: 0
MaxLevel: 10
>> symbol=firstRecord.Symbol

symbol =

000001

上图可以看到,可以通过访问对象的Count方法,然后通过Item(i)的方式访问集合里的元素,得到单个元素后,可以通过其属性对字段进行方法。上图可以看到列表对象下标还是从0开始的。

问题

  上述的方法,如果再MATLAB里面以循环的方式读取4876条记录,并且每条记录里面,有两个长度为10的BidList和AskList数组,也需要循环读取。对于1只股票的1天的数据读取,需要9秒,按照整个市场3000只股票来算的话,光是读取数据就需要3000*9/3600=7.5个小时,显然这是没办法接受的。

   慢的地方不在于C#方法的调用,而是在MATLAB循环里,于是需要优化,有两个地方可以优化:

  • 有一些数据重复了,比如股票代码,涨跌停价,这些都是不变的,所以没必要每条记录里都存储。
  • 去除MATLAB里的循环。

这里重点在于如何去除MATLAB里的循环

优化一

在C#和MATLAB数据结构进行互操作里,可以对基本的数据类型进行直接的转换。

↑基本类型可以直接转换,参考: using-arrays-with-net-applications

↑MATLAB基本类型和C#类型对应,参考: data-conversion-with-c-and-matlab-types

有了这种机制,可以在C#里直接返回基本类型的数组,然后在MATLAB里直接转换,这样就不需要循环了。根据这个思路,修改后有了第二个版本ReadMarketDataV2:

public static object[] ReadMarketDataV2(string selectFileName)
{
    string stockCode = "";
    double preClose = 0;
    double highLimit = 0;
    double lowLimit = 0;
    double open = 0;
    int time = 0;

    List<MarketData> d = ReadMarketData(selectFileName);
    object[][] multi = new object[d.Count][];
    int[] times = new int[d.Count];
    double[] prices = new double[d.Count];
    long[] volumes = new long[d.Count];
    long[] turnovers = new long[d.Count];
    int index = 0;
    foreach (var md in d)
    {

        if (string.IsNullOrEmpty(stockCode))
        {
            stockCode = md.Symbol;
        }

        if (preClose <= 0)
        {
            preClose = md.PreClose;
        }

        int.TryParse(md.Time.ToString("HHmmss"), out time);

        if (highLimit <= 0)
        {
            highLimit = md.HighLimit;
        }

        if (lowLimit <= 0)
        {
            lowLimit = md.LowLimit;
        }

        if (open <= 0)
        {
            open = md.Open;
        }

        double price = md.CurrentPrice;
        long volume = md.Volume;
        long turnover = (long)md.Turnover;
        double[] bidePrice = new double[10];
        double[] bideVol = new double[10];
        for (int i = 0; i < md.BidLevel; i++)
        {
            bidePrice[i] = md.BidList[i].Price;
            bideVol[i] = md.BidList[i].Size;
        }

        double[] askPrice = new double[10];
        double[] askVol = new double[10];
        for (int i = 0; i < md.AskLevel; i++)
        {
            askPrice[i] = md.AskList[i].Price;
            askVol[i] = md.AskList[i].Size;
        }

        times[index] = time;
        prices[index] = price;
        volumes[index] = volume;
        turnovers[index] = turnover;
        multi[index] = new object[] { bidePrice, bideVol, askPrice, askVol };
        index += 1;
    }

    return new object[] { stockCode, preClose, highLimit, lowLimit, open, multi, times, prices, volumes, turnovers };
}

在这个方法里面,返回的是一个Object类型的数组,将整个列表里面不变的对象单独拿出来,比如股票代码,涨跌停价等,这些只需要一个对象来表示即可。另外将一些数据做成了单个数组,比如时间,最新价,成交量,成交额,这些对象是一个有着4876个对象的数组,多个数组之间可以根据下标Index来对应,这样就可以直接在MATLAB里转换。另外还有个multi对象,里面是一个数组,它有4876行4列,每个元素里面有一个有10个数据的数组。

>> marketData=ReadTASMDDLL.ReadTool.ReadMarketDataV2('E:\用户目录\桌面\MD20200611')

marketData = 

  Object[] (具有属性):

            Length: 10
        LongLength: 10
              Rank: 1
          SyncRoot: [1×1 System.Object[]]
        IsReadOnly: 0
       IsFixedSize: 1
    IsSynchronized: 0

>> symbol=marketData(1)

symbol = 

000001

>> times=int32(marketData(7))

可以看到,这里的marketData对象已经是数组了,在MATLAB里面,数组的下标是从1开始的,对于时间,直接使用int32就能直接转换为MATLAB里面可以直接使用的类型,从而避免了4876次大循环。

经过这一优化,时间从9秒减少为了1.5秒,对于3000多只股票,读取时间从7.5优化为了1.25小时。

优化二

经过上面的优化后,可以看到,还有一个对象multi仍然存在循环,这个对象是一个多维数组,他是一个4876行,4列的数组,每个元素又是一个长度为10的数组,将这个对象展开为二维数组,这样就可以去掉循环,直接使用对象转换了,这里的优化方法为,定义两个二维数组,分别将买卖十档的价格和数量保存起来,因为价格和数量数据类型不一样,一个是double一个是int32,所以需要分两个对象。

double[,] askBidPrice = new double[20, d.Count];
int[,] askBidVol = new int[20, d.Count];

这样,将4876*4*10的多维数组,转换为了2个 20*4876的二位数组。这20行里,前10行为买一到买十的数据,后10行为卖一到卖十数据,修改后的方法为ReadMarketDataV3:

public static object[] ReadMarketDataV3(string selectFileName)
{
    string stockCode = "";
    double preClose = 0;
    double highLimit = 0;
    double lowLimit = 0;
    double open = 0;
    int time = 0;

    List<MarketData> d = ReadMarketData(selectFileName);
    int[] times = new int[d.Count];
    double[] prices = new double[d.Count];
    long[] volumes = new long[d.Count];
    long[] turnovers = new long[d.Count];
    int index = 0;

    double[,] askBidPrice = new double[20, d.Count];
    int[,] askBidVol = new int[20, d.Count];
    foreach (var md in d)
    {
        if (string.IsNullOrEmpty(stockCode))
        {
            stockCode = md.Symbol;
        }

        if (preClose <= 0)
        {
            preClose = md.PreClose;
        }

        int.TryParse(md.Time.ToString("HHmmss"), out time);

        if (highLimit <= 0)
        {
            highLimit = md.HighLimit;
        }

        if (lowLimit <= 0)
        {
            lowLimit = md.LowLimit;
        }

        if (open <= 0)
        {
            open = md.Open;
        }

        double price = md.CurrentPrice;
        long volume = md.Volume;
        long turnover = (long)md.Turnover;

        for (int i = 0; i < 10; i++)
        {
            if (i < md.BidLevel)
            {
                askBidPrice[i, index] = md.BidList[i].Price;
                askBidVol[i, index] = md.BidList[i].Size;
            }
            else
            {
                askBidPrice[i, index] = 0;
                askBidVol[i, index] = 0;
            }
        }

        for (int i = 0; i < 10; i++)
        {
            if (i < md.AskLevel)
            {
                askBidPrice[i + 10, index] = md.AskList[i].Price;
                askBidVol[i + 10, index] = md.AskList[i].Size;
            }
            else
            {
                askBidPrice[i + 10, index] = 0;
                askBidVol[i + 10, index] = 0;
            }
        }

        times[index] = time;
        prices[index] = price;
        volumes[index] = volume;
        turnovers[index] = turnover;
        index += 1;
    }
    return new object[] { stockCode, preClose, highLimit, lowLimit, open, times, prices, volumes, turnovers, askBidPrice, askBidVol };
}

现在,使用ReadMarketDataV3方法,可以一次性直接转换买卖十档数据,避免了循环:

>> marketData=ReadTASMDDLL.ReadTool.ReadMarketDataV3('E:\用户目录\桌面\MD20200611')

marketData = 

  Object[] (具有属性):

            Length: 11
        LongLength: 11
              Rank: 1
          SyncRoot: [1×1 System.Object[]]
        IsReadOnly: 0
       IsFixedSize: 1
    IsSynchronized: 0

>> askbidPrices=marketData(10)

askbidPrices = 

  Double[,] (具有属性):

            Length: 97520
        LongLength: 97520
              Rank: 2
          SyncRoot: [1×1 System.Double[,]]
        IsReadOnly: 0
       IsFixedSize: 1
    IsSynchronized: 0

>> askbidPrices=double(marketData(10))

现在,经过两次优化,将所有的循环都去掉了,MATLAB里使用C# dll读取单个文件,并转换为MATLAB能处理的数据的时间从9秒优化到了0.5秒,这样基本能满足使用要求了。

总结

    本文展示了如何优化C#的返回数据类型结构,并利用MATLAB基本类型数组的直接转换特点,移除了对数据的循环访问,从而避免了耗时的循环,进而极大提升了MATLAB的数据访问效率。另外,即便对于同一个二进制文件,使用C#来读取,要比MATLAB直接读取要快,这是MATLAB和C#这两种语言的不同特性决定的。

   MATLAB是一种解释性语言,跟Python,PHP一样,他们的特点是,边解析边运行,比较适合用来快速验证想法,有一些语法特性,比如不必须事先定义数据类型,但是缺点是运行效率没有编译型语言快。C#,Java,C++这些语言是典型的编译型,强类型语言。代码写好后,会经过编译器进行编译优化,形成二进制代码,运行效率更高。

     在实际的开发中可以充分利用两种不同类型语言的优点提高工作效率。比如读取二进制数据,可以利用编译型语言效率高的特点,将读取的数据直接返回给MATLAB处理;再比如MATLAB语言除了是一种编译型语言之外,他在矩阵运算方面具有优势,所以在需要进行复杂的矩阵运算时,可以将逻辑模块放在MATLAB里面运算,运算完成之后把数据返回给C#进行后续处理。