十种不同风格的 CTP接口 框架

下面给你 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 条铁律(务必遵守)

  1. InitEx 必须传 8 个参数 ,license_key 即使空也要传。
  2. GetBricks / GetKLine 必须 restype = c_char_p,否则 64 位指针截断崩溃。
  3. 回调必须保持强引用 (每个框架都用 self._cb 或 _CB 保活)。
  4. 退出时先 SetOnDataCallback(NULL) 再 Stop + Cleanup,否则 DLL 线程可能回调已释放对象。
相关推荐
2601_967980112 小时前
深入理解词向量算法及其在Python中的实践
python·word2vec·glove·fasttext·词向量
129Lab2 小时前
电池循环测试数据自动化分析:用Python+AI工具构建一键式寿命评估流水线
python·数据分析·电池测试·数据自动化·寿命评估
treasure1402 小时前
【复古】浅尝Django前后端不分离
前端·css·python·django·bootstrap·html·web
Dmz丶3 小时前
前端转 AI 100 天 Day 44:前端集成实战——用 React 给 Agent 做个正式界面
python
jay神3 小时前
基于 YOLOv8 的手势识别系统(Web 版)
前端·人工智能·python·深度学习·yolo·计算机视觉·计算机毕业设计
DeepVisionary3 小时前
阿里云上线旗舰多模态新模型,不公布跑分且限时14天退役
python·自动化
东莞市云毅网络有限公司3 小时前
FAQ 覆盖度打分:TF-IDF 与向量嵌入双路召回的 Python 实现
python·数据清洗·rag·企业知识库·文档解析
爱笑的k114 小时前
nano-vllm 源码剖析:千行 Python 手写离线推理引擎的调度、显存与并行时序
开发语言·python·vllm
用户8356290780514 小时前
使用 Python 控制 Excel 工作表的分页位置
后端·python