AmiBroker Code Fixing Problems from Data

หลายๆครั้งที่ผมเห็น นักพัฒนากลยุทธ์ระบบเทรด (เรียกสั้นว่า “ควอนทฺ” quants) ได้ทำการสร้างกลยุทธ์ที่ดีมีหลักการเหตุผล ถึงขั้นที่จะนำไปเทรดได้กำไรจริง แต่ปรากฎว่า ผลลัพธ์จากการทดสอบ (backtest result) กลับบอกว่ากลยุทธ์นั้นไม่กำไร/ขาดทุน ทำให้ quants เข้าใจผิดทิ้งกลยุทธ์นั้นไป (++ผิดหวังและท้อแท้) พลาดโอกาสในการลงทุนด้วยระบบของตนเอง เพียงเพราะ “ปัญหาที่เกิดจากข้อมูลต่อการทดสอบ” ซึ่งจริงๆแล้วสามารถป้องกันได้ด้วยการใช้โค้ดไม่กี่บรรทัด

กลยุทธ์ที่ดีมีหลักการเหตุผล แต่ผลการทดสอบกลับขาดทุน
อาจเพราะ โค้ดในการทดสอบไม่ตรวจสอบข้อมูลที่ผิดเพี้ยน

เพื่อช่วยป้องกันเหตุการณ์ที่น่าเสียดายข้างต้น (แทนที่จะได้กลยุทธ์ดีๆของตนเองมาเทรดทำกำไร) ดังกล่าว quants จะต้องเข้าใจและโค้ด (Understanding & Coding) เรื่องการจัดการข้อมูลที่เกี่ยวข้องกับ หุ้นที่… split, delisted, renamed, suspended, และ tick size ตั้งแต่ก่อนจะทำการทดสอบ

ดังนั้นจึงมีความจำเป็นมากๆที่ต้องเพิ่ม AmiBroker Code เพื่อหลีกเลี่ยงปัญหาต่างๆข้างต้น ถึงแม้ว่าโค้ดในส่วนนี้ จะไม่เกี่ยวข้องใดๆกับกลยุทธ์ก็ตาม โดยที่ Code ตัวช่วยจะเป็นชุดคำสั่งโค้ดเล็ก หรือที่เรียกกันว่า “Code Snippet” ซึ่งในโพสนี้จะแสดงตัวอย่างจริงพร้อมคำอธิบาย ตามด้วยวิธีแก้ไขปัญหาที่เกิดจากข้อมูลทั้งแบบแก้ไขที่ตัวข้อมูลและแบบแก้ไขที่โค้ด

 ปัญหา ตัวอย่าง วิธีสังเกตุ และทางแก้ไข

กรณี 1 แตกพาร์แบบปรกติ Stock Split

เช่น GL 1 หุ้น แตกเป็น 10 หุ้น เมื่อวันที่ 15/05/13

ปัญหาในกรณีสามารถสังเกตุได้จาก Backtest Result ว่ามีการขาดทุนหนักๆ มากกว่าค่า ApplyStop ประเภท StopLoss (ทั้งกระนั้นผลจากการทดสอบก็เชื่อถือไม่ได้อยู่ดี)

จากรูปข้างบนถ้าถือ position ข้ามวันที่  split จะทำให้ ผล backtest ขาดทุน 90% เพราะ AmiBroker รู้แต่ว่า ราคาเปลี่ยนจากประมาณ 100 บาท/หุ้น เหลือ 10 บาท/หุ้น และไม่ได้ปรับจำนวนหุ้นของ position นี้ จาก 1 เป็น 10 หุ้น ให้โดยอัตโนมัติ ทั้งๆที่ในโลกความเป็นจริงกลับไม่มีการขาดทุน เพราะทางโบลกเกอร์จะทำการปรับจำนวนหุ้นให้ที่ราคาหลัง split

ดังนั้น quants ควรต้องจัดการปัญหาในส่วนนี้ ซึ่งหลักๆ มีอยู่ 2 ทาง คือ…

1.1. แก้ไขข้อมูลด้วยการปรับ split

ภาพข้างล่างคือการแก้ไขข้อมูลปรับ  split ซึ่งจะทำให้ผลการทดสอบเป็นไปอย่างถูกต้อง ตรงนี้คงต้องขอเน้นคำว่า “ผลการทดสอบ” เพราะ ในด้าน Backtest ทุกอย่างจะไม่มีปัญหาใดๆจาก split ที่ถูกปรับค่าแล้ว เลือกหุ้น GL แล้วไปที่ Menu Symbol->Split->Date 15/05/2013 วันแรกหลังถูก split, new:old = 10:1

ซึ่งจะทำให้ได้กราฟหุ้นหลัง Split Adjusted เป็น

แต่ปัญหาหลังปรับ Split ที่ตามมา คือ “พี่ป๊อบ ปองกูล โลกสองใบ” (555) เพราะ GL ก่อน split ที่ราคาประมาณ 20-100 บาท ในช่วง 2010 ถึง 2013 อาจมีผู้เล่น players ต่างจากหลัง ที่ราคาหลัง Split มาอยู่ที่ 10 บาท/หุ้น

เช่นกรณี AOT ที่ราคา 400/หุ้น และหลัง split (จำนวนหุ้น new:old ที่ 10:1) เป็น 40/หุ้น ณ 09/02/17 ทำให้นักลงทุนรายย่อยจำนวนมากสามารถลงทุนใน AOT ได้ และราคาหุ้นได้ขึ้นเกิน 70 บาท ประมาณ 75% ภายใน 1 ปี ซึ่งคงเป็นไปได้ยากถ้า AOT ไม่มีการ split แล้วหุ้นจะขึ้นจาก 400 ไปเป็น 700 บาท (75%)

อีกทั้งกลยุทธ์ที่มีเงื่อนไขทางด้านราคา Price Range เช่น Buy = C < 10; ซึ่งใน Backtest ด้วยข้อมูลหลัง split จะทำให้สามารถ (แบบผิดๆ) เข้าซื้อแทบในทุกช่วงเวลา ทั้งที่ในโลกของความจริง มันเป็นไปไม่ได้ (พี่ป๊อบจะมีโลกสองใบไม่ได้)

ยิ่งหุ้นที่มีราคาสูงๆ หลักหลายร้อย
ปัญหาเรื่องการปรับ Split ยิ่งมีผล
โดยเฉพาะกลยุทธ์ที่มี Price Range

1.2. ใช้โค้ดแก้ปัญหาเรื่อง Split

ลองกลับไปมองภาพบนสุดที่ไม่มีการปรับ split จะเห็นได้ว่า เราสามารถเขียนโค้ดหลีกเลี่ยงการถือ position ข้ามวันที่จะมีการ split ด้วย

  • การไม่ซื้อ buyConAvoidSplit ที่กำลังจะมีการ split ในอีกไม่กี่วันข้างหน้า
  • การขายหุ้น sellConAvoidSplit ที่กำลังจะมีการ split ในอีกไม่กี่วันข้างหน้า

ด้วยการตรวจสอบทั้ง ceiling บวกเกิน 35% และ floor ติดลบเกิน 35% จากราคาเปิดหรือปิดของวันนี้ Ref(C, 0) กับพรุ่งนี้ Ref(C, 1) และวันหลังวันพรุ่งนี้ Ref(C, 2)

Code snippet ข้างต้น…

  1. จะซื้อเมื่อไม่เจอ split  (detectSplit ไม่จริง) ภายใน 1 ถึง 2 วันข้างหน้า โดยให้นำ buyConAvoidSplit ไป AND กับ ฺBuy ของกลยุทธ์ของเรา
  2. จะขายเมื่อเจอการ split (detectSplit เป็นจริง) ภายใน 1-2 วันข้างหน้า โดยให้นำ sellConAvoidSplit ไป OR กับ Sell ของกลยุทธ์ของเรา

ล่าสุด กลต ประกาศปรับ ceiling และ floor ชั่วคราวไม่เกิน 15% (0.15) ระหว่างวันที่ 18/03/20 ถึง 30/06/20 ทำให้ต้องมีการเพิ่มช่วงของเงื่อนไขในการ detectSplit เป็น

ควรมีข้อมูลทั้งแบบ Split และ Free Split
แต่ถ้าต้องเลือก ให้ใช้ Free Split เป็นหลัก
แล้วใช้ Code ช่วยจัดการปัญหาด้านข้อมูล

กรณี 2 กรณีแตกพาร์แบบตรงข้าม Reverse Stock Split

เช่น BTS 6.25 หุ้น รวมเป็น 1 หุ้น เมื่อวันที่ 10/08/2012

ปัญหาในกรณีสามารถสังเกตุได้จาก Backtest Result ว่ามีกำไรหลายร้อยเปอร์เซ็นต์ มากกว่าค่า StopProfit (ทั้งกระนั้นผลจากการทดสอบก็เชื่อถือไม่ได้อยู่ดี)

จากรูปข้างบนถ้าถือ position ข้ามวันที่  split จะทำให้ ผล backtest กำไร 600% (ใส่สีแดงเพราะว่ามันกำไรแบบผิดๆ) จากราคา 0.85 ไปเป็น 5.38 ซึ่งกรณีนี้ก็เหมือนกับกรณี 1 ที่อธิบายไปแล้ว โดยในกรณี BTS นี้ จะไม่หนักเท่ากับ PTT เพราะราคา BTS ที่เปลี่ยนไปยังอยู่ใน Price Range เดิม ที่นักลงทุนทั่วๆไปลงทุนกัน (แต่ถ้าเปลี่ยนจาก 1 บาท ไปเป็น 100 นี่ก็ต้องว่ากันอีกที)

2.1. แก้ไขข้อมูลด้วยการปรับ split

ปรกติกรณี Reverse Split จะไม่ค่อยเจอกัน แต่ในกรณี BTS นี่โดนเต็มๆ 2 ดอก (จ่ายไป 4,000 ออกจากรัชดาแทบหมดตัว) เพราะมี ราคาหุ้น BTS ที่ผิดเพี้ยนก่อนวัน reverse split สังเกตุในรูป/ตารางว่า วันที่ 09/08/12 มี Close และ High เป็น 5.38 ในขณะที่ ราคาเปิดจริงๆ  Open คือ 0.86

ดังนั้นถ้าจะแก้ไขปัญหาของ BTS จะต้องแก้ปัญหาเรื่องราคาที่ผิดเพี้ยนก่อน และค่อยแก้ปัญหาเรื่อง split

  1. ต้องเข้าไปแก้ไขด้วย Menu Symbol->Quote Editor… แล้วไปเลือกวันที่ 09/08/12 ให้แก้ไข High กับ Close เป็น 0.86
  2. แล้วไปที่  Menu Symbol->Split->Date 10/08/2012 วันแรกที่ถูก split, new:old = 0.64:4

หลังจากที่ทำขั้นตอนทั้ง 2 ข้างต้นเสร็จจะได้รูปเหมือนด้านล่าง

ข้อมูลเรื่อง Split สามารถดูได้ที่ set.or.th ของ factsheet หุ้นนั้นๆ เช่น กรณีนี้ คือ https://www.set.or.th/set/factsheet.do?symbol=BTS โดยให้เข้าใจว่า ที่ set.or.th ชี้แจงเป็นราคาพาร์ เก่า:ใหม่ แต่ใน AmiBroker ใช้จำนวนหุ้น new:old ทำให้สามารถกรอก สัดส่วนจาก set.or.th เข้าไปใน AmiBroker ได้เลย ไม่ต้องคิดเยอะ

หมายเหตุ: เรื่องราคาหุ้นที่ผิดเพี้ยนเช่นในกรณี BTS นี้มีบ้าง วิธีที่ดีที่สุดในกรณีที่ data provider ไม่ทำให้ คือการเขียนโปรแกรมตรวจสอบและแก้ไข ด้วย VBA ผ่านทาง AmiBroker’s OLE ซึ่งมีสอนในคอร์ส AmiBroker Quant Course (แอบขายคอร์สครับ) ทั้งบทที่ 5 AmiBroker OLE Automation

2.2. ใช้โค้ดแก้ปัญหาเรื่อง Reverse Split

วิธีแก้ Reverse Split แบบที่สอง สามารถใช้โค้ดชุดเดียวกันกับ Split ได้เลย เพราะโค้ดที่แสดงไว้มองส่วนต่างเป็นแบบ Absolute สังเกตุ ฟังก์ชั่น abs ในโค้ดบรรทัดที่ 2 และ 3 ซึ่งมองการกระโดดของราคาทั้งขาขึ้นและขาลง (ไม่น่าเชื่อว่า ผมจะใช้เวลาเกือบ 10 ปี ถึงกลับมาแก้ไขโค้ดในส่วนนี้ บางทีอะไรที่มันใกล้ตัวเกินไป เราก็มักจะมองข้าม โดยเฉพาะยิ่งกรณีที่มันใช้งานได้อยู่แล้ว)

 

กรณี 3 ออกจากตลาด Delisted

เช่น  PHATRA เมื่อวันที่ 24/08/2012

กรณีนี้ถ้ากลยุทธ์ใช้ PositionSize เล็กๆ จะสังเกตุได้ยากมาก ยกเว้นเสียแต่ว่านั่งตรวจสอบเป็นรายตัว หรือให้สังเกตุที่ #bars in largest win/loss จาก Backtest Result ว่าทำไมมันถือมานานมาก ซึ่งก็ยังไม่แน่เพราะนอนอยู่ดี (ปวดหัวครับ ใช้โค้ดเถอะ บรรทัดเดียวเอง)

ปัญหาที่เกิดขึ้นใน  backtest result ถ้ายังมี position ณ สิ้นวันของหุ้น delisted จะเกิดเป็น open position ซึ่งจะไม่มีวันปิดสถานะ (hanging ค้างคา) เนื่องจากไม่มีข้อมูลใดๆทั้งสิ้นมาใช้ในการคำนวณหาเงื่อนไข Sell หรือเงื่อนไข ApplyStop ซึ่งในกรณีนี้ quants ไม่สามารถแก้ไขที่ข้อมูลได้ วิธีเดียวที่ทำให้คือ การใช้ Code ให้ขายหุ้นทิ้งในวันสุดท้าย (หรือก่อนวันสุดท้าย) ที่จะ delisted ตามที่แสดงไว้ข้างล่าง

หมายเหตุ 1. ตอนแรกผมนึกว่าใช้ ApplyStop แบบ NBar ถ้าถือเกิน … วัน ก็ให้ขายทิ้งเพื่อปิด position แต่ปรากฎว่า AmiBroker มองว่าถ้าไม่มีข้อมูล จะไม่นับเป็นวันที่ถือครอง จริงๆ อาจลองใช้ Pad and Align กับ SET Index ซึ่งช่วยจัดการพวก data holes แต่ผมต้องการแก้ปัญหาด้วยโค้ดและเพื่อกันลืมไปแก้ใน AmiBroker Interface เลยใช้แบบนี้ตามโค้ดข้างต้นแน่นอนสุด (ไม่ต้องไปปรับค่าใดๆที่ Interface ของ Analysis settings->General)

หมายเหตุ 2. ค่าลบหนึ่งในสูตรขึ้นอยู่กับการ SetTradeDelays ด้วย แนะนำให้ลองเปลี่ยนเป็น 0 ดู ทั้งในสูตร และใน SetTradeDelays เพื่อศึกษาการทำงานของสูตร

 

กรณี 4 เปลี่ยนชื่อ Renamed

เช่น หุ้น SPPT  ได้มีการเปลี่ยนชื่อเป็น  NEX เมื่อวันที่ 17/05/19

วิธีสังเกตุจะเหมือนกับกรณี Delisted ที่มี position ถือนานๆยาวๆผิดปรกติ และปัญหาก็จะเหมือนกัน คือ ถ้ามีการถือ Position ก่อนการเปลี่ยนชื่อ หุ้นตัวนี้จะค้างเป็น hanging/dangling position ไปเรื่อยๆ ไม่มีวันปิด

ชื่อเก่า SPPT

ชื่อใหม่ NEX

4.1 แก้ไขข้อมูลด้วยการ Merge

  1. ให้เลือกหุ้นชื่อใหม่ (Destination) ไว้ก่อน เช่น NEX
  2. ไปที่ Menu Symbol->Merge…  ที่ merge with ให้เลือกชื่อตัวเก่า เช่น SPPT
  3. ติ๊กที่ช่อง Delete “merge with” security afterwards อย่าลืมทำขั้นนี้ไม่งั้นหุ้นชื่อเก่าจะยังอยู่ ทำให้เกิดปัญหาเดิม แต่ถ้าลืมก็ให้ไปลบหุ้นเก่าทิ้งเอง

 

4.2 ใช้โค้ดแก้ปัญหาเรื่อง Renamed ใช้โค้ดของกรณี Delisted ด้านบนได้เลย

ใช้โค้ดของ Delisted จบ (หล่อได้อีก)

 

กรณี 5 ราคาหุ้นผิดเพี้ยนจากระดับราคา และช่วงราคา Tick Size

ปัญหานี้จะสร้างความผิดเพี้ยนใน ผลการทดสอบ เมื่อมีการเทรดเยอะๆ จำนวนเทรดเยอะๆ เพราะทุกครั้งที่ซื้อขาย ราคามันเพี้ยนนิดๆหน่อยๆสะสมเป็นมาก ปัญหานี้จะเกิดกับข้อมูลที่ถูกต้อง 100%  ด้วย ถ้าการผนวก Slippage ไม่ว่าจะเป็นแบบให้ค่าเป็น fix percent หรือ random ก็ตาม ดังนั้นกรณีนี้จึงป้องกันด้วย Code ซึ่งได้สอนไปแล้วเมื่อ 5 ปี ที่แล้ว (บางทีก็ลืมว่าเคยสอนอะไรไปบ้าง) ให้อ่านที่ https://thaiquants.com/vlog/adjust-tick/

Code แก้ปัญหาเรื่อง Tick Size จะจัดการกับ “ราคา” BuyPrice และ SellPrice ซึ่งต่างจากปัญหากรณีอื่นๆข้างต้นที่จัดการกับ “เงื่อนไข” Buy และ Sell

 

จากตัวอย่างที่ยกมาข้างต้น ภายใต้การทดสอบตั้งแต่ 01/01/2012 ถึง 31/12/2013 เทรดเฉพาะ GL, BTS, และ PRATRA ด้วยการทำ Forced Signals ส่วนกรณีอื่นก็คล้ายๆกัน

ผล Backtest เมื่อไม่ใช้ Code ป้องกัน Split และ Delisted จะสังเกตุได้ว่า BTS กำไรจาก reverse split และ GL กำไรจาก split ซึ่งไม่ถูกต้อง ส่วนสถานะ Long (n-bar) คือการใช้ NBar Stop ในการปิด position สำหรับการทดสอบ แต่จะเห็นได้ว่า PHATRA ยังคงเป็น Open Long สถานะเปิดค้างไว้ hanging position ซึ่งจะไม่มีวันปิด (อย่างเศร้า)

 

ผล Backtest เมื่อใช้ Code ป้องกัน Split และ Delisted รวมถึง Tick size จะเป็นว่า GL และ BTS ถูกปิด position ก่อนที่จะมีการ split  และ PHATRA จะถูกปิดก่อนที่จะ delisted

 

อย่างที่ชี้แจงตั้งแต่ต้นว่า…

บางครั้งปัญหาไม่ได้อยู่ที่กลยุทธ์

แต่อยู่ที่ข้อมูลและวิธีที่เราจัดการมัน

ThaiQuants หวังว่า นักลงทุนด้วยระบบเทรดในเมืองไทย คนไทย จะนำโค้ดที่อธิบายไว้ไปใช้กัน เพื่อให้ได้ผล Backtest ที่ถูกต้อง กำจัด/บรรเทาปัญหาที่มาจากข้อมูล

 

All the Best

ThaiQuants.com

 

Full AmiBroker Code fixing Problems from Data