DDE never die

nØthing spec¡al by Jimsjoo·2024년 3월 27일
0

VBA

목록 보기
5/13

DDE is something you come across when you're using a brokerage firm's HTS, and it's a fairly old technology. But I still love it because it's a simple way to get real-time data. There is a DDE replacement called RTD, but for some reason it doesn't seem to be widely used. I made it so that the data is saved as a CSV file instead of stacked in a worksheet.

Sub startDDE()
  Dim aLinks, i As Long, strCode As String, Procedure As String
  
  aLinks = ThisWorkbook.LinkSources(xlOLELinks)
  aLinks = Filter(aLinks, ";시간", True)
  
  If IsEmpty(aLinks) Then
    Debug.Print "Error: DDE LinkSource Empty"
    Exit Sub
  End If
  For i = LBound(aLinks) To UBound(aLinks)
      strCode = Right(split(aLinks(i), ";")(0), 8)
      Procedure = "'onData " & Chr(34) & strCode & Chr(34) & "'"
      ThisWorkbook.SetLinkOnData aLinks(i), Procedure
  Next
  Call openFiles
  Debug.Print "Starting DDE Recording..."
End Sub

The Procedure variable stores the onData function and parameters that are called each time data is received.

Procedure = "'onData " & Chr(34) & strCode & Chr(34) & "'"

We have coded the onData() procedure to pass the ticker code as a parameter. The startDDE() procedure will run only the first time, and the onData() procedure will run every few thousand DDE link updates. To keep the code as short and fast as possible, we used worksheet functions frequently.

Sub onData(strCode As String)
  Dim rng As Range
  Dim str As String
  Dim idx As Long

  Call CheckFutures
  On Error GoTo ErrHandler
  
  If GetQueueStatus(QS_INPUT) <> 0 Then DoEvents
  
  idx = WorksheetFunction.Match(strCode, Main.Range(Main.[C1], Main.[C1].End(xlDown)), 0)
  Set rng = Main.Range(Main.Cells(idx, 2), Main.Cells(idx, 2).End(xlToRight))
  str = Join(Application.Transpose(Application.Transpose(rng.Value)), ",")
  
  Select Case Left(strCode, 3)
  Case "101": flFutures.WriteLine str: Debug.Print "onData: " & str
  Case "201": flCallOptions.WriteLine str
  Case "301": flPutoptions.WriteLine str
  End Select
  flAll.WriteLine str
  
  Set rng = Nothing
  Exit Sub
  
ErrHandler:
  Debug.Print "Error: " & Err.Description
  Err.Clear
  Call closeFiles
  Call stopDDE
End Sub

The parameter strCode that comes into the onData() procedure is the entry in the DDElink that was just updated, and the idea is to find this entry in the worksheet and save all of its rows to a CSV.

The worksheet will have futures and calls/puts together, and will save three CSV files, one for futures, one for calls, and one for puts. Futures, calls, and puts have ticker codes that start with 101~, 201~, 301~, and so on, so we can use this to separate the three. The Select Case syntax does just that.

  Select Case Left(strCode, 3)
  Case "101": flFutures.WriteLine str
  Case "201": flCallOptions.WriteLine str
  Case "301": flPutoptions.WriteLine str
  End Select

We extract only the first three characters from the string (str) to be output to a file and save them separately. However, it is inconvenient to save each of the three to a separate CSV, so we use flAll.WriteLine str to save them all without distinguishing between futures, calls, and puts.

The stopDDE() procedure is responsible for releasing the procedure from the DDE link and closing the CSV file it was writing to.

Sub stopDDE()
    Dim aLinks, i As Long

    aLinks = ThisWorkbook.LinkSources(xlOLELinks)
    If IsEmpty(aLinks) Then Exit Sub
    For i = LBound(aLinks) To UBound(aLinks)
        ThisWorkbook.SetLinkOnData aLinks(i), ""
    Next
    Call closeFiles
    Debug.Print "Ending DDE Recording..."
    End
End Sub

This time it is a procedure to open and close a CSV file. We use the scripting library instead of VB's legacy syntax. Since file handling is done in the two procedures below as well as in onData(), we use the following file objects as global variables

Dim fso As Object, flCallOptions, flPutoptions, flFutures, flAll
Sub openFiles()
  Dim fnCallOptions As String, fnPutOptions As String, fnFutures As String, fnAll As String
  Dim str As String
  
  fnFutures = ThisWorkbook.Path & "\" & Date & "_futures.csv"
  fnCallOptions = ThisWorkbook.Path & "\" & Date & "_calloptions.csv"
  fnPutOptions = ThisWorkbook.Path & "\" & Date & "_putoptions.csv"
  fnAll = ThisWorkbook.Path & "\" & Date & "_all.csv"
   
  Set fso = CreateObject("scripting.filesystemobject")
  
  '신규파일이면 컬럼명 추가, 기존파일이면 생략
  If Len(Dir(fnFutures)) = 0 Then str = "종목명,종목코드,시간,현재가,잔존일"
  Set flFutures = fso.OpenTextFile(fnFutures, 8, True)
  If Len(str) <> 0 Then flFutures.WriteLine str: str = ""
  
  If Len(Dir(fnCallOptions)) = 0 Then str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flCallOptions = fso.OpenTextFile(fnCallOptions, 8, True)
  If Len(str) <> 0 Then flCallOptions.WriteLine str: str = ""
  
  If Len(Dir(fnPutOptions)) = 0 Then str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flPutoptions = fso.OpenTextFile(fnPutOptions, 8, True)
  If Len(str) <> 0 Then flPutoptions.WriteLine str: str = ""
  
  If Len(Dir(fnAll)) = 0 Then str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flAll = fso.OpenTextFile(fnAll, 8, True)
  If Len(str) <> 0 Then flAll.WriteLine str
End Sub

Sub closeFiles()
  On Error Resume Next
  flFutures.Close
  flCallOptions.Close
  flPutoptions.Close
  flAll.Close
  Set flFutures = Nothing
  Set flCallOptions = Nothing
  Set flPutoptions = Nothing
  Set flAll = Nothing
  Set fso = Nothing
End Sub

You may also need to close a chapter and email a CSV file of the day's work. The following is a common mailing procedure that you can find if you search for it. There are several ways to send emails, but I chose this one because it's the one I'm most familiar with. You'll need to set up your mail settings in Outlook beforehand.

(I haven't solved the problem yet, you have to press the [Allow] button, I think Outlook's security should fix it...)
(Update) As expected, there is a solution in Outlook Options, as shown below.

Sub sendMail()
  Dim OutApp As Object, OutMail As Object, BodyText As String
  Dim file_fut As String, file_cal As String, file_put As String
  Set OutApp = CreateObject("Outlook.Application")
  Set OutMail = OutApp.CreateItem(0)
  
  BodyText = _
    "Please do not reply to this email, as this inbox is not monitored." & Chr(13) & _
    "Best," & Chr(13) & _
    "Seungjoo from Seoul"
  
  On Error Resume Next
  With OutMail
    .To = "********@gmail.com"
    .CC = ""
    .BCC = ""
    .Subject = "KOSPI200 Futures and Options(" & Date & ")"
    .Body = BodyText
    file_fut = ThisWorkbook.Path & "\" & Date & "_futures.csv"
    file_cal = ThisWorkbook.Path & "\" & Date & "_calloptions.csv"
    file_put = ThisWorkbook.Path & "\" & Date & "_putoptions.csv"
    If Len(Dir(file_fut)) <> 0 Then .Attachments.Add file_fut
    If Len(Dir(file_cal)) <> 0 Then .Attachments.Add file_cal
    If Len(Dir(file_put)) <> 0 Then .Attachments.Add file_put
    .Send   'or use .Display
  End With
  On Error GoTo 0
  Set OutMail = Nothing
  Set OutApp = Nothing
  Debug.Print "Sent the data using e-mail..."
End Sub

However, there is one issue that I haven't seen when dealing with just one instrument, and I don't know if it's a memory leak due to the high number of DDE link updates, but sometimes the DDE link updates for futures stop updating (for OTM options, it may be the quotes as there is almost no trading, but not the futures quotes).

In my experience, this happens once in the morning and once in the afternoon. Initially, I used the last resort of quitting excel and reopening the file, but if I press F2 in the cell with the stuck DDE link to enter cell edit mode and then press ENTER to exit without modifying anything, it works fine.

So, as a last resort, I compared the time of the DDE link of the gift and the time of the computer, and if the difference was more than 3 minutes, I judged that the DDE link of the gift was stuck and sent F2 and ENTER. However, the DDE link was not revived at once, so the time difference of more than 3 minutes remained, causing dozens of F2 and ENTER key presses.

Naturally, the screen flashes, and my heart sinks. So to ensure that it only runs once, I stored the stuck DDElink time entry in the registry, and when it runs, I pull out the stuck time from the registry and compare it to the current DDElink time (If CDate(GetSetting("DDE20", "FUT", "F2")) = CDate(ts1) Then Exit Sub)

The procedure that does this is CheckFutures().

Sub CheckFutures()
  Dim ts0 As Date, ts1 As Date
  
  On Error Resume Next
  ts1 = RetTime(Main.[D2].Value2): ts0 = Time()
  If Err.Number <> 0 Then Exit Sub
  If CDate(GetSetting("DDE20", "FUT", "F2")) = CDate(ts1) Then Exit Sub
  
  If Abs(DateDiff("n", ts0, ts1)) > 3 Then
    Main.[D2].Activate
    With Application
      AppActivate .Caption: .SendKeys "{F2}": .SendKeys "{ENTER}": End With
    Call SaveSetting("DDE20", "FUT", "F2", ts1)
  End If
End Sub

'// DDE링크 시간포맷이 hhmmss이므로 이를 오늘자 시간으로 만들어주는 함수
Function RetTime(IntTime As Long) As Date
  RetTime = TimeSerial(Int(IntTime / 10000), Int((IntTime Mod 10000) / 100), (IntTime Mod 100))
End Function

Finally, we need to have it run automatically when the market opens in the morning and closes in the afternoon. We want to run startDDE at the opening of the day, stopDDE at the closing of the day to stop DDE recording, email the CSV file one minute later, and exit Excel one minute later.

This can be done in the _Open() event procedure that is fired when the workbook is opened.

Private Sub Workbook_Open()
  With Application
    On Error Resume Next
    .OnTime TimeValue("09:00:00"), "startDDE", , False
    .OnTime TimeValue("15:40:00"), "stopDDE", , False
    .OnTime TimeValue("15:41:00"), "sendMail", , False
    .OnTime TimeValue("15:42:00"), "shutDownThis", , False
  
    .OnTime TimeValue("09:00:00"), "startDDE"
    .OnTime TimeValue("15:40:00"), "stopDDE"
    .OnTime TimeValue("15:41:00"), "sendMail"
    .OnTime TimeValue("15:42:00"), "shutDownThis"
  End With
End Sub

The following is a procedure to exit Excel. Note that it saves on exit.

Sub shutDownThis()
  ThisWorkbook.Save
  Application.Quit
End Sub

Here's the full source code.

Option Explicit
'시스템큐
Declare Function GetQueueStatus Lib "user32" (ByVal fuFlags As Long) As Long
Public Const QS_KEY = &H1
Public Const QS_MOUSEMOVE = &H2
Public Const QS_MOUSEBUTTON = &H4
Public Const QS_MOUSE = (QS_MOUSEMOVE Or QS_MOUSEBUTTON)
Public Const QS_INPUT = (QS_MOUSE Or QS_KEY)

Dim fso As Object, flCallOptions, flPutoptions, flFutures, flAll

Sub startDDE()
  Dim aLinks, i As Long, strCode As String, Procedure As String
  
  aLinks = ThisWorkbook.LinkSources(xlOLELinks)
  aLinks = Filter(aLinks, ";시간", True)
  
  If IsEmpty(aLinks) Then
    Debug.Print "Error: DDE LinkSource Empty"
    Exit Sub
  End If
  For i = LBound(aLinks) To UBound(aLinks)
      strCode = Right(split(aLinks(i), ";")(0), 8)
      Procedure = "'onData " & Chr(34) & strCode & Chr(34) & "'"
      ThisWorkbook.SetLinkOnData aLinks(i), Procedure
  Next
  Call openFiles
  Debug.Print "Starting DDE Recording..."
End Sub

Sub onData(strCode As String)
  Dim rng As Range
  Dim str As String
  Dim idx As Long

  Call CheckFutures
  On Error GoTo ErrHandler
  
  If GetQueueStatus(QS_INPUT) <> 0 Then DoEvents
  
  idx = WorksheetFunction.Match(strCode, Main.Range(Main.[C1], Main.[C1].End(xlDown)), 0)
  Set rng = Main.Range(Main.Cells(idx, 2), Main.Cells(idx, 2).End(xlToRight))
  str = Join(Application.Transpose(Application.Transpose(rng.Value)), ",")
  
  Select Case Left(strCode, 3)
  Case "101": flFutures.WriteLine str: Debug.Print "onData: " & str
  Case "201": flCallOptions.WriteLine str
  Case "301": flPutoptions.WriteLine str
  End Select
  flAll.WriteLine str
  
  Set rng = Nothing
  Exit Sub
  
ErrHandler:
  Debug.Print "Error: " & Err.Description
  Err.Clear
  Call closeFiles
  Call stopDDE
End Sub

Sub stopDDE()
    Dim aLinks, i As Long

    aLinks = ThisWorkbook.LinkSources(xlOLELinks)
    If IsEmpty(aLinks) Then Exit Sub
    For i = LBound(aLinks) To UBound(aLinks)
        ThisWorkbook.SetLinkOnData aLinks(i), ""
    Next
    Call closeFiles
    Debug.Print "Ending DDE Recording..."
    End
End Sub

Sub openFiles()
  Dim fnCallOptions As String, fnPutOptions As String, fnFutures As String, fnAll As String
  Dim str As String
  
  fnFutures = ThisWorkbook.Path & "\" & Date & "_futures.csv"
  fnCallOptions = ThisWorkbook.Path & "\" & Date & "_calloptions.csv"
  fnPutOptions = ThisWorkbook.Path & "\" & Date & "_putoptions.csv"
  fnAll = ThisWorkbook.Path & "\" & Date & "_all.csv"
   
  Set fso = CreateObject("scripting.filesystemobject")
  
  '신규파일이면 컬럼명 추가, 기존파일이면 생략
  If Len(Dir(fnFutures)) = 0 Then str = "종목명,종목코드,시간,현재가,잔존일"
  Set flFutures = fso.OpenTextFile(fnFutures, 8, True)
  If Len(str) <> 0 Then flFutures.WriteLine str: str = ""
  
  If Len(Dir(fnCallOptions)) = 0 Then _
str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flCallOptions = fso.OpenTextFile(fnCallOptions, 8, True)
  If Len(str) <> 0 Then flCallOptions.WriteLine str: str = ""
  
  If Len(Dir(fnPutOptions)) = 0 Then _
str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flPutoptions = fso.OpenTextFile(fnPutOptions, 8, True)
  If Len(str) <> 0 Then flPutoptions.WriteLine str: str = ""
  
  If Len(Dir(fnAll)) = 0 Then _
str = "종목명,종목코드,시간,현재가,잔존일,미결제약정,이론가,행사가격,델타,감마,베가,세타,로,내재변동성,역사적변동성,CD금리"
  Set flAll = fso.OpenTextFile(fnAll, 8, True)
  If Len(str) <> 0 Then flAll.WriteLine str
End Sub

Sub closeFiles()
  On Error Resume Next
  flFutures.Close
  flCallOptions.Close
  flPutoptions.Close
  flAll.Close
  Set flFutures = Nothing
  Set flCallOptions = Nothing
  Set flPutoptions = Nothing
  Set flAll = Nothing
  Set fso = Nothing
End Sub

Sub sendMail()
  Dim OutApp As Object, OutMail As Object, BodyText As String
  Dim file_fut As String, file_cal As String, file_put As String
  Set OutApp = CreateObject("Outlook.Application")
  Set OutMail = OutApp.CreateItem(0)
  
  BodyText = _
    "Please do not reply to this email, as this inbox is not monitored." & Chr(13) & _
    "Best," & Chr(13) & _
    "Seungjoo from Seoul"
  
  On Error Resume Next
  With OutMail
    .To = "*******@gmail.com"
    .CC = ""
    .BCC = ""
    .Subject = "KOSPI200 Futures and Options(" & Date & ")"
    .Body = BodyText
    file_fut = ThisWorkbook.Path & "\" & Date & "_futures.csv"
    file_cal = ThisWorkbook.Path & "\" & Date & "_calloptions.csv"
    file_put = ThisWorkbook.Path & "\" & Date & "_putoptions.csv"
    If Len(Dir(file_fut)) <> 0 Then .Attachments.Add file_fut
    If Len(Dir(file_cal)) <> 0 Then .Attachments.Add file_cal
    If Len(Dir(file_put)) <> 0 Then .Attachments.Add file_put
    .Send   'or use .Display
  End With
  On Error GoTo 0
  Set OutMail = Nothing
  Set OutApp = Nothing
  Debug.Print "Sent the data using e-mail..."
End Sub

Sub shutDownThis()
  ThisWorkbook.Save
  Application.Quit
End Sub

Sub CheckFutures()
  Dim ts0 As Date, ts1 As Date
  
  On Error Resume Next
  ts1 = RetTime(Main.[D2].Value2): ts0 = Time()
  If Err.Number <> 0 Then Exit Sub
  If CDate(GetSetting("DDE20", "FUT", "F2")) = CDate(ts1) Then Exit Sub
  
  If Abs(DateDiff("n", ts0, ts1)) > 3 Then
    Main.[D2].Activate
    With Application
      AppActivate .Caption: .SendKeys "{F2}": .SendKeys "{ENTER}": End With
    Call SaveSetting("DDE20", "FUT", "F2", ts1)
  End If
End Sub

Function RetTime(IntTime As Long) As Date
  RetTime = TimeSerial(Int(IntTime / 10000), Int((IntTime Mod 10000) / 100), (IntTime Mod 100))
End Function
profile
harmonized or torn between programming and finance

0개의 댓글