2016 年 6 月

第 31 卷,第 6 期

测试运行 - 预测市场简介

作者 James McCaffrey

James McCaffrey假设你想预测一场即将举行的足球冠军赛的结果(Xrays 对决 Yanks)。现在有一个 20 人的足球专家组,你给他们每个人 500 美元的代币。我们允许这些专家购买和销售两队各自的份额,这在某种程度上类似股票市场的运作方式。

当一名专家购买其中一队(比如 Xrays)的份额,那么该队的份额价格会上涨,而另一队的份额价格会下落。经过一段时间,专家们会购买和销售两队的份额,直到价格趋于稳定,然后你就能够推理出每个队获胜的概率。

你在冠军赛举行前一天停止交易。等比赛结束并决出获胜者之后,你根据交易关闭时球队的最后价格将收益支付给拥有获胜球队一方份额的专家们。因为专家们知道他们会获得收益,所以他们有动力在交易中给出他们真实的观点。

刚才我描述的即称为预测市场。在本文中,我将介绍预测市场背后的数学知识,以及如何在代码中实现主要函数。在日常工作中你可能永远也不需要创建预测市场,但是我认为你会发现这个概念很有趣。此外,本文中介绍的一些编程技巧可以运用在更常见的软件开发方案中。

要理解本文的内容,你至少需要拥有初级编程技巧,但可能对预测市场一无所知。我将介绍一个完整的演示程序,你也可以从本文随附的下载地址获得源代码。该演示使用 C#,但是如果你愿意使用另一种语言重构代码,应该也不会有太大困难。

请注意,这是主要面向软件开发人员的对预测市场的简略介绍。我对术语和定义进行了一些修改,以保持主要概念尽可能清楚明确。

示例:

也许解释预测市场的最佳方法是通过具体示例演示。请查看图 1 中的演示程序。显示一些初始消息后,演示输出以此开头:

Setting liquidity parameter = 100.0

Initial number of shares owned of teams [0] and [1] are:
0 0

Initial inferred probabilities of winning are:
 0.5000  0.5000

预测市场演示
图 1 预测市场演示

流动性参数将在稍后详细解释,现在你只需知道流动性控制市场价格对购买和销售作出反应的大小。流动值越大,价格变动越小。

最初,这些专家并不拥有任何份额。因为每队拥有的份额数是相同的(均为零),所以每队获胜的初始推理概率是 0.50,这是合理的。

该演示输出的下一部分是:

Current costs for one share of each team are:
 $0.5012  $0.5012

在任意时间点,每队的份额都有一个特定的价格。专家们需要知道这个价格,因为他们是在进行真实的货币交易。因为获胜的初始概率是相等的,所以每队的份额价格自然也是相同的。

该演示输出的下一部分是:

Update: expert [01] buys 20 shares of team [0]

Cost of transaction to expert was: $10.50

专家 #1 相信球队 0(Xrays)会获胜,并购买了球队 0 的 20 个份额。专家的成本是 10.50 美元。请注意,20 个份额的价格(10.50 美元)和单个份额价格的 20 倍(20 * 0.5012 美元 = 10.02 美元)是不同的。随着每个份额被购买,球队的额外份额的价格会上涨。该演示输出的下一部分是:

New number of shares owned of teams [0] and [1] are:
20 0

New inferred probabilities of winning are:
 0.5498  0.4502

该演示显示了更新的每个球队份额的余额的数目,(x, y) = (20, 0),并计算和显示更新的每个球队获胜的推理概率 (0.55, 0.45)。因为这些专家购买的球队 0 的份额比球队 1 的份额多,所以球队 0 获胜的推理概率一定比球队 1 的更大。概率的计算方法稍后将详细介绍。

接下来,该演示显示:

Current costs for one share of each team are:
 $0.5511  $0.4514

Update: expert [02] buys 20 shares of team [1]

Cost of transaction to expert was: $9.50

计算和显示每个球队每一份额的新成本。请注意,现在球队 0 的价格(0.55 美元)比球队 1(0.45美元)的价格贵一点。这会让专家们有动力购买球队 1 的份额(如果他们认为相对于球队 1 获胜的可能性来说该价格很划算)。在本示例中,该演示模拟专家 #2 以 9.50 美元的成本购买球队 1 的 20 个份额。接下来:

New number of shares owned of teams [0] and [1] are:
20 20

New inferred probabilities of winning are:
 0.5000  0.5000

现在每个球队有 20 个剩余份额,因此每个球队获胜的推理概率恢复为 0.50 和 0.50。

该演示输出的下一部分是:

Current costs for one share of each team are:
 $0.5012  $0.5012

Update: expert [03] buys 60 shares of team [0]
Cost of transaction to expert was: $34.43

New number of shares owned of teams [0] and [1] are:
80 20

New inferred probabilities of winning are:
 0.6457  0.3543

专家 #3 坚信球队 0 会获胜,因此他以 34.43 美元的成本购买了球队 0 的 60 个份额。此交易将剩余份额的数目改变为(80、20),并导致新的获胜推理概率明显偏向到球队 0(0.65、0.35)。

接下来,专家 #1 看到他在球队 0 的份额价值大幅上涨到大约每个份额 0.6468 美元:

Current costs for one share of each team are:
 $0.6468  $0.3555

Update: expert [01] sells 10 shares of team [0]
Cost of transaction to expert was: $-6.34

New number of shares owned of teams [0] and [1] are:
70 20

New inferred probabilities of winning are:
 0.6225  0.3775

专家 #1 觉得球队 0 相对于其获胜的机会来说有一点价格过高,于是他出售了 20 个份额中的 10 个,获得 6.34 美元(表示为负号)。新的推理概率调整回更加相等的状态,但是球队 0 仍被预测为有 0.63 的获胜概率。

该演示以关闭交易结束。预测市场的目标是计算出最终概率。在 Xrays 和 Yanks 间的比赛开始后,专家们将根据其持有的获胜球队的份额获得收益(基于获胜球队的最终份额价格)。此收益将鼓励专家给出他们真实的观点。

预测市场的四个重要等式。

基本的预测市场使用四个数学等式,如图 2 所示。请耐心听我讲解一下,这些等式可能在第一次出现时并没有那么完整。有几种数学模型可用于定义预测市场。本文介绍的模型基于对数市场得分规则 (LMSR)。

预测市场的四个重要等式。
图 2 预测市场的四个重要等式

等式 1 是和一组剩余份额(x、y)相关的成本函数。该等式来自经济理论,并不那么明显。从开发人员的角度看,你可以将该等式看作一个帮助程序函数。它接受 x(即方案 0 持有的份额数)和 y(即方案 1 持有的份额数),并返回一个值。变量 b 在所有四个等式中是流动性参数。假设 x = 20 且 y = 10。若 b = 100.0,则 C(x,y) = 100.0 * ln(exp(20/100) + exp(10/100)) = 100.0 * ln(1.22 + 1.11) = 100.0 * 0.8444 = 84.44 美元。返回值在等式 2 中使用。

等式 2 是对于购买者的交易成本。假设当前份额余额组是(20、10),且一个专家购买了方案 0 的 30 个份额。对这个专家来说,他的交易成本使用等式 2 计算为 C(20+30, 10) - C(20, 10) = C(50, 10) - C(20, 10) = 101.30 - 84.44 = 16.86 美元。如果一个专家销售份额,则交易成本将是一个负值,表示已向该专家支付收益。

等式 3 从技术上来说是方案 0 的边际价格,它基于一组份额余额(x、y)。但是边际价格可以被粗略的解释为方案获胜的概率。等式 4 是方案 1 的边际价格(概率)。如果你进一步研究这两个等式,你将注意到它们的总和必须为 1.0,因为这是一组概率的要求。

实现预测市场的四个重要等式的方法很简单。该演示程序实现成本等式 1,如下:

static double Cost(int[] outstanding, double liq)
{
  double sum = 0.0;
  for (int i = 0; i < 2; ++i)
    sum += Math.Exp(outstanding[i] / liq);
  return liq * Math.Log(sum);
}

成本方法实际上是等式 1 的准确转换。请注意,方法成本假定仅有两种方案。为简单起见,不执行错误检查。

等式 2 也非常容易实现:

static double CostOfTrans(int[] outstanding, int idx, int nShares, double liq)
{
  int[] after = new int[2];
  Array.Copy(outstanding, after, 2);
  after[idx] += nShares;
  return Cost(after, liq) - Cost(outstanding, liq);
}

数组以交易后持有份额余额的新数目命名,然后方法只需调用成本帮助程序方法两次。掌握了计算交易成本的方法后,就能很容易地编写方法以计算从两个方案购买单个份额的成本:

static double[] CostForOneShare(int[] outstanding, double liq)
{
  double[] result = new double[2];
  result[0] = CostOfTrans(outstanding, 0, 1, liq);
  result[1] = CostOfTrans(outstanding, 1, 1, liq);
  return result;
}

这些专家可以使用单个份额的成本计算购买某一方案的 n 个份额大概所需的成本。

方法概率返回数组中每个方案获胜的两个边际价格(推理概率):

static double[] Probabilities(int[] outstanding, double liq)
{
  double[] result = new double[2];
  double denom = 0.0;
  for (int i = 0; i < 2; ++i)
    denom += Math.Exp(outstanding[i] / liq);
  for (int i = 0; i < 2; ++i)
    result[i] = Math.Exp(outstanding[i] / liq) / denom;
  return result;
}

如果你将方法概率的代码与等式 3 和 4 比较,你会再次发现,代码直接来自数学定义。

演示程序

为了创建演示程序,我启动了 Visual Studio 并选择了 C# 控制台应用程序模板。我把这个项目命名为 PredictionMarket。该演示对 Microsoft .NET Framework 的依赖程度并不明显,因此,任何 Visual Studio 版本都可以正常运行。

加载模板代码后,我在解决方案资源管理器窗口中将文件 Program.cs 重命名为更具描述性的 PredictionMarketProgram.cs,并允许 Visual Studio 自动重命名 Program 类。在源代码的顶端,我删除了所有引用不必要的 .Net 命名空间的 using 语句,仅保留对顶层 System 命名空间的引用。

完整的演示代码如图 3 所示,其中包含一些较小的修改和为了节省空间而删除的一些 WriteLine 语句。所有程序控制逻辑都包含在 Main 方法中。预测市场的所有功能都在四个静态方法中,并且有两种 ShowVector 帮助程序显示方法。

图 3 预测市场演示

using System;
namespace PredictionMarket
{
  class PredictionMarketProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin prediction market demo ");
      Console.WriteLine("Goal is to predict winner of Xrays");
      Console.WriteLine("vs. Yanks using expert opinions");
      double liq = 100.0;
      Console.WriteLine("Setting liquidity parameter = " +
        liq.ToString("F1"));
      int[] outstanding = new int[] { 0, 0 };
      Console.WriteLine("Initial number of shares owned are:");
      ShowVector(outstanding);
      double[] probs = Probabilities(outstanding, liq);
      Console.WriteLine("Initial probabilities of winning:");
      ShowVector(probs, 4, " ");
      Console.WriteLine("=================================");
      double[] costPerShare = CostForOneShare(outstanding, liq);
      Console.WriteLine("Current costs for one share are: ");
      ShowVector(costPerShare, 4, " $");
      Console.WriteLine("Update: expert [01] buys 20 shares " +
        "of team [0]");
      double costTrans = CostOfTrans(outstanding, 0, 20, liq);
      Console.WriteLine("Cost of transaction to expert was: $" +
        costTrans.ToString("F2"));
      outstanding = new int[] { 20, 0 };
      Console.WriteLine("New number of shares owned are: ");
      ShowVector(outstanding);
      probs = Probabilities(outstanding, liq);
      Console.WriteLine("New inferred probs of winning:");
      ShowVector(probs, 4, " ");
      Console.WriteLine("=================================");
      costPerShare = CostForOneShare(outstanding, liq);
      Console.WriteLine("Current costs for one share are:");
      ShowVector(costPerShare, 4, " $");
      Console.WriteLine("Update: expert [02] buys 20 shares " +
        "of team [1]");
      costTrans = CostOfTrans(outstanding, 1, 20, liq);
      Console.WriteLine("Cost of transaction to expert was: $" +
        costTrans.ToString("F2"));
      outstanding = new int[] { 20, 20 };
      Console.WriteLine("New number of shares owned are:");
      ShowVector(outstanding);
      probs = Probabilities(outstanding, liq);
      Console.WriteLine("New inferred probs of winning:");
      ShowVector(probs, 4, " ");
      Console.WriteLine("=================================");
      costPerShare = CostForOneShare(outstanding, liq);
      Console.WriteLine("Current costs for one share are:");
      ShowVector(costPerShare, 4, " $");
      Console.WriteLine("Update: expert [03] buys 60 shares " +
        "of team [0]");
      costTrans = CostOfTrans(outstanding, 0, 60, liq);
      Console.WriteLine("Cost of transaction to expert was: $" +
        costTrans.ToString("F2"));
      outstanding = new int[] { 80, 20 };
      Console.WriteLine("New number of shares owned are:");
      ShowVector(outstanding);
      probs = Probabilities(outstanding, liq);
      Console.WriteLine("New inferred probs of winning:");
      ShowVector(probs, 4, " ");
      Console.WriteLine("=================================");
      costPerShare = CostForOneShare(outstanding, liq);
      Console.WriteLine("Current costs for one share are: ");
      ShowVector(costPerShare, 4, " $");
      Console.WriteLine("Update: expert [01] sells 10 shares " +
        "of team [0]");
      costTrans = CostOfTrans(outstanding, 0, -10, liq);
      Console.WriteLine("Cost of transaction to expert was: $" +
        costTrans.ToString("F2"));
      outstanding = new int[] { 70, 20 };
      Console.WriteLine("New number of shares owned are:");
      ShowVector(outstanding);
      probs = Probabilities(outstanding, liq);
      Console.WriteLine("New inferred probs of winning:");
      ShowVector(probs, 4, " ");
      Console.WriteLine("=================================");
      Console.WriteLine("Update: Market Closed");
      Console.WriteLine("\nEnd prediction market demo \n");
      Console.ReadLine();
    } // Main()
    static double[]Probabilities(int[] outstanding,
      double liq)
    {
      double[] result = new double[2];
      double denom = 0.0;
      for (int i = 0; i < 2; ++i)
        denom += Math.Exp(outstanding[i] / liq);
      for (int i = 0; i < 2; ++i)
        result[i] = Math.Exp(outstanding[i] / liq) / denom;
      return result;
    }
    static double Cost(int[] outstanding, double liq)
    {
      double sum = 0.0;
      for (int i = 0; i < 2; ++i)
        sum += Math.Exp(outstanding[i] / liq);
      return liq * Math.Log(sum);
    }
    static double CostOfTrans(int[] outstanding, int idx,
      int nShares, double liq)
    {
      int[] after = new int[2];
      Array.Copy(outstanding, after, 2);
      after[idx] += nShares;
      return Cost(after, liq) - Cost(outstanding, liq);
    }
    static double[] CostForOneShare(int[] outstanding,
      double liq)
    {
      double[] result = new double[2];
      result[0] = CostOfTrans(outstanding, 0, 1, liq);
      result[1] = CostOfTrans(outstanding, 1, 1, liq);
      return result;
    }
    static void ShowVector(double[] vector, int dec, string pre)
    {
      for (int i = 0; i < vector.Length; ++i)
        Console.Write(pre + vector[i].ToString("F" + dec) + " ");
      Console.WriteLine("\n");
    }
    static void ShowVector(int[] vector)
    {
      for (int i = 0; i < vector.Length; ++i)
        Console.Write(vector[i] + " ");
      Console.WriteLine("\n");
    }
  } // Program class
} // ns

显示一些初始消息后,Main 方法中的程序执行以此开始:

double liq = 100.0;
int[] outstanding = new int[] { 0, 0 };
ShowVector(outstanding);

变量 liq 是流动性参数。100.0 是典型的值,但是如果你通过调整值进行实验,你将发现交易后该值是如何影响份额价格变化的。流动值越大,变化越小。命名为余额的数组持有所有专家在两队各自拥有的总份额数。请注意,流动性参数需要传递到四个静态市场预测方法。其替代设计是将方法封装到 C# 类并将流动性定义为成员字段。

接下来,剩余的份额数被用来决定每队获胜的推理概率:

double[] probs = Probabilities(outstanding, liq);
Console.WriteLine("Initial probabilities of winning:");
ShowVector(probs, 4, " ");

接下来,演示程序演示从两队购买各自单个份额的成本:

double[] costPerShare = CostForOneShare(outstanding, liq);
Console.WriteLine("Current costs for one share are: ");
ShowVector(costPerShare, 4, " $");

在真实的预测市场中,该信息对市场专家很有用,可帮助他们评估一队的份额价格太高还是太低(相对于专家认为该队会获胜的观点)。

该演示程序模拟其中的一个专家购买了一些份额,如下:

Console.WriteLine("Update: expert [01] buys 20 shares of team [0]");
double costTrans = CostOfTrans(outstanding, 0, 20, liq);
Console.WriteLine("Cost of transaction to expert was: $" +
  costTrans.ToString("F2"));

在真实的预测市场中,系统需要维护大量有关专家的账户余额和拥有的份额数的信息。

接下来,剩余的份额数会更新,如下:

outstanding = new int[] { 20, 0 };
Console.WriteLine("New number of shares owned on teams [0] " +
  "and [1] are: ");
ShowVector(outstanding);

如果你回头参考图 2 中的数学等式,你将注意到所有的等式都需要每队/每个方案的剩余份额的数目(x、y)。

剩余份额的数目被更新后,该信息将用来预测每队或每个方案获胜的修正概率。

probs = Probabilities(outstanding, liq);
Console.WriteLine("New inferred probabilities of
  winning are: ");
ShowVector(probs, 4, " ");

请注意,这些值实际上是边际价格,但是将它们看作概率很有用。最终,预测市场的目标是生成每队或每个方案获胜的可能性,因此市场趋于稳定后最终的概率组正是你需要的。

演示程序最后会将以下五项操作再重复三次:

  • 显示每队一个份额的当前成本
  • 执行购买或销售交易
  • 显示交易成本
  • 更新剩余份额的总数
  • 更新每队获胜的概率

请注意,演示程序从两队的概率相等的情况开始。这在很多真实的预测市场方案中是不现实的。可以使用不相等的概率初始化预测市场,以求出等式 3 和 4 中的 x 和 y 。

总结

本文中的信息基于 Robin Hanson 于 2002 年发表的研究论文《Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation》。你可以使用任何搜索工具在互联网上的很多位置找到该论文的 PDF 版本。

预测市场并非只是一个抽象的理论概念。在过去几年中,成立了多家公司,他们实际上就是实现真实货币的预测市场。

活动研究的领域被称为组合预测市场。除了选择两个方案中的一个获胜,专家们可以在组合比赛中购买份额,如球队 A 对决球队 B,球队 J 对决球队 K。组合预测市场比单个市场要复杂得多。


Dr.James McCaffrey供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他参与过多个 Microsoft 产品的工作,包括 Internet Explorer 和 Bing。Scripto可通过 jammc@microsoft.com 与 McCaffrey 取得联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Pallavi Choudhury、Gaz Iqbal、Umesh Madan 和 Tien Suwandy