Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e231e31a1a | ||
|
|
52ee8c7c82 | ||
|
|
e3cd533c4a | ||
|
|
63ff835de9 | ||
|
|
5a517e3201 | ||
|
|
70217d38de |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user