请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364477 新帖:3

跨平台的EA交易: 资金管理

英雄联盟发表于:4 月 17 日 16:41回复(1)


目录

  1. 简介

  2. 目标

  3. 基类

  4. 资金管理类和类型

  5. 资金管理对象的容器

  6. 实例

  7. 结论

简介

资金管理是EA交易中的通用功能,它使得EA交易可以动态决定下一次交易进场时的手数大小。在本文中,我们将介绍几个资金管理类,它们可以在跨平台EA交易中自动化交易量的计算。

目标

  • 了解和应用交易中使用的常用资金管理方法

  • 允许EA交易动态从列表中选择可用的资金管理方法

  • 与 MQL4 和 MQL5 都兼容

基类

本文中描述的所有资金管理类都有一个基类作为它们的父类,名称为 CMoney, 派生于 CMoneyBase,CMoneyBase 类是如下定义的:

class CMoneyBase : public CObject
  {protected:
   bool              m_active;
   double            m_volume;
   double            m_balance;
   double            m_balance_inc;
   int               m_period;
   bool              m_equity;
   string            m_name;
   CSymbolManager   *m_symbol_man;
   CSymbolInfo      *m_symbol;
   CAccountInfo     *m_account;
   CEventAggregator *m_event_man;
   CObject          *m_container;public:
                     CMoneyBase(void);
                    ~CMoneyBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_MONEY;}
   //- 初始化   virtual bool      Init(CSymbolManager*,CAccountInfo*,CEventAggregator*);
   bool              InitAccount(CAccountInfo*);
   bool              InitSymbol(CSymbolManager*);
   CObject          *GetContainer(void);
   void              SetContainer(CObject*);
   virtual bool      Validate(void);
   //- 取得和设置属性   bool              Active(void) const;
   void              Active(const bool);
   void              Equity(const bool);
   bool              Equity(void) const;
   void              LastUpdate(const datetime);
   datetime          LastUpdate(void) const;
   void              Name(const string);
   string            Name(void) const;
   double            Volume(const string,const double,const ENUM_ORDER_TYPE,const double);
   void              Volume(const double);
   double            Volume(void) const;protected:
   virtual void      OnLotSizeUpdated(void);
   virtual bool      UpdateLotSize(const string,const double,const ENUM_ORDER_TYPE,const double);
  };

类中的大多数方法都是用来读取和设置类成员的,所以很容易理解。在实际应用程序中,真正重要的有三个方法,它们是 UpdateLotSize, OnLotSizeUpdated, 和 Volume。

UpdateLotSize 方法是真正进行交易量计算的地方,这也是从基类扩展而来的主方法,所以,在这个方法中可以找到资金管理类之间的大多数差别。对于基类 CMoneyBase, 这个方法可以看作是虚方法,因为它什么都不做,只是返回一个 true 的数值:

bool CMoneyBase::UpdateLotSize(const string,const double,const ENUM_ORDER_TYPE,const double)
  {
   return true;
  }

有的时候,在计算交易量之后,需要更新某些变量用来做未来的计算,在这样的情况下,就使用 OnLotSizeUpdated 方法,这个方法是在 UpdateLotSize 方法中自动调用的。以下代码显示的就是这个方法:

void CMoneyBase::OnLotSizeUpdated(void)
  {
   m_symbol=m_symbol_man.Get();
   double maxvol=m_symbol.LotsMax();
   double minvol=m_symbol.LotsMin();
   if(m_volume<minvol)
      m_volume=minvol;
   if(m_volume>maxvol)
      m_volume=maxvol;
  }

为了取得资金管理对象计算的实际交易量,EA交易不需要调用 UpdateLotSize 或者 OnLotSizeUpdated 方法,而是调用类中的 Volume 方法,这个方法将在它的代码中自动调用另外两个方法:

double CMoneyBase::Volume(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl=0)
  {
   if(!Active())
      return 0;
   if(UpdateLotSize(symbol,price,type,sl))
      OnLotSizeUpdated();
   return m_volume;
  }

资金管理类和类型

固定手数

这是最常见的设置手数大小方法,绝大多数交易者都熟悉它。通过使用固定手数大小,所有的交易都有一致的交易量大小,而不管随着时间的推移账户余额或者净值的增减。

在这种类型的资金管理中,我们只需要一个固定的交易数量。所以,它与 CMoney/CMoneyBase 的主要差别可以在它的构造函数中找到,在其中我们指定了固定的手数大小:

CMoneyFixedLotBase::CMoneyFixedLotBase(double volume)
  {
   Volume(volume);
  }

如果我们需要动态改变资金管理方法的输出,我们只需要简单通过调用 Volumne 方法来修改它的 m_volume 类成员就可以了。


固定风险 (固定分数)

风险百分比或者固定分数的资金管理方法是指在每次交易中分配账户余额或者净值的某一百分比,这在标准库中是以 CmoneyFixedRisk 实现的。如果交易出现亏损,亏损值就是等于进场时账户余额的百分比数值。这个亏损不是指任意亏损,而是指交易中可能出现的最大亏损,也就是说市场触及了交易的止损值。这种方法需要非零的止损才能工作。

每次交易风险百分比的计算是按照下面公式的:

Volume = (balance * account_percentage / ticks) / tick_value

其中:

  • balance – 账户的余额或者净值

  • account_percentage – 风险占账户的百分比(范围: 0.0-1.0)

  • ticks – 止损值,用分时表示

  • tick_value – 使用存款币别计算的数值,交易品种或者资产在每个分时的变化(根据是 1.0 手)

分时(tick)的定义是给定资产或者货币对的最小可能的价格变化。比如, EURUSD 在分数 pip 价格 (5位小数的经纪商) 的分时大小是 0.00001, 它就是货币对最小可能的价格变化。当止损值以点数或者pip表示的时候,结果就是交易进场价格和止损价格之间的差距,单位是点数或者pip。

对于相同的货币对,货币的分时值在4位小数经纪商和5位小数经纪商处是不同的。这是因为,对于4位小数经纪商,1个分时等于1个点(或者pip),而在5位小数经纪商处,一个pip等于10个点。

看一个固定风险资金管理的例子,假设我们在一个美元账户中余额为1000美元,而每次交易风险百分比是 5%,假设分时值为 0.1 而在5位小数经纪商处有 200-point (20 pips) 的止损:

Volume = (1000 * 0.05 / 200) / 0.1 = 2.5 lot

计算得到的手数大小的增加要以来于风险百分比和可用余额的增加,而减少是根据止损和分时数值相关的。账户余额,风险和分时值是基本固定的,而止损数值变量通常是不同的(动态计算),根据之一点,固定风险对于那些在进场价格和止损之间差距没有上方界限的策略来说是不适合的。另一方面,止损值过小可能导致很大的手数大小,而在有些对手数设置有限制的经纪商处可能会有一些问题。这个问题在 MetaTrader 5 中基本可以解决,如果大小太大,可以把订单分成多个交易,然而在 MetaTrader 4 中没有这样的功能 – 必须准备好交易量大小 (分成几个小的交易)来处理大的交易量大小,或者简单避免超出允许的最大手数大小。

在它的 UpdateLotSize 方法中使用的公式:

bool CMoneyFixedFractionalBase::UpdateLotSize(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl)
  {
   m_symbol=m_symbol_man.Get(symbol);
   double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      double balance=m_equity==false?m_account.Balance():m_account.Equity();
      double ticks=0;
      if(price==0.0)
        {
         if(type==ORDER_TYPE_BUY)
            ticks=MathAbs(m_symbol.Bid()-sl)/m_symbol.TickSize();
         else if(type==ORDER_TYPE_SELL)
            ticks=MathAbs(m_symbol.Ask()-sl)/m_symbol.TickSize();
        }
      else ticks=MathAbs(price-sl)/m_symbol.TickSize();
      m_volume=((balance*(m_risk/100))/ticks)/m_symbol.TickValue();
     }
   return NormalizeDouble(last_volume-m_volume,2)==0;
  }

首先,我们取得止损的分时数量,在那之后,我们使用真正的公式来更新类的 m_volume 成员,它会在最终输出中使用。


固定比例

固定比例资金管理计算的是账户中当前余额按比例的交易量大小。这可以认为是一种特殊的固定手数资金管理类型,手数的大小是自动调整的,而不是由交易者人工指定的。如果账户在增长,手数的大小也会在每个阈值之后增长。如果账户在减少,手数大小也会对应地减小。

和固定风险资金管理不同,固定比例不需要非零的止损。这就适合在不需要止损,而使用其他方式管理(根据存款币别的利润/亏损来平仓,等等)的交易中使用。

固定比例资金管理中交易量大小的计算通常使用下面的公式表示:

Volume = base_volume + (balance / balance_increase) *  volume_increment

其中:

  • base_volume – 不论账户大小而增加到总交易量上的交易量

  • balance – 当前账户余额

  • balance_increase – 账户中的余额增长引发手数大小增长的数量

  • volume_increment – 当余额改变了 balance_increase后,交易量要在总交易量上增加/减去的量

举个例子,假定我们的基础交易量是 0.0 手, 而账户中每增加$1000应当把交易量增加0.1手,当前账户价值为 $2,500. 总交易量就应该如下计算:

Volume = 0 + (2500 / 1000) * 0.1 = 0.25 手

这种方法有很多变化,其中的一种是,手数的大小只在某些水平上做更新 (这是一种固定比例资金管理的实现方法). 比如,在上面提到的例子中,计算的交易量为0.25手,但是有时候它可能还是0.2手,只会在余额达到或者超过$3000的时候增加到0.3手。

它的 UpdateLotSize 方法可以如下实现:

bool CMoneyFixedRatioBase::UpdateLotSize(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl=0)
  {
   m_symbol=m_symbol_man.Get(symbol);
   double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      double balance=m_equity==false?m_account.Balance():m_account.Equity();      
      m_volume=m_volume_base+((int)(balance/m_balance_inc))*m_volume_inc;
      m_balance=balance;
     }
   return NormalizeDouble(last_volume-m_volume,2)==0;
  }


每点固定风险 (固定保证金)

每点固定风险的工作方式是,止损中的每个点价值相当于存款中的某个数值,该算法根据交易者想要的分时数值来计算手数大小。比如,如果账户币别是 USD,如果每点固定风险是 2.0,止损中的每个点价值就是 $2。如果交易的止损是200个点,交易的最大风险就是$400 (如果市场达到了交易的止损,亏损就是$400).

对于一个典型的交易者,使用这种类型的资金管理更容易处理,因为交易的风险是以交易者最熟悉的数值来表示的,也就是说,存款。交易者只要声明想要的分时数值,交易量就可以自动计算。分时数值,或者说价格最小变化引起的利润/亏损变化将保持相同,但是总的风险将依赖于交易的止损。

使用这种方法做资金管理的公式,它的 UpdateLotSize 方法将以下面的方式实现:

bool CMoneyFixedRiskPerPointBase::UpdateLotSize(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl=0)
  {
   m_symbol=m_symbol_man.Get(symbol);
   double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      double balance=m_equity==false?m_account.Balance():m_account.Equity();
      m_volume=(m_risk/m_symbol.TickValue());
     }
   return NormalizeDouble(last_volume-m_volume,2)==0;
  }


固定风险 (固定保证金)

固定保证金风险的方法就相当于 MQL5 标准库中的 CMoneyFixedMargin 类,这实际上是每点固定风险资金管理方法的特例。但是,与每点固定风险不同的是,这种方法在交易量的计算中考虑的是整个止损值,不论止损大小如何,风险还是保持不变。在前面的例子中,我们止损是200个点,而最大风险是$400,如果止损减小到100个点,每点固定风险的交易的最大风险将减小($200),而固定保证金资金管理中,最大风险将保持不变($400)。

根据这种公式,我们可以以下面的方式实现 UpdateLotSize 方法:

bool CMoneyFixedRiskBase::UpdateLotSize(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl)
  {
   m_symbol=m_symbol_man.Get(symbol);
   double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      double balance=m_equity==false?m_account.Balance():m_account.Equity();
      double ticks=0;
      if(price==0.0)
        {
         if(type==ORDER_TYPE_BUY)
            ticks=MathAbs(m_symbol.Bid()-sl)/m_symbol.TickSize();
         else if(type==ORDER_TYPE_SELL)
            ticks=MathAbs(m_symbol.Ask()-sl)/m_symbol.TickSize();
        }
      else ticks=MathAbs(price-sl)/m_symbol.TickSize();
      m_volume=(m_risk/m_symbol.TickValue())/ticks;
     }
   return NormalizeDouble(last_volume-m_volume,2)==0;
  }

这里使用的公式和每点固定风险很类似,只是我们需要取得止损的分时数值,然后使用这个值来除之前公式的输出值。


资金管理对象的容器

与之前文章讨论过的信号类类似,我们的资金管理对象将也有一个容器,这可以使EA交易动态地在一系列在平台中载入的,可用的资金管理对象中做出选择。也就是说,这个容器将作为资金管理类与EA交易其他代码之间的中介。这个对象的基类是 CMoneysBase, 它的定义显示如下:

class CMoneysBase : public CArrayObj
  {protected:
   bool              m_active;
   int               m_selected;
   CEventAggregator *m_event_man;
   CObject          *m_container;public:
                     CMoneysBase(void);
                    ~CMoneysBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_MONEYS;}
   //- 初始化   virtual bool      Init(CSymbolManager*,CAccountInfo*,CEventAggregator*);
   CObject          *GetContainer(void);
   void              SetContainer(CObject*);
   virtual bool      Validate(void) const;
   //- 读取和设置函数   virtual bool      Active(void) const;
   virtual void      Active(const bool);
   virtual int       Selected(void) const;
   virtual void      Selected(const int);
   virtual bool      Selected(const string);
   //- 交易量的计算   virtual double    Volume(const string,const double,const ENUM_ORDER_TYPE,const double);
  };

因为这个对象是设计用于包含多个资金管理对象的,它需要至少两个方法才能在EA交易中使用:

  1. 选择, 或者可以在资金管理方法中做动态切换

  2. 使用选定的资金管理对象并取得它的计算的交易量


 选择是通过两种方法做到的: 通过赋予资金管理对象在对象数组中的索引 (CMoneysBase 扩展自 CArrayObj), 或者通过名称搜寻到对象 (CMoneyBase/CMoney 的 Name 方法). 下面展示了重载的 Selected 方法,它接受整数型参数 (或索引):

CMoneysBase::Selected(const int value)
  {
   m_selected=value;
  }

下面展示了重载的 Selected 方法,它接受字符串型参数 (资金管理对象的名称). 请注意,这需要资金管理对象的非空名称,它可以通过 Name 方法来设置。

bool CMoneysBase::Selected(const string select)
  {
   for(int i=0;i<Total();i++)
     {
      CMoney *money=At(i);
      if(!CheckPointer(money))
         continue;
      if(StringCompare(money.Name(),select))
        {
         Selected(i);
         return true;
        }
     }
   return false;
  }

第三个重载的方法是没有参数的方法,它只是简单返回选中的资金管理对象的索引,它只是在EA交易想知道它现在使用的是哪个资金管理方法的时候有用:

int CMoneysBase::Selected(void) const  {
   return m_selected;
  }

实际的交易量是通过这个对象的 Volume 方法计算的,这个方法首先取得选中的资金管理对象的指针,然后调用它自己的 Volume 方法,CMoneysBase 的 Volume 方法的代码展示如下:

double CMoneysBase::Volume(const string symbol,const double price,const ENUM_ORDER_TYPE type,const double sl=0)
  {
   CMoney *money=At(m_selected);
   if(CheckPointer(money))
      return money.Volume(symbol,price,type,sl);
   return 0;
  }

在此,该方法通过对象数组访问对象并以指针保存它,为了避免错误,必须保证真实元素确实存在于对象数组中。


实例

作为例子,我们将使用前一篇文章中最后的例子,我们这样修改它,引入本文中介绍的资金管理类,把它们放到一个单独的容器中,然后把它们加入订单管理器。大多数增加的代码处理都是在EA的 OnInit 函数中进行,显示如下:

int OnInit()
  {//-   order_manager=new COrderManager();
   money_manager = new CMoneys();
   CMoney *money_fixed= new CMoneyFixedLot(0.05);
   //CMoney *money_ff= new CMoneyFixedFractional(5);   CMoney *money_ratio= new CMoneyFixedRatio(0,0.1,1000);
   //CMoney *money_riskperpoint= new CMoneyFixedRiskPerPoint(0.1);   //CMoney *money_risk= new CMoneyFixedRisk(100);   
   money_manager.Add(money_fixed);
   //money_manager.Add(money_ff);   money_manager.Add(money_ratio);
   //money_manager.Add(money_riskperpoint);   //money_manager.Add(money_risk);   order_manager.AddMoneys(money_manager);
   //order_manager.Account(money_manager);   symbol_manager=new CSymbolManager();
   symbol_info=new CSymbolInfo();
   if(!symbol_info.Name(Symbol()))
      Print("没有设置交易品种");
   symbol_manager.Add(GetPointer(symbol_info));
   order_manager.Init(symbol_manager,new CAccountInfo());

   MqlParam params[1];
   params[0].type=TYPE_STRING;#ifdef __MQL5__   params[0].string_value="Examples\\Heiken_Ashi";#else
   params[0].string_value="Heiken Ashi";#endif
   SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar);
   SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar);
   signals=new CSignals();
   signals.Add(GetPointer(signal_ha));
   signals.Add(GetPointer(signal_ma));
   signals.Init(GetPointer(symbol_manager),NULL);//-   return(INIT_SUCCEEDED);
  }

在此,我们包含了用于使用固定分数,固定风险和固定每点风险的资金管理方法,然而,因为这些方法需要非零止损,而我们的EA交易现在进场交易时止损为0,我们现在还不能使用这些方法,此时,我们将只使用固定手数和固定比例的资金管理方法。如果出现这些对象返回无效止损的事件(小于0), 就会使用订单管理器中默认的手数大小 (默认为 0.1 手, 在 CorderManager/COrderManagerBase 的 m_lotsize 类成员中) .

COrderManager 有它自己的类成员,为指向资金管理容器(CMoney)的指针,所以, 使用 COrderManager 也就自然在源文件中包含了资金管理的头文件。如果一个EA交易不会使用 COrderManager,那么就需要在它的源代码中使用 #include 包含资金管理类。

关于 OnTick 函数, 我们这样修改 EA,对于买入仓位, EA 将使用固定手数,而对于卖出仓位,它会使用固定比例的手数大小。这可以在订单管理器的 TradeOpen 方法被调用之前,修改选中的资金管理类型来做到,使用 CMoneys 的 Selected 方法:

void OnTick()
  {//-   if(symbol_info.RefreshRates())
     {
      signals.Check();
      if(signals.CheckOpenLong())
        {
         close_last();
         //Print("买入交易进场..");         money_manager.Selected(0);
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
        }
      else if(signals.CheckOpenShort())
        {
         close_last();
         //Print("卖出交易进场..");         money_manager.Selected(1);
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
        }
     }
  }

因为资金管理从本质上说只是纯计算,我们可以想象,在 MetaTrader 4 和 MetaTrader 5 中计算所得的手数大小是相同的。下面显示了 MetaTrader 4 中 EA 的测试结果 (前 10 个交易):

#时间类型订单大小价格止损获利利润余额
12017.01.02 00:00卖出11.001.051000.000000.00000
22017.01.03 03:00平仓11.001.046790.000000.00000419.9610419.96
32017.01.03 03:00买入20.051.046790.000000.00000
42017.01.03 10:00平仓20.051.045970.000000.00000-4.1010415.86
52017.01.03 10:00卖出31.001.045970.000000.00000
62017.01.03 20:00平仓31.001.042850.000000.00000312.0010727.86
72017.01.03 20:00买入40.051.042850.000000.00000
82017.01.03 22:00平仓40.051.041020.000000.00000-9.1510718.71
92017.01.03 22:00卖出51.001.041020.000000.00000
102017.01.04 02:00平仓51.001.041900.000000.00000-89.0410629.67
112017.01.04 02:00买入60.051.041900.000000.00000
122017.01.04 03:00平仓60.051.039420.000000.00000-12.4010617.27
132017.01.04 03:00卖出71.001.039420.000000.00000
142017.01.04 06:00平仓71.001.040690.000000.00000-127.0010490.27
152017.01.04 06:00买入80.051.040690.000000.00000
162017.01.05 11:00平仓80.051.051490.000000.0000054.0510544.32
172017.01.05 11:00卖出91.001.051490.000000.00000
182017.01.05 16:00平仓91.001.053190.000000.00000-170.0010374.32
192017.01.05 16:00买入100.051.053190.000000.00000
202017.01.06 05:00平仓100.051.058690.000000.0000027.5210401.84


在 MetaTrader 5 中, 我们可以看到下面的结果 (对冲模式,前10个交易):














订单

开启时间订单交易品种类型Volume价格止损获利时间状态注释
2017.01.02 00:00:002EURUSD卖出1.00 / 1.001.05100

2017.01.02 00:00:00已执行
2017.01.03 03:00:003EURUSD买入1.00 / 1.001.04669

2017.01.03 03:00:00已执行
2017.01.03 03:00:004EURUSD买入0.05 / 0.051.04669

2017.01.03 03:00:00已执行
2017.01.03 10:00:005EURUSD卖出0.05 / 0.051.04597

2017.01.03 10:00:00已执行
2017.01.03 10:00:006EURUSD卖出1.00 / 1.001.04597

2017.01.03 10:00:00已执行
2017.01.03 20:00:007EURUSD买入1.00 / 1.001.04273

2017.01.03 20:00:00已执行
2017.01.03 20:00:008EURUSD买入0.05 / 0.051.04273

2017.01.03 20:00:00已执行
2017.01.03 22:00:009EURUSD卖出0.05 / 0.051.04102

2017.01.03 22:00:00已执行
2017.01.03 22:00:0010EURUSD卖出1.00 / 1.001.04102

2017.01.03 22:00:00已执行
2017.01.04 02:00:0011EURUSD买入1.00 / 1.001.04180

2017.01.04 02:00:00已执行
2017.01.04 02:00:0012EURUSD买入0.05 / 0.051.04180

2017.01.04 02:00:00已执行
2017.01.04 03:00:0013EURUSD卖出0.05 / 0.051.03942

2017.01.04 03:00:00已执行
2017.01.04 03:00:0014EURUSD卖出1.00 / 1.001.03942

2017.01.04 03:00:00已执行
2017.01.04 06:00:0015EURUSD买入1.00 / 1.001.04058

2017.01.04 06:00:00已执行
2017.01.04 06:00:0016EURUSD买入0.05 / 0.051.04058

2017.01.04 06:00:00已执行
2017.01.05 11:00:0017EURUSD卖出0.05 / 0.051.05149

2017.01.05 11:00:00已执行
2017.01.05 11:00:0018EURUSD卖出1.00 / 1.001.05149

2017.01.05 11:00:00已执行
2017.01.05 16:00:0019EURUSD买入1.00 / 1.001.05307

2017.01.05 16:00:00已执行
2017.01.05 16:00:0020EURUSD买入0.05 / 0.051.05307

2017.01.05 16:00:00已执行
2017.01.06 05:00:0021EURUSD卖出0.05 / 0.051.05869

2017.01.06 05:00:00已执行


因为订单管理器已经处理了两个平台(和语言)之间的差异, 手数大小计算的方法和结果将是相同的, 如有任何差异都是订单管理器自身的问题。

结论

本文展示了如何在跨平台 EA 交易中应用资金管理方法,它介绍了5个不同的资金管理方法,它也提供了为这些对象指针准备的自定义容器对象,它可以用于动态选择资金管理方法。


全部回复

0/140

量化课程

    移动端课程