ریئل ٹائم سروس کی مثال استعمال کرتے ہوئے Q اور KDB+ زبان کی خصوصیات

آپ پڑھ سکتے ہیں کہ KDB+ کی بنیاد، Q پروگرامنگ لینگویج کیا ہیں، ان کی طاقتیں اور کمزوریاں کیا ہیں آرٹیکل اور مختصراً تعارف میں۔ آرٹیکل میں، ہم Q پر ایک سروس نافذ کریں گے جو آنے والے ڈیٹا سٹریم پر کارروائی کرے گی اور "ریئل ٹائم" موڈ میں ہر منٹ میں مختلف ایگریگیشن فنکشنز کا حساب لگائے گی (یعنی، اس کے پاس ڈیٹا کے اگلے حصے سے پہلے ہر چیز کا حساب لگانے کا وقت ہوگا)۔ کیو کی اہم خصوصیت یہ ہے کہ یہ ایک ویکٹر لینگویج ہے جو آپ کو کسی ایک آبجیکٹ کے ساتھ نہیں بلکہ ان کی صفوں، صفوں کی صفوں اور دیگر پیچیدہ اشیاء کے ساتھ کام کرنے کی اجازت دیتی ہے۔ زبانیں جیسے کیو اور اس کے رشتہ دار کے، جے، اے پی ایل اپنے اختصار کے لیے مشہور ہیں۔ اکثر، ایک پروگرام جو جاوا جیسی مانوس زبان میں کوڈ کی کئی اسکرینیں لیتا ہے، ان پر چند سطروں میں لکھا جا سکتا ہے۔ یہ وہی ہے جو میں اس مضمون میں ظاہر کرنا چاہتا ہوں۔

ریئل ٹائم سروس کی مثال استعمال کرتے ہوئے Q اور KDB+ زبان کی خصوصیات

تعارف

KDB+ ایک کالمی ڈیٹا بیس ہے جو ڈیٹا کی بہت بڑی مقدار پر مرکوز ہے، جسے ایک مخصوص طریقے سے ترتیب دیا گیا ہے (بنیادی طور پر وقت کے مطابق)۔ یہ بنیادی طور پر مالیاتی اداروں - بینکوں، سرمایہ کاری کے فنڈز، انشورنس کمپنیوں میں استعمال ہوتا ہے۔ Q زبان KDB+ کی اندرونی زبان ہے جو آپ کو اس ڈیٹا کے ساتھ مؤثر طریقے سے کام کرنے کی اجازت دیتی ہے۔ Q نظریہ اختصار اور کارکردگی ہے، جب کہ وضاحت کی قربانی دی جاتی ہے۔ اس کا جواز یہ ہے کہ ویکٹر کی زبان کو کسی بھی صورت میں سمجھنا مشکل ہو گا، اور ریکارڈنگ کی اختصار اور بھرپوری آپ کو پروگرام کا بہت بڑا حصہ ایک سکرین پر دیکھنے کی اجازت دیتی ہے، جو بالآخر اسے سمجھنا آسان بنا دیتا ہے۔

اس مضمون میں ہم Q میں ایک مکمل پروگرام نافذ کرتے ہیں اور آپ اسے آزمانا چاہیں گے۔ ایسا کرنے کے لیے، آپ کو اصل Q کی ضرورت ہوگی۔ آپ kx کمپنی کی ویب سائٹ پر مفت 32 بٹ ورژن ڈاؤن لوڈ کر سکتے ہیں۔ www.kx.com. وہاں، اگر آپ دلچسپی رکھتے ہیں، تو آپ کو کتاب، Q کے حوالے سے معلومات ملیں گی۔ Q For Mortals اور اس موضوع پر مختلف مضامین۔

مسئلہ کی تشکیل

ایک ذریعہ ہے جو ہر 25 ملی سیکنڈ میں ڈیٹا کے ساتھ ایک ٹیبل بھیجتا ہے۔ چونکہ KDB+ بنیادی طور پر فنانس میں استعمال ہوتا ہے، اس لیے ہم فرض کریں گے کہ یہ ٹرانزیکشنز (تجارت) کا ایک جدول ہے، جس میں درج ذیل کالم ہیں: وقت (ملی سیکنڈ میں وقت)، سم (اسٹاک ایکسچینج میں کمپنی کا عہدہ - IBM, AAPL،…)، قیمت (وہ قیمت جس پر حصص خریدے گئے تھے)، سائز (لین دین کا سائز)۔ 25 ملی سیکنڈ کا وقفہ صوابدیدی ہے، نہ بہت چھوٹا اور نہ زیادہ لمبا۔ اس کی موجودگی کا مطلب ہے کہ ڈیٹا پہلے سے بفر شدہ سروس میں آتا ہے۔ موجودہ بوجھ کے لحاظ سے متحرک بفرنگ سمیت سروس سائیڈ پر بفرنگ کو نافذ کرنا آسان ہوگا، لیکن سادگی کے لیے، ہم ایک مقررہ وقفہ پر توجہ مرکوز کریں گے۔

سروس کو sym کالم سے ہر آنے والی علامت کے لیے ہر منٹ کو مجموعی افعال کا ایک سیٹ شمار کرنا چاہیے - زیادہ سے زیادہ قیمت، اوسط قیمت، رقم کا سائز، وغیرہ۔ مفید معلومات. سادگی کے لیے، ہم فرض کریں گے کہ تمام فنکشنز کو بتدریج حساب کیا جا سکتا ہے، یعنی نئی قدر حاصل کرنے کے لیے، دو نمبروں کو جاننا کافی ہے - پرانی اور آنے والی اقدار۔ مثال کے طور پر، فنکشنز max، اوسط، sum میں یہ خاصیت ہوتی ہے، لیکن میڈین فنکشن ایسا نہیں کرتا۔

ہم یہ بھی فرض کریں گے کہ آنے والی ڈیٹا سٹریم کا وقت مقرر ہے۔ اس سے ہمیں صرف آخری لمحات کے ساتھ کام کرنے کا موقع ملے گا۔ عملی طور پر، کچھ اپ ڈیٹس دیر سے ہونے کی صورت میں موجودہ اور پچھلے منٹ کے ساتھ کام کرنے کے قابل ہونا کافی ہے۔ سادگی کے لیے، ہم اس کیس پر غور نہیں کریں گے۔

جمع کرنے کے افعال

مطلوبہ جمع افعال ذیل میں درج ہیں۔ میں نے ان میں سے زیادہ سے زیادہ کو سروس پر بوجھ بڑھانے کے لیے لیا:

  • زیادہ - زیادہ سے زیادہ قیمت - فی منٹ زیادہ سے زیادہ قیمت۔
  • کم - کم قیمت - فی منٹ کم از کم قیمت۔
  • پہلی قیمت - پہلی قیمت - پہلی قیمت فی منٹ۔
  • آخری قیمت - آخری قیمت - آخری قیمت فی منٹ۔
  • فرسٹ سائز - پہلا سائز - پہلا تجارتی سائز فی منٹ۔
  • lastSize - آخری سائز - ایک منٹ میں آخری تجارتی سائز۔
  • numTrades - شمار i - فی منٹ تجارت کی تعداد۔
  • حجم - رقم کا سائز - فی منٹ تجارتی سائز کا مجموعہ۔
  • pvolume - sum price - قیمتوں کا مجموعہ فی منٹ، اوسط قیمت کے لیے درکار ہے۔
  • - ٹرن اوور کی قیمت* سائز - لین دین کا کل حجم فی منٹ۔
  • avgPrice – pvolume%numTrades – اوسط قیمت فی منٹ۔
  • avgSize – حجم%numTrades – اوسط تجارت کا سائز فی منٹ۔
  • vwap - ٹرن اوور% حجم - لین دین کے سائز کے حساب سے اوسط قیمت فی منٹ۔
  • کم حجم – رقم کا حجم – پورے وقت میں لین دین کا جمع کردہ سائز۔

آئیے فوری طور پر ایک غیر واضح نکتہ پر بات کرتے ہیں - ان کالموں کو پہلی بار اور اس کے بعد کے ہر منٹ کے لیے کیسے شروع کیا جائے۔ فرسٹ پرائس قسم کے کچھ کالموں کو ہر بار کالعدم کرنے کے لیے شروع کیا جانا چاہیے؛ ان کی قدر غیر متعین ہے۔ دیگر والیوم کی اقسام کو ہمیشہ 0 پر سیٹ کیا جانا چاہیے۔ ایسے کالم بھی ہوتے ہیں جن کے لیے مشترکہ نقطہ نظر کی ضرورت ہوتی ہے - مثال کے طور پر، cumVolume کو پچھلے منٹ سے کاپی کرنا ضروری ہے، اور پہلی ایک کے لیے 0 پر سیٹ کرنا چاہیے۔ آئیے ڈکشنری ڈیٹا کا استعمال کرتے ہوئے ان تمام پیرامیٹرز کو سیٹ کرتے ہیں۔ قسم (ریکارڈ کے مطابق):

// list ! list – создать словарь, 0n – float null, 0N – long null, `sym – тип символ, `sym1`sym2 – список символов
initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time; // список всех вычисляемых колонок, reverse объяснен ниже

میں نے سہولت کے لیے ڈکشنری میں sym اور time کا اضافہ کیا، اب initWith فائنل ایگریگیٹڈ ٹیبل سے ایک ریڈی میڈ لائن ہے، جہاں صحیح سم اور ٹائم سیٹ کرنا باقی ہے۔ آپ اسے ٹیبل میں نئی ​​قطاریں شامل کرنے کے لیے استعمال کر سکتے ہیں۔

ایگریگیشن فنکشن بناتے وقت ہمیں aggCols کی ضرورت ہوگی۔ فہرست کو اس ترتیب کی وجہ سے الٹا ہونا چاہیے جس میں Q میں اظہارات کا اندازہ کیا جاتا ہے (دائیں سے بائیں)۔ مقصد یہ یقینی بنانا ہے کہ کیلکولیشن ہائی سے کم والیوم تک جائے، کیونکہ کچھ کالم پچھلے کالم پر منحصر ہوتے ہیں۔

وہ کالم جنہیں پچھلے ایک سے ایک نئے منٹ میں کاپی کرنے کی ضرورت ہے، سہولت کے لیے سم کالم شامل کیا جاتا ہے:

rollColumns:`sym`cumVolume;

آئیے اب کالموں کو گروپس میں تقسیم کرتے ہیں کہ انہیں کس طرح اپ ڈیٹ کیا جانا چاہیے۔ تین اقسام کی تمیز کی جا سکتی ہے:

  1. جمع کرنے والے (حجم، ٹرن اوور،..) - ہمیں آنے والی قدر کو پچھلی قیمت میں شامل کرنا چاہیے۔
  2. ایک خاص نقطہ کے ساتھ (اعلی، کم، ..) - منٹ میں پہلی قیمت آنے والے ڈیٹا سے لی جاتی ہے، باقی کا حساب فنکشن کے ذریعے کیا جاتا ہے۔
  3. آرام کریں۔ ہمیشہ فنکشن کا استعمال کرتے ہوئے حساب کیا جاتا ہے۔

آئیے ان کلاسز کے لیے متغیرات کی وضاحت کریں:

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

حساب کتاب کا حکم

ہم مجموعی جدول کو دو مراحل میں اپ ڈیٹ کریں گے۔ کارکردگی کے لیے، ہم سب سے پہلے آنے والی میز کو سکڑتے ہیں تاکہ ہر کریکٹر اور منٹ کے لیے صرف ایک قطار ہو۔ حقیقت یہ ہے کہ ہمارے تمام افعال بڑھتے ہوئے ہیں اور اس بات کی ضمانت دیتے ہیں کہ اس اضافی قدم کا نتیجہ تبدیل نہیں ہوگا۔ آپ سلیکٹ کا استعمال کرتے ہوئے ٹیبل کو سکڑ سکتے ہیں:

select high:max price, low:min price … by sym,time.minute from table

اس طریقہ کار کا ایک نقصان ہے - حسابی کالموں کا سیٹ پہلے سے طے شدہ ہے۔ خوش قسمتی سے، Q میں، سلیکٹ کو ایک فنکشن کے طور پر بھی لاگو کیا جاتا ہے جہاں آپ متحرک طور پر تخلیق کردہ دلائل کو تبدیل کر سکتے ہیں:

?[table;whereClause;byClause;selectClause]

میں دلائل کے فارمیٹ کی تفصیل سے وضاحت نہیں کروں گا؛ ہمارے معاملے میں، صرف اور منتخب اظہارات غیر معمولی ہوں گے اور وہ فارم کے کالم!اظہار کی لغات ہونے چاہئیں۔ اس طرح، سکڑنے والے فنکشن کی وضاحت اس طرح کی جا سکتی ہے:

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size"); // each это функция map в Q для одного списка
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

وضاحت کے لیے، میں نے پارس فنکشن کا استعمال کیا، جو Q اظہار کے ساتھ سٹرنگ کو ایک قدر میں بدل دیتا ہے جسے eval فنکشن میں منتقل کیا جا سکتا ہے اور جو فنکشن سلیکٹ میں درکار ہے۔ یہ بھی نوٹ کریں کہ پری پروسیس کو منتخب فنکشن کے پروجیکشن (یعنی جزوی طور پر بیان کردہ دلائل کے ساتھ ایک فنکشن) کے طور پر بیان کیا گیا ہے، ایک دلیل (ٹیبل) غائب ہے۔ اگر ہم ٹیبل پر پری پروسیس لاگو کرتے ہیں، تو ہمیں ایک کمپریسڈ ٹیبل ملے گا۔

دوسرا مرحلہ مجموعی جدول کو اپ ڈیٹ کر رہا ہے۔ آئیے پہلے الگورتھم کو pseudocode میں لکھتے ہیں:

for each sym in inputTable
  idx: row index in agg table for sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

Q میں، لوپس کے بجائے نقشہ/کم کرنے کے فنکشنز کا استعمال کرنا عام ہے۔ لیکن چونکہ کیو ایک ویکٹر لینگویج ہے اور ہم تمام سمبلز پر ایک ہی وقت میں تمام کارروائیوں کو آسانی سے لاگو کر سکتے ہیں، اس کے بعد ہم ایک ہی وقت میں تمام علامتوں پر آپریشن کرتے ہوئے، ہم کسی لوپ کے بغیر بالکل بھی کر سکتے ہیں:

idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…

لیکن ہم آگے جا سکتے ہیں، Q کے پاس ایک منفرد اور انتہائی طاقتور آپریٹر ہے - جنرلائزڈ اسائنمنٹ آپریٹر۔ یہ آپ کو اشاریہ جات، افعال اور دلائل کی فہرست کا استعمال کرتے ہوئے ایک پیچیدہ ڈیٹا ڈھانچے میں اقدار کے سیٹ کو تبدیل کرنے کی اجازت دیتا ہے۔ ہمارے معاملے میں یہ اس طرح لگتا ہے:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], в нашем случае функция – это присваивание
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

بدقسمتی سے، کسی ٹیبل کو تفویض کرنے کے لیے آپ کو قطاروں کی فہرست کی ضرورت ہوتی ہے، کالموں کی نہیں، اور آپ کو فلپ فنکشن کا استعمال کرتے ہوئے میٹرکس (کالموں کی فہرست سے قطاروں کی فہرست) کو منتقل کرنا ہوتا ہے۔ یہ ایک بڑی میز کے لیے مہنگا ہے، اس لیے اس کے بجائے ہم نقشہ فنکشن کا استعمال کرتے ہوئے، ہر کالم پر الگ الگ ایک عمومی تفویض کا اطلاق کرتے ہیں (جو کہ apostrophe کی طرح لگتا ہے):

.[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

ہم دوبارہ فنکشن پروجیکشن کا استعمال کرتے ہیں۔ یہ بھی نوٹ کریں کہ Q میں، فہرست بنانا بھی ایک فنکشن ہے اور ہم اسے ہر (map) فنکشن کا استعمال کرکے فہرستوں کی فہرست حاصل کرنے کے لیے کال کرسکتے ہیں۔

اس بات کو یقینی بنانے کے لیے کہ حسابی کالموں کا سیٹ درست نہیں ہے، ہم اوپر کا اظہار متحرک طور پر بنائیں گے۔ آئیے پہلے ہر کالم کا حساب لگانے کے لیے فنکشنز کی وضاحت کرتے ہیں، قطار اور inp متغیرات کا استعمال کرتے ہوئے مجموعی اور ان پٹ ڈیٹا کا حوالہ دیتے ہیں:

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!
 ("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");

کچھ کالم خاص ہوتے ہیں؛ ان کی پہلی قدر کا حساب فنکشن سے نہیں ہونا چاہیے۔ ہم یہ تعین کر سکتے ہیں کہ یہ قطار [`numTrades] کالم کے لحاظ سے پہلا ہے - اگر اس میں 0 ہے، تو قدر پہلے ہے۔ Q میں ایک سلیکٹ فنکشن ہے - ?

// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];

یہاں میں نے اپنے فنکشن کے ساتھ ایک عمومی اسائنمنٹ کو بلایا (گھبگھرالی منحنی خطوط وحدانی میں اظہار)۔ اسے موجودہ قدر (پہلی دلیل) اور ایک اضافی دلیل ملتی ہے، جسے میں 4th پیرامیٹر میں پاس کرتا ہوں۔

آئیے بیٹری اسپیکر کو الگ سے شامل کریں، کیونکہ فنکشن ان کے لیے ایک جیسا ہے:

// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;

یہ Q معیارات کے مطابق ایک عام تفویض ہے، لیکن میں ایک ہی وقت میں اقدار کی فہرست تفویض کر رہا ہوں۔ آخر میں، آئیے مین فنکشن بنائیں:

// ":",/:aggExprs ~ map[{":",x};aggExpr] => ":row[`high]|inp`high" присвоим вычисленное значение переменной, потому что некоторые колонки зависят от уже вычисленных значений
// string[cols],'exprs ~ map[,;string[cols];exprs] => "high:row[`high]|inp`high" завершим создание присваивания. ,’ расшифровывается как map[concat]
// ";" sv exprs – String from Vector (sv), соединяет список строк вставляя “;” посредине
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}";

اس اظہار کے ساتھ، میں متحرک طور پر ایک سٹرنگ سے ایک فنکشن بناتا ہوں جس میں وہ اظہار ہوتا ہے جو میں نے اوپر دیا تھا۔ نتیجہ اس طرح نظر آئے گا:

{[aggTable;idx;inp] rows:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols ;(cumVolume:row[`cumVolume]+inp`cumVolume;… ; high:?[isFirst;inp`high;row[`high]|inp`high])]}

کالم کی تشخیص کی ترتیب الٹی ہے کیونکہ Q میں تشخیصی ترتیب دائیں سے بائیں ہوتی ہے۔

اب ہمارے پاس حساب کے لیے دو اہم افعال ضروری ہیں، ہمیں صرف تھوڑا سا انفراسٹرکچر شامل کرنا ہے اور سروس تیار ہے۔

آخری مراحل

ہمارے پاس پری پروسیس اور اپڈیٹ ایگ فنکشنز ہیں جو تمام کام کرتے ہیں۔ لیکن منٹوں کے ذریعے درست منتقلی کو یقینی بنانا اور جمع کرنے کے لیے اشاریہ جات کا حساب لگانا اب بھی ضروری ہے۔ سب سے پہلے، آئیے init فنکشن کی وضاحت کریں:

init:{
  tradeAgg:: 0#enlist[initWith]; // создаем пустую типизированную таблицу, enlist превращает словарь в таблицу, а 0# означает взять 0 элементов из нее
  currTime::00:00; // начнем с 0, :: означает, что присваивание в глобальную переменную
  currSyms::`u#`symbol$(); // `u# - превращает список в дерево, для ускорения поиска элементов
  offset::0; // индекс в tradeAgg, где начинается текущая минута 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // кэш для последних значений roll колонок, таблица с ключом sym
 }

ہم رول فنکشن کی بھی وضاحت کریں گے، جو موجودہ منٹ کو بدل دے گا:

roll:{[tm]
  if[currTime>tm; :init[]]; // если перевалили за полночь, то просто вызовем init
  rollCache,::offset _ rollColumns#tradeAgg; // обновим кэш – взять roll колонки из aggTable, обрезать, вставить в rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

ہمیں نئے حروف کو شامل کرنے کے لیے ایک فنکشن کی ضرورت ہوگی:

addSyms:{[syms]
  currSyms,::syms; // добавим в список известных
  // добавим в таблицу sym, time и rollColumns воспользовавшись обобщенным присваиванием.
  // Функция ^ подставляет значения по умолчанию для roll колонок, если символа нет в кэше. value flip table возвращает список колонок в таблице.
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

اور آخر میں، upd فنکشن (Q سروسز کے لیے اس فنکشن کا روایتی نام)، جسے کلائنٹ ڈیٹا شامل کرنے کے لیے کہتے ہیں:

upd:{[tblName;data] // tblName нам не нужно, но обычно сервис обрабатывает несколько таблиц 
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
  updMinute[data] each tm; // добавим данные для каждой минуты
};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm]; // поменяем минуту, если необходимо
  data:select from data where time=tm; // фильтрация
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // новые символы
  updateAgg[`tradeAgg;offset+currSyms?syms;data]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

بس۔ یہاں ہماری خدمت کا مکمل کوڈ ہے، جیسا کہ وعدہ کیا گیا ہے، صرف چند سطریں:

initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time;
rollColumns:`sym`cumVolume;

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
@[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols];
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / '

init:{
  tradeAgg::0#enlist[initWith];
  currTime::00:00;
  currSyms::`u#`symbol$();
  offset::0;
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg;
 };
roll:{[tm]
  if[currTime>tm; :init[]];
  rollCache,::offset _ rollColumns#tradeAgg;
  offset::count tradeAgg;
  currSyms::`u#`$();
 };
addSyms:{[syms]
  currSyms,::syms;
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 };

upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm];
  data:select from data where time=tm;
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms];
  updateAgg[`tradeAgg;offset+currSyms?syms;data];
 };

ٹیسٹنگ

آئیے سروس کی کارکردگی کو چیک کرتے ہیں۔ ایسا کرنے کے لیے، آئیے اسے ایک الگ عمل میں چلائیں (کوڈ کو service.q فائل میں ڈالیں) اور init فنکشن کو کال کریں:

q service.q –p 5566

q)init[]

دوسرے کنسول میں، دوسرا Q عمل شروع کریں اور پہلے سے جڑیں:

h:hopen `:host:5566
h:hopen 5566 // если оба на одном хосте

سب سے پہلے، آئیے علامتوں کی ایک فہرست بنائیں - 10000 ٹکڑے اور ایک بے ترتیب میز بنانے کے لیے ایک فنکشن شامل کریں۔ دوسرے کنسول میں:

syms:`IBM`AAPL`GOOG,-9997?`8
rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}

میں نے فہرست میں تین حقیقی علامتیں شامل کیں تاکہ انہیں ٹیبل میں تلاش کرنا آسان ہو۔ rnd فنکشن n قطاروں کے ساتھ ایک بے ترتیب جدول بناتا ہے، جہاں وقت t سے t+25 ملی سیکنڈ تک مختلف ہوتا ہے۔

اب آپ سروس کو ڈیٹا بھیجنے کی کوشش کر سکتے ہیں (پہلے دس گھنٹے شامل کریں):

{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10

آپ سروس میں چیک کر سکتے ہیں کہ ٹیبل کو اپ ڈیٹ کر دیا گیا ہے:

c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL

نتیجہ:

sym|time|high|low|firstPrice|lastPrice|firstSize|lastSize|numTrades|volume|pvolume|turnover|avgPrice|avgSize|vwap|cumVolume
--|--|--|--|--|--------------------------------
AAPL|09:27|9.258904|9.258904|9.258904|9.258904|8|8|1|8|9.258904|74.07123|9.258904|8|9.258904|2888
AAPL|09:28|9.068162|9.068162|9.068162|9.068162|7|7|1|7|9.068162|63.47713|9.068162|7|9.068162|2895
AAPL|09:31|4.680449|0.2011121|1.620827|0.2011121|1|5|4|14|9.569556|36.84342|2.392389|3.5|2.631673|2909
AAPL|09:33|2.812535|2.812535|2.812535|2.812535|6|6|1|6|2.812535|16.87521|2.812535|6|2.812535|2915
AAPL|09:34|5.099025|5.099025|5.099025|5.099025|4|4|1|4|5.099025|20.3961|5.099025|4|5.099025|2919

آئیے اب یہ معلوم کرنے کے لیے لوڈ ٹیسٹنگ کرتے ہیں کہ سروس فی منٹ کتنے ڈیٹا پر کارروائی کر سکتی ہے۔ میں آپ کو یاد دلاتا ہوں کہ ہم نے اپ ڈیٹ کا وقفہ 25 ملی سیکنڈ پر سیٹ کیا ہے۔ اس کے مطابق، صارفین کو ڈیٹا کی درخواست کرنے کے لیے وقت دینے کے لیے سروس کو (اوسط طور پر) کم از کم 20 ملی سیکنڈ فی اپ ڈیٹ میں فٹ ہونا چاہیے۔ دوسرے عمل میں درج ذیل درج کریں:

tm:10:00:00.000
stressTest:{[n] 1 string[tm]," "; times,::h ({st:.z.T; upd[`trade;x]; .z.T-st};rnd[n;tm]); tm+:25}
start:{[n] times::(); do[4800;stressTest[n]]; -1 " "; `min`avg`med`max!(min times;avg times;med times;max times)}

4800 دو منٹ ہے۔ آپ ہر 1000 ملی سیکنڈ میں 25 قطاروں کے لیے پہلے چلانے کی کوشش کر سکتے ہیں:

start 1000

میرے معاملے میں، نتیجہ فی اپ ڈیٹ چند ملی سیکنڈ کے قریب ہے۔ تو میں فوری طور پر قطاروں کی تعداد بڑھا کر 10.000 کر دوں گا:

start 10000

نتیجہ:

min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030

ایک بار پھر، کچھ خاص نہیں، لیکن یہ 24 ملین لائنز فی منٹ، 400 ہزار فی سیکنڈ ہے۔ 25 ملی سیکنڈ سے زیادہ کے لیے، اپ ڈیٹ صرف 5 بار سست ہوا، بظاہر جب منٹ تبدیل ہوا۔ آئیے 100.000 تک بڑھائیں:

start 100000

نتیجہ:

min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532

جیسا کہ آپ دیکھ سکتے ہیں، سروس بمشکل مقابلہ کر سکتی ہے، لیکن اس کے باوجود یہ تیز رہنے کا انتظام کرتی ہے۔ اعداد و شمار کا اتنا حجم (240 ملین قطاریں فی منٹ) بہت زیادہ ہے؛ ایسی صورتوں میں، سروس کے کئی کلون (یا درجنوں کلون) لانچ کرنا عام بات ہے، جن میں سے ہر ایک کرداروں کے صرف ایک حصے پر کارروائی کرتا ہے۔ پھر بھی، نتیجہ ایک تشریح شدہ زبان کے لیے متاثر کن ہے جو بنیادی طور پر ڈیٹا اسٹوریج پر مرکوز ہے۔

سوال یہ پیدا ہوسکتا ہے کہ وقت ہر اپ ڈیٹ کے سائز کے ساتھ غیر خطی کیوں بڑھتا ہے۔ وجہ یہ ہے کہ shrink فنکشن دراصل ایک C فنکشن ہے، جو updateAgg سے کہیں زیادہ موثر ہے۔ ایک مخصوص اپ ڈیٹ سائز (تقریباً 10.000) سے شروع ہو کر، اپڈیٹ اےگ اپنی حد تک پہنچ جاتا ہے اور پھر اس کے عمل درآمد کا وقت اپ ڈیٹ کے سائز پر منحصر نہیں ہوتا ہے۔ یہ ابتدائی مرحلہ Q کی وجہ سے ہے کہ سروس ڈیٹا کی اتنی مقدار کو ہضم کرنے کے قابل ہے۔ یہ نمایاں کرتا ہے کہ بڑے ڈیٹا کے ساتھ کام کرتے وقت صحیح الگورتھم کا انتخاب کرنا کتنا ضروری ہے۔ ایک اور نکتہ میموری میں ڈیٹا کا صحیح ذخیرہ ہے۔ اگر ڈیٹا کالمری طور پر ذخیرہ نہیں کیا گیا تھا یا وقت کے مطابق آرڈر نہیں کیا گیا تھا، تو ہم TLB کیش مس جیسی چیز سے واقف ہو جائیں گے - پروسیسر ایڈریس کیش میں میموری پیج ایڈریس کی عدم موجودگی۔ پتہ کی تلاش میں ناکام ہونے کی صورت میں تقریباً 30 گنا زیادہ وقت لگتا ہے، اور اگر ڈیٹا بکھر جاتا ہے، تو یہ کئی بار سروس کو سست کر سکتا ہے۔

حاصل يہ ہوا

اس آرٹیکل میں، میں نے ظاہر کیا کہ KDB+ اور Q ڈیٹا بیس نہ صرف بڑے ڈیٹا کو ذخیرہ کرنے اور اسے سلیکٹ کے ذریعے آسانی سے حاصل کرنے کے لیے موزوں ہیں، بلکہ ڈیٹا پروسیسنگ سروسز بنانے کے لیے بھی موزوں ہیں جو کہ لاکھوں قطاروں/گیگا بائٹس ڈیٹا کو ہضم کرنے کے قابل ہیں۔ ایک واحد Q عمل Q زبان ہی ڈیٹا پروسیسنگ سے متعلق الگورتھم کے انتہائی جامع اور موثر نفاذ کی اجازت دیتی ہے کیونکہ اس کی ویکٹر نوعیت، بلٹ ان ایس کیو ایل بولی مترجم اور لائبریری فنکشنز کا ایک بہت کامیاب سیٹ۔

میں نوٹ کروں گا کہ مذکورہ بالا صرف اس کا حصہ ہے جو Q کر سکتا ہے، اس میں دیگر منفرد خصوصیات بھی ہیں۔ مثال کے طور پر، ایک انتہائی سادہ IPC پروٹوکول جو انفرادی Q عمل کے درمیان حد کو مٹا دیتا ہے اور آپ کو ان سینکڑوں عملوں کو ایک نیٹ ورک میں جوڑنے کی اجازت دیتا ہے، جو دنیا کے مختلف حصوں میں درجنوں سرورز پر موجود ہو سکتے ہیں۔

ماخذ: www.habr.com

نیا تبصرہ شامل کریں