/* * 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); } } }