/*
* 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");
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using ProtoBuf;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.Securities.CurrencyConversion;
namespace QuantConnect.Securities
{
///
/// Represents a holding of a currency in cash.
///
[ProtoContract(SkipConstructor = true)]
public class Cash
{
private ICurrencyConversion _currencyConversion;
private readonly object _locker = new object();
///
/// Event fired when this instance is updated
/// , ,
///
public event EventHandler Updated;
///
/// Event fired when this instance's is set/updated
///
public event EventHandler CurrencyConversionUpdated;
///
/// Gets the symbols of the securities required to provide conversion rates.
/// If this cash represents the account currency, then an empty enumerable is returned.
///
public IEnumerable SecuritySymbols => CurrencyConversion.ConversionRateSecurities.Any()
? CurrencyConversion.ConversionRateSecurities.Select(x => x.Symbol)
// we do this only because Newtonsoft.Json complains about empty enumerables
: new List(0);
///
/// Gets the object that calculates the conversion rate to account currency
///
[JsonIgnore]
public ICurrencyConversion CurrencyConversion
{
get
{
return _currencyConversion;
}
internal set
{
var lastConversionRate = 0m;
if (_currencyConversion != null)
{
lastConversionRate = _currencyConversion.ConversionRate;
_currencyConversion.ConversionRateUpdated -= OnConversionRateUpdated;
}
_currencyConversion = value;
if (_currencyConversion != null)
{
if (lastConversionRate != 0m)
{
// If a user adds cash with an initial conversion rate and then this is overriden to a SecurityCurrencyConversion,
// we want to keep the previous rate until the new one is updated.
_currencyConversion.ConversionRate = lastConversionRate;
}
_currencyConversion.ConversionRateUpdated += OnConversionRateUpdated;
}
CurrencyConversionUpdated?.Invoke(this, EventArgs.Empty);
}
}
private void OnConversionRateUpdated(object sender, decimal e)
{
OnUpdate();
}
///
/// Gets the symbol used to represent this cash
///
[ProtoMember(1)]
public string Symbol { get; }
///
/// Gets or sets the amount of cash held
///
[ProtoMember(2)]
public decimal Amount { get; private set; }
///
/// Gets the conversion rate into account currency
///
[ProtoMember(3)]
public decimal ConversionRate
{
get
{
return _currencyConversion.ConversionRate;
}
internal set
{
if (_currencyConversion == null)
{
CurrencyConversion = new ConstantCurrencyConversion(Symbol, null, value);
}
_currencyConversion.ConversionRate = value;
}
}
///
/// The symbol of the currency, such as $
///
[ProtoMember(4)]
public string CurrencySymbol { get; }
///
/// Gets the value of this cash in the account currency
///
public decimal ValueInAccountCurrency => Amount * ConversionRate;
///
/// Initializes a new instance of the class
///
/// The symbol used to represent this cash
/// The amount of this currency held
/// The initial conversion rate of this currency into the
public Cash(string symbol, decimal amount, decimal conversionRate)
{
if (string.IsNullOrEmpty(symbol))
{
throw new ArgumentException(Messages.Cash.NullOrEmptyCashSymbol);
}
Amount = amount;
Symbol = symbol.LazyToUpper();
CurrencySymbol = Currencies.GetCurrencySymbol(Symbol);
CurrencyConversion = new ConstantCurrencyConversion(Symbol, null, conversionRate);
}
///
/// Marks this cash object's conversion rate as being potentially outdated
///
public void Update()
{
_currencyConversion.Update();
}
///
/// Adds the specified amount of currency to this Cash instance and returns the new total.
/// This operation is thread-safe
///
/// The amount of currency to be added
/// The amount of currency directly after the addition
public decimal AddAmount(decimal amount)
{
lock (_locker)
{
Amount += amount;
}
OnUpdate();
return Amount;
}
///
/// Sets the Quantity to the specified amount
///
/// The amount to set the quantity to
public void SetAmount(decimal amount)
{
var updated = false;
// lock can be null when proto deserializing this instance
lock (_locker ?? new object())
{
if (Amount != amount)
{
Amount = amount;
// only update if there was actually one
updated = true;
}
}
if (updated)
{
OnUpdate();
}
}
///
/// Ensures that we have a data feed to convert this currency into the base currency.
/// This will add a and create a at the lowest resolution if one is not found.
///
/// The security manager
/// The subscription manager used for searching and adding subscriptions
/// The market map that decides which market the new security should be in
/// Will be used to consume
/// Will be used to create required new
/// The account currency
/// The default resolution to use for the internal subscriptions
/// Returns the added , otherwise null
public List EnsureCurrencyDataFeed(SecurityManager securities,
SubscriptionManager subscriptions,
IReadOnlyDictionary marketMap,
SecurityChanges changes,
ISecurityService securityService,
string accountCurrency,
Resolution defaultResolution = Resolution.Minute
)
{
// this gets called every time we add securities using universe selection,
// so must of the time we've already resolved the value and don't need to again
if (CurrencyConversion.DestinationCurrency != null)
{
return null;
}
if (Symbol == accountCurrency)
{
CurrencyConversion = ConstantCurrencyConversion.Identity(accountCurrency);
return null;
}
// existing securities
var securitiesToSearch = securities.Select(kvp => kvp.Value)
.Concat(changes.AddedSecurities)
.Where(s => ProvidesConversionRate(s.Type));
// Create a SecurityType to Market mapping with the markets from SecurityManager members
var markets = securities.Select(x => x.Key)
.GroupBy(x => x.SecurityType)
.ToDictionary(x => x.Key, y => y.Select(symbol => symbol.ID.Market).ToHashSet());
if (markets.ContainsKey(SecurityType.Cfd) && !markets.ContainsKey(SecurityType.Forex))
{
markets.Add(SecurityType.Forex, markets[SecurityType.Cfd]);
}
if (markets.ContainsKey(SecurityType.Forex) && !markets.ContainsKey(SecurityType.Cfd))
{
markets.Add(SecurityType.Cfd, markets[SecurityType.Forex]);
}
var forexEntries = GetAvailableSymbolPropertiesDatabaseEntries(SecurityType.Forex, marketMap, markets);
var cfdEntries = GetAvailableSymbolPropertiesDatabaseEntries(SecurityType.Cfd, marketMap, markets);
var cryptoEntries = GetAvailableSymbolPropertiesDatabaseEntries(SecurityType.Crypto, marketMap, markets);
var potentialEntries = forexEntries
.Concat(cfdEntries)
.Concat(cryptoEntries)
.ToList();
if (!potentialEntries.Any(x =>
Symbol == x.Key.Symbol.Substring(0, x.Key.Symbol.Length - x.Value.QuoteCurrency.Length) ||
Symbol == x.Value.QuoteCurrency))
{
// currency not found in any tradeable pair
Log.Error(Messages.Cash.NoTradablePairFoundForCurrencyConversion(Symbol, accountCurrency, marketMap.Where(kvp => ProvidesConversionRate(kvp.Key))));
CurrencyConversion = ConstantCurrencyConversion.Null(accountCurrency, Symbol);
return null;
}
// Special case for crypto markets without direct pairs (They wont be found by the above)
// This allows us to add cash for "StableCoins" that are 1-1 with our account currency without needing a conversion security.
// Check out the StableCoinsWithoutPairs static var for those that are missing their 1-1 conversion pairs
if (marketMap.TryGetValue(SecurityType.Crypto, out var market)
&&
(Currencies.IsStableCoinWithoutPair(Symbol + accountCurrency, market)
|| Currencies.IsStableCoinWithoutPair(accountCurrency + Symbol, market)))
{
CurrencyConversion = ConstantCurrencyConversion.Identity(accountCurrency, Symbol);
return null;
}
var requiredSecurities = new List();
var potentials = potentialEntries
.Select(x => QuantConnect.Symbol.Create(x.Key.Symbol, x.Key.SecurityType, x.Key.Market));
var minimumResolution = subscriptions.Subscriptions.Select(x => x.Resolution).DefaultIfEmpty(defaultResolution).Min();
var makeNewSecurity = new Func(symbol =>
{
var securityType = symbol.ID.SecurityType;
// use the first subscription defined in the subscription manager
var type = subscriptions.LookupSubscriptionConfigDataTypes(securityType, minimumResolution, false).First();
var objectType = type.Item1;
var tickType = type.Item2;
// set this as an internal feed so that the data doesn't get sent into the algorithm's OnData events
var config = subscriptions.SubscriptionDataConfigService.Add(symbol,
minimumResolution,
fillForward: true,
extendedMarketHours: false,
isInternalFeed: true,
subscriptionDataTypes: new List>
{new Tuple(objectType, tickType)}).First();
var newSecurity = securityService.CreateSecurity(symbol,
config,
addToSymbolCache: false);
Log.Trace("Cash.EnsureCurrencyDataFeed(): " + Messages.Cash.AddingSecuritySymbolForCashCurrencyFeed(symbol, Symbol));
securities.Add(symbol, newSecurity);
requiredSecurities.Add(config);
return newSecurity;
});
CurrencyConversion = SecurityCurrencyConversion.LinearSearch(Symbol,
accountCurrency,
securitiesToSearch.ToList(),
potentials,
makeNewSecurity);
return requiredSecurities;
}
///
/// Returns a that represents the current .
///
/// A that represents the current .
public override string ToString()
{
return ToString(Currencies.USD);
}
///
/// Returns a that represents the current .
///
/// A that represents the current .
public string ToString(string accountCurrency)
{
return Messages.Cash.ToString(this, accountCurrency);
}
private static IEnumerable> GetAvailableSymbolPropertiesDatabaseEntries(
SecurityType securityType,
IReadOnlyDictionary marketMap,
IReadOnlyDictionary> markets
)
{
var marketJoin = new HashSet();
{
string market;
if (marketMap.TryGetValue(securityType, out market))
{
marketJoin.Add(market);
}
HashSet existingMarkets;
if (markets.TryGetValue(securityType, out existingMarkets))
{
foreach (var existingMarket in existingMarkets)
{
marketJoin.Add(existingMarket);
}
}
}
return marketJoin.SelectMany(market => SymbolPropertiesDatabase.FromDataFolder()
.GetSymbolPropertiesList(market, securityType));
}
private static bool ProvidesConversionRate(SecurityType securityType)
{
return securityType == SecurityType.Forex || securityType == SecurityType.Crypto || securityType == SecurityType.Cfd;
}
private void OnUpdate()
{
Updated?.Invoke(this, EventArgs.Empty);
}
}
}