adding project logic
This commit is contained in:
@ -29,23 +29,27 @@ const (
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
pool *redis.Pool
|
||||
userData app.UserDataProvider
|
||||
store *store.Store
|
||||
config Config
|
||||
notify domain.Notifier
|
||||
authMutex deadlock.Mutex
|
||||
pool *redis.Pool
|
||||
userData app.UserDataProvider
|
||||
store *store.Store
|
||||
orderStore domain.OrderStore
|
||||
fixSender domain.FIXSender
|
||||
config Config
|
||||
notify domain.Notifier
|
||||
authMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
func newController(pool *redis.Pool, userData app.UserDataProvider,
|
||||
s *store.Store, config Config, n domain.Notifier,
|
||||
s *store.Store, orderStore domain.OrderStore, fixSender domain.FIXSender, config Config, n domain.Notifier,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
pool: pool,
|
||||
userData: userData,
|
||||
store: s,
|
||||
config: config,
|
||||
notify: n,
|
||||
pool: pool,
|
||||
userData: userData,
|
||||
store: s,
|
||||
orderStore: orderStore,
|
||||
fixSender: fixSender,
|
||||
config: config,
|
||||
notify: n,
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,3 +292,51 @@ func allowed(origin string, config Config) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetOrders godoc
|
||||
// @Summary List received FIX orders
|
||||
// @Description Returns all NewOrderSingle messages received via FIX
|
||||
// @Tags fix
|
||||
// @Produce json
|
||||
// @Success 200 {array} domain.Order
|
||||
// @Router /qfixdpl/v1/orders [get]
|
||||
func (cont *Controller) GetOrders(ctx *gin.Context) {
|
||||
orders := cont.orderStore.GetOrders()
|
||||
ctx.JSON(http.StatusOK, orders)
|
||||
}
|
||||
|
||||
// SendQuote godoc
|
||||
// @Summary Send a FIX Quote
|
||||
// @Description Sends a Quote (MsgType S) back to the FIX client for a given order
|
||||
// @Tags fix
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param quote body QuoteRequest true "Quote details"
|
||||
// @Success 200 {object} Msg
|
||||
// @Failure 400 {object} HTTPError
|
||||
// @Failure 404 {object} HTTPError
|
||||
// @Failure 500 {object} HTTPError
|
||||
// @Router /qfixdpl/v1/quotes [post]
|
||||
func (cont *Controller) SendQuote(ctx *gin.Context) {
|
||||
var req QuoteRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
bidPx, offerPx, bidSize, offerSize, err := req.toDecimals()
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = cont.fixSender.SendQuote(req.ClOrdID, req.QuoteID, req.Symbol, req.Currency, bidPx, offerPx, bidSize, offerSize); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, Msg{Text: "quote sent"})
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type HTTPError struct {
|
||||
Error string
|
||||
}
|
||||
@ -16,3 +22,42 @@ type Credentials struct {
|
||||
type Session struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
type QuoteRequest struct {
|
||||
ClOrdID string `json:"cl_ord_id" binding:"required"`
|
||||
QuoteID string `json:"quote_id" binding:"required"`
|
||||
Symbol string `json:"symbol" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
BidPx string `json:"bid_px" binding:"required"`
|
||||
OfferPx string `json:"offer_px" binding:"required"`
|
||||
BidSize string `json:"bid_size"`
|
||||
OfferSize string `json:"offer_size"`
|
||||
}
|
||||
|
||||
func (r QuoteRequest) toDecimals() (bidPx, offerPx, bidSize, offerSize decimal.Decimal, err error) {
|
||||
bidPx, err = decimal.NewFromString(r.BidPx)
|
||||
if err != nil {
|
||||
return bidPx, offerPx, bidSize, offerSize, fmt.Errorf("invalid bid_px: %w", err)
|
||||
}
|
||||
|
||||
offerPx, err = decimal.NewFromString(r.OfferPx)
|
||||
if err != nil {
|
||||
return bidPx, offerPx, bidSize, offerSize, fmt.Errorf("invalid offer_px: %w", err)
|
||||
}
|
||||
|
||||
if r.BidSize != "" {
|
||||
bidSize, err = decimal.NewFromString(r.BidSize)
|
||||
if err != nil {
|
||||
return bidPx, offerPx, bidSize, offerSize, fmt.Errorf("invalid bid_size: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.OfferSize != "" {
|
||||
offerSize, err = decimal.NewFromString(r.OfferSize)
|
||||
if err != nil {
|
||||
return bidPx, offerPx, bidSize, offerSize, fmt.Errorf("invalid offer_size: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return bidPx, offerPx, bidSize, offerSize, nil
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ func SetRoutes(api *API) {
|
||||
qfixdpl := v1.Group("/")
|
||||
qfixdpl.Use(cont.AuthRequired)
|
||||
qfixdpl.GET("/health", cont.HealthCheck)
|
||||
qfixdpl.GET("/orders", cont.GetOrders)
|
||||
qfixdpl.POST("/quotes", cont.SendQuote)
|
||||
|
||||
backoffice := qfixdpl.Group("/backoffice")
|
||||
backoffice.Use(cont.BackOfficeUser)
|
||||
|
||||
@ -32,7 +32,7 @@ type Config struct {
|
||||
EnableJWTAuth bool
|
||||
}
|
||||
|
||||
func New(userData app.UserDataProvider, storeInstance *store.Store, config Config, notify domain.Notifier) *API {
|
||||
func New(userData app.UserDataProvider, storeInstance *store.Store, orderStore domain.OrderStore, fixSender domain.FIXSender, config Config, notify domain.Notifier) *API {
|
||||
// Set up Gin
|
||||
var engine *gin.Engine
|
||||
if version.Environment() == version.EnvironmentTypeProd {
|
||||
@ -58,7 +58,7 @@ func New(userData app.UserDataProvider, storeInstance *store.Store, config Confi
|
||||
}
|
||||
|
||||
api := &API{
|
||||
Controller: newController(NewPool(), userData, storeInstance, config, notify),
|
||||
Controller: newController(NewPool(), userData, storeInstance, orderStore, fixSender, config, notify),
|
||||
Router: engine,
|
||||
Port: config.Port,
|
||||
}
|
||||
|
||||
41
src/client/data/orders.go
Normal file
41
src/client/data/orders.go
Normal file
@ -0,0 +1,41 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"quantex.com/qfixdpl/src/domain"
|
||||
)
|
||||
|
||||
type InMemoryOrderStore struct {
|
||||
mu sync.RWMutex
|
||||
orders []domain.Order
|
||||
}
|
||||
|
||||
func NewOrderStore() *InMemoryOrderStore {
|
||||
return &InMemoryOrderStore{}
|
||||
}
|
||||
|
||||
func (s *InMemoryOrderStore) SaveOrder(order domain.Order) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.orders = append(s.orders, order)
|
||||
}
|
||||
|
||||
func (s *InMemoryOrderStore) GetOrders() []domain.Order {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
result := make([]domain.Order, len(s.orders))
|
||||
copy(result, s.orders)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *InMemoryOrderStore) GetOrderByClOrdID(id string) (domain.Order, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, o := range s.orders {
|
||||
if o.ClOrdID == id {
|
||||
return o, true
|
||||
}
|
||||
}
|
||||
return domain.Order{}, false
|
||||
}
|
||||
105
src/client/fix/application.go
Normal file
105
src/client/fix/application.go
Normal file
@ -0,0 +1,105 @@
|
||||
// Package fix implements the QuickFIX acceptor application.
|
||||
package fix
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/newordersingle"
|
||||
"quantex.com/qfixdpl/src/domain"
|
||||
)
|
||||
|
||||
type application struct {
|
||||
router *quickfix.MessageRouter
|
||||
orderStore domain.OrderStore
|
||||
onLogon func(quickfix.SessionID)
|
||||
onLogout func(quickfix.SessionID)
|
||||
}
|
||||
|
||||
func newApplication(orderStore domain.OrderStore) *application {
|
||||
app := &application{
|
||||
router: quickfix.NewMessageRouter(),
|
||||
orderStore: orderStore,
|
||||
}
|
||||
|
||||
app.router.AddRoute(newordersingle.Route(app.handleNewOrderSingle))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *application) OnCreate(sessionID quickfix.SessionID) {
|
||||
slog.Info("FIX session created", "session", sessionID.String())
|
||||
}
|
||||
|
||||
func (a *application) OnLogon(sessionID quickfix.SessionID) {
|
||||
slog.Info("FIX client logged on", "session", sessionID.String())
|
||||
if a.onLogon != nil {
|
||||
a.onLogon(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) OnLogout(sessionID quickfix.SessionID) {
|
||||
slog.Info("FIX client logged out", "session", sessionID.String())
|
||||
if a.onLogout != nil {
|
||||
a.onLogout(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) ToAdmin(_ *quickfix.Message, _ quickfix.SessionID) {}
|
||||
|
||||
func (a *application) ToApp(_ *quickfix.Message, _ quickfix.SessionID) error { return nil }
|
||||
|
||||
func (a *application) FromAdmin(_ *quickfix.Message, _ quickfix.SessionID) quickfix.MessageRejectError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *application) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
|
||||
return a.router.Route(msg, sessionID)
|
||||
}
|
||||
|
||||
func (a *application) handleNewOrderSingle(msg newordersingle.NewOrderSingle, sessionID quickfix.SessionID) quickfix.MessageRejectError {
|
||||
clOrdID, err := msg.GetClOrdID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
symbol, err := msg.GetSymbol()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
side, err := msg.GetSide()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ordType, err := msg.GetOrdType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orderQty, err := msg.GetOrderQty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
price, _ := msg.GetPrice() // Price is optional for some OrdTypes
|
||||
|
||||
order := domain.Order{
|
||||
ClOrdID: clOrdID,
|
||||
Symbol: symbol,
|
||||
Side: string(side),
|
||||
OrdType: string(ordType),
|
||||
OrderQty: orderQty,
|
||||
Price: price,
|
||||
SessionID: sessionID.String(),
|
||||
ReceivedAt: time.Now(),
|
||||
}
|
||||
|
||||
a.orderStore.SaveOrder(order)
|
||||
|
||||
slog.Info("NewOrderSingle received", "clOrdID", clOrdID, "symbol", symbol, "side", order.Side)
|
||||
|
||||
return nil
|
||||
}
|
||||
141
src/client/fix/manager.go
Normal file
141
src/client/fix/manager.go
Normal file
@ -0,0 +1,141 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/gen/enum"
|
||||
"quantex.com/qfixdpl/quickfix/gen/field"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote"
|
||||
"quantex.com/qfixdpl/src/app"
|
||||
"quantex.com/qfixdpl/src/domain"
|
||||
)
|
||||
|
||||
// Manager wraps the QuickFIX acceptor and implements domain.FIXSender.
|
||||
type Manager struct {
|
||||
acceptor *quickfix.Acceptor
|
||||
app *application
|
||||
sessionsMu sync.RWMutex
|
||||
sessions map[string]quickfix.SessionID
|
||||
orderStore domain.OrderStore
|
||||
notify domain.Notifier
|
||||
cfg app.FIXConfig
|
||||
}
|
||||
|
||||
func NewManager(cfg app.FIXConfig, orderStore domain.OrderStore, notify domain.Notifier) *Manager {
|
||||
return &Manager{
|
||||
sessions: make(map[string]quickfix.SessionID),
|
||||
orderStore: orderStore,
|
||||
notify: notify,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start() error {
|
||||
fixApp := newApplication(m.orderStore)
|
||||
fixApp.onLogon = m.onLogon
|
||||
fixApp.onLogout = m.onLogout
|
||||
m.app = fixApp
|
||||
|
||||
f, err := os.Open(m.cfg.SettingsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening FIX settings file %q: %w", m.cfg.SettingsFile, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
settings, err := quickfix.ParseSettings(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing FIX settings: %w", err)
|
||||
}
|
||||
|
||||
storeFactory := quickfix.NewMemoryStoreFactory()
|
||||
logFactory := quickfix.NewNullLogFactory()
|
||||
|
||||
acceptor, err := quickfix.NewAcceptor(fixApp, storeFactory, settings, logFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating FIX acceptor: %w", err)
|
||||
}
|
||||
|
||||
m.acceptor = acceptor
|
||||
|
||||
if err = m.acceptor.Start(); err != nil {
|
||||
return fmt.Errorf("starting FIX acceptor: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("FIX acceptor started", "settings", m.cfg.SettingsFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Stop() {
|
||||
if m.acceptor != nil {
|
||||
m.acceptor.Stop()
|
||||
slog.Info("FIX acceptor stopped")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) onLogon(sessionID quickfix.SessionID) {
|
||||
m.sessionsMu.Lock()
|
||||
m.sessions[sessionID.String()] = sessionID
|
||||
m.sessionsMu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) onLogout(sessionID quickfix.SessionID) {
|
||||
m.sessionsMu.Lock()
|
||||
delete(m.sessions, sessionID.String())
|
||||
m.sessionsMu.Unlock()
|
||||
}
|
||||
|
||||
// SendQuote implements domain.FIXSender.
|
||||
func (m *Manager) SendQuote(clOrdID, quoteID, symbol, currency string, bidPx, offerPx, bidSize, offerSize decimal.Decimal) error {
|
||||
order, ok := m.orderStore.GetOrderByClOrdID(clOrdID)
|
||||
if !ok {
|
||||
return fmt.Errorf("order not found: %s", clOrdID)
|
||||
}
|
||||
|
||||
m.sessionsMu.RLock()
|
||||
sessionID, ok := m.sessions[order.SessionID]
|
||||
m.sessionsMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("session not active for order %s (session %s)", clOrdID, order.SessionID)
|
||||
}
|
||||
|
||||
q := quote.New(
|
||||
field.NewQuoteID(quoteID),
|
||||
field.NewQuoteType(enum.QuoteType_INDICATIVE),
|
||||
field.NewTransactTime(time.Now()),
|
||||
)
|
||||
|
||||
q.SetSymbol(symbol)
|
||||
q.SetQuoteID(quoteID)
|
||||
|
||||
if currency != "" {
|
||||
q.SetCurrency(currency)
|
||||
}
|
||||
|
||||
q.SetBidPx(bidPx, 8)
|
||||
q.SetOfferPx(offerPx, 8)
|
||||
|
||||
if !bidSize.IsZero() {
|
||||
q.SetBidSize(bidSize, 8)
|
||||
}
|
||||
|
||||
if !offerSize.IsZero() {
|
||||
q.SetOfferSize(offerSize, 8)
|
||||
}
|
||||
|
||||
if err := quickfix.SendToTarget(q, sessionID); err != nil {
|
||||
return fmt.Errorf("sending FIX quote: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("Quote sent", "clOrdID", clOrdID, "quoteID", quoteID, "symbol", symbol)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user