下面给你 10 个框架,每个都是完整可跑 的独立设计,覆盖不同使用场景。它们都基于你那份 CTPStrategy.cpp 的精确签名(InitEx 8 参数、回调 void(int, const char*, int)、GetBricks/GetKLine 返回 const char*)。你可以挑一个最顺手的用,也可以混搭。
约定:每个框架都假设 DLL 名为
ctp.dll,放在同目录。SimNow 账号broker="9999"、license_key=""即可。
框架 1:极简函数式(脚本党最爱)
特点:没有类,没有框架,纯函数 + 全局状态。适合写一次性脚本。
python
# ctp_func.py
import ctypes, json, threading, time
from collections import defaultdict
_dll = ctypes.CDLL("./ctp.dll")
# --- 签名(只声明用到的)---
_p = ctypes.c_char_p
_i = ctypes.c_int
_d = ctypes.c_double
_dll.InitEx.argtypes = [_p]*8
_dll.InitEx.restype = _i
_dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, _i, _p, _i)]
_dll.Start.restype = _i
_dll.Stop.restype = None
_dll.Cleanup.restype = None
_dll.IsRunning.restype = _i
_dll.IsTdReady.restype = _i
_dll.SubscribeSymbol.argtypes = [_p]
_dll.SubscribeKLine.argtypes = [_p, _i]
_dll.SubscribeRenkoMulti.argtypes = [_p, ctypes.POINTER(_i), _i]
_dll.GetBricks.argtypes = [_p, _i, _i]; _dll.GetBricks.restype = _p
_dll.GetKLine.argtypes = [_p, _i, _i]; _dll.GetKLine.restype = _p
_dll.BuyOpen.argtypes = [_p, _i, _d]; _dll.BuyOpen.restype = _i
_dll.SellOpen.argtypes = [_p, _i, _d]; _dll.SellOpen.restype = _i
# --- 全局缓存 ---
handlers = defaultdict(list)
ticks, account, positions = {}, {}, {}
def on(event, fn=None):
"""@on('tick') 或 on('tick', fn)"""
def reg(f):
handlers[event].append(f)
return f
return reg if fn is None else reg(fn)
def _cb(msg_type, data, length):
try:
msg = json.loads(data[:length].decode("utf-8", "ignore"))
t = msg.get("type", msg_type)
if t == 31: ticks[msg["instrument_id"]] = msg
elif t == 4: account.update(msg)
elif t == 90: positions[msg["symbol"]] = msg
for fn in handlers.get(t, ()):
try: fn(msg)
except Exception as e: print("回调异常:", e)
except Exception:
pass
_CB = ctypes.CFUNCTYPE(None, _i, _p, _i)(_cb)
_dll.SetOnDataCallback(_CB)
def init(user, password, broker="9999", **kw):
return _dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id", "simnow_client_test").encode(),
kw.get("auth_code", "0000000000000000").encode(),
kw.get("td_addr", "tcp://182.254.243.31:30001").encode(),
kw.get("md_addr", "tcp://182.254.243.31:30011").encode(),
kw.get("license_key", "").encode(),
)
def start(): _dll.Start()
def stop(): _dll.Stop(); _dll.Cleanup()
def ready(): return _dll.IsTdReady() == 1
def subscribe(*symbols):
for s in symbols: _dll.SubscribeSymbol(s.encode())
def subscribe_kline(symbol, *periods):
for p in periods: _dll.SubscribeKLine(symbol.encode(), p)
def subscribe_renko_multi(symbol, *ticks_):
arr = (_i * len(ticks_))(*ticks_)
_dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def get_bricks(symbol, brick_ticks, n=100):
r = _dll.GetBricks(symbol.encode(), brick_ticks, n)
return json.loads(r.decode()) if r else []
def get_klines(symbol, period, n=100):
r = _dll.GetKLine(symbol.encode(), period, n)
return json.loads(r.decode()) if r else []
def buy(symbol, vol=1, price=0.0): return _dll.BuyOpen(symbol.encode(), vol, price)
def sell(symbol, vol=1, price=0.0): return _dll.SellOpen(symbol.encode(), vol, price)
用法:
python
from ctp_func import *
@on("tick")
def _(m): print(m["instrument_id"], m["last_price"])
@on("account")
def _(m): print("权益", m["dynamic_equity"])
init("127922", "123@")
start()
subscribe("IM2612")
subscribe_kline("IM2612", 60)
subscribe_renko_multi("IM2612", 3, 5, 9)
try:
while not ready(): time.sleep(0.1)
input("运行中,回车退出\n")
finally:
stop()
框架 2:装饰器路由(Flask 风格)
特点 :@app.on("tick")、@app.on("tick", symbol="IM2612"),路由清晰,像写 web 一样写交易。
python
# ctp_router.py
import ctypes, json, threading, time
from collections import defaultdict
from functools import wraps
class Route:
def __init__(self, event, symbol=None, once=False):
self.event, self.symbol, self.once = event, symbol, once
def match(self, msg):
if self.symbol is None: return True
return msg.get("instrument_id") == self.symbol or msg.get("symbol") == self.symbol
class CTPApp:
EVENT = {"tick":31,"kline":91,"brick":88,"account":4,"position_profit":90,
"order":33,"trade":32,"instrument":23,"connected":10,"login":20,
"error":13,"log":0,"disconnected":11,"order_error":35}
def __init__(self, dll_path="ctp.dll"):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.routes = defaultdict(list)
self.once_routes = defaultdict(list)
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._dispatch)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
for fn in ("Start",):
getattr(self.dll, fn).restype = i
for fn in ("Stop","Cleanup"):
getattr(self.dll, fn).restype = None
for fn in ("IsRunning","IsTdReady"):
getattr(self.dll, fn).restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p, ctypes.POINTER(i), i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _resolve(self, event):
return self.EVENT.get(event, event) if isinstance(event, str) else event
def on(self, event, *, symbol=None):
eid = self._resolve(event)
def deco(fn):
self.routes[eid].append(Route(eid, symbol))
return fn
return deco
def once(self, event, *, symbol=None):
eid = self._resolve(event)
def deco(fn):
self.once_routes[eid].append((Route(eid, symbol), fn))
return fn
return deco
def _dispatch(self, msg_type, data, length):
try:
msg = json.loads(data[:length].decode("utf-8","ignore"))
eid = msg.get("type", msg_type)
# 缓存
if eid == 31: self.cache["tick"][msg["instrument_id"]] = msg
elif eid == 4: self.cache["account"] = msg
elif eid == 90: self.cache["position"][msg["symbol"]] = msg
elif eid == 88: self.cache["brick"][f"{msg['symbol']}_{msg['brick_ticks']}"] = msg
elif eid == 23: self.cache["instrument"][msg["instrument_id"]] = msg
for r in list(self.routes.get(eid, ())):
if r.match(msg):
try: self._call_route(r, msg)
except Exception as e: print(e)
# once
for r, fn in list(self.once_routes.get(eid, ())):
if r.match(msg):
try: fn(msg)
except Exception: pass
self.once_routes[eid].remove((r, fn))
except Exception:
pass
def _call_route(self, route, msg):
# route 没存 fn,重新查
for r in self.routes[route.event]:
pass
# 简化:直接存 (route, fn)
def _dispatch(self, msg_type, data, length):
try:
msg = json.loads(data[:length].decode("utf-8","ignore"))
eid = msg.get("type", msg_type)
if eid == 31: self.cache["tick"][msg["instrument_id"]] = msg
elif eid == 4: self.cache["account"] = msg
elif eid == 90: self.cache["position"][msg["symbol"]] = msg
elif eid == 88: self.cache["brick"][f"{msg['symbol']}_{msg['brick_ticks']}"] = msg
elif eid == 23: self.cache["instrument"][msg["instrument_id"]] = msg
for r, fn in list(self.routes.get(eid, ())):
if r.match(msg):
try: fn(msg)
except Exception as e: print("route err:", e)
for r, fn in list(self.once_routes.get(eid, ())):
if r.match(msg):
try: fn(msg)
except Exception: pass
self.once_routes[eid].remove((r, fn))
except Exception:
pass
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode(),
)
def start(self, *, wait_ready=True, timeout=30):
self.dll.Start()
if wait_ready:
end = time.time() + timeout
while time.time() < end:
if self.dll.IsTdReady() == 1: return True
time.sleep(0.1)
return False
return True
def stop(self):
self.dll.Stop(); self.dll.Cleanup()
self._cb = None
def run(self):
try:
while self.dll.IsRunning() == 1: time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
self.stop()
def subscribe(self, *symbols):
for s in symbols: self.dll.SubscribeSymbol(s.encode())
def subscribe_renko_multi(self, symbol, *ticks):
arr = (ctypes.c_int * len(ticks))(*ticks)
self.dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def subscribe_kline(self, symbol, *periods):
for p in periods: self.dll.SubscribeKLine(symbol.encode(), p)
def bricks(self, symbol, ticks, n=100):
r = self.dll.GetBricks(symbol.encode(), ticks, n)
return json.loads(r.decode()) if r else []
def klines(self, symbol, period, n=100):
r = self.dll.GetKLine(symbol.encode(), period, n)
return json.loads(r.decode()) if r else []
def buy(self, symbol, vol=1, price=0.0): return self.dll.BuyOpen(symbol.encode(), vol, price)
def sell(self, symbol, vol=1, price=0.0): return self.dll.SellOpen(symbol.encode(), vol, price)
def buy_close(self, symbol, vol=1, price=0.0): return self.dll.BuyClose(symbol.encode(), vol, price)
def sell_close(self, symbol, vol=1, price=0.0): return self.dll.SellClose(symbol.encode(), vol, price)
用法:
python
from ctp_router import CTPApp
app = CTPApp("ctp.dll")
@app.on("tick")
def on_tick(m): print(f"{m['instrument_id']} {m['last_price']}")
@app.on("tick", symbol="IM2612")
def on_im(m): pass
@app.once("login")
def on_login(m): print("登录成功")
app.init("127922", "123@")
app.start()
app.subscribe("IM2612")
app.subscribe_renko_multi("IM2612", 3, 5, 9)
app.run()
框架 3:数据快照 + 策略基类(量化风)
特点 :内置 Strategy 基类,on_bar/on_tick/on_brick 需要自己实现。ctx 提供只读快照,策略不直接碰 DLL。
python
# ctp_quant.py
import ctypes, json, threading, time
from collections import defaultdict, deque
from dataclasses import dataclass, field
@dataclass
class Tick:
symbol: str; last: float; bid: float; ask: float
volume: int; oi: float; raw: dict
@dataclass
class Bar:
symbol: str; period: int; o: float; h: float; l: float; c: float
volume: int; start: int; raw: dict
@dataclass
class Brick:
symbol: str; ticks: int; o: float; h: float; l: float; c: float
idx: int; direction: int; raw: dict
@dataclass
class Account:
balance: float = 0; available: float = 0; margin: float = 0
position_pnl: float = 0; dynamic_equity: float = 0; raw: dict = field(default_factory=dict)
@dataclass
class Position:
symbol: str; long_vol: int = 0; long_avg: float = 0; long_pnl: float = 0
short_vol: int = 0; short_avg: float = 0; short_pnl: float = 0; raw: dict = field(default_factory=dict)
class Context:
"""策略看到的只读视图"""
def __init__(self, engine):
self._e = engine
@property
def account(self) -> Account: return self._e.account
def tick(self, symbol) -> Tick | None: return self._e.ticks.get(symbol)
def position(self, symbol) -> Position | None: return self._e.positions.get(symbol)
def bars(self, symbol, period) -> list[Bar]: return list(self._e.klines.get((symbol, period), ()))
def bricks(self, symbol, ticks) -> list[Brick]: return list(self._e.bricks.get((symbol, ticks), ()))
def buy(self, symbol, vol=1, price=0): return self._e.buy(symbol, vol, price)
def sell(self, symbol, vol=1, price=0): return self._e.sell(symbol, vol, price)
def buy_close(self, symbol, vol=1, price=0): return self._e.buy_close(symbol, vol, price)
def sell_close(self, symbol, vol=1, price=0): return self._e.sell_close(symbol, vol, price)
def log(self, *a): print("[strategy]", *a)
class Strategy:
"""继承我,实现 on_tick / on_bar / on_brick"""
def on_start(self, ctx): pass
def on_tick(self, ctx, tick: Tick): pass
def on_bar(self, ctx, bar: Bar): pass
def on_brick(self, ctx, brick: Brick): pass
class Engine:
def __init__(self, dll_path="ctp.dll", history=500):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.ticks: dict[str, Tick] = {}
self.account = Account()
self.positions: dict[str, Position] = {}
self.klines: dict[tuple, deque] = defaultdict(lambda: deque(maxlen=history))
self.bricks: dict[tuple, deque] = defaultdict(lambda: deque(maxlen=history))
self.strategy: Strategy | None = None
self.ctx = Context(self)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i
self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes = [p]
self.dll.SubscribeKLine.argtypes = [p, i]
self.dll.SubscribeRenkoMulti.argtypes = [p, ctypes.POINTER(i), i]
self.dll.GetBricks.argtypes = [p, i, i]; self.dll.GetBricks.restype = p
self.dll.GetKLine.argtypes = [p, i, i]; self.dll.GetKLine.restype = p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes = [p, i, d]; getattr(self.dll, fn).restype = i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31:
tk = Tick(m["instrument_id"], m["last_price"], m["bid_price1"], m["ask_price1"],
m["volume"], m["open_interest"], m)
self.ticks[tk.symbol] = tk
if self.strategy: self.strategy.on_tick(self.ctx, tk)
elif t == 4:
self.account = Account(m["balance"], m["available"], m["margin"],
m["position_pnl"], m["dynamic_equity"], m)
elif t == 90:
p = Position(m["symbol"], m["long_volume"], m["long_avg_price"], m["long_floating_pnl"],
m["short_volume"], m["short_avg_price"], m["short_floating_pnl"], m)
self.positions[p.symbol] = p
elif t == 91:
b = Bar(m["symbol"], m["period"], m["open"], m["high"], m["low"], m["close"],
m["volume"], m["start_time"], m)
self.klines[(b.symbol, b.period)].append(b)
if self.strategy: self.strategy.on_bar(self.ctx, b)
elif t == 88:
br = Brick(m["symbol"], m["brick_ticks"], m["open"], m["high"], m["low"], m["close"],
m["index"], m["direction"], m)
self.bricks[(br.symbol, br.ticks)].append(br)
if self.strategy: self.strategy.on_brick(self.ctx, br)
except Exception:
pass
def set_strategy(self, s: Strategy): self.strategy = s
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
def start(self, *, wait_ready=True, timeout=30):
self.dll.Start()
if wait_ready:
end = time.time() + timeout
while time.time() < end:
if self.dll.IsTdReady() == 1: break
time.sleep(0.1)
if self.strategy: self.strategy.on_start(self.ctx)
def stop(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
def subscribe(self, *symbols):
for s in symbols: self.dll.SubscribeSymbol(s.encode())
def subscribe_renko_multi(self, symbol, *ticks):
arr = (ctypes.c_int * len(ticks))(*ticks)
self.dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def subscribe_kline(self, symbol, *periods):
for p in periods: self.dll.SubscribeKLine(symbol.encode(), p)
def buy(self, s, vol=1, price=0.0): return self.dll.BuyOpen(s.encode(), vol, price)
def sell(self, s, vol=1, price=0.0): return self.dll.SellOpen(s.encode(), vol, price)
def buy_close(self, s, vol=1, price=0.0): return self.dll.BuyClose(s.encode(), vol, price)
def sell_close(self, s, vol=1, price=0.0): return self.dll.SellClose(s.encode(), vol, price)
def run(self):
try:
while self.dll.IsRunning() == 1: time.sleep(0.5)
except KeyboardInterrupt: pass
finally: self.stop()
用法:
python
from ctp_quant import Engine, Strategy
class MyStrat(Strategy):
def on_start(self, ctx):
ctx.log("策略启动,可用资金:", ctx.account.available)
def on_tick(self, ctx, tick):
pass
def on_bar(self, ctx, bar):
if bar.period == 60:
bars = ctx.bars(bar.symbol, 60)
if len(bars) >= 2 and bars[-2].c < bars[-1].c:
ctx.log(f"{bar.symbol} 阳线 C={bar.c}")
def on_brick(self, ctx, brick):
if brick.direction == 1 and brick.ticks == 9:
ctx.log(f"9tick 红砖 #{brick.idx}")
engine = Engine("ctp.dll")
engine.set_strategy(MyStrat())
engine.init("127922", "123@")
engine.start()
engine.subscribe("IM2612")
engine.subscribe_kline("IM2612", 60)
engine.subscribe_renko_multi("IM2612", 3, 5, 9)
engine.run()
框架 4:异步事件循环(asyncio)
特点 :async for 消费事件,await 等待条件,适合和其他 async 生态(aiohttp、websockets)集成。
python
# ctp_async.py
import asyncio, ctypes, json, threading
from collections import defaultdict
class AsyncCTP:
def __init__(self, dll_path="ctp.dll"):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.loop = asyncio.get_event_loop()
self.queues: dict[int, list[asyncio.Queue]] = defaultdict(list)
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p, ctypes.POINTER(i), i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31: self.cache["tick"][m["instrument_id"]] = m
elif t == 4: self.cache["account"] = m
# 从 DLL 线程 post 到 asyncio
for q in self.queues.get(t, ()):
self.loop.call_soon_threadsafe(q.put_nowait, m)
for q in self.queues.get("*", ()):
self.loop.call_soon_threadsafe(q.put_nowait, m)
except Exception:
pass
async def stream(self, event="*"):
"""async for msg in ctp.stream('tick')"""
eid = self.EVENT.get(event, event) if isinstance(event, str) else event
q = asyncio.Queue()
self.queues[eid].append(q)
try:
while True:
yield await q.get()
finally:
self.queues[eid].remove(q)
async def wait_ready(self, timeout=30):
loop = asyncio.get_event_loop()
end = loop.time() + timeout
while loop.time() < end:
if self.dll.IsTdReady() == 1: return True
await asyncio.sleep(0.1)
return False
async def close(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
EVENT = {"tick":31,"kline":91,"brick":88,"account":4,"position_profit":90,"login":20,
"connected":10,"error":13,"order":33,"trade":32}
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
def start(self): self.dll.Start()
def subscribe(self, *symbols):
for s in symbols: self.dll.SubscribeSymbol(s.encode())
def subscribe_renko_multi(self, symbol, *ticks):
arr = (ctypes.c_int * len(ticks))(*ticks)
self.dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def subscribe_kline(self, symbol, *periods):
for p in periods: self.dll.SubscribeKLine(symbol.encode(), p)
def buy(self, s, vol=1, price=0.0): return self.dll.BuyOpen(s.encode(), vol, price)
def sell(self, s, vol=1, price=0.0): return self.dll.SellOpen(s.encode(), vol, price)
用法:
python
import asyncio
from ctp_async import AsyncCTP
async def main():
ctp = AsyncCTP("ctp.dll")
ctp.init("127922", "123@")
ctp.start()
await ctp.wait_ready()
ctp.subscribe("IM2612")
ctp.subscribe_renko_multi("IM2612", 3, 5, 9)
async def consume_ticks():
async for m in ctp.stream("tick"):
print(f"tick {m['instrument_id']} {m['last_price']}")
async def consume_bricks():
async for m in ctp.stream("brick"):
print(f"brick {m['symbol']}[{m['brick_ticks']}] C={m['close']}")
try:
await asyncio.gather(consume_ticks(), consume_bricks())
finally:
await ctp.close()
asyncio.run(main())
框架 5:插件/中间件架构
特点 :数据流经过一条 Middleware 管道,可以插日志、风控、指标计算。适合做复杂系统。
python
# ctp_pipeline.py
import ctypes, json, time
from collections import defaultdict
from typing import Callable
class Middleware:
"""实现 __call__(msg) -> msg | None,返回 None 表示丢弃"""
def __call__(self, msg): return msg
class LogMiddleware(Middleware):
def __init__(self, events): self.events = set(events)
def __call__(self, msg):
if msg["type"] in self.events:
print(f"[{msg['type']}] {msg.get('instrument_id', msg.get('symbol',''))}")
return msg
class FilterMiddleware(Middleware):
def __init__(self, symbols): self.symbols = set(symbols)
def __call__(self, msg):
sym = msg.get("instrument_id") or msg.get("symbol")
return msg if sym is None or sym in self.symbols else None
class CTPPipeline:
def __init__(self, dll_path="ctp.dll"):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.middlewares: list[Middleware] = []
self.handlers = defaultdict(list)
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._pipeline)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p,ctypes.POINTER(i),i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def use(self, mw: Middleware):
self.middlewares.append(mw); return self
def on(self, event_id, fn=None):
def reg(f): self.handlers[event_id].append(f); return f
return reg if fn is None else reg(fn)
def _pipeline(self, mt, data, ln):
try:
msg = json.loads(data[:ln].decode("utf-8","ignore"))
msg.setdefault("type", mt)
# 依次经过中间件
for mw in self.middlewares:
msg = mw(msg)
if msg is None: return
# 缓存
t = msg["type"]
if t == 31: self.cache["tick"][msg["instrument_id"]] = msg
elif t == 4: self.cache["account"] = msg
elif t == 90: self.cache["position"][msg["symbol"]] = msg
# 分发
for fn in self.handlers.get(t, ()):
try: fn(msg)
except Exception as e: print("handler err:", e)
except Exception:
pass
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
def start(self, *, wait_ready=True, timeout=30):
self.dll.Start()
if wait_ready:
end = time.time()+timeout
while time.time()<end:
if self.dll.IsTdReady()==1: return True
time.sleep(0.1)
return True
def stop(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
def run(self):
try:
while self.dll.IsRunning()==1: time.sleep(0.5)
except KeyboardInterrupt: pass
finally: self.stop()
def subscribe(self, *symbols):
for s in symbols: self.dll.SubscribeSymbol(s.encode())
def subscribe_kline(self, symbol, *periods):
for p in periods: self.dll.SubscribeKLine(symbol.encode(), p)
def subscribe_renko_multi(self, symbol, *ticks):
arr=(ctypes.c_int*len(ticks))(*ticks); self.dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def buy(self, s, vol=1, price=0.0): return self.dll.BuyOpen(s.encode(), vol, price)
def sell(self, s, vol=1, price=0.0): return self.dll.SellOpen(s.encode(), vol, price)
用法:
python
from ctp_pipeline import CTPPipeline, LogMiddleware, FilterMiddleware
pipe = CTPPipeline("ctp.dll")
pipe.use(FilterMiddleware(["IM2612"])) # 只保留 IM2612
pipe.use(LogMiddleware([31, 88, 90])) # 打印 tick/brick/position
@pipe.on(88)
def on_brick(m): print("红砖" if m["direction"]==1 else "绿砖")
@pipe.on(90)
def on_pnl(m):
if m["long_volume"] > 0:
print(f"多单浮盈 {m['long_floating_pnl']}")
pipe.init("127922", "123@")
pipe.start()
pipe.subscribe("IM2612", "IF2612")
pipe.subscribe_renko_multi("IM2612", 3, 5, 9)
pipe.run()
框架 6:状态机 / 交易机器人
特点 :内置 State 枚举,交易有明确状态转移,避免重复下单。适合做简单的自动交易。
python
# ctp_bot.py
import ctypes, json, time
from enum import Enum
from collections import defaultdict
class State(Enum):
IDLE = 0
LONG = 1
SHORT = 2
PENDING = 3
class TradingBot:
def __init__(self, dll_path, symbol, *, qty=1):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.symbol = symbol
self.qty = qty
self.state = State.IDLE
self.last_price = 0.0
self.account = {}
self.position = {}
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p,ctypes.POINTER(i),i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31 and m["instrument_id"] == self.symbol:
self.last_price = m["last_price"]
self._on_tick(m)
elif t == 88 and m["symbol"] == self.symbol:
self._on_brick(m)
elif t == 4:
self.account = m
elif t == 90 and m["symbol"] == self.symbol:
self.position = m
elif t == 33 and m["instrument_id"] == self.symbol:
self._on_order(m)
except Exception:
pass
def _on_tick(self, m): pass
def _on_brick(self, m): pass
def _on_order(self, m):
# 报单完成后重置状态
if m["order_status"] in (0, 5): # AllTraded / Canceled
if self.state == State.PENDING:
self.state = State.IDLE
def open_long(self):
if self.state != State.IDLE: return
self.state = State.PENDING
self.dll.BuyOpen(self.symbol.encode(), self.qty, self.last_price)
print(f"[BOT] 买入开仓 {self.symbol}@{self.last_price}")
def close_long(self):
if self.state != State.LONG: return
self.state = State.PENDING
self.dll.SellClose(self.symbol.encode(), self.qty, self.last_price)
print(f"[BOT] 卖出平仓 {self.symbol}@{self.last_price}")
def open_short(self):
if self.state != State.IDLE: return
self.state = State.PENDING
self.dll.SellOpen(self.symbol.encode(), self.qty, self.last_price)
print(f"[BOT] 卖出开仓 {self.symbol}@{self.last_price}")
def close_short(self):
if self.state != State.SHORT: return
self.state = State.PENDING
self.dll.BuyClose(self.symbol.encode(), self.qty, self.last_price)
def init(self, user, password, **kw):
return self.dll.InitEx(
kw.get("broker","9999").encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
def start(self):
self.dll.Start()
for _ in range(300):
if self.dll.IsTdReady() == 1: break
time.sleep(0.1)
self.dll.SubscribeSymbol(self.symbol.encode())
self.dll.SubscribeRenkoMulti(self.symbol.encode(), (ctypes.c_int*1)(5), 1)
def stop(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
def run(self):
try:
while self.dll.IsRunning()==1: time.sleep(1)
except KeyboardInterrupt: pass
finally: self.stop()
用法:
python
from ctp_bot import TradingBot, State
class MyBot(TradingBot):
def _on_brick(self, m):
if m["brick_ticks"] != 5: return
# 极简策略:红砖开多,绿砖平多开空
if m["direction"] == 1 and self.state == State.IDLE:
self.open_long(); self.state = State.LONG
elif m["direction"] == -1 and self.state == State.LONG:
self.close_long(); self.state = State.SHORT
bot = MyBot("ctp.dll", "IM2612", qty=1)
bot.init("127922", "123@")
bot.start()
bot.run()
框架 7:带 HTTP API 的服务化框架
特点 :ctp.dll 跑在服务端,通过 HTTP/WebSocket 对外提供接口。适合做交易终端。
python
# ctp_server.py --- 依赖 aiohttp: pip install aiohttp
import asyncio, ctypes, json, time
from aiohttp import web
from collections import defaultdict
class CTPServer:
def __init__(self, dll_path, host="127.0.0.1", port=8080):
self.dll = ctypes.CDLL(dll_path)
self.host, self.port = host, port
self._bind()
self.ws_clients: set = set()
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p,ctypes.POINTER(i),i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31: self.cache["tick"][m["instrument_id"]] = m
elif t == 4: self.cache["account"] = m
elif t == 90: self.cache["position"][m["symbol"]] = m
self._broadcast({"type": t, "data": m})
except Exception:
pass
def _broadcast(self, payload):
if not self.ws_clients: return
msg = json.dumps(payload, ensure_ascii=False)
for ws in list(self.ws_clients):
try:
asyncio.create_task(ws.send_str(msg))
except Exception:
self.ws_clients.discard(ws)
async def handle_ws(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
self.ws_clients.add(ws)
try:
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
await self._handle_ws_cmd(ws, json.loads(msg.data))
finally:
self.ws_clients.discard(ws)
return ws
async def _handle_ws_cmd(self, ws, cmd):
op = cmd.get("op")
if op == "subscribe":
for s in cmd.get("symbols", []):
self.dll.SubscribeSymbol(s.encode())
elif op == "buy":
self.dll.BuyOpen(cmd["symbol"].encode(), cmd.get("vol",1), cmd.get("price",0))
elif op == "sell":
self.dll.SellOpen(cmd["symbol"].encode(), cmd.get("vol",1), cmd.get("price",0))
elif op == "bricks":
r = self.dll.GetBricks(cmd["symbol"].encode(), cmd["ticks"], cmd.get("n",100))
await ws.send_json({"op": "bricks", "data": json.loads(r.decode()) if r else []})
async def handle_tick(self, request):
sym = request.query.get("symbol")
return web.json_response(self.cache["tick"].get(sym, {}))
async def handle_account(self, request):
return web.json_response(self.cache["account"])
async def handle_position(self, request):
sym = request.query.get("symbol")
if sym: return web.json_response(self.cache["position"].get(sym, {}))
return web.json_response(self.cache["position"])
async def handle_buy(self, request):
body = await request.json()
ret = self.dll.BuyOpen(body["symbol"].encode(), body.get("vol",1), body.get("price",0))
return web.json_response({"ret": ret})
async def handle_ready(self, request):
return web.json_response({"ready": self.dll.IsTdReady() == 1})
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
async def run(self):
app = web.Application()
app.router.add_get("/ws", self.handle_ws)
app.router.add_get("/tick", self.handle_tick)
app.router.add_get("/account", self.handle_account)
app.router.add_get("/position", self.handle_position)
app.router.add_post("/buy", self.handle_buy)
app.router.add_get("/ready", self.handle_ready)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, self.host, self.port)
await site.start()
print(f"服务已启动: http://{self.host}:{self.port}")
self.dll.Start()
for _ in range(300):
if self.dll.IsTdReady() == 1: break
await asyncio.sleep(0.1)
# 保持运行
try:
while True: await asyncio.sleep(3600)
except KeyboardInterrupt:
pass
finally:
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
await runner.cleanup()
if __name__ == "__main__":
srv = CTPServer("ctp.dll")
srv.init("127922", "123@")
asyncio.run(srv.run())
用法:
python
# 客户端
import requests, websockets, asyncio, json
print(requests.get("http://127.0.0.1:8080/tick?symbol=IM2612").json())
print(requests.get("http://127.0.0.1:8080/account").json())
async def ws():
async with websockets.connect("ws://127.0.0.1:8080/ws") as w:
await w.send(json.dumps({"op": "subscribe", "symbols": ["IM2612"]}))
async for msg in w:
print(json.loads(msg))
asyncio.run(ws())
框架 8:同步阻塞式高阶 API
特点 :await ctp.wait_for("tick", lambda m: m["instrument_id"]=="IM2612")------像同步代码一样等待条件。适合测试和脚本。
python
# ctp_wait.py
import ctypes, json, time, threading
from collections import defaultdict
from typing import Callable
class SyncCTP:
def __init__(self, dll_path="ctp.dll"):
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.conditions: list[tuple[int, Callable, threading.Event, dict]] = []
self.lock = threading.Lock()
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p,ctypes.POINTER(i),i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31: self.cache["tick"][m["instrument_id"]] = m
elif t == 4: self.cache["account"] = m
elif t == 90: self.cache["position"][m["symbol"]] = m
with self.lock:
for eid, pred, ev, holder in self.conditions:
if eid == t and pred(m):
holder["result"] = m
ev.set()
except Exception:
pass
def wait_for(self, event, predicate=lambda m: True, timeout=10):
"""阻塞直到满足条件,返回消息;超时抛 TimeoutError"""
eid = self.EVENT.get(event, event) if isinstance(event, str) else event
ev = threading.Event()
holder = {}
entry = (eid, predicate, ev, holder)
with self.lock:
self.conditions.append(entry)
try:
if not ev.wait(timeout):
raise TimeoutError(f"等待 {event} 超时")
return holder["result"]
finally:
with self.lock:
self.conditions.remove(entry)
def wait_until(self, condition: Callable[[], bool], timeout=30, interval=0.1):
"""等待任意条件成立"""
end = time.time() + timeout
while time.time() < end:
if condition(): return True
time.sleep(interval)
return False
EVENT = {"tick":31,"kline":91,"brick":88,"account":4,"position_profit":90,"login":20,
"connected":10,"error":13}
def init(self, user, password, broker="9999", **kw):
return self.dll.InitEx(
broker.encode(), user.encode(), password.encode(),
kw.get("app_id","simnow_client_test").encode(),
kw.get("auth_code","0000000000000000").encode(),
kw.get("td_addr","tcp://182.254.243.31:30001").encode(),
kw.get("md_addr","tcp://182.254.243.31:30011").encode(),
kw.get("license_key","").encode())
def start(self, *, wait_ready=True, timeout=30):
self.dll.Start()
if wait_ready:
self.wait_until(lambda: self.dll.IsTdReady()==1, timeout)
def stop(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
def subscribe(self, *syms):
for s in syms: self.dll.SubscribeSymbol(s.encode())
def subscribe_kline(self, symbol, *periods):
for p in periods: self.dll.SubscribeKLine(symbol.encode(), p)
def subscribe_renko_multi(self, symbol, *ticks):
arr=(ctypes.c_int*len(ticks))(*ticks); self.dll.SubscribeRenkoMulti(symbol.encode(), arr, len(arr))
def buy(self, s, vol=1, price=0.0): return self.dll.BuyOpen(s.encode(), vol, price)
def sell(self, s, vol=1, price=0.0): return self.dll.SellOpen(s.encode(), vol, price)
def buy_close(self, s, vol=1, price=0.0): return self.dll.BuyClose(s.encode(), vol, price)
def sell_close(self, s, vol=1, price=0.0): return self.dll.SellClose(s.encode(), vol, price)
def account(self): return self.cache["account"]
def tick(self, s): return self.cache["tick"].get(s, {})
def position(self, s): return self.cache["position"].get(s, {})
def bricks(self, s, t, n=100):
r = self.dll.GetBricks(s.encode(), t, n)
return json.loads(r.decode()) if r else []
def klines(self, s, p, n=100):
r = self.dll.GetKLine(s.encode(), p, n)
return json.loads(r.decode()) if r else []
用法(像同步脚本一样自然):
python
from ctp_wait import SyncCTP
ctp = SyncCTP("ctp.dll")
ctp.init("127922", "123@")
ctp.start()
ctp.subscribe("IM2612")
ctp.subscribe_renko_multi("IM2612", 3, 5, 9)
# 等待第一个 tick
tick = ctp.wait_for("tick", lambda m: m["instrument_id"]=="IM2612", timeout=10)
print(f"第一个 tick: {tick['last_price']}")
# 等待第一个 5tick 红砖
brick = ctp.wait_for("brick", lambda m: m["brick_ticks"]==5 and m["direction"]==1, timeout=60)
print(f"第一个红砖: C={brick['close']}")
# 下单并等成交
ctp.buy("IM2612", 1, tick["last_price"])
trade = ctp.wait_for("trade", lambda m: m["instrument_id"]=="IM2612", timeout=10)
print(f"成交: {trade['price']} x {trade['volume']}")
ctp.stop()
框架 9:YAML/TOML 配置驱动
特点:全部由配置文件驱动,Python 代码只是启动器。适合运维部署。
python
# ctp_config.py --- 依赖 pyyaml: pip install pyyaml
import ctypes, json, time, yaml
from collections import defaultdict
class ConfiguredCTP:
def __init__(self, config_path, dll_path="ctp.dll"):
with open(config_path, "r", encoding="utf-8") as f:
self.cfg = yaml.safe_load(f)
self.dll = ctypes.CDLL(dll_path)
self._bind()
self.handlers = defaultdict(list)
self.cache = defaultdict(dict)
self._cb = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(self._on_msg)
self.dll.SetOnDataCallback(self._cb)
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes=[p]
self.dll.SubscribeKLine.argtypes=[p,i]
self.dll.SubscribeRenkoMulti.argtypes=[p,ctypes.POINTER(i),i]
self.dll.GetBricks.argtypes=[p,i,i]; self.dll.GetBricks.restype=p
self.dll.GetKLine.argtypes=[p,i,i]; self.dll.GetKLine.restype=p
for fn in ("BuyOpen","SellOpen","BuyClose","SellClose"):
getattr(self.dll, fn).argtypes=[p,i,d]; getattr(self.dll,fn).restype=i
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31: self.cache["tick"][m["instrument_id"]] = m
elif t == 4: self.cache["account"] = m
elif t == 90: self.cache["position"][m["symbol"]] = m
for fn in self.handlers.get(t, ()):
try: fn(m)
except Exception: pass
except Exception:
pass
def on(self, event_id):
def deco(fn):
self.handlers[event_id].append(fn); return fn
return deco
def run(self):
acc = self.cfg["account"]
self.dll.InitEx(
acc["broker"].encode(), acc["user"].encode(), acc["password"].encode(),
acc.get("app_id","simnow_client_test").encode(),
acc.get("auth_code","0000000000000000").encode(),
acc["td_addr"].encode(), acc["md_addr"].encode(),
acc.get("license_key","").encode())
self.dll.Start()
end = time.time() + 30
while time.time() < end and self.dll.IsTdReady()!=1:
time.sleep(0.1)
sub = self.cfg.get("subscribe", {})
for s in sub.get("symbols", []):
self.dll.SubscribeSymbol(s.encode())
for s, periods in sub.get("kline", {}).items():
for p in periods: self.dll.SubscribeKLine(s.encode(), p)
for s, ticks in sub.get("renko", {}).items():
arr = (ctypes.c_int * len(ticks))(*ticks)
self.dll.SubscribeRenkoMulti(s.encode(), arr, len(arr))
try:
while self.dll.IsRunning()==1: time.sleep(0.5)
except KeyboardInterrupt: pass
finally:
self.dll.Stop(); self.dll.Cleanup(); self._cb = None
配置文件 config.yaml:
yaml
account:
broker: "9999"
user: "127922"
password: "123@"
app_id: "simnow_client_test"
auth_code: "0000000000000000"
td_addr: "tcp://182.254.243.31:30001"
md_addr: "tcp://182.254.243.31:30011"
license_key: ""
subscribe:
symbols: ["IM2612", "IF2612"]
kline:
IM2612: [60, 300]
renko:
IM2612: [3, 5, 9]
logging:
level: INFO
events: [31, 88, 90]
用法:
python
from ctp_config import ConfiguredCTP
app = ConfiguredCTP("config.yaml")
@app.on(31)
def on_tick(m): print("tick", m["instrument_id"], m["last_price"])
@app.on(88)
def on_brick(m): print("brick", m["symbol"], m["brick_ticks"], m["direction"])
app.run()
框架 10:多合约 + 回放/实盘双模式
特点 :Engine 同时支持实盘和回放(从 JSON 文件回放历史 tick),方便回测。核心是 DataSource 抽象。
python
# ctp_replay.py
import ctypes, json, time
from abc import ABC, abstractmethod
from collections import defaultdict
class DataSource(ABC):
@abstractmethod
def start(self): ...
@abstractmethod
def stop(self): ...
@abstractmethod
def subscribe(self, *symbols): ...
@abstractmethod
def set_on_msg(self, cb): ...
class LiveSource(DataSource):
def __init__(self, dll_path, cfg):
self.dll = ctypes.CDLL(dll_path)
self.cfg = cfg
self._cb_ref = None
self._bind()
def _bind(self):
p, i, d = ctypes.c_char_p, ctypes.c_int, ctypes.c_double
self.dll.InitEx.argtypes = [p]*8; self.dll.InitEx.restype = i
self.dll.SetOnDataCallback.argtypes = [ctypes.CFUNCTYPE(None, i, p, i)]
self.dll.Start.restype = i; self.dll.Stop.restype = None; self.dll.Cleanup.restype = None
self.dll.IsRunning.restype = i; self.dll.IsTdReady.restype = i
self.dll.SubscribeSymbol.argtypes = [p]
self.dll.SubscribeKLine.argtypes = [p, i]
self.dll.SubscribeRenkoMulti.argtypes = [p, ctypes.POINTER(i), i]
def set_on_msg(self, cb):
self._cb_ref = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_int)(cb)
self.dll.SetOnDataCallback(self._cb_ref)
def start(self):
c = self.cfg
self.dll.InitEx(
c["broker"].encode(), c["user"].encode(), c["password"].encode(),
c.get("app_id","simnow_client_test").encode(),
c.get("auth_code","0000000000000000").encode(),
c["td_addr"].encode(), c["md_addr"].encode(),
c.get("license_key","").encode())
self.dll.Start()
end = time.time() + 30
while time.time() < end and self.dll.IsTdReady() != 1:
time.sleep(0.1)
def stop(self):
self.dll.Stop(); self.dll.Cleanup(); self._cb_ref = None
def subscribe(self, *symbols):
for s in symbols: self.dll.SubscribeSymbol(s.encode())
class ReplaySource(DataSource):
def __init__(self, jsonl_path, speed=10.0):
self.path = jsonl_path
self.speed = speed
self.cb = None
self._running = False
def set_on_msg(self, cb): self.cb = cb
def start(self): self._running = True
def stop(self): self._running = False
def subscribe(self, *symbols): pass # 回放不需要
def run(self):
last_ts = None
with open(self.path, "r", encoding="utf-8") as f:
for line in f:
if not self._running: break
if not line.strip(): continue
m = json.loads(line)
if last_ts and self.speed > 0:
dt = m.get("_ts", 0) - last_ts
if 0 < dt < 60: time.sleep(dt / self.speed)
last_ts = m.get("_ts", last_ts)
self.cb(m["type"], m["_json"].encode(), len(m["_json"].encode()))
class UnifiedEngine:
def __init__(self, source: DataSource):
self.source = source
self.handlers = defaultdict(list)
self.cache = defaultdict(dict)
self.ticks = {}
self.account = {}
self.positions = {}
self.source.set_on_msg(self._on_msg)
def _on_msg(self, mt, data, ln):
try:
m = json.loads(data[:ln].decode("utf-8","ignore"))
t = m.get("type", mt)
if t == 31:
self.ticks[m["instrument_id"]] = m
elif t == 4: self.account = m
elif t == 90: self.positions[m["symbol"]] = m
for fn in self.handlers.get(t, ()):
try: fn(m)
except Exception as e: print("err:", e)
except Exception:
pass
def on(self, event_id):
def deco(fn): self.handlers[event_id].append(fn); return fn
return deco
def start(self):
self.source.start()
if isinstance(self.source, ReplaySource):
self.source.run()
def stop(self): self.source.stop()
def subscribe(self, *s): self.source.subscribe(*s)
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "replay":
src = ReplaySource("ticks.jsonl", speed=20)
else:
src = LiveSource("ctp.dll", {
"broker": "9999", "user": "127922", "password": "123@",
"td_addr": "tcp://182.254.243.31:30001",
"md_addr": "tcp://182.254.243.31:30011",
})
eng = UnifiedEngine(src)
@eng.on(31)
def on_tick(m): print(m["instrument_id"], m["last_price"])
eng.start()
if isinstance(src, LiveSource):
eng.subscribe("IM2612")
try:
while True: time.sleep(0.5)
except KeyboardInterrupt: pass
finally: eng.stop()
回放文件格式 ticks.jsonl(每行一个):
json
{"_ts": 1700000000, "type": 31, "_json": "{\"type\":31,\"instrument_id\":\"IM2612\",\"last_price\":3500.0}"}
{"_ts": 1700000001, "type": 31, "_json": "{\"type\":31,\"instrument_id\":\"IM2612\",\"last_price\":3501.0}"}
用法:
bash
python ctp_replay.py # 实盘
python ctp_replay.py replay # 回放
十框架对比速查
| # | 框架 | 风格 | 最适合 | 代码量 |
|---|---|---|---|---|
| 1 | ctp_func |
函数式 | 一次性脚本 | ~120 行 |
| 2 | ctp_router |
装饰器路由 | 多合约事件分发 | ~200 行 |
| 3 | ctp_quant |
策略基类 | 量化策略开发 | ~250 行 |
| 4 | ctp_async |
asyncio | 异步生态集成 | ~180 行 |
| 5 | ctp_pipeline |
中间件管道 | 复杂业务流 | ~150 行 |
| 6 | ctp_bot |
状态机 | 自动交易机器人 | ~170 行 |
| 7 | ctp_server |
HTTP/WebSocket | 远程终端 | ~200 行 |
| 8 | ctp_wait |
同步等待 | 测试/脚本 | ~160 行 |
| 9 | ctp_config |
YAML 配置 | 运维部署 | ~130 行 |
| 10 | ctp_replay |
双模式 | 回测+实盘 | ~230 行 |
所有框架共享的 4 条铁律(务必遵守)
InitEx必须传 8 个参数 ,license_key即使空也要传。GetBricks/GetKLine必须restype = c_char_p,否则 64 位指针截断崩溃。- 回调必须保持强引用 (每个框架都用
self._cb或_CB保活)。 - 退出时先
SetOnDataCallback(NULL)再Stop+Cleanup,否则 DLL 线程可能回调已释放对象。