Bloomberg Automation - (6).自動執行HP、輸入查詢條件並抓取畫面的內容

Automation Bloomberg HP Function, input item data, and capture HP screen

本系列的主題如下,所有的程式碼可以從 這裡 下載。

 

前一篇介紹了透過程式讓 Bloomberg 顯示 ALLQ 並抓取畫面的內容,這種作法邏輯上分成2個步驟:(1)輸入Ticker及指令(2)抓取畫面,本篇要講另一種3個步驟的實例:(1)輸入Ticker及指令(2)輸入查詢條件(3)抓取畫面,以下將以 HP 功能講解。

HP畫面抓取處理

當使用者要抓取某筆債券的BVAL歷史價格畫面,例如要抓取 US594918BT09 債券 2017/12/01 ~ 2017/12/29  BVAL 的 BidPrice及AskPrice,會有下面幾個步驟:(1).輸入US594918BT09後再輸入HP,(2)顯示HP畫面後,輸入BVAL顯示BVAL價格, (3)輸入要查詢的日期區間,Bloomberg 的日期格式為 mm/dd/yyyy,所以輸入內容為 12/01/2017 ~ 12/29/2017,(4)輸入Bid Price及Ask Price(輸入後畫面顯示的Price文字會變成Px,所以最終顯示的文字為Bid Px/Ask Px,(5).用 Print Screen 或相關軟體抓取畫面。

各步驟需輸入的項目如下:

程式要處理的步驟如下:

  1. 將ISIN及HP指令傳送至Bloomberg
  2. 等待Bloomberg顯示HP畫面
  3. 輸入查詢條件,例如Range、Market、Source
  4. 傳送<COPY>指令讓Bloomberg將HP畫面存至Clipboard
  5. 將Clipboard(剪貼簿)的圖片及文字取出,並判斷內容是否正確,若不正確需回到步驟1重新執行
  6. 將圖片及文字回傳給呼叫端做後續處理

要處理的步驟比前一篇ALLQ多了步驟3  "輸入查詢條件",本篇將說明這個步驟的實作,其它的步驟可參考ALLQ文章的說明。要透過程式輸入各欄位的資料,可透過盤鍵按下 TAB 或 Shift + TAB讓游標往右或往左移動,游標移至要輸入的欄位後,再送出要輸入的值就可以。Bloomberg DDE可以送出<TABR>及<TABL>逹到鍵盤按下 TAB 以及 Shift + TAB 的效果,了解這個指令後,接下來就要計算要送出幾個 <TABR>及<TABL>,下圖是按下TAB鍵游標移動的順序,其中①的位置是輸入Ticker的位置,只要有傳送<GO>指令,游標就會跑到這個位置,所以在傳送的指令中間有<GO>時,就必需從①的位置再重新計算位置,等一下在實作時就會看到這個情況。Currency項目為Read Only,所以按TAB游標不會停在此項目,所以TAB順序不計算此項目。Shift + TAB 的順序就是TAB的反向順序,例如游標在①的位置,按下Shift + TAB游標就會停在 Source 項目

我們的輸入條件為 BVAL 在 2017/12/01 ~ 2017/12/29 這段時間的 Bid Price及Ask Price,所以當顯示HP畫面後,游標會停在①的位置,接下來就要輸入查詢條件,輸入順序為:

  1. 游標停在Ticker輸入區
  2. 按 Shift + TAB至Source 項目,輸入 BVAL 以及<GO>,此時Bloomberg 會立刻抓取BVAL 價格
  3. 上述步驟執行<GO>後,游標回到Ticker輸入區
  4. 按 TAB游標到 QZ095296 Corp項目
  5. 按 TAB 到Ragnge 開始日期,輸入 12/01/2017,不過DDE不需 / 符號,所以需傳 12012017
  6. 按 TAB 到Ragnge 結束日期,輸入 12/29/2017,不過DDE不需 / 符號,所以需傳 12292017
  7. 按 TAB游標到 Period項目
  8. 按 TAB 到Market項目,輸入Bid Price
  9. 按 TAB 到Market第二個項目,輸入Ask Price
  10. 按 TAB到其它項目,執行<GO>依據輸入的條件重新抓資料

依據上面的步驟重新整理後的順序如下

 

了解移動游標的作法後,可將上面各個步驟組成傳送給Bloomberg 的DDE字串,例如 InputQueryFields method 程式碼

private void InputQueryFields(int windowNum, string pricingSource, DateTime startDate, DateTime endDate)
        {

            string ddeCommand = "";

            ddeCommand = "<TABL>"       // Shift TAB (go to Source field)
                       + pricingSource  // input Pricing Source
                       + "<GO>"         // Hit GO
                       + "<TABR>"       // Hit TAB (go to Securities Name field)
                       + "<TABR>"       // Hit TAB (go to Range start date field)
                       + startDate.ToString("MMddyyyy") // input start date
                       + "<TABR>"       // Hit TAB (go to Range end date field)
                       + endDate.ToString("MMddyyyy")   // input end date
                       + "<TABR>"       // Hit TAB (go to Period field)
                       + "<TABR>"       // Hit TAB (go to first Market field)
                       + "Bid Price"    // input Bid Price
                       + "<TABR>"       // Hit TAB (go to second Market field)
                       + "Ask Price"    // input Ask Price
                       + "<TABR>"       // Hit TAB (go to other field)
                       + "<GO>";        // Query Data

            base.DDEInputCommand(windowNum, ddeCommand);

        }

 

接下來要說明主要程式,先新增BloombergDDE_HP Class,程式碼如下:

    public class BloombergDDE_HP : BloombergDDEBase
    {
        private bool _disposed = false;

        private IProgress<HPProgressArgs> _blpGetDataCallback;

        // Protected implementation of Dispose pattern.
        protected override void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            if (disposing)
            {

                _blpGetDataCallback = null;
            }

            _disposed = true;
            // Call base class implementation.
            base.Dispose(disposing);
        }


        public async Task ProcessHPAsync(List<BloombergTicker> tickers,
                                    int windowNum,
                                    IProgress<HPProgressArgs> blpGetDataCallback,
                                    DateTime startDate,
                                    DateTime endDate)
        {

            
            if (tickers.Count == 0)
            {
                return;
            }

            try
            {

                _blpGetDataCallback = blpGetDataCallback;

                foreach (BloombergTicker ticker in tickers)
                {
                    // simulate mouse click input field, then hit ESC key
                    base.ClickAndESCInputField(windowNum);

                    // input ISIN and HP
                    InputHP(windowNum, ticker.ISIN, ticker.MarketSector);

                    // input query fields
                    InputQueryFields(windowNum, ticker.PricingSource, startDate, endDate);


                    // copy screen
                    await GetScreenDataAsync(windowNum, ticker);

                }

            }
            catch (Exception)
            {
                throw;
            }

        }

        // input ISIN and HP
        private void InputHP(int windowNum, string ISIN, string marketSector)
        {
        
            base.InputISINAndFunction(windowNum, ISIN, marketSector, "HP");
        }


        // input query fields
        private void InputQueryFields(int windowNum, string pricingSource, DateTime startDate, DateTime endDate)
        {

            string ddeCommand = "";

            ddeCommand = "<TABL>"       // Shift TAB (go to Source field)
                       + pricingSource  // input Pricing Source
                       + "<GO>"         // Hit GO
                       + "<TABR>"       // Hit TAB (go to Securities Name field)
                       + "<TABR>"       // Hit TAB (go to Range start date field)
                       + startDate.ToString("MMddyyyy") // input start date
                       + "<TABR>"       // Hit TAB (go to Range end date field)
                       + endDate.ToString("MMddyyyy")   // input end date
                       + "<TABR>"       // Hit TAB (go to Period field)
                       + "<TABR>"       // Hit TAB (go to first Market field)
                       + "Bid Price"    // input Bid Price
                       + "<TABR>"       // Hit TAB (go to second Market field)
                       + "Ask Price"    // input Ask Price
                       + "<TABR>"       // Hit TAB (go to other field)
                       + "<GO>";        // Query Data

            base.DDEInputCommand(windowNum, ddeCommand);

        }

        // 解析畫面
        private async Task GetScreenDataAsync(int winNum, BloombergTicker processTicker)
        {
            string clipboardText = null;
            Image clipboardImage = null;
            Stopwatch timerRetryCopyWaitTime = new Stopwatch();

            try
            {

                ClipboardHelper.Clear();

                // input copy function
                base.CopyScreen(winNum);
                await SleepAsync(300);

                timerRetryCopyWaitTime.Restart();


                DateTime timeOutDate = DateTime.Now.Add(TimeSpan.FromSeconds(20));


                bool success = false;

                while (!success && (DateTime.Now < timeOutDate))
                {

                    clipboardText = ClipboardHelper.GetText();


                    if (string.IsNullOrEmpty(clipboardText) || clipboardText.Length < 100)
                    {
                        clipboardText = null;
                    }
                    else
                    {
                        clipboardImage = ClipboardHelper.GetImage();
                    }

                    if (clipboardText != null && clipboardImage != null)
                    {
                        success = true;
                    }
                    else
                    {
                        // wait 100 Milliseconds
                        if (DateTime.Now < timeOutDate.AddMilliseconds(100))
                        {
                            await SleepAsync(100);
                        }

                        // resend copy function after 2 seconds
                        if (timerRetryCopyWaitTime.Elapsed.TotalMilliseconds > 2000)
                        {
                            ClipboardHelper.Clear();
                            base.CopyScreen(winNum);
                            await SleepAsync(2000);
                            timerRetryCopyWaitTime.Restart();
                        }
                    }


                }


                // Callback
                if (success)
                {
                    CallBackEvent(winNum, processTicker, clipboardText, clipboardImage, "");
                }
                else
                {
                    CallBackEvent(winNum, processTicker, clipboardText, clipboardImage, "Timeout!");
                }



            }
            catch (Exception ex)
            {
                CallBackEvent(winNum, processTicker, clipboardText, clipboardImage, ex.Message);
                throw;
            }
        }

        private void CallBackEvent(int winNum, BloombergTicker data, string text, Image image, string errorMessage)
        {
            if(_blpGetDataCallback != null)
            {
                HPProgressArgs arg = new HPProgressArgs();
                arg.Windownum = winNum;
                arg.ISIN = data.ISIN;
                arg.PricingSource = data.PricingSource;
                arg.Text = text;
                arg.Image = image;

                if (!string.IsNullOrEmpty(errorMessage))
                {
                    arg.IsError = true;
                    arg.ErrorMessage = errorMessage;
                }

                _blpGetDataCallback.Report(arg);
            }

        }
    }

 

BloombergDDE_HP class  的架構跟上一篇的 BloombergDDE_ALLQ class很像,所以只講解差異的部份,在 ProcessHPAsync method多了 startDate及endDate 參數,就是HP畫面的起迄日期,至於 BGN/BVAL 則是指定在 ticker參數的PricingSource屬性,ticker 的內容如下:

List<BloombergTicker> tickers = new List<BloombergTicker>
{
  new BloombergTicker {
    ISIN = "US594918BT09",
    MarketSector = "CORP" ,
    PricingSource = "BVAL"
  }
};

ProcessHPAsync method處理邏輯如下:

  1. 執行 base.ClickAndESCInputField 模擬滑鼠在Ticker輸入區按下左鍵的動作,並且送ESC鍵讓Bloomberg放棄原本在處理的工作
  2. 執行 InputHP method 傳送Ticker及HP指令,Blommberg接收指令後顯示HP內容
  3. 執行 InputQueryFields method 輸入查詢條件
  4. 執行GetScreenDataAsync method 將HP畫面回傳給呼叫端程式

步驟2、4與上一篇ALLQ作法相同,步驟3是本篇一開始講解的輸入查詢條件,因此不再說明。

步驟1則是模擬滑鼠在Tikcer輸入區按下左鍵及送出ESC,會做此動作的原因是 Bloomberg 要接受DDE指令一定要處於可輸入資料的狀態,也就是當我們在Terminal 按下鍵盤按鍵時,Terminal 會顯示對應的字元,但是在特殊情況下Bloomberg處於不可輸入資料的狀態,所以按下任何按鍵Bloomberg都不會有反應,在此情況下就無法處理DDE指令,必需用滑鼠點選畫面上可以輸入資料的欄位才可繼續處理DDE指令。

在實際運作的經驗,因為程式多了 "輸入查詢條件" 的動作,使得Bloomberg容易出現不可輸入資料的狀態,所以才需模擬滑鼠點選輸入欄位的動作,這部份的程式碼有一點複雜,需要使用Win32 API,有興趣的人可以直接看程式碼了解實作方式。

接下來我們增加一個畫面測試我們寫的功能,名稱為 frmHP,畫面如下:

測試的程式碼如下:

     private async void btnExecute_Click(object sender, EventArgs e)
        {
            try
            {
                picResult.Image = null;
                txtResult.Text = "";

                string pricingSource = cboPricingSource.SelectedItem.ToString();
                
                List<BloombergTicker> tickers = new List<BloombergTicker>
                {
                    new BloombergTicker {
                        ISIN =txtISIN.Text,
                        MarketSector ="CORP" ,
                        PricingSource = pricingSource
                        }
                };

                using (BloombergDDE_HP bloomberg = new BloombergDDE_HP())
                {
                    DateTime pricingStartDate, pricingEndDate;

                    pricingStartDate = dtStartDate.Value;
                    pricingEndDate = dtEndDate.Value;

                    var blpGetDataCallback = new Progress<HPProgressArgs>(BLPDataArrive);
                    int windowNum = int.Parse(cboWindowNum.Text);

                    await bloomberg.ProcessHPAsync(tickers, windowNum, blpGetDataCallback, pricingStartDate, pricingEndDate);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void BLPDataArrive(HPProgressArgs args)
        {

            int winNum = args.Windownum;
            string ISIN = args.ISIN;
            string pricingSource = args.PricingSource;
            Image image = args.Image;
            string text = args.Text;
            bool isSuccess = !args.IsError;
            string errorMessage = args.ErrorMessage;

            if (isSuccess)
            {
                picResult.Image = image;
                txtResult.Text = text;
            }
            else
            {
                // Error handle

            }

        }

 

程式碼的邏輯與前一篇 ALLQ 一樣,差異之處則是多了起迄日期 以及 PricingSource(BGN、BVAL....)的處理,另外要注意的是,程式中會在Market項目填入 Bid Price/Ask Price,這個項目的內容在中文版本的Bloomberg 就需改成中文字串,這個範例使用英文版本,所以在執行測試程式之前需先將Bloomberg 的界面改成英文界面,更改界面的作法可參考  Bloomberg Automation - (4).更改 Bloomberg 界面的語系

執行後的畫面如下:

圖.測試程式顯示HP畫面的圖片

圖.測試程式顯示HP畫面的文字

我們也可將tickers List 加上多筆資料讓程式依序處理HP,將程式修改如下就可看到Bloombeg分別顯示每一筆ISIN 的HP,以及測試畫面顯示對應的圖片及文字。

List<BloombergTicker> tickers = new List<BloombergTicker>
{
   new BloombergTicker {ISIN="US594918BT09", MarketSector="CORP", PricingSource = "BGN" },
   new BloombergTicker {ISIN="US594918BT09", MarketSector="CORP", PricingSource = "BVAL" },
   new BloombergTicker {ISIN="XS1733841735", MarketSector="CORP", PricingSource = "BGN" },
   new BloombergTicker {ISIN="USY39694AA51", MarketSector="CORP", PricingSource = "BGN"}
};

以上為HP的作法說明,重點在如何輸入查詢條件,因此有類似需輸入條件的功能就可用本篇的作法,下一篇將說明使用Bloomberg GRAB 指令將畫面當作附件Email 給其它人。