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
+
+
+
+ | # |
+ Type |
+ Session |
+ Message |
+
+
+
+
+
`);
+
+ 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({