// Create a FIXClient with common routes for market connectors func NewFIXClient(c common.Config) *FIXClient { app := &FIXClient{ MessageRouter: quickfix.NewMessageRouter(), marketConnectorName: c.MarketConnectorName, } // Initiate message bus listening for requests if msgbus, err := messagebus.Connect(c.MessageBusURL); err != nil { log.Fatalf("error: Cannot connect to order message bus @ %v", c.MessageBusURL) } else { app.MessageBus = msgbus } // Connect to database storage driver if storage, err := database.NewOrderStore(c.DatabaseDriver, c.DatabaseUrl, nil); err != nil { log.Fatalf("error: Cannot connect to database driver %v @ %v", c.DatabaseDriver, c.DatabaseUrl) } else { app.OrderStore = storage } // QuickFIX settings from config appSettings := quickfix.NewSettings() var settings *quickfix.SessionSettings settings = appSettings.GlobalSettings() settings.Set("SocketConnectHost", "127.0.0.1") settings.Set("SocketConnectPort", "5001") settings.Set("HeartBtInt", "30") settings.Set("SenderCompID", "CYAN") settings.Set("TargetCompID", "CORP") settings.Set("ResetOnLogon", "Y") settings.Set("FileLogPath", "tmp") settings = quickfix.NewSessionSettings() settings.Set("BeginString", "FIX.4.4") if session, err := appSettings.AddSession(settings); err != nil { log.WithError(err).Fatal("FIX Session Error") } else { app.Session = session } // FIX routes app.AddRoute(fix44er.Route(app.onFIX44ExecutionReport)) app.AddRoute(fix44ocj.Route(app.onFIX44OrderCancelReject)) // FIX logging logFactory := quickfix.NewNullLogFactory() // quickfix.NewScreenLogFactory() // Create initiator if initiator, err := quickfix.NewInitiator(app, quickfix.NewMemoryStoreFactory(), appSettings, logFactory); err != nil { log.WithError(err).Fatal("FIX NewInitiator Error") } else { app.Initiator = initiator } return app }
// Common Order FIX client routines serving requests from order router // if a market connector has special case it will need to implement its own start routine like below func (app FIXClient) Start() error { err := app.Initiator.Start() // Subscribe to order flow topics // NEW app.MessageBus.Subscribe("order.NewOrderRequest.MC."+app.marketConnectorName, func(m *messagebus.Msg) { request := new(proto.NewOrderRequest) if err := request.Unmarshal(m.Data); err == nil { order := request.Order //TODO: this is only prototype, migrate common tasks: instruments / limits processing fixmsg := fix44nos.Message{ ClOrdID: strconv.Itoa(int(order.OrderKey)) + "." + strconv.Itoa(int(order.Version)), Side: util.ProtoEnumToFIXEnum(int(order.Side)), TransactTime: time.Now(), OrdType: util.ProtoEnumToFIXEnum(int(order.OrderType)), } // Instrument specific fixmsg.Instrument.Symbol = &order.Symbol fixmsg.OrderQty = &order.Quantity if order.OrderType == proto.OrderType_LIMIT || order.OrderType == proto.OrderType_LIMIT_ON_CLOSE { fixmsg.Price = &order.LimitPrice } // Broker specific fixmsg.Account = &order.BrokerAccount fixmsg.HandlInst = stringPtr(util.ProtoEnumToFIXEnum(int(order.HandleInst))) // 142 SenderLocationID // Mandatory for CME exchanges. It contains a 2-character country. For the US and Canada, the state/province is included. fixmsg.SenderLocationID = stringPtr("UK") log.Info("MC->FIX FIX44NewOrderSingle") if err := quickfix.SendToTarget(fixmsg, app.Session); err != nil { log.WithError(err).Fatal("FIX quickfix.SendToTarget Error") } } }) // TODO: CANCEL // TODO: REPLACE return err }
// Common behaviours to persist and publish populated Execution entity into our data layer and message bus func (app FIXClient) processExecutionReport(er *proto.Execution) quickfix.MessageRejectError { // Find the Order by using the OrderKey without the Version // Need to try to LOAD the order, if that fails then alert stating unsolicited order received keyVersion := strings.Split(er.ClientOrderId, ".") if len(keyVersion) != 2 { return quickfix.ValueIsIncorrect(tag.ClOrdID) } if oKey, err := strconv.Atoi(keyVersion[0]); err != nil { return quickfix.ValueIsIncorrect(tag.ClOrdID) } else { if ord, err := app.OrderStore.OrderGetByOrderKey(int32(oKey)); err != nil { log.WithError(err).Warnf("Received ExecutionReport with OrderKey(%v) does not exist.", oKey) return nil // TODO: ignore or quickfix.ValueIsIncorrect(tag.ClOrdID) } else { er.OrderId = ord.OrderId } } // Persist the execution report if err := app.OrderStore.ExecutionCreate(er); err == nil { //publish to message bus data, _ := er.Marshal() app.MessageBus.Publish("order.Execution", data) } else { log.WithField("execution", er).WithError(err).Error("[ MC ] ERROR Create Reject Execution") } // Do any validating and if we are good to continue // Check if we need to cancel any execution // Check if we need to correct any execution switch er.ExecType { case proto.Execution_TRADE_CANCEL: case proto.Execution_TRADE_CORRECT: //TODO: TryGetAmendedExecution case proto.Execution_RESTATED: case proto.Execution_REJECTED: } // Check if the trade is fully filled or is done for day or canceled // .. Send message to TradePump to say trade filled and already to book. return nil }
func (m *MCSimulator) Start() { if err := m.app.Start(); err != nil { log.WithError(err).Fatal("FIX Client Start Error") } }
// start the logic spinning code for OrderRouter, using a single for..select.. pattern so there is no need to lock resources // hence to avoid synchronisation issues func (self *OrderRouter) Start() { go func() { for { select { case <-self.stopChan: self.msgbus.Close() self.msgbusService.Close() return // update known market connector list based on their heartbeat status case hbMsg := <-self.mcChan: svcName := strings.Replace(hbMsg.Name, "MC.", "", 1) if hbMsg.Status == service.RUNNING { self.mclist[svcName] = time.Now() } else { delete(self.mclist, svcName) } // remove market connector if we have not heard from it for a while case <-time.After(6 * time.Second): for name, lastHb := range self.mclist { if time.Since(lastHb).Seconds() > 6 { delete(self.mclist, name) } } case req := <-self.reqChan: func() { // wrap in func for defer reply requestError := "" var order *proto.Order // make sure reply message sent in the end defer func() { var data []byte errCode := int32(0) if requestError != "" { errCode = int32(-1) } switch req.RequestType { case REQ_NEW: resp := &proto.NewOrderResponse{ ErrorCode: errCode, ErrorMessage: &requestError, Order: order, } data, _ = resp.Marshal() case REQ_CANCEL: resp := &proto.CancelOrderResponse{ ErrorCode: errCode, ErrorMessage: &requestError, Order: order, } data, _ = resp.Marshal() case REQ_REPLACE: resp := &proto.ReplaceOrderResponse{ ErrorCode: errCode, ErrorMessage: &requestError, Order: order, } data, _ = resp.Marshal() } self.msgbus.Publish(req.ReplyAddr, data) }() // Prepare order if req.RequestType == REQ_CANCEL || req.RequestType == REQ_REPLACE { // retrieve previous order for state check if prev_order, err := self.orderStore.OrderGet(req.OrderId); err != nil { requestError = "Order does not exists" return } else { // rule.1: working orders if prev_order.MessageType == proto.Order_NEW { if !(prev_order.OrderStatus == proto.OrderStatus_PARTIALLY_FILLED || prev_order.OrderStatus == proto.OrderStatus_NEW) { requestError = fmt.Sprintf("Disallowed due to order status: %s", prev_order.OrderStatus) return } } // rule.2: pending cancel rejected if prev_order.MessageType == proto.Order_CANCEL { if !(prev_order.OrderStatus == proto.OrderStatus_REJECTED) { requestError = "Pending cancel on order" return } } // rule.2: pending replace acked by broker if prev_order.MessageType == proto.Order_REPLACE { if !(prev_order.OrderStatus == proto.OrderStatus_NEW || prev_order.OrderStatus == proto.OrderStatus_REPLACED || prev_order.OrderStatus == proto.OrderStatus_REJECTED) { requestError = "Pending replace on order" return } } // prepare for new order entry if req.RequestType == REQ_CANCEL { pReq := req.Request.(*proto.CancelOrderRequest) order = prev_order order.OrderId = 0 // wipe order id for new id order.Version++ // bump up order key version order.SubmitDatetime = time.Now().UTC().Format(time.RFC3339Nano) // timing this cancel order.Trader = pReq.Trader // trader who tries to cancel order.TraderId = pReq.TraderId // trader who tries to cancel order.Source = pReq.Source // source of cancel order.MessageType = proto.Order_CANCEL order.OrderStatus = proto.OrderStatus_CANCEL_RECEIVED } if req.RequestType == REQ_REPLACE { pReq := req.Request.(*proto.ReplaceOrderRequest) pReq.Order.OrderKey = prev_order.OrderKey order := pReq.Order order.OrderId = 0 // wipe order id for new id order.OrderKey = prev_order.OrderKey // in case client failed to provide correct order key order.Version = prev_order.Version + 1 // bump up order key version order.SubmitDatetime = time.Now().UTC().Format(time.RFC3339Nano) // timing this replace order.Trader = pReq.Trader // trader who tries to replace order.TraderId = pReq.TraderId // trader who tries to replace order.MessageType = proto.Order_REPLACE order.OrderStatus = proto.OrderStatus_REPLACE_RECEIVED } //TODO: check ClientGUID consistency } } else { // New order pReq := req.Request.(*proto.NewOrderRequest) order = pReq.Order order.OrderId = 0 order.Version = 0 } // Persist order entry if err := self.orderStore.OrderCreate(order); err != nil { requestError = fmt.Sprint(err) log.WithError(err).Error("[ OR ] ERROR Create order") return } //TODO: allocations // Check target market connector is up if _, ok := self.mclist[order.MarketConnector]; ok == false { log.Warnf("OR->CL REJECT OrderKey: %v MC: %v : %v", order.OrderKey, order.MarketConnector, "LINK TO BROKER DOWN") // REJECT due to market connector down requestError = "LINK TO BROKER DOWN" // create Reject execution self.reject(order, "LINK TO BROKER DOWN") } else { log.WithField("order", order).Infof("OR->MC OrderKey: %v", order.OrderKey) // relay order with idents to its market connector // todo: special case on MC.AlgoAggregator var data []byte switch req.RequestType { case REQ_NEW: request := req.Request.(*proto.NewOrderRequest) data, _ = request.Marshal() case REQ_CANCEL: request := req.Request.(*proto.CancelOrderRequest) data, _ = request.Marshal() case REQ_REPLACE: request := req.Request.(*proto.ReplaceOrderRequest) data, _ = request.Marshal() } self.msgbus.Publish("order.NewOrderRequest.MC."+order.MarketConnector, data) } }() } } }() }