← กลับไปยังบทความ
July 3, 2026
อ่าน 5 นาที

พื้นที่พารามิเตอร์สองแกน: ทำไมการสวีปส่วนใหญ่ของคุณจึงควรแทบจะฟรี

พื้นที่พารามิเตอร์สองแกน: ทำไมการสวีปส่วนใหญ่ของคุณจึงควรแทบจะฟรี
#อัลโกเทรดดิ้ง
#แบ็กเทสต์
#การค้นหาพารามิเตอร์
#ออปติไมเซชัน
#การแคช
#คำสาปแห่งมิติ
Part 4 of 10 · Collection
High-Performance Backtest Engines

ส่วนหนึ่งของซีรีส์ "แบ็กเทสต์ไร้ภาพลวงตา" (Backtests Without Illusions)

คำสาปแห่งมิติ (curse of dimensionality) มักถูกเล่าในฐานะคำเตือน: พารามิเตอร์ทุกตัวที่คุณเพิ่มเข้ามาจะคูณขนาดของพื้นที่ค้นหา ดังนั้นกลยุทธ์ 18 มิติจึงดูสิ้นหวังเกินกว่าจะสวีปได้ กรอบคิดแบบนั้นแอบตั้งสมมติฐานเงียบ ๆ ว่าทุกมิติมีต้นทุนการประเมินเท่ากัน ซึ่งไม่จริง ในเอนจิน dual/triple-timeframe ของเรา พารามิเตอร์สองในสามแทบไม่มีต้นทุนในการสวีป ขณะที่อีกหนึ่งในสามแบกภาระค่าคำนวณเกือบทั้งหมดไว้ เมื่อคุณมองเห็นการแบ่งนี้แล้ว คำบ่นว่า "พื้นที่ค้นหาใหญ่เกินไป" ก็ไม่ใช่คำบ่นที่ถูกต้องอีกต่อไป คำถามที่ถูกต้องคือ: คุณกำลังจ่ายให้กับมิติไหนกันแน่?

มิติที่ไม่จำเป็นต้องคำนวณซ้ำ

เบนช์มาร์กการค้นหาพารามิเตอร์ของเรารันกลยุทธ์โมเมนตัมแบบหลายไทม์เฟรม: เส้นค่าเฉลี่ยเคลื่อนที่แบบ Hull และตัวแปรที่ปรับเรียบสามชั้นของมัน (HMA3) บนสองหรือสามไทม์เฟรม พร้อมเกตวัดระยะแยกเชิงทิศทาง (directional-separation gate) ที่ตัดสินว่าจุดตัด (crossover) หนึ่ง ๆ "สะอาด" พอที่จะลงมือหรือไม่ พื้นที่ค้นหาเต็มรูปแบบของตัวแปร triple-TF มีสิบแปดมิติ — ค่า period และความยาว HMA ต่อไทม์เฟรม บวกกับชุดค่าเกณฑ์ separation สำหรับการเข้าและออกบนแต่ละไทม์เฟรม

วิธีสวีปแบบตรงไปตรงมาที่สุดคือลูปเดียว: เลือกเวกเตอร์พารามิเตอร์หนึ่งชุด สร้างอินดิเคเตอร์ทุกตัวขึ้นใหม่จากศูนย์ รันซิมูเลชัน ให้คะแนน แล้วทำซ้ำ เราก็เริ่มจากตรงนั้น มันช้า และมันช้าด้วยเหตุผลที่พอเราตั้งชื่อให้มันได้ก็ชวนให้อายตัวเอง: สำหรับการทดลองที่อยู่ติดกันจำนวนมหาศาล เรากำลังคำนวณอินดิเคเตอร์ที่ไม่ได้เปลี่ยนแปลงเลยซ้ำใหม่

ลองขยับค่าเกณฑ์ entry-separation จาก 0.03 เป็น 0.035 แล้วรันใหม่ เส้น Hull MA บนแท่ง 1 นาทีตลอดหนึ่งปีจะเหมือนกับการทดลองก่อนหน้าทุกบิต — ค่าเกณฑ์ไม่ปรากฏอยู่ที่ไหนเลยในนิยามของมัน แต่ลูปแบบตรงไปตรงมาก็ยังสร้างมันขึ้นใหม่อยู่ดี บนแท่งกว่าครึ่งล้านแท่ง ลึกสามไทม์เฟรม ทุกครั้งไม่มีข้อยกเว้น เรากำลังใช้งบคำนวณเกือบทั้งหมดไปกับการคำนวณปริมาณที่ไม่แปรผัน (invariant) ต่อพารามิเตอร์ที่เรากำลังปรับจริง ๆ ซ้ำแล้วซ้ำอีก

ข้อสังเกตนั้นคือทั้งหมดของบทความนี้ ทันทีที่คุณสังเกตเห็นว่าพารามิเตอร์บางตัวเปลี่ยนอินดิเคเตอร์ และบางตัวเปลี่ยนเพียงกฎการตัดสินใจที่ใช้กับอินดิเคเตอร์ที่ตรึงไว้ พื้นที่พารามิเตอร์ก็เลิกเป็นกลุ่มเมฆแบนราบของมิติ และกลายเป็นสองชั้นซ้อนกันที่มีป้ายราคาต่างกันลิบลับ

สองแกน ไม่ใช่แกนเดียว

สองแกนของพื้นที่พารามิเตอร์ที่มีป้ายราคาต่างกัน: กริดอินดิเคเตอร์ราคาแพงแบบเบาบาง ซึ่งแต่ละเซลล์ปลดล็อกแผ่นคอนฟิกค่าเกณฑ์ที่หนาแน่นและแทบจะฟรี

แบ่งพารามิเตอร์ตามสิ่งที่การประเมินมันใหม่บังคับให้คุณต้องคำนวณซ้ำ:

  • แกนราคาแพง — พารามิเตอร์อินดิเคเตอร์ ค่า period ของไทม์เฟรมและความยาว HMA เปลี่ยนตัวใดตัวหนึ่งแล้วคุณต้องสร้างอินดิเคเตอร์ใหม่ตลอดทั้งซีรีส์ราคา: รีแซมเปิลไทม์เฟรมที่สูงกว่า รันการถ่วงน้ำหนักแบบ Hull คำนวณซีรีส์จุดตัดและค่า separation ที่จุดตัดแต่ละจุดใหม่ทั้งหมด นี่คือการกวาด O(n) ต่อแท่งของประวัติราคา และคุณจ่ายเต็มราคาสำหรับทุกชุดค่าผสมที่แตกต่างกัน ในกลยุทธ์ triple-TF แกนนี้มีหกพารามิเตอร์: (period, hma_length) สำหรับไทม์เฟรมสูง กลาง และต่ำอย่างละชุด

  • แกนราคาถูก — ค่าเกณฑ์ตัดสินใจ เกต directional-separation ที่ตัดสิน — จากซีรีส์จุดตัดและ separation ที่คำนวณไว้เรียบร้อยแล้ว — ว่าจะเข้าหรือออก เปลี่ยนค่าเกณฑ์แล้วไม่มีอินดิเคเตอร์ตัวไหนขยับ คุณแค่รันการกวาด O(n) รอบเดียวใหม่บนสัญญาณที่คำนวณล่วงหน้าไว้ ตรวจเกตและบันทึกการฟิล ในกลยุทธ์ triple-TF แกนนี้มีสิบสองพารามิเตอร์ (ค่าเกณฑ์ separation สี่ตัว — entry-buy, entry-sell, exit-buy, exit-sell — บนแต่ละไทม์เฟรมจากทั้งหมดสาม) ส่วนตัวแปร dual-TF มีแปดตัว (ค่าเกณฑ์สี่ตัวบนแต่ละไทม์เฟรมจากทั้งหมดสอง)

ดังนั้นรูปทรงที่แท้จริงของพื้นที่นี้คือ: แพง 6 + ถูก 12 = 18 สำหรับ triple-TF และ แพง 4 + ถูก 8 = 12 สำหรับ dual-TF สองในสามของมิติทั้งหมดอาศัยอยู่บนแกนราคาถูก และสองแกนนี้ไม่ได้ต่างกันแค่จำนวน — ต้นทุนต่อหน่วยของมันต่างกันมากกว่าสามลำดับขนาด (orders of magnitude) ซึ่งเป็นตัวเลขที่ส่วนที่เหลือของบทความนี้จะว่าด้วย

โครงสร้างนี้ซ้อนกันอย่างเคร่งครัด แต่ละจุดบนแกนราคาแพง — (period, hma_length) ที่เป็นรูปธรรมหนึ่งชุดต่อไทม์เฟรม — นิยามชุดอาร์เรย์สัญญาณที่ตรึงไว้หนึ่งชุด และบนรากฐานที่ตรึงไว้นั้นวางอยู่แผ่นทั้งแผ่นของแกนราคาถูก: เวกเตอร์ค่าเกณฑ์นับพัน แต่ละตัวคือการกวาดเร็ว ๆ บนอาร์เรย์ชุดเดิม คุณจ่ายค่ารากฐานครั้งเดียวแล้วเฉลี่ยต้นทุนมันไปทั่วทั้งแผ่น

ทำไมอินดิเคเตอร์จึงไม่แปรผันต่อค่าเกณฑ์

การแคชนี้ใช้ได้ก็เพราะข้อเท็จจริงทางคณิตศาสตร์ ไม่ใช่รายละเอียดการอิมพลีเมนต์ที่บังเอิญโชคดี และคุ้มค่าที่จะระบุให้แม่นยำเพราะมันคือสมมติฐานที่แบกรับน้ำหนักทั้งหมด: อาร์เรย์อินดิเคเตอร์เป็นฟังก์ชันของ (period, hma_length) เท่านั้น ค่าเกณฑ์ separation ไม่ปรากฏที่ใดเลยในนิยามของพวกมัน

พูดอย่างเป็นรูปธรรม สำหรับแต่ละไทม์เฟรม เอนจินจะคำนวณอาร์เรย์ที่จัดเรียงตรงกันสี่ชุดล่วงหน้าบนดัชนีฐาน: เส้น Hull MA hma, ตัวปรับเรียบสามชั้น hma3, ซีรีส์จุดตัด cross (+1 ซื้อ / −1 ขาย / 0) และเปอร์เซ็นต์ separation ที่จุดตัดแต่ละจุด ทุกตัวล้วนอนุพัทธ์มาจากราคาและพารามิเตอร์อินดิเคเตอร์สองตัว ส่วนค่าเกณฑ์ — ตัวเลขที่คุณเอา separation ไปเทียบด้วย — จะถูกใช้ทีหลัง ณ เวลาตัดสินใจ มันไม่เคยแตะอาร์เรย์เลย

นี่ไม่ใช่การจัดเรียงใหม่ที่ปลอดภัยโดยอัตโนมัติ มันปลอดภัยก็เพราะเราระมัดระวังเรื่องเมื่อไรที่ข้อมูลกลายเป็นสิ่งที่รู้ได้ ซึ่งเป็นประเด็นของงานว่าด้วยอคติ look-ahead ในซีรีส์เดียวกันนี้ อินดิเคเตอร์ไทม์เฟรมสูงบนแท่งฐาน i คำนวณจากแท่งเทียนไทม์เฟรมสูงที่ปิดแล้ว บวกกับแท่งที่กำลังก่อตัวซึ่งราคาปิดสะสม (running close) เท่ากับราคาปิดฐานปัจจุบัน close[i] — ค่าที่รู้ได้ ณ แท่ง i ไม่มีอะไรจากอนาคตรั่วไหลเข้ามา ดังนั้นสัญญาณที่คำนวณล่วงหน้า ณ แท่ง i จึงเป็นสิ่งเดียวกันเป๊ะกับที่บอตเทรดสดจะเห็น ณ แท่ง i และมันยังคงถูกต้องไม่ว่าคุณจะทดสอบมันกับค่าเกณฑ์ตัวไหนในภายหลัง การแคชสัญญาณที่รั่วก็แค่แคชการรั่วเอาไว้ด้วย ส่วนการแคชสัญญาณเชิงเหตุปัจจัย (causal) คือการแคชสิ่งที่คุณเทรดได้จริง

ความไม่แปรผันนี้มอบการแยกตัวประกอบ (factorization) ให้เรา เขียนการประเมินเวกเตอร์พารามิเตอร์เต็มหนึ่งชุด θ = (indicator_params, threshold_params) เป็น:

signals   = build_indicators(indicator_params)      # EXPENSIVE, depends only on indicator_params
score     = simulate(signals, threshold_params)     # CHEAP, reuses signals across all thresholds

build_indicators ไม่อ่าน threshold_params เลย ข้อเท็จจริงเพียงข้อเดียวนี้คือสิ่งที่อนุญาตให้ใช้แคชได้: ตรึง indicator_params ไว้ ปรับ threshold_params ได้ตามใจ แล้ว signals ก็คือค่าคงที่ที่คุณคำนวณเพียงครั้งเดียว

สถาปัตยกรรม: คำนวณครั้งเดียว สวีปหลายรอบ

แคชสัญญาณที่ใช้พารามิเตอร์อินดิเคเตอร์เป็นคีย์ ป้อนให้การสวีปแบบแบตช์ความเร็วสูง: การสร้างอินดิเคเตอร์หนึ่งครั้งกระจายออกเป็นแถวกว้างของการประเมินค่าเกณฑ์ที่ใช้อาร์เรย์แคชชุดเดียวกัน

เอนจิน (scripts/engine_multitf.py คอมมิต bfc8aaa ในแบ็กเทสเตอร์ของเรา) อิมพลีเมนต์การแยกตัวประกอบนี้ด้วยสองชิ้นส่วนที่ชื่อของมันบอกหน้าที่ตรงตัว

SignalCache — แกนราคาแพงที่ถูกเมมโมไอซ์ (memoized) มันคือดิกชันนารีที่ใช้ (period_bars, hma_length) เป็นคีย์ ขอสัญญาณของไทม์เฟรมหนึ่งจากมัน แล้วมันจะคืน TFSignals ที่แคชไว้หากชุดอินดิเคเตอร์นั้นเคยถูกสร้างมาก่อน มิฉะนั้นก็สร้างครั้งเดียวแล้วเก็บไว้ เพราะคีย์มีเพียงพารามิเตอร์อินดิเคเตอร์ ทุกคอนฟิกค่าเกณฑ์ที่ใช้ชุดอินดิเคเตอร์ร่วมกัน — และในการสวีปค่าเกณฑ์แบบหนาแน่นนั่นหมายถึงหลายพันคอนฟิก — จะชนรายการแคชเดียวกัน ไทม์เฟรมที่สูงกว่าได้ประโยชน์จากเรื่องนี้เป็นพิเศษ: กริดหยาบ ๆ ของ period ตัวเลือกสักสี่ค่า คูณความยาว HMA อีกหยิบมือหนึ่ง คือจำนวนการสร้างราคาแพงที่แตกต่างกันเพียงน้อยนิด และแต่ละครั้งถูกใช้ซ้ำทั่วทั้งแผ่นค่าเกณฑ์ที่ซ้อนอยู่ข้างบนมัน

sweep_separations — แกนราคาถูกที่รันเป็นแบตช์ มันรับอาร์เรย์สัญญาณที่แคชไว้กับเมทริกซ์ของเวกเตอร์ค่าเกณฑ์ (sps รูปร่าง [m, 12]) แล้วรันทั้งหมดผ่านเคอร์เนลคอมไพล์แล้วตัวเดียว แต่ละแถวคือการกวาด O(n) หนึ่งรอบ: เดินไปตามแท่ง ใช้เกต บันทึกการฟิลแบบไร้การรั่วที่ open[i+1] รวมยอด PnL และเวลาที่ถือสถานะ ไม่มีอินดิเคเตอร์ถูกสร้างใหม่ภายในลูปนี้ — มันอ่าน cross และ separation ตรงจากแคช ซิมูเลชันชั้นในถูกคอมไพล์แบบ JIT (Numba) ดังนั้นเมื่ออุ่นเครื่องแล้ว ต้นทุนต่อคอนฟิกจะถูกครอบงำโดยการสแกนเชิงเส้นรอบเดียวบนแท่งราคา ไม่ใช่โอเวอร์เฮดของ Python

สองชิ้นส่วนนี้ประกอบกันเป็นการค้นหาแบบซ้อนตามธรรมชาติ: ลูปนอกเดินไปตามแกนราคาแพง (แต่ละรอบสร้างและแคชชุดอินดิเคเตอร์หนึ่งชุด) ส่วนลูปในกระจายแบตช์กว้าง ๆ ไปทั่วแกนราคาถูกบนอาร์เรย์ที่แคชไว้เหล่านั้น ในโค้ด รูปทรงก็เป็นแบบนั้นเป๊ะ — ลูปราคาแพงเล็ก ๆ ห่อแบตช์ราคาถูกกว้าง ๆ:

cache = SignalCache(base_close, base_ts)           # keyed by (period, hma_length)

for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid:   # EXPENSIVE axis (coarse)
    htf = cache.get(htf_p, htf_h)                  # built once, then a cache hit forever
    mtf = cache.get(mtf_p, mtf_h)
    ltf = cache.get(ltf_p, ltf_h)

    sps = sample_thresholds(m=4000)                # CHEAP axis: [m, 12] threshold vectors
    pnl, n_trades, bars_in_pos = sweep_separations(  # one compiled batch, no indicator work
        base_close, base_open, htf, ltf, sps, mtf=mtf)

คุณแตะตัวสร้างราคาแพงให้น้อยครั้งเท่าที่กริดอินดิเคเตอร์อนุญาต แล้วปล่อยให้การสวีปราคาถูกรับปริมาณงานไป — แถว sps นับพันวิ่งบนอาร์เรย์ที่ไม่เคยขยับเลย นั่นคือการออปติไมซ์ทั้งหมด — ไม่มีการประมาณค่า ไม่มีความเที่ยงตรงที่สูญเสีย มีแค่การปฏิเสธที่จะคำนวณสิ่งที่ไม่ได้เปลี่ยนซ้ำอีกรอบ

ต้นทุนที่แท้จริงของ "ฟรี": ตัวเลข

แผนภูมิแท่งเปรียบเทียบต้นทุนต่อคอนฟิก: แท่งสูงของการคำนวณใหม่ต่อคอนฟิก ข้างเศษเสี้ยวบาง ๆ ของการกวาดค่าเกณฑ์จากแคช พร้อมคำกำกับช่องว่างราว 1,600 เท่า

เราวัดสองแกนนี้บนเวิร์กโหลดสาธิต: แท่ง 1 นาทีของ ETHUSDT เต็มหนึ่งปี (~527k แท่ง) แบบ triple-TF โดยอุ่นอินดิเคเตอร์และคอมไพล์ JIT ให้เสร็จนอกช่วงจับเวลา

บนแกนราคาถูก sweep_separations ทำความเร็วต่อเนื่องได้ ~5,600 คอนฟิกค่าเกณฑ์ต่อวินาที นั่นคือซิมูเลชันครบวงจร — เกต การฟิล PnL การเปิดสถานะ — สำหรับแต่ละคอนฟิก บนแท่งกว่าครึ่งล้านแท่ง ที่ราว 180 ไมโครวินาทีต่อคอนฟิก เหตุผลที่มันเร็วขนาดนั้นได้ก็เพราะมันไม่ทำงานอินดิเคเตอร์เลยแม้แต่นิดเดียว: ทุกคอนฟิกอ่านอาร์เรย์ cross และ separation ที่แคชไว้ชุดเดียวกัน

ทีนี้ลองตีราคาทางเลือกอีกทาง การสร้างชุดอินดิเคเตอร์ triple-TF หนึ่งครั้งใช้เวลาราวหลายร้อยมิลลิวินาที (~0.3 s) — รีแซมเปิลสามไทม์เฟรม ถ่วงน้ำหนักแบบ Hull สกัดจุดตัดและ separation ตลอดทั้งปี ถ้าคุณคำนวณอินดิเคเตอร์ใหม่ภายในลูปคอนฟิก — ดีไซน์ลูปเดียวแบบตรงไปตรงมา — คอนฟิกทั้ง 5,600 ต่อวินาทีทุกตัวจะต้องจ่ายค่าสร้างอินดิเคเตอร์เต็มจำนวนแทน ต้นทุนต่อคอนฟิกพองจาก ~180 ไมโครวินาทีเป็น ~0.3 วินาที:

ต้นทุนต่อคอนฟิก คอนฟิก/วินาที
แกนราคาถูก (สัญญาณจากแคช) ~180 µs ~5,600
คำนวณอินดิเคเตอร์ใหม่ต่อคอนฟิก ~0.3 s ~3.4

อัตราส่วนคือ ~1,600 เท่า การสวีปแกนราคาถูกบนสัญญาณจากแคชถูกกว่าดีไซน์ตรงไปตรงมาที่สร้างอินดิเคเตอร์ใหม่ให้ทุกเวกเตอร์ค่าเกณฑ์ราวสามลำดับขนาด พูดให้เป็นรูปธรรม: แบตช์คอนฟิกค่าเกณฑ์ไม่กี่พันตัวที่จบภายในไม่ถึงวินาทีบนเส้นทางแคช จะกินเวลาเกือบชั่วโมงถ้าแต่ละคอนฟิกต้องสร้างอินดิเคเตอร์ของตัวเองใหม่ ผลลัพธ์เดียวกัน ความเที่ยงตรงเดียวกัน ไม่มีทางลัดในคณิตศาสตร์ — ความต่างเพียงอย่างเดียวคือฝ่ายหนึ่งคำนวณสิ่งที่ไม่แปรผันซ้ำ ส่วนอีกฝ่ายไม่ทำ

นี่ไม่ใช่ไมโครออปติไมเซชันที่คุณโรยเพิ่มตอนท้าย มันเปลี่ยนว่าการค้นหาแบบไหนเป็นไปได้จริง ที่ 5,600 คอนฟิก/วินาที แกนค่าเกณฑ์หนาแน่นพอที่จะสำรวจได้อย่างจริงจัง — คุณมีกำลังจ่ายให้กริดละเอียดหรือการสุ่มแบบ random/QMC ยาว ๆ ต่อชุดอินดิเคเตอร์หนึ่งชุด — ขณะที่แกนราคาแพงยังคงเป็นการสร้างหยาบ ๆ เพียงหยิบมือโดยตั้งใจ งบคำนวณไหลไปยังจุดที่พารามิเตอร์มีต้นทุนจริง ๆ

คำสาปแห่งมิติ ฉบับตีราคาใหม่

กริดพารามิเตอร์แบนราบสม่ำเสมอที่ถูกติดป้ายใหม่เป็นแผนที่ราคาสองระดับ: เซลล์ราคาแพงไม่กี่ช่องถูกตีกรอบหนา ส่วนที่เหลือแรเงาว่าแทบจะฟรี

ย้อนกลับไปที่กรอบคิดที่บทความนี้เปิดมา คำสาปแห่งมิติบอกว่าพื้นที่ค้นหาโตแบบเอกซ์โพเนนเชียลตามจำนวนพารามิเตอร์ ดังนั้นมิติที่มากขึ้นย่อมแย่ลงโดยสิ้นเชิง นั่นเป็นเรื่องจริงในแง่ขนาดของกริด แต่ชวนให้เข้าใจผิดในแง่ต้นทุนของการครอบคลุมมัน เพราะมันตีราคาทุกมิติเท่ากันหมด

พอคุณแยกแกนออกจากกัน จำนวนมิติก็อ่านได้ต่างออกไป กลยุทธ์ triple-TF มี 18 มิติ แต่มีเพียง 6 มิติเท่านั้นที่แพง อีก 12 มิติเป็นมิติแกนราคาถูกที่ขยายกริดโดยไม่ขยายบิลค่าคำนวณอย่างมีนัยสำคัญ — คุณโยนคอนฟิกเป็นพัน ๆ ใส่พวกมันได้ในราคาเศษสตางค์ กลยุทธ์ dual-TF มี 12 มิติ โดยแพงเพียง 4 และถูก 8 ในทั้งสองกรณี "คำสาป" ส่วนใหญ่กระจุกอยู่บนแกนที่แทบไม่มีต้นทุนในการสวีป

ดังนั้นวิธีคิดเรื่องต้นทุนการค้นหาอย่างซื่อตรงจึงไม่ใช่ "มีพารามิเตอร์กี่ตัว" แต่เป็น "มีพารามิเตอร์ราคาแพงกี่ตัว และกริดของพวกมันหยาบได้แค่ไหน" แกนราคาแพงคือที่ที่เอกซ์โพเนนเชียลทำร้ายคุณจริง ๆ และเป็นที่ที่คุณต้องการกริดเล็ก ๆ ที่เลือกมาอย่างดี — period ตัวเลือกไม่กี่ค่า ช่วงความยาว HMA พอประมาณ — อาจกลั่นแบบหยาบ-สู่-ละเอียด ในจิตวิญญาณเดียวกับการเจาะลึกแบบปรับความละเอียด ที่เราใช้ในที่อื่น ส่วนแกนราคาถูกคือที่ที่คุณฟุ่มเฟือยได้ เพราะคอนฟิกค่าเกณฑ์เพิ่มอีกหนึ่งตัวราคาแค่ 180 ไมโครวินาที

การตีราคาใหม่นี้ใช้ได้ทั่วไปเกินกว่ากลยุทธ์ HMA ของเราไปมาก แพตเทิร์นนี้ — พารามิเตอร์บางตัวเปลี่ยนฟีเจอร์ แต่พารามิเตอร์ส่วนใหญ่เปลี่ยนกฎที่ใช้กับฟีเจอร์ที่ตรึงไว้ — โผล่ซ้ำอยู่ทุกหนแห่งในการเทรดเชิงระบบ ความยาวอินดิเคเตอร์ ความถี่การรีแซมเปิล และหน้าต่างมองย้อนหลัง (lookback) นั้นแพง ส่วนค่าเกณฑ์เข้า/ออก ระยะสต็อป ตัวคูณขนาดสถานะ และเกตยืนยันสัญญาณนั้นถูก เมื่อใดก็ตามที่พารามิเตอร์ตัวหนึ่งเพียงเปลี่ยนรูปขอบเขตการตัดสินใจบนสัญญาณที่คำนวณไว้แล้ว มันเป็นของแกนราคาถูก และควรถูกสวีปที่นั่น ชัยชนะจากการแคชในตระกูลเดียวกันยังปรากฏในการแคช parquet แบบหลายไทม์เฟรม และในภาพที่กว้างกว่าอย่างบันไดความเร็วเอนจิน

ตรงไหนที่มื้อกลางวันฟรีมีบิลตามมา

แกนราคาถูกนั้นถูกในแง่การคำนวณ แต่ไม่ถูกในแง่สถิติ และการเอาสองอย่างนี้มาปนกันคือวิธีเปลี่ยนชัยชนะด้านประสิทธิภาพให้กลายเป็นเครื่องจักรผลิตการโอเวอร์ฟิต

ทุกคอนฟิกค่าเกณฑ์ที่คุณประเมินคือการทดลอง (trial) หนึ่งครั้ง และเมื่อคุณรันการทดลองนับพันบนชุดข้อมูลเดียว ตัวที่ดีที่สุดในนั้นดูดีส่วนหนึ่งก็เพราะโชค การทำให้การทดลองเหล่านั้นถูกลง 1,600 เท่าไม่ได้ทำให้โชคหายไป — มันทำให้โชคสะสมได้ง่ายขึ้น การสวีปค่าเกณฑ์แบบหนาแน่นคือสถานการณ์ที่การพองตัวจากการทดสอบพหุคูณ (multiple testing) กัดแรงที่สุดพอดี: ผู้ท้าชิงจำนวนมาก ประวัติราคาเส้นเดียว และกฎคัดเลือกที่รายงานค่าสูงสุด เอนจินที่เร็วที่สุดในโลกยินดีจะยื่นเวกเตอร์ค่าเกณฑ์ที่ฟิตเข้ากับนอยซ์ของหน้าต่างทดสอบของคุณอย่างงดงาม — แล้วล้มเหลวนอกกลุ่มตัวอย่าง — ให้คุณ

ดังนั้นวินัยต้องขยายตามความเร็ว ทันทีที่ต้นทุนคำนวณของการทดลองหนึ่งครั้งดิ่งเข้าใกล้ศูนย์ การทำบัญชีเชิงสถิติก็กลายเป็นข้อจำกัดที่ผูกมัดแทน และคุณต้องจ่ายมันอย่างชัดแจ้ง:

  • ปรับลดตามจำนวนการทดลอง ให้คะแนนผู้ชนะโดยเทียบกับจำนวนคอนฟิกที่คุณลองไป ไม่ใช่เทียบกับศูนย์ อัตราส่วนชาร์ปแบบปรับลด (Deflated Sharpe Ratio) และความน่าจะเป็นของการโอเวอร์ฟิตในแบ็กเทสต์ มีไว้เพื่อสิ่งนี้โดยเฉพาะ — มันแปลง "เราลองเวกเตอร์ค่าเกณฑ์ไป 4,000 ตัว" ให้กลายเป็นส่วนลดบนความได้เปรียบ (edge) ที่รายงาน
  • ตรวจสอบนอกกลุ่มตัวอย่าง ทีละโฟลด์ การสวีปราคาถูกก็ยังต้องรันภายใต้การแบ่งแบบวอล์ก-ฟอร์เวิร์ด ที่ซื่อสัตย์ ค่าเกณฑ์ที่ชนะเฉพาะในกลุ่มตัวอย่างนั้นไร้ค่าไม่ว่าคุณจะหามันเจอเร็วแค่ไหน เอนจินของเรารักษาการฟิลให้ไร้การรั่ว (open[i+1]) ก็เพื่อให้แกนราคาถูกไม่มีทางซื้อผลงานด้วยการแอบดูอนาคตได้
  • เลือกที่ราบมากกว่ายอดแหลม เพราะแกนค่าเกณฑ์หนาแน่นและเร็ว คุณจึงทำแผนที่พื้นผิวการตอบสนอง (response surface) ของมันได้ทั้งผืน ไม่ใช่แค่หา argmax ของมัน บริเวณค่าเกณฑ์กว้าง ๆ ที่ใช้ได้ทั้งบริเวณคือความได้เปรียบของจริง ส่วนจุดสูงสุดแหลมโดดเดี่ยวคือสิ่งประดิษฐ์จากการฟิต — การแยกแยะที่ราบ-กับ-ยอดแหลม ซึ่งความเร็วที่เรากำลังบรรยายอยู่นี่เองทำให้จ่ายไหว

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

จะจัดโครงสร้างการค้นหาของคุณเองอย่างไร

การนำสิ่งนี้ไปใช้กับกลยุทธ์ของคุณเอง งานส่วนใหญ่คือการจัดหมวดหมู่ — แยกพารามิเตอร์ของคุณไปไว้บนแกนที่ถูกต้อง — แล้วตามด้วยลูปซ้อน:

  1. ติดป้ายพารามิเตอร์ทุกตัวตามสิ่งที่มันบังคับให้คุณคำนวณซ้ำ ถ้าเปลี่ยนมันแล้วอาร์เรย์อินดิเคเตอร์/ฟีเจอร์เปลี่ยนตาม มันแพง ถ้ามันเพียงเปลี่ยนการเปรียบเทียบ ค่าเกณฑ์ หรือกฎกำหนดขนาดสถานะที่ใช้กับอาร์เรย์ที่ตรึงไว้ มันถูก เมื่อสงสัย ให้ถามว่า: พารามิเตอร์ตัวนี้ปรากฏอยู่ที่ไหนสักแห่งในนิยามของอินดิเคเตอร์หรือไม่? ถ้าไม่ มันถูก
  2. ตั้งคีย์แคชบนแกนราคาแพงเท่านั้น เมมโมไอซ์การสร้างฟีเจอร์ด้วยพารามิเตอร์อินดิเคเตอร์ของมัน (แบบเดียวกับที่ SignalCache ทำด้วย (period, hma_length)) การทดลองที่อยู่ติดกันซึ่งใช้ชุดอินดิเคเตอร์ร่วมกันก็จะได้ใช้อาร์เรย์ชุดเดิมซ้ำแบบฟรี ๆ
  3. รันแกนราคาถูกเป็นแบตช์บนฟีเจอร์ที่แคชไว้ รันคอนฟิกค่าเกณฑ์เป็นลูปคอมไพล์แน่น ๆ บนสัญญาณที่คำนวณล่วงหน้า ไม่ใช่การประเมินใหม่เต็มรูปแบบ นี่คือที่มาของทรูพุตของคุณ — และในกรณีของเราคือ ~5,600 คอนฟิก/วินาที
  4. ซ้อนลูป: แพงอยู่นอก ถูกอยู่ใน รักษากริดราคาแพงให้เล็กและจงใจ (หยาบ หรือหยาบ-สู่-ละเอียด) ปล่อยให้การสวีปราคาถูกหนาแน่นเต็มที่ ใช้งบตรงจุดที่พารามิเตอร์มีต้นทุนจริง
  5. จัดงบจำนวนการทดลองโดยเทียบกับการโอเวอร์ฟิต ไม่ใช่เทียบกับนาฬิกา ในเมื่อนาฬิกาไม่ใช่ขีดจำกัดอีกต่อไป ก็ให้ deflated Sharpe / PBO เป็นขีดจำกัดแทน ตัดสินใจว่าคุณมีกำลังจ่ายเชิงสถิติให้การทดลองบนแกนราคาถูกได้กี่ครั้ง แล้วตรวจสอบผู้ชนะนอกกลุ่มตัวอย่าง

ผลตอบแทนเชิงวิศวกรรมและราวกันตกเชิงสถิติคือสองด้านของแนวคิดเดียวกัน: การแยกแกนทำให้คุณค้นหามิติราคาถูกได้อย่างถี่ถ้วน ซึ่งก็คือเหตุผลที่คุณต้องปรับลดค่าตามระดับความถี่ถ้วนที่คุณค้นหานั่นเอง

ข้อสรุปสำคัญ

  1. มิติแต่ละมิติต้นทุนไม่เท่ากัน พารามิเตอร์ของกลยุทธ์แยกออกเป็นแกนราคาแพง (อินดิเคเตอร์ — คำนวณใหม่ตลอดทั้งซีรีส์) และแกนราคาถูก (ค่าเกณฑ์ — การกวาด O(n) บนสัญญาณที่คำนวณล่วงหน้า) ในเอนจินของเรานั่นคือแพง 6 + ถูก 12 สำหรับ triple-TF และ 4 + 8 สำหรับ dual-TF
  2. อินดิเคเตอร์ไม่แปรผันต่อค่าเกณฑ์ และความไม่แปรผันนั้นคือการออปติไมซ์ทั้งหมด การสร้างฟีเจอร์ขึ้นกับพารามิเตอร์อินดิเคเตอร์เท่านั้น คุณจึงสร้างครั้งเดียว แคชด้วยคีย์ (period, hma_length) แล้วใช้ซ้ำทั่วทุกคอนฟิกค่าเกณฑ์ที่ใช้ชุดเดียวกัน
  3. แกนราคาถูกวิ่งถูกกว่า ~1,600 เท่า ~5,600 คอนฟิกค่าเกณฑ์/วินาที (~180 µs ต่อตัว) บนสัญญาณจากแคช เทียบกับ ~0.3 s ต่อคอนฟิกถ้าคุณสร้างอินดิเคเตอร์ใหม่ทุกครั้ง ความเที่ยงตรงเท่ากันทุกประการ — ความต่างเดียวคือการปฏิเสธที่จะคำนวณสิ่งที่ไม่แปรผันซ้ำ
  4. คำสาปแห่งมิติแท้จริงแล้วคือคำสาปของมิติราคาแพง จำนวนพารามิเตอร์ส่วนใหญ่อาศัยอยู่บนแกนที่แทบจะฟรีในการสวีป จงรักษากริดราคาแพงให้หยาบ และฟุ่มเฟือยกับกริดราคาถูก
  5. ความเร็วย้ายข้อจำกัดที่ผูกมัดจากการคำนวณไปยังสถิติ การสวีปที่หนาแน่นและเร็วคือเครื่องจักรทดสอบพหุคูณ จงปรับลดตามจำนวนการทดลอง ตรวจสอบทีละโฟลด์ และเลือกที่ราบมากกว่ายอดแหลม — มื้อกลางวันฟรีมีจริง แต่บิลค่าสถิติไม่ใช่ของเลือกจ่ายได้

เอนจินฉบับเต็ม — SignalCache, sweep_separations, ซิมูเลชันหลายไทม์เฟรมแบบไร้การรั่ว และเทสต์ความเท่าเทียม (parity test) ที่ตรึงมันไว้กับเซแมนติกส์แท่งกำลังก่อตัวแบบเดียวกับการรันสด — อยู่ใน scripts/engine_multitf.py (คอมมิต bfc8aaa) ในแบ็กเทสเตอร์ของเรา ครั้งหน้าที่ใครสักคนบอกคุณว่ากลยุทธ์ 18 พารามิเตอร์ใหญ่เกินกว่าจะค้นหา ลองถามเขาว่าพารามิเตอร์เหล่านั้นมีกี่ตัวที่ขยับอินดิเคเตอร์จริง ๆ โดยปกติแล้วคือหนึ่งในสาม และที่เหลือแทบจะฟรี

ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

ก้าวนำหน้าตลาด

สมัครรับจดหมายข่าวของเราเพื่อรับข้อมูลเชิงลึกการเทรดด้วย AI เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ