diff --git a/.gitignore b/.gitignore index 18c6a90..65aea70 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ tmp bin .exe dist/ +certs/ +config/*.local.cfg diff --git a/README.md b/README.md index c083fe4..a85c8c9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,18 @@ make build This will try to connect to a FIX acceptor on `localhost:5001` and expose the UI on `localhost:8080`. You can modify the quickfix config for this found in config/tradeclient.cfg to suit your own needs. +## Locate + +The `Locate` tab sends `QuoteRequest (35=R)` and `OrderStatusRequest (35=H)` for Alpaca's Locate FIX 4.2 profile; Locates are created from the Orders tab (Buy, Market or Limit, TIF IOC or FOK). Every inbound application message, including `Quote`, `QuoteAcknowledgement`, `BusinessMessageReject` and `ExecutionReport`, is listed on the Locate tab. + +```sh +./bin/traderui -file-store -clordid-prefix "$(date +%s)-" config/locate.cfg +``` + +- `config/locate.cfg` sets mutual TLS and the Locate CompIDs; fill in the certificate paths and server name. +- `-file-store` keeps sequence numbers in `FileStorePath` so the session resumes across restarts. Do not set `ResetOnLogon` against the Locate acceptor. +- `-clordid-prefix` keeps generated ClOrdIDs unique across restarts; the acceptor rejects a repeated ClOrdID. + ## Licensing This software is available under the QuickFIX Software License. Please see the [LICENSE](https://github.com/quickfixgo/traderui/blob/main/LICENSE) for the terms specified by the QuickFIX Software License. diff --git a/assets/js/site.js b/assets/js/site.js index 71b0066..df71e4e 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -15,6 +15,7 @@ $(function() { setInterval(function() { App.orders.fetch({reset: true}); App.executions.fetch({reset: true}); + App.messages.fetch({reset: true}); }, 1000); @@ -39,6 +40,11 @@ var App = new( Backbone.View.extend({ session_ids: options.session_ids }); + this.locateForm = new App.Models.LocateForm({ + session_ids: options.session_ids + }); + + this.messages = new App.Collections.Messages(options.messages); this.orders = new App.Collections.Orders(options.orders); this.executions = new App.Collections.Executions(options.executions); this.router = new App.Router(); @@ -55,6 +61,7 @@ var App = new( Backbone.View.extend({ $("#nav-order").addClass("active"); $("#nav-execution").removeClass("active"); $("#nav-secdef").removeClass("active"); + $("#nav-locate").removeClass("active"); }, showExecutions: function() { @@ -66,6 +73,7 @@ var App = new( Backbone.View.extend({ $("#nav-order").removeClass("active"); $("#nav-execution").addClass("active"); $("#nav-secdef").removeClass("active"); + $("#nav-locate").removeClass("active"); }, showSecurityDefinitions: function() { @@ -74,6 +82,19 @@ var App = new( Backbone.View.extend({ $("#nav-order").removeClass("active"); $("#nav-execution").removeClass("active"); $("#nav-secdef").addClass("active"); + $("#nav-locate").removeClass("active"); + }, + + showLocate: function() { + var locateView = new App.Views.Locate({model: this.locateForm}); + var messagesView = new App.Views.MessagesView({collection: this.messages}); + + $("#app").html(locateView.render().el); + $("#app").append(messagesView.render().el); + $("#nav-order").removeClass("active"); + $("#nav-execution").removeClass("active"); + $("#nav-secdef").removeClass("active"); + $("#nav-locate").addClass("active"); }, showOrderDetails: function(id) { @@ -108,6 +129,7 @@ App.Router = Backbone.Router.extend({ "orders": "index", "executions": "executions", "secdefs": "secdefs", + "locate": "locate", "orders/:id": "orderDetails", "executions/:id": "executionDetails", }, @@ -124,6 +146,10 @@ App.Router = Backbone.Router.extend({ App.showSecurityDefinitions(); }, + locate: function() { + App.showLocate(); + }, + orderDetails: function(id) { App.showOrderDetails(id) }, @@ -145,6 +171,15 @@ App.Models.SecurityDefinitionRequest = Backbone.Model.extend({ urlRoot: "securitydefinitionrequest" }); +App.Models.QuoteRequest = Backbone.Model.extend({ + urlRoot: "/quoterequests" +}); + +App.Models.OrderStatusRequest = Backbone.Model.extend({ + urlRoot: "/orderstatusrequests" +}); + +App.Models.LocateForm = Backbone.Model.extend({}); App.Models.OrderTicket = Backbone.Model.extend({}); App.Models.SecurityDefinitionForm = Backbone.Model.extend({}); @@ -153,6 +188,11 @@ App.Collections.Orders = Backbone.Collection.extend({ comparator: 'id' }); +App.Collections.Messages = Backbone.Collection.extend({ + url: '/messages', + comparator: 'id' +}); + App.Collections.Executions = Backbone.Collection.extend({ url: '/executions', comparator: 'id' @@ -461,6 +501,129 @@ App.Views.Executions = Backbone.View.extend({ } }); +App.Views.MessageRowView = Backbone.View.extend({ + tagName: 'tr', + template: _.template(` +<%= id %> +<%= App.prettyMsgType(msg_type) %> +<%= session_id %> +<%- raw %> +`), + + render: function() { + this.$el.html(this.template(this.model.attributes)); + return this; + } +}); + +App.Views.MessagesView = Backbone.View.extend({ + initialize: function() { + this.listenTo(this.collection, 'reset', this.addAll); + }, + + render: function() { + this.$el.html(` +

Inbound messages

+ + + + + + + + + + + +
#TypeSessionMessage
`); + + this.addAll(); + + return this; + }, + + addAll: function() { + this.$("tbody").empty(); + this.collection.forEach(this.addOne, this); + return this; + }, + + addOne: function(message) { + var row = new App.Views.MessageRowView({model: message}); + this.$("tbody").append(row.render().el); + } +}); + +App.Views.Locate = Backbone.View.extend({ + template: _.template(` +

Quote request

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ +

Order status request

+
+
+ + +
+
+ + +
+ +
+

Create a Locate from the Orders tab: Buy, Market or Limit, TIF IOC (partial allowed) or FOK (all-or-none).

+`), + + events: { + "submit #quote-request": "sendQuote", + "submit #status-request": "sendStatus" + }, + + sendQuote: function(e) { + e.preventDefault(); + var req = new App.Models.QuoteRequest(); + req.set({ + session_id: this.$('#quote_session').val(), + symbol: this.$('#quote_symbol').val(), + quote_req_id: this.$('#quote_req_id').val() + }); + req.save(); + }, + + sendStatus: function(e) { + e.preventDefault(); + var req = new App.Models.OrderStatusRequest(); + req.set({ + session_id: this.$('#status_session').val(), + order_id: this.$('#status_order_id').val() + }); + req.save(); + }, + + render: function() { + this.$el.html(this.template(this.model.attributes)); + return this; + } +}); + App.Views.OrdersView = Backbone.View.extend({ initialize: function() { this.listenTo(this.collection, 'reset', this.addAll); @@ -670,6 +833,7 @@ App.Views.OrderTicket = Backbone.View.extend({