millionchart
English

na، خطاها و محاسبات عددی

na چیست و چطور آزمایش می‌شود، تقسیم بر صفر چه می‌دهد، فرق % و rem، گرد کردن چطور کار می‌کند، و چرا 0.1 به‌علاوهٔ 0.2 برابر 0.3 نیست.

حساب در اسکریپت از چند قاعدهٔ ساده پیروی می‌کند. دانستن آن‌ها شما را از دو غافلگیری رایج نجات می‌دهد: مقایسه‌ای با na که هرگز درست نمی‌شود، و باقی‌مانده‌ای با علامت اشتباه.

na#

na یعنی «اینجا مقداری نیست». از یک میانگین پیش از آنکه کندل کافی داشته باشد این را می‌گیرید، همین‌طور از close[5] در پنج کندل اول، از 0 / 0، و از جذر یک عدد منفی. خطا نیست و اسکریپت به اجرا ادامه می‌دهد.

عبارتنتیجه
na + 1na
0 / 0na
math.sqrt(-1)na
na > 1 و na < 1false
na == nafalse

حساب با na نتیجهٔ na می‌دهد؛ مقایسه با na همیشه و در هر دو جهت نادرست است. برای همین x == na هرگز نمی‌تواند مقدار گم‌شده را پیدا کند:

Cuprel · هشدار W-NA-COMPARE
indicator "Comparing with na"
    edition = 1

let average = tech.sma(close, 20)
plot(if average == na then 0 else average)

با is_na(x) آزمایش کنید. مقدار گم‌شده را با ?? جایگزین کنید که وقتی سمت چپ na است سمت راست را برمی‌دارد: x ?? 0 هر جا x گم شده باشد صفر می‌دهد.

Cuprel
indicator "Missing values"
    edition = 1

let average = tech.sma(close, 20)
plot(if is_na(average) then 0 else average, name = "Tested with is_na")
plot(average ?? close, name = "Coalesced")

a ?? b ?? c اولین مقداری را برمی‌دارد که na نباشد. این عملگر از مقایسه محکم‌تر و از + شل‌تر می‌چسبد، پس x ?? 0 > 5 یعنی (x ?? 0) > 5.

خط در جایی که مقدارش na است شکاف می‌اندازد. and و or با na مثل نادرست رفتار می‌کنند: true and na برابر false است و na or true برابر true.

تقسیم#

/ همیشه اعشاری تقسیم می‌کند: 7 / 2 برابر 3.5 است. تقسیم عددی غیرصفر بر صفر بی‌نهایت می‌دهد، نه na: 1 / 0 > 5 درست است و is_na(1 / 0) نادرست. فقط 0 / 0 برابر na است. اگر مقسوم‌علیه می‌تواند صفر شود، اول آن را آزمایش کنید:

Cuprel
indicator "Safe ratio"
    edition = 1

let range = high - low
let body_share = if range == 0 then na else math.abs(close - open) / range
plot(body_share, name = "Body as a share of the range")

برای اینکه از تقسیم یک عدد صحیح بگیرید، نتیجه را گرد کنید: math.floor(7 / 2) برابر 3 است، math.ceil(7 / 2) برابر 4، و int(7.9) اعشار را می‌برد و 7 می‌دهد (int(-7.9) برابر -7 است).

باقی‌مانده: % و rem#

% باقی‌ماندهٔ تقسیم است و علامت مقسوم‌علیه را می‌گیرد، پس وقتی مقسوم‌علیه مثبت است هرگز منفی نمی‌شود. برای همین برای جا دادن یک عدد در یک بازه درست است. rem(a, b) باقی‌ماندهٔ دیگر است که علامت مقسوم را می‌گیرد.

Cuprel
indicator "Two remainders"
    edition = 1

let wrapped = (bar.index - 3) % 7
let cut = rem(bar.index - 3, 7)
plot(wrapped, name = "%: always 0 to 6")
plot(cut, name = "rem: negative on the first bars")
عبارتنتیجه
-7 % 32
7 % -3-2
rem(-7, 3)-1
rem(7, -3)1
5.5 % 21.5

گرد کردن#

math.round(x) به نزدیک‌ترین عدد صحیح گرد می‌کند و نیم را از صفر دورتر می‌برد: 2.5 می‌شود 3 و -2.5 می‌شود -3. math.round(x, n) n رقم اعشار نگه می‌دارد. math.floor به پایین می‌رود و math.ceil به بالا، با هر علامتی: math.floor(-1.5) برابر -2 است.

Cuprel
indicator "Rounding"
    edition = 1

let step = 0.25
plot(math.round(close / step) * step, name = "Close on a quarter step")
plot(math.floor(close), name = "Floor")
plot(math.ceil(close), name = "Ceil")

اعشارها دقیق نیستند#

عددها کسرهای دودویی‌اند، پس بعضی اعشارها را نمی‌شود دقیق ذخیره کرد: 0.1 + 0.2 == 0.3 برابر false است. قیمت‌ها را با یک تلرانس کوچک مقایسه کنید، یا اول هر دو طرف را گرد کنید:

Cuprel
indicator "Comparing decimals"
    edition = 1

let a = 0.1 + 0.2
let same = math.abs(a - 0.3) < 1e-9
plot(if same then 1 else 0, name = "Equal within a tolerance")

عددهای تصادفی#

math.random(min, max) عددی از min تا max، بدون خود max، می‌دهد (اگر ننویسید، 0 و 1). این عددها واقعاً تصادفی نیستند: هر اجرای همان اسکریپت همان عددها را می‌دهد، روی هر دستگاه، پس دو اجرای یک اسکریپت نتیجهٔ یکسان می‌دهند و می‌شود نتیجه را تکرار کرد.

  • عددها از یک seed اجرا می‌آیند که موتور از هویت اسکریپت (شناسهٔ اسکریپتِ ذخیره‌شده؛ برای اسکریپتی که ذخیره نشده، نامی که در سرآیندش آمده)، مقدار ورودی‌ها، یک seed اختیاری که خودتان می‌گذارید و هویت داده‌ها می‌سازد. هر جای اسکریپت که math.random را صدا می‌زند عددهای خودش را دارد، و یک کندل هر مقدار تاریخچه که بارگذاری شده باشد همان عددها را می‌گیرد.
  • بازنویسی یا قالب‌بندی دوبارهٔ اسکریپت، یا افزودن یک کامنت، عددهایش را نگه می‌دارد؛ عوض کردن یک ورودی، یا نامِ اسکریپتِ ذخیره‌نشده، عددهای تازه می‌دهد. برای امتحان یک سری عدد تازه همین راه را بروید.
  • فراخوانی‌ای که seed خودش را دارد، math.random(0, 1, 42)، عددهایی می‌دهد که فقط به همان seed بستگی دارند؛ پس با ویرایش اسکریپت عوض نمی‌شوند.
  • وقتی فراخوانی seed خودش را ندارد، موتور seed اجرا را در یک یادداشت به نام W-ENGINE-RANDOM-SEED نشان می‌دهد تا بشود به نتیجه اشاره کرد و آن را تکرار کرد.
Cuprel
indicator "Fixed noise"
    edition = 1

plot(math.random(-1, 1, seed = 42), name = "Same numbers on every run")

بدون seed اسکریپت اجرا می‌شود و یادداشت اضافه می‌شود:

Cuprel · هشدار W-ENGINE-RANDOM-SEED
indicator "Noise"
    edition = 1

plot(math.random(-1, 1), name = "Noise")

عددهای یکسان روی هر دستگاه#

اسکریپت روی هر دستگاهی که پلتفرم پشتیبانی می‌کند همان عددها را می‌دهد. جمع، تفریق، ضرب، تقسیم و جذر همه‌جا دقیق‌اند. تابع‌های دیگر ریاضی (math.log، math.log10، math.exp، math.pow، math.sin، math.cos، math.tan، math.asin، math.acos، math.atan) همان تابع‌هایی‌اند که نتیجه‌شان قبلاً در مرورگرها و سرورها در رقم آخر فرق می‌کرد؛ پس پلتفرم خودش آن‌ها را با یک روش ثابت حساب می‌کند و وقتی اسکریپتی لازمشان داشته باشد، آن روش را به‌عنوان یک بخش از موتور بارگذاری می‌کند. اندازهٔ یک تیک نماد (symbol.tick) هم روی هر دستگاهی یک عدد اعشاری ثابت است، مثل 0.00001.

آنچه پلتفرم دربارهٔ این تابع‌ها می‌گوید: یک پیاده‌سازی ثابت روی هر دستگاه پشتیبانی‌شده همان نتیجه را می‌دهد؛ بیشترین خطایی که روی مجموعهٔ آزمون فعلی اندازه‌گیری شده، حداکثر 0.85 ulp است؛ و گرد کردن درست وعده داده نمی‌شود. ulp یک واحد از آخرین رقمی است که یک عدد می‌تواند نگه دارد. پس جواب ممکن است کمی کمتر از یک واحد با جواب دقیق ریاضی فاصله داشته باشد، و این فاصله همه‌جا دقیقاً یکی است؛ این عدد (0.85) همان است که روی مجموعهٔ آزمون پلتفرم اندازه‌گیری شده، نه وعده‌ای دربارهٔ هر عددی که به تابع داده شود.

یک نکتهٔ احتیاطی. دستهٔ قیمتی (bucket) که با تقسیم ساده ساخته شود، مثل math.floor(price / symbol.tick)، باز هم ممکن است قیمتی را که دقیقاً روی لبهٔ دسته است در دستهٔ کناری بگذارد، چون قیمتی مثل 1.1 و تیکی مثل 0.00001 را نمی‌شود در مبنای دو (دودویی) دقیق نگه داشت. جواب روی هر دستگاه یکی است، ولی همیشه همان جواب اعشاری نیست که با دست می‌نویسید. کمی فاصله از لبه بگذارید، یا اول قیمت را گرد کنید.

اشتباه‌هایی که موتور متوقف می‌کند#

بعضی حساب‌ها اشتباهِ برنامه‌اند، نه اشتباهِ داده. حلقه‌ای با گام صفر (E-LOOP-STEP)، حجم na در یک سفارش (E-QTY-NA) و حلقه‌ای که بودجه‌اش تمام می‌شود (E-LIMIT-OPS) اسکریپت را با پیامی که خط را نام می‌برد متوقف می‌کنند. نامی که تعریف نکرده‌اید (E3171) پیش از اجرای اسکریپت رد می‌شود. هر چیز دربارهٔ مقدارهای گم‌شده na است، نه خطا.

نگاه کنید به: مقدارها و نوع‌ها، انتخاب‌ها و حلقه‌ها، اشتباه‌های رایج.