From 85c5131976584eb2f78ba91bc3a7b03d2bc33195 Mon Sep 17 00:00:00 2001 From: Mike Kipnis Date: Sat, 17 Feb 2024 13:22:41 -0500 Subject: [PATCH 1/3] golang client --- .../components/fix_adapter.go | 360 ++++++++++++++++++ .../components/investors.go | 73 ++++ .../components/market_data.go | 53 +++ .../components/ref_data.go | 9 + .../components/rest_adapter.go | 247 ++++++++++++ .../config/tradeclient.cfg | 11 + MiscClients/golang_rest_service/go.mod | 48 +++ .../investors/create_investor_tokens.py | 24 ++ .../investors/investors.db | Bin 0 -> 12288 bytes .../python_client/test_client.py | 128 +++++++ .../golang_rest_service/rest_to_fix.go | 83 ++++ 11 files changed, 1036 insertions(+) create mode 100644 MiscClients/golang_rest_service/components/fix_adapter.go create mode 100644 MiscClients/golang_rest_service/components/investors.go create mode 100644 MiscClients/golang_rest_service/components/market_data.go create mode 100644 MiscClients/golang_rest_service/components/ref_data.go create mode 100644 MiscClients/golang_rest_service/components/rest_adapter.go create mode 100644 MiscClients/golang_rest_service/config/tradeclient.cfg create mode 100644 MiscClients/golang_rest_service/go.mod create mode 100644 MiscClients/golang_rest_service/investors/create_investor_tokens.py create mode 100644 MiscClients/golang_rest_service/investors/investors.db create mode 100644 MiscClients/golang_rest_service/python_client/test_client.py create mode 100644 MiscClients/golang_rest_service/rest_to_fix.go diff --git a/MiscClients/golang_rest_service/components/fix_adapter.go b/MiscClients/golang_rest_service/components/fix_adapter.go new file mode 100644 index 0000000..46cee93 --- /dev/null +++ b/MiscClients/golang_rest_service/components/fix_adapter.go @@ -0,0 +1,360 @@ +/* + Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ + +package components + +import ( + "encoding/json" + "fmt" + + "github.com/quickfixgo/enum" + "github.com/quickfixgo/field" + fix44incr "github.com/quickfixgo/fix44/marketdataincrementalrefresh" + fix44mdr "github.com/quickfixgo/fix44/marketdatarequest" + fix44mdsfr "github.com/quickfixgo/fix44/marketdatasnapshotfullrefresh" + fix44sl "github.com/quickfixgo/fix44/securitylist" + fix44slr "github.com/quickfixgo/fix44/securitylistrequest" + "github.com/quickfixgo/quickfix" + + _ "github.com/mattn/go-sqlite3" + + "sync" +) + +type FIXTradeClient struct { + market_data_lock sync.Mutex + market_data map[Instrument]*InstrumentMarketData + + instrument_map_lock sync.Mutex + instrument_map map[string]Instrument + + order_status_lock sync.Mutex + investor_order_map map[string][]string + order_execution_report_map map[string][]ExecutionReport + + serviceSessionID quickfix.SessionID + fix_session_password string +} + +func NewFIXTradeClient(password_in string) *FIXTradeClient { + + trade_client := FIXTradeClient{} + + trade_client.market_data = make(map[Instrument]*InstrumentMarketData) + trade_client.instrument_map = make(map[string]Instrument) + trade_client.investor_order_map = make(map[string][]string) + trade_client.order_execution_report_map = make(map[string][]ExecutionReport) + + trade_client.fix_session_password = password_in + + return &trade_client +} + +func (fix_trade_client *FIXTradeClient) getInstrumentMap() map[string]Instrument { + fix_trade_client.instrument_map_lock.Lock() + defer fix_trade_client.instrument_map_lock.Unlock() + return fix_trade_client.instrument_map +} + +func (fix_trade_client *FIXTradeClient) insertInstrument(symbol_field string, instrument Instrument) { + fix_trade_client.instrument_map_lock.Lock() + fix_trade_client.instrument_map[symbol_field] = instrument + fix_trade_client.instrument_map_lock.Unlock() +} + +func (fix_trade_client *FIXTradeClient) insertMarketData(instrument Instrument, instrument_market_data *InstrumentMarketData) { + fix_trade_client.market_data_lock.Lock() + fix_trade_client.market_data[instrument] = instrument_market_data + fix_trade_client.market_data_lock.Unlock() +} + +func (fix_trade_client *FIXTradeClient) hasMarketData(instrument Instrument) bool { + fix_trade_client.market_data_lock.Lock() + _, present := fix_trade_client.market_data[instrument] + fix_trade_client.market_data_lock.Unlock() + return present +} + +func (fix_trade_client *FIXTradeClient) getInstrumentMarketData(instrument Instrument) InstrumentMarketData { + fix_trade_client.market_data_lock.Lock() + defer fix_trade_client.market_data_lock.Unlock() + var market_data_out = NewMarketDataSnapshot() + market_data_out = (*fix_trade_client.market_data[instrument]) + return market_data_out +} + +func (fix_trade_client *FIXTradeClient) UpdateMarketData(instrument Instrument, entry_type enum.MDEntryType, price int, size int) { + fix_trade_client.market_data_lock.Lock() + (*fix_trade_client.market_data[instrument]).InsertMarketDataEntry(entry_type, price, size) + //instrument_market_data.InsertMarketDataEntry(entry_type, price, size) + //fix_trade_client.market_data[instrument] = instrument_market_data + fix_trade_client.market_data_lock.Unlock() +} + +func (e *FIXTradeClient) insertInvestorOrderMap(investor_name string, client_order_id string) { + e.order_status_lock.Lock() + e.investor_order_map[investor_name] = + append(e.investor_order_map[investor_name], client_order_id) + e.order_status_lock.Unlock() +} + +func (fix_trade_client *FIXTradeClient) insertExecutionReport(execution_report ExecutionReport) { + fix_trade_client.order_status_lock.Lock() + fix_trade_client.order_execution_report_map[execution_report.OrderId] = append(fix_trade_client.order_execution_report_map[execution_report.OrderId], execution_report) + fix_trade_client.order_status_lock.Unlock() +} + +func (fix_trade_client *FIXTradeClient) getExecutionReport(order_id string) []ExecutionReport { + fix_trade_client.order_status_lock.Lock() + defer fix_trade_client.order_status_lock.Unlock() + return fix_trade_client.order_execution_report_map[order_id] +} + +// OnCreate implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) OnCreate(sessionID quickfix.SessionID) { +} + +// OnLogon implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) OnLogon(sessionID quickfix.SessionID) { + + var security_req_id field.SecurityReqIDField = field.NewSecurityReqID("SecurityListRequest1") + var security_list_request_type = field.NewSecurityListRequestType(enum.SecurityListRequestType_ALL_SECURITIES) + + var security_list_request = fix44slr.New(security_req_id, security_list_request_type) + fmt.Printf("Security List Request : " + security_list_request.Message.String()) + + fix_trade_client.serviceSessionID = sessionID + quickfix.SendToTarget(security_list_request, sessionID) +} + +// OnLogout implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) OnLogout(sessionID quickfix.SessionID) { + +} + +// FromAdmin implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + return nil +} + +// ToAdmin implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) { + + // Login with username and password + if msg.IsMsgTypeOf(string(enum.MsgType_LOGON)) { + var sender_comp_id field.SenderCompIDField + msg.Header.Get(&sender_comp_id) + msg.Body.Set(field.NewUsername(sender_comp_id.String())) + msg.Body.Set(field.NewPassword(fix_trade_client.fix_session_password)) + } +} + +// ToApp implemented as part of Application interface +func (fix_trade_client *FIXTradeClient) ToApp(msg *quickfix.Message, sessionID quickfix.SessionID) (err error) { + fmt.Printf("Sending: %s", msg.String()) + return +} + +// FromApp implemented as part of Application interface. This is the callback for all Application level messages from the counter party. +func (fix_trade_client *FIXTradeClient) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + + if msg.IsMsgTypeOf(string(enum.MsgType_SECURITY_LIST)) { + + var group = fix44sl.NewNoRelatedSymRepeatingGroup() + msg.Body.GetGroup(group) + + var instruments []Instrument + for symbol_index := 0; symbol_index < group.Len(); symbol_index++ { + + var symbol_field field.SymbolField + var security_exchange field.SecurityExchangeField + group.Get(symbol_index).Get(&symbol_field) + group.Get(symbol_index).Get(&security_exchange) + fmt.Println("Group : " + symbol_field.String() + ":" + security_exchange.String() + "\n") + + var requested_instrument = Instrument{security_exchange.String(), symbol_field.String()} + + fix_trade_client.insertInstrument(symbol_field.String(), requested_instrument) + + instruments = append(instruments, requested_instrument) + + type instrument_type struct { + Type string `json:"type"` + } + + var text field.TextField + group.Get(symbol_index).Get(&text) + + var json_instrument_definition instrument_type + + err := json.Unmarshal([]byte(text.String()), &json_instrument_definition) + + if err != nil { + fmt.Println(err) + } + + fmt.Println("Type: " + json_instrument_definition.Type) + + } + + var md_req_id = field.NewMDReqID("MarketDataRequest1") + var subscription_request_type = field.NewSubscriptionRequestType(enum.SubscriptionRequestType_SNAPSHOT_PLUS_UPDATES) + var market_depth = field.NewMarketDepth(0) + + var market_data_request = fix44mdr.New(md_req_id, subscription_request_type, market_depth) + + var no_md_entry_types = field.NewNoMDEntryTypes(2) + market_data_request.Set(no_md_entry_types) + + var no_md_update_type = field.NewMDUpdateType(enum.MDUpdateType_INCREMENTAL_REFRESH) + market_data_request.Set(no_md_update_type) + + var md_request_group = fix44mdr.NewNoMDEntryTypesRepeatingGroup() + var bids = field.NewMDEntryType(enum.MDEntryType_BID) + md_request_group.Add().Set(bids) + market_data_request.Body.SetGroup(md_request_group) + + var offers = field.NewMDEntryType(enum.MDEntryType_OFFER) + md_request_group.Add().Set(offers) + market_data_request.Body.SetGroup(md_request_group) + + var symbol_group = fix44mdr.NewNoRelatedSymRepeatingGroup() + for symbol_index := 0; symbol_index < len(instruments); symbol_index++ { + + var group = symbol_group.Add() + group.Set(field.NewSymbol(instruments[symbol_index].Symbol)) + group.Set(field.NewSecurityExchange(instruments[symbol_index].SecurityExchange)) + market_data_request.Body.SetGroup(symbol_group) + + } + + quickfix.SendToTarget(market_data_request, sessionID) + } else if msg.IsMsgTypeOf(string(enum.MsgType_MARKET_DATA_SNAPSHOT_FULL_REFRESH)) { + + var symbol_field field.SymbolField + var security_exchange field.SecurityExchangeField + msg.Body.Get(&symbol_field) + msg.Body.Get(&security_exchange) + + fmt.Println("Market Data Full Snapshot Symbol :" + symbol_field.String() + ":" + security_exchange.String()) + + var market_data_instrument = Instrument{security_exchange.String(), symbol_field.String()} + + if fix_trade_client.hasMarketData(market_data_instrument) { + // We already received an incremental refresh, incremental refresh takes the precendence because its published by the matching engine + return + } + + // Processing the data from DataService + var md_entry_type_repeating_group = fix44mdsfr.NewNoMDEntriesRepeatingGroup() + msg.Body.GetGroup(md_entry_type_repeating_group) + + var instrument_market_data = NewMarketDataSnapshot() + + for market_data_entry_index := 0; market_data_entry_index < md_entry_type_repeating_group.Len(); market_data_entry_index++ { + + var entry_type field.MDEntryTypeField + var entry_price field.MDEntryPxField + var entry_size field.MDEntrySizeField + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_type) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_price) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_size) + + instrument_market_data.InsertMarketDataEntry(enum.MDEntryType(entry_type.String()), + int(entry_price.IntPart()), int(entry_size.IntPart())) + } + + fix_trade_client.insertMarketData(market_data_instrument, &instrument_market_data) + + } else if msg.IsMsgTypeOf(string(enum.MsgType_MARKET_DATA_INCREMENTAL_REFRESH)) { + + var md_entry_type_repeating_group = fix44incr.NewNoMDEntriesRepeatingGroup() + msg.Body.GetGroup(md_entry_type_repeating_group) + + //refresh_market_data := make(map[Instrument]InstrumentMarketData) + + for market_data_entry_index := 0; market_data_entry_index < md_entry_type_repeating_group.Len(); market_data_entry_index++ { + + var entry_type field.MDEntryTypeField + var entry_price field.MDEntryPxField + var entry_size field.MDEntrySizeField + var symbol_field field.SymbolField + var security_exchange field.SecurityExchangeField + + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_type) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_price) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&entry_size) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&symbol_field) + md_entry_type_repeating_group.Get(market_data_entry_index).Get(&security_exchange) + + var market_data_instrument = Instrument{security_exchange.String(), symbol_field.String()} + + fix_trade_client.UpdateMarketData(market_data_instrument, enum.MDEntryType(entry_type.String()), int(entry_price.IntPart()), int(entry_size.IntPart())) + } + + } else if msg.IsMsgTypeOf(string(enum.MsgType_EXECUTION_REPORT)) { + + var order_status_field field.OrdStatusField + var symbol_field field.SymbolField + var security_exchange field.SecurityExchangeField + var orig_cl_ord_id field.OrigClOrdIDField + var order_id field.OrderIDField + var side field.SideField + var order_qty field.OrderQtyField + var cum_qty field.CumQtyField + var leaves_qty field.LeavesQtyField + var exec_id field.ExecIDField + var price field.PriceField + var stop_px field.StopPxField + var last_px field.LastPxField + var last_qty field.LastQtyField + var avg_px field.AvgPxField + + msg.Body.Get(&order_status_field) + msg.Body.Get(&symbol_field) + msg.Body.Get(&security_exchange) + msg.Body.Get(&orig_cl_ord_id) + msg.Body.Get(&order_id) + msg.Body.Get(&side) + msg.Body.Get(&order_qty) + msg.Body.Get(&cum_qty) + msg.Body.Get(&leaves_qty) + msg.Body.Get(&exec_id) + msg.Body.Get(&price) + msg.Body.Get(&stop_px) + msg.Body.Get(&last_px) + msg.Body.Get(&last_qty) + msg.Body.Get(&avg_px) + + var instrument_out = Instrument{security_exchange.String(), symbol_field.String()} + var execution_report_out ExecutionReport + + execution_report_out.Instrument = instrument_out + execution_report_out.OrderStatusField = order_status_field.String() + execution_report_out.OrigClOrdId = orig_cl_ord_id.String() + execution_report_out.OrderId = order_id.String() + execution_report_out.Side = side.String() + execution_report_out.OrderQty = int32(order_qty.IntPart()) + execution_report_out.CumQty = int32(cum_qty.IntPart()) + execution_report_out.LeavesQty = int32(leaves_qty.IntPart()) + execution_report_out.ExecId = exec_id.String() + execution_report_out.Price = int32(price.IntPart()) + execution_report_out.StopPx = int32(stop_px.IntPart()) + execution_report_out.LastPx = int32(last_px.IntPart()) + execution_report_out.LastQty = int32(last_qty.IntPart()) + execution_report_out.AvgPx = int32(avg_px.IntPart()) + + fix_trade_client.insertExecutionReport(execution_report_out) + + order_qty_int := int(order_qty.Value().IntPart()) + fmt.Println(order_qty_int) + + fmt.Println("Execution Report : " + + string(order_status_field.Value()) + ":" + + symbol_field.Value() + ":" + security_exchange.Value() + ":" + + orig_cl_ord_id.Value() + ":" + order_id.Value() + ":" + string(side.Value()) + ":" + + string(order_qty_int) + string(int(cum_qty.IntPart()))) + } + return +} diff --git a/MiscClients/golang_rest_service/components/investors.go b/MiscClients/golang_rest_service/components/investors.go new file mode 100644 index 0000000..5ceb4cc --- /dev/null +++ b/MiscClients/golang_rest_service/components/investors.go @@ -0,0 +1,73 @@ +/* + Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ + +package components + +import ( + "database/sql" + "fmt" + "os" + + "github.com/golang-jwt/jwt" +) + +type InvestorCredentials struct { + InvestorSecret string + InvestorName string +} + +func PopulateInvestorCredenital(investors_db string) map[string]InvestorCredentials { + + var investor_credentials_map = make(map[string]InvestorCredentials) + + db, err := sql.Open("sqlite3", investors_db) + + if err != nil { + fmt.Println("Unable to open", investors_db) + os.Exit(0) + } + + row, err := db.Query("SELECT * FROM investor") + defer row.Close() + + if row == nil { + fmt.Println("Investor database is empty ... exiting") + os.Exit(0) + } + + for row.Next() { // Iterate and fetch the records from result cursor + var jwt_token string + var secret string + + row.Scan(&jwt_token, &secret) + + token, err := jwt.Parse(jwt_token, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(secret), nil + }) + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + + data := claims["username"].(string) + name := data + fmt.Println(name) + + var investor_credentials InvestorCredentials + investor_credentials.InvestorSecret = secret + investor_credentials.InvestorName = name + + investor_credentials_map[jwt_token] = investor_credentials + + } else { + fmt.Println(err) + } + + } + + return investor_credentials_map +} diff --git a/MiscClients/golang_rest_service/components/market_data.go b/MiscClients/golang_rest_service/components/market_data.go new file mode 100644 index 0000000..a7dff33 --- /dev/null +++ b/MiscClients/golang_rest_service/components/market_data.go @@ -0,0 +1,53 @@ +/* + Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ + +package components + +import "github.com/quickfixgo/enum" + +type PriceLevel struct { + Price int `json:"price"` + Size int `json:"size"` + Side string `json:"side"` +} + +type InstrumentMarketData struct { + LastTradedPrice int `json:"last_traded_price"` + Volume int `json:"volume"` + OpenPrice int `json:"open_price"` + LowPrice int `json:"low_price"` + HighPrice int `json:"high_price"` + + Bids map[int]int `json:"bids"` + Asks map[int]int `json:"asks"` +} + +func NewMarketDataSnapshot() InstrumentMarketData { + new_instrument_market_data := InstrumentMarketData{ + Bids: make(map[int]int), + Asks: make(map[int]int), + } + + return new_instrument_market_data +} + +func (mds *InstrumentMarketData) InsertMarketDataEntry(entry_type enum.MDEntryType, price int, size int) { + + if entry_type == enum.MDEntryType_BID { + mds.Bids[price] = size + } else if entry_type == enum.MDEntryType_OFFER { + mds.Asks[price] = size + } else if entry_type == enum.MDEntryType_TRADE { + mds.LastTradedPrice = price + } else if entry_type == enum.MDEntryType_OPENING_PRICE { + mds.OpenPrice = price + } else if entry_type == enum.MDEntryType_TRADING_SESSION_LOW_PRICE { + mds.LowPrice = price + } else if entry_type == enum.MDEntryType_TRADING_SESSION_HIGH_PRICE { + mds.HighPrice = price + } else if entry_type == enum.MDEntryType_TRADE_VOLUME { + mds.Volume = size + } + +} diff --git a/MiscClients/golang_rest_service/components/ref_data.go b/MiscClients/golang_rest_service/components/ref_data.go new file mode 100644 index 0000000..926a2e0 --- /dev/null +++ b/MiscClients/golang_rest_service/components/ref_data.go @@ -0,0 +1,9 @@ +/* +Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ +package components + +type Instrument struct { + SecurityExchange string `json:"security_exchange"` + Symbol string `json:"symbol"` +} diff --git a/MiscClients/golang_rest_service/components/rest_adapter.go b/MiscClients/golang_rest_service/components/rest_adapter.go new file mode 100644 index 0000000..a2d2990 --- /dev/null +++ b/MiscClients/golang_rest_service/components/rest_adapter.go @@ -0,0 +1,247 @@ +/* + Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ + +package components + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/quickfixgo/enum" + "github.com/quickfixgo/field" + "github.com/quickfixgo/quickfix" + "github.com/shopspring/decimal" + + "github.com/golang-jwt/jwt" + + fix44nos "github.com/quickfixgo/fix44/newordersingle" + fix44ocr "github.com/quickfixgo/fix44/ordercancelrequest" +) + +type NewOrderSingle struct { + Side string `json:"side"` + OrderQty int32 `json:"order_qty"` + Price int32 `json:"price"` + OrdType string `json:"order_type"` + TimeInForce string `json:"time_in_force"` +} + +type ExecutionReport struct { + Instrument Instrument `json:"instrument"` + OrderStatusField string `json:"order_status_field"` + OrigClOrdId string `json:"orig_cl_ord_id"` + OrderId string `json:"order_id"` + Side string `json:"side"` + OrderQty int32 `json:"order_qty"` + CumQty int32 `json:"cum_qty"` + LeavesQty int32 `json:"leaves_qty"` + ExecId string `json:"exec_id"` + Price int32 `json:"price"` + StopPx int32 `json:"stop_px"` + LastPx int32 `json:"last_px"` + LastQty int32 `json:"last_qty"` + AvgPx int32 `json:"avg_Ppx"` +} + +type InvestorOrderRequest struct { + Instrument Instrument `json:"instrument"` + UserToken string `json:"user_token"` + NewOrderSingle NewOrderSingle `json:"new_order_single"` +} + +type InvestorOrderReply struct { + ClientOrderId string `json:"client_order_id"` +} + +type InvestorOrdersRequest struct { + UserToken string `json:"user_token"` +} + +type InvestorOrdersReply struct { + Orders []string `json:"orders"` +} + +type OrderStatusRequest struct { + UserToken string `json:"user_token"` + Orders []string `json:"orders"` +} + +type OrderStatusRequestReply struct { + OrderStatus map[string][]ExecutionReport `json:"OrderStatus"` +} + +type CancelOrder struct { + Side string `json:"side"` + ClntOrdId string `json:"client_order_id"` +} + +type InvestorOrderCancelRequest struct { + Instrument Instrument `json:"instrument"` + UserToken string `json:"user_token"` + CancelOrder CancelOrder `json:"cancel_order"` +} + +type InvestorCancelOrderReply struct { + ClientCancelOrderId string `json:"client_cancel_order_id"` +} + +func create_order_token(data_out interface{}, secret_key string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "data": data_out, + "timestamp": time.Now().Unix(), + }) + + tokenString, err := token.SignedString([]byte(secret_key)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +var start_time = time.Now().Unix() + +func InstrumentService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient) { + rest_service.GET("/instruments", func(c *gin.Context) { + + jsonString, err := json.Marshal(fix_trade_client.getInstrumentMap()) + fmt.Println(err) + + c.String(200, string(jsonString[:])) + }) +} + +func MarketDataService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient) { + rest_service.POST("/market_data", func(c *gin.Context) { + var instruments []Instrument + c.BindJSON(&instruments) + + //var market_data_out = make(map[instrument]instrument_market_data) + var market_data_out = make(map[string]InstrumentMarketData) + //var market_data_out = make(map[string]instrument_market_data) + + for _, instrument_in := range instruments { + fmt.Println(instrument_in) + var market_data_instrument, _ = json.Marshal(instrument_in) + market_data_out[string(market_data_instrument)] = fix_trade_client.getInstrumentMarketData(instrument_in) + } + + jsonString, err := json.Marshal(market_data_out) + fmt.Println(err) + c.String(200, string(jsonString)) + }) +} + +func SubmitOrderService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient, investors *map[string]InvestorCredentials) { + rest_service.POST("/submit_order", func(c *gin.Context) { + + var investor_order_in InvestorOrderRequest + c.BindJSON(&investor_order_in) + var investor_credentials = (*investors)[investor_order_in.UserToken] + + start_time = start_time + 1 + var client_order_id = investor_credentials.InvestorName + ":" + strconv.Itoa(int(start_time)) + var clordid = field.NewClOrdID(client_order_id) + var side = field.NewSide(enum.Side(investor_order_in.NewOrderSingle.Side)) + var transacttime = field.NewTransactTime(time.Now()) + var ordtype = field.NewOrdType(enum.OrdType(investor_order_in.NewOrderSingle.OrdType)) + + var new_order = fix44nos.New(clordid, side, transacttime, ordtype) + + new_order.Body.Set(field.NewSymbol(investor_order_in.Instrument.Symbol)) + new_order.Body.Set(field.NewSecurityExchange(investor_order_in.Instrument.SecurityExchange)) + new_order.Body.Set(field.NewPrice(decimal.NewFromInt32(investor_order_in.NewOrderSingle.Price), 0)) + new_order.Body.Set(field.NewOrderQty(decimal.NewFromInt32(investor_order_in.NewOrderSingle.OrderQty), 0)) + new_order.Body.Set(field.NewTimeInForce(enum.TimeInForce(investor_order_in.NewOrderSingle.TimeInForce))) + + print("SessionID : " + fix_trade_client.serviceSessionID.String()) + quickfix.SendToTarget(new_order, fix_trade_client.serviceSessionID) + + var order_reply InvestorOrderReply + order_reply.ClientOrderId = client_order_id + + fix_trade_client.insertInvestorOrderMap(investor_credentials.InvestorName, client_order_id) + + var message_out, _ = create_order_token(order_reply, investor_credentials.InvestorSecret) + + c.String(200, string(message_out)) + }) +} + +func CancelOrderService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient, investors *map[string]InvestorCredentials) { + rest_service.POST("/cancel_order", func(c *gin.Context) { + + var investor_order_cancel_request_in InvestorOrderCancelRequest + c.BindJSON(&investor_order_cancel_request_in) + var investor_credentials = (*investors)[investor_order_cancel_request_in.UserToken] + + start_time = start_time + 1 + var cancel_client_order_id = investor_credentials.InvestorName + ":" + strconv.Itoa(int(start_time)) + var orig_clnt_ord_id = field.NewOrigClOrdID(investor_order_cancel_request_in.CancelOrder.ClntOrdId) + var cl_ord_id = field.NewClOrdID(cancel_client_order_id) + var side = field.NewSide(enum.Side(investor_order_cancel_request_in.CancelOrder.Side)) + var transact_time = field.NewTransactTime(time.Now()) + + var cancel_order = fix44ocr.New(orig_clnt_ord_id, cl_ord_id, side, + transact_time) + + cancel_order.Body.Set(field.NewSymbol(investor_order_cancel_request_in.Instrument.Symbol)) + cancel_order.Body.Set(field.NewSecurityExchange(investor_order_cancel_request_in.Instrument.SecurityExchange)) + + quickfix.SendToTarget(cancel_order, fix_trade_client.serviceSessionID) + + var order_cancel_reply InvestorOrderReply + order_cancel_reply.ClientOrderId = cancel_client_order_id + + var message_out, _ = create_order_token(order_cancel_reply, investor_credentials.InvestorSecret) + + c.String(200, string(message_out)) + + }) +} + +func InvestorOrdersService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient, investors *map[string]InvestorCredentials) { + + rest_service.POST("/investor_orders", func(c *gin.Context) { + + var investor_orders_request_in InvestorOrdersRequest + c.BindJSON(&investor_orders_request_in) + var investor_credentials = (*investors)[investor_orders_request_in.UserToken] + + var investor_orders_out InvestorOrdersReply + investor_orders_out.Orders = fix_trade_client.investor_order_map[investor_credentials.InvestorName] + + var message_out, _ = create_order_token(investor_orders_out, investor_credentials.InvestorSecret) + + c.String(200, string(message_out)) + + }) +} + +func InvestorOrderStatusService(rest_service *gin.Engine, fix_trade_client *FIXTradeClient, investors *map[string]InvestorCredentials) { + + rest_service.POST("/order_status_request", func(c *gin.Context) { + + var order_status_request_in OrderStatusRequest + c.BindJSON(&order_status_request_in) + var investor_credentials = (*investors)[order_status_request_in.UserToken] + + var order_status_request_reply_out OrderStatusRequestReply + + order_status_request_reply_out.OrderStatus = make(map[string][]ExecutionReport) + + for _, order := range order_status_request_in.Orders { + order_status_request_reply_out.OrderStatus[order] = fix_trade_client.getExecutionReport(order) + } + + var message_out, _ = create_order_token(order_status_request_reply_out, investor_credentials.InvestorSecret) + + c.String(200, string(message_out)) + + }) +} diff --git a/MiscClients/golang_rest_service/config/tradeclient.cfg b/MiscClients/golang_rest_service/config/tradeclient.cfg new file mode 100644 index 0000000..587b7a5 --- /dev/null +++ b/MiscClients/golang_rest_service/config/tradeclient.cfg @@ -0,0 +1,11 @@ +[DEFAULT] +SocketConnectHost=127.0.0.1 +SocketConnectPort=15001 +HeartBtInt=30 +SenderCompID=CRYPTO_TRADER_1 +TargetCompID=FIX_GWY_1 +ResetOnLogon=Y +FileLogPath=tmp + +[SESSION] +BeginString=FIX.4.4 \ No newline at end of file diff --git a/MiscClients/golang_rest_service/go.mod b/MiscClients/golang_rest_service/go.mod new file mode 100644 index 0000000..17e59bc --- /dev/null +++ b/MiscClients/golang_rest_service/go.mod @@ -0,0 +1,48 @@ +module golang_rest_service + +go 1.21.5 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/mattn/go-sqlite3 v1.14.22 + github.com/quickfixgo/enum v0.1.0 + github.com/quickfixgo/field v0.1.0 + github.com/quickfixgo/fix44 v0.1.0 + github.com/quickfixgo/quickfix v0.9.0 + github.com/shopspring/decimal v1.3.1 +) + +require ( + github.com/armon/go-proxyproto v0.1.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/quickfixgo/tag v0.1.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/MiscClients/golang_rest_service/investors/create_investor_tokens.py b/MiscClients/golang_rest_service/investors/create_investor_tokens.py new file mode 100644 index 0000000..e355377 --- /dev/null +++ b/MiscClients/golang_rest_service/investors/create_investor_tokens.py @@ -0,0 +1,24 @@ + # Copyright (C) 2024 Mike Kipnis - DistributedATS +import jwt +import sqlite3 + +connection = sqlite3.connect("investors.db") + +cursor = connection.cursor() + +cursor.execute("CREATE TABLE IF NOT EXISTS investor (jwt TEXT PRIMARY KEY, secret TEXT)") +connection.commit() + +investors = [ + {'username':'investor_1', 'secret':'secret_1'}, + {'username':'investor_2', 'secret':'secret_2'}, + {'username':'investor_3', 'secret':'secret_3'}, + {'username':'investor_4', 'secret':'secret_4'}, + {'username':'investor_5', 'secret':'secret_5'} +] + +for investor in investors: + secret = investor.pop('secret') + encoded_jwt = jwt.encode(investor, secret, algorithm="HS256") + cursor.execute("REPLACE INTO investor (jwt,secret) VALUES (?,?)", (encoded_jwt,secret)) + connection.commit() \ No newline at end of file diff --git a/MiscClients/golang_rest_service/investors/investors.db b/MiscClients/golang_rest_service/investors/investors.db new file mode 100644 index 0000000000000000000000000000000000000000..94e5178ff0a306002e3bd77708806bcc56397b5b GIT binary patch literal 12288 zcmeI$Pj8xF90zddI&0e?z4bB^j$68Q#Xl5pLyHQctNbz43jqP83ZkGzmxb)KSFm@m z_p<9=&JN2mZ1vojaM|*Ek|#Wm5Ab~X#LMrIx`Gp0ggu?RdPo$JGXzDE4+McA$Z67F zBz@P=WN^5fB-hRSxpstnQyB^d;mXTRPW~u^9?0zL)-#>(e%&IbRD{^f{ydzrd z-CH8C26O9a;m!Ml%y})3B93QTQ81e%HNB=U!s+8&+r3F6ccV>RpWS$3Nq>obLhv~E zC-)iqoeZIY00@8p2!H?xfB*=900@8p2&5|zpB#P(^45wU8I6JS(cw8|&5<343ph_K z4{996jeWIO<13fRP2`}^TN!RW>`TdlJ6V`+Z*4XzD|M|ncK4FMDO5`BAsq@=?oypK zN5;@^&Dl;g8Ox+P5?7L2Sy9S;jcPv?j#@cBPANcl6Ce;KjYcaFmg`%lU!FSrRH8ky zDzXnNOEOt|S!v2XsWYVqW*eXsXHyE$-ULt#x~DD_X58k2Qho<%iZj1-q@U6-#t$ydjYpfj4% zFKq)9;!H{b*hhgueZ*HR)26JU-73&jy*st)rmc70^_sMO-O|ZGZJI)n9&7`UC)oF- z{=Y!53;Zjd%YDneO7~9=Zvz1k009sH0T2KI5C8!X0D*r{AWBL#G=*|~-%>4;SVd`U jyze dict: + response = requests.get(base_url + '/instruments') + return json.loads(response.text) + + +def get_market_data(base_url: str, instrument_list: list) -> dict: + response = requests.post(base_url + '/market_data', json=list(instrument_list)) + response_dict = json.loads(response.text) + return response_dict + + +def submit_order_from_market_data(base_url: str, investor: dict, instrument: dict, side: str, + price: int, order_qty: int): + new_order_single = {} + new_order_single['user_token'] = investor['token'] + new_order_single['instrument'] = instrument + new_order_single['new_order_single'] = {} + new_order_single['new_order_single']['side'] = side + new_order_single['new_order_single']['order_type'] = '1' + new_order_single['new_order_single']['price'] = price + new_order_single['new_order_single']['order_qty'] = order_qty + new_order_single['new_order_single']['time_in_force'] = '0' + response = requests.post(base_url + '/submit_order', json=new_order_single) + response_dict = jwt.decode(response.text, investor['secret'], algorithms="HS256") + + return response_dict + + +def cancel_order(base_url: str, investor: dict, instrument: dict, client_order_id: str): + cancel_order = {} + cancel_order['user_token'] = investor['token'] + cancel_order['instrument'] = instrument + cancel_order['cancel_order'] = {} + cancel_order['cancel_order']['side'] = '1' + cancel_order['cancel_order']['client_order_id'] = client_order_id + response = requests.post(base_url + '/cancel_order', json=cancel_order) + response_dict = jwt.decode(response.text, investor['secret'], algorithms="HS256") + + return response_dict + + +def get_inverstor_orders(base_url: str, investor: dict): + investor_order_request = {} + investor_order_request['user_token'] = investor['token'] + response = requests.post(base_url + '/investor_orders', json=investor_order_request) + response_dict = jwt.decode(response.text, investor['secret'], algorithms="HS256") + return response_dict + + +def get_order_status(base_url: str, investor: dict, orders: list): + order_status_request = {} + order_status_request['user_token'] = investor['token'] + order_status_request['orders'] = orders + response = requests.post(base_url + '/order_status_request', json=order_status_request) + response_dict = jwt.decode(response.text, investor['secret'], algorithms="HS256") + return response_dict + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser( + prog='Basic REST Client', + description='Basic test client for golang RESTAPI to DistributedATS') + + parser.add_argument('-u', '--base_url', help='BASE_URL of RESTAPI', required=True) + args = parser.parse_args() + + base_url = args.base_url + print("BaseURL: " + args.base_url) + + instruments = get_instruments(base_url) + market_data = get_market_data(base_url, instruments.values()) + + for instrument, instrument_market_data in market_data.items(): + print("Instrument:") + print_json(json.loads(instrument)) + print_json(instrument_market_data) + passive_order_response = submit_order_from_market_data(base_url, investor_1, json.loads(instrument), '1', + instrument_market_data['open_price'], 100) + print("Passive Order Response:") + print_json(passive_order_response) + time.sleep(1) + aggressive_order_response = submit_order_from_market_data(base_url, investor_2, json.loads(instrument), '2', + instrument_market_data['open_price'], 50) + print("Aggressive Order Response:") + print_json(aggressive_order_response) + time.sleep(1) + print("Cancelling remainder of the passive order:") + cancel_response = cancel_order(base_url, investor_1, json.loads(instrument), + passive_order_response['data']['client_order_id']) + print("Cancel Order Response:") + print_json(cancel_response) + break + + print("Getting all orders submitted by secret_1") + investor_orders = get_inverstor_orders(base_url, investor_1) + for order in investor_orders['data']['orders']: + print_json(order) + + print("Lets check orders") + order_status = get_order_status(base_url, investor_1, investor_orders['data']['orders']) + print_json(order_status) + + exit(0) diff --git a/MiscClients/golang_rest_service/rest_to_fix.go b/MiscClients/golang_rest_service/rest_to_fix.go new file mode 100644 index 0000000..880374a --- /dev/null +++ b/MiscClients/golang_rest_service/rest_to_fix.go @@ -0,0 +1,83 @@ +/* + Copyright (C) 2024 Mike Kipnis - DistributedATS +*/ + +package main + +import ( + "bytes" + "fmt" + "io" + "os" + + "github.com/gin-gonic/gin" + "github.com/quickfixgo/quickfix" + + _ "github.com/mattn/go-sqlite3" + + components "golang_rest_service/components" + + flag "github.com/spf13/pflag" +) + +func main() { + + var cfgFileName string + + var rest_port_number *int = flag.Int("rest_port_number", 28100, "REST Service Port Number") + var quickfix_config *string = flag.String("quickfix_config", "config/tradeclient.cfg", "QuickFIX Config") + var investors_db *string = flag.String("investor_db", "investors/investors.db", "Investors DB") + + flag.Parse() + + var investors = components.PopulateInvestorCredenital(*investors_db) + + cfg, err := os.Open(*quickfix_config) + if err != nil { + fmt.Printf("error opening %v, %v", cfgFileName, err) + os.Exit(0) + } + defer cfg.Close() + + stringData, readErr := io.ReadAll(cfg) + if readErr != nil { + fmt.Printf("error reading cfg: %s,", readErr) + } + + fmt.Printf("String Data: %s", stringData) + appSettings, err := quickfix.ParseSettings(bytes.NewReader(stringData)) + if err != nil { + fmt.Printf("error reading cfg: %s,", err) + } + + fixTradeClient := components.NewFIXTradeClient("TEST") + fileLogFactory, err := quickfix.NewFileLogFactory(appSettings) + + if err != nil { + fmt.Printf("error creating file log factory: %s,", err) + } + + initiator, err := quickfix.NewInitiator(fixTradeClient, quickfix.NewMemoryStoreFactory(), appSettings, fileLogFactory) + if err != nil { + fmt.Printf("unable to create initiator: %s", err) + } + + err = initiator.Start() + if err != nil { + fmt.Printf("unable to start initiator: %s", err) + } + + rest_service := gin.Default() + + components.InstrumentService(rest_service, fixTradeClient) + components.MarketDataService(rest_service, fixTradeClient) + components.SubmitOrderService(rest_service, fixTradeClient, &investors) + components.CancelOrderService(rest_service, fixTradeClient, &investors) + components.InvestorOrdersService(rest_service, fixTradeClient, &investors) + components.InvestorOrderStatusService(rest_service, fixTradeClient, &investors) + + rest_run_port := fmt.Sprintf(":%d", *rest_port_number) + + rest_service.Run(rest_run_port) + +} From b2fc43f8d63adcb10d71b423f94779d6e339a5dc Mon Sep 17 00:00:00 2001 From: Mike Kipnis Date: Sat, 17 Feb 2024 13:39:43 -0500 Subject: [PATCH 2/3] golang client --- MiscClients/golang_rest_service/go.sum | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 MiscClients/golang_rest_service/go.sum diff --git a/MiscClients/golang_rest_service/go.sum b/MiscClients/golang_rest_service/go.sum new file mode 100644 index 0000000..d8c152d --- /dev/null +++ b/MiscClients/golang_rest_service/go.sum @@ -0,0 +1,125 @@ +github.com/armon/go-proxyproto v0.1.0 h1:TWWcSsjco7o2itn6r25/5AqKBiWmsiuzsUDLT/MTl7k= +github.com/armon/go-proxyproto v0.1.0/go.mod h1:Xj90dce2VKbHzRAeiVQAMBtj4M5oidoXJ8lmgyW21mw= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quickfixgo/enum v0.1.0 h1:TnCPOqxAWA5/IWp7lsvj97x7oyuHYgj3STBJlBzZGjM= +github.com/quickfixgo/enum v0.1.0/go.mod h1:65gdG2/8vr6uOYcjZBObVHMuTEYc5rr/+aKVWTrFIrQ= +github.com/quickfixgo/field v0.1.0 h1:JVO6fVD6Nkyy8e/ROYQtV/nQhMX/BStD5Lq7XIgYz2g= +github.com/quickfixgo/field v0.1.0/go.mod h1:Zu0qYmpj+gljlB2HgpUt9EcTIThs2lIQb8C57qbJr8o= +github.com/quickfixgo/fix44 v0.1.0 h1:g/rTl6mXDlG7iIMbY7zaPbHcj9N/B+tteOZ01yGzeSQ= +github.com/quickfixgo/fix44 v0.1.0/go.mod h1:d6Ia02Eq/JYgKCn/2V9FHxguAl1Alp/yu/xVpry82dA= +github.com/quickfixgo/quickfix v0.9.0 h1:WshR3GUSxR69ZrSQfppKs2zZ12dTYtU3JUgQg+PAOdA= +github.com/quickfixgo/quickfix v0.9.0/go.mod h1:t5Z881dOZ2Dz5vM6KIbMCx3YpAiFPFf/iCLCSn91Qqo= +github.com/quickfixgo/tag v0.1.0 h1:R2A1Zf7CBE903+mOQlmTlfTmNZQz/yh7HunMbgcsqsA= +github.com/quickfixgo/tag v0.1.0/go.mod h1:l/drB1eO3PwN9JQTDC9Vt2EqOcaXk3kGJ+eeCQljvAI= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 246b9cc25fd9626a73792a77306c230c059f505b Mon Sep 17 00:00:00 2001 From: Mike Kipnis Date: Sat, 17 Feb 2024 19:52:33 +0000 Subject: [PATCH 3/3] golang webservice --- DataService/sql/sqlite/distributed_ats.db | Bin 3248128 -> 3248128 bytes .../components/fix_adapter.go | 12 +++--------- .../golang_rest_service/python_client/README | 1 + .../python_client/test_client.py | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) create mode 100644 MiscClients/golang_rest_service/python_client/README diff --git a/DataService/sql/sqlite/distributed_ats.db b/DataService/sql/sqlite/distributed_ats.db index 5da0cdc6b199116d90ea31c65dc4056788626f68..b1db69644d658145e64d1b820da87680c605b003 100644 GIT binary patch delta 6894 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 CI{P31 delta 2839 zcmb7Fdu$ZP8QW~! z4;nBEQ3&z8JBiD6Kj_miAdgj|vQrI7TrB?~mHGK>kKI_cdkXnkW&=vb0-D5w{2(T$2#T z@8vTsA6RnmE}X5LRqEw?^6SCEAsT%?AoR24dkYXtc>`Os?Z9GOHCV1aaw-y z>X0h7#G0L+jl2hn)XIuuwClzG&;Z>Mqh-%{e!^~zhEOksZAU0<{xcI{=~gwwkA!&; z&m30U;uMn;LCEkH#u~Ju-WR4Zs>zLfwTo)%s~hWUz5ZFXjZ>qa);x} zx~d?=sqQSWd%KoH?+89_ldXI+o`+NL9o!#@^#IY?yyonT4_%pc!+6<$q|!C_(EVnS|oCJJ84N9Cf2u?R%r~TFD&&#en5P(K|=uG>DBH zK;w*v$R4sF4pM{nMjglt8xw#<%k+ z7Tr?Ejd9z!$>s~@M)q5lupZ_bvn*I~|09#W?WB8+5M2LwGpdG&iNaP2Uxn>`85d)- za$V_Gn&I;$$+zUg@>D zLavmV3yU0kWYccSMlGb^Ac{??^bxuM^&>xZm0BnlN#~@w;&pL}a93ExkMmn1jZ7^^ zE@pLCfmvT%j$UD*fMX`p>aLlj>$RMfWZ*H+>f>|J=LocEWUM8u1+D*h0YO1%D=7=Q zml#$C>6);pi{#dMcNIC{QZQQtMgNGDTnKY={)GFty*21>NhDXX+#=UjA~%h9k#Tqu zAGQR@p(e0~u*I%_*@Ql{kRz2*Uj6Pml*Vyi<2zzMzNFg%s7plG^%L9Bw{)AjUNR-n zb=C88XEoHSKQ+jLdS9;3>sMbf$oz(?5ZM?aFfzAhk@_=(z&9(mq0X;1MTjx7CPHRc z)Tlp+5F>PRg!rl!sVx!GW-PQi!n_TBb(KN<)%CeXgmo5^O2b`nY!9j4IiVFd7^=_b zbNt*;;n+j!ixFbvu`)u8C@)8dk;h9BV&t)+?g@iDmPd$@$FdOdRpuIgoABQ$zM`T+ z-DRlW?=Q?XTv|iK?+q=#7LJPddcA5#?_21Sh4lCFUfr?>y@^S%Pi`bvPRVKQ(LD%2 z155x0zyizw4q(xvd-%V8+$MBTNlXbPEEZh+J^lp0p7-;T<+|l<%k!2T?jd)Yd!3ul z+0BFIcg)MpQ`vF$BKsy=&n7Z|X5M33n3Dg$i7$!;!Z*S>VXIK12bzV`CT3bs8AHlj zgr%f;m0Q*`(h*^}X+17xn$trHRvZD#_4&ICkL z0Xq)=m%X}lOdR9by|6_Ow)BZx`lKy=a&Mo)RGJaKP2t-xEA*Sz>Gs;MNK8W01B1;hd30SSOaKoTGskOD{rIQ83jQdYpm@oOjrOUXhm|1bViem}nkmTTlq z?sxHSmy$rn_Dr(1AJ{VqkOpu8(g7LzfjyHK-;7f*#WK%P#(YbOc^>l|kaf^(8ZcR51CSE8{y8m%OJ_i$ke!hGARwZt4Zbsx2=8cc{d;j QkPFBI