XAULearn

Session Expansion Lab v1.3 · ซอร์สโค้ด

Pine Script ต้นฉบับ

ไฟล์นี้ถูกคัดลอกจากต้นฉบับใน G:\Trader\PineSCript\ ตอนสร้างเว็บ จึงเป็นโค้ดชุดเดียวกับที่ทำงานอยู่บนชาร์ตจริง ณ วันที่ปรับปรุง

Session_Expansion_Lab_v1.pine 1,134 บรรทัด 92 KB ต้นฉบับแก้ไขล่าสุด 8 กันยายน 2569 ดาวน์โหลด .pine

Pine Script ยังไม่มีไวยากรณ์เฉพาะสำหรับตัวไฮไลต์ที่ใช้ จึงไฮไลต์ด้วยไวยากรณ์ TypeScript ซึ่งใกล้เคียงกันพอที่จะอ่านง่าย ตัวเลขบรรทัดตรงกับไฟล์จริง ใช้อ้างอิงในบทเรียนได้ เช่น session-lab-v1.pine:120

สารบัญส่วนของโค้ด (28)
  1. 3 SESSION EXPANSION LAB v1.3 — เครื่องมือวิจัย ไม่ใช่เครื่องมือให้สัญญาณ
  2. 59 ① เซสชันและระดับ
  3. 102 ② ประเภทวัน
  4. 148 ②b Bollinger Bands
  5. 173 ③ สัญญาณที่จะทดสอบ
  6. 198 ④ ห้องทดลอง
  7. 199 ⑦ ตรวจก่อนยิง
  8. 281 ⑤ การแสดงผล
  9. 337 ATR รายวันของ "เมื่อวาน" — ห้ามมองอนาคต
  10. 344 กรอบ Asia — สะสมระหว่างเซสชัน แล้วล็อกเมื่อจบ
  11. 380 Bollinger Bands — เส้น และป้ายทางเลือก
  12. 406 A · ประเภทวัน — คำนวณครั้งเดียวตอนล็อก
  13. 465 ระดับการขยายตัว — จากกรอบที่ล็อกแล้ว ไม่ขยับอีกทั้งวัน
  14. 471 สัญญาณ + การบันทึกผล
  15. 573 สถิติ — คำนวณบนบาร์สุดท้ายเท่านั้น
  16. 575 หมายเหตุ: อย่าตั้งชื่อพารามิเตอร์ว่า from หรือ to — เป็นคำสงวนของ Pine
  17. 664 การวาด
  18. 706 ตารางผลวิจัย
  19. 709 ⑦ ตรวจก่อนยิง — คำนวณ
  20. 979 ⑦ ตรวจก่อนยิง — ตารางแยก
  21. 981 แยกตารางโดยตั้งใจ · ส่วนบนของไฟล์ตอบว่า "ป้ายนี้มีข้อมูลไหม" ซึ่งเป็นคำถาม
  22. 1004 ① ราคาสามจุด และการตรวจข้างที่ผิดพลาดง่ายที่สุด
  23. 1022 ② ขนาดไม้และเงินที่เสี่ยงจริง
  24. 1043 ③ R:R ที่จริง ไม่ใช่บนกระดาษ
  25. 1052 ④ จุดหยุดอยู่ในสัญญาณรบกวนไหม — ด่านเดียวกับส่วนวิจัย
  26. 1070 ⑤ ระดับที่คนอื่นก็เห็น
  27. 1086 ⑥ สวนเทรนด์ — ข้อที่แพงที่สุดในบันทึกทั้งหมด
  28. 1100 ⑦ เพดานประจำวัน — กันการล้างแค้น
//@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<float>()

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<float>()   // ผลต่อไม้ หักต้นทุนแล้ว
var int[]   pType    = array.new<int>()     // ป้าย A · กรอบ Asia ÷ ATR (ล็อกแล้ว)
var int[]   pTypeB   = array.new<int>()     // ป้าย B · ความกว้าง BB (ล็อกแล้ว)
var int[]   pHealth  = array.new<int>()     // สุขภาพสัญญาณตอนเข้า
var int[]   pAligned = array.new<int>()     // ทิศที่เข้า ตรงกับอคติที่ล็อกไว้ไหม
var int[]   pSide    = array.new<int>()     // +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<int>()
    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<line>()
var label[] lvlLabels = array.new<label>()

if showBox and newSession
    asiaBox := box.new(bar_index, lockHi, bar_index + 1, lockLo,
         border_color = boxBord, bgcolor = boxBg)
if showBox and haveDay and inTrade and not na(asiaBox)
    box.set_right(asiaBox, bar_index + 1)

if showLvl and newSession
    // ⚠ กับดัก Pine: `for i = 0 to array.size(x) - 1` เมื่อ array ว่างจะกลายเป็น
    // `for i = 0 to -1` ซึ่ง Pine นับ *ถอยหลัง* ลูปจึงรันด้วย i = 0 บน array ที่ว่าง
    // แล้วระเบิดเป็น RE10045 "Index 0 is out of bounds, array size is 0"
    // ภาษาอื่นลูปนี้ไม่รันเลย นี่คือสำนวนที่ยืมข้ามภาษามาแล้วผิด
    // while + pop เขียนผิดแบบนี้ไม่ได้ เพราะมันเช็คขนาดทุกรอบอยู่แล้ว
    while array.size(lvlLines) > 0
        line.delete(array.pop(lvlLines))
    while array.size(lvlLabels) > 0
        label.delete(array.pop(lvlLabels))
    for i = 1 to 5
        hitPct = hitOf(i)
        // ความจางไล่ตามโอกาสถูกแตะ — ระดับที่เกิดบ่อยเด่นกว่าระดับที่ไกล
        // โดยยังเป็นสีเดียวกับที่ผู้ใช้เลือก ถ้าตั้งสีมาโปร่งอยู่แล้ว
        // ระดับไกลจะจางลงต่อจากฐานนั้น ไม่ใช่ทับค่าที่ตั้งไว้
        transp = math.min(90, color.t(lvlCol) + (i <= 2 ? 0 : i <= 4 ? 25 : 45))
        col    = color.new(lvlCol, transp)
        xEnd   = bar_index + lvlExt
        array.push(lvlLines, line.new(bar_index, lvlUp(i), xEnd, lvlUp(i),
             color = col, style = lvlSty, width = lvlWidth))
        array.push(lvlLines, line.new(bar_index, lvlDn(i), xEnd, lvlDn(i),
             color = col, style = lvlSty, width = lvlWidth))
        array.push(lvlLabels, label.new(xEnd, lvlUp(i),
             str.tostring(math.round(hitPct * 100)) + "%", style = label.style_none,
             textcolor = lblCol, size = lblSize))
        array.push(lvlLabels, label.new(xEnd, lvlDn(i),
             str.tostring(math.round(hitPct * 100)) + "%", style = label.style_none,
             textcolor = lblCol, size = lblSize))

// ═══════════════════════════════════════════════════════════════════════════
//  ตารางผลวิจัย
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
//  ⑦ ตรวจก่อนยิง — คำนวณ
// ═══════════════════════════════════════════════════════════════════════════
ptcATR   = ta.atr(ptcAtrLn)
ptcTrEma = request.security(syminfo.tickerid, ptcTrTF, ta.ema(close, ptcTrLen)[1],
     lookahead = barmerge.lookahead_on)

ptcSide  = ptcSideS == "ซื้อ" ? 1 : -1
ptcE     = ptcNow or ptcEntry <= 0 ? close : ptcEntry
ptcS     = ptcSL > 0 ? ptcSL : ptcE - ptcSide * ptcSLx * ptcATR
ptcSDist = math.abs(ptcE - ptcS)
ptcT     = ptcTP > 0 ? ptcTP : ptcE + ptcSide * ptcRR * ptcSDist
ptcTDist = math.abs(ptcT - ptcE)

ptcRiskMoney = ptcBal * ptcRisk / 100.0
ptcRawLots   = ptcSDist > 0 ? ptcRiskMoney / (ptcSDist * ptcContract) : 0.0
ptcLots      = floorLot(ptcRawLots, ptcLotStep)
ptcRiskReal  = ptcLots * ptcSDist * ptcContract
ptcCost      = ptcLots * (spreadIn * ptcContract + 2.0 * ptcCommis)
ptcGross     = ptcLots * ptcTDist * ptcContract
ptcLoss      = ptcLots * ptcSDist * ptcContract

ptcRRraw = ptcSDist > 0 ? ptcTDist / ptcSDist : na
ptcRRnet = (ptcLoss + ptcCost) > 0 ? (ptcGross - ptcCost) / (ptcLoss + ptcCost) : na

// ── ด่าน · ทุกด่านตอบได้แค่ ผ่าน / เตือน / ห้าม ─────────────────────────
ptcSLbad  = ptcSide == 1 ? ptcS >= ptcE : ptcS <= ptcE
ptcTPbad  = ptcSide == 1 ? ptcT <= ptcE : ptcT >= ptcE
ptcLotBad = ptcLots < ptcMinLot
ptcSprdX  = spreadIn > 0 ? ptcSDist / spreadIn : na
ptcTight  = not na(ptcSprdX) and ptcSprdX < minSprd
ptcAtrX   = ptcATR > 0 ? ptcSDist / ptcATR : na
ptcNarrow = not na(ptcAtrX) and ptcAtrX < ptcNoise
ptcBroad  = not na(ptcAtrX) and ptcAtrX > ptcWide
ptcRRbad  = not na(ptcRRnet) and ptcRRnet < ptcMinRR
ptcRRwarn = not na(ptcRRnet) and ptcRRnet >= ptcMinRR and ptcRRnet < ptcWarnRR

// ระดับที่คนอื่นก็เห็น — ใช้กรอบ Asia ที่ล็อกไว้แล้ว ซึ่งไม่รีเพนต์
ptcTolPx  = ptcATR * ptcTol
ptcSLnear = haveDay and (math.abs(ptcS - lockHi) < ptcTolPx or
     math.abs(ptcS - lockLo) < ptcTolPx)
ptcTPblock = haveDay and (ptcSide == 1 ? (lockHi > ptcE and lockHi < ptcT)
     : (lockLo < ptcE and lockLo > ptcT))

ptcFade = ptcTrend and not na(ptcTrEma) and
     ((ptcSide == 1 and close < ptcTrEma) or (ptcSide == -1 and close > ptcTrEma))

ptcDayCap = ptcBal * ptcDayPct / 100.0
ptcOverDay = ptcLossToday + ptcRiskReal > ptcDayCap
ptcOverCnt = ptcTradesDone >= ptcMaxTrades

// เตือนไม่นับเป็นข้อห้าม — มีแต่ข้อที่เป็นเลขคณิตหรือเพดานเท่านั้นที่ห้ามได้
ptcBlocked = ptcSLbad or ptcTPbad or ptcLotBad or ptcTight or ptcRRbad or
     ptcOverDay or ptcOverCnt

tPos = tblPos == "ขวาบน" ? position.top_right : tblPos == "ขวาล่าง" ? position.bottom_right
     : tblPos == "ซ้ายบน" ? position.top_left : position.bottom_left

var table T = table.new(tPos, 5, 24, border_width = 1,
     frame_color = tblFrame, frame_width = 1)

cell(int c, int r, string txt, color tc, color bg) =>
    table.cell(T, c, r, txt, text_color = tc, bgcolor = bg, text_size = tblSize,
         text_halign = text.align_left)

if showTbl and barstate.islast
    table.clear(T, 0, 0, 4, 23)
    n = array.size(pnl)
    hdr = tblBg
    ink = tblInk
    dim = tblDim

    cell(0, 0, "SESSION EXPANSION LAB", tblAcc, hdr)
    cell(1, 0, "", ink, hdr)
    cell(2, 0, "", ink, hdr)
    cell(3, 0, "", ink, hdr)
    cell(4, 0, "", ink, hdr)

    // แถวนี้บอกว่าวันนี้ *ทั้งสองป้าย* ว่าอย่างไร ถ้ามันไม่ตรงกัน นั่นคือข้อมูล
    cell(0, 1, "วันนี้", dim, hdr)
    onPct = splitMode == "เปอร์เซ็นไทล์ (สมดุล · แนะนำ)"
    cell(1, 1, "A " + dayTypeName + (onPct
         ? (na(coilRank) ? "" : " (P" + str.tostring(coilRank, "#") + ")")
         : (na(coilVal) ? "" : " (" + str.tostring(coilVal, "#.00") + ")")), ink, hdr)
    cell(2, 1, showB ? "B " + dayTypeNameB +
         (na(coilB) ? "" : " (" + str.tostring(coilB, "#") + ")") : "B ปิดอยู่",
         showB ? ink : dim, hdr)
    cell(3, 1, "ทิศ " + (dirBias > 0 ? "บน" : dirBias < 0 ? "ล่าง" : "กลาง"), dim, hdr)
    cell(4, 1, isAsym ? "⚠ ไม่สมมาตร" : lvlSet, isAsym ? tblBad : dim, hdr)

    // ── ตรวจก่อนดูสถิติ: จุดเข้าอยู่ห่างขอบกรอบพอไหม ──────────────────────
    // ระดับที่เปอร์เซ็นไทล์ต่ำอาจอยู่ในกองศูนย์ (38% ของเซสชันไม่หลุดกรอบล่างเลย)
    // และระยะที่วัดเป็นดอลลาร์อาจเล็กกว่าสเปรดไม่กี่เท่า ซึ่งไม่ใช่การเบรกเอาต์
    // แต่เป็นการแตะขอบกรอบแล้วจ่ายค่าธรรมเนียม
    entryOffUp = haveDay ? array.get(mUp, entryLvl - 1) * lockATR : na
    entryOffDn = haveDay ? array.get(mDn, entryLvl - 1) * lockATR : na
    minOff     = na(entryOffUp) ? na : math.min(entryOffUp, entryOffDn)
    sprdMult   = spreadIn > 0 and not na(minOff) ? minOff / spreadIn : na
    tooTight   = not na(sprdMult) and sprdMult < minSprd

    cell(0, 17, tooTight ? "⚠ จุดเข้าอยู่ในสัญญาณรบกวน" : "ระยะจุดเข้า",
         tooTight ? tblBad : dim, hdr)
    cell(1, 17, na(minOff) ? "-" : "$" + str.tostring(minOff, "#.00"), ink, hdr)
    cell(2, 17, na(sprdMult) ? "-" : str.tostring(sprdMult, "#") + " เท่าของสเปรด",
         tooTight ? tblBad : dim, hdr)
    cell(3, 17, "ต้องเกิน " + str.tostring(minSprd), dim, hdr)
    cell(4, 17, isAsym and entryLvl <= 2 ? "⚠ อยู่ในกองศูนย์" : "", tblBad, hdr)

    if n < minCell * 2
        cell(0, 3, "ข้อมูลไม่พอ", tblWarn, hdr)
        cell(1, 3, str.tostring(n) + " ไม้", ink, hdr)
        cell(2, 3, "ต้องการอย่างน้อย " + str.tostring(minCell * 2), dim, hdr)
        cell(0, 4, "เครื่องมือนี้จะไม่สรุปจากตัวอย่างน้อย", dim, hdr)
        cell(0, 5, "เลื่อนกราฟไปด้านหลังหรือใช้ TF ใหญ่ขึ้น", dim, hdr)
    else
        isEnd = math.floor(n * (1 - oosFrac)) - 1

        // เกณฑ์สูงขึ้นตามจำนวนชุดที่ทดลอง — ราคาของการดูซ้ำ ต้องมองเห็น
        // ⚠ การดูสองป้ายพร้อมกันคือการดูสองครั้ง ไม่ใช่ครั้งเดียว จึงคูณสองให้อัตโนมัติ
        // ข้อนี้ผู้ใช้ลืมนับง่ายที่สุด และเป็นช่องทางที่ผลบวกปลอมเดินเข้ามา
        looks   = lookCnt * (showB ? 2 : 1)
        penalty = math.sqrt(2 * math.log(math.max(2, looks)))
        sd = 0.0
        for i = 0 to n - 1
            sd += math.pow(array.get(pnl, i), 2)
        sd := math.sqrt(sd / n)

        // ── บล็อกป้าย A และ B — วาดด้วยโค้ดชุดเดียวกัน ─────────────────────
        // เดิมมีสวิตช์ให้ดูทีละป้าย ซึ่งบังคับให้ผู้ใช้จำตัวเลขข้ามการสลับ
        // นั่นคือการค้นหาที่ไม่มีใครนับ · ตอนนี้ทั้งสองอยู่ในสายตาพร้อมกัน
        float spA = na
        float spB = na
        float pA  = na
        float pB  = na
        bool  okA = false
        bool  okB = false

        for blk = 0 to (showB ? 1 : 0)
            isB  = blk == 1
            src  = isB ? pTypeB : pType
            row0 = isB ? 8 : 2
            // ความเบ้ของกลุ่มต้องอยู่ตรงหัวบล็อก เพราะมันเป็นสิ่งแรกที่ต้องดู
            // ก่อนอ่านตัวเลขใด ๆ ข้างล่าง — กลุ่มที่เบ้มากทำให้ผล "ไม่ต่างกัน" ไร้ความหมาย
            [nCo, xCo, yCo] = typeStat(src, 1, 0, n - 1)
            [nNo, xNo, yNo] = typeStat(src, 0, 0, n - 1)
            [nSl, xSl, ySl] = typeStat(src, -1, 0, n - 1)
            biggest = math.max(nCo, math.max(nNo, nSl))
            smallest = math.min(nCo, math.min(nNo, nSl))
            skew = smallest > 0 ? biggest / float(smallest) : na
            cell(0, row0, (isB ? "ป้าย B · ความกว้าง BB" : "ป้าย A · กรอบ Asia ÷ ATR") +
                 (na(skew) ? "" : " · เบ้ " + str.tostring(skew, "#.0") + " ต่อ 1"),
                 na(skew) or skew > 4 ? tblBad : tblAcc, hdr)
            cell(1, row0, "n", dim, hdr)
            cell(2, row0, "expectancy", dim, hdr)
            cell(3, row0, "ชนะ", dim, hdr)
            cell(4, row0, "นอกกลุ่ม", dim, hdr)

            int r = row0 + 1
            for t = 1 to -1
                [cIS, eIS, wIS] = typeStat(src, t, 0, isEnd)
                [cOOS, eOOS, wOOS] = typeStat(src, t, isEnd + 1, n - 1)
                enough = cIS >= minCell
                cell(0, r, typeName(t), ink, hdr)
                cell(1, r, str.tostring(cIS), enough ? ink : dim, hdr)
                cell(2, r, enough ? str.tostring(eIS, "#.00") : "ไม่พอ",
                     not enough ? dim : eIS > 0 ? tblGood : tblBad, hdr)
                cell(3, r, enough ? str.tostring(wIS * 100, "#") + "%" : "-", dim, hdr)
                cell(4, r, cOOS >= minCell ? str.tostring(eOOS, "#.00")
                     : "n=" + str.tostring(cOOS),
                     cOOS < minCell ? dim : eOOS > 0 ? tblGood : tblBad, hdr)
                r += 1

            [cC, eC, wC] = typeStat(src, 1, 0, n - 1)
            [cS, eS, wS] = typeStat(src, -1, 0, n - 1)
            ok = cC >= minCell and cS >= minCell
            sp = ok ? eC - eS : na
            nd = ok ? penalty * sd / math.sqrt(math.min(cC, cS)) : na
            pv = runPlac and ok ? placeboOf(src, sp, n) : na

            cell(0, r, "ต่างกัน (อัดแน่น − คลาย)", tblAcc, hdr)
            cell(1, r, ok ? str.tostring(sp, "#.00") : "ข้อมูลไม่พอ", ink, hdr)
            cell(2, r, "ต้องเกิน", dim, hdr)
            cell(3, r, ok ? str.tostring(nd, "#.00") : "-", ink, hdr)
            cell(4, r, ok ? (math.abs(sp) >= nd ? "ผ่านเกณฑ์" : "ไม่ผ่านเกณฑ์") : "-",
                 ok and math.abs(sp) >= nd ? tblGood : tblBad, hdr)

            cell(0, r + 1, "สับป้ายมั่ว " + str.tostring(placRuns) + " รอบ", dim, hdr)
            cell(1, r + 1, na(pv) ? "-" : str.tostring(pv * 100, "#") + "%", ink, hdr)
            cell(2, r + 1, "ที่ทำได้ดีเท่าหรือดีกว่า", dim, hdr)
            cell(3, r + 1, na(pv) ? "-"
                 : pv <= 0.05 ? "ผ่าน" : pv <= 0.20 ? "ก้ำกึ่ง" : "ไม่ผ่าน",
                 na(pv) ? dim : pv <= 0.05 ? tblGood : pv <= 0.20 ? tblWarn : tblBad, hdr)
            cell(4, r + 1, "ลอง " + str.tostring(looks) + " ชุด" +
                 (showB ? " (สองป้าย)" : ""), looks > 20 ? tblWarn : dim, hdr)

            if isB
                spB := sp
                pB  := pv
                okB := ok
            else
                spA := sp
                pA  := pv
                okA := ok

        // ── เทียบสองป้าย — เหตุผลทั้งหมดที่ต้องแสดงพร้อมกัน ────────────────
        cmp = not showB
             ? "วัดป้ายเดียว — ไม่มีอะไรให้เทียบ เปิด “วัดป้าย B” เพื่อตรวจว่าผลผูกกับวิธีวัดไหม"
             : not (okA and okB) ? "ยังเทียบไม่ได้ — ป้ายใดป้ายหนึ่งข้อมูลไม่พอ"
             : na(pA) or na(pB) ? "ยังเทียบไม่ได้ — placebo ยังไม่ได้รัน"
             : pA > 0.20 and pB > 0.20
             ? "ทั้งสองป้ายไม่ผ่าน — สมมติฐานตายทั้งสาย ไม่ใช่แค่วัดผิดวิธี"
             : pA <= 0.05 and pB <= 0.05
             ? "ผ่านทั้งสองป้าย — สองวิธีวัดที่ไม่เกี่ยวกันชี้ทางเดียวกัน ควรสนใจ"
             : "⚠ ผ่านป้ายเดียว — สิ่งที่วัดได้ผูกกับวิธีวัด ไม่ใช่กับตลาด อย่าเพิ่งเชื่อ"
        cmpCol = not showB ? dim
             : not (okA and okB) or na(pA) or na(pB) ? tblWarn
             : pA <= 0.05 and pB <= 0.05 ? tblGood
             : pA > 0.20 and pB > 0.20 ? tblBad : tblWarn
        cell(0, 14, cmp, cmpCol, hdr)

        // ── แยกทิศ — ขึ้นกับลงคือสองระบบ ────────────────────────────────
        // ก่อนหน้านี้เอามารวมถังเดียว ซึ่งเฉลี่ยระบบที่ EV บวกเข้ากับระบบที่ EV ลบ
        // แล้วได้ตัวเลขที่ไม่ได้อธิบายอะไรเลย
        [cL, eL, wL] = sideStat(1)
        [cSh, eSh, wSh] = sideStat(-1)
        cell(0, 15, "แยกทิศ", tblAcc, hdr)
        cell(1, 15, "ขึ้น n=" + str.tostring(cL), dim, hdr)
        cell(2, 15, cL >= minCell ? str.tostring(eL, "#.00") : "ไม่พอ",
             cL < minCell ? dim : eL > 0 ? tblGood : tblBad, hdr)
        cell(3, 15, "ลง n=" + str.tostring(cSh), dim, hdr)
        cell(4, 15, cSh >= minCell ? str.tostring(eSh, "#.00") : "ไม่พอ",
             cSh < minCell ? dim : eSh > 0 ? tblGood : tblBad, hdr)

        // ── มิติทิศทาง — ข้อที่ ScalperVBubble ไม่มีและเป็นสาเหตุที่มันตาย ──
        [cAl, eAl] = dirStat(1)
        [cAg, eAg] = dirStat(-1)
        cell(0, 16, useDir ? "เข้าตรงทิศที่ล็อกไว้" : "ไม่ได้ทดสอบมิติทิศทาง",
             useDir ? tblAcc : dim, hdr)
        cell(1, 16, useDir ? str.tostring(cAl) + " ไม้" : "-", ink, hdr)
        cell(2, 16, useDir and cAl >= minCell ? str.tostring(eAl, "#.00") : "ไม่พอ", ink, hdr)
        cell(3, 16, useDir ? "สวนทิศ " + str.tostring(cAg) : "", dim, hdr)
        cell(4, 16, useDir and cAg >= minCell ? str.tostring(eAg, "#.00") : "ไม่พอ", ink, hdr)

        // ถ้าฝั่งขึ้นกับฝั่งลงให้คำตอบคนละทาง การสรุปรวมคือการซ่อนความจริง
        sidesSplit = cL >= minCell and cSh >= minCell and
             ((eL > 0 and eSh < 0) or (eL < 0 and eSh > 0))

        // คำตัดสินรวมยึดป้าย A เป็นหลัก เพราะเป็นสมมติฐานต้นทาง ส่วน B คือพยาน
        verdict = tooTight ? "⚠ จุดเข้าอยู่ในสัญญาณรบกวน — ผลไม่มีความหมาย"
             : sidesSplit ? "⚠ ขึ้นกับลงให้คำตอบคนละทาง — ต้องแยกทดสอบ ห้ามสรุปรวม"
             : not okA ? "ข้อมูลไม่พอ — ยังตัดสินไม่ได้"
             : not na(pA) and pA > 0.20
             ? "ประเภทวันเป็นของประดับ — การสับป้ายมั่วก็ทำได้พอกัน"
             : spA > 0 ? "ประเภทวันมีข้อมูลจริง — วันอัดแน่นดีกว่า"
             : "⚠ กลับด้านจากที่คาด — วันคลายแล้วดีกว่า ตรวจสมมติฐานใหม่"
        vc = tooTight or sidesSplit ? tblBad
             : not okA ? tblWarn
             : not na(pA) and pA > 0.20 ? tblBad : tblGood
        cell(0, 18, verdict, vc, hdr)

        cell(0, 19, "เตือน", tblBad, hdr)
        cell(1, 19, "นี่คือผลบนกราฟเดียว", dim, hdr)
        cell(2, 19, "ไม่ใช่ walk-forward", dim, hdr)
        cell(3, 19, "ห้ามใช้สด", tblBad, hdr)
        cell(0, 20, "ต้องพอร์ตไปทดสอบนอกกราฟก่อนเสมอ", dim, hdr)
        cell(0, 21, isCond
             ? "โอกาสถูกแตะที่แสดง คูณด้วย 0.66 แล้ว (สัดส่วนเซสชันที่หลุดกรอบ)"
             : isAsym ? "⚠ ชุดไม่สมมาตรพิสูจน์แล้วว่าเป็น artifact ของตลาดกระทิง"
             : "ที่มาของระดับ: xaulearn.com/research/avr-asymmetry/", dim, hdr)

// ═══════════════════════════════════════════════════════════════════════════
//  ⑦ ตรวจก่อนยิง — ตารางแยก
// ═══════════════════════════════════════════════════════════════════════════
//  แยกตารางโดยตั้งใจ · ส่วนบนของไฟล์ตอบว่า "ป้ายนี้มีข้อมูลไหม" ซึ่งเป็นคำถาม
//  ตอนทำวิจัย · ส่วนนี้ตอบว่า "ออเดอร์นี้ประกอบถูกไหม" ซึ่งเป็นคำถามตอนจะยิง
//  ปนกันในตารางเดียวเมื่อไร คนอ่านจะเอาคำตอบของคำถามหนึ่งไปใช้กับอีกคำถาม
var table P = table.new(posOf(ptcPos), 4, 20, border_width = 1,
     frame_color = tblFrame, frame_width = 1)

pcell(int c, int r, string txt, color tc) =>
    table.cell(P, c, r, txt, text_color = tc, bgcolor = tblBg, text_size = tblSize,
         text_halign = text.align_left)

if ptcOn and barstate.islast
    table.clear(P, 0, 0, 3, 19)
    ok = tblGood
    wn = tblWarn
    bd = tblBad
    ink = tblInk
    dim = tblDim

    pcell(0, 0, "ตรวจก่อนยิง", tblAcc)
    pcell(1, 0, ptcSideS + " " + str.tostring(ptcLots, "#.###") + " ล็อต", ink)
    pcell(2, 0, "", dim)
    pcell(3, 0, "", dim)

    // ── ① ราคาสามจุด และการตรวจข้างที่ผิดพลาดง่ายที่สุด ───────────────
    pcell(0, 1, "จุดเข้า", dim)
    pcell(1, 1, str.tostring(ptcE, format.mintick), ink)
    pcell(2, 1, ptcNow ? "ราคาปัจจุบัน" : "กรอกเอง", dim)
    pcell(3, 1, "", dim)

    pcell(0, 2, "จุดหยุด", dim)
    pcell(1, 2, str.tostring(ptcS, format.mintick), ptcSLbad ? bd : ink)
    pcell(2, 2, ptcSLbad ? "⛔ อยู่ผิดข้างของจุดเข้า"
         : "ห่าง " + str.tostring(ptcSDist, "#.00") + "$", ptcSLbad ? bd : dim)
    pcell(3, 2, ptcSLbad ? "ห้าม" : "ผ่าน", ptcSLbad ? bd : ok)

    pcell(0, 3, "เป้าหมาย", dim)
    pcell(1, 3, str.tostring(ptcT, format.mintick), ptcTPbad ? bd : ink)
    pcell(2, 3, ptcTPbad ? "⛔ อยู่ผิดข้างของจุดเข้า"
         : "ห่าง " + str.tostring(ptcTDist, "#.00") + "$", ptcTPbad ? bd : dim)
    pcell(3, 3, ptcTPbad ? "ห้าม" : "ผ่าน", ptcTPbad ? bd : ok)

    // ── ② ขนาดไม้และเงินที่เสี่ยงจริง ────────────────────────────────
    pcell(0, 4, "ขนาดไม้", dim)
    pcell(1, 4, str.tostring(ptcLots, "#.###") + " ล็อต", ptcLotBad ? bd : ink)
    pcell(2, 4, ptcLotBad
         ? "⛔ ต่ำกว่าล็อตต่ำสุด " + str.tostring(ptcMinLot, "#.###") +
           " — จุดหยุดกว้างเกินไปสำหรับเงินก้อนนี้"
         : "จากเสี่ยง " + str.tostring(ptcRisk, "#.##") + "% = " +
           str.tostring(ptcRiskMoney, "#.00") + "$", ptcLotBad ? bd : dim)
    pcell(3, 4, ptcLotBad ? "ห้าม" : "ผ่าน", ptcLotBad ? bd : ok)

    pcell(0, 5, "เสี่ยงจริงหลังปัดล็อต", dim)
    pcell(1, 5, str.tostring(ptcRiskReal, "#.00") + "$", ink)
    pcell(2, 5, "ตั้งใจไว้ " + str.tostring(ptcRiskMoney, "#.00") + "$ · ปัดล็อตลงเสมอ", dim)
    pcell(3, 5, "", dim)

    pcell(0, 6, "ต้นทุนไปกลับ", dim)
    pcell(1, 6, str.tostring(ptcCost, "#.00") + "$", ink)
    pcell(2, 6, "สเปรด " + str.tostring(spreadIn, "#.00") + "$/ออนซ์" +
         (ptcCommis > 0 ? " + คอมมิชชัน" : "") + " · ใช้ค่าเดียวกับส่วนวิจัย", dim)
    pcell(3, 6, "", dim)

    // ── ③ R:R ที่จริง ไม่ใช่บนกระดาษ ─────────────────────────────────
    pcell(0, 7, "R:R", dim)
    pcell(1, 7, na(ptcRRraw) ? "-" : str.tostring(ptcRRraw, "#.00") + " บนกระดาษ", dim)
    pcell(2, 7, na(ptcRRnet) ? "-"
         : str.tostring(ptcRRnet, "#.00") + " หลังหักต้นทุนจริง",
         ptcRRbad ? bd : ptcRRwarn ? wn : ok)
    pcell(3, 7, ptcRRbad ? "ห้าม" : ptcRRwarn ? "เตือน" : "ผ่าน",
         ptcRRbad ? bd : ptcRRwarn ? wn : ok)

    // ── ④ จุดหยุดอยู่ในสัญญาณรบกวนไหม — ด่านเดียวกับส่วนวิจัย ─────────
    pcell(0, 8, "จุดหยุด ÷ สเปรด", dim)
    pcell(1, 8, na(ptcSprdX) ? "-" : str.tostring(ptcSprdX, "#") + " เท่า",
         ptcTight ? bd : ink)
    pcell(2, 8, ptcTight
         ? "⛔ จุดหยุดอยู่ในสัญญาณรบกวน — จ่ายค่าธรรมเนียมเพื่อแตะขอบ ไม่ใช่เทรดการเคลื่อนไหว"
         : "ต้องเกิน " + str.tostring(minSprd) + " เท่า", ptcTight ? bd : dim)
    pcell(3, 8, ptcTight ? "ห้าม" : "ผ่าน", ptcTight ? bd : ok)

    pcell(0, 9, "จุดหยุด ÷ ATR", dim)
    pcell(1, 9, na(ptcAtrX) ? "-" : str.tostring(ptcAtrX, "#.00") + " เท่า",
         ptcNarrow or ptcBroad ? wn : ink)
    pcell(2, 9, ptcNarrow ? "แคบกว่าความผันผวนปกติ — โอกาสโดนสะกิดสูง"
         : ptcBroad ? "กว้างกว่าความผันผวนปกติมาก — ขนาดไม้จะเล็กจนต้นทุนกินสัดส่วนใหญ่"
         : "อยู่ในช่วงปกติ", ptcNarrow or ptcBroad ? wn : dim)
    pcell(3, 9, ptcNarrow or ptcBroad ? "เตือน" : "ผ่าน",
         ptcNarrow or ptcBroad ? wn : ok)

    // ── ⑤ ระดับที่คนอื่นก็เห็น ───────────────────────────────────────
    pcell(0, 10, "จุดหยุดชิดขอบกรอบ Asia", dim)
    pcell(1, 10, not haveDay ? "-" : ptcSLnear ? "ใช่" : "ไม่", ptcSLnear ? wn : ink)
    pcell(2, 10, not haveDay ? "ยังไม่มีกรอบของวันนี้"
         : ptcSLnear ? "ซุกใต้ขอบที่คนอื่นก็เห็น — เป็นจุดที่ถูกกวาดบ่อย"
         : "กรอบ " + str.tostring(lockLo, format.mintick) + " – " +
           str.tostring(lockHi, format.mintick), ptcSLnear ? wn : dim)
    pcell(3, 10, not haveDay ? "-" : ptcSLnear ? "เตือน" : "ผ่าน", ptcSLnear ? wn : ok)

    pcell(0, 11, "ระดับขวางก่อนถึงเป้า", dim)
    pcell(1, 11, not haveDay ? "-" : ptcTPblock ? "มี" : "ไม่มี", ptcTPblock ? wn : ink)
    pcell(2, 11, ptcTPblock
         ? "ขอบกรอบ Asia อยู่ระหว่างจุดเข้ากับเป้า — ราคาอาจหยุดตรงนั้นก่อน"
         : "ไม่มีขอบกรอบขวาง", ptcTPblock ? wn : dim)
    pcell(3, 11, not haveDay ? "-" : ptcTPblock ? "เตือน" : "ผ่าน", ptcTPblock ? wn : ok)

    // ── ⑥ สวนเทรนด์ — ข้อที่แพงที่สุดในบันทึกทั้งหมด ──────────────────
    pcell(0, 12, "เทียบเทรนด์ " + ptcTrTF, dim)
    pcell(1, 12, not ptcTrend ? "ปิดอยู่" : na(ptcTrEma) ? "-"
         : close > ptcTrEma ? "ราคาเหนือ EMA" : "ราคาใต้ EMA", ptcFade ? wn : ink)
    pcell(2, 12, ptcFade ? "⚑ กำลังสวนเทรนด์ — ความผิดพลาดที่แพงที่สุดในบันทึกของเรา"
         : "ไปทางเดียวกับเทรนด์", ptcFade ? wn : dim)
    pcell(3, 12, not ptcTrend ? "-" : ptcFade ? "เตือน" : "ผ่าน", ptcFade ? wn : ok)

    pcell(0, 13, ptcFade ? "เคยฟาดที่ CONF 90-95 แล้วทองวิ่ง +129$ สวนหน้า" : "",
         ptcFade ? wn : dim)
    pcell(1, 13, "", dim)
    pcell(2, 13, "", dim)
    pcell(3, 13, "", dim)

    // ── ⑦ เพดานประจำวัน — กันการล้างแค้น ────────────────────────────
    pcell(0, 14, "งบขาดทุนวันนี้", dim)
    pcell(1, 14, str.tostring(math.max(0, ptcDayCap - ptcLossToday), "#.00") + "$ เหลือ",
         ptcOverDay ? bd : ink)
    pcell(2, 14, ptcOverDay ? "⛔ ไม้นี้จะทะลุเพดานรายวัน"
         : "เพดาน " + str.tostring(ptcDayCap, "#.00") + "$ · เสียแล้ว " +
           str.tostring(ptcLossToday, "#.00") + "$", ptcOverDay ? bd : dim)
    pcell(3, 14, ptcOverDay ? "ห้าม" : "ผ่าน", ptcOverDay ? bd : ok)

    pcell(0, 15, "จำนวนไม้วันนี้", dim)
    pcell(1, 15, str.tostring(ptcTradesDone) + " / " + str.tostring(ptcMaxTrades),
         ptcOverCnt ? bd : ink)
    pcell(2, 15, ptcOverCnt ? "⛔ ครบเพดานแล้ว — พรุ่งนี้ค่อยว่ากัน"
         : "ยังเทรดได้อีก " + str.tostring(ptcMaxTrades - ptcTradesDone) + " ไม้",
         ptcOverCnt ? bd : dim)
    pcell(3, 15, ptcOverCnt ? "ห้าม" : "ผ่าน", ptcOverCnt ? bd : ok)

    // ── คำตัดสิน — มีสองแบบเท่านั้น และจะไม่มีวันมีแบบที่สาม ──────────
    pcell(0, 16, ptcBlocked ? "⛔ หยุด — เจอข้อห้าม" : "ตรวจแล้วไม่พบข้อห้าม",
         ptcBlocked ? bd : ok)
    pcell(1, 16, "", dim)
    pcell(2, 16, "", dim)
    pcell(3, 16, "", dim)

    // แถวนี้เขียนตายตัว และเป็นเหตุผลทั้งหมดที่โมดูลนี้มีหน้าตาแบบนี้
    pcell(0, 17, "ไม่มีช่องใดในตารางนี้บอกว่าเทรดนี้จะกำไร", tblAcc)
    pcell(1, 17, "", dim)
    pcell(2, 17, "", dim)
    pcell(3, 17, "", dim)

    pcell(0, 18, "มันตรวจได้แค่ว่าออเดอร์ประกอบถูกและรอดได้ · การตัดสินใจยังเป็นของคุณ", dim)
    pcell(1, 18, "", dim)
    pcell(2, 18, "", dim)
    pcell(3, 18, "", dim)