//@version=6 // ═══════════════════════════════════════════════════════════════════════════ // SESSION EXPANSION LAB v1.3 — เครื่องมือวิจัย ไม่ใช่เครื่องมือให้สัญญาณ // ═══════════════════════════════════════════════════════════════════════════ // // คำถามเดียวที่เครื่องมือนี้ตอบ: // // "ช่วงที่สัญญาณทำเงิน อธิบายได้ด้วยสภาวะตลาดที่วัดได้ล่วงหน้า // หรือเป็นแค่โชค" // // มันจึงเดินนาฬิกาสองเรือนขนานกัน แล้วถามว่าเรือนแรกทำนายเรือนที่สองได้ไหม // // A · ประเภทวัน — วัดตอนเปิดเซสชันแล้วล็อกค่า ห้ามจัดใหม่ย้อนหลัง // B · สุขภาพสัญญาณ — expectancy เคลื่อนที่ของสัญญาณตัวมันเอง // // ถ้า A ทำนาย B ได้ → "อย่าเทรดวันแบบนี้" คือกฎที่มีเนื้อ // ถ้าทำนายไม่ได้ → ตัวกรองประเภทวันของเราคือของประดับ และควรรู้ก่อนเสียเงิน // // ── ทำไมต้องมีตัวยับยั้ง ไม่ใช่แค่ตัวหาจุดเข้า ──────────────────────────── // Patrick Nill วัดไว้ว่า ~70% ของวันเป็นวันกรอบ การรู้ว่า "วันนี้ไม่ใช่วัน // ที่ควรเทรดตามการขยายตัว" จึงมีค่ามากกว่าการหาจุดเข้าเพิ่มอีกสิบจุด // // ── ระดับที่ใช้ ────────────────────────────────────────────────────────── // ตัวคูณที่ใส่มาเป็นค่าที่คาลิเบรตจากทองจริง 948 เซสชัน ไม่ใช่ค่าที่ยืมมา // จาก NQ ซึ่งผิดทั้งขนาดและทิศทางความไม่สมมาตร กดดู "ค่าที่ยืมมา" ได้ // เพื่อเทียบ แต่ค่าตั้งต้นคือของจริง // // ระดับ เปอร์เซ็นไทล์ ขึ้น ลง โอกาสถูกแตะ // L1 P40 0.115 0.021 60.0% // AVR- P55 0.271 0.173 45.0% // AVR P70 0.415 0.355 30.1% // AVR+ P85 0.715 0.674 15.1% // Max P95 1.081 1.088 5.1% // // base rate พวกนี้คือของแพงที่สุดในเครื่องมือนี้ เพราะมันทำให้ตั้ง R:R ได้ // ด้วยเหตุผล — ถ้าแตะ L1 แล้ว โอกาสไปต่อถึง AVR คือ 30.1/60.0 = 50% // ไม่ใช่ 30% และไม่ใช่การเดา // // ── สิ่งที่เครื่องมือนี้จะไม่ทำ ──────────────────────────────────────────── // ไม่มีลูกศรเข้าออก ไม่มีคะแนนความมั่นใจ ไม่บอกทิศ // และมันต้องพร้อมตอบว่า "ข้อมูลไม่พอ" เป็นคำตอบส่วนใหญ่ // ถ้ามันเจอ edge บ่อย ๆ แปลว่าสร้างมันผิด // // วาดด้วย line/label/box/table เท่านั้น จึงไม่กินโควตา 64 plots เลย // ═══════════════════════════════════════════════════════════════════════════ indicator("Session Expansion Lab v1.3", "SEL v1.3", overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500) // ─── กลุ่มการตั้งค่า ─────────────────────────────────────────────────────── var string G_SES = "① เซสชันและระดับ" var string G_DAY = "② การจำแนกประเภทวัน (ล็อกตอนเปิด)" var string G_SIG = "③ นิยามสัญญาณที่จะทดสอบ" var string G_LAB = "④ ห้องทดลอง — สองนาฬิกา · placebo · บัญชีนับครั้ง" var string G_BB = "②b Bollinger Bands — ป้ายทางเลือกไว้เทียบ" var string G_PTC = "⑦ ตรวจก่อนยิง — ด่านตรวจออเดอร์" var string G_PTA = "⑦a บัญชีและเพดานประจำวัน" // ── ① เซสชันและระดับ ───────────────────────────────────────────────────── asiaSes = input.session("1400-2300", "กรอบ Asia (เวลา Exchange)", group = G_SES) tradeSes = input.session("2300-1400", "ช่วงที่วัดการขยายตัว", group = G_SES) atrLen = input.int(20, "ATR รายวัน", minval = 5, maxval = 100, group = G_SES) // ── ชุดระดับ ──────────────────────────────────────────────────────────── // v1.1: ค่าตั้งต้นเปลี่ยนจาก "ไม่สมมาตร" เป็น "มีเงื่อนไข" หลังการทดสอบพบว่า // ความไม่สมมาตรเป็นเงาของตลาดกระทิง ไม่ใช่คุณสมบัติของเซสชัน // corr(ราคาเปลี่ยน, ความไม่สมมาตร) = +0.74 · ปีที่ทองนิ่งอัตราส่วน = 1.00 พอดี // ในขาลงกลับด้านเป็น 0.83 · ตัดเทรนด์ออกแล้วสับป้าย 2,000 รอบ ได้ p = 0.109 // รายละเอียด: xaulearn.com/research/avr-asymmetry/ lvlSet = input.string("มีเงื่อนไข (แนะนำ)", "ชุดระดับ", options = ["มีเงื่อนไข (แนะนำ)", "สมมาตร", "ไม่สมมาตร (เดิม)", "ยืมจาก NQ"], group = G_SES, tooltip = "มีเงื่อนไข = คิดเฉพาะเซสชันที่หลุดกรอบจริง (66% ของโอกาส) " + "ซึ่งเป็น base rate ที่ระบบเบรกเอาต์ต้องใช้\n\n" + "สมมาตร = รวมสองฝั่งเป็นการแจกแจงเดียว 1,864 ตัวอย่าง ไม่เดิมพันทิศทาง\n\n" + "ไม่สมมาตร (เดิม) = ค่าที่คาลิเบรตแยกฝั่ง — พิสูจน์แล้วว่าเป็น artifact " + "ของตลาดกระทิง เก็บไว้ให้เทียบเท่านั้น\n\n" + "ยืมจาก NQ = ค่าที่ใช้อยู่ในเครื่องมือเดิม ไม่ได้วัดจากทอง") // เฉพาะเซสชันที่หลุดกรอบจริง 66% ของโอกาส — base rate ของระบบเบรกเอาต์ var float[] M_COND = array.from(0.279, 0.403, 0.581, 0.854, 1.220) // รวมสองฝั่ง 1,864 ตัวอย่าง — ไม่เดิมพันทิศทาง var float[] M_SYM = array.from(0.064, 0.228, 0.399, 0.694, 1.096) // แยกฝั่งตามการคาลิเบรตเดิม — เก็บไว้เทียบ ไม่แนะนำให้ใช้ var float[] M_ASYU = array.from(0.115, 0.271, 0.415, 0.715, 1.081) var float[] M_ASYD = array.from(0.021, 0.173, 0.355, 0.674, 1.088) var float[] M_NQ = array.from(0.326, 0.600, 0.850, 1.100, 1.350) isCond = lvlSet == "มีเงื่อนไข (แนะนำ)" isAsym = lvlSet == "ไม่สมมาตร (เดิม)" mUp = isCond ? M_COND : lvlSet == "สมมาตร" ? M_SYM : isAsym ? M_ASYU : M_NQ mDn = isCond ? M_COND : lvlSet == "สมมาตร" ? M_SYM : isAsym ? M_ASYD : M_NQ // โอกาสถูกแตะ — ชุดมีเงื่อนไขต้องคูณด้วยสัดส่วนเซสชันที่หลุดกรอบจริง // ถึงจะเทียบกับชุดอื่นได้ ไม่งั้นมันดูดีกว่าความจริง 1.5 เท่า var float BRK_FRAC = 0.66 var float[] HIT_C = array.from(0.600, 0.450, 0.300, 0.150, 0.050) hitOf(int i) => h = array.get(HIT_C, i - 1) isCond ? h * BRK_FRAC : h // ── ② ประเภทวัน ───────────────────────────────────────────────────────── // ตัวแปรเดียว วัดได้ตอนเปิดเซสชัน และมีกลไกรองรับ: // กรอบ Asia แคบเทียบกับ ATR = ตลาดอัดแน่น = มีแนวโน้มจะขยายตัว // กรอบ Asia กว้างแล้ว = เคลื่อนไปเยอะแล้ว = การขยายตัวต่อมีโอกาสน้อยลง // // ⚑ ค่าคงที่ 0.45/0.90 ที่ใช้มาตลอด แบ่งวันออกมาเป็น 334/113/31 — เบ้เกือบ 11 ต่อ 1 // เกณฑ์ที่ต้องเอาชนะแปรผันกับ 1÷√(กลุ่มเล็กสุด) ดังนั้นกลุ่ม 31 ไม้เป็นตัวกำหนดพลัง // ของการทดสอบทั้งหมด และการมี 334 ไม้ในอีกกลุ่มช่วยอะไรไม่ได้เลย // การแบ่งด้วยเปอร์เซ็นไทล์บังคับให้กลุ่มสมดุลโดยโครงสร้าง จึงเป็นค่าตั้งต้นตั้งแต่ v1.2 splitMode = input.string("เปอร์เซ็นไทล์ (สมดุล · แนะนำ)", "วิธีแบ่งกลุ่มประเภทวัน", options = ["เปอร์เซ็นไทล์ (สมดุล · แนะนำ)", "ค่าคงที่ (เดิม)"], group = G_DAY, tooltip = "เปอร์เซ็นไทล์ = เทียบค่าวันนี้กับเซสชันที่ผ่านมา แล้วแบ่งตามอันดับ " + "กลุ่มจึงมีขนาดใกล้เคียงกันเสมอ และใช้เกณฑ์ชุดเดียวกันทั้งสองป้าย " + "ทำให้เทียบกันได้จริง · " + "ค่าคงที่ = ค่าเดิม 0.45/0.90 เก็บไว้ทำซ้ำผลเก่าเท่านั้น ไม่แนะนำให้ใช้ตัดสิน · " + "หมายเหตุ: เปอร์เซ็นไทล์ถามว่า อัดแน่นเทียบกับช่วงหลังนี้ไหม " + "ซึ่งเป็นคนละคำถามกับ อัดแน่นในเชิงสัมบูรณ์ไหม") pcLo = input.float(33, "อัดแน่น: เปอร์เซ็นไทล์ต่ำกว่า", minval = 5, maxval = 45, step = 1, group = G_DAY) pcHi = input.float(67, "คลายแล้ว: เปอร์เซ็นไทล์สูงกว่า", minval = 55, maxval = 95, step = 1, group = G_DAY) histLen = input.int(250, "เทียบกับกี่เซสชันย้อนหลัง", minval = 40, maxval = 2000, step = 10, group = G_DAY, tooltip = "หน้าต่างเลื่อน ไม่ใช่ทั้งประวัติ เพราะระดับความผันผวนของทองเปลี่ยนไปมาก " + "จนค่าจากปี 2023 เทียบกับปีนี้ตรง ๆ ไม่ได้") minHist = input.int(40, "ต้องมีประวัติกี่เซสชันก่อนเริ่มจำแนก", minval = 20, maxval = 500, step = 10, group = G_DAY, tooltip = "ก่อนถึงจำนวนนี้ อันดับเปอร์เซ็นไทล์ยังหยาบเกินไป จึงติดป้ายว่า ปกติ ไปก่อน " + "ไม่ใช่เดาว่าอัดแน่นหรือคลาย") coilLo = input.float(0.45, "โหมดค่าคงที่ — อัดแน่น: Asia/ATR ต่ำกว่า", minval = 0.05, maxval = 2.0, step = 0.05, group = G_DAY) coilHi = input.float(0.90, "โหมดค่าคงที่ — คลายแล้ว: Asia/ATR สูงกว่า", minval = 0.10, maxval = 3.0, step = 0.05, group = G_DAY) // ป้ายประเภทวันมีได้มากกว่าหนึ่งแบบ และนั่นคือประเด็นทั้งหมด — งาน VWAP สอนเราว่า // placebo ตอบได้แค่ "ดีกว่าสุ่มไหม" ไม่ได้ตอบว่า "ดีกว่าป้ายอื่นที่หาได้ฟรีไหม" // จึงต้องมีป้ายที่สองให้สลับไปวัดด้วยตัวชี้วัดชุดเดียวกัน showB = input.bool(true, "วัดป้าย B (ความกว้าง BB) เทียบไปพร้อมกัน", group = G_DAY, tooltip = "สองป้ายตอบคำถามเดียวกันคือ ตลาดอัดแน่นหรือคลายแล้ว แต่วัดคนละทางสนิท · " + "ตารางแสดงพร้อมกันทั้งสอง ไม่ใช่สลับดูทีละอัน — เพราะการสลับไปมา " + "แล้วจำตัวเลขเอง คือการค้นหาแบบหนึ่งที่ไม่มีใครนับ และเป็นวิธีที่เราหลอกตัวเองได้ง่ายที่สุด · " + "ปิดได้ถ้ากราฟหน่วง เพราะ placebo ต้องรันสองรอบ") useDir = input.bool(true, "เพิ่มตัวแปรที่รู้ทิศทางด้วย", group = G_DAY, tooltip = "ScalperVBubble ล้มเพราะใช้ Kaufman-ER ซึ่งไม่สนใจทิศ จึงปล่อยให้ " + "เทรดสวนเทรนด์ผ่านไปได้ ตัวนี้เพิ่มตำแหน่งราคาปิดในกรอบ Asia " + "เพื่อทดสอบว่าการรู้ทิศช่วยจริงไหม") // ── ②b Bollinger Bands ────────────────────────────────────────────────── // ค่าตั้งต้นตรงกับที่ผู้ใช้ตั้งไว้บนชาร์ตจริง (20 · VWMA · close · 1.2) // บทบาทของมันที่นี่มีสองอย่างเท่านั้น: เป็นเส้นให้ดู และเป็นป้ายทางเลือกให้ทดสอบ // มันไม่ได้ถูกใส่มาเพื่อสร้างสัญญาณเข้า และจะไม่ถูกใช้แบบนั้นจนกว่าจะผ่านตารางนี้ก่อน bbShow = input.bool(true, "วาด Bollinger Bands", group = G_BB) bbLen = input.int(20, "Length", minval = 1, group = G_BB) bbMaType = input.string("VWMA", "Basis MA Type", options = ["SMA", "EMA", "SMMA (RMA)", "WMA", "VWMA"], group = G_BB) bbSrc = input(close, "Source", group = G_BB) bbMult = input.float(1.2, "StdDev", minval = 0.001, maxval = 50, step = 0.1, group = G_BB) bbOff = input.int(0, "Offset", minval = -500, maxval = 500, group = G_BB) bbRank = input.int(120, "ช่วงเทียบเปอร์เซ็นไทล์ความกว้าง (บาร์)", minval = 20, maxval = 1000, step = 10, group = G_BB, tooltip = "ความกว้างดิบเทียบข้ามช่วงเวลาไม่ได้ เพราะราคาทองเปลี่ยนระดับไปมาก " + "จึงต้องแปลงเป็นเปอร์เซ็นไทล์เทียบกับตัวมันเองก่อน") bbCoil = input.float(25.0, "อัดแน่น: เปอร์เซ็นไทล์ต่ำกว่า", minval = 1, maxval = 99, step = 5, group = G_BB) bbLoose = input.float(75.0, "คลายแล้ว: เปอร์เซ็นไทล์สูงกว่า", minval = 1, maxval = 99, step = 5, group = G_BB) bbBasCol = input.color(#2962FF, "สีเส้นกลาง", group = G_BB, inline = "bc") bbUpCol = input.color(#F23645, "สีเส้นบน", group = G_BB, inline = "bc") bbLoCol = input.color(#089981, "สีเส้นล่าง", group = G_BB, inline = "bc") bbWidth = input.int(1, "ความหนาเส้น", minval = 1, maxval = 5, group = G_BB) bbFill = input.color(color.rgb(33, 150, 243, 95), "สีพื้นระหว่างเส้น", group = G_BB) // ── ③ สัญญาณที่จะทดสอบ ────────────────────────────────────────────────── entryLvl = input.int(1, "เข้าเมื่อแตะระดับที่", minval = 1, maxval = 4, group = G_SIG) tgtLvl = input.int(3, "เป้าหมายระดับที่", minval = 2, maxval = 5, group = G_SIG) stopMode = input.string("ล็อก R:R ให้เท่ากันสองฝั่ง", "วิธีตั้งจุดหยุด", options = ["ล็อก R:R ให้เท่ากันสองฝั่ง", "อิงกรอบ Asia", "อิงระยะเข้า"], group = G_SIG, tooltip = "เดิมใช้ 'อิงกรอบ Asia' ซึ่งสมมาตรทั้งสองฝั่ง แต่จุดเข้าไม่สมมาตร " + "R:R จึงต่างกันได้ถึง 53% โดยไม่ได้เกิดจากขอบอะไรเลย\n\n" + "โหมดล็อก R:R วางจุดหยุดให้ระยะเสี่ยงเป็นสัดส่วนคงที่ของระยะกำไร " + "ทั้งขึ้นและลง ความต่างที่เหลือจึงมาจากตลาด ไม่ใช่จากเรขาคณิต") targetRR = input.float(1.5, "R:R ที่ต้องการ (โหมดล็อก R:R)", minval = 0.5, maxval = 5.0, step = 0.1, group = G_SIG) stopBack = input.float(0.5, "ตัวคูณจุดหยุด (สองโหมดที่เหลือ)", minval = 0.1, maxval = 4.0, step = 0.1, group = G_SIG) spreadIn = input.float(0.30, "สเปรดเฉลี่ยของโบรกคุณ (ดอลลาร์)", minval = 0.0, step = 0.05, group = G_SIG, tooltip = "ใช้ตรวจว่าจุดเข้าอยู่ห่างขอบกรอบพอจะเรียกว่าเบรกเอาต์ไหม " + "หรืออยู่ในสัญญาณรบกวน") minSprd = input.int(10, "จุดเข้าต้องห่างขอบอย่างน้อยกี่เท่าของสเปรด", minval = 1, maxval = 100, group = G_SIG, tooltip = "ต่ำกว่านี้ไม่ใช่การเบรกเอาต์ เป็นการแตะขอบกรอบแล้วจ่ายสเปรด") costPer = input.float(0.35, "ต้นทุนไปกลับ (ดอลลาร์ต่อออนซ์)", minval = 0.0, step = 0.05, group = G_SIG, tooltip = "spread + commission ของโบรกคุณ ระบบที่ขอบบางตายตรงนี้") // ── ④ ห้องทดลอง ───────────────────────────────────────────────────────── // ── ⑦ ตรวจก่อนยิง ──────────────────────────────────────────────────────── // // โมดูลนี้ตอบคำถามคนละข้อกับทั้งไฟล์ที่เหลือ · ส่วนบนถามว่า "ป้ายนี้มีข้อมูลไหม" // ส่วนนี้ถามว่า **"ออเดอร์ที่กำลังจะยิง ประกอบถูกและรอดได้ไหม"** // // ⚑ กฎเหล็ก: **มันห้ามได้ แต่อนุญาตไม่ได้** — คำตัดสินมีสองแบบเท่านั้น // คือ "หยุด" กับ "ไม่พบข้อห้าม" · คำว่า "เข้าได้" หรือ "ความมั่นใจ %" // จะไม่ปรากฏที่ใดในโมดูลนี้ // // เหตุผล: เราฆ่าสมมติฐานสัญญาณไปสี่สายแล้ว (การขยายตัวรายเซสชัน · กลไก VWAP // สามข้อบนสามระบอบ · สัญญาณ DeskTrend · การเข้าแบบลิมิต 12 ชุด) · // เครื่องมือที่ให้คะแนนจะสร้างความมั่นใจที่ไม่มีของรองรับ — และมีบันทึกว่า // สแต็กเคยพิมพ์ CONF 90-95 ให้ฟาดที่ 4045-69 แล้วทองวิ่ง +129$ สวนหน้า // // เครื่องมือที่ห้ามได้อย่างเดียว สร้างความมั่นใจปลอมไม่ได้ · ที่ให้คะแนนได้ จะสร้างเสมอ ptcOn = input.bool(false, "เปิดด่านตรวจก่อนยิง", group = G_PTC, tooltip = "ปิดไว้ตอนทำวิจัย เปิดตอนจะยิงออเดอร์ · ใช้ตารางแยกคนละมุมกับตารางวิจัย " + "เพราะเป็นคนละคำถามและคนละจังหวะ") ptcSideS = input.string("ซื้อ", " ทิศทาง", options = ["ซื้อ", "ขาย"], group = G_PTC) ptcNow = input.bool(true, " ใช้ราคาปัจจุบันเป็นจุดเข้า", group = G_PTC) ptcEntry = input.float(0.0, " จุดเข้า (ถ้าไม่ใช้ราคาปัจจุบัน)", minval = 0, group = G_PTC) ptcSL = input.float(0.0, " จุดหยุด (0 = คิดจาก ATR ให้)", minval = 0, group = G_PTC) ptcTP = input.float(0.0, " เป้าหมาย (0 = คิดจาก R:R ให้)", minval = 0, group = G_PTC) ptcRR = input.float(2.0, " R:R ที่ใช้เมื่อไม่ได้กรอกเป้าหมาย", minval = 0.5, maxval = 10, step = 0.1, group = G_PTC) ptcSLx = input.float(1.5, " ตัวคูณ ATR เมื่อไม่ได้กรอกจุดหยุด", minval = 0.2, maxval = 10, step = 0.1, group = G_PTC) ptcAtrLn = input.int(14, " ATR ของ TF นี้ (ใช้วัดสัญญาณรบกวน)", minval = 2, maxval = 200, group = G_PTC) ptcMinRR = input.float(1.0, " R:R หลังหักต้นทุน ต้องไม่ต่ำกว่า", minval = 0.1, maxval = 10, step = 0.1, group = G_PTC) ptcWarnRR = input.float(1.5, " ต่ำกว่านี้ให้เตือน", minval = 0.1, maxval = 10, step = 0.1, group = G_PTC) ptcNoise = input.float(0.30, " จุดหยุดแคบกว่ากี่เท่า ATR ถือว่าอยู่ในสัญญาณรบกวน", minval = 0.05, maxval = 2, step = 0.05, group = G_PTC) ptcWide = input.float(3.0, " จุดหยุดกว้างกว่ากี่เท่า ATR ถือว่ากว้างเกิน", minval = 1, maxval = 10, step = 0.5, group = G_PTC) ptcTol = input.float(0.25, " ถือว่า 'ชิด' ระดับเมื่อห่างน้อยกว่ากี่เท่า ATR", minval = 0.05, maxval = 2, step = 0.05, group = G_PTC) ptcTrend = input.bool(true, " เตือนเมื่อสวนเทรนด์ TF ใหญ่", group = G_PTC) ptcTrTF = input.timeframe("240", " TF ที่ใช้ดูเทรนด์", group = G_PTC) ptcTrLen = input.int(50, " EMA", minval = 5, maxval = 400, group = G_PTC) ptcPos = input.string("ซ้ายล่าง", " ตำแหน่งตารางด่านตรวจ", options = ["ขวาบน", "ขวาล่าง", "ซ้ายบน", "ซ้ายล่าง"], group = G_PTC) ptcBal = input.float(300.0, "เงินในบัญชี (ดอลลาร์)", minval = 1, group = G_PTA) ptcRisk = input.float(1.0, "เสี่ยงต่อไม้ (%)", minval = 0.05, maxval = 10, step = 0.05, group = G_PTA) ptcCommis = input.float(0.0, "คอมมิชชันต่อล็อต ต่อข้าง (ดอลลาร์)", minval = 0.0, step = 0.5, group = G_PTA) ptcContract = input.float(100.0, "ขนาดสัญญา (ออนซ์ต่อล็อต)", minval = 1, group = G_PTA) ptcMinLot = input.float(0.01, "ล็อตต่ำสุด", minval = 0.001, step = 0.001, group = G_PTA) ptcLotStep = input.float(0.01, "ขั้นล็อต", minval = 0.001, step = 0.001, group = G_PTA) ptcDayPct = input.float(3.0, "เพดานขาดทุนรายวัน (% ของบัญชี)", minval = 0.5, maxval = 20, step = 0.5, group = G_PTA) ptcLossToday = input.float(0.0, " ขาดทุนไปแล้ววันนี้ (ดอลลาร์ ใส่เป็นบวก)", minval = 0, step = 1, group = G_PTA) ptcTradesDone = input.int(0, "ไม้ที่ทำไปแล้ววันนี้", minval = 0, maxval = 100, group = G_PTA) ptcMaxTrades = input.int(4, " เพดานไม้ต่อวัน", minval = 1, maxval = 100, group = G_PTA, tooltip = "Patrick Nill: แพ้สี่ไม้ติดให้หยุด · เพดานนี้บังคับสิ่งเดียวกันโดยไม่ต้องใช้ใจ") healthN = input.int(20, "สุขภาพสัญญาณ: ดูย้อนหลังกี่ไม้", minval = 5, maxval = 100, group = G_LAB) minCell = input.int(15, "ขั้นต่ำต่อช่องก่อนยอมสรุป", minval = 5, maxval = 200, group = G_LAB, tooltip = "ต่ำกว่านี้เครื่องมือจะขึ้นว่า 'ข้อมูลไม่พอ' แทนที่จะให้ตัวเลข " + "ที่ดูน่าเชื่อถือแต่เป็นสัญญาณรบกวน") oosFrac = input.float(0.30, "สัดส่วนข้อมูลนอกกลุ่ม", minval = 0.1, maxval = 0.5, step = 0.05, group = G_LAB) runPlac = input.bool(true, "รัน placebo (สุ่มจุดเข้าแบบจับคู่)", group = G_LAB, tooltip = "สุ่มโดยให้จำนวนไม้ ทิศทาง และระยะถือเท่าของจริงทุกอย่าง " + "เหลือต่างกันแค่จังหวะ ถ้าชนะการสุ่มไม่ได้ จังหวะไม่มีข้อมูล") placRuns = input.int(100, "จำนวนรอบ placebo", minval = 50, maxval = 500, step = 50, group = G_LAB, tooltip = "แต่ละรอบต้องสับป้ายทั้งชุดแล้วรวมใหม่ ยิ่งเทรดเยอะยิ่งหนัก " + "ถ้า Pine ขึ้นว่าใช้เวลาเกิน ให้ลดเลขนี้ลง") lookCnt = input.int(1, "ทดลองไปแล้วกี่ชุดพารามิเตอร์", minval = 1, maxval = 100000, group = G_LAB, tooltip = "นับตามจริง ทุกครั้งที่ปรับค่าแล้วดูผลใหม่คือหนึ่งชุด " + "Pine จำข้ามการคอมไพล์ไม่ได้ คุณจึงต้องพิมพ์เอง " + "เกณฑ์นัยสำคัญจะสูงขึ้นตามเลขนี้ ถ้าใส่ต่ำกว่าจริง คุณโกหกตัวเอง") // ── ⑤ การแสดงผล ───────────────────────────────────────────────────────── var string G_VTB = "⑤a ตาราง" var string G_VLN = "⑤b เส้นและป้ายบนชาร์ต" showTbl = input.bool(true, "แสดงตารางผลวิจัย", group = G_VTB) tblPos = input.string("ขวาบน", "ตำแหน่ง", options = ["ขวาบน", "ขวาล่าง", "ซ้ายบน", "ซ้ายล่าง"], group = G_VTB) tblSizeS = input.string("เล็ก", "ขนาดตัวอักษร", options = ["จิ๋ว", "เล็ก", "ปกติ", "ใหญ่", "ใหญ่มาก"], group = G_VTB) tblBg = input.color(color.new(color.gray, 80), "สีพื้นหลัง", group = G_VTB) tblInk = input.color(color.new(color.white, 0), "สีตัวอักษรหลัก", group = G_VTB) tblDim = input.color(color.new(color.gray, 30), "สีตัวอักษรรอง", group = G_VTB) tblAcc = input.color(color.new(color.orange, 0), "สีหัวข้อ", group = G_VTB) tblGood = input.color(color.new(color.lime, 20), "สีค่าบวก", group = G_VTB) tblBad = input.color(color.new(color.red, 20), "สีค่าลบ", group = G_VTB) tblWarn = input.color(color.new(color.yellow, 0), "สีคำเตือน", group = G_VTB) tblFrame = input.color(color.new(color.gray, 60), "สีกรอบตาราง", group = G_VTB) showLvl = input.bool(true, "วาดระดับการขยายตัว", group = G_VLN) lvlCol = input.color(color.orange, "สีเส้นระดับ", group = G_VLN, tooltip = "ระดับที่โอกาสถูกแตะสูงจะทึบกว่า ระดับที่ไกลจะจางลง " + "เพื่อให้เห็นน้ำหนักของแต่ละระดับโดยไม่ต้องอ่านตัวเลข") lvlWidth = input.int(1, "ความหนาเส้น", minval = 1, maxval = 5, group = G_VLN) lvlStyS = input.string("จุดไข่ปลา", "ประเภทเส้น", options = ["เส้นทึบ", "เส้นประ", "จุดไข่ปลา"], group = G_VLN) lvlExt = input.int(60, "ความยาวเส้น (บาร์)", minval = 10, maxval = 500, step = 10, group = G_VLN, tooltip = "บนกรอบเวลาเล็ก 60 บาร์สั้นกว่าหนึ่งเซสชัน — M3 60 บาร์ = 3 ชั่วโมง " + "ถ้าเส้นจบก่อนเซสชันจบ ให้เพิ่มเลขนี้") lblSizeS = input.string("จิ๋ว", "ขนาดตัวอักษรบนชาร์ต", options = ["จิ๋ว", "เล็ก", "ปกติ", "ใหญ่", "ใหญ่มาก"], group = G_VLN) lblCol = input.color(color.new(color.orange, 25), "สีตัวอักษรบนชาร์ต", group = G_VLN) showBox = input.bool(true, "วาดกรอบ Asia", group = G_VLN) boxBord = input.color(color.new(color.gray, 40), "สีขอบกรอบ Asia", group = G_VLN) boxBg = input.color(color.new(color.gray, 92), "สีพื้นกรอบ Asia", group = G_VLN) // แปลงชื่อไทยเป็นค่าคงที่ของ Pine sizeOf(string sz) => sz == "จิ๋ว" ? size.tiny : sz == "เล็ก" ? size.small : sz == "ปกติ" ? size.normal : sz == "ใหญ่" ? size.large : size.huge // ปัดล็อตลงตามขั้นของโบรกเกอร์เสมอ — ปัดขึ้นคือเสี่ยงเกินที่ตั้งใจไว้ floorLot(float raw, float stepv) => stepv <= 0 ? raw : math.floor(raw / stepv) * stepv posOf(string s) => s == "ขวาบน" ? position.top_right : s == "ขวาล่าง" ? position.bottom_right : s == "ซ้ายบน" ? position.top_left : position.bottom_left tblSize = sizeOf(tblSizeS) lblSize = sizeOf(lblSizeS) lvlSty = lvlStyS == "เส้นทึบ" ? line.style_solid : lvlStyS == "เส้นประ" ? line.style_dashed : line.style_dotted // ═══════════════════════════════════════════════════════════════════════════ // ATR รายวันของ "เมื่อวาน" — ห้ามมองอนาคต // ใช้ [1] และ lookahead_off เพื่อให้ค่าที่ได้เป็นค่าที่ปิดแล้วจริง ๆ // ═══════════════════════════════════════════════════════════════════════════ prevATR = request.security(syminfo.tickerid, "D", ta.atr(atrLen)[1], lookahead = barmerge.lookahead_off) // ═══════════════════════════════════════════════════════════════════════════ // กรอบ Asia — สะสมระหว่างเซสชัน แล้วล็อกเมื่อจบ // ═══════════════════════════════════════════════════════════════════════════ inAsia = not na(time(timeframe.period, asiaSes, syminfo.timezone)) inTrade = not na(time(timeframe.period, tradeSes, syminfo.timezone)) var float aHi = na // ระหว่างสะสม var float aLo = na var float lockHi = na // ล็อกแล้ว ใช้ทั้งวัน var float lockLo = na var float lockATR = na var int dayId = 0 asiaStart = inAsia and not inAsia[1] asiaEnd = not inAsia and inAsia[1] if asiaStart aHi := high aLo := low else if inAsia aHi := math.max(nz(aHi, high), high) aLo := math.min(nz(aLo, low), low) // ── การล็อก: ทุกอย่างที่ใช้ตัดสินใจทั้งวัน ถูกแช่ไว้ ณ วินาทีนี้ ─────────── // นี่คือกฎที่กันการวัดแบบวงกลม ถ้าเราจัดประเภทวันใหม่หลังจากเห็นผลแล้ว // "วันเทรนด์ทำกำไรดี" จะกลายเป็นประโยคที่จริงโดยอัตโนมัติและไร้ความหมาย newSession = asiaEnd and not na(aHi) and not na(aLo) and not na(prevATR) and prevATR > 0 if newSession lockHi := aHi lockLo := aLo lockATR := prevATR dayId += 1 asiaRange = lockHi - lockLo haveDay = not na(lockHi) and not na(lockATR) and lockATR > 0 // ═══════════════════════════════════════════════════════════════════════════ // Bollinger Bands — เส้น และป้ายทางเลือก // ═══════════════════════════════════════════════════════════════════════════ bbMa(source, len, _type) => switch _type "SMA" => ta.sma(source, len) "EMA" => ta.ema(source, len) "SMMA (RMA)" => ta.rma(source, len) "WMA" => ta.wma(source, len) "VWMA" => ta.vwma(source, len) bbBasis = bbMa(bbSrc, bbLen, bbMaType) bbDev = bbMult * ta.stdev(bbSrc, bbLen) bbUpper = bbBasis + bbDev bbLower = bbBasis - bbDev // ความกว้างต้องหารด้วยเส้นกลาง ไม่งั้นทองที่ $4,400 กับทองที่ $1,800 เทียบกันไม่ได้ // แล้วยังต้องแปลงเป็นเปอร์เซ็นไทล์อีกชั้น เพราะแม้แต่ความกว้างสัมพัทธ์ก็ยังไหลไปตามระบอบ bbW = bbBasis != 0 ? (bbUpper - bbLower) / bbBasis : na bbPct = ta.percentrank(bbW, bbRank) pu = plot(bbShow ? bbUpper : na, "BB Upper", color = bbUpCol, linewidth = bbWidth, offset = bbOff) pl = plot(bbShow ? bbLower : na, "BB Lower", color = bbLoCol, linewidth = bbWidth, offset = bbOff) plot(bbShow ? bbBasis : na, "BB Basis", color = bbBasCol, linewidth = bbWidth, offset = bbOff) fill(pu, pl, title = "BB Background", color = bbShow ? bbFill : na) // ═══════════════════════════════════════════════════════════════════════════ // A · ประเภทวัน — คำนวณครั้งเดียวตอนล็อก // ═══════════════════════════════════════════════════════════════════════════ var int dayType = 0 var int dayTypeB = 0 // -1 คลายแล้ว · 0 ปกติ · +1 อัดแน่น var float coilVal = na var int dirBias = 0 // -1 ล่างกรอบ · 0 กลาง · +1 บนกรอบ // คำนวณ *ทั้งสองป้าย* ทุกเซสชัน ไม่มีสวิตช์ให้เลือกดูทีละอัน // เหตุผลอยู่ในบันทึกของงาน VWAP: การสลับไปมาแล้วจำตัวเลขเอง คือการค้นหาแบบหนึ่ง // ที่ไม่มีใครนับ — ถ้าอยากเทียบสองป้าย ต้องเห็นพร้อมกันในตารางเดียว // อันดับเทียบเฉพาะเซสชัน *ก่อนหน้า* เท่านั้น ค่าวันนี้ยังไม่ถูกใส่เข้ากองตอนจัดอันดับ // ไม่งั้นวันนี้จะช่วยกำหนดอันดับของตัวเอง ซึ่งคือการมองอนาคตแบบละเอียดอ่อน rankAgainst(float[] hist, float v) => int m = array.size(hist) int c = 0 for i = 0 to math.max(0, m - 1) if m == 0 break if array.get(hist, i) <= v c += 1 m > 0 ? 100.0 * c / m : na var float coilB = na var float coilRank = na var float[] coilHist = array.new() if newSession // ทั้งสองป้ายล็อกค่า ณ วินาทีที่เปิดเซสชันเหมือนกัน ไม่งั้นเทียบกันไม่ได้ // และเงื่อนไขไม่รีเพนต์ก็จะหายไปพร้อมกัน coilVal := asiaRange / lockATR coilB := bbPct // จัดอันดับก่อน แล้วค่อยเก็บค่าวันนี้ลงกอง ลำดับนี้สำคัญ coilRank := rankAgainst(coilHist, coilVal) array.push(coilHist, coilVal) if array.size(coilHist) > histLen array.shift(coilHist) usePct = splitMode == "เปอร์เซ็นไทล์ (สมดุล · แนะนำ)" ready = array.size(coilHist) > minHist // โหมดเปอร์เซ็นไทล์ใช้ *เกณฑ์ชุดเดียวกัน* กับทั้งสองป้าย ขนาดกลุ่มจึงใกล้เคียงกัน // โดยโครงสร้าง และความต่างที่เหลือคือความต่างของ "วิธีวัด" ล้วน ๆ ซึ่งคือสิ่งที่ทดสอบ dayType := usePct ? (ready and not na(coilRank) ? (coilRank < pcLo ? 1 : coilRank > pcHi ? -1 : 0) : 0) : (coilVal < coilLo ? 1 : coilVal > coilHi ? -1 : 0) dayTypeB := na(bbPct) ? 0 : usePct ? (bbPct < pcLo ? 1 : bbPct > pcHi ? -1 : 0) : (bbPct < bbCoil ? 1 : bbPct > bbLoose ? -1 : 0) // ตำแหน่งราคาปิดในกรอบ Asia — ตัวแปรที่รู้ทิศ ซึ่งเป็นสิ่งที่ Kaufman-ER ไม่มี posInRange = asiaRange > 0 ? (close - lockLo) / asiaRange : 0.5 dirBias := posInRange > 0.66 ? 1 : posInRange < 0.34 ? -1 : 0 typeName(int t) => t == 1 ? "อัดแน่น" : t == -1 ? "คลายแล้ว" : "ปกติ" dayTypeName = typeName(dayType) dayTypeNameB = typeName(dayTypeB) // ═══════════════════════════════════════════════════════════════════════════ // ระดับการขยายตัว — จากกรอบที่ล็อกแล้ว ไม่ขยับอีกทั้งวัน // ═══════════════════════════════════════════════════════════════════════════ lvlUp(int i) => lockHi + array.get(mUp, i - 1) * lockATR lvlDn(int i) => lockLo - array.get(mDn, i - 1) * lockATR // ═══════════════════════════════════════════════════════════════════════════ // สัญญาณ + การบันทึกผล // // นิยาม: ในช่วงวัดผล ถ้าราคาแตะระดับ entryLvl ให้เข้าตามทิศที่แตะ // เป้าหมาย = ระดับ tgtLvl · หยุดขาดทุน = กลับเข้ากรอบ Asia // หนึ่งไม้ต่อวัน ต่อทิศ — กันการนับซ้ำในวันที่แกว่งไปมา // ═══════════════════════════════════════════════════════════════════════════ var int posSide = 0 var float posEntry = na var float posTgt = na var float posStop = na var int posDay = 0 var int posType = 0 var int posTypeB = 0 var int posDir = 0 // อคติทิศทางที่ล็อกไว้ตอนเปิดเซสชัน var int posBar = 0 var float[] pnl = array.new() // ผลต่อไม้ หักต้นทุนแล้ว var int[] pType = array.new() // ป้าย A · กรอบ Asia ÷ ATR (ล็อกแล้ว) var int[] pTypeB = array.new() // ป้าย B · ความกว้าง BB (ล็อกแล้ว) var int[] pHealth = array.new() // สุขภาพสัญญาณตอนเข้า var int[] pAligned = array.new() // ทิศที่เข้า ตรงกับอคติที่ล็อกไว้ไหม var int[] pSide = array.new() // +1 ขึ้น / -1 ลง — ต้องแยกสถิติเสมอ var int lastLongDay = 0 var int lastShortDay = 0 // สุขภาพสัญญาณ ณ ตอนนี้: expectancy ของ healthN ไม้ล่าสุด signalHealth() => n = array.size(pnl) if n < healthN 0 // ยังไม่รู้ ถือว่าเป็นกลาง else float s = 0.0 for i = n - healthN to n - 1 s += array.get(pnl, i) avg = s / healthN avg > 0 ? 1 : avg < 0 ? -1 : 0 // จุดหยุดตามโหมดที่เลือก // ล็อก R:R — ระยะเสี่ยง = ระยะกำไร ÷ R:R ที่ต้องการ ทำให้สองฝั่งเทียบกันได้ // อิงกรอบ — วิธีเดิม สมมาตรทั้งสองฝั่งขณะที่จุดเข้าไม่สมมาตร // อิงระยะเข้า — สัดส่วนของระยะที่จุดเข้าห่างจากขอบกรอบ stopFor(int side, float entry, float target) => float st = na if stopMode == "ล็อก R:R ให้เท่ากันสองฝั่ง" st := entry - side * math.abs(target - entry) / math.max(0.1, targetRR) else if stopMode == "อิงกรอบ Asia" st := side == 1 ? lockHi - stopBack * asiaRange : lockLo + stopBack * asiaRange else off = math.abs(entry - (side == 1 ? lockHi : lockLo)) st := entry - side * stopBack * off st // ── เปิดสถานะ ──────────────────────────────────────────────────────────── if haveDay and inTrade and posSide == 0 and barstate.isconfirmed eUp = lvlUp(entryLvl) eDn = lvlDn(entryLvl) if high >= eUp and dayId != lastLongDay posSide := 1 posEntry := eUp posTgt := lvlUp(tgtLvl) posStop := stopFor(1, eUp, lvlUp(tgtLvl)) posDay := dayId posType := dayType posTypeB := dayTypeB posDir := useDir ? dirBias : 0 posBar := bar_index lastLongDay := dayId else if low <= eDn and dayId != lastShortDay posSide := -1 posEntry := eDn posTgt := lvlDn(tgtLvl) posStop := stopFor(-1, eDn, lvlDn(tgtLvl)) posDay := dayId posType := dayType posTypeB := dayTypeB posDir := useDir ? dirBias : 0 posBar := bar_index lastShortDay := dayId // ── ปิดสถานะ ───────────────────────────────────────────────────────────── // เขียนแบบนี้เพราะฟังก์ชันของ Pine แก้ค่าตัวแปรระดับบนไม่ได้ (posSide := 0 // ในฟังก์ชันคือ error) หาราคาปิดก่อน แล้วค่อยบันทึกและล้างสถานะข้างนอก float exitPx = na if posSide != 0 and barstate.isconfirmed if posSide == 1 exitPx := low <= posStop ? posStop : high >= posTgt ? posTgt : dayId != posDay ? close : na // วันจบก่อนถึงเป้าหรือหยุด else exitPx := high >= posStop ? posStop : low <= posTgt ? posTgt : dayId != posDay ? close : na if not na(exitPx) array.push(pnl, posSide * (exitPx - posEntry) - costPer) array.push(pType, posType) array.push(pTypeB, posTypeB) array.push(pHealth, signalHealth()) array.push(pAligned, posSide == posDir ? 1 : posDir == 0 ? 0 : -1) array.push(pSide, posSide) posSide := 0 // ═══════════════════════════════════════════════════════════════════════════ // สถิติ — คำนวณบนบาร์สุดท้ายเท่านั้น // ═══════════════════════════════════════════════════════════════════════════ // หมายเหตุ: อย่าตั้งชื่อพารามิเตอร์ว่า from หรือ to — เป็นคำสงวนของ Pine // (ใช้ในไวยากรณ์ลูป for) คอมไพเลอร์จะฟ้อง syntax error ที่พารามิเตอร์ตัวแรก // ซึ่งชี้ผิดที่ และหาสาเหตุยากถ้าไม่รู้ typeStat(int[] src, int typ, int i0, int i1) => float s = 0.0 int c = 0 int w = 0 for i = i0 to i1 if array.get(src, i) == typ s += array.get(pnl, i) c += 1 if array.get(pnl, i) > 0 w += 1 [c, c > 0 ? s / c : na, c > 0 ? w / float(c) : na] // ขึ้นกับลงคือสองระบบ ไม่ใช่ระบบเดียว การรวมกันคือการซ่อนว่าฝั่งไหนใช้ได้ sideStat(int sd) => float s = 0.0 int c = 0 int w = 0 for i = 0 to math.max(0, array.size(pnl) - 1) if array.size(pnl) == 0 break if array.get(pSide, i) == sd s += array.get(pnl, i) c += 1 if array.get(pnl, i) > 0 w += 1 [c, c > 0 ? s / c : na, c > 0 ? w / float(c) : na] // สับป้ายแบบสุ่ม โดยคง pnl ของทุกไม้ไว้เหมือนเดิมทุกประการ ถ้าความต่างที่วัดได้จริง // จมอยู่กลางการกระจายของการสับมั่ว แปลว่าป้ายนั้นไม่มีข้อมูล เราแค่หั่นตัวเลขจน // บังเอิญเจอความต่าง · seed คงที่ ผลจึงทำซ้ำได้ ไม่ใช่กด refresh จนได้ผลที่ชอบ // // เดิมเขียน inline เพราะเข้าใจว่าฟังก์ชัน Pine แก้ตัวแปรระดับบนไม่ได้ — จริง // แต่มัน *อ่าน* ได้ และ *คืนค่า* ได้ ซึ่งพอสำหรับงานนี้ พอทำเป็นฟังก์ชันแล้ว // จึงเรียกกับป้ายไหนก็ได้ และนั่นคือสิ่งที่ทำให้เทียบสองป้ายพร้อมกันเป็นไปได้ placeboOf(int[] src, float realSpread, int n) => int seed = 20260907 int beat = 0 int[] shuf = array.new() for i = 0 to n - 1 array.push(shuf, array.get(src, i)) for pr = 0 to placRuns - 1 for i = n - 1 to 1 seed := (seed * 1103515 + 12345) % 2147483647 j = math.abs(seed) % (i + 1) tmp = array.get(shuf, i) array.set(shuf, i, array.get(shuf, j)) array.set(shuf, j, tmp) float sC = 0.0 float sS = 0.0 int nC = 0 int nS = 0 for i = 0 to n - 1 lbl = array.get(shuf, i) if lbl == 1 sC += array.get(pnl, i) nC += 1 else if lbl == -1 sS += array.get(pnl, i) nS += 1 if nC > 0 and nS > 0 if math.abs(sC / nC - sS / nS) >= math.abs(realSpread) beat += 1 beat / float(placRuns) dirStat(int aligned) => float s = 0.0 int c = 0 // การ์ดกันกับดักเดียวกับข้างบน ตอนนี้ผู้เรียกกันไว้แล้ว แต่ฟังก์ชันไม่ควร // พึ่งว่าผู้เรียกจะจำได้ตลอดไป for i = 0 to math.max(0, array.size(pnl) - 1) if array.size(pnl) == 0 break if array.get(pAligned, i) == aligned s += array.get(pnl, i) c += 1 [c, c > 0 ? s / c : na] // ── placebo ──────────────────────────────────────────────────────────── // สับป้ายประเภทวันแบบสุ่ม โดยคง pnl ทุกไม้ไว้เหมือนเดิม แล้วดูว่าความต่าง // ที่วัดได้จริงอยู่ตรงไหนของการกระจายนั้น — ตอบคำถามเดียวว่า "ป้ายประเภทวัน // มีข้อมูล หรือเราแค่หั่นตัวเลขจนบังเอิญเจอความต่าง" // // PRNG เขียนแบบ inline เพราะฟังก์ชันของ Pine แก้ตัวแปรระดับบนไม่ได้ // seed คงที่ ผลจึงทำซ้ำได้ ไม่ใช่กด refresh จนได้ผลที่ชอบ // ═══════════════════════════════════════════════════════════════════════════ // การวาด // ═══════════════════════════════════════════════════════════════════════════ var box asiaBox = na var line[] lvlLines = array.new() var label[] lvlLabels = array.new