Automation Bloomberg ALLQ Function, and capture ALLQ screen
本系列的主題如下,所有的程式碼可以從 這裡 下載。
-
Bloomberg Automation - (1).DDE 基礎架構
-
Bloomberg Automation - (2).Terminal 畫面留存並解析內容
-
Bloomberg Automation - (3).Login 及 Logout
-
Bloomberg Automation - (4).更改 Bloomberg 界面的語系
-
Bloomberg Automation - (5).自動執行ALLQ並抓取畫面的內容
-
Bloomberg Automation - (6).自動執行HP、輸入查詢條件並抓取畫面的內容
-
Bloomberg Automation - (7).使用 GRAB 指令 Email 畫面
前面幾篇介紹了 Bloomberg DDE 的基礎架構及指令後,接下來開始說明如何將實務上常用到的功能加以自動化,本篇先講ALLQ自動化,讓Bloomberg 顯示指定 ISIN 的ALLQ 畫面,再將畫面內容以圖片及文字的格式留存下來。
ALLQ畫面抓取處理
當使用者要抓取某筆債券ALLQ的畫面,以US594918BT09債券為例會有下面幾個步驟:輸入US594918BT09後再輸入ALLQ,顯示下圖ALLQ畫面後,再用 Print Screen 或相關軟體抓取畫面。
若是程式要處理的步驟如下:
- 將ISIN及ALLQ指令傳送至Bloomberg
- 等待Bloomberg顯示ALLQ畫面
- 傳送<COPY>指令讓Bloomberg將ALLQ畫面存至Clipboard,若忘記<COPY>指令的作法,可參考 Bloomberg Automation - (2).Terminal 畫面留存並解析內容 。
- 將Clipboard(剪貼簿)的圖片及文字取出,並判斷內容是否正確,若不正確需回到步驟1重新執行
- 將圖片及文字回傳給呼叫端做後續處理
首先新增 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的邏輯
- 透過 ClipboardHelper.GetText() 取得Clipboard 的文字內容
- 檢查內容是否正確,最簡單的作法是判斷內容是否有值以及文字長度,若長度小於100則為不合理的內容需捨棄重新抓取,會判斷長度的原因是Bloomberg 將畫面存至Clipboard 的過程中,有可能Clipboard 的內容還沒處理完畢這時程式就開始抓取Clipboard 的文字,就會發生取出的內容不完整的情況,因此當長度小於100就當成是有問題的資料
- 取得文字後再呼叫ClipboardHelper.GetImage() 取得圖片內容
- 當文字及圖片都取得後就設定 success = true離開迴圈
- 若無文字或圖片則先等待100 milliseconds
- 每隔2秒重送Copy指令給Bloomberg,所以程式判斷已超過2秒就執行重新Copy指令的相關動作
- 回到步驟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 的處理。