数据被包装在接收数据结构中,其中m_pReportV3指向RCV_REPORT_STRUCTExV3记录数组。JSL 可以使用窥视加载 DLL 并解析二进制数据,而 Python 可以通过ctypes处理 Windows 消息循环。
Challenge
JSL 本身无法创建 Windows 消息循环,因此我集成了 Python 来管理它。DLL 使用窗口句柄和消息 ID 进行初始化,然后将数据发送到该窗口。我需要在 JSL 中解析这些数据,并使用指定的字段填充 JMP 表。
My Proposed Solution
以下是我的实现。它使用 JSL 定义表并解析数据,使用 Python 处理消息循环。我添加了注释来解释每个步骤。您能否查看并提出改进建议?
// StockReceiver.jsl
// Create a JMP data table with required columns
dt = New Table("Stock Data",
New Column("Market", Character, Width(4)), // Market identifier (e.g., 'HS')
New Column("Code", Character, Width(10)), // Stock code
New Column("Name", Character, Width(32)), // Stock name
New Column("Time", Numeric, Format("yyyy-mm-dd hh:mm:ss")), // Transaction time
New Column("LastClose", Numeric, Format("Fixed Dec", 12, 2)), // Previous close
New Column("Open", Numeric, Format("Fixed Dec", 12, 2)), // Opening price
New Column("High", Numeric, Format("Fixed Dec", 12, 2)), // Highest price
New Column("Low", Numeric, Format("Fixed Dec", 12, 2)), // Lowest price
New Column("NewPrice", Numeric, Format("Fixed Dec", 12, 2)), // Latest price
New Column("Volume", Numeric, Format("Fixed Dec", 12, 2)), // Volume
New Column("Amount", Numeric, Format("Fixed Dec", 12, 2)), // Amount
New Column("BuyPrice1", Numeric, Format("Fixed Dec", 12, 3)), // Bid price 1
New Column("BuyVolume1", Numeric, Format("Fixed Dec", 12, 2)),// Bid volume 1
New Column("SellPrice1", Numeric, Format("Fixed Dec", 12, 3)),// Ask price 1
New Column("SellVolume1", Numeric, Format("Fixed Dec", 12, 2)),// Ask volume 1
New Column("BuyPrice2", Numeric, Format("Fixed Dec", 12, 3)), // Bid price 2
New Column("BuyVolume2", Numeric, Format("Fixed Dec", 12, 2)),// Bid volume 2
New Column("SellPrice2", Numeric, Format("Fixed Dec", 12, 3)),// Ask price 2
New Column("SellVolume2", Numeric, Format("Fixed Dec", 12, 2)),// Ask volume 2
New Column("BuyPrice3", Numeric, Format("Fixed Dec", 12, 3)), // Bid price 3
New Column("BuyVolume3", Numeric, Format("Fixed Dec", 12, 2)),// Bid volume 3
New Column("SellPrice3", Numeric, Format("Fixed Dec", 12, 3)),// Ask price 3
New Column("SellVolume3", Numeric, Format("Fixed Dec", 12, 2)),// Ask volume 3
New Column("BuyPrice4", Numeric, Format("Fixed Dec", 12, 3)), // Bid price 4
New Column("BuyVolume4", Numeric, Format("Fixed Dec", 12, 2)),// Bid volume 4
New Column("SellPrice4", Numeric, Format("Fixed Dec", 12, 3)),// Ask price 4
New Column("SellVolume4", Numeric, Format("Fixed Dec", 12, 2)),// Ask volume 4
New Column("BuyPrice5", Numeric, Format("Fixed Dec", 12, 3)), // Bid price 5
New Column("BuyVolume5", Numeric, Format("Fixed Dec", 12, 2)),// Bid volume 5
New Column("SellPrice5", Numeric, Format("Fixed Dec", 12, 3)),// Ask price 5
New Column("SellVolume5", Numeric, Format("Fixed Dec", 12, 2)) // Ask volume 5
);
// Global variables for managing the receiver
global:hwnd = 0; // Window handle
global:dll = 0; // DLL object
global:running = 1; // Flag to control the message loop
// Function to parse RCV_DATA and extract RCV_REPORT_STRUCTExV3 fields
ParseStockData = Function({lParam},
// Check the data type (RCV_REPORT = 0x3f001234)
dataType = Peek(lParam, 0, "int");
If(dataType == 1061153620, // RCV_REPORT
// Get number of records and pointer to report data
packetNum = Peek(lParam, 4, "int"); // m_nPacketNum
reportPtr = Peek(lParam, 276, "int"); // m_pReportV3 offset
// Process each record
For(i = 0, i < packetNum, i++,
baseOffset = reportPtr + i * Peek(reportPtr, 0, "short"); // Offset based on m_cbSize
// Extract fields from RCV_REPORT_STRUCTExV3
time = Peek(baseOffset, 2, "int"); // m_time (time_t)
market = Peek(baseOffset, 6, "short"); // m_wMarket
code = Peek(baseOffset, 8, "char[10]"); // m_szLabel
name = Peek(baseOffset, 18, "char[32]"); // m_szName
lastClose = Peek(baseOffset, 50, "float"); // m_fLastClose
open = Peek(baseOffset, 54, "float"); // m_fOpen
high = Peek(baseOffset, 58, "float"); // m_fHigh
low = Peek(baseOffset, 62, "float"); // m_fLow
newPrice = Peek(baseOffset, 66, "float"); // m_fNewPrice
volume = Peek(baseOffset, 70, "float"); // m_fVolume
amount = Peek(baseOffset, 74, "float"); // m_fAmount
buyPrice1 = Peek(baseOffset, 78, "float"); // m_fBuyPrice[0]
buyPrice2 = Peek(baseOffset, 82, "float"); // m_fBuyPrice[1]
buyPrice3 = Peek(baseOffset, 86, "float"); // m_fBuyPrice[2]
buyPrice4 = Peek(baseOffset, 126, "float"); // m_fBuyPrice4
buyPrice5 = Peek(baseOffset, 134, "float"); // m_fBuyPrice5
buyVolume1 = Peek(baseOffset, 90, "float"); // m_fBuyVolume[0]
buyVolume2 = Peek(baseOffset, 94, "float"); // m_fBuyVolume[1]
buyVolume3 = Peek(baseOffset, 98, "float"); // m_fBuyVolume[2]
buyVolume4 = Peek(baseOffset, 130, "float"); // m_fBuyVolume4
buyVolume5 = Peek(baseOffset, 138, "float"); // m_fBuyVolume5
sellPrice1 = Peek(baseOffset, 102, "float"); // m_fSellPrice[0]
sellPrice2 = Peek(baseOffset, 106, "float"); // m_fSellPrice[1]
sellPrice3 = Peek(baseOffset, 110, "float"); // m_fSellPrice[2]
sellPrice4 = Peek(baseOffset, 142, "float"); // m_fSellPrice4
sellPrice5 = Peek(baseOffset, 150, "float"); // m_fSellPrice5
sellVolume1 = Peek(baseOffset, 114, "float"); // m_fSellVolume[0]
sellVolume2 = Peek(baseOffset, 118, "float"); // m_fSellVolume[1]
sellVolume3 = Peek(baseOffset, 122, "float"); // m_fSellVolume[2]
sellVolume4 = Peek(baseOffset, 146, "float"); // m_fSellVolume4
sellVolume5 = Peek(baseOffset, 154, "float"); // m_fSellVolume5
// Convert time_t to readable format (milliseconds for JMP)
timeStr = Format(DateTime(time * 1000), "yyyy-mm-dd hh:mm:ss");
// Add row to table
dt << Add Rows(1);
row = N Rows(dt);
:Market[row] = Char(market); // Convert market code to string
:Code[row] = code;
:Name[row] = name;
:Time[row] = timeStr;
:LastClose[row] = lastClose;
:Open[row] = open;
:High[row] = high;
:Low[row] = low;
:NewPrice[row] = newPrice;
:Volume[row] = volume;
:Amount[row] = amount;
:BuyPrice1[row] = buyPrice1;
:BuyPrice2[row] = buyPrice2;
:BuyPrice3[row] = buyPrice3;
:BuyPrice4[row] = buyPrice4;
:BuyPrice5[row] = buyPrice5;
:BuyVolume1[row] = buyVolume1;
:BuyVolume2[row] = buyVolume2;
:BuyVolume3[row] = buyVolume3;
:BuyVolume4[row] = buyVolume4;
:BuyVolume5[row] = buyVolume5;
:SellPrice1[row] = sellPrice1;
:SellPrice2[row] = sellPrice2;
:SellPrice3[row] = sellPrice3;
:SellPrice4[row] = sellPrice4;
:SellPrice5[row] = sellPrice5;
:SellVolume1[row] = sellVolume1;
:SellVolume2[row] = sellVolume2;
:SellVolume3[row] = sellVolume3;
:SellVolume4[row] = sellVolume4;
:SellVolume5[row] = sellVolume5;
);
Return(1); // Message processed
,
Return(0); // Unknown data type, not processed
);
);
// Load the DLL
dll = Load DLL("I:\DZH\HailiLive\StockDrv.dll");
If(Is Empty(dll),
Throw("Failed to load StockDrv.dll");
);
global:dll = dll;
// Run Python code to handle Windows message loop
Run Python(
"
import ctypes
from ctypes import wintypes
import jmp
# Constants for message handling
WM_APP = 0x8000
My_Msg_StkData = WM_APP + 1
RCV_REPORT = 0x3f001234
# Windows API type definitions
HWND = wintypes.HWND
UINT = wintypes.UINT
WPARAM = wintypes.WPARAM
LPARAM = wintypes.LPARAM
WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_long, HWND, UINT, WPARAM, LPARAM)
# Load user32.dll for Windows API calls
user32 = ctypes.WinDLL('user32')
# Define WNDCLASSEX structure for window registration
class WNDCLASSEX(ctypes.Structure):
_fields_ = [
('cbSize', UINT),
('style', UINT),
('lpfnWndProc', WNDPROC),
('cbClsExtra', ctypes.c_int),
('cbWndExtra', ctypes.c_int),
('hInstance', wintypes.HINSTANCE),
('hIcon', wintypes.HICON),
('hCursor', wintypes.HCURSOR),
('hbrBackground', wintypes.HBRUSH),
('lpszMenuName', wintypes.LPCWSTR),
('lpszClassName', wintypes.LPCWSTR),
('hIconSm', wintypes.HICON)
]
# Window procedure to handle messages
@WNDPROC
def WndProc(hWnd, msg, wParam, lParam):
if msg == My_Msg_StkData and wParam == RCV_REPORT:
# Call JSL to process the data
result = jmp.run_jsl('ParseStockData(' + str(lParam) + ');')
return result
return user32.DefWindowProcW(hWnd, msg, wParam, lParam)
# Get global variables from JSL
hInstance = ctypes.windll.kernel32.GetModuleHandleW(None) # Current module handle
dll = jmp.get_global('dll') # DLL object
running = jmp.get_global('running') # Loop control flag
# Register window class
wcex = WNDCLASSEX()
wcex.cbSize = ctypes.sizeof(WNDCLASSEX)
wcex.style = 0x3 # CS_HREDRAW | CS_VREDRAW
wcex.lpfnWndProc = WNDPROC(WndProc)
wcex.cbClsExtra = 0
wcex.cbWndExtra = 0
wcex.hInstance = hInstance
wcex.hIcon = 0
wcex.hCursor = 0
wcex.hbrBackground = 0
wcex.lpszMenuName = None
wcex.lpszClassName = 'StockDataWindow'
wcex.hIconSm = 0
if not user32.RegisterClassExW(ctypes.byref(wcex)):
raise Exception('Failed to register window class')
# Create window
hWnd = user32.CreateWindowExW(
0, 'StockDataWindow', 'Stock Receiver', 0,
0, 0, 100, 100, 0, 0, hInstance, None
)
if not hWnd:
raise Exception('Failed to create window')
jmp.set_global('hwnd', hWnd) # Store window handle in JSL
# Initialize the DLL
stock_init = dll.Stock_Init
stock_init.argtypes = [HWND, UINT, ctypes.c_int]
stock_init.restype = ctypes.c_int
result = stock_init(hWnd, My_Msg_StkData, 4) # 4 = RCV_WORK_SENDMSG
if result != 1:
raise Exception('Stock_Init failed: ' + str(result))
# Message loop
msg = ctypes.wintypes.MSG()
while running[0]:
if user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) > 0:
user32.TranslateMessage(ctypes.byref(msg))
user32.DispatchMessageW(ctypes.byref(msg))
else:
break
# Cleanup
stock_quit = dll.Stock_Quit
stock_quit.argtypes = [HWND]
stock_quit.restype = ctypes.c_int
stock_quit(hWnd)
user32.DestroyWindow(hWnd)
user32.UnregisterClassW('StockDataWindow', hInstance)
print('Stock data receiver stopped')
",
"Stock data receiver started" // Output when Python starts
);
// Function to stop the receiver
StopReceiver = Function({},
global:running = 0; // Signal Python to exit loop
If(global:hwnd != 0,
Call DLL("Stock_Quit", dll, global:hwnd); // Cleanup DLL
global:hwnd = 0;
);
Write("Stock data receiver stopped.\!n");
);
// Display startup message
Write("Stock data receiver initialized. Run StopReceiver() to stop.\!n");关键细节
- DLL函数:
- Stock_Init(HWND hWnd,UINT Msg,int nWorkMode) :初始化 DLL,成功时返回 1。
- Stock_Quit(HWND hWnd) :停止消息发送,成功时返回 1。
- 数据结构偏移:
- 接收数据: m_pReportV3位于字节 276(在m_wDataType 、 m_nPacketNum 、 m_文件 、 磁盘管理之后)。
- RCV_REPORT_STRUCTExV3 :与#pragma pack(1)对齐的字段,例如m_时间为 2、 m_fBuyPrice5为 134、 m_fSellVolume5为 154。
- 执行:
- JSL 加载 DLL 并定义表。
- Python 创建一个窗口,初始化 DLL,并运行消息循环。
- 消息触发解析股票数据来填充表。
您的问题
- 使用窥视进行的二进制解析对于偏移量是否正确?我假设由于#pragma pack(1)而没有填充。
- 消息循环是否可以针对高频数据进行优化?
- 我是否应该添加缓冲来减少表更新(例如每 100 行)?
如果您能就潜在问题、性能增强或替代方法提供反馈,我将不胜感激。提前感谢您的专业知识!
此致敬意,谢谢专家!