Compare commits

...

6 Commits
11368 ... 10652

Author SHA1 Message Date
Stefano Raggi
e231e31a1a Trigger build 2021-02-08 15:57:34 +01:00
Stefano Raggi
52ee8c7c82 Bug fixes + unit test updates 2021-02-08 15:40:45 +01:00
Stefano Raggi
e3cd533c4a Add sandbox check in Subscribe 2021-02-08 15:40:45 +01:00
Stefano Raggi
63ff835de9 Update Tradier config.json settings 2021-02-08 15:40:45 +01:00
Stefano Raggi
5a517e3201 Fix brokerage unit tests 2021-02-08 15:40:45 +01:00
Stefano Raggi
70217d38de Tradier brokerage updates
- Removed old authentication code (refresh tokens) and settings
- Added "tradier-use-sandbox" config setting
2021-02-08 15:40:45 +01:00
9 changed files with 126 additions and 362 deletions

View File

@@ -1,11 +1,11 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -157,12 +157,12 @@ namespace QuantConnect.Brokerages.Tradier
/// Bid Price
[JsonProperty(PropertyName = "bid")]
public decimal Bid = 0;
public decimal? Bid = 0;
/// Bid Size:
[JsonProperty(PropertyName = "bidsize")]
public decimal BidSize = 0;
/// Bid Exchange
[JsonProperty(PropertyName = "bidexch")]
public string BigExchange = "";
@@ -173,7 +173,7 @@ namespace QuantConnect.Brokerages.Tradier
/// Asking Price
[JsonProperty(PropertyName = "ask")]
public decimal Ask = 0;
public decimal? Ask = 0;
/// Asking Quantity
[JsonProperty(PropertyName = "asksize")]
@@ -278,7 +278,7 @@ namespace QuantConnect.Brokerages.Tradier
[JsonProperty(PropertyName = "next_change")]
public string NextChange;
/// Market Status: State
/// Market Status: State
[JsonProperty(PropertyName = "state")]
public string State;
@@ -397,7 +397,7 @@ namespace QuantConnect.Brokerages.Tradier
/// </summary>
public class TradierStreamSession
{
/// Trading Stream: Session Id
/// Trading Stream: Session Id
public string SessionId;
/// Trading Stream: Stream URL
public string Url;

View File

@@ -15,7 +15,6 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -63,6 +62,13 @@ namespace QuantConnect.Brokerages.Tradier
/// <returns>The new enumerator for this subscription request</returns>
public IEnumerator<BaseData> Subscribe(SubscriptionDataConfig dataConfig, EventHandler newDataAvailableHandler)
{
// streaming is not supported by sandbox
if (_useSandbox)
{
throw new NotSupportedException(
"TradierBrokerage.DataQueueHandler.Subscribe(): The sandbox does not support data streaming.");
}
if (!CanSubscribe(dataConfig.Symbol))
{
return Enumerable.Empty<BaseData>().GetEnumerator();
@@ -74,7 +80,7 @@ namespace QuantConnect.Brokerages.Tradier
return enumerator;
}
private static bool CanSubscribe(Symbol symbol)
private bool CanSubscribe(Symbol symbol)
{
return (symbol.ID.SecurityType == SecurityType.Equity || symbol.ID.SecurityType == SecurityType.Option)
&& !symbol.Value.Contains("-UNIVERSE-");
@@ -207,7 +213,7 @@ namespace QuantConnect.Brokerages.Tradier
//Authenticate a request:
request.Accept = "application/json";
request.Headers.Add("Authorization", "Bearer " + AccessToken);
request.Headers.Add("Authorization", "Bearer " + _accessToken);
//Add the desired data:
var postData = "symbols=" + symbolJoined + "&filter=trade&sessionid=" + session.SessionId;

View File

@@ -47,28 +47,26 @@ namespace QuantConnect.Brokerages.Tradier
/// </summary>
public partial class TradierBrokerage : Brokerage, IDataQueueHandler, IHistoryProvider
{
private readonly string _accountID;
private readonly bool _useSandbox;
private readonly string _accountId;
private readonly string _accessToken;
// we're reusing the equity exchange here to grab typical exchange hours
private static readonly EquityExchange Exchange =
new EquityExchange(MarketHoursDatabase.FromDataFolder().GetExchangeHours(Market.USA, null, SecurityType.Equity));
//Access and Refresh Tokens:
private string _previousResponseRaw = "";
private DateTime _issuedAt;
private TimeSpan _lifeSpan = TimeSpan.FromSeconds(86399); // 1 second less than a day
private readonly object _lockAccessCredentials = new object();
// polling timers for refreshing access tokens and checking for fill events
private Timer _refreshTimer;
private Timer _orderFillTimer;
// polling timer for checking for fill events
private readonly Timer _orderFillTimer;
//Tradier Spec:
private readonly Dictionary<TradierApiRequestType, TimeSpan> _rateLimitPeriod;
private readonly Dictionary<TradierApiRequestType, DateTime> _rateLimitNextRequest;
//Endpoints:
private const string RequestEndpoint = @"https://api.tradier.com/v1/";
private readonly string _requestEndpoint;
private readonly IOrderProvider _orderProvider;
private readonly ISecurityProvider _securityProvider;
private readonly IDataAggregator _aggregator;
@@ -88,42 +86,6 @@ namespace QuantConnect.Brokerages.Tradier
private readonly FixedSizeHashQueue<int> _cancelledQcOrderIDs = new FixedSizeHashQueue<int>(10000);
private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager;
/// <summary>
/// Event fired when our session has been refreshed/tokens updated
/// </summary>
public event EventHandler<TokenResponse> SessionRefreshed;
/// <summary>
/// When we expect this access token to expire, leaves an hour of padding
/// </summary>
private DateTime ExpectedExpiry
{
get { return _issuedAt + _lifeSpan - TimeSpan.FromMinutes(60); }
}
/// <summary>
/// Access Token Access:
/// </summary>
public string AccessToken { get; private set; }
/// <summary>
/// Refresh Token Access:
/// </summary>
public string RefreshToken { get; private set; }
/// <summary>
/// The QC User id, used for refreshing the session
/// </summary>
public int UserId { get; private set; }
/// <summary>
/// Get the last string returned
/// </summary>
public string LastResponse
{
get { return _previousResponseRaw; }
}
/// <summary>
/// Returns the brokerage account's base currency
/// </summary>
@@ -132,13 +94,23 @@ namespace QuantConnect.Brokerages.Tradier
/// <summary>
/// Create a new Tradier Object:
/// </summary>
public TradierBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider, IDataAggregator aggregator, string accountID)
public TradierBrokerage(
IOrderProvider orderProvider,
ISecurityProvider securityProvider,
IDataAggregator aggregator,
bool useSandbox,
string accountId,
string accessToken)
: base("Tradier Brokerage")
{
_orderProvider = orderProvider;
_securityProvider = securityProvider;
_aggregator = aggregator;
_accountID = accountID;
_useSandbox = useSandbox;
_accountId = accountId;
_accessToken = accessToken;
_requestEndpoint = useSandbox ? "https://sandbox.tradier.com/v1/" : "https://api.tradier.com/v1/";
_subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager();
_subscriptionManager.SubscribeImpl += (s, t) =>
@@ -170,6 +142,11 @@ namespace QuantConnect.Brokerages.Tradier
_rateLimitPeriod[TradierApiRequestType.Standard] = TimeSpan.FromMilliseconds(500);
_rateLimitPeriod[TradierApiRequestType.Data] = TimeSpan.FromMilliseconds(500);
// we can poll orders once a second in sandbox and twice a second in production
var orderPollingIntervalInSeconds = Config.GetDouble("tradier-order-poll-interval", 1.0);
var interval = (int)(1000 * orderPollingIntervalInSeconds);
_orderFillTimer = new Timer(state => CheckForFills(), null, interval, interval);
Task.Factory.StartNew(() =>
{
IEnumerator<TradierStreamData> pipe = null;
@@ -209,42 +186,6 @@ namespace QuantConnect.Brokerages.Tradier
#region Tradier client implementation
/// <summary>
/// Set the access token and login information for the tradier brokerage
/// </summary>
/// <param name="userId">Userid for this brokerage</param>
/// <param name="accessToken">Viable access token</param>
/// <param name="refreshToken">Our refresh token</param>
/// <param name="issuedAt">When the token was issued</param>
/// <param name="lifeSpan">Life span for our token.</param>
public void SetTokens(int userId, string accessToken, string refreshToken, DateTime issuedAt, TimeSpan lifeSpan)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
_issuedAt = issuedAt;
_lifeSpan = lifeSpan;
UserId = userId;
if (_refreshTimer != null)
{
_refreshTimer.Dispose();
}
if (_orderFillTimer != null)
{
_orderFillTimer.Dispose();
}
var dueTime = ExpectedExpiry - DateTime.UtcNow;
if (dueTime < TimeSpan.Zero) dueTime = TimeSpan.Zero;
var period = TimeSpan.FromDays(1).Subtract(TimeSpan.FromMinutes(-1));
_refreshTimer = new Timer(state => RefreshSession(), null, dueTime, period);
// we can poll orders once a second in sandbox and twice a second in production
double orderPollingIntervalInSeconds = Config.GetDouble("tradier-order-poll-interval", 1.0);
var interval = (int)(1000 * orderPollingIntervalInSeconds);
_orderFillTimer = new Timer(state => CheckForFills(), null, interval, interval);
}
/// <summary>
/// Execute a authenticated call:
/// </summary>
@@ -262,9 +203,9 @@ namespace QuantConnect.Brokerages.Tradier
lock (_lockAccessCredentials)
{
var client = new RestClient(RequestEndpoint);
var client = new RestClient(_requestEndpoint);
client.AddDefaultHeader("Accept", "application/json");
client.AddDefaultHeader("Authorization", "Bearer " + AccessToken);
client.AddDefaultHeader("Authorization", "Bearer " + _accessToken);
//client.AddDefaultHeader("Content-Type", "application/x-www-form-urlencoded");
//Wait for the API rate limiting
@@ -306,7 +247,7 @@ namespace QuantConnect.Brokerages.Tradier
// tradier sometimes sends back poorly formed messages, response will be null
// and we'll extract from it below
}
if (fault != null && fault.Fault != null)
if (fault?.Fault != null)
{
// JSON Errors:
Log.Trace(method + "(1): Parameters: " + string.Join(",", parameters));
@@ -320,14 +261,13 @@ namespace QuantConnect.Brokerages.Tradier
{
if (request.Method == Method.DELETE)
{
string orderId = "[unknown]";
var parameter = request.Parameters.FirstOrDefault(x => x.Name == "orderId");
if (parameter != null) orderId = parameter.Value.ToString();
var orderId = raw.ResponseUri.Segments.LastOrDefault() ?? "[unknown]";
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "OrderAlreadyFilled",
"Unable to cancel the order because it has already been filled. TradierOrderId: " + orderId
));
}
return new T();
return default(T);
}
// this happens when a request for historical data should return an empty response
@@ -379,72 +319,6 @@ namespace QuantConnect.Brokerages.Tradier
return response;
}
/// <summary>
/// Verify we have a user session; or refresh the access token.
/// </summary>
public bool RefreshSession()
{
// if session refreshing disabled, do nothing
if (!Config.GetBool("tradier-refresh-session", true))
return true;
//Send:
//Get: {"sAccessToken":"123123","iExpiresIn":86399,"dtIssuedAt":"2014-10-15T16:59:52-04:00","sRefreshToken":"123123","sScope":"read write market trade stream","sStatus":"approved","success":true}
// Or: {"success":false}
var raw = "";
bool success;
lock (_lockAccessCredentials)
{
try
{
//Create the client for connection:
var client = new RestClient("https://www.quantconnect.com/terminal/");
//Create the GET call:
var request = new RestRequest("processTradier", Method.GET);
request.AddParameter("uid", UserId.ToStringInvariant(), ParameterType.GetOrPost);
request.AddParameter("accessToken", AccessToken, ParameterType.GetOrPost);
request.AddParameter("refreshToken", RefreshToken, ParameterType.GetOrPost);
//Submit the call:
var result = client.Execute(request);
raw = result.Content;
//Decode to token response: update internal access parameters:
var newTokens = JsonConvert.DeserializeObject<TokenResponse>(result.Content);
if (newTokens != null && newTokens.Success)
{
AccessToken = newTokens.AccessToken;
RefreshToken = newTokens.RefreshToken;
_issuedAt = newTokens.IssuedAt;
_lifeSpan = TimeSpan.FromSeconds(newTokens.ExpiresIn);
Log.Trace("SESSION REFRESHED: Access: " + AccessToken + " Refresh: " + RefreshToken + " Issued At: " + _lifeSpan + " JSON>>"
+ result.Content);
OnSessionRefreshed(newTokens);
success = true;
}
else
{
Log.Error("Tradier.RefreshSession(): Error Refreshing Session: URL: " + client.BuildUri(request) + " Response: " + result.Content);
success = false;
}
}
catch (Exception err)
{
Log.Error(err, "Raw: " + raw);
success = false;
}
}
if (!success)
{
// if we can't refresh our tokens then we must stop the algorithm
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, "RefreshSession", "Failed to refresh access token: " + raw));
}
return success;
}
/// <summary>
/// Using this auth token get the tradier user:
/// </summary>
@@ -466,10 +340,9 @@ namespace QuantConnect.Brokerages.Tradier
/// Returns null if the request was unsucessful
/// </remarks>
/// <returns>Balance</returns>
public TradierBalanceDetails GetBalanceDetails(string accountId)
public TradierBalanceDetails GetBalanceDetails()
{
var request = new RestRequest("accounts/{accountId}/balances", Method.GET);
request.AddParameter("accountId", accountId, ParameterType.UrlSegment);
var request = new RestRequest($"accounts/{_accountId}/balances", Method.GET);
var balContainer = Execute<TradierBalance>(request, TradierApiRequestType.Standard);
//Log.Trace("TradierBrokerage.GetBalanceDetails(): Bal Container: " + JsonConvert.SerializeObject(balContainer));
return balContainer.Balances;
@@ -484,11 +357,10 @@ namespace QuantConnect.Brokerages.Tradier
/// <returns>Array of the symbols we hold.</returns>
public List<TradierPosition> GetPositions()
{
var request = new RestRequest("accounts/{accountId}/positions", Method.GET);
request.AddParameter("accountId", _accountID, ParameterType.UrlSegment);
var request = new RestRequest($"accounts/{_accountId}/positions", Method.GET);
var positionContainer = Execute<TradierPositionsContainer>(request, TradierApiRequestType.Standard);
if (positionContainer.TradierPositions == null || positionContainer.TradierPositions.Positions == null)
if (positionContainer.TradierPositions?.Positions == null)
{
// we had a successful call but there weren't any positions
Log.Trace("Tradier.Positions(): No positions found");
@@ -504,14 +376,13 @@ namespace QuantConnect.Brokerages.Tradier
/// <remarks>
/// Returns null if the request was unsucessful
/// </remarks>
public List<TradierEvent> GetAccountEvents(long accountId)
public List<TradierEvent> GetAccountEvents()
{
var request = new RestRequest("accounts/{accountId}/history", Method.GET);
request.AddUrlSegment("accountId", accountId.ToStringInvariant());
var request = new RestRequest($"accounts/{_accountId}/history", Method.GET);
var eventContainer = Execute<TradierEventContainer>(request, TradierApiRequestType.Standard);
if (eventContainer.TradierEvents == null || eventContainer.TradierEvents.Events == null)
if (eventContainer.TradierEvents?.Events == null)
{
// we had a successful call but there weren't any events
Log.Trace("Tradier.GetAccountEvents(): No events found");
@@ -524,14 +395,13 @@ namespace QuantConnect.Brokerages.Tradier
/// <summary>
/// GainLoss of recent trades for this account:
/// </summary>
public List<TradierGainLoss> GetGainLoss(long accountId)
public List<TradierGainLoss> GetGainLoss()
{
var request = new RestRequest("accounts/{accountId}/gainloss");
request.AddUrlSegment("accountId", accountId.ToStringInvariant());
var request = new RestRequest($"accounts/{_accountId}/gainloss");
var gainLossContainer = Execute<TradierGainLossContainer>(request, TradierApiRequestType.Standard);
if (gainLossContainer.GainLossClosed == null || gainLossContainer.GainLossClosed.ClosedPositions == null)
if (gainLossContainer.GainLossClosed?.ClosedPositions == null)
{
// we had a successful call but there weren't any records returned
Log.Trace("Tradier.GetGainLoss(): No gain loss found");
@@ -546,8 +416,7 @@ namespace QuantConnect.Brokerages.Tradier
/// </summary>
public List<TradierOrder> GetIntradayAndPendingOrders()
{
var request = new RestRequest("accounts/{accountId}/orders");
request.AddUrlSegment("accountId", _accountID.ToStringInvariant());
var request = new RestRequest($"accounts/{_accountId}/orders");
var ordersContainer = Execute<TradierOrdersContainer>(request, TradierApiRequestType.Standard);
if (ordersContainer.Orders == null)
@@ -565,14 +434,14 @@ namespace QuantConnect.Brokerages.Tradier
/// </summary>
public TradierOrderDetailed GetOrder(long orderId)
{
var request = new RestRequest("accounts/{accountId}/orders/" + orderId);
request.AddUrlSegment("accountId", _accountID.ToStringInvariant());
var request = new RestRequest($"accounts/{_accountId}/orders/" + orderId);
var detailsParent = Execute<TradierOrderDetailedContainer>(request, TradierApiRequestType.Standard);
if (detailsParent == null || detailsParent.DetailedOrder == null)
if (detailsParent?.DetailedOrder == null)
{
Log.Error("Tradier.GetOrder(): Null response.");
return new TradierOrderDetailed();
}
return detailsParent.DetailedOrder;
}
@@ -580,7 +449,7 @@ namespace QuantConnect.Brokerages.Tradier
/// Place Order through API.
/// accounts/{account-id}/orders
/// </summary>
public TradierOrderResponse PlaceOrder(string accountId,
public TradierOrderResponse PlaceOrder(
TradierOrderClass classification,
TradierOrderDirection direction,
string symbol,
@@ -592,8 +461,7 @@ namespace QuantConnect.Brokerages.Tradier
TradierOrderDuration duration = TradierOrderDuration.GTC)
{
//Compose the request:
var request = new RestRequest("accounts/{accountId}/orders");
request.AddUrlSegment("accountId", accountId.ToStringInvariant());
var request = new RestRequest($"accounts/{_accountId}/orders");
//Add data:
request.AddParameter("class", GetEnumDescription(classification));
@@ -617,7 +485,7 @@ namespace QuantConnect.Brokerages.Tradier
/// <summary>
/// Update an exiting Tradier Order:
/// </summary>
public TradierOrderResponse ChangeOrder(string accountId,
public TradierOrderResponse ChangeOrder(
long orderId,
TradierOrderType type = TradierOrderType.Market,
TradierOrderDuration duration = TradierOrderDuration.GTC,
@@ -625,10 +493,10 @@ namespace QuantConnect.Brokerages.Tradier
decimal stop = 0)
{
//Create Request:
var request = new RestRequest("accounts/{accountId}/orders/{orderId}");
request.AddUrlSegment("accountId", accountId.ToStringInvariant());
request.AddUrlSegment("orderId", orderId.ToStringInvariant());
request.Method = Method.PUT;
var request = new RestRequest($"accounts/{_accountId}/orders/{orderId}")
{
Method = Method.PUT
};
//Add Data:
request.AddParameter("type", GetEnumDescription(type));
@@ -643,13 +511,13 @@ namespace QuantConnect.Brokerages.Tradier
/// <summary>
/// Cancel the order with this account and id number
/// </summary>
public TradierOrderResponse CancelOrder(string accountId, long orderId)
public TradierOrderResponse CancelOrder(long orderId)
{
//Compose Request:
var request = new RestRequest("accounts/{accountId}/orders/{orderId}");
request.AddUrlSegment("accountId", accountId.ToStringInvariant());
request.AddUrlSegment("orderId", orderId.ToStringInvariant());
request.Method = Method.DELETE;
var request = new RestRequest($"accounts/{_accountId}/orders/{orderId}")
{
Method = Method.DELETE
};
//Transmit Request:
return Execute<TradierOrderResponse>(request, TradierApiRequestType.Orders);
@@ -667,7 +535,7 @@ namespace QuantConnect.Brokerages.Tradier
//Send Request:
var request = new RestRequest("markets/quotes", Method.GET);
var csvSymbols = String.Join(",", symbols);
var csvSymbols = string.Join(",", symbols);
request.AddParameter("symbols", csvSymbols, ParameterType.QueryString);
var dataContainer = Execute<TradierQuoteContainer>(request, TradierApiRequestType.Data, "quotes");
@@ -791,15 +659,6 @@ namespace QuantConnect.Brokerages.Tradier
return obj;
}
/// <summary>
/// Event invocator for the SessionRefreshed event
/// </summary>
protected virtual void OnSessionRefreshed(TokenResponse e)
{
var handler = SessionRefreshed;
if (handler != null) handler(this, e);
}
#endregion
#region IBrokerage implementation
@@ -807,10 +666,7 @@ namespace QuantConnect.Brokerages.Tradier
/// <summary>
/// Returns true if we're currently connected to the broker
/// </summary>
public override bool IsConnected
{
get { return _issuedAt + _lifeSpan > DateTime.Now; }
}
public override bool IsConnected => !_disconnect;
/// <summary>
/// Gets all open orders on the account.
@@ -860,7 +716,7 @@ namespace QuantConnect.Brokerages.Tradier
{
return new List<CashAmount>
{
new CashAmount(GetBalanceDetails(_accountID).TotalCash, Currencies.USD)
new CashAmount(GetBalanceDetails().TotalCash, Currencies.USD)
};
}
@@ -1010,7 +866,7 @@ namespace QuantConnect.Brokerages.Tradier
var orderDuration = GetOrderDuration(order.TimeInForce);
var limitPrice = GetLimitPrice(order);
var stopPrice = GetStopPrice(order);
var response = ChangeOrder(_accountID, activeOrder.Order.Id,
var response = ChangeOrder(activeOrder.Order.Id,
orderType,
orderDuration,
limitPrice,
@@ -1070,7 +926,7 @@ namespace QuantConnect.Brokerages.Tradier
foreach (var orderID in order.BrokerId)
{
var id = Parse.Long(orderID);
var response = CancelOrder(_accountID, id);
var response = CancelOrder(id);
if (response == null)
{
// this can happen if the order has already been filled
@@ -1094,8 +950,6 @@ namespace QuantConnect.Brokerages.Tradier
public override void Connect()
{
_disconnect = false;
if (IsConnected) return;
RefreshSession();
}
/// <summary>
@@ -1106,6 +960,14 @@ namespace QuantConnect.Brokerages.Tradier
_disconnect = true;
}
/// <summary>
/// Dispose of the brokerage instance
/// </summary>
public override void Dispose()
{
_orderFillTimer.DisposeSafely();
}
/// <summary>
/// Event invocator for the Message event
/// </summary>
@@ -1137,7 +999,7 @@ namespace QuantConnect.Brokerages.Tradier
$"{order.Quantity.ToStringInvariant()} units of {order.Symbol}{stopLimit}"
);
var response = PlaceOrder(_accountID,
var response = PlaceOrder(
order.Classification,
order.Direction,
order.Symbol,
@@ -1977,4 +1839,5 @@ namespace QuantConnect.Brokerages.Tradier
}
}
}
}

View File

@@ -13,15 +13,10 @@
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using QuantConnect.Configuration;
using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.Packets;
using QuantConnect.Securities;
using QuantConnect.Util;
@@ -39,59 +34,21 @@ namespace QuantConnect.Brokerages.Tradier
public static class Configuration
{
/// <summary>
/// Gets the account ID to be used when instantiating a brokerage
/// Gets whether to use the developer sandbox or not
/// </summary>
public static int QuantConnectUserID
{
get { return Config.GetInt("qc-user-id"); }
}
public static bool UseSandbox => Config.GetBool("tradier-use-sandbox");
/// <summary>
/// Gets the account ID to be used when instantiating a brokerage
/// </summary>
public static string AccountID
{
get { return Config.Get("tradier-account-id"); }
}
public static string AccountId => Config.Get("tradier-account-id");
/// <summary>
/// Gets the access token from configuration
/// </summary>
public static string AccessToken
{
get { return Config.Get("tradier-access-token"); }
}
/// <summary>
/// Gets the refresh token from configuration
/// </summary>
public static string RefreshToken
{
get { return Config.Get("tradier-refresh-token"); }
}
/// <summary>
/// Gets the date time the tokens were issued at from configuration
/// </summary>
public static DateTime TokensIssuedAt
{
get { return Config.GetValue<DateTime>("tradier-issued-at"); }
}
/// <summary>
/// Gets the life span of the tokens from configuration
/// </summary>
public static TimeSpan LifeSpan
{
get { return TimeSpan.FromSeconds(Config.GetInt("tradier-lifespan")); }
}
public static string AccessToken => Config.Get("tradier-access-token");
}
/// <summary>
/// File path used to store tradier token data
/// </summary>
public const string TokensFile = "tradier-tokens.txt";
/// <summary>
/// Initializes a new instance of he TradierBrokerageFactory class
/// </summary>
@@ -111,31 +68,12 @@ namespace QuantConnect.Brokerages.Tradier
{
get
{
string accessToken, refreshToken, issuedAt, lifeSpan;
// always need to grab account ID from configuration
var accountID = Configuration.AccountID.ToStringInvariant();
var data = new Dictionary<string, string>();
if (File.Exists(TokensFile))
var data = new Dictionary<string, string>
{
var tokens = JsonConvert.DeserializeObject<TokenResponse>(File.ReadAllText(TokensFile));
accessToken = tokens.AccessToken;
refreshToken = tokens.RefreshToken;
issuedAt = tokens.IssuedAt.ToString(CultureInfo.InvariantCulture);
lifeSpan = "86399";
}
else
{
accessToken = Configuration.AccessToken;
refreshToken = Configuration.RefreshToken;
issuedAt = Configuration.TokensIssuedAt.ToString(CultureInfo.InvariantCulture);
lifeSpan = Configuration.LifeSpan.TotalSeconds.ToString(CultureInfo.InvariantCulture);
}
data.Add("tradier-account-id", accountID);
data.Add("tradier-access-token", accessToken);
data.Add("tradier-refresh-token", refreshToken);
data.Add("tradier-issued-at", issuedAt);
data.Add("tradier-lifespan", lifeSpan);
{ "tradier-use-sandbox", Configuration.UseSandbox.ToStringInvariant() },
{ "tradier-account-id", Configuration.AccountId.ToStringInvariant() },
{ "tradier-access-token", Configuration.AccessToken.ToStringInvariant() }
};
return data;
}
}
@@ -155,32 +93,22 @@ namespace QuantConnect.Brokerages.Tradier
public override IBrokerage CreateBrokerage(LiveNodePacket job, IAlgorithm algorithm)
{
var errors = new List<string>();
var accountID = Read<string>(job.BrokerageData, "tradier-account-id", errors);
var useSandbox = Read<bool>(job.BrokerageData, "tradier-use-sandbox", errors);
var accountId = Read<string>(job.BrokerageData, "tradier-account-id", errors);
var accessToken = Read<string>(job.BrokerageData, "tradier-access-token", errors);
var refreshToken = Read<string>(job.BrokerageData, "tradier-refresh-token", errors);
var issuedAt = Read<DateTime>(job.BrokerageData, "tradier-issued-at", errors);
var lifeSpan = TimeSpan.FromSeconds(Read<double>(job.BrokerageData, "tradier-lifespan", errors));
var brokerage = new TradierBrokerage(
algorithm.Transactions,
algorithm.Transactions,
algorithm.Portfolio,
Composer.Instance.GetExportedValueByTypeName<IDataAggregator>(Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager")),
accountID);
useSandbox,
accountId,
accessToken);
// if we're running live locally we'll want to save any new tokens generated so that they can easily be retrieved
if (Config.GetBool("tradier-save-tokens"))
{
brokerage.SessionRefreshed += (sender, args) =>
{
File.WriteAllText(TokensFile, JsonConvert.SerializeObject(args, Formatting.Indented));
};
}
brokerage.SetTokens(job.UserId, accessToken, refreshToken, issuedAt, lifeSpan);
//Add the brokerage to the composer to ensure its accessible to the live data feed.
// Add the brokerage to the composer to ensure its accessible to the live data feed.
Composer.Instance.AddPart<IDataQueueHandler>(brokerage);
Composer.Instance.AddPart<IHistoryProvider>(brokerage);
return brokerage;
}
@@ -191,27 +119,5 @@ namespace QuantConnect.Brokerages.Tradier
public override void Dispose()
{
}
/// <summary>
/// Reads the tradier tokens from the <see cref="TokensFile"/> or from configuration
/// </summary>
public static TokenResponse GetTokens()
{
// pick a source for our tokens
if (File.Exists(TokensFile))
{
Log.Trace("Reading tradier tokens from " + TokensFile);
return JsonConvert.DeserializeObject<TokenResponse>(File.ReadAllText(TokensFile));
}
return new TokenResponse
{
AccessToken = Config.Get("tradier-access-token"),
RefreshToken = Config.Get("tradier-refresh-token"),
IssuedAt = Config.GetValue<DateTime>("tradier-issued-at"),
ExpiresIn = Config.GetInt("tradier-lifespan")
};
}
}
}

View File

@@ -77,12 +77,9 @@
"ib-version": "974",
// tradier configuration
"tradier-use-sandbox": true,
"tradier-account-id": "",
"tradier-access-token": "",
"tradier-refresh-token": "",
"tradier-issued-at": "",
"tradier-lifespan": "",
"tradier-refresh-session": true,
// oanda configuration
"oanda-environment": "Practice",
@@ -348,7 +345,7 @@
"result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler",
"data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed",
"real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler",
"transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler"
"transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler"
}
}
}

View File

@@ -124,6 +124,7 @@ namespace QuantConnect.Tests.Brokerages
// these securities don't need to be real, just used for the ISecurityProvider impl, required
// by brokerages to track holdings
SecurityProvider[accountHolding.Symbol] = CreateSecurity(accountHolding.Symbol);
SecurityProvider[accountHolding.Symbol].Holdings.SetHoldings(accountHolding.AveragePrice, accountHolding.Quantity);
}
brokerage.OrderStatusChanged += (sender, args) =>
{
@@ -448,7 +449,7 @@ namespace QuantConnect.Tests.Brokerages
Assert.AreEqual(GetDefaultQuantity(), afterQuantity - beforeQuantity);
}
[Test, Ignore("This test requires reading the output and selection of a low volume security for the Brokerage")]
[Test, Explicit("This test requires reading the output and selection of a low volume security for the Brokerage")]
public void PartialFills()
{
var manualResetEvent = new ManualResetEvent(false);

View File

@@ -24,7 +24,7 @@ using QuantConnect.Securities;
namespace QuantConnect.Tests.Brokerages.Tradier
{
[TestFixture, Ignore("This test requires a configured and active Tradier account")]
[TestFixture, Explicit("This test requires a configured and active Tradier account")]
public class TradierBrokerageHistoryProviderTests
{
private static TestCaseData[] TestParameters
@@ -54,10 +54,11 @@ namespace QuantConnect.Tests.Brokerages.Tradier
{
TestDelegate test = () =>
{
var useSandbox = Config.GetBool("tradier-use-sandbox");
var accountId = Config.Get("tradier-account-id");
var accessToken = Config.Get("tradier-access-token");
var brokerage = new TradierBrokerage(null, null, null, "");
brokerage.SetTokens(0, accessToken, "", DateTime.Now, Time.OneDay);
var brokerage = new TradierBrokerage(null, null, null, useSandbox, accountId, accessToken);
var now = DateTime.UtcNow;

View File

@@ -1,11 +1,11 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@@ -15,10 +15,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using NUnit.Framework;
using QuantConnect.Brokerages.Tradier;
using QuantConnect.Interfaces;
@@ -28,7 +26,7 @@ using QuantConnect.Securities;
namespace QuantConnect.Tests.Brokerages.Tradier
{
[TestFixture, Ignore("This test requires a configured and active Tradier account")]
[TestFixture, Explicit("This test requires a configured and active Tradier account")]
public class TradierBrokerageTests : BrokerageTests
{
/// <summary>
@@ -51,20 +49,11 @@ namespace QuantConnect.Tests.Brokerages.Tradier
/// <returns>A connected brokerage instance</returns>
protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider)
{
var accountID = TradierBrokerageFactory.Configuration.AccountID;
var tradier = new TradierBrokerage(orderProvider, securityProvider, new AggregationManager(), accountID);
var useSandbox = TradierBrokerageFactory.Configuration.UseSandbox;
var accountId = TradierBrokerageFactory.Configuration.AccountId;
var accessToken = TradierBrokerageFactory.Configuration.AccessToken;
var qcUserID = TradierBrokerageFactory.Configuration.QuantConnectUserID;
var tokens = TradierBrokerageFactory.GetTokens();
tradier.SetTokens(qcUserID, tokens.AccessToken, tokens.RefreshToken, tokens.IssuedAt, TimeSpan.FromSeconds(tokens.ExpiresIn));
// keep the tokens up to date in the event of a refresh
tradier.SessionRefreshed += (sender, args) =>
{
File.WriteAllText(TradierBrokerageFactory.TokensFile, JsonConvert.SerializeObject(args, Formatting.Indented));
};
return tradier;
return new TradierBrokerage(orderProvider, securityProvider, new AggregationManager(), useSandbox, accountId, accessToken);
}
/// <summary>
@@ -92,10 +81,10 @@ namespace QuantConnect.Tests.Brokerages.Tradier
{
var tradier = (TradierBrokerage) Brokerage;
var quotes = tradier.GetQuotes(new List<string> {symbol.Value});
return quotes.Single().Ask;
return quotes.Single().Ask ?? 0;
}
[Test, TestCaseSource("OrderParameters")]
[Test, TestCaseSource(nameof(OrderParameters))]
public void AllowsOneActiveOrderPerSymbol(OrderTestParameters parameters)
{
// tradier's api gets special with zero holdings crossing in that they need to fill the order
@@ -124,10 +113,11 @@ namespace QuantConnect.Tests.Brokerages.Tradier
Assert.IsTrue(orderFilledOrCanceled);
}
[Test, Ignore("This test exists to manually verify how rejected orders are handled when we don't receive an order ID back from Tradier.")]
public void ShortZnga()
[Test, Explicit("This test exists to manually verify how rejected orders are handled when we don't receive an order ID back from Tradier.")]
public void ShortInvalidSymbol()
{
PlaceOrderWaitForStatus(new MarketOrder(Symbols.ZNGA, -1, DateTime.Now), OrderStatus.Invalid, allowFailedSubmission: true);
var symbol = Symbol.Create("XYZ", SecurityType.Equity, Market.USA);
PlaceOrderWaitForStatus(new MarketOrder(symbol, -1, DateTime.Now), OrderStatus.Invalid, allowFailedSubmission: true);
// wait for output to be generated
Thread.Sleep(20*1000);