FIX开发问题整理(Python版),2023-07-01

(2023.07.01 Sat @SZ 颖隆大厦)

由JSON生成FIX message

JSON序列作为用户/开发者提供的信息,成为被转化为FIX message的输入。该过程中建议使用Python pydantic model实现数据校验。

基本步骤如下

  • 提供基础信息,生成JSON序列
  • 通过pydantic model对JSON序列校验
  • 将校验过的JSON序列复制给quickfix中对应的字段

首先由用户提供基础信息,生成JSON序列,该案例中 仅列出部分信息

userinfo = 
{
    "body": {
        "deliver_to_location_id": "XX",
        "sending_time": "20220122-08:18:07.578",
        "cl_ord_id": "TXXX001",
        "side": "B",
        "symbol": "HKD",
        "order_qty": 9981,
        "currency": "EUR",
        "ord_type": "1",
        "transact_time": "20230315-13:36:42.113",
        "trade_date": "20220122",
        "security_id_source": "A",
        "settl_date": "20220826",
        "num_days_interest": 31,
        "security_type": "CHEQUE",
        "product": 4,
        "maturity_date": "20220926",
        "action_type": "N",
        "coupon_day_count": 7,
        "text": "this is a test text",
    }
}

定义pydantic model,包含FIX中的必要字段。

import datetime
from typing  import Union, Optional
from pydantic import BaseModel


class SomeModel(BaseModel):
    cl_ord_id: str  # 11
    currency: str  # 15
    msg_seq_num: int = 1  # 34
    order_qty: Union[float, int]  # 38
    ord_type: str  # 40
    sender_comp_id: str = "Placeholder"  # by default, set to Placeholder# 49
    sending_time: str = datetime.datetime.now().isoformat().replace("-", "").replace("T", "-")[:21]  # set to now string by default, 52
    side: str  # 54
    symbol: str  # 55
    transact_time: str  # 60
    trade_date: str  # 75
    deliver_to_location_id: str = "FX"  # 145
    security_id_source: str  # 22
    num_days_interest: int  # 157, int
    security_type: str  # 167
    product: int  # 460
    maturity_date: str  # 541
    action_type: str  # 6812
    coupon_day_count: int  # 1950
    text: str = ""  # 58

将输入的JSON序列转换为SomeModel对象。

user_info = SomeModel(**userinfo["body"])

此时查看 user_info对象,返回如下

>> user_info
SomeModel(cl_ord_id='TXXX001', currency='EUR', msg_seq_num=1, order_qty=9981.0, ord_type='1', sender_comp_id='Placeholder', sending_time='20220122-08:18:07.578', side='B', symbol='HKD', transact_time='20230315-13:36:42.113', trade_date='20220122', deliver_to_location_id='XX', security_id_source='A', num_days_interest=31, security_type='CHEQUE', product=4, maturity_date='20220926', action_type='N', coupon_day_count=7, text='this is a test text')

考虑到quickfix中的字段命名采用Camel case,仲需要将JSON中 的snake case字段名做变换。

name_mapping = {key: "".join(t.capitalize() for t in key.split("_")) 
for key in SomeModel.__fields__.keys()}

接下来将用户输入的信息传递给FIX对象,采用Python中的quickfix工具包生成FIX message。赋值方式:给quickfix中的对应字段的对象赋值,并将该对象传递给quickfix.Message方法。

import  quickfix as fix
message = fix.Message()
# header initialisation
header = message.getHeader()
header.setField(fix.BeginString(fix.BeginString_FIX44))
header.setField(fix.MsgType(fix.MsgType_NewOrderSingle))  # 39 = D
for u in userinfo.dict().keys():
    if u == "side" or "time" in u.lower():
        continue
    print(u)
    try:
        tmpcmd = f"fix.{name_mapping[u]}(userinfo.{u})"
        print(tmpcmd)
        tmp = eval(tmpcmd)
        message.setField(tmp)
    except:
        print(f"key = {u}")
str(message).replace("\x01", "|")

输出结果如下:

cl_ord_id
fix.ClOrdID(userinfo.cl_ord_id)
currency
fix.Currency(userinfo.currency)
msg_seq_num
fix.MsgSeqNum(userinfo.msg_seq_num)
order_qty
fix.OrderQty(userinfo.order_qty)
ord_type
fix.OrdType(userinfo.ord_type)
sender_comp_id
fix.SenderCompID(userinfo.sender_comp_id)
sending_time
fix.SendingTime(userinfo.sending_time)
-------ERROR------
symbol
fix.Symbol(userinfo.symbol)
-------ERROR------
transact_time
fix.TransactTime(userinfo.transact_time)
-------ERROR------
trade_date
fix.TradeDate(userinfo.trade_date)
deliver_to_location_id
fix.DeliverToLocationID(userinfo.deliver_to_location_id)
security_id_source
fix.SecurityIDSource(userinfo.security_id_source)
num_days_interest
fix.NumDaysInterest(userinfo.num_days_interest)
security_type
fix.SecurityType(userinfo.security_type)
product
fix.Product(userinfo.product)
maturity_date
fix.MaturityDate(userinfo.maturity_date)
action_type
fix.ActionType(userinfo.action_type)
-------ERROR------
coupon_day_count
fix.CouponDayCount(userinfo.coupon_day_count)
-------ERROR------
text
fix.Text(userinfo.text)
'8=FIX.4.4|9=140|35=D|11=TXXX001|15=EUR|22=A|34=1|38=9981|40=1|
49=Placeholder|58=this is a test text|75=20220122|145=XX|
157=31|167=CHEQUE|460=4|541=20220926|10=005|'

(2023.07.02 Sun @SZ 颖隆大厦)
上面的案例只是给出了最简形式的实现 。从运行结果的log中发现有若干error,包括所有和timestamp有关的对象无法赋值,quickfix不包含的方法/对象无法赋值和quickfix.Symbol无法赋值。

下面的代码是对上面代码的细化,针对不同情况使用不同的处理方式。

首先定义不包含在quickfix中的方法和对象,ActionTypeCouponDayCount,是Bloomberg FX系统中使用的对象。

class ActionType(fix.StringField):
    number = 6812
    required = False
    messages = []
    groups = []
 
    def __init__(self, value=None):
        if value is None:
            fix.StringField.__init__(self, self.number)
        else:
            fix.StringField.__init__(self, self.number, value)
 

class CouponDayCount(fix.IntField):
    number = 1950
    required = False
    messages = []
    groups = []
 
    def __init__(self, value=None):
        if value is None:
            fix.IntField.__init__(self, self.number)
        else:
            fix.IntField.__init__(self, self.number, value)

for u in userinfo.dict().keys():
    # for different objects in FIX, diff methods are used to process data
    if name_mapping[u] not in dir(fix):
        # user-defined object
        user_defined_key = name_mapping[u]
        print(f"user defined key: {user_defined_key}")
        class_type = eval(f"{user_defined_key}.__base__.__name__")
        if class_type.lower().startswith("string"):
            tmp = eval(f"{user_defined_key}('{userinfo.dict()[u]}')")
        elif class_type.lower().startswith("int"):
            tmp = eval(f"{user_defined_key}({userinfo.dict()[u]})")
        message.setField(tmp)
        continue
        
    tmp_key = eval(f"fix.{name_mapping[u]}")
    print(f"type(tmp_key) = {type(tmp_key)}")
    if isinstance(tmp_key, str):
        # string-type object
        print(1)
        continue
    tmp_type = tmp_key.__base__.__name__
    if tmp_type in ("UtcTimeStampField"):
        # timestamp object
        tmp_key = eval(f"fix.{name_mapping[u]}()")
        tmp_value = getattr(userinfo, f"{u}")  # eval(f"userinfo.{u}")
        tmp_key.setString(tmp_value)
        header.setField(tmp_key)
    elif tmp_type in ("StringField", "IntField"):
        # string/int object
        tmpcmd = f"fix.{name_mapping[u]}(userinfo.{u})"
        print(tmpcmd)
        tmp = eval(tmpcmd)
        message.setField(tmp)
    elif tmp_type in ("CharField"):
        # char, side
        if u == "side":
            side_mapping = {"B": fix.Side_BUY, "S": fix.Side_SELL}
            tmp = eval(f"fix.Side('{side_mapping[userinfo.dict()[u]]}')")
            message.setField(tmp)

运行结果

>> str(message).replace("\x01", "|")
'8=FIX.4.4|9=209|35=D|52=20220122-08:18:07.578|60=20230315-13:36:42.113|
11=TXXX001|15=EUR|22=A|34=1|38=9981|40=1|49=Placeholder|54=1|
58=this is a test text|75=20220122|145=XX|157=31|167=CHEQUE|460=4|
541=20220926|1950=7|6812=N|10=042|'

对于quickfix中没有的对象,根据对象类型由quickfix.StringField等类继承从而实现定义,对于timestamp类型对象,将该对象赋值给header,而对于side对象,则加入判断并加入引号实现赋值。

在FIX message中加入repeated group

(2023.07.08, 09 Sat, Sun @SZ 汇泰大厦)
生成FIX message过程中不时需要加入group,比如party/counterparty group, note group(Bloomberg-specific)等。

FIX中group信息中可能不只包含一条信息,也就是相同的tag可能会出现多次。在传输group信息到FIX中之前需要先从quickfix.Group继承创建新的group类。

下面案例创建一个计算party个数的group。tag 453用于计算累计有多少个party,而tag 448, 452和447是每个party中对应的信息。

import quickfix as fix

class NoPartyIds(fix.Group):
    number = 453
    required = False
    messages = ['PARTY']
 
    def __init__(self):
        fields = fix.IntArray(3)
        fields[0] = 448
        fields[1] = 452
        fields[2] = 447
        fields[3] = 0
        fix.Group.__init__(self, self.number, 453, fields)

另需要从pydantic.BaseModel中创建一个PartyModel,其中的tag 447是optional,即用户传递值的时候可不对447赋值。

class Parties(BaseModel):
    """there may be more than one parties, which share the same key
    """
    party_id: str  # 448
    party_id_source: Optional[str] = None  # 447
    party_role: int  # 452

同时要在SomeModel中加入Parties,否则数据验证时无法将用户初始化的Party相关数据传入FIX信息。对前面已经定义的SomeModel做继承,加入新的parties变量,默认状态为None

class SomeModel(SomeModel):
    parties: Optional[list[Parties]] = None

用户输入的数据也加入相应字段。

user_msg['body'].update({'parties': [{'party_id': 'p1', 
'party_role': 0}, {'party_id': 'p2', 'party_id_source': 'blp', 
'party_role': 1}]})

并重新进行数据验证

user_info = SomeModel(**userinfo["body"])

也需要重新对name_mapping做定义

name_mapping = {key: "".join(t.capitalize() for t in key.split("_")) 
for key in SomeModel.__fields__.keys()}

接下来在FIX信息的赋值过程中,加入处理group的部分。(代码需debug)

for u in userinfo.dict().keys():
    # for different objects in FIX, diff methods are used to process data
    if name_mapping[u] not in dir(fix):
        # user-defined object
        user_defined_key = name_mapping[u]
        print(f"user defined key: {user_defined_key}")
        class_type = eval(f"{user_defined_key}.__base__.__name__")
        if class_type.lower().startswith("string"):
            tmp = eval(f"{user_defined_key}('{userinfo.dict()[u]}')")
        elif class_type.lower().startswith("int"):
            tmp = eval(f"{user_defined_key}({userinfo.dict()[u]})")
        elif class_type.lower().startswith("basemodel"):
            for i, t in enumerate(userinfo.dict()[u]):
                if u == "parties":
                    grp = NoPartyIds()
                print(f"i = {i}")
                print(f"t = {t}")
                for k in t.keys():
                    if t[k] is None:
                        print(f"{k} has an empty value, simply jump")
                        continue
                    # tmp = f"fix.{name_mapping[k]}('{userinfo.dict()[u][i][k]}')"
                    # print(f"tmp = {tmp}")
                    # eval_command = eval(tmp)
                    # print(f"eval_command: {eval_command}")
                    #class_
                    print(f"k = {k}")
                    class_type1 = eval(f"fix.{name_mapping[k]}.__base__.__name__")
                    print(f"class type 1: {class_type1}")
                    tmp_eval = eval(f"userinfo.dict()['{u}'][{i}]['{k}']")
                    print(f"tmp_eval: {tmp_eval}")
                    if class_type1.lower() in ("stringfield"):
                        t1 = f"fix.{name_mapping[k]}('{tmp_eval}')"
                        print(f"t1 = {t1}")
                        tmp = eval(t1)
                    elif class_type1.lower() in ("intfield"):
                        tmp = eval(f"fix.{name_mapping[k]}({tmp_eval})")
                    grp.setField(tmp)
            message.addGroup(grp)
            print(f"message after adding group: {str(message)}")
            
        message.setField(tmp)
        continue
        
    tmp_key = eval(f"fix.{name_mapping[u]}")
    print(f"type(tmp_key) = {type(tmp_key)}")
    if isinstance(tmp_key, str):
        # string-type object
        print(1)
        continue
    tmp_type = tmp_key.__base__.__name__
    if tmp_type in ("UtcTimeStampField"):
        # timestamp object
        tmp_key = eval(f"fix.{name_mapping[u]}()")
        tmp_value = getattr(userinfo, f"{u}")  # eval(f"userinfo.{u}")
        tmp_key.setString(tmp_value)
        header.setField(tmp_key)
    elif tmp_type in ("StringField", "IntField"):
        # string/int object
        tmpcmd = f"fix.{name_mapping[u]}(userinfo.{u})"
        print(tmpcmd)
        tmp = eval(tmpcmd)
        message.setField(tmp)
    elif tmp_type in ("CharField"):
        # char, side
        if u == "side":
            side_mapping = {"B": fix.Side_BUY, "S": fix.Side_SELL}
            tmp = eval(f"fix.Side('{side_mapping[userinfo.dict()[u]]}')")
            message.setField(tmp)
str(message).replace("\x01", "|")

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容