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

林俊宇的學習筆記
19 min readMar 4, 2022

--

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

前一篇介紹了透過程式讓 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 給其它人。

--

--