Strasmore Research

สโตแคสติก %K %D: สัญญาณจริงหรือสัญญาณรบกวน

สโตแคสติกคืออะไร วัดอะไรกันแน่ และสัญญาณ overbought oversold ใช้ได้จริงไหม เราวัดผลตอบแทนล่วงหน้า 5, 10 และ 20 วันหลังสัญญาณตัด เทียบกับค่าพื้นฐานของหุ้นใหญ่สหรัฐ

สโตแคสติก (Stochastic Oscillator) คือมาตรวัดตำแหน่ง ไม่ใช่เครื่องบอกว่าหุ้นแพงหรือถูก มันตอบคำถามเดียวคือราคาปิดล่าสุดอยู่ตรงไหนภายในกรอบสูงสุดถึงต่ำสุดของ 14 วันทำการที่ผ่านมา แล้วรายงานออกมาเป็นตัวเลข 0 ถึง 100 หน้านี้ไม่หยุดอยู่ที่นิยาม เราอธิบายสูตรของ %K และ %D ทีละขั้น แล้ววัดด้วยข้อมูลจริงว่าหลังสัญญาณตัดลงต่ำกว่า 80 และตัดขึ้นเหนือ 20 ผลตอบแทนล่วงหน้า 5, 10 และ 20 วันทำการของหุ้นใหญ่สหรัฐห่างจากค่าฐานของวันธรรมดาแค่ไหน

สโตแคสติกคืออะไร และคำนวณจากอะไร

สูตรของ %K ดิบมีบรรทัดเดียว คือ 100 คูณด้วย (ราคาปิดวันนี้ ลบ จุดต่ำสุดของ 14 วัน) หารด้วย (จุดสูงสุดของ 14 วัน ลบ จุดต่ำสุดของ 14 วัน)

ลองแทนตัวเลขสมมติเพื่อให้เห็นภาพ ถ้าใน 14 วันล่าสุดหุ้นตัวหนึ่งมีจุดต่ำสุดที่ 100 ดอลลาร์ จุดสูงสุดที่ 120 ดอลลาร์ และวันนี้ปิดที่ 115 ดอลลาร์ ค่า %K คือ 100 คูณ (115 ลบ 100) หาร (120 ลบ 100) เท่ากับ 75 ปิดที่ยอดกรอบพอดีได้ 100 ปิดที่ก้นกรอบพอดีได้ 0 เท่านั้นเอง

สิ่งที่ตัวเลขนี้บอกจึงมีอย่างเดียว คือตำแหน่งของราคาปิดภายในกรอบล่าสุด สูตรไม่รู้ว่ากรอบนั้นกว้าง 2% หรือ 25% ไม่รู้ว่าปริมาณการซื้อขายหนาหรือบาง และไม่มีตัวแปรใดในสูตรที่พูดถึงวันพรุ่งนี้ เส้น 80 ที่เรียกกันว่า overbought กับเส้น 20 ที่เรียกว่า oversold เป็นเส้นที่คนขีดขึ้นบนมาตรวัดตำแหน่ง ไม่ใช่ข้อสรุปเรื่องทิศทางราคา

กรณีที่ค่าหลอกตามากที่สุดคือหุ้นที่นิ่งผิดปกติ ถ้ากรอบ 14 วันแคบเพียงไม่กี่เซ็นต์ การขยับเล็กน้อยก็ผลัก %K จาก 10 ไป 90 ได้ภายในวันเดียว ตัวเลขยังถูกต้องตามสูตรทุกประการ แต่ความหมายในเชิงราคาแทบไม่เหลือ

%K กับ %D ต่างกันอย่างไร แล้ว fast กับ slow คืออะไร

  • fast %K คือค่าดิบที่ได้จากสูตรข้างบนโดยตรง
  • fast %D คือค่าเฉลี่ยเคลื่อนที่ 3 วันของ fast %K
  • slow %K คือเส้นเดียวกับ fast %D ทุกประการ จุดนี้เองที่ทำให้คนสับสนกันมากที่สุด
  • slow %D คือค่าเฉลี่ยเคลื่อนที่ 3 วันของ slow %K อีกชั้นหนึ่ง

ค่าตั้งต้นที่เห็นบ่อยที่สุดคือ 14, 3, 3 ซึ่งหมายถึงกรอบ 14 วัน แล้วเฉลี่ยซ้อนกันสองชั้น ชั้นละ 3 วัน ปัญหาคือแพลตฟอร์มหนึ่งเรียกเส้นที่เฉลี่ยแล้วว่า %K อีกแพลตฟอร์มเรียกเส้นเดียวกันว่า %D บางเจ้าเปลี่ยนค่าเฉลี่ยจากแบบธรรมดาเป็นแบบเอ็กซ์โพเนนเชียล และบางเจ้านับกรอบ 14 วันโดยรวมหรือไม่รวมวันปัจจุบันไม่เหมือนกัน ผลคือกราฟสองจอที่อ่านแท่งเทียนชุดเดียวกันพิมพ์เลขคนละค่า กลไกนี้เหมือนกับที่อธิบายไว้ใน ทำไมค่า RSI ของแต่ละแพลตฟอร์มไม่ตรงกัน และใช้กับสโตแคสติกได้ตรงตัว

แผงด้านล่างวางเส้นทั้งสามชั้นไว้บนแท่งเทียนชุดเดียวกันของ AAPL ตั้งแต่ต้นเดือนพฤษภาคมถึงสิ้นเดือนสิงหาคม 2026 เพื่อให้เห็นว่าการเฉลี่ยแต่ละชั้นทำอะไรกับเส้น

คำสั่งดึงข้อมูลเส้นสโตแคสติกสามชั้นของ AAPL พฤษภาคมถึงสิงหาคม 2026
84 rows (showing 20)
datek_fast_pctk_slow_pctd_slow_pct
2026-05-0176.474.472
2026-05-0464.772.473.5
2026-05-0588.376.574.4
2026-05-0697.883.677.5
2026-05-0782.789.683.2
2026-05-0895.191.988.3
2026-05-119390.390.6
2026-05-1298.495.592.6
2026-05-1394.395.293.7
2026-05-1492.495.195.3
2026-05-1591.892.894.4
2026-05-1885.289.892.6
2026-05-1987.988.390.3
2026-05-2096.689.989.3
2026-05-2198.294.390.8
2026-05-2292.695.893.3
2026-05-2688.793.294.4
2026-05-2791.290.893.3
2026-05-2896.892.292.1
2026-05-2988.19291.7
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH
    bars AS (
        SELECT
            date,
            argMax(toFloat64(close), _ingest_time) AS close,
            argMax(toFloat64(high),  _ingest_time) AS high,
            argMax(toFloat64(low),   _ingest_time) AS low
        FROM global_markets.stocks_daily_aggs
        WHERE ticker = 'AAPL'
          AND date >= '2026-02-01'
          AND date <  '2026-09-01'
        GROUP BY date
    ),
    ranges AS (
        SELECT
            date,
            close,
            count()   OVER w14 AS n14,
            max(high) OVER w14 AS hh14,
            min(low)  OVER w14 AS ll14
        FROM bars
        WINDOW w14 AS (ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW)
    ),
    fast AS (
        SELECT
            date,
            if(hh14 > ll14, 100 * (close - ll14) / (hh14 - ll14), 50) AS k_fast
        FROM ranges
        WHERE n14 = 14
    ),
    slow AS (
        SELECT
            date,
            k_fast,
            avg(k_fast) OVER w3 AS k_slow,
            count()     OVER w3 AS n3
        FROM fast
        WINDOW w3 AS (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    ),
    lines AS (
        SELECT
            date,
            k_fast,
            k_slow,
            avg(k_slow) OVER w3b AS d_slow,
            count()     OVER w3b AS n3b
        FROM slow
        WHERE n3 = 3
        WINDOW w3b AS (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    )
SELECT
    date,
    round(k_fast, 1) AS k_fast_pct,
    round(k_slow, 1) AS k_slow_pct,
    round(d_slow, 1) AS d_slow_pct
FROM lines
WHERE n3b = 3
  AND date >= '2026-05-01'
ORDER BY date
ลองรันด้วยตัวเอง

เส้น k_fast_pct คือค่าดิบ เส้น k_slow_pct คือค่าเฉลี่ย 3 วันของมัน และ d_slow_pct คือการเฉลี่ยซ้ำอีกชั้น ทุกครั้งที่เพิ่มการเฉลี่ยหนึ่งชั้น เส้นจะแกว่งน้อยลงและขยับช้าลง ซึ่งเป็นผลทางคณิตศาสตร์ของการหาค่าเฉลี่ย ไม่ใช่คุณสมบัติวิเศษของอินดิเคเตอร์ ในแถวสุดท้ายของหน้าต่างนี้ ค่าดิบอยู่ที่ 74.7 เส้นช้าอยู่ที่ 77.8 และเส้น %D อยู่ที่ 71.3 ทั้งสามค่ามาจากแท่งเทียนชุดเดียวกัน ต่างกันแค่จำนวนชั้นของการเฉลี่ย ถ้าจอของคุณพิมพ์เลขไม่ตรงกับตารางนี้ คำถามแรกคือจอนั้นวาดเส้นไหน ไม่ใช่ว่าข้อมูลใครถูก

สัญญาณตัด 80 และตัด 20 เกิดบ่อยแค่ไหน

นิยามที่ใช้ทั้งหน้านี้ตรงไปตรงมา สัญญาณตัดลงต่ำกว่า 80 คือวันที่เส้น %K แบบช้าปิดวันก่อนหน้าที่ระดับ 80 ขึ้นไป แล้วปิดวันนี้ต่ำกว่า 80 ส่วนสัญญาณตัดขึ้นเหนือ 20 คือวันที่ปิดวันก่อนหน้าที่ระดับ 20 ลงมา แล้วปิดวันนี้สูงกว่า 20 กลุ่มตัวอย่างคือหุ้นใหญ่สหรัฐสภาพคล่องสูง 20 ตัว นับตั้งแต่ต้นปี 2016 รายชื่อเต็มอยู่ในบันทึกวิธีคำนวณท้ายหน้า

คำสั่งดึงข้อมูลจำนวนสัญญาณตัด 80 และตัด 20 ต่อปี จากหุ้นใหญ่ 20 ตัว
yearcross_down_80_countcross_up_20_count
2016231140
2017206134
2018212163
2019260119
2020236155
2021219154
2022212200
2023234159
2024244151
2025223168
2026117112
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH
    bars AS (
        SELECT
            ticker,
            date,
            argMax(toFloat64(close), _ingest_time) AS close,
            argMax(toFloat64(high),  _ingest_time) AS high,
            argMax(toFloat64(low),   _ingest_time) AS low
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('AAPL','MSFT','NVDA','AMZN','GOOGL','META','JPM','JNJ','XOM','PG','KO','WMT','HD','UNH','CVX','PEP','MRK','CSCO','ORCL','MCD')
          AND date >= '2015-01-01'
          AND date <  '2026-09-01'
        GROUP BY ticker, date
    ),
    ranges AS (
        SELECT
            ticker, date, close,
            count()   OVER w14 AS n14,
            max(high) OVER w14 AS hh14,
            min(low)  OVER w14 AS ll14
        FROM bars
        WINDOW w14 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW)
    ),
    fast AS (
        SELECT
            ticker, date, close,
            if(hh14 > ll14, 100 * (close - ll14) / (hh14 - ll14), 50) AS k_fast
        FROM ranges
        WHERE n14 = 14
    ),
    slow AS (
        SELECT
            ticker, date, close,
            avg(k_fast) OVER w3 AS k_slow,
            count()     OVER w3 AS n3
        FROM fast
        WINDOW w3 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    ),
    fwd AS (
        SELECT
            ticker, date, close, k_slow,
            lagInFrame(k_slow, 1)  OVER wl AS k_prev,
            leadInFrame(close, 20) OVER wf AS c20
        FROM slow
        WHERE n3 = 3
        WINDOW
            wl AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
            wf AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    )
SELECT
    toYear(date)                                        AS year,
    countIf((k_prev >= 80) AND (k_slow < 80))           AS cross_down_80_count,
    countIf((k_prev <= 20) AND (k_slow > 20))           AS cross_up_20_count
FROM fwd
WHERE date >= '2016-01-01'
  AND c20 > 0
GROUP BY year
ORDER BY year
ลองรันด้วยตัวเอง

ปีแรกในตารางคือ 2016 ซึ่งมีสัญญาณตัดลงต่ำกว่า 80 รวม 231 ครั้ง และตัดขึ้นเหนือ 20 รวม 140 ครั้ง จากหุ้นเพียง 20 ตัว นี่คือจุดที่ต้องตั้งสติก่อนอ่านต่อ สัญญาณแบบนี้ไม่ใช่ของหายาก มันเกิดหลายสิบครั้งต่อปีในพอร์ตขนาดเล็กชุดเดียว ของที่เกิดบ่อยขนาดนี้ต้องพิสูจน์ตัวเองด้วยค่าเฉลี่ยจำนวนมาก ไม่ใช่ด้วยภาพหน้าจอสองสามใบที่เลือกมาแล้ว แถวสุดท้ายคือปี 2026 ซึ่งนับได้ไม่เต็มปี เนื่องจากทุกวันในตัวอย่างต้องมีราคาล่วงหน้าครบ 20 วันทำการ

หลังสัญญาณแล้วอะไรเกิดขึ้น เทียบกับค่าฐาน

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

อีกจุดหนึ่งที่ต้องเข้าใจก่อนคือเราใช้ค่ามัธยฐาน ไม่ใช่ค่าเฉลี่ย ค่ามัธยฐานคือค่ากลางที่แบ่งตัวอย่างครึ่งบนครึ่งล่าง มันไม่ถูกลากไปโดยวันที่หุ้นตัวเดียวกระโดด 20% ซึ่งเป็นสิ่งที่เกิดขึ้นได้เสมอในตัวอย่างหลายหมื่นวัน

คำสั่งดึงข้อมูลค่ามัธยฐานผลตอบแทนล่วงหน้าหลังสัญญาณ เทียบกับค่าฐาน
signal_groupmedian_fwd_5d_pctmedian_fwd_10d_pctmedian_fwd_20d_pct
%K ตัดลงต่ำกว่า 800.370.731.39
%K ตัดขึ้นเหนือ 200.520.951.55
ค่าฐาน: ทุกวันทำการ0.390.821.48
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH
    bars AS (
        SELECT
            ticker,
            date,
            argMax(toFloat64(close), _ingest_time) AS close,
            argMax(toFloat64(high),  _ingest_time) AS high,
            argMax(toFloat64(low),   _ingest_time) AS low
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('AAPL','MSFT','NVDA','AMZN','GOOGL','META','JPM','JNJ','XOM','PG','KO','WMT','HD','UNH','CVX','PEP','MRK','CSCO','ORCL','MCD')
          AND date >= '2015-01-01'
          AND date <  '2026-09-01'
        GROUP BY ticker, date
    ),
    ranges AS (
        SELECT
            ticker, date, close,
            count()   OVER w14 AS n14,
            max(high) OVER w14 AS hh14,
            min(low)  OVER w14 AS ll14
        FROM bars
        WINDOW w14 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW)
    ),
    fast AS (
        SELECT
            ticker, date, close,
            if(hh14 > ll14, 100 * (close - ll14) / (hh14 - ll14), 50) AS k_fast
        FROM ranges
        WHERE n14 = 14
    ),
    slow AS (
        SELECT
            ticker, date, close,
            avg(k_fast) OVER w3 AS k_slow,
            count()     OVER w3 AS n3
        FROM fast
        WINDOW w3 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    ),
    fwd AS (
        SELECT
            ticker, date, close, k_slow,
            lagInFrame(k_slow, 1)  OVER wl AS k_prev,
            leadInFrame(close, 5)  OVER wf AS c5,
            leadInFrame(close, 10) OVER wf AS c10,
            leadInFrame(close, 20) OVER wf AS c20
        FROM slow
        WHERE n3 = 3
        WINDOW
            wl AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
            wf AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    ),
    tagged AS (
        SELECT
            100 * (c5  / close - 1)             AS fwd5_pct,
            100 * (c10 / close - 1)             AS fwd10_pct,
            100 * (c20 / close - 1)             AS fwd20_pct,
            cityHash64(ticker, toString(date))  AS h,
            arrayJoin(arrayConcat(
                if((k_prev >= 80) AND (k_slow < 80), ['%K ตัดลงต่ำกว่า 80'], emptyArrayString()),
                if((k_prev <= 20) AND (k_slow > 20), ['%K ตัดขึ้นเหนือ 20'], emptyArrayString()),
                ['ค่าฐาน: ทุกวันทำการ']
            ))                                  AS signal_group
        FROM fwd
        WHERE date >= '2016-01-01'
          AND c5 > 0 AND c10 > 0 AND c20 > 0
    )
SELECT
    signal_group,
    round(quantileDeterministic(0.5)(fwd5_pct,  h), 2) AS median_fwd_5d_pct,
    round(quantileDeterministic(0.5)(fwd10_pct, h), 2) AS median_fwd_10d_pct,
    round(quantileDeterministic(0.5)(fwd20_pct, h), 2) AS median_fwd_20d_pct
FROM tagged
GROUP BY signal_group
ORDER BY indexOf(['%K ตัดลงต่ำกว่า 80', '%K ตัดขึ้นเหนือ 20', 'ค่าฐาน: ทุกวันทำการ'], signal_group)
ลองรันด้วยตัวเอง

ค่ามัธยฐานผลตอบแทน 10 วันทำการหลังสัญญาณตัดลงต่ำกว่า 80 อยู่ที่ 0.73% หลังสัญญาณตัดขึ้นเหนือ 20 อยู่ที่ 0.95% และค่าฐานของทุกวันทำการอยู่ที่ 0.82% ระยะ 5 วันให้ภาพชุดเดียวกัน คือ 0.37% กับ 0.52% เทียบกับค่าฐาน 0.39% และระยะ 20 วันคือ 1.39% กับ 1.55% เทียบกับค่าฐาน 1.48%

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

อัตราชนะเล่าเรื่องเดียวกันจากอีกมุม อัตราชนะคือสัดส่วนของครั้งที่ราคาปิดในอนาคตอยู่สูงกว่าราคาปิดของวันที่เกิดสัญญาณ

คำสั่งดึงข้อมูลอัตราชนะหลังสัญญาณ เทียบกับค่าฐานของทุกวันทำการ
signal_groupwin_rate_5d_pctwin_rate_10d_pctwin_rate_20d_pctobservation_count
%K ตัดลงต่ำกว่า 8055.857.959.32394
%K ตัดขึ้นเหนือ 2056.558.760.41655
ค่าฐาน: ทุกวันทำการ55.357.459.751713
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH
    bars AS (
        SELECT
            ticker,
            date,
            argMax(toFloat64(close), _ingest_time) AS close,
            argMax(toFloat64(high),  _ingest_time) AS high,
            argMax(toFloat64(low),   _ingest_time) AS low
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('AAPL','MSFT','NVDA','AMZN','GOOGL','META','JPM','JNJ','XOM','PG','KO','WMT','HD','UNH','CVX','PEP','MRK','CSCO','ORCL','MCD')
          AND date >= '2015-01-01'
          AND date <  '2026-09-01'
        GROUP BY ticker, date
    ),
    ranges AS (
        SELECT
            ticker, date, close,
            count()   OVER w14 AS n14,
            max(high) OVER w14 AS hh14,
            min(low)  OVER w14 AS ll14
        FROM bars
        WINDOW w14 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW)
    ),
    fast AS (
        SELECT
            ticker, date, close,
            if(hh14 > ll14, 100 * (close - ll14) / (hh14 - ll14), 50) AS k_fast
        FROM ranges
        WHERE n14 = 14
    ),
    slow AS (
        SELECT
            ticker, date, close,
            avg(k_fast) OVER w3 AS k_slow,
            count()     OVER w3 AS n3
        FROM fast
        WINDOW w3 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    ),
    fwd AS (
        SELECT
            ticker, date, close, k_slow,
            lagInFrame(k_slow, 1)  OVER wl AS k_prev,
            leadInFrame(close, 5)  OVER wf AS c5,
            leadInFrame(close, 10) OVER wf AS c10,
            leadInFrame(close, 20) OVER wf AS c20
        FROM slow
        WHERE n3 = 3
        WINDOW
            wl AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
            wf AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    ),
    tagged AS (
        SELECT
            100 * (c5  / close - 1) AS fwd5_pct,
            100 * (c10 / close - 1) AS fwd10_pct,
            100 * (c20 / close - 1) AS fwd20_pct,
            arrayJoin(arrayConcat(
                if((k_prev >= 80) AND (k_slow < 80), ['%K ตัดลงต่ำกว่า 80'], emptyArrayString()),
                if((k_prev <= 20) AND (k_slow > 20), ['%K ตัดขึ้นเหนือ 20'], emptyArrayString()),
                ['ค่าฐาน: ทุกวันทำการ']
            ))                      AS signal_group
        FROM fwd
        WHERE date >= '2016-01-01'
          AND c5 > 0 AND c10 > 0 AND c20 > 0
    )
SELECT
    signal_group,
    round(100 * countIf(fwd5_pct  > 0) / count(), 1) AS win_rate_5d_pct,
    round(100 * countIf(fwd10_pct > 0) / count(), 1) AS win_rate_10d_pct,
    round(100 * countIf(fwd20_pct > 0) / count(), 1) AS win_rate_20d_pct,
    count()                                          AS observation_count
FROM tagged
GROUP BY signal_group
ORDER BY indexOf(['%K ตัดลงต่ำกว่า 80', '%K ตัดขึ้นเหนือ 20', 'ค่าฐาน: ทุกวันทำการ'], signal_group)
ลองรันด้วยตัวเอง

ที่ระยะ 10 วันทำการ อัตราชนะอยู่ที่ 57.9% หลังสัญญาณตัดลงต่ำกว่า 80 อยู่ที่ 58.7% หลังสัญญาณตัดขึ้นเหนือ 20 และอยู่ที่ 57.4% สำหรับค่าฐาน ที่ระยะ 20 วันทำการ ค่าฐานอยู่ที่ 59.7% ส่วนกลุ่มสัญญาณตัดลงอยู่ที่ 59.3% ขนาดตัวอย่างอยู่ในคอลัมน์สุดท้าย มี 2394 ครั้งสำหรับสัญญาณตัดลงต่ำกว่า 80 และ 51713 วันหุ้นสำหรับค่าฐาน ตัวอย่างระดับหลายหมื่นวันแบบนี้ทำให้ค่าฐานนิ่งพอจะใช้เป็นไม้บรรทัดได้

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

เทรนด์เปลี่ยนความหมายของตัวเลขเดิม

เส้นค่าเฉลี่ย 200 วัน (MA200) คือค่าเฉลี่ยของราคาปิด 200 วันทำการล่าสุด นิยมใช้เป็นเส้นแบ่งหยาบ ๆ ว่าหุ้นอยู่ในโซนขาขึ้นหรือขาลงระยะยาว สูตรของสโตแคสติกมีคุณสมบัติหนึ่งที่ต้องรู้ก่อนอ่านตารางถัดไป เมื่อราคาทำจุดสูงสุดใหม่ต่อเนื่อง ราคาปิดย่อมอยู่ใกล้ยอดของกรอบ 14 วันแทบทุกวัน ค่า %K จึงค้างอยู่เหนือ 80 ได้ยาวเป็นสัปดาห์โดยที่ไม่มีอะไรผิดปกติ นี่คือสาเหตุที่คำว่า overbought ในเทรนด์ขาขึ้นแรงแทบไม่ได้บอกอะไรเลย

คำสั่งดึงข้อมูลสัญญาณเดียวกัน แยกตามตำแหน่งราคาเทียบเส้นค่าเฉลี่ย 200 วัน
signal_groupmedian_fwd_10d_pctwin_rate_10d_pctsignal_count
ตัดลง 80 / ราคาเหนือ MA2000.6957.41922
ตัดลง 80 / ราคาใต้ MA2000.9159.4470
ตัดขึ้น 20 / ราคาเหนือ MA2000.7557.9917
ตัดขึ้น 20 / ราคาใต้ MA2001.3159.8731
SQL เบื้องหลังตัวเลขแต่ละตัว
WITH
    bars AS (
        SELECT
            ticker,
            date,
            argMax(toFloat64(close), _ingest_time) AS close,
            argMax(toFloat64(high),  _ingest_time) AS high,
            argMax(toFloat64(low),   _ingest_time) AS low
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('AAPL','MSFT','NVDA','AMZN','GOOGL','META','JPM','JNJ','XOM','PG','KO','WMT','HD','UNH','CVX','PEP','MRK','CSCO','ORCL','MCD')
          AND date >= '2015-01-01'
          AND date <  '2026-09-01'
        GROUP BY ticker, date
    ),
    ranges AS (
        SELECT
            ticker, date, close,
            count()    OVER w14  AS n14,
            max(high)  OVER w14  AS hh14,
            min(low)   OVER w14  AS ll14,
            count()    OVER w200 AS n200,
            avg(close) OVER w200 AS ma200
        FROM bars
        WINDOW
            w14  AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW),
            w200 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 199 PRECEDING AND CURRENT ROW)
    ),
    fast AS (
        SELECT
            ticker, date, close, n200, ma200,
            if(hh14 > ll14, 100 * (close - ll14) / (hh14 - ll14), 50) AS k_fast
        FROM ranges
        WHERE n14 = 14
    ),
    slow AS (
        SELECT
            ticker, date, close, n200, ma200,
            avg(k_fast) OVER w3 AS k_slow,
            count()     OVER w3 AS n3
        FROM fast
        WINDOW w3 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
    ),
    fwd AS (
        SELECT
            ticker, date, close, n200, ma200, k_slow,
            lagInFrame(k_slow, 1)  OVER wl AS k_prev,
            leadInFrame(close, 10) OVER wf AS c10
        FROM slow
        WHERE n3 = 3
        WINDOW
            wl AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),
            wf AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    ),
    split AS (
        SELECT
            100 * (c10 / close - 1)            AS fwd10_pct,
            cityHash64(ticker, toString(date)) AS h,
            concat(
                if((k_prev >= 80) AND (k_slow < 80), 'ตัดลง 80', 'ตัดขึ้น 20'),
                ' / ',
                if(close >= ma200, 'ราคาเหนือ MA200', 'ราคาใต้ MA200')
            )                                  AS signal_group
        FROM fwd
        WHERE date >= '2016-01-01'
          AND c10 > 0
          AND n200 = 200
          AND (((k_prev >= 80) AND (k_slow < 80)) OR ((k_prev <= 20) AND (k_slow > 20)))
    )
SELECT
    signal_group,
    round(quantileDeterministic(0.5)(fwd10_pct, h), 2) AS median_fwd_10d_pct,
    round(100 * countIf(fwd10_pct > 0) / count(), 1)   AS win_rate_10d_pct,
    count()                                            AS signal_count
FROM split
GROUP BY signal_group
ORDER BY indexOf([
    'ตัดลง 80 / ราคาเหนือ MA200',
    'ตัดลง 80 / ราคาใต้ MA200',
    'ตัดขึ้น 20 / ราคาเหนือ MA200',
    'ตัดขึ้น 20 / ราคาใต้ MA200'
], signal_group)
ลองรันด้วยตัวเอง

เมื่อสัญญาณตัดลงต่ำกว่า 80 เกิดขณะราคายืนเหนือ MA200 ซึ่งเกิด 1922 ครั้งในตัวอย่าง ค่ามัธยฐาน 10 วันถัดไปอยู่ที่ 0.69% และอัตราชนะอยู่ที่ 57.4% เมื่อสัญญาณเดียวกันเกิดขณะราคาอยู่ใต้ MA200 ซึ่งเกิด 470 ครั้ง ตัวเลขคู่เดียวกันคือ 0.91% กับ 59.4% ฝั่งสัญญาณตัดขึ้นเหนือ 20 ก็แยกออกเป็นสองโลกเช่นกัน คือ 0.75% กับอัตราชนะ 57.9% เมื่อราคาอยู่เหนือ MA200 และ 1.31% กับ 59.8% เมื่อราคาอยู่ใต้เส้น

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

สัญญาณไหนคุ้มที่จะอดนอนเฝ้าตลาดสหรัฐ

ตลาดหุ้นสหรัฐเปิดเวลา 20:30 หรือ 21:30 ตามเวลาไทย ขึ้นกับว่าอยู่ในช่วง daylight saving หรือไม่ ต้นทุนของการเฝ้าจอช่วงนั้นคือการนอน ซึ่งเป็นต้นทุนจริงและควรถูกเทียบกับสิ่งที่ได้กลับมา

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

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

บันทึกวิธีคำนวณและข้อจำกัดของตัวเลขชุดนี้
  • กลุ่มตัวอย่างคือหุ้นสหรัฐขนาดใหญ่สภาพคล่องสูง 20 ตัว ได้แก่ AAPL, MSFT, NVDA, AMZN, GOOGL, META, JPM, JNJ, XOM, PG, KO, WMT, HD, UNH, CVX, PEP, MRK, CSCO, ORCL และ MCD ใช้ราคาปิดรายวันตั้งแต่ต้นปี 2016 ถึงกลางปี 2026
  • พารามิเตอร์คือกรอบ 14 วันทำการ แล้วเฉลี่ยสองชั้น ชั้นละ 3 วัน สัญญาณทั้งหมดอ่านจากเส้น %K แบบช้า ซึ่งเท่ากับ fast %D
  • ผลตอบแทนล่วงหน้าเป็นแบบราคาปิดต่อราคาปิด ไม่หักค่าคอมมิชชัน ไม่รวมสเปรด ไม่รวมภาษี และไม่ปรับด้วยเงินปันผล ตัวเลขจริงที่เทรดได้จะต่ำกว่านี้
  • ทุกวันในตัวอย่างต้องมีราคาล่วงหน้าครบทั้ง 5, 10 และ 20 วันทำการ ทั้งกลุ่มสัญญาณและค่าฐานจึงเป็นชุดวันเดียวกันเป๊ะ เทียบกันได้โดยตรง
  • หน้าต่างผลตอบแทนซ้อนทับกัน ตัวอย่างจึงไม่เป็นอิสระต่อกันอย่างสมบูรณ์ ค่ามัธยฐานถูกเลือกใช้เพื่อลดอิทธิพลของวันสุดโต่ง
  • รายชื่อหุ้นถูกเลือกจากบริษัทที่ใหญ่และมีสภาพคล่องในปัจจุบัน ผลลัพธ์จึงมีอคติจากการอยู่รอด (survivorship bias) และไม่ครอบคลุมหุ้นขนาดเล็กที่กรอบ 14 วันแคบกว่ามาก
  • แถวข้อมูลรายวันถูกรวมแบบหนึ่งวันต่อหนึ่งหุ้นก่อนคำนวณ เพื่อกันแถวซ้ำจากการนำเข้าข้อมูลหลายรอบ

FAQ

%K เหนือ 80 แปลว่าหุ้นแพงเกินไปหรือไม่

ไม่ใช่ ค่า 80 ขึ้นไปบอกเพียงว่าราคาปิดอยู่ใกล้ยอดของกรอบสูงสุดต่ำสุด 14 วันล่าสุด ในตารางด้านบน ค่ามัธยฐานผลตอบแทน 10 วันหลังสัญญาณตัดลงจาก 80 อยู่ที่ 0.73% ขณะที่ค่าฐานของทุกวันทำการอยู่ที่ 0.82% ระดับ 80 เป็นเส้นที่คนกำหนด ไม่ใช่ระดับที่ข้อมูลกำหนด

สโตแคสติกต่างจาก RSI อย่างไร

สองตัวนี้วัดคนละอย่างแม้จะแสดงผลบนสเกล 0 ถึง 100 เหมือนกัน RSI เปรียบเทียบขนาดเฉลี่ยของวันที่ราคาขึ้นกับวันที่ราคาลง ส่วนสโตแคสติกวัดตำแหน่งของราคาปิดภายในกรอบสูงสุดต่ำสุด หุ้นที่ค่อย ๆ ไต่ขึ้นทุกวันกับหุ้นที่พุ่งวันเดียวแล้วนิ่งจึงให้ค่าสองตัวนี้ต่างกันได้มาก

ทำไมค่าสโตแคสติกในแต่ละโปรแกรมไม่ตรงกัน

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

ค่า 14, 3, 3 มาจากไหน และเปลี่ยนได้ไหม

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


ทุกแผงในหน้านี้กางคำสั่ง SQL ที่อยู่เบื้องหลังให้ดูได้ ถ้าอยากเปลี่ยนกรอบจาก 14 เป็น 9 เปลี่ยนเส้นแบ่งจาก 80 เป็น 90 หรือเปลี่ยนรายชื่อหุ้นเป็นชุดที่คุณเฝ้าอยู่จริง ลองถามเป็นภาษาคนบน Strasmore terminal แล้วรันคำถามเดิมด้วยตัวเลขของคุณเอง

#stochastic oscillator#อินดิเคเตอร์#technical analysis#momentum#backtest