免費下載
訂閱模組
搜尋

集保相關欄位調整


選擇單元...
在7月中我們會針對集保中心每週收盤後所提供的股東統計資料欄位,包含大戶持股比例/人數、散戶持股比例/人數等選股欄位進行調整。

以下我們會說明調整的原因,調整後的行為,以及如何事先調整腳本來因應此次的修改。

 

1. 調整原因

集保中心會定期公佈每檔股票當週的各級股東的人數,張數,比例等數字。這些欄位包含”大戶持股比例”, “大戶持股人數”, “大戶持股張數”, “散戶持股比例”, “散戶持股人數”, “散戶持股張數”, “總持股人數”, “集保張數”, 以及 “集保張數佔發行張數百分比”等。

例如以下的選股腳本:

value1 = GetField(“大戶持股比例”, “W”, param:=1000); // 千張大戶持股比例

 

這些欄位資訊一般都是在星期六上午公佈當週的持股統計,所以如果是執行選股的話,通常要在星期六/日才有辦法取得該週的資料。

由於目前XS是把這些欄位標示成週頻率,每一週一筆,所以在使用歷史資料進行回測時,只要回測運算的那根K棒位於同一週的話,腳本就可以看到這一筆資料,產生了偷看到未來資料的問題。

例如以下的選股腳本:

value1 = GetField("大戶持股人數", "W", param:=1000);

value2 = GetField("大戶持股人數", "W", param:=1000)[1];

condition1 = value1 > value2;

if condition1 then ret=1;

Print(

  FormatDate("yyyy/MM/dd", Date), 

  NumToStr(value1, 0), NumToStr(value2, 0), condition1);

 

如果回測的區間是2023/06/05 ~ 2023/06/21,執行商品為00878.TW的話,那麼目前XS跑出來的資料是這樣子的:


2023/06/05 125 122 TRUE 

2023/06/06 125 122 TRUE 

2023/06/07 125 122 TRUE 

2023/06/08 125 122 TRUE 

2023/06/09 125 122 TRUE 

2023/06/12 124 125 FALSE 

2023/06/13 124 125 FALSE 

2023/06/14 124 125 FALSE 

2023/06/15 124 125 FALSE 

2023/06/16 124 125 FALSE 

2023/06/19 123 124 FALSE 

2023/06/20 123 124 FALSE 

2023/06/21 123 124 FALSE 


 

在6/5日這一天,XS就認為當週的千張大戶股東人數比上一週增加。可是事實是這樣子嗎?

從股權分散表的圖看起來,6/5日到6/9日那一週千張大戶人數是125人,的確是比上一週增加的,可是如果我們時間回推到6/5日那一週,因為交易所是在週末才公佈當週的大戶持股人數,所以6/5日,6/6日這幾天,使用者是沒有辦法知道這件事情的!使用者必須等到6/9日收盤後(星期五),通常要等到6/10日(星期六)時如果執行選股的話,才會知道這件事情。

也就是說目前XS回測的結果,會讓使用者提前知道當週收盤後才會發生的事情,那麼回測的績效就會產生偏差。

目前為了解決這樣子的問題,有用戶很聰明的在腳本內加入了以下的判斷方式:

var: sameweek(0);

if GetInfo("FilterMode") = 1 then  

    sameweek = 0 

else

    sameweek = 1; 

value1 = GetField("大戶持股人數", "W", param:=1000)[sameweek];

value2 = GetField("大戶持股人數", "W", param:=1000)[sameweek+1];

condition1 = value1 > value2;

if condition1 then ret=1;

Print(

  FormatDate("yyyy/MM/dd", Date), 

  NumToStr(value1, 0), NumToStr(value2, 0), condition1);

在紅字的部分,腳本內利用GetInfo("FilterMode")來判斷目前是跑選股,還是跑選股回測/回溯,如果是跑回測/回溯的話,那麼抓取大戶持股資料時,就往前偏一期,也就是說不抓當週的資料,改抓上一週的資料。

這樣子跑出來的資料是長這個樣子的:


2023/06/05 122 129 FALSE 

2023/06/06 122 129 FALSE 

2023/06/07 122 129 FALSE 

2023/06/08 122 129 FALSE 

2023/06/09 122 129 FALSE 

2023/06/12 125 122 TRUE 

2023/06/13 125 122 TRUE 

2023/06/14 125 122 TRUE 

2023/06/15 125 122 TRUE 

2023/06/16 125 122 TRUE 

2023/06/19 124 125 FALSE 

2023/06/20 124 125 FALSE 

2023/06/21 124 125 FALSE 


 

在6/5日到6/8日之間,這時候腳本就不認為千張大戶股東人數增加(正確)。

可是,6/9日那一天也不認為有增加,腳本要等到6/12日(星期一)才會認為真的增加了。

這代表著,使用者要等到星期一下午收盤時(因為這是執行選股,每日收盤後資料才會異動)才會發現千張大戶股東人數增加了,可是事實上,因為資料是星期五收盤後就公佈了,所以星期一開盤前市場上的人就都已經知道這件事情了!!

也就是說透過自行偏移一期的方式,雖然可以解決提前偷看資料的問題,可是也產生了訊號延後到星期一收盤後才會產生的問題。

 

2. 調整方式

為了讓腳本抓取資料的期別可以跟事實一致,在新版的環境內(預計2023/7月中旬上線),XS選股中心會改成依照集保公布的日期來決定腳本可以看到的資料期別,不管是跑選股,選股回溯,或是選股回測,都不會發生取到未來資料的狀況。簡單來說,只要使用 GetField("大戶持股比例", "W"), 系統就會自行判斷在當下的那個時間點腳本應該要看到的大戶持股比例資料,使用者再也不需要在腳本內自行決定是否需要抓取當期值還是前期值。

以下是第一版的腳本,在新版環境內看到的數值:


2023/06/05 122 129 FALSE 

2023/06/06 122 129 FALSE 

2023/06/07 122 129 FALSE 

2023/06/08 122 129 FALSE 

2023/06/09 125 122 TRUE 

2023/06/12 125 122 TRUE 

2023/06/13 125 122 TRUE 

2023/06/14 125 122 TRUE 

2023/06/15 125 122 TRUE 

2023/06/16 124 125 FALSE 

2023/06/19 124 125 FALSE 

2023/06/20 124 125 FALSE 

2023/06/21 123 124 FALSE 


 

注意到在6/5日到6/8日,此時抓到的是上一週的數值,所以千張持股人數不會增加。可是6/9日(星期五)這一天,抓到的會是當週收盤後所公佈的更新數值(125人),所以從選股的角度而言,星期五收盤後就會觸發這個條件,用戶可以在星期一開盤時進場。

 

3. 腳本因應方式

3.1. 依照新舊版環境決定是否要抓取前期值

如果您原先的選股腳本並沒有針對現行集保欄位會偷看到答案做任何處理的話,那麼您的腳本無須做任何修改。

可是如果您已經使用類似GetInfo(“FilterMode”)的機制來判斷是否要抓當期或是前一期的資料,那麼我們建議您可以先做這樣子的修改,避免系統上線時抓錯資料。

var: sameweek(0);

if GetInfo("TDCC_Mode") = 0 then begin

    if GetInfo("FilterMode") = 1 then  

        sameweek = 0 

    else

        sameweek = 1; 

end;

value1 = GetField("大戶持股人數", "W", param:=1000)[sameweek];

value2 = GetField("大戶持股人數", "W", param:=1000)[sameweek+1];

condition1 = value1 > value2;

if condition1 then ret=1;

Print(

  FormatDate("yyyy/MM/dd", Date), 

  NumToStr(value1, 0), NumToStr(value2, 0), condition1);

請注意上面腳本內紅字的調整。

GetInfo(“TDCC_Mode”)是為了這次調整新增的判斷方式:如果您的程式是執行在目前的環境的話,會回傳0,如果您的程式是執行在調整後的環境的話,則會回傳1

所以上面程式碼的意思就是,如果是跑在舊的環境的話,那就在回測/回溯時抓前一期的資料,可是如果是跑在新的環境的話,那就不需要特別處理,直接抓取大戶持股人數即可。

之後等到新環境上線之後,您就可以移除這些邏輯,改成最直覺的寫法即可。

 

3.2. 請使用GetField來抓取前期值[1]

我們發現有很多客戶會使用以下的寫法來比對當期欄位數值跟前期欄位數值,例如以下的腳本:

value1 = GetField("大戶持股人數", "W", param:=1000);

if value1 > value1[1] then ret=1;

這樣子的寫法在這次改版後可能會產生問題。

由於之後系統在執行GetField("大戶持股人數", "W", param:=1000)時,會依照實際資料是否公佈來決定腳本會看到的大戶持股人數的欄位,例如如果今天是星期四的話,那這時候就會抓到上一週末所公佈的數值,如果是星期五的話,就會抓到當週公佈的數值。

上面的腳本如果是跑日頻率的話:

  • 如果今天是星期四,value1是上一週的大戶持股人數,
  • 上一根K棒是星期三,value1也是上一週的大戶持股人數,
  • value1 = value1[1],都抓到同一期的資料,不會觸發

上面的腳本如果是跑週頻率的話,也會有問題喔:

  • 如果今天是星期四,value1是上一週的大戶持股人數,
  • 上一根(週)K棒是上星期五,value1也是上一週的大戶持股人數,
  • value1 = value1[1],都抓到同一期的資料,不會觸發

基本上,當欄位是依照資料實際公佈日來決定數值時,腳本內使用value1[1]的方式,都很容易會產生問題。相關的議題我們會再另外一篇文章內說明。

要避開這樣子的問題,請務必使用GetField(..)[1]這樣子的方式來抓取前期數值,例如:

// 使用這樣子的寫法
//
if GetField("大戶持股人數", "W", param:=1000) > 
   GetField("大戶持股人數", "W", param:=1000)[1] then ret=1;

 

// 不要這樣子寫
//
value1 = GetField("大戶持股人數", "W", param:=1000);
if value1 > value1[1] then ret=1;


// 使用這樣子的寫法
//
if trueall(
   GetField("大戶持股人數", "W", param:=1000) > 
   GetField("大戶持股人數", "W", param:=1000)[1], 3) then ret=1;

// 不要這樣子寫
//
value1 = GetField("大戶持股人數", "W", param:=1000);
if trueall(value1 > value1[1], 3) then ret=1;


// 使用這樣子的寫法
// 
if LinearReg(GetField("大戶持股人數", "W", param:=1000), 5) > 0 
then ret=1;

// 不要這樣子寫(LinearReg會抓取前期序列數值)
//
value1 = GetField("大戶持股人數", "W", param:=1000);
if LinearReg(value1, 5) > 0 then ret=1;