From cbdfb75a6181a0355f99029539cb7bc0e41f3ce9 Mon Sep 17 00:00:00 2001 From: bas Date: Thu, 16 Jun 2022 11:19:35 +0200 Subject: [PATCH 01/26] Initial checkin --- orders/advice-orders/README.md | 5 + orders/advice-orders/demo.js | 702 ++++++++++++++++++++++++++++++++ orders/advice-orders/index.html | 137 +++++++ 3 files changed, 844 insertions(+) create mode 100644 orders/advice-orders/README.md create mode 100644 orders/advice-orders/demo.js create mode 100644 orders/advice-orders/index.html diff --git a/orders/advice-orders/README.md b/orders/advice-orders/README.md new file mode 100644 index 0000000..57d3c74 --- /dev/null +++ b/orders/advice-orders/README.md @@ -0,0 +1,5 @@ +# Client-side Samples for Advice Orders + +This is a demonstration on how to create an order advice for an end customer. + +Live demo: https://saxobank.github.io/openapi-samples-js/orders/advice-orders/ \ No newline at end of file diff --git a/orders/advice-orders/demo.js b/orders/advice-orders/demo.js new file mode 100644 index 0000000..deb2621 --- /dev/null +++ b/orders/advice-orders/demo.js @@ -0,0 +1,702 @@ +/*jslint browser: true, for: true, long: true, unordered: true */ +/*global window console demonstrationHelper */ + +(function () { + // Create a helper function to remove some boilerplate code from the example itself. + const demo = demonstrationHelper({ + "responseElm": document.getElementById("idResponse"), + "javaScriptElm": document.getElementById("idJavaScript"), + "accessTokenElm": document.getElementById("idBearerToken"), + "retrieveTokenHref": document.getElementById("idHrefRetrieveToken"), + "tokenValidateButton": document.getElementById("idBtnValidate"), + "accountsList": document.getElementById("idCbxAccount"), + "footerElm": document.getElementById("idFooter") + }); + const fictivePrice = 70; // SIM doesn't allow calls to price endpoint for most instruments + let managedAccountsResponseData = null; + + /** + * Helper function to convert the json string to an object, with error handling. + * @param {boolean} isFromNewOrderEdit Use new order object, or the one for order modifications. + * @return {Object} The newOrderObject from the input field - null if invalid + */ + function getOrderObjectFromJson(isFromNewOrderEdit) { + const orderObjectId = ( + isFromNewOrderEdit + ? "idNewOrderObject" + : "idChangeOrderObject" + ); + let newOrderObject = null; + try { + newOrderObject = JSON.parse(document.getElementById(orderObjectId).value); + if (newOrderObject.hasOwnProperty("AccountKey")) { + // This is the case for single orders, or conditional/related orders + // This function is used for other order types as well, so more order types are considered + newOrderObject.AccountKey = demo.user.accountKey; + } + if (newOrderObject.hasOwnProperty("Orders")) { + // This is the case for OCO, related and conditional orders + newOrderObject.Orders.forEach(function (order) { + if (order.hasOwnProperty("AccountKey")) { + order.AccountKey = demo.user.accountKey; + } + }); + } + document.getElementById(orderObjectId).value = JSON.stringify(newOrderObject, null, 4); + } catch (e) { + console.error(e); + } + return newOrderObject; + } + + /** + * Add an accountKey to the list. + * @param {string} accountKey The value. + * @param {string} description Description to display. + * @return {Element} The accountKey list. + */ + function addAccountToAccountKeyList(accountKey, description) { + const cbxAccountKeys = document.getElementById("idCbxManagedAccountKey"); + const option = document.createElement("option"); + option.text = description; + option.value = accountKey; + cbxAccountKeys.add(option); + return cbxAccountKeys; + } + + /** + * Display the active end client AccountKey in the POST /order editor. + * @return {void} + */ + function updateNewOrderEdit() { + console.error("Not implemented"); + } + + /** + * Remove all items from a combo box. + * @param {string} id The identiefier. + * @return {void} + */ + function clearCombobox(id) { + const cbx = document.getElementById(id); + let i; + for (i = cbx.options.length - 1; i >= 0; i -= 1) { + cbx.remove(i); + } + } + + /** + * Get details about clients under a particular owner. + * @return {void} + */ + function getAccountKeys() { + fetch( + demo.apiUrl + "/port/v1/accounts?IncludeSubAccounts=true&ClientKey=" + encodeURIComponent(demo.user.clientKey), + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + let accountCount = 0; + clearCombobox("idCbxManagedAccountKey"); + responseJson.Data.forEach(function (account) { + if (account.ClientKey === demo.user.clientKey) { + addAccountToAccountKeyList(account.AccountKey, "Your account: " + account.AccountId + " (" + account.AccountType + ")"); + } else if (account.Active) { + addAccountToAccountKeyList(account.AccountKey, "Account end client: " + account.AccountId + " (" + account.AccountType + ")"); + accountCount += 1; + } + }); + managedAccountsResponseData = responseJson.Data; // Keep, so ClientKeys can be looked up by AccountKey + console.log("Found " + accountCount + " managed accounts.\nResponse: " + JSON.stringify(responseJson, null, 4)); + updateNewOrderEdit(); + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * This demo can be used for not only Stocks. You can change the model in the editor to Bond, SrdOnStock, etc. + * @param {Object} responseJson The response with the references. + * @return {string} A message pointing you at the feature to change the order object. + */ + function getRelatedAssetTypesMessage(responseJson) { + let result = ""; + let i; + let relatedInstrument; + + function addAssetTypeToMessage(assetType) { + if (relatedInstrument.AssetType === assetType) { + result += ( + result === "" + ? "" + : "\n\n" + ) + "The response below indicates there is a related " + assetType + ".\nYou can change the order object to AssetType '" + assetType + "' and Uic '" + relatedInstrument.Uic + "' to test " + assetType + " orders."; + } + } + + if (responseJson.hasOwnProperty("RelatedInstruments")) { + for (i = 0; i < responseJson.RelatedInstruments.length; i += 1) { + relatedInstrument = responseJson.RelatedInstruments[i]; + addAssetTypeToMessage("Bond"); + addAssetTypeToMessage("SrdOnStock"); + // The other way around works as well. Show message for Stock. + addAssetTypeToMessage("Stock"); + } + } + if (responseJson.hasOwnProperty("RelatedOptionRootsEnhanced")) { + // Don't loop. Just take the first, for demo purposes. + relatedInstrument = responseJson.RelatedOptionRootsEnhanced[0]; + result += ( + result === "" + ? "" + : "\n\n" + ) + "The response below indicates there are related options.\nYou can use OptionRootId '" + relatedInstrument.OptionRootId + "' in the options example."; + } + return result; + } + + /** + * This function collects the access rights of the logged in user. + * @return {void} + */ + function getAccessRights() { + fetch( + demo.apiUrl + "/root/v1/user", + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + const responseText = "\n\nResponse: " + JSON.stringify(responseJson, null, 4); + if (responseJson.AccessRights.CanTrade) { + console.log("You are allowed to place orders." + responseText); + } else { + console.error("You are not allowed to place orders." + responseText); + } + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * This is an example of getting the trading settings of an instrument. + * @return {void} + */ + function getConditions() { + + /** + * The instrument is tradable, but there might be limitations. If so, display them. + * @param {Object} detailsObject The response with the instrument details. + * @return {void} + */ + function checkTradingStatus(detailsObject) { + let statusDescription = "This instrument has trading limitations:\n"; + if (detailsObject.TradingStatus !== "Tradable") { + if (detailsObject.hasOwnProperty("NonTradableReason")) { + switch (detailsObject.NonTradableReason) { + case "ETFsWithoutKIIDs": + statusDescription += "The issuer has not provided a Key Information Document (KID) for this instrument."; + break; + case "ExpiredInstrument": + statusDescription += "This instrument has expired."; + break; + case "NonShortableInstrument": + statusDescription += "Short selling is not available for this instrument."; + break; + case "NotOnlineClientTradable": + statusDescription += "This instrument is not tradable online."; + break; + case "OfflineTradableBonds": + statusDescription += "This instrument is tradable offline."; + break; + case "ReduceOnlyInstrument": + statusDescription += "This instrument is reduce-only."; + break; + default: + // There are reasons "OtherReason" and "None". + statusDescription += "This instrument is not tradable."; + } + statusDescription += "\n(" + detailsObject.NonTradableReason + ")"; + } else { + // Somehow not reason was supplied. + statusDescription += "Status: " + detailsObject.TradingStatus; + } + window.alert(statusDescription); + } + } + + /** + * Verify if the selected OrderType is supported for the instrument. + * @param {Object} orderObject The object used to POST the new order. + * @param {Array} orderTypes Array with supported order types (Market, Limit, etc). + * @return {void} + */ + function checkSupportedOrderTypes(orderObject, orderTypes) { + if (orderTypes.indexOf(orderObject.OrderType) === -1) { + window.alert("The order type " + orderObject.OrderType + " is not supported for this instrument."); + } + } + + /** + * Verify if the selected account is capable of handling this instrument. + * @param {Array} tradableOn Supported account list. + * @return {void} + */ + function checkSupportedAccounts(tradableOn) { + // First, get the id of the active account: + const activeAccountId = demo.user.accounts.find(function (i) { + return i.accountKey === demo.user.accountKey; + }).accountId; + // Next, check if instrument is allowed on this account: + if (tradableOn.length === 0) { + window.alert("This instrument cannot be traded on any of your accounts."); + } else if (tradableOn.indexOf(activeAccountId) === -1) { + window.alert("This instrument cannot be traded on the selected account " + activeAccountId + ", but only on " + tradableOn.join(", ") + "."); + } + } + + function calculateFactor(tickSize) { + let numberOfDecimals = 0; + if ((tickSize % 1) !== 0) { + numberOfDecimals = tickSize.toString().split(".")[1].length; + } + return Math.pow(10, numberOfDecimals); + } + + function checkTickSize(orderObject, tickSize) { + const factor = calculateFactor(tickSize); // Modulo doesn't support fractions, so multiply with a factor + if (Math.round(orderObject.OrderPrice * factor) % Math.round(tickSize * factor) !== 0) { + window.alert("The price of " + orderObject.OrderPrice + " doesn't match the tick size of " + tickSize); + } + } + + function checkTickSizes(orderObject, tickSizeScheme) { + let tickSize = tickSizeScheme.DefaultTickSize; + let i; + for (i = 0; i < tickSizeScheme.Elements.length; i += 1) { + if (orderObject.OrderPrice <= tickSizeScheme.Elements[i].HighPrice) { + tickSize = tickSizeScheme.Elements[i].TickSize; // The price is below a threshold and therefore not the default + break; + } + } + checkTickSize(orderObject, tickSize); + } + + function checkMinimumTradeSize(orderObject, detailsObject) { + if (orderObject.Amount < detailsObject.MinimumTradeSize) { + window.alert("The order amount must be at least the minimumTradeSize of " + detailsObject.MinimumTradeSize); + } + } + + function checkMinimumOrderValue(orderObject, detailsObject) { + const price = ( + orderObject.hasOwnProperty("OrderPrice") + ? orderObject.OrderPrice + : fictivePrice // SIM doesn't allow calls to price endpoint for most instruments so just take something + ); + if (orderObject.Amount * price < detailsObject.MinimumOrderValue) { + window.alert("The order value (amount * price) must be at least the minimumOrderValue of " + detailsObject.MinimumOrderValue); + } + } + + function checkLotSizes(orderObject, detailsObject) { + if (orderObject.Amount < detailsObject.MinimumLotSize) { + window.alert("The amount must be at least the minimumLotSize of " + detailsObject.MinimumLotSize); + } + if (detailsObject.hasOwnProperty("LotSize") && orderObject.Amount % detailsObject.LotSize !== 0) { + window.alert("The amount must be the lot size or a multiplication of " + detailsObject.LotSize); + } + } + + const newOrderObject = getOrderObjectFromJson(true); + fetch( + demo.apiUrl + "/ref/v1/instruments/details/" + newOrderObject.Uic + "/" + newOrderObject.AssetType + "?AccountKey=" + encodeURIComponent(demo.user.accountKey) + "&FieldGroups=OrderSetting", + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + console.log(getRelatedAssetTypesMessage(responseJson) + "\n\n" + JSON.stringify(responseJson, null, 4)); + if (responseJson.IsTradable === false) { + window.alert("This instrument is not tradable!"); + // For demonstration purposes, the validation continues, but an order ticket shouldn't be shown! + } + checkTradingStatus(responseJson); + checkSupportedOrderTypes(newOrderObject, responseJson.SupportedOrderTypes); + if (newOrderObject.OrderType !== "Market" && newOrderObject.OrderType !== "TraspasoIn") { + if (responseJson.hasOwnProperty("TickSizeScheme")) { + checkTickSizes(newOrderObject, responseJson.TickSizeScheme); + } else if (responseJson.hasOwnProperty("TickSize")) { + checkTickSize(newOrderObject, responseJson.TickSize); + } + } + checkSupportedAccounts(responseJson.TradableOn); + checkMinimumTradeSize(newOrderObject, responseJson); + if (newOrderObject.AssetType === "Stock") { + checkMinimumOrderValue(newOrderObject, responseJson); + } + if (newOrderObject.AssetType === "Stock" && responseJson.LotSizeType !== "NotUsed") { + checkLotSizes(newOrderObject, responseJson); + } + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * This is an example of an order validation. + * @return {void} + */ + function preCheckNewOrder() { + // Bug: Preview doesn't check for limit outside market hours + + function getErrorMessage(responseJson, defaultMessage) { + let errorMessage; + if (responseJson.hasOwnProperty("ErrorInfo")) { + // Be aware that the ErrorInfo.Message might contain line breaks, escaped like "\r\n"! + errorMessage = ( + responseJson.ErrorInfo.hasOwnProperty("Message") + ? responseJson.ErrorInfo.Message + : responseJson.ErrorInfo.ErrorCode // In some cases (AllocationKeyDoesNotMatchAccount) the message is not available + ); + // There can be error messages per order. Try to add them. + if (responseJson.hasOwnProperty("Orders")) { + responseJson.Orders.forEach(function (order) { + errorMessage += "\n- " + getErrorMessage(order, ""); + }); + } + } else { + errorMessage = defaultMessage; + } + return errorMessage; + } + + const newOrderObject = getOrderObjectFromJson(true); + newOrderObject.FieldGroups = ["Costs", "MarginImpactBuySell"]; + fetch( + demo.apiUrl + "/trade/v2/orders/precheck", + { + "method": "POST", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value, + "Content-Type": "application/json; charset=utf-8", + "X-Request-ID": Math.random() // This prevents error 409 (Conflict) from identical previews within 15 seconds + }, + "body": JSON.stringify(newOrderObject) + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + // Response must have PreCheckResult property being "Ok" + if (responseJson.PreCheckResult === "Ok") { + // Secondly, you can have a PreCheckResult of "Ok", but still a (functional) error + // Order could be placed if the account had sufficient margin and funding. + // In this case all calculated cost and margin values are in the response, together with an ErrorInfo object: + if (responseJson.hasOwnProperty("ErrorInfo")) { + // Be aware that the ErrorInfo.Message might contain line breaks, escaped like "\r\n"! + console.error(getErrorMessage(responseJson, "") + "\n\n" + JSON.stringify(responseJson, null, 4)); + } else { + // The order can be placed + console.log("The order can be placed:\n\n" + JSON.stringify(responseJson, null, 4)); + } + } else { + // Order request is syntactically correct, but the order cannot be placed, as it would violate semantic rules + // This can be something like: {"ErrorInfo":{"ErrorCode":"IllegalInstrumentId","Message":"Instrument ID is invalid"},"EstimatedCashRequired":0.0,"PreCheckResult":"Error"} + console.error(getErrorMessage(responseJson, "Order request is syntactically correct, but the order cannot be placed, as it would violate semantic rules:") + "\n\n" + JSON.stringify(responseJson, null, 4) + "\n\nX-Correlation header (for troubleshooting with Saxo): " + response.headers.get("X-Correlation")); + } + }); + } else { + // This can be something like: {"Message":"One or more properties of the request are invalid!","ModelState":{"Orders":["Stop leg of OCO order must have OrderType of either: TrailingStopIfTraded, StopIfTraded, StopLimit"]},"ErrorCode":"InvalidModelState"} + // The developer (you) must fix this. + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * Add an Order to the list. + * @param {string} orderId The value. + * @param {string} description Description to display. + * @return {Element} The orders list. + */ + function addOrderToOrdersList(orderId, description) { + const cbxOrders = document.getElementById("idCbxOrderId"); + const option = document.createElement("option"); + option.text = description; + option.value = orderId; + cbxOrders.add(option); + return cbxOrders; + } + + /** + * Retrieve order details and display them in the PATCH /order editor. + * @return {void} + */ + function getOrderDetailsAndUpdateModifyOrderEdit() { + console.error("Not implemented"); + } + + /** + * This is an example of placing a single leg order. + * @return {void} + */ + function placeNewOrder() { + const headersObject = { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value, + "Content-Type": "application/json; charset=utf-8" + }; + const newOrderObject = getOrderObjectFromJson(true); + fetch( + demo.apiUrl + "/trade/v2/orders", + { + "method": "POST", + "headers": headersObject, + "body": JSON.stringify(newOrderObject) + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + const xRequestId = response.headers.get("X-Request-ID"); + console.log("Successful request:\n" + JSON.stringify(responseJson, null, 4) + ( + xRequestId === null + ? "" + : "\nX-Request-ID response header: " + xRequestId + )); + addOrderToOrdersList(responseJson.OrderId, "New order " + responseJson.OrderId).value = responseJson.OrderId; + getOrderDetailsAndUpdateModifyOrderEdit(); + }); + } else { + console.debug(response); + if (response.status === 403) { + // Don't add this check to your application, but for learning purposes: + // An HTTP Forbidden indicates that your app is not enabled for trading. + // See https://www.developer.saxo/openapi/appmanagement + demo.processError(response, "Your app might not be enabled for trading."); + } else { + demo.processError(response); + } + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * Get the ClientKey of an AccountKey by using a cached response. + * @return {void} + */ + function getClientKeyOfSelectedAccount() { + const accountKeyToFind = document.getElementById("idCbxManagedAccountKey").value; + let result = demo.user.accountKey; + if (managedAccountsResponseData === null) { + console.error("Request managed accounts first, before making this request."); + } + managedAccountsResponseData.forEach(function (account) { + if (account.AccountKey === accountKeyToFind) { + result = account.ClientKey; + } + }); + return result; + } + + /** + * This is an example of getting detailed information of a specific order (in this case the last placed order). + * @return {void} + */ + function getOrderDetails() { + const clientKey = getClientKeyOfSelectedAccount(); + const orderId = document.getElementById("idCbxOrderId").value; + fetch( + demo.apiUrl + "/port/v1/orders/" + orderId + "/details?ClientKey=" + encodeURIComponent(clientKey), + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + if (responseJson === null) { + console.error("The order wasn't found in the list of active orders. Is order " + orderId + " still open?"); + } else { + console.log("Response: " + JSON.stringify(responseJson, null, 4)); + } + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * This is an example of updating a single leg order. + * @return {void} + */ + function modifyLastOrder() { + const newOrderObject = getOrderObjectFromJson(false); + const headersObject = { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value, + "Content-Type": "application/json; charset=utf-8" + }; + fetch( + demo.apiUrl + "/trade/v2/orders", + { + "method": "PATCH", + "headers": headersObject, + "body": JSON.stringify(newOrderObject) + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + const xRequestId = response.headers.get("X-Request-ID"); + console.log("Successful request:\n" + JSON.stringify(responseJson, null, 4) + ( + xRequestId === null + ? "" + : "\nX-Request-ID response header: " + xRequestId + )); + }); + } else { + // If you get a 404 NotFound, the order might already be executed! + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * This is an example of removing an order from the book. + * @return {void} + */ + function cancelLastOrder() { + const adviceDeleteAction = document.getElementById("idCbxAdviceDeleteAction").value; + const orderId = document.getElementById("idCbxOrderId").value; + const accountKey = document.getElementById("idCbxManagedAccountKey").value; + // DELETE /trade/v2/orders/123?AdviceDeleteAction=CancelAdvice + fetch( + demo.apiUrl + "/trade/v2/orders/" + orderId + "?AccountKey=" + encodeURIComponent(accountKey) + "&AdviceDeleteAction=" + adviceDeleteAction, + { + "method": "DELETE", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + // Response must have an OrderId + console.log(JSON.stringify(responseJson, null, 4)); + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * Order changes are broadcasted via ENS. Retrieve the recent events to see what you can expect. + * @return {void} + */ + function getHistoricalEnsEvents() { + const fromDate = new Date(); + fromDate.setMinutes(fromDate.getMinutes() - 5); + fetch( + demo.apiUrl + "/ens/v1/activities?Activities=Orders&FromDateTime=" + fromDate.toISOString(), + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + console.log("Found " + responseJson.Data.length + " events in the last 5 minutes:\n\n" + JSON.stringify(responseJson, null, 4)); + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * Retrieve the list of pending order advices. + * @return {void} + */ + function getOrders() { + const accountKey = document.getElementById("idCbxManagedAccountKey").value; + const clientKey = getClientKeyOfSelectedAccount(); + fetch( + demo.apiUrl + "/port/v1/orders?Status=All&ClientKey=" + encodeURIComponent(clientKey) + "&AccountKey=" + encodeURIComponent(accountKey), + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + responseJson.Data.forEach(function (order) { + addOrderToOrdersList(order.OrderId, order.OrderId + " (" + order.OrderStatus + ")"); + }); + console.log("All open orders for account '" + accountKey + "'.\n\n" + JSON.stringify(responseJson, null, 4)); + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + demo.setupEvents([ + {"evt": "change", "elmId": "idCbxManagedAccountKey", "func": updateNewOrderEdit, "funcsToDisplay": [updateNewOrderEdit]}, + {"evt": "change", "elmId": "idCbxOrderId", "func": getOrderDetailsAndUpdateModifyOrderEdit, "funcsToDisplay": [getOrderDetailsAndUpdateModifyOrderEdit]}, + {"evt": "click", "elmId": "idBtnGetAccountKeys", "func": getAccountKeys, "funcsToDisplay": [getAccountKeys]}, + {"evt": "click", "elmId": "idBtnGetAccessRights", "func": getAccessRights, "funcsToDisplay": [getAccessRights]}, + {"evt": "click", "elmId": "idBtnGetConditions", "func": getConditions, "funcsToDisplay": [getConditions]}, + {"evt": "click", "elmId": "idBtnPreCheckOrder", "func": preCheckNewOrder, "funcsToDisplay": [preCheckNewOrder]}, + {"evt": "click", "elmId": "idBtnPlaceNewOrder", "func": placeNewOrder, "funcsToDisplay": [placeNewOrder]}, + {"evt": "click", "elmId": "idBtnGetOrderDetails", "func": getOrderDetails, "funcsToDisplay": [getOrderDetails]}, + {"evt": "click", "elmId": "idBtnModifyLastOrder", "func": modifyLastOrder, "funcsToDisplay": [modifyLastOrder]}, + {"evt": "click", "elmId": "idBtnCancelLastOrder", "func": cancelLastOrder, "funcsToDisplay": [cancelLastOrder]}, + {"evt": "click", "elmId": "idBtnGetOrders", "func": getOrders, "funcsToDisplay": [getOrders]}, + {"evt": "click", "elmId": "idBtnHistoricalEnsEvents", "func": getHistoricalEnsEvents, "funcsToDisplay": [getHistoricalEnsEvents]} + ]); + demo.displayVersion("trade"); +}()); diff --git a/orders/advice-orders/index.html b/orders/advice-orders/index.html new file mode 100644 index 0000000..9f5d71e --- /dev/null +++ b/orders/advice-orders/index.html @@ -0,0 +1,137 @@ + + + + + + + + + + + Demo for Advice Orders + + + + +
+
+ +
+ +
+

Give advice on orders

+ +

The order advice flow is only for advisors. The example here is not an advice, but shows how to integrate the flow in your apps. To get a SIM-account configured for order advices, contact your Saxo Bank account manager.

+
+Order tickets must warn before trading a complex product, provide a KID download and the cost breakdown. See the sample. +
+
+ + +
+
+
+
New order advice:
+ + + + +
+ + +
+
Update order advice:
+ + + +
+ +
+
+
+ +
+
+ +
+Response:
Click button to launch function.
+
+JS code:
Click button to show code.
+
+
+ + From 2346610248c3ef1bf4623c950aacd87f257b702e Mon Sep 17 00:00:00 2001 From: bas Date: Thu, 16 Jun 2022 16:38:41 +0200 Subject: [PATCH 02/26] Order details --- orders/advice-orders/demo.js | 87 +++++++++++++++++++++++---------- orders/advice-orders/index.html | 9 ++-- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/orders/advice-orders/demo.js b/orders/advice-orders/demo.js index deb2621..524c009 100644 --- a/orders/advice-orders/demo.js +++ b/orders/advice-orders/demo.js @@ -26,19 +26,20 @@ ? "idNewOrderObject" : "idChangeOrderObject" ); + const accountKey = document.getElementById("idCbxManagedAccountKey").value; let newOrderObject = null; try { newOrderObject = JSON.parse(document.getElementById(orderObjectId).value); if (newOrderObject.hasOwnProperty("AccountKey")) { // This is the case for single orders, or conditional/related orders // This function is used for other order types as well, so more order types are considered - newOrderObject.AccountKey = demo.user.accountKey; + newOrderObject.AccountKey = accountKey; } if (newOrderObject.hasOwnProperty("Orders")) { // This is the case for OCO, related and conditional orders newOrderObject.Orders.forEach(function (order) { if (order.hasOwnProperty("AccountKey")) { - order.AccountKey = demo.user.accountKey; + order.AccountKey = accountKey; } }); } @@ -69,7 +70,7 @@ * @return {void} */ function updateNewOrderEdit() { - console.error("Not implemented"); + getOrderObjectFromJson(true); } /** @@ -260,9 +261,10 @@ * @return {void} */ function checkSupportedAccounts(tradableOn) { + const accountKey = document.getElementById("idCbxManagedAccountKey").value; // First, get the id of the active account: - const activeAccountId = demo.user.accounts.find(function (i) { - return i.accountKey === demo.user.accountKey; + const activeAccountId = managedAccountsResponseData.find(function (i) { + return i.AccountKey === accountKey; }).accountId; // Next, check if instrument is allowed on this account: if (tradableOn.length === 0) { @@ -326,8 +328,9 @@ } const newOrderObject = getOrderObjectFromJson(true); + const accountKey = document.getElementById("idCbxManagedAccountKey").value; fetch( - demo.apiUrl + "/ref/v1/instruments/details/" + newOrderObject.Uic + "/" + newOrderObject.AssetType + "?AccountKey=" + encodeURIComponent(demo.user.accountKey) + "&FieldGroups=OrderSetting", + demo.apiUrl + "/ref/v1/instruments/details/" + newOrderObject.Uic + "/" + newOrderObject.AssetType + "?AccountKey=" + encodeURIComponent(accountKey) + "&FieldGroups=OrderSetting", { "method": "GET", "headers": { @@ -455,14 +458,6 @@ return cbxOrders; } - /** - * Retrieve order details and display them in the PATCH /order editor. - * @return {void} - */ - function getOrderDetailsAndUpdateModifyOrderEdit() { - console.error("Not implemented"); - } - /** * This is an example of placing a single leg order. * @return {void} @@ -514,23 +509,28 @@ */ function getClientKeyOfSelectedAccount() { const accountKeyToFind = document.getElementById("idCbxManagedAccountKey").value; - let result = demo.user.accountKey; if (managedAccountsResponseData === null) { console.error("Request managed accounts first, before making this request."); + throw "Request managed accounts first, before making this request."; } - managedAccountsResponseData.forEach(function (account) { - if (account.AccountKey === accountKeyToFind) { - result = account.ClientKey; - } - }); - return result; + return managedAccountsResponseData.find(function (i) { + return i.AccountKey === accountKeyToFind; + }).ClientKey; } /** - * This is an example of getting detailed information of a specific order (in this case the last placed order). + * Retrieve order details and display them in the PATCH /order editor. * @return {void} */ - function getOrderDetails() { + function getOrderDetailsAndUpdateModifyOrderEdit() { + + function copyOptionalProperty(source, dest, propertyName) { + if (source.hasOwnProperty("propertyName")) { + dest[propertyName] = responseJson[propertyName]; + } + } + + const accountKey = document.getElementById("idCbxManagedAccountKey").value; const clientKey = getClientKeyOfSelectedAccount(); const orderId = document.getElementById("idCbxOrderId").value; fetch( @@ -544,9 +544,43 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { + let orderObject; if (responseJson === null) { console.error("The order wasn't found in the list of active orders. Is order " + orderId + " still open?"); } else { + // Only simple stock orders are considered here. Make your own logic for ContractOptions etc. + orderObject = { + "AccountKey": accountKey, + "OrderId": orderId, + "BuySell": responseJson.BuySell, + "Amount": responseJson.Amount, // Check for OrderAmountType! + "AssetType": responseJson.AssetType, + "Uic": responseJson.Uic, + "OrderType": responseJson.OpenOrderType, + "OrderDuration": responseJson.Duration + }; + switch (responseJson.OpenOrderType) { + case "Limit": // A buy order will be executed when the price falls below the provided price point; a sell order when the price increases beyond the provided price point. + case "StopIfBid": // A buy order will be executed when the bid price increases to the provided price point; a sell order when the price falls below. + case "StopIfOffered": // A buy order will be executed when the ask price increases to the provided price point; a sell order when the price falls below. + case "StopIfTraded": // A buy order will be executed when the last price increases to the provided price point; a sell order when the price falls below. + orderObject.OrderPrice = responseJson.Price; + break; + case "StopLimit": // A buy StopLimit order will turn in to a regular limit order once the price goes beyond the OrderPrice. The limit order will have a OrderPrice of the StopLimitPrice. + orderObject.OrderPrice = responseJson.Price; + orderObject.StopLimitPrice = responseJson.StopLimitPrice; + break; + case "TrailingStop": // A trailing stop order type is used to guard a position against a potential loss, but the order price follows that of the position when the price goes up. It does so in steps, trying to keep a fixed distance to the current price. + case "TrailingStopIfBid": + case "TrailingStopIfOffered": + case "TrailingStopIfTraded": + orderObject.OrderPrice = responseJson.Price; + orderObject.TrailingstopDistanceToMarket = responseJson.TrailingStopDistanceToMarket; + orderObject.TrailingStopStep = responseJson.TrailingStopStep; + } + copyOptionalProperty(responseJson, orderObject, "Triggers"); + copyOptionalProperty(responseJson, orderObject, "ExternalReference"); + document.getElementById("idChangeOrderObject").value = JSON.stringify(orderObject, null, 4); console.log("Response: " + JSON.stringify(responseJson, null, 4)); } }); @@ -671,9 +705,12 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { + // Empty order list: + clearCombobox("idCbxOrderId"); responseJson.Data.forEach(function (order) { - addOrderToOrdersList(order.OrderId, order.OrderId + " (" + order.OrderStatus + ")"); + addOrderToOrdersList(order.OrderId, order.OrderId + " (" + order.Status + ")"); }); + getOrderDetailsAndUpdateModifyOrderEdit(); console.log("All open orders for account '" + accountKey + "'.\n\n" + JSON.stringify(responseJson, null, 4)); }); } else { @@ -692,7 +729,7 @@ {"evt": "click", "elmId": "idBtnGetConditions", "func": getConditions, "funcsToDisplay": [getConditions]}, {"evt": "click", "elmId": "idBtnPreCheckOrder", "func": preCheckNewOrder, "funcsToDisplay": [preCheckNewOrder]}, {"evt": "click", "elmId": "idBtnPlaceNewOrder", "func": placeNewOrder, "funcsToDisplay": [placeNewOrder]}, - {"evt": "click", "elmId": "idBtnGetOrderDetails", "func": getOrderDetails, "funcsToDisplay": [getOrderDetails]}, + {"evt": "click", "elmId": "idBtnGetOrderDetails", "func": getOrderDetailsAndUpdateModifyOrderEdit, "funcsToDisplay": [getOrderDetailsAndUpdateModifyOrderEdit]}, {"evt": "click", "elmId": "idBtnModifyLastOrder", "func": modifyLastOrder, "funcsToDisplay": [modifyLastOrder]}, {"evt": "click", "elmId": "idBtnCancelLastOrder", "func": cancelLastOrder, "funcsToDisplay": [cancelLastOrder]}, {"evt": "click", "elmId": "idBtnGetOrders", "func": getOrders, "funcsToDisplay": [getOrders]}, diff --git a/orders/advice-orders/index.html b/orders/advice-orders/index.html index 9f5d71e..7aec17a 100644 --- a/orders/advice-orders/index.html +++ b/orders/advice-orders/index.html @@ -35,10 +35,8 @@

Example on Order Advices

Give advice on orders

-

The order advice flow is only for advisors. The example here is not an advice, but shows how to integrate the flow in your apps. To get a SIM-account configured for order advices, contact your Saxo Bank account manager.

-
-Order tickets must warn before trading a complex product, provide a KID download and the cost breakdown. See the sample. +See other samples for finding instruments, trading in complex instruments and compliance requirements.

- From 5e3ede6352dca7bef607bebab144b69f098d0ec7 Mon Sep 17 00:00:00 2001 From: bas Date: Fri, 17 Jun 2022 11:22:05 +0200 Subject: [PATCH 03/26] Fixes after first demo --- orders/advice-orders/demo.js | 70 +++++++++++++++++++-------------- orders/advice-orders/index.html | 10 ++--- orders/stocks/demo.js | 4 +- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/orders/advice-orders/demo.js b/orders/advice-orders/demo.js index 524c009..475483a 100644 --- a/orders/advice-orders/demo.js +++ b/orders/advice-orders/demo.js @@ -265,7 +265,7 @@ // First, get the id of the active account: const activeAccountId = managedAccountsResponseData.find(function (i) { return i.AccountKey === accountKey; - }).accountId; + }).AccountId; // Next, check if instrument is allowed on this account: if (tradableOn.length === 0) { window.alert("This instrument cannot be traded on any of your accounts."); @@ -526,7 +526,7 @@ function copyOptionalProperty(source, dest, propertyName) { if (source.hasOwnProperty("propertyName")) { - dest[propertyName] = responseJson[propertyName]; + dest[propertyName] = source[propertyName]; } } @@ -548,38 +548,15 @@ if (responseJson === null) { console.error("The order wasn't found in the list of active orders. Is order " + orderId + " still open?"); } else { - // Only simple stock orders are considered here. Make your own logic for ContractOptions etc. + // For the PATCH a limited set of fields is required. Feel free to add those fields you'd like to update. orderObject = { "AccountKey": accountKey, "OrderId": orderId, - "BuySell": responseJson.BuySell, - "Amount": responseJson.Amount, // Check for OrderAmountType! "AssetType": responseJson.AssetType, - "Uic": responseJson.Uic, "OrderType": responseJson.OpenOrderType, "OrderDuration": responseJson.Duration }; - switch (responseJson.OpenOrderType) { - case "Limit": // A buy order will be executed when the price falls below the provided price point; a sell order when the price increases beyond the provided price point. - case "StopIfBid": // A buy order will be executed when the bid price increases to the provided price point; a sell order when the price falls below. - case "StopIfOffered": // A buy order will be executed when the ask price increases to the provided price point; a sell order when the price falls below. - case "StopIfTraded": // A buy order will be executed when the last price increases to the provided price point; a sell order when the price falls below. - orderObject.OrderPrice = responseJson.Price; - break; - case "StopLimit": // A buy StopLimit order will turn in to a regular limit order once the price goes beyond the OrderPrice. The limit order will have a OrderPrice of the StopLimitPrice. - orderObject.OrderPrice = responseJson.Price; - orderObject.StopLimitPrice = responseJson.StopLimitPrice; - break; - case "TrailingStop": // A trailing stop order type is used to guard a position against a potential loss, but the order price follows that of the position when the price goes up. It does so in steps, trying to keep a fixed distance to the current price. - case "TrailingStopIfBid": - case "TrailingStopIfOffered": - case "TrailingStopIfTraded": - orderObject.OrderPrice = responseJson.Price; - orderObject.TrailingstopDistanceToMarket = responseJson.TrailingStopDistanceToMarket; - orderObject.TrailingStopStep = responseJson.TrailingStopStep; - } copyOptionalProperty(responseJson, orderObject, "Triggers"); - copyOptionalProperty(responseJson, orderObject, "ExternalReference"); document.getElementById("idChangeOrderObject").value = JSON.stringify(orderObject, null, 4); console.log("Response: " + JSON.stringify(responseJson, null, 4)); } @@ -664,10 +641,11 @@ * @return {void} */ function getHistoricalEnsEvents() { + const accountKey = document.getElementById("idCbxManagedAccountKey").value; const fromDate = new Date(); - fromDate.setMinutes(fromDate.getMinutes() - 5); + fromDate.setMinutes(fromDate.getMinutes() - 10); fetch( - demo.apiUrl + "/ens/v1/activities?Activities=Orders&FromDateTime=" + fromDate.toISOString(), + demo.apiUrl + "/ens/v1/activities?Activities=Orders&FromDateTime=" + fromDate.toISOString() + "&AccountKey=" + encodeURIComponent(accountKey), { "method": "GET", "headers": { @@ -677,7 +655,7 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - console.log("Found " + responseJson.Data.length + " events in the last 5 minutes:\n\n" + JSON.stringify(responseJson, null, 4)); + console.log("Found " + responseJson.Data.length + " event(s) in the last 10 minutes:\n\n" + JSON.stringify(responseJson, null, 4)); }); } else { demo.processError(response); @@ -721,9 +699,43 @@ }); } + /** + * Reflect the new status in the new order advice edit. + * @return {void} + */ + function updateCreateOrderEdit() { + const newStatus = document.getElementById("idCbxAdviceCreateAction").value; + const orderObject = getOrderObjectFromJson(true); + if (!orderObject.hasOwnProperty("Triggers")) { + orderObject.Triggers = [{ + "TriggerType": "Advice" + }]; + } + orderObject.Triggers[0].ApprovalAction = newStatus; + document.getElementById("idNewOrderObject").value = JSON.stringify(orderObject, null, 4); + } + + /** + * Reflect the new status in the modify order advice edit. + * @return {void} + */ + function updateModifyOrderEdit() { + const newStatus = document.getElementById("idCbxAdviceModifyAction").value; + const orderObject = getOrderObjectFromJson(false); + if (!orderObject.hasOwnProperty("Triggers")) { + orderObject.Triggers = [{ + "TriggerType": "Advice" + }]; + } + orderObject.Triggers[0].ApprovalAction = newStatus; + document.getElementById("idChangeOrderObject").value = JSON.stringify(orderObject, null, 4); + } + demo.setupEvents([ {"evt": "change", "elmId": "idCbxManagedAccountKey", "func": updateNewOrderEdit, "funcsToDisplay": [updateNewOrderEdit]}, {"evt": "change", "elmId": "idCbxOrderId", "func": getOrderDetailsAndUpdateModifyOrderEdit, "funcsToDisplay": [getOrderDetailsAndUpdateModifyOrderEdit]}, + {"evt": "change", "elmId": "idCbxAdviceCreateAction", "func": updateCreateOrderEdit, "funcsToDisplay": [updateCreateOrderEdit]}, + {"evt": "change", "elmId": "idCbxAdviceModifyAction", "func": updateModifyOrderEdit, "funcsToDisplay": [updateModifyOrderEdit]}, {"evt": "click", "elmId": "idBtnGetAccountKeys", "func": getAccountKeys, "funcsToDisplay": [getAccountKeys]}, {"evt": "click", "elmId": "idBtnGetAccessRights", "func": getAccessRights, "funcsToDisplay": [getAccessRights]}, {"evt": "click", "elmId": "idBtnGetConditions", "func": getConditions, "funcsToDisplay": [getConditions]}, diff --git a/orders/advice-orders/index.html b/orders/advice-orders/index.html index 7aec17a..fd99019 100644 --- a/orders/advice-orders/index.html +++ b/orders/advice-orders/index.html @@ -69,6 +69,11 @@

Give advice on orders


+ +
+ + + + + +
+ +
+Response:
Click button to launch function.
+
+JS code:
Click button to show code.
+
+ + + From 5ceed20cbc9469eb9debc453fab9269623ce3140 Mon Sep 17 00:00:00 2001 From: bas Date: Tue, 28 Jun 2022 11:31:37 +0200 Subject: [PATCH 09/26] Retrieve sessions in two ways --- account-history/performance/demo.js | 8 +- orders/advice-orders/demo.js | 2 +- orders/block-orders/demo.js | 2 +- orders/pre-market-and-after-hours/demo.js | 88 ++++++++++++++++---- orders/pre-market-and-after-hours/index.html | 3 +- orders/stocks/demo.js | 2 +- 6 files changed, 82 insertions(+), 23 deletions(-) diff --git a/account-history/performance/demo.js b/account-history/performance/demo.js index 82c1060..28878a5 100644 --- a/account-history/performance/demo.js +++ b/account-history/performance/demo.js @@ -80,7 +80,7 @@ * @return {void} */ function getClientTimeseries() { - getTimeseries(demo.apiUrl + "/hist/v4/performance/timeseries?ClientKey=" + demo.user.clientKey); + getTimeseries(demo.apiUrl + "/hist/v4/performance/timeseries?ClientKey=" + encodeURIComponent(demo.user.clientKey)); } /** @@ -88,7 +88,7 @@ * @return {void} */ function getAccountTimeseries() { - getTimeseries(demo.apiUrl + "/hist/v4/performance/timeseries?ClientKey=" + demo.user.clientKey + "&AccountKey=" + demo.user.accountKey); + getTimeseries(demo.apiUrl + "/hist/v4/performance/timeseries?ClientKey=" + encodeURIComponent(demo.user.clientKey) + "&AccountKey=" + encodeURIComponent(demo.user.accountKey)); } /** @@ -123,7 +123,7 @@ * @return {void} */ function getClientSummary() { - getSummary(demo.apiUrl + "/hist/v4/performance/summary?ClientKey=" + demo.user.clientKey); + getSummary(demo.apiUrl + "/hist/v4/performance/summary?ClientKey=" + encodeURIComponent(demo.user.clientKey)); } /** @@ -131,7 +131,7 @@ * @return {void} */ function getAccountSummary() { - getSummary(demo.apiUrl + "/hist/v4/performance/summary?ClientKey=" + demo.user.clientKey + "&AccountKey=" + demo.user.accountKey); + getSummary(demo.apiUrl + "/hist/v4/performance/summary?ClientKey=" + encodeURIComponent(demo.user.clientKey) + "&AccountKey=" + encodeURIComponent(demo.user.accountKey)); } /** diff --git a/orders/advice-orders/demo.js b/orders/advice-orders/demo.js index 5d75468..8d11b81 100644 --- a/orders/advice-orders/demo.js +++ b/orders/advice-orders/demo.js @@ -538,7 +538,7 @@ const clientKey = getClientKeyOfSelectedAccount(); const orderId = document.getElementById("idCbxOrderId").value; fetch( - demo.apiUrl + "/port/v1/orders/" + orderId + "?ClientKey=" + encodeURIComponent(clientKey), + demo.apiUrl + "/port/v1/orders/" + encodeURIComponent(demo.user.clientKey) + "/" + orderId, { "method": "GET", "headers": { diff --git a/orders/block-orders/demo.js b/orders/block-orders/demo.js index 6eeb30d..17effac 100644 --- a/orders/block-orders/demo.js +++ b/orders/block-orders/demo.js @@ -483,7 +483,7 @@ */ function getOrderDetails() { fetch( - demo.apiUrl + "/port/v1/orders/" + lastOrderId + "?ClientKey=" + demo.user.clientKey, + demo.apiUrl + "/port/v1/orders/" + encodeURIComponent(demo.user.clientKey) + "/" + lastOrderId, { "method": "GET", "headers": { diff --git a/orders/pre-market-and-after-hours/demo.js b/orders/pre-market-and-after-hours/demo.js index 31dd629..8525329 100644 --- a/orders/pre-market-and-after-hours/demo.js +++ b/orders/pre-market-and-after-hours/demo.js @@ -46,8 +46,48 @@ * Returns trading schedule for a given uic and asset type. * @return {void} */ - function getTradingSchedule() { + function getTradingSessions(sessions) { + const now = new Date(); + let currentTradingSession = "Undefined"; + let responseText = "Current local time: " + now.toLocaleTimeString() + "\n"; + sessions.forEach(function (session) { + const startTime = new Date(session.StartTime); + const endTime = new Date(session.EndTime); + if (now >= startTime && now < endTime) { + // This is the session we are in now, usually the first. + currentTradingSession = session.State; + responseText += "--> "; + } + responseText += "'" + session.State + "' from " + startTime.toLocaleString() + " to " + endTime.toLocaleString() + "\n"; + }); + switch (currentTradingSession) { + case "PreMarket": + case "PostMarket": + //case "PreTrading": + //case "PostTrading": + //case "PreAutomatedTrading": + //case "PostAutomatedTrading": + responseText += "\nWe are outside the AutomatedTrading session, but trading is possible because market is in an extended trading session."; + break; + case "AutomatedTrading": + responseText += "\nWe are in the regular 'AutomatedTrading' session."; + break; + case "Undefined": + responseText += "\nWe are in an unknown trading session. Please report this to Saxo, because this is wrong!"; + break; + default: + responseText += "\nThe market is closed with state: " + currentTradingSession; + } + console.log(responseText); + } + + /** + * Returns trading schedule for a given uic and asset type by using the TradingSchedule endpoint. + * @return {void} + */ + function getTradingSessionsFromTradingSchedule() { const newOrderObject = getOrderObjectFromJson(); + // Saxo has two endpoints serving the trading sessions. Choice is yours. fetch( demo.apiUrl + "/ref/v1/instruments/tradingschedule/" + newOrderObject.Uic + "/" + newOrderObject.AssetType, { @@ -59,18 +99,35 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - const now = new Date(); - let responseText = "Current local time: " + now.toLocaleTimeString() + "\n"; - responseJson.Sessions.forEach(function (session) { - const startTime = new Date(session.StartTime); - const endTime = new Date(session.EndTime); - if (now >= startTime && now < endTime) { - // This is the session we are in now, usually the first. - responseText += "--> "; - } - responseText += session.State + " from " + startTime.toLocaleString() + " to " + endTime.toLocaleString() + "\n"; - }); - console.log(responseText); + getTradingSessions(responseJson.Sessions); + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + + /** + * Returns trading schedule for a given uic and asset type by using the instrument details. + * @return {void} + */ + function getTradingSessionsFromInstrument() { + const newOrderObject = getOrderObjectFromJson(); + // Saxo has two endpoints serving the trading sessions. Choice is yours. + fetch( + demo.apiUrl + "/ref/v1/instruments/details/" + newOrderObject.Uic + "/" + newOrderObject.AssetType + "?FieldGroups=TradingSessions", + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + getTradingSessions(responseJson.TradingSessions.Sessions); }); } else { demo.processError(response); @@ -202,7 +259,7 @@ */ function getOrderDetails() { fetch( - demo.apiUrl + "/port/v1/orders/" + lastOrderId + "?ClientKey=" + demo.user.clientKey, + demo.apiUrl + "/port/v1/orders/" + encodeURIComponent(demo.user.clientKey) + "/" + lastOrderId, { "method": "GET", "headers": { @@ -319,7 +376,8 @@ } demo.setupEvents([ - {"evt": "click", "elmId": "idBtnGetTradingSchedule", "func": getTradingSchedule, "funcsToDisplay": [getTradingSchedule]}, + {"evt": "click", "elmId": "idBtnGetSessionsFromTradingSchedule", "func": getTradingSessionsFromTradingSchedule, "funcsToDisplay": [getTradingSessionsFromTradingSchedule, getTradingSessions]}, + {"evt": "click", "elmId": "idBtnGetSessionsFromInstrument", "func": getTradingSessionsFromInstrument, "funcsToDisplay": [getTradingSessionsFromInstrument, getTradingSessions]}, {"evt": "click", "elmId": "idBtnPreCheckOrder", "func": preCheckNewOrder, "funcsToDisplay": [preCheckNewOrder]}, {"evt": "click", "elmId": "idBtnPlaceNewOrder", "func": placeNewOrder, "funcsToDisplay": [placeNewOrder]}, {"evt": "click", "elmId": "idBtnGetOrderDetails", "func": getOrderDetails, "funcsToDisplay": [getOrderDetails]}, diff --git a/orders/pre-market-and-after-hours/index.html b/orders/pre-market-and-after-hours/index.html index 71a91e2..43f7bb3 100644 --- a/orders/pre-market-and-after-hours/index.html +++ b/orders/pre-market-and-after-hours/index.html @@ -51,7 +51,8 @@

Execute orders in the Pre Market and After Hours trading sessions

} }
- + +
diff --git a/orders/stocks/demo.js b/orders/stocks/demo.js index c16b49f..0bc7e9c 100644 --- a/orders/stocks/demo.js +++ b/orders/stocks/demo.js @@ -594,7 +594,7 @@ */ function getOrderDetails() { fetch( - demo.apiUrl + "/port/v1/orders/" + lastOrderId + "?ClientKey=" + demo.user.clientKey, + demo.apiUrl + "/port/v1/orders/" + encodeURIComponent(demo.user.clientKey) + "/" + lastOrderId, { "method": "GET", "headers": { From 97c60f113726acfec5a59b41900992dc6db1dca8 Mon Sep 17 00:00:00 2001 From: bas Date: Tue, 28 Jun 2022 15:54:27 +0200 Subject: [PATCH 10/26] Process different response --- orders/advice-orders/demo.js | 2 +- orders/block-orders/demo.js | 2 +- orders/pre-market-and-after-hours/demo.js | 2 +- orders/stocks/demo.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/orders/advice-orders/demo.js b/orders/advice-orders/demo.js index 8d11b81..6b77fb4 100644 --- a/orders/advice-orders/demo.js +++ b/orders/advice-orders/demo.js @@ -549,7 +549,7 @@ if (response.ok) { response.json().then(function (responseJson) { let orderObject; - if (responseJson === null) { + if (responseJson.Data.length === 0) { console.error("The order wasn't found in the list of active orders. Is order " + orderId + " still open?"); } else { // For the PATCH a limited set of fields is required. Feel free to add those fields you'd like to update. diff --git a/orders/block-orders/demo.js b/orders/block-orders/demo.js index 17effac..dbbf2e3 100644 --- a/orders/block-orders/demo.js +++ b/orders/block-orders/demo.js @@ -493,7 +493,7 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - if (responseJson === null) { + if (responseJson.Data.length === 0) { console.error("The order wasn't found in the list of active orders. Is order " + lastOrderId + " still open?"); } else { console.log("Order correlation: " + responseJson.CorrelationKey + "\n\nResponse: " + JSON.stringify(responseJson, null, 4)); diff --git a/orders/pre-market-and-after-hours/demo.js b/orders/pre-market-and-after-hours/demo.js index 8525329..b2f203e 100644 --- a/orders/pre-market-and-after-hours/demo.js +++ b/orders/pre-market-and-after-hours/demo.js @@ -269,7 +269,7 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - if (responseJson === null) { + if (responseJson.Data.length === 0) { console.error("The order wasn't found in the list of active orders. Is order " + lastOrderId + " still open?"); } else { console.log("Response: " + JSON.stringify(responseJson, null, 4)); diff --git a/orders/stocks/demo.js b/orders/stocks/demo.js index 0bc7e9c..b43e4cb 100644 --- a/orders/stocks/demo.js +++ b/orders/stocks/demo.js @@ -604,7 +604,7 @@ ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - if (responseJson === null) { + if (responseJson.Data.length === 0) { console.error("The order wasn't found in the list of active orders. Is order " + lastOrderId + " still open?"); } else { console.log("Response: " + JSON.stringify(responseJson, null, 4)); From 2429b9012ad5f95771847244c865f90bf7f7de84 Mon Sep 17 00:00:00 2001 From: bas Date: Tue, 28 Jun 2022 16:12:22 +0200 Subject: [PATCH 11/26] Some linting --- account-history/performance/demo.js | 40 +++++++++++------------ account-history/unsettled-amounts/demo.js | 21 +++++------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/account-history/performance/demo.js b/account-history/performance/demo.js index 28878a5..0b126b2 100644 --- a/account-history/performance/demo.js +++ b/account-history/performance/demo.js @@ -13,26 +13,26 @@ "footerElm": document.getElementById("idFooter") }); - addDateParam(-6, -1, "pastFiveDays") - addDateParam(-11, -1, "pastTenDays") + addDateParam(-6, -1, "pastFiveDays"); + addDateParam(-11, -1, "pastTenDays"); /** * Returns query parameters within the from and to range, for the named option - * @param {number} from - * @param {number} to - * @param {string} name + * @param {number} from + * @param {number} to + * @param {string} name */ function addDateParam(from, to, name) { - var dateParam = "" - var toDate = new Date() - toDate.setDate(toDate.getDate() + to) - var fromDate = new Date() - fromDate.setDate(fromDate.getDate() + from) - dateParam = "&FromDate=" + fromDate.toISOString().split("T")[0] + "&ToDate=" - dateParam += toDate.toISOString().split("T")[0] + var dateParam = ""; + var toDate = new Date(); + var fromDate = new Date(); + toDate.setDate(toDate.getDate() + to); + fromDate.setDate(fromDate.getDate() + from); + dateParam = "&FromDate=" + fromDate.toISOString().split("T")[0] + "&ToDate="; + dateParam += toDate.toISOString().split("T")[0]; document.getElementsByName(name).forEach(function (option) { - option.value = dateParam - }) + option.value = dateParam; + }); } /** * Reads the selected value of the dropdowns to return the relevant input for the request. @@ -41,10 +41,10 @@ */ function read(identifier) { const selected = document.getElementById("idCbx" + identifier).selectedOptions; + var fieldGroups = ""; + var i; //if set is returned(only FieldGroup), get all inputs if (selected.length > 1) { - let fieldGroups = ""; - let i; // forEach cannot be applied to this object, for reasons. for (i = 0; i < selected.length; i += 1) { //if All is included in selected, only return that entry. @@ -162,10 +162,10 @@ } demo.setupEvents([ - { "evt": "click", "elmId": "idBtnGetClientSummary", "func": getClientSummary, "funcsToDisplay": [getSummary, getClientSummary] }, - { "evt": "click", "elmId": "idBtnGetAccountSummary", "func": getAccountSummary, "funcsToDisplay": [getSummary, getAccountSummary] }, - { "evt": "click", "elmId": "idBtnGetClientTimeseries", "func": getClientTimeseries, "funcsToDisplay": [getTimeseries, getClientTimeseries] }, - { "evt": "click", "elmId": "idBtnGetAccountTimeseries", "func": getAccountTimeseries, "funcsToDisplay": [getTimeseries, getAccountTimeseries] } + {"evt": "click", "elmId": "idBtnGetClientSummary", "func": getClientSummary, "funcsToDisplay": [getSummary, getClientSummary]}, + {"evt": "click", "elmId": "idBtnGetAccountSummary", "func": getAccountSummary, "funcsToDisplay": [getSummary, getAccountSummary]}, + {"evt": "click", "elmId": "idBtnGetClientTimeseries", "func": getClientTimeseries, "funcsToDisplay": [getTimeseries, getClientTimeseries]}, + {"evt": "click", "elmId": "idBtnGetAccountTimeseries", "func": getAccountTimeseries, "funcsToDisplay": [getTimeseries, getAccountTimeseries]} ]); demo.displayVersion("hist"); }()); diff --git a/account-history/unsettled-amounts/demo.js b/account-history/unsettled-amounts/demo.js index 76f47ae..cb5f8e7 100644 --- a/account-history/unsettled-amounts/demo.js +++ b/account-history/unsettled-amounts/demo.js @@ -74,10 +74,10 @@ * @returns {string} */ function parseResponse(response, requestType, requestUrl) { - console.log(requestUrl) + console.log(requestUrl); let endpointDisplay = "Endpoint: \n\t" + requestUrl.split("?")[0].split("openapi")[1] + "?\nParameters: \n\t" + requestUrl.split("?")[1].replaceAll("&", "&\n\t") + "\n"; if (requestType === "Instruments") { - return "Instruments for which amounts are owed\n" + endpointDisplay + return "Instruments for which amounts are owed\n" + endpointDisplay; } let details = []; @@ -127,7 +127,7 @@ if (amountTypeSource !== "All") { parameters += "&AmountTypeSource=" + amountTypeSource; } - let endpoint = demo.apiUrl + "/hist/v1/unsettledamounts" + parameters + let endpoint = demo.apiUrl + "/hist/v1/unsettledamounts" + parameters; fetch( endpoint, { @@ -167,7 +167,7 @@ console.log("You must select a Currency and AmountTypeId first.\nIf the dropdown is empty, execute the 'Get amounts by amount type' first"); return; } - let endpoint = demo.apiUrl + "/hist/v1/unsettledamounts/instruments" + parameters + let endpoint = demo.apiUrl + "/hist/v1/unsettledamounts/instruments" + parameters; fetch( endpoint, { @@ -261,15 +261,12 @@ }); } - - - demo.setupEvents([ - { "evt": "click", "elmId": "idBtnGetUnsettledAmountsByCurrency", "func": getUnsettledAmountsByCurrencyClick, "funcsToDisplay": [getUnsettledAmountsByCurrencyClick, getUnsettledAmountsByCurrency] }, - { "evt": "click", "elmId": "idBtnGetUnsettledAmountsByAmountType", "func": getUnsettledAmountsByAmountTypeClick, "funcsToDisplay": [getUnsettledAmountsByAmountTypeClick, getUnsettledAmountsByCurrency] }, - { "evt": "click", "elmId": "idBtnGetUnsettledAmountsByInstruments", "func": getUnsettledAmountsByInstruments, "funcsToDisplay": [getUnsettledAmountsByInstruments] }, - { "evt": "click", "elmId": "idBtnGetUnsettledAmountsByExchange", "func": getUnsettledAmountsByExchange, "funcsToDisplay": [getUnsettledAmountsByExchange] }, - { "evt": "click", "elmId": "idBtnGetUnsettledAmountsForExchange", "func": getUnsettledAmountsForExchange, "funcsToDisplay": [getUnsettledAmountsForExchange] } + {"evt": "click", "elmId": "idBtnGetUnsettledAmountsByCurrency", "func": getUnsettledAmountsByCurrencyClick, "funcsToDisplay": [getUnsettledAmountsByCurrencyClick, getUnsettledAmountsByCurrency]}, + {"evt": "click", "elmId": "idBtnGetUnsettledAmountsByAmountType", "func": getUnsettledAmountsByAmountTypeClick, "funcsToDisplay": [getUnsettledAmountsByAmountTypeClick, getUnsettledAmountsByCurrency]}, + {"evt": "click", "elmId": "idBtnGetUnsettledAmountsByInstruments", "func": getUnsettledAmountsByInstruments, "funcsToDisplay": [getUnsettledAmountsByInstruments]}, + {"evt": "click", "elmId": "idBtnGetUnsettledAmountsByExchange", "func": getUnsettledAmountsByExchange, "funcsToDisplay": [getUnsettledAmountsByExchange]}, + {"evt": "click", "elmId": "idBtnGetUnsettledAmountsForExchange", "func": getUnsettledAmountsForExchange, "funcsToDisplay": [getUnsettledAmountsForExchange]} ]); demo.displayVersion("hist"); }()); From 9409d623acfa0cdac491a971d826cf602653d84e Mon Sep 17 00:00:00 2001 From: bas Date: Thu, 30 Jun 2022 09:44:11 +0200 Subject: [PATCH 12/26] Find supported exchanges --- instruments/instrument-retrieval/demo.js | 14 +++--- orders/pre-market-and-after-hours/demo.js | 46 ++++++++++++++++++++ orders/pre-market-and-after-hours/index.html | 2 + 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/instruments/instrument-retrieval/demo.js b/instruments/instrument-retrieval/demo.js index bb712e0..7cc8da1 100644 --- a/instruments/instrument-retrieval/demo.js +++ b/instruments/instrument-retrieval/demo.js @@ -72,6 +72,13 @@ } function processDetailsListResponse(assetType, responseJson) { + // We have the Uic - collect the details + responseJson.Data.forEach(function (instrument) { + const filterOnExchangeId = document.getElementById("idCbxExchange").value; + if (filterOnExchangeId === "-" || filterOnExchangeId === instrument.Exchange.ExchangeId) { + instrumentIds.push(instrument.AssetType + "," + instrument.Uic + "," + instrument.Exchange.ExchangeId + ",\"" + instrument.Description.trim() + "\""); + } + }); if (responseJson.hasOwnProperty("__next")) { // Recursively get next bulk console.debug("Found '__next': " + responseJson.__next); @@ -81,13 +88,6 @@ "callback": processDetailsListResponse }); } - // We have the Uic - collect the details - responseJson.Data.forEach(function (instrument) { - const filterOnExchangeId = document.getElementById("idCbxExchange").value; - if (filterOnExchangeId === "-" || filterOnExchangeId === instrument.Exchange.ExchangeId) { - instrumentIds.push(instrument.AssetType + "," + instrument.Uic + "," + instrument.Exchange.ExchangeId + ",\"" + instrument.Description.trim() + "\""); - } - }); } function processOptionSearchResponse(assetType, responseJson) { diff --git a/orders/pre-market-and-after-hours/demo.js b/orders/pre-market-and-after-hours/demo.js index b2f203e..24329bd 100644 --- a/orders/pre-market-and-after-hours/demo.js +++ b/orders/pre-market-and-after-hours/demo.js @@ -42,6 +42,51 @@ return newOrderObject; } + /** + * Retrieve all exchanges and filter the ones with 'PreMarket' and 'PostMarket' trading sessions. + * @return {void} + */ + function getSupportedExchanges() { + fetch( + demo.apiUrl + "/ref/v1/exchanges?$top=1000", // Get the first 1.000 (actually there are around 200 exchanges available) + { + "method": "GET", + "headers": { + "Authorization": "Bearer " + document.getElementById("idBearerToken").value + } + } + ).then(function (response) { + if (response.ok) { + response.json().then(function (responseJson) { + const now = new Date(); + let responseText = ""; + responseJson.Data.forEach(function (exchange) { + exchange.ExchangeSessions.forEach(function (session) { + const startTime = new Date(session.StartTime); + const endTime = new Date(session.EndTime); + if (session.State === "PreMarket" || session.State === "PostMarket") { + if (now >= startTime && now < endTime) { + // This is the session we are in now. + responseText += "--> "; + } + responseText += exchange.Name + " (" + exchange.ExchangeId + ") has state '" + session.State + "' from " + startTime.toLocaleString() + " to " + endTime.toLocaleString() + "\n"; + } + }); + }); + if (responseText === "") { + console.log("No exchanges found with support for Pre-, or PostMarket trading."); + } else { + console.log(responseText); + } + }); + } else { + demo.processError(response); + } + }).catch(function (error) { + console.error(error); + }); + } + /** * Returns trading schedule for a given uic and asset type. * @return {void} @@ -376,6 +421,7 @@ } demo.setupEvents([ + {"evt": "click", "elmId": "idBtnGetSupportedExchanges", "func": getSupportedExchanges, "funcsToDisplay": [getSupportedExchanges]}, {"evt": "click", "elmId": "idBtnGetSessionsFromTradingSchedule", "func": getTradingSessionsFromTradingSchedule, "funcsToDisplay": [getTradingSessionsFromTradingSchedule, getTradingSessions]}, {"evt": "click", "elmId": "idBtnGetSessionsFromInstrument", "func": getTradingSessionsFromInstrument, "funcsToDisplay": [getTradingSessionsFromInstrument, getTradingSessions]}, {"evt": "click", "elmId": "idBtnPreCheckOrder", "func": preCheckNewOrder, "funcsToDisplay": [preCheckNewOrder]}, diff --git a/orders/pre-market-and-after-hours/index.html b/orders/pre-market-and-after-hours/index.html index 43f7bb3..719ce4f 100644 --- a/orders/pre-market-and-after-hours/index.html +++ b/orders/pre-market-and-after-hours/index.html @@ -51,6 +51,8 @@

Execute orders in the Pre Market and After Hours trading sessions

} }
+ +

From 444b520547f0aaffc22b283da5ebe3d41be29d80 Mon Sep 17 00:00:00 2001 From: bas Date: Fri, 1 Jul 2022 16:55:09 +0200 Subject: [PATCH 13/26] Update ENS sample --- assets/js/boilerplate.js | 27 ++- .../corporateaction-events-monitoring/demo.js | 37 ++- websockets/historical-market-data/demo.js | 4 +- websockets/options-chain/demo.js | 22 +- websockets/order-events-monitoring/demo.js | 228 ++++++++++-------- websockets/order-events-monitoring/index.html | 87 ++++++- websockets/primary-monitoring/demo.js | 6 +- websockets/protobuf/demo.js | 15 +- websockets/realtime-quotes/demo.js | 15 +- websockets/trade-messages/demo.js | 13 +- 10 files changed, 288 insertions(+), 166 deletions(-) diff --git a/assets/js/boilerplate.js b/assets/js/boilerplate.js index 6b42711..f84c317 100644 --- a/assets/js/boilerplate.js +++ b/assets/js/boilerplate.js @@ -2,7 +2,7 @@ /*global console */ /* - * boilerplate v1.29 + * boilerplate v1.30 * * This script contains a set of helper functions for validating the token and populating the account selection. * Logging to the console is mirrored to the output in the examples. @@ -410,6 +410,10 @@ function demonstrationHelper(settings) { */ function getDataFromApi() { + /** + * For security reasons a welcome message can be displayed. The customer can verify if this is as expected. + * @return {void} + */ function showWelcomeMessage(responseJson) { const lastLoginTime = new Date(responseJson.LastLoginTime); let message = "Welcome " + responseJson.Name + "."; @@ -427,6 +431,19 @@ function demonstrationHelper(settings) { console.log(message); } + /** + * Fire an event to let possible subscribers (in demo.js) know that accountKey, clientKey and userKey are available. + * @return {void} + */ + function triggerDemoDataLoadedEvent() { + const demoDataLoadedEvent = new Event("demoDataLoaded"); + document.dispatchEvent(demoDataLoadedEvent); + } + + /** + * Response is received. It is a BATCH response (https://github.com/SaxoBank/openapi-samples-js/tree/main/batch-request). Unpack it, and populate required fields. + * @return {void} + */ function processBatchResponse(responseArray) { const requestIdMarker = "X-Request-Id:"; let requestId = ""; @@ -468,6 +485,7 @@ function demonstrationHelper(settings) { } }); console.log("The token is valid - hello " + user.name + "\nUserId: " + userId + "\nClientId: " + clientId); + triggerDemoDataLoadedEvent(); functionToRun(); // Run the function } @@ -552,7 +570,12 @@ function demonstrationHelper(settings) { * @return {void} */ function setupEvent(eventToSetup) { - document.getElementById(eventToSetup.elmId).addEventListener(eventToSetup.evt, function () { + const elm = ( + eventToSetup.elmId === "" + ? document + : document.getElementById(eventToSetup.elmId) + ); + elm.addEventListener(eventToSetup.evt, function () { run(eventToSetup.func); displaySourceCode(eventToSetup.funcsToDisplay); }); diff --git a/websockets/corporateaction-events-monitoring/demo.js b/websockets/corporateaction-events-monitoring/demo.js index dc02854..b923e9c 100644 --- a/websockets/corporateaction-events-monitoring/demo.js +++ b/websockets/corporateaction-events-monitoring/demo.js @@ -1,4 +1,4 @@ -/*jslint this: true, browser: true, long: true, bitwise: true */ +/*jslint this: true, browser: true, long: true, bitwise: true, unordered: true */ /*global window console demonstrationHelper */ /** @@ -83,7 +83,6 @@ } } - /** * This is an example of subscribing to ENS, which notifies about corporate action activities: Announcement, update and payment * @return {void} @@ -99,9 +98,9 @@ ], "CorporateActionEventFilter": { "CANotificationTypes": [ - "Announcement", - "Update", - "Payment" + "Announcement", + "Update", + "Payment" ] }, "FieldGroups": [ @@ -110,7 +109,6 @@ ] } }; - if (ensSubscription.lastSequenceId !== null) { // If specified and message with SequenceId available in ENS cache, streaming of events start from SequenceId. // If sequenceId not found in ENS system, Subscription Error with "Sequence id unavailable". @@ -190,9 +188,7 @@ const contextId = document.getElementById("idContextId").value; const urlPathEns = "/ens/v1/activities/subscriptions/" + encodeURIComponent(contextId); - removeSubscription(ensSubscription.isActive, urlPathEns, function () { - console.log("ENS unsubscribed."); - }); + removeSubscription(ensSubscription.isActive, urlPathEns, callbackOnSuccess); } /** @@ -201,6 +197,7 @@ */ function recreateSubscriptions() { unsubscribe(function () { + console.log("ENS unsubscribed."); if (ensSubscription.isActive) { subscribeEns(); // Resubscribe and retrieve missed messages, if any } @@ -257,11 +254,12 @@ /** * This function processes the heartbeat messages, containing info about system health. * https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages + * @param {number} messageId The message sequence number * @param {Array} payload The list of messages * @return {void} */ - function handleHeartbeat(payload) { - // Heartbeat messages are sent every 20 seconds. If there is a minute without messages, this is an error. + function handleHeartbeat(messageId, payload) { + // Heartbeat messages are sent every "responseJson.InactivityTimeout" seconds. If there is a minute without messages, this indicates an error. if (Array.isArray(payload)) { payload.forEach(function (heartbeatMessages) { heartbeatMessages.Heartbeats.forEach(function (heartbeat) { @@ -276,7 +274,7 @@ ensSubscription.isRecentDataReceived = true; break; } - console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString()); + console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString() + " (#" + messageId + ")"); break; default: console.error("Unknown heartbeat message received: " + JSON.stringify(payload)); @@ -425,11 +423,11 @@ // Remember the last SequenceId. This can be used to retrieve the gap after an unwanted disconnect. getNewLastSequenceId(message.payload); ensSubscription.isRecentDataReceived = true; - console.log("Streaming corporate action activities from ENS " + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); + console.log("Streaming corporate action activities from ENS #" + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); break; case "_heartbeat": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages - handleHeartbeat(message.payload); + handleHeartbeat(message.messageId, message.payload); break; case "_resetsubscriptions": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages @@ -479,14 +477,6 @@ }); } - /** - * When you want to use a different account, there is no need to setup a different connection. Just delete the existing subscription and open a new subscription. - * @return {void} - */ - function switchAccount() { - recreateSubscriptions(); - } - /** * This is an example of unsubscribing, including a reset of the state. * @return {void} @@ -494,6 +484,7 @@ function unsubscribeAndResetState() { unsubscribe(function () { ensSubscription.isActive = false; + console.log("ENS unsubscribed."); }); } @@ -508,7 +499,7 @@ window.clearInterval(ensSubscription.activityMonitor); } - document.getElementById("idContextId").value = "MyApp_CoAc_" + Date.now(); // Some unique + document.getElementById("idContextId").value = "MyApp_CoAc_" + Date.now(); // Some unique value document.getElementById("idReferenceId").value = ensSubscription.reference; // Display reference id demo.setupEvents([ {"evt": "click", "elmId": "idBtnCreateConnection", "func": createConnection, "funcsToDisplay": [createConnection]}, diff --git a/websockets/historical-market-data/demo.js b/websockets/historical-market-data/demo.js index 5313dae..2805cad 100644 --- a/websockets/historical-market-data/demo.js +++ b/websockets/historical-market-data/demo.js @@ -256,10 +256,10 @@ messages.forEach(function (message) { switch (message.referenceId) { case "MyChartDataEvent": - console.log("Streaming trade level change event " + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); + console.log("Streaming chart data event #" + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); break; case "_heartbeat": - console.debug("Heartbeat event " + message.messageId + " received: " + JSON.stringify(message.payload)); + console.debug("Heartbeat event #" + message.messageId + " received: " + JSON.stringify(message.payload)); break; case "_resetsubscriptions": // The server is not able to send messages and client needs to reset subscriptions by recreating them. diff --git a/websockets/options-chain/demo.js b/websockets/options-chain/demo.js index ed0bf60..8acf235 100644 --- a/websockets/options-chain/demo.js +++ b/websockets/options-chain/demo.js @@ -252,6 +252,14 @@ * @return {void} */ function prepareSnapshotForWindowUpdates() { + + function createEmptyStrike(nameOfSide1, nameOfSide2) { + const emptyStrike = {}; + emptyStrike[nameOfSide1] = {}; + emptyStrike[nameOfSide2] = {}; + return emptyStrike; + } + snapshot.Expiries.forEach(function (expiry) { const nameOfSide1 = ( isFxBinaryOption() @@ -278,10 +286,7 @@ expiry.Strikes = []; // Add empty strikes for (i = 0; i < expiry.StrikeCount; i += 1) { - expiry.Strikes.push({ - [nameOfSide1]: {}, - [nameOfSide2]: {} - }); + expiry.Strikes.push(createEmptyStrike(nameOfSide1, nameOfSide2)); } } }); @@ -505,11 +510,12 @@ /** * This function processes the heartbeat messages, containing info about system health. * https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages + * @param {number} messageId The message sequence number * @param {Array} payload The list of messages * @return {void} */ - function handleHeartbeat(payload) { - // Heartbeat messages are sent every 20 seconds. If there is a minute without messages, this is an error. + function handleHeartbeat(messageId, payload) { + // Heartbeat messages are sent every "responseJson.InactivityTimeout" seconds. If there is a minute without messages, this indicates an error. if (Array.isArray(payload)) { payload.forEach(function (heartbeatMessages) { heartbeatMessages.Heartbeats.forEach(function (heartbeat) { @@ -524,7 +530,7 @@ optionsChainSubscription.isRecentDataReceived = true; break; } - console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString()); + console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString() + " (#" + messageId + ")"); break; default: console.error("Unknown heartbeat message received: " + JSON.stringify(payload)); @@ -660,7 +666,7 @@ break; case "_heartbeat": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages - handleHeartbeat(message.payload); + handleHeartbeat(message.messageId, message.payload); break; case "_resetsubscriptions": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages diff --git a/websockets/order-events-monitoring/demo.js b/websockets/order-events-monitoring/demo.js index 2cbf28a..4de7e74 100644 --- a/websockets/order-events-monitoring/demo.js +++ b/websockets/order-events-monitoring/demo.js @@ -1,4 +1,4 @@ -/*jslint this: true, browser: true, long: true, bitwise: true */ +/*jslint this: true, browser: true, long: true, bitwise: true, unordered: true */ /*global window console demonstrationHelper */ /** @@ -18,26 +18,26 @@ "footerElm": document.getElementById("idFooter") }); // These objects contains the state of the subscriptions, so a reconnect can be processed and health can be monitored. - const balanceSubscription = { - "reference": "MyBalanceEvent", + const ensSubscription = { + "reference": "", "isActive": false, "activityMonitor": null, - "isRecentDataReceived": false + "isRecentDataReceived": false, + "lastSequenceId": null }; - const positionSubscription = { - "reference": "MyPositionEvent", + const balanceSubscription = { + "reference": "", "isActive": false, "activityMonitor": null, "isRecentDataReceived": false }; - const ensSubscription = { - "reference": "MyEnsEvent", + const positionSubscription = { + "reference": "", "isActive": false, "activityMonitor": null, - "isRecentDataReceived": false, - "lastSequenceId": null + "isRecentDataReceived": false }; - let connection; + let connection = null; /** * Test if the browser supports the features required for websockets. @@ -52,22 +52,51 @@ ); } + /** + * Get the input data and convert it to an object to be sent as request body. + * @param {string} textAreaId Identifies which textarea element to use. + * @return {Object} The object from the input field - null if invalid. + */ + function getObjectFromTextArea(textAreaId) { + let object = null; + try { + object = JSON.parse(document.getElementById(textAreaId).value); + if (object.hasOwnProperty("Arguments")) { + if (object.Arguments.hasOwnProperty("AccountKey")) { + object.Arguments.AccountKey = demo.user.accountKey; + } + if (object.Arguments.hasOwnProperty("ClientKey")) { + object.Arguments.ClientKey = demo.user.clientKey; + } + } + document.getElementById(textAreaId).value = JSON.stringify(object, null, 4); + } catch (e) { + console.error(e); + } + return object; + } + + /** + * Get the ContextId from the ENS post body. + * @return {string} The ContextId which was entered in the TextArea. + */ + function getContextId() { + const data = getObjectFromTextArea("idEnsRequestObject"); + return data.ContextId; + } + /** * This is an example of constructing the websocket connection. * @return {void} */ function createConnection() { const accessToken = document.getElementById("idBearerToken").value; - const contextId = encodeURIComponent(document.getElementById("idContextId").value); + const contextId = encodeURIComponent(getContextId()); const streamerUrl = demo.streamerUrl + "?authorization=" + encodeURIComponent("BEARER " + accessToken) + "&contextId=" + contextId; if (!isWebSocketsSupportedByBrowser()) { console.error("This browser doesn't support WebSockets."); throw "This browser doesn't support WebSockets."; } - if (contextId !== document.getElementById("idContextId").value) { - console.error("Invalid characters in Context ID."); - throw "Invalid characters in Context ID."; - } try { connection = new window.WebSocket(streamerUrl); connection.binaryType = "arraybuffer"; @@ -96,24 +125,22 @@ } /** - * This is an example of subscribing to changes in the account balance. + * This is an example of subscribing to ENS, which notifies about order and position events. And withdrawals and deposits are published on this subscription. * @return {void} */ - function subscribeBalances() { - const data = { - "ContextId": document.getElementById("idContextId").value, - "ReferenceId": balanceSubscription.reference, - "RefreshRate": 10000, // Default is every second, which probably is too chatty - "Arguments": { - "AccountKey": demo.user.accountKey, - "ClientKey": demo.user.clientKey, - "FieldGroups": [ - "MarginOverview" - ] - } - }; + function subscribeEns() { + const data = getObjectFromTextArea("idEnsRequestObject"); + if (ensSubscription.lastSequenceId !== null) { + // If specified and message with SequenceId available in ENS cache, streaming of events start from SequenceId. + // If sequenceId not found in ENS system, Subscription Error with "Sequence id unavailable". + // If not specified and FromDateTime is not specified, subscription will be real-time subscription. + data.Arguments.SequenceId = ensSubscription.lastSequenceId; + document.getElementById("idEnsRequestObject").value = JSON.stringify(data, null, 4); + console.log("Supplied last received SequenceId to retrieve gap: " + ensSubscription.lastSequenceId); + } + ensSubscription.reference = data.ReferenceId; fetch( - demo.apiUrl + "/port/v1/balances/subscriptions", + demo.apiUrl + "/ens/v1/activities/subscriptions", { "method": "POST", "headers": { @@ -124,16 +151,20 @@ } ).then(function (response) { if (response.ok) { - balanceSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. - balanceSubscription.isActive = true; + ensSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. + ensSubscription.isActive = true; response.json().then(function (responseJson) { // Monitor connection every "InactivityTimeout" seconds. - if (balanceSubscription.activityMonitor === null) { - balanceSubscription.activityMonitor = window.setInterval(function () { - monitorActivity(balanceSubscription); + if (ensSubscription.activityMonitor === null) { + ensSubscription.activityMonitor = window.setInterval(function () { + monitorActivity(ensSubscription); }, responseJson.InactivityTimeout * 1000); } - console.log("Subscription for balance changes created with readyState " + connection.readyState + " and data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); + console.log("Subscription for order changes created with" + ( + connection === null + ? "" + : " readyState " + connection.readyState + " and" + ) + " data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); }); } else { demo.processError(response); @@ -144,22 +175,14 @@ } /** - * This is an example of subscribing to changes in net positions. - * These changes are published in ENS as well, this can be used as a catch up, every minute, because due to price changes is has many updates. + * This is an example of subscribing to changes in the account balance. * @return {void} */ - function subscribePositions() { - const data = { - "ContextId": document.getElementById("idContextId").value, - "ReferenceId": positionSubscription.reference, - "RefreshRate": 60000, // Default is every second, which probably is too chatty - "Arguments": { - "AccountKey": demo.user.accountKey, - "ClientKey": demo.user.clientKey - } - }; + function subscribeBalances() { + const data = getObjectFromTextArea("idBalancesRequestObject"); + balanceSubscription.reference = data.ReferenceId; fetch( - demo.apiUrl + "/port/v1/netpositions/subscriptions", + demo.apiUrl + "/port/v1/balances/subscriptions", { "method": "POST", "headers": { @@ -170,16 +193,20 @@ } ).then(function (response) { if (response.ok) { - positionSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. - positionSubscription.isActive = true; + balanceSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. + balanceSubscription.isActive = true; response.json().then(function (responseJson) { // Monitor connection every "InactivityTimeout" seconds. - if (positionSubscription.activityMonitor === null) { - positionSubscription.activityMonitor = window.setInterval(function () { - monitorActivity(positionSubscription); + if (balanceSubscription.activityMonitor === null) { + balanceSubscription.activityMonitor = window.setInterval(function () { + monitorActivity(balanceSubscription); }, responseJson.InactivityTimeout * 1000); } - console.log("Subscription for position changes created with readyState " + connection.readyState + " and data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); + console.log("Subscription for balance changes created with" + ( + connection === null + ? "" + : " readyState " + connection.readyState + " and" + ) + " data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); }); } else { demo.processError(response); @@ -190,35 +217,15 @@ } /** - * This is an example of subscribing to ENS, which notifies about order and position events. And withdrawals and deposits are published on this subscription. + * This is an example of subscribing to changes in net positions. + * These changes are published in ENS as well, this can be used as a catch up, every minute, because due to price changes is has many updates. * @return {void} */ - function subscribeEns() { - const data = { - "ContextId": document.getElementById("idContextId").value, - "ReferenceId": ensSubscription.reference, - "Arguments": { - "AccountKey": demo.user.accountKey, - "Activities": [ - "AccountFundings", - "Orders", - "Positions" - ], - "FieldGroups": [ - "DisplayAndFormat", - "ExchangeInfo" - ] - } - }; - if (ensSubscription.lastSequenceId !== null) { - // If specified and message with SequenceId available in ENS cache, streaming of events start from SequenceId. - // If sequenceId not found in ENS system, Subscription Error with "Sequence id unavailable". - // If not specified and FromDateTime is not specified, subscription will be real-time subscription. - data.Arguments.SequenceId = ensSubscription.lastSequenceId; - console.log("Supplied last received SequenceId to retrieve gap: " + ensSubscription.lastSequenceId); - } + function subscribePositions() { + const data = getObjectFromTextArea("idPositionsRequestObject"); + positionSubscription.reference = data.ReferenceId; fetch( - demo.apiUrl + "/ens/v1/activities/subscriptions", + demo.apiUrl + "/port/v1/netpositions/subscriptions", { "method": "POST", "headers": { @@ -229,16 +236,20 @@ } ).then(function (response) { if (response.ok) { - ensSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. - ensSubscription.isActive = true; + positionSubscription.isRecentDataReceived = true; // Start positive, will be set to 'false' after the next monitor health check. + positionSubscription.isActive = true; response.json().then(function (responseJson) { // Monitor connection every "InactivityTimeout" seconds. - if (ensSubscription.activityMonitor === null) { - ensSubscription.activityMonitor = window.setInterval(function () { - monitorActivity(ensSubscription); + if (positionSubscription.activityMonitor === null) { + positionSubscription.activityMonitor = window.setInterval(function () { + monitorActivity(positionSubscription); }, responseJson.InactivityTimeout * 1000); } - console.log("Subscription for order changes created with readyState " + connection.readyState + " and data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); + console.log("Subscription for position changes created with" + ( + connection === null + ? "" + : " readyState " + connection.readyState + " and" + ) + " data: " + JSON.stringify(data, null, 4) + ".\n\nResponse: " + JSON.stringify(responseJson, null, 4)); }); } else { demo.processError(response); @@ -287,7 +298,7 @@ } } - const contextId = document.getElementById("idContextId").value; + const contextId = getContextId(); const urlPathEns = "/ens/v1/activities/subscriptions/" + encodeURIComponent(contextId); const urlPathBalance = "/port/v1/balances/subscriptions/" + encodeURIComponent(contextId); const urlPathPosition = "/port/v1/netpositions/subscriptions/" + encodeURIComponent(contextId); @@ -366,11 +377,12 @@ /** * This function processes the heartbeat messages, containing info about system health. * https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages + * @param {number} messageId The message sequence number * @param {Array} payload The list of messages * @return {void} */ - function handleHeartbeat(payload) { - // Heartbeat messages are sent every 20 seconds. If there is a minute without messages, this is an error. + function handleHeartbeat(messageId, payload) { + // Heartbeat messages are sent every "responseJson.InactivityTimeout" seconds. If there is a minute without messages, this indicates an error. if (Array.isArray(payload)) { payload.forEach(function (heartbeatMessages) { heartbeatMessages.Heartbeats.forEach(function (heartbeat) { @@ -391,7 +403,7 @@ positionSubscription.isRecentDataReceived = true; break; } - console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString()); + console.debug("No data, but heartbeat received for " + heartbeat.OriginatingReferenceId + " @ " + new Date().toLocaleTimeString() + " (#" + messageId + ")"); break; default: console.error("Unknown heartbeat message received: " + JSON.stringify(payload)); @@ -540,19 +552,19 @@ // Remember the last SequenceId. This can be used to retrieve the gap after an unwanted disconnect. getNewLastSequenceId(message.payload); ensSubscription.isRecentDataReceived = true; - console.log("Streaming order/position/fundings event from ENS " + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); + console.log("Streaming order/position/fundings event from ENS #" + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); break; case balanceSubscription.reference: balanceSubscription.isRecentDataReceived = true; - console.log("Streaming balance change event " + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); + console.log("Streaming balance change event #" + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); break; case positionSubscription.reference: positionSubscription.isRecentDataReceived = true; - console.log("Streaming position change event " + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); + console.log("Streaming position change event #" + message.messageId + " received: " + JSON.stringify(message.payload, null, 4)); break; case "_heartbeat": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages - handleHeartbeat(message.payload); + handleHeartbeat(message.messageId, message.payload); break; case "_resetsubscriptions": // https://www.developer.saxo/openapi/learn/plain-websocket-streaming#PlainWebSocketStreaming-Controlmessages @@ -584,7 +596,7 @@ */ function extendSubscription() { fetch( - demo.apiUrl + "/streamingws/authorize?contextid=" + encodeURIComponent(document.getElementById("idContextId").value), + demo.apiUrl + "/streamingws/authorize?contextid=" + encodeURIComponent(getContextId()), { "method": "PUT", "headers": { @@ -635,8 +647,28 @@ window.clearInterval(ensSubscription.activityMonitor); } - document.getElementById("idContextId").value = "MyApp_" + Date.now(); // Some unique value + /** + * Sync data of js config with the edits in the HTML. + * @return {void} + */ + function populateTextAreas() { + const defaultContextId = "MyApp_" + Date.now(); // Some unique value + const lastWeek = new Date(); + let data = getObjectFromTextArea("idEnsRequestObject"); + lastWeek.setDate(lastWeek.getDate() - 7); + data.ContextId = defaultContextId; + data.Arguments.FromDateTime = lastWeek.toISOString(); + document.getElementById("idEnsRequestObject").value = JSON.stringify(data, null, 4); + data = getObjectFromTextArea("idBalancesRequestObject"); + data.ContextId = defaultContextId; + document.getElementById("idBalancesRequestObject").value = JSON.stringify(data, null, 4); + data = getObjectFromTextArea("idPositionsRequestObject"); + data.ContextId = defaultContextId; + document.getElementById("idPositionsRequestObject").value = JSON.stringify(data, null, 4); + } + demo.setupEvents([ + {"evt": "demoDataLoaded", "elmId": "", "func": populateTextAreas, "funcsToDisplay": [populateTextAreas]}, {"evt": "click", "elmId": "idBtnCreateConnection", "func": createConnection, "funcsToDisplay": [createConnection]}, {"evt": "click", "elmId": "idBtnStartListener", "func": startListener, "funcsToDisplay": [startListener]}, {"evt": "click", "elmId": "idBtnSubscribeEns", "func": subscribeEns, "funcsToDisplay": [subscribeEns]}, diff --git a/websockets/order-events-monitoring/index.html b/websockets/order-events-monitoring/index.html index 8726df3..50d3f3f 100644 --- a/websockets/order-events-monitoring/index.html +++ b/websockets/order-events-monitoring/index.html @@ -9,6 +9,12 @@ Demo for Monitoring the Order Events +