2015年5月7日 星期四

用 PHP 計算移動平均線 MA 與指數移動平均線 EMA 的方法

這兩天實作完 KD 與 OBV 指標後, 接著對超簡單的移動平均線 MA 與有一點變化的指數移動平均線這兩種指標有點興趣了, 雖然隨便在哪一個理財網站都可以查到, 但那要動手查呀, 對懶人不利. 凡事都想自己動手做, 這就是 maker 無法克制的衝動.

所謂移動平均線就是過去數天收盤價的平均值, 表示那段期間買進者的平均持股成本. 常用的有短線的 MA5 (周線), MA10 (雙周線), 中線的 MA20 (月線), MA60 (季線), MA120 (半年線), 以及長線的 MA240 (年線) 等. 詳參 :

# 什麼是均線
# 從均線型態找出強勢股

首先要在追蹤資料表設定欄位, 我只看到季線而已 :

      $data_array["trade_date"]="date";                   //交易日 2015-05-07
      $data_array["close"]="float unsigned";           //收盤價
      $data_array["MA5"]="float";                          //5日移動平均
      $data_array["MA10"]="float";                        //10日移動平均
      $data_array["MA20"]="float";                        //20日移動平均
      $data_array["MA60"]="float";                        //60日移動平均
      $data_array["MADIF510"]="float";                //交叉(+黃金-死亡)
      $data_array["MADIF520"]="float";                //交叉(+黃金-死亡)

然後就可以進行計算了, MA5 顧名思義, 必須有五日以上收盤資料才行. MA5 的計算方法其實很簡單, 只要依交易日下降排序, 再取前五筆收盤價計算其平均值即可, 而且可以在查詢資料庫時, 利用 MySQL 的聚合函數 AVG 直接計算, 程式如下 :

    $SQL="SELECT * FROM ".$table." ORDER BY trade_date DESC";
    $RS1=run_sql($SQL);
    //計算 MA5  
    if (count($RS1) >= 5) { //有 5 筆以上資料才計算
      $trade_date=$RS1[0]["trade_date"]; //交易日期
      $SQL="SELECT AVG(close) FROM ".$table." ORDER BY trade_date DESC LIMIT 5";
      $RS2=run_sql($SQL);
      $MA5=round($RS2[0][0],2);  //取小數兩位
      //更新追蹤資料表
      $data_array["MA5"]=$MA5;  //紀錄今日 MA5 結果
      update($table,$data_array,"trade_date",$trade_date);
      $data_array=null;  //清空陣列
      $RS2=NULL;
      }
    else {echo "未達 5 筆收盤資料無法計算 MA5.<br>";}
    RS1=null;

其他 MA10, MA20, MA60 也是如此計算, 只要將 5 改成 10,20, 60 即可.

其次是 EMA 指數移動平均線, MA 雖然簡單易懂, 但卻有反應太慢的問題, 這是大部分落後指標共通的問題. 而 EMA 則是透過一個指數係數, 給予時間較近者較大之權值, 以提高移動平均線的靈敏度, 因此在大漲或大跌時, EMA 會迅速呈現較劇烈之變化, 當然有一好沒兩好, 詳參 :

# 指數平均線 (EMA) 是移動平均線的先鋒軍

根據這篇的介紹, EMA 常用參數為 EMA12 (快速) 與 EMA50 (慢速), 注意這裡的 12 與 50 雖然意義上是天數, 但那只是用來計算權重係數, 並不是說必須要有 50 天的收盤價才能進行計算. 計算 EMA 只要有一筆收盤紀錄即可, 計算公式如下 :

今日EMA=(今日收盤價-昨日EMA)*權重係數+昨日EMA
權重係數=2/(期間+1)

故 EMA12 之權重係數為 2/13, 而 EMA50 則為 2/51

首先須在追蹤資料表新增如下欄位 :

      $data_array["trade_date"]="date";                    //交易日 2015-05-07
      $data_array["close"]="float unsigned";            //收盤價
      $data_array["EMA12"]="float";                       //12日指數移動平均
      $data_array["EMA50"]="float";                       //50日指數移動平均
      $data_array["EMADIF"]="float";                     //EMA交叉(+黃金-死亡)

PHP 程式如下 :

    $SQL="SELECT * FROM ".$table." ORDER BY trade_date DESC";
    $RS1=run_sql($SQL);
    //計算 EMA12, EMA50 :
    if (count($RS1) != 0) { //有 1 筆以上資料才計算
      $trade_date=$RS1[0]["trade_date"]; //交易日期
      $close=$RS1[0]["close"]; //今日收盤價
      if (count($RS1) == 1) {
        $EMA12=$RS1[0]["close"]; //初值=收盤價
        $EMA50=$RS1[0]["close"]; //初值=收盤價
        $EMADIF=0;
        }
      else {
        $EMA12P=$RS1[1]["EMA12"]; //前一日 EMA12
        $EMA12=$EMA12P + 2/13*($close-$EMA12P);
        $EMA50P=$RS1[1]["EMA50"]; //前一日 EMA50
        $EMA50=$EMA50P + 2/51*($close-$EMA50P);
        $EMADIF=$EMA12-$EMA50;
        }
      //更新追蹤資料表
      $data_array["EMA12"]=$EMA12; //顯示時再取小數2位
      $data_array["EMA50"]=$EMA50; //顯示時再取小數2位
      $data_array["EMADIF"]=$EMADIF; //顯示時再取小數2位
      update($table,$data_array,"trade_date",$trade_date);
      $data_array=null;  //清空陣列
      }
    else {echo "無收盤資料無法計算 EMA.<br>";}
    RS1=null;

看起來似乎還比 MA 簡單呢, 因為不用再查一次資料表.

[2015-06-05 修正] :

昨晚檢查一周來財工程式的執行數據, 發現計算出來的平均值不正確, 在 Google 搜尋過後才搞清楚, 原來 MySQL 的 LIMIT 功能不是我想的那樣, 它是在最後才 apply 的, 例如下面的 SQL :

"SELECT AVG(close) FROM ".$table." ORDER BY trade_date DESC LIMIT 5";

MySQL 會先對 close 欄位的所有數據取平均值後, 再取前面五筆, 由於平均值結果只有一筆, 所以也就得到一筆資料, 但卻是全部 close 的平均, 而非最近五筆 close 的平均, 參考下列文章 :

Select average from MySQL table with LIMIT
# Subquery with limit clause : Sub query « Select Clause « SQL / MySQL
# Limit not working for avg query
avg() and limit

"you have to consider how LIMIT works. It's the last thing that happens as the query is executed."

所以, 上面所有計算平均值的 SQL 指令都必須修改, 例如 MA5 要改成 :

      $SQL="SELECT AVG(items.close) FROM (SELECT close FROM ".$table.
           " ORDER BY trade_date DESC LIMIT 5) AS items";

亦即要採取 sub-query 的兩層查詢方法, 先用一個 SELECT ... LIMIT  5 指令找出最近的五筆紀錄, 並命名為 items, 再用 AVG 去計算 close 欄位的平均值.


沒有留言 :