Bloomberg Automation - (5).自動執行ALLQ並抓取畫面的內容

Automation Bloomberg ALLQ Function, and capture ALLQ screen

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

 

前面幾篇介紹了 Bloomberg DDE 的基礎架構及指令後,接下來開始說明如何將實務上常用到的功能加以自動化,本篇先講ALLQ自動化,讓Bloomberg 顯示指定 ISIN 的ALLQ 畫面,再將畫面內容以圖片及文字的格式留存下來。

ALLQ畫面抓取處理

當使用者要抓取某筆債券ALLQ的畫面,以US594918BT09債券為例會有下面幾個步驟:輸入US594918BT09後再輸入ALLQ,顯示下圖ALLQ畫面後,再用 Print Screen 或相關軟體抓取畫面。

 

若是程式要處理的步驟如下:

  1. 將ISIN及ALLQ指令傳送至Bloomberg
  2. 等待Bloomberg顯示ALLQ畫面
  3. 傳送<COPY>指令讓Bloomberg將ALLQ畫面存至Clipboard,若忘記<COPY>指令的作法,可參考 Bloomberg Automation - (2).Terminal 畫面留存並解析內容 。
  4. 將Clipboard(剪貼簿)的圖片及文字取出,並判斷內容是否正確,若不正確需回到步驟1重新執行
  5. 將圖片及文字回傳給呼叫端做後續處理

首先新增 BloombergTicker Class存放Ticker相關內容,程式碼如下:

public class BloombergTicker
{
  public string ISIN { get; set; }
  public string MarketSector { get; set; }
  public string PricingSource { get; set; }
}

ISIN 為本次要處理的ISIN,MarketSector 填入 CORP、MTGE等內容,PricingSource 在本次ALLQ功能不會用到,在介紹HP 功能時會填入BGN、BVAL等內容。

接下來在 BloombergDDEBase 新增下列程式碼:

//Input ISIN and Function, then hit <GO>
public void InputISINAndFunction(int windowNum, string ISIN, string marketSector, string function)
{
	string ticker = ISIN + " <" + marketSector + ">" + function;
	string commandString = ticker + BloombergKeys.Go;
	DDEInputCommand(windowNum, commandString);
}

public async Task SleepAsync(int milliseconds)
{
	if (milliseconds != 0)
	{
		await Task.Delay(milliseconds);
	}
}

public async Task SleepAsync(TimeSpan delay)
{
	if (delay.TotalMilliseconds != 0)
	{
		await Task.Delay(delay);
	}
}

InputISINAndFunction method會組出傳送給Bloomberg 的字串,字串包含 ISIN 及功能代碼,例如 US594918BT09 <CORP>ALLQ<GO>

SleepAsync 則是 Asynchronous(非同步)版本的 Sleep功能,實務上採用Asynchronous的架構存取Clipboard的速度會比較快,當處理的筆數很多時速度會有很大的差異。

接下來新增BloombergDDE_ALLQ Class,程式碼如下:

public class BloombergDDE_ALLQ : BloombergDDEBase
{
	private bool _disposed = false;
	private IProgress<ALLQProgressArgs> _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 ProcessALLQAsync(List<BloombergTicker> tickers,
								int windowNum,
								IProgress<ALLQProgressArgs> blpGetDataCallback)
	{
		if (tickers.Count == 0)
		{
			return;
		}

		try
		{
			_blpGetDataCallback = blpGetDataCallback;

			foreach (BloombergTicker ticker in tickers)
			{
				// input ISIN and ALLQ
				InputALLQ(windowNum, ticker.ISIN, ticker.MarketSector);

				// copy screen
				await GetScreenDataAsync(windowNum, ticker);
			}
		}
		catch (Exception)
		{
			throw;
		}
	}

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

	// copy screen
	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)
		{
			ALLQProgressArgs arg = new ALLQProgressArgs();
			arg.Windownum = winNum;
			arg.ISIN = data.ISIN;
			arg.Text = text;
			arg.Image = image;

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

			_blpGetDataCallback.Report(arg);
		}

	}
}

BloombergDDE_ALLQ繼承 BloombergDDEBase Class,因此BloombergDDE_ALLQ就可使用之前已完成的各項DDE 功能。

ProcessALLQAsync method的參數tickers 為本次要處理的ticker,當程式取得ALLQ畫面後會將畫面回傳給呼叫端的程式做後續處理,參數 blpGetDataCallback就是要放呼叫端要做後續處理的程式(delegate method)。

InputALLQ 將Ticker及ALLQ指令傳送到Blommberg顯示ALLQ內容,接下來執行GetScreenDataAsync 讓Blommberg將畫面的內容存至Clipboard,在說明Clipboard 的存取邏輯前,先說明實務上常會碰到Clipboard內容不正確的例外情況:

  • 同時間其它程式也在更改Clipboard 的內容,造成Clipboard內容不正確,例如其它使用者使用VNC 連至Bloomberg電腦,該使用者若在自已的個人電腦做文書處理工作,有使用到複製、貼上或清空資料等動作,這時VNC也會同步更改Bloomberg 那台電腦的Clipboard,造成Clipboard內容不正確
  • Bloomberg還在處理ALLQ指令的過程中,程式就送出<COPY>指令,所以Bloomberg有可能未正確處理<COPY>指令,此時Clipboard的內容可能為空白或是只有部份內容,或是殘留前一筆的內容
  • 程式執行過程有非預期的狀況導致Clipboard一直無資料,例如DDE過程有錯誤的情況發生

所以在處理Clipboard時,當程式經過一段時間無資料,可以先嘗試重新送<COPY>指令,讓Bloomberg重新將畫面存至Clipboard,看看是否可取得資料,若嘗試幾次仍無法取得資料時,則需透過Timeout機制讓程式跳離以避免讓使用者一直等待處理結果,並誤認為程式當機。

另外即使取得資料也應該要判斷內容是否正確,若內容不正確就需重送ISIN及ALLQ指令,例如在傳送ISIN、ALLQ、<COPY>等相關指令時,有使用者操作Bloomberg導致有些指令並沒有被正確的執行,此時Clipboard 可能會殘留前一筆ISIN的資料,這會造成內容與目前處理的ISIN是不同的資料,此時就應該要從頭開始重送指令,本篇最後面會說明如何判斷是否為同樣的資料。

知道這些例外情況後就比較容易了解GetScreenDataAsync method 的邏輯,首先 ClipboardHelper.Clear()先清除Clipboard 的內容,再透過base.CopyScreen讓Bloomberg 執行Copy指令將畫面存至 Clipboard,並等待一段時間(300 milliseconds)。接下來將現在的時間加20秒當作TimeOut時間並存入timeOutDate 變數中,當程式處理超過20秒時會強迫跳離程式。

接下來的while迴圈會處理抓取Clipboard的邏輯

  1. 透過 ClipboardHelper.GetText() 取得Clipboard 的文字內容
  2. 檢查內容是否正確,最簡單的作法是判斷內容是否有值以及文字長度,若長度小於100則為不合理的內容需捨棄重新抓取,會判斷長度的原因是Bloomberg 將畫面存至Clipboard 的過程中,有可能Clipboard 的內容還沒處理完畢這時程式就開始抓取Clipboard 的文字,就會發生取出的內容不完整的情況,因此當長度小於100就當成是有問題的資料
  3. 取得文字後再呼叫ClipboardHelper.GetImage() 取得圖片內容
  4. 當文字及圖片都取得後就設定 success = true離開迴圈
  5. 若無文字或圖片則先等待100 milliseconds
  6. 每隔2秒重送Copy指令給Bloomberg,所以程式判斷已超過2秒就執行重新Copy指令的相關動作
  7. 回到步驟1次再次執行迴圈,需等到取得Clipboard的內容或是Timeout才會離開迴圈

迴圈結束後不論是否取得Clipboard 的內容皆會呼叫CallBackEvent將流程返回呼叫端,呼叫端可從 IsError變數得知是否已成功取得資料,若成功可將文字及圖片做進一步處理,例如顯示在畫面或存檔。

上面用 ClipboardHelper Class 做Clipboard 的文字及圖片處理,程式碼如下:

public class ClipboardHelper
{
  public static void Clear()
  {
    try
    {
      Clipboard.Clear();
    }
    catch (Exception) { }
  }

  public static string GetText()
  {
    string result = null;

    try
    {
      if (Clipboard.ContainsText(TextDataFormat.UnicodeText))
      {
        result = Clipboard.GetText(TextDataFormat.UnicodeText);
      }
      else if (Clipboard.ContainsText(TextDataFormat.Text))
      {
        result = Clipboard.GetText(TextDataFormat.Text);
      }
      else if (Clipboard.ContainsText(TextDataFormat.Rtf))
      {
        result = Clipboard.GetText(TextDataFormat.Rtf);
      }
      else if (Clipboard.ContainsText(TextDataFormat.Html))
      {
        result = Clipboard.GetText(TextDataFormat.Html);
      }

      return result;
    }
    catch (Exception)
    {
      return null;
    }
  }

  public static Image GetImage()
  {
    Image result = null;
    try
    {
      if (Clipboard.ContainsImage())
      {
        result = Clipboard.GetImage();
      }
      return result;
    }
    catch (Exception)
    {
      return null;
    }
  }
}

 

其中 Clear 跟 GetImage method 都是Clipboard 的基本功能在此就不再特別說明,主要要說明的是GetText中會依序判斷文字格式是否為UnicodeText、Text、Rtf....,這邊需特別注意 UnicodeText需比Text優先判斷,因為債券名稱中的利率若有分數的格式時,例如 4 ¾,其中¾在UnicodeText格式才可正確取得,若用Text格式則會變成?符號。

接下來我們增加一個畫面測試我們寫的功能,名稱為 frmALLQ,畫面跟前幾篇文章的frmDDE類似,但增加了顯示圖片的PictureBox,名稱為picResult,以及顯示文字內容的RichTextBox,名稱為 txtResult,如下圖

測試的程式碼如下:

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

                List<BloombergTicker> tickers = new List<BloombergTicker>
                {
                    new BloombergTicker {ISIN=txtISIN.Text, MarketSector="CORP" }
                };

                using (BloombergDDE_ALLQ bloomberg = new BloombergDDE_ALLQ())
                {
                    Progress<ALLQProgressArgs> blpGetDataCallback = new Progress<ALLQProgressArgs>(BLPDataArrive);
                    int windowNum = int.Parse(cboWindowNum.Text);

                    await bloomberg.ProcessALLQAsync(tickers, windowNum, blpGetDataCallback);
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void BLPDataArrive(ALLQProgressArgs args)
        {

            int winNum = args.Windownum;
            string ISIN = args.ISIN;
            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
            {
                // error handle
            }
        }

 

btnExecute_Click中把要處理的ticker加入到 tickers List,因為畫面只有一個ticker欄位,所以只加入一筆資料。Progress<ALLQProgressArgs> blpGetDataCallback 則是指定由BLPDataArrive mthod處理回傳的圖片及文字內容,執行bloomberg.ProcessALLQAsync就可進行 ALLQ處理,當取得Bloomberg畫面後會呼叫 BLPDataArrive  method,程式將圖片及文字內容顯示在畫面上,執行後的畫面如下。

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

 

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

 

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

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

由本篇文章可知讓Bloomberg顯示ALLQ的作法不複雜,比較複雜的反而是Clipboard 的處理,除了以上講解的相關處理邏輯外,在實際運用上應該還需做到驗證Clipboard 的內容與Bloomberg ALLQ的內容是否一致,實際作法可解析取得的文字內容並做到下面幾點的驗證,才可避免Clipboard 與 Bloomberg ALLQ 內容不符的情況

  • 功能名稱需一致:畫面上會有功能的名稱,例如 ALLQ 的功能名稱為 All Quotes,所以文字內容要判斷是否有All Quotes字串
  • ticker要一致:以債券的ALLQ畫面為例,最好的作法是判斷ISIN是否相同,但ALLQ畫面沒有顯示ISIN,所以我們可以改用債券名稱判斷,例如我們要判斷文字內容的債券名稱是否為 MSFT 3.7 08/08/46 

ALLQ的作法已介紹完畢,下一篇文章將說明 HP 的處理。