Files
quantconnect--lean/Tests/Brokerages/SymbolPropertiesDatabaseSymbolMapperTests.cs
2026-07-13 13:02:50 +08:00

294 lines
15 KiB
C#

/*
* 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 NUnit.Framework;
using QuantConnect.Brokerages;
using QuantConnect.Securities;
using System;
using System.IO;
using System.Linq;
namespace QuantConnect.Tests.Brokerages
{
[TestFixture]
public class SymbolPropertiesDatabaseSymbolMapperTests
{
[TestCaseSource(nameof(BrokerageSymbols))]
public void ReturnsCryptoSecurityType(string market, string brokerageSymbol, string leanSymbol)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
var symbol = mapper.GetLeanSymbol(brokerageSymbol, SecurityType.Crypto, market);
Assert.AreEqual(leanSymbol, symbol.Value);
Assert.AreEqual(market, symbol.ID.Market);
}
[TestCaseSource(nameof(BrokerageSymbols))]
public void ReturnsCorrectLeanSymbol(string market, string brokerageSymbol, string leanSymbol)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
var symbol = mapper.GetLeanSymbol(brokerageSymbol, SecurityType.Crypto, market);
Assert.AreEqual(leanSymbol, symbol.Value);
Assert.AreEqual(SecurityType.Crypto, symbol.ID.SecurityType);
Assert.AreEqual(market, symbol.ID.Market);
}
[TestCaseSource(nameof(CryptoSymbols))]
public void ReturnsCorrectBrokerageSymbol(Symbol symbol, string brokerageSymbol)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(symbol.ID.Market);
Assert.AreEqual(brokerageSymbol, mapper.GetBrokerageSymbol(symbol));
}
[TestCaseSource(nameof(CurrencyPairs))]
public void ThrowsOnCurrencyPairs(string market, string brokerageSymbol)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSecurityType(brokerageSymbol));
}
[TestCase(Market.Coinbase)]
[TestCase(Market.Bitfinex)]
[TestCase(Market.Binance)]
public void ThrowsOnNullOrEmptySymbols(string market)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
string ticker = null;
Assert.IsFalse(mapper.IsKnownBrokerageSymbol(ticker));
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(ticker, SecurityType.Crypto, market));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSecurityType(ticker));
ticker = "";
Assert.IsFalse(mapper.IsKnownBrokerageSymbol(ticker));
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(ticker, SecurityType.Crypto, market));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSecurityType(ticker));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSymbol(Symbol.Create(ticker, SecurityType.Crypto, market)));
}
[TestCaseSource(nameof(UnknownSymbols))]
public void ThrowsOnUnknownSymbols(string brokerageSymbol, SecurityType type, string market)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(brokerageSymbol, type, market));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSymbol(Symbol.Create(brokerageSymbol.Replace("-", ""), type, market)));
}
[TestCaseSource(nameof(UnknownSecurityType))]
public void ThrowsOnUnknownSecurityType(string brokerageSymbol, SecurityType type, string market)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(brokerageSymbol, type, market));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSymbol(Symbol.Create(brokerageSymbol.Replace("-", ""), type, market)));
}
[TestCaseSource(nameof(UnknownMarket))]
public void ThrowsOnUnknownMarket(string brokerageSymbol, SecurityType type, string market)
{
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(brokerageSymbol, type, market));
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSymbol(Symbol.Create(brokerageSymbol.Replace("-", ""), type, market)));
}
[TestCase("GetLeanSymbol")]
[TestCase("GetBrokerageSymbol")]
[TestCase("IsKnownLeanSymbol")]
[TestCase("GetBrokerageSecurityType")]
[TestCase("IsKnownBrokerageSymbol")]
public void SymbolMapperRefreshesMappingsPeriodically(string methodName)
{
// Test symbol that is not in the SPDB
const string market = Market.Binance;
const string newSymbol = "CUSTOMCRIPTOUSDT";
const SecurityType securityType = SecurityType.Crypto;
var mapper = new SymbolPropertiesDatabaseSymbolMapper(market);
var testSymbol = Symbol.Create(newSymbol, securityType, market);
// Verify symbol doesn't exist initially using the appropriate method
switch (methodName)
{
case "GetLeanSymbol":
Assert.Throws<ArgumentException>(() => mapper.GetLeanSymbol(newSymbol, securityType, market));
break;
case "GetBrokerageSymbol":
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSymbol(testSymbol));
break;
case "IsKnownLeanSymbol":
Assert.IsFalse(mapper.IsKnownLeanSymbol(testSymbol));
break;
case "GetBrokerageSecurityType":
Assert.Throws<ArgumentException>(() => mapper.GetBrokerageSecurityType(newSymbol));
break;
case "IsKnownBrokerageSymbol":
Assert.IsFalse(mapper.IsKnownBrokerageSymbol(newSymbol));
break;
}
// Add manually the new symbol to the SPDB
var path = Path.Combine(Globals.DataFolder, "symbol-properties", "symbol-properties-database.csv");
var newEntry = $"{Environment.NewLine}binance,CUSTOMCRIPTOUSDT,crypto,CUSTOMCRIPTOUSDT,USDT,1,0.00000001,0.01,CUSTOMCRIPTOUSDT,0.01";
File.AppendAllText(path, newEntry);
try
{
// Reset the SPDB
SymbolPropertiesDatabase.Reset();
// Simulate that 15 minutes have passed since the last reload
var lastReloadField = typeof(SymbolPropertiesDatabaseSymbolMapper).GetField("_lastReloadTime", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
lastReloadField.SetValue(mapper, DateTime.UtcNow.AddMinutes(-16));
// Now verify each method can find the symbol
switch (methodName)
{
case "GetLeanSymbol":
var symbol = mapper.GetLeanSymbol(newSymbol, securityType, market);
Assert.IsNotNull(symbol);
Assert.AreEqual(newSymbol, symbol.Value);
Assert.AreEqual(securityType, symbol.SecurityType);
Assert.AreEqual(market, symbol.ID.Market);
break;
case "GetBrokerageSymbol":
Assert.AreEqual(newSymbol, mapper.GetBrokerageSymbol(testSymbol));
break;
case "IsKnownLeanSymbol":
Assert.IsTrue(mapper.IsKnownLeanSymbol(testSymbol));
break;
case "GetBrokerageSecurityType":
Assert.AreEqual(securityType, mapper.GetBrokerageSecurityType(newSymbol));
break;
case "IsKnownBrokerageSymbol":
Assert.IsTrue(mapper.IsKnownBrokerageSymbol(newSymbol));
break;
}
}
finally
{
// Clean up the SPDB
var lines = File.ReadAllLines(path).Where(l => !l.Contains(newSymbol)).ToArray();
File.WriteAllText(path, string.Join(Environment.NewLine, lines));
SymbolPropertiesDatabase.Reset();
}
}
private static TestCaseData[] BrokerageSymbols => new[]
{
new TestCaseData(Market.Coinbase, "ETH-USD", "ETHUSD"),
new TestCaseData(Market.Coinbase, "ETH-BTC", "ETHBTC"),
new TestCaseData(Market.Coinbase, "BTC-USD", "BTCUSD"),
new TestCaseData(Market.Coinbase, "BTC-USDC", "BTCUSDC"),
new TestCaseData(Market.Coinbase, "ATOM-USD", "ATOMUSD"),
new TestCaseData(Market.Bitfinex, "tBTCUSD", "BTCUSD"),
new TestCaseData(Market.Bitfinex, "tBTCUST", "BTCUSDT"),
new TestCaseData(Market.Bitfinex, "tETHUSD", "ETHUSD"),
new TestCaseData(Market.Bitfinex, "tADAUST", "ADAUSDT"),
new TestCaseData(Market.Bitfinex, "tCOMP:USD", "COMPUSD"),
new TestCaseData(Market.Bitfinex, "tCOMP:UST", "COMPUSDT"),
new TestCaseData(Market.Binance, "ETHUSDT", "ETHUSDT"),
new TestCaseData(Market.Binance, "ETHBTC", "ETHBTC"),
new TestCaseData(Market.Binance, "BTCUSDT", "BTCUSDT"),
new TestCaseData(Market.Binance, "ATOMTUSD", "ATOMTUSD"),
new TestCaseData(Market.Binance, "ATOMUSDC", "ATOMUSDC"),
new TestCaseData(Market.Binance, "ATOMUSDT", "ATOMUSDT")
};
private static TestCaseData[] CryptoSymbols => new[]
{
new TestCaseData(Symbol.Create("ETHUSD", SecurityType.Crypto, Market.Coinbase), "ETH-USD"),
new TestCaseData(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase), "BTC-USD"),
new TestCaseData(Symbol.Create("ETHBTC", SecurityType.Crypto, Market.Coinbase), "ETH-BTC"),
new TestCaseData(Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase), "BTC-USDC"),
new TestCaseData(Symbol.Create("ATOMUSD", SecurityType.Crypto, Market.Coinbase), "ATOM-USD"),
new TestCaseData(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Bitfinex), "tBTCUSD"),
new TestCaseData(Symbol.Create("BTCUSDT", SecurityType.Crypto, Market.Bitfinex), "tBTCUST"),
new TestCaseData(Symbol.Create("ETHUSD", SecurityType.Crypto, Market.Bitfinex), "tETHUSD"),
new TestCaseData(Symbol.Create("ADAUSDT", SecurityType.Crypto, Market.Bitfinex), "tADAUST"),
new TestCaseData(Symbol.Create("COMPUSD", SecurityType.Crypto, Market.Bitfinex), "tCOMP:USD"),
new TestCaseData(Symbol.Create("COMPUSDT", SecurityType.Crypto, Market.Bitfinex), "tCOMP:UST"),
new TestCaseData(Symbol.Create("ETHUSDT", SecurityType.Crypto, Market.Binance), "ETHUSDT"),
new TestCaseData(Symbol.Create("ETHBTC", SecurityType.Crypto, Market.Binance), "ETHBTC"),
new TestCaseData(Symbol.Create("BTCUSDT", SecurityType.Crypto, Market.Binance), "BTCUSDT"),
new TestCaseData(Symbol.Create("ATOMTUSD", SecurityType.Crypto, Market.Binance), "ATOMTUSD"),
new TestCaseData(Symbol.Create("ATOMUSDC", SecurityType.Crypto, Market.Binance), "ATOMUSDC"),
new TestCaseData(Symbol.Create("ATOMUSDT", SecurityType.Crypto, Market.Binance), "ATOMUSDT")
};
private static TestCaseData[] CurrencyPairs => new[]
{
new TestCaseData(Market.Coinbase, ""),
new TestCaseData(Market.Coinbase, "EURUSD"),
new TestCaseData(Market.Coinbase, "GBP-USD"),
new TestCaseData(Market.Coinbase, "USD-JPY"),
new TestCaseData(Market.Bitfinex, ""),
new TestCaseData(Market.Bitfinex, "EURUSD"),
new TestCaseData(Market.Bitfinex, "GBP-USD"),
new TestCaseData(Market.Bitfinex, "USD-JPY"),
new TestCaseData(Market.Binance, ""),
new TestCaseData(Market.Binance, "EURUSD"),
new TestCaseData(Market.Binance, "GBPUSD"),
new TestCaseData(Market.Binance, "USDJPY")
};
private static TestCaseData[] UnknownSymbols => new[]
{
new TestCaseData("AAA-BBB", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("USD-BTC", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("EUR-USD", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("GBP-USD", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("USD-JPY", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("BTC-ETH", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("USDC-BTC", SecurityType.Crypto, Market.Coinbase),
new TestCaseData("USD-BTC", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("EUR-USD", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("GBP-USD", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("USD-JPY", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("BTC-ETH", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("USDC-BTC", SecurityType.Crypto, Market.Bitfinex),
new TestCaseData("AAABBB", SecurityType.Crypto, Market.Binance),
new TestCaseData("USDBTC", SecurityType.Crypto, Market.Binance),
new TestCaseData("EURUSD", SecurityType.Crypto, Market.Binance),
new TestCaseData("GBPUSD", SecurityType.Crypto, Market.Binance),
new TestCaseData("USDJPY", SecurityType.Crypto, Market.Binance),
new TestCaseData("BTCETH", SecurityType.Crypto, Market.Binance),
new TestCaseData("BTCUSD", SecurityType.Crypto, Market.Binance)
};
private static TestCaseData[] UnknownSecurityType => new[]
{
new TestCaseData("BTC-USD", SecurityType.Forex, Market.Coinbase)
};
private static TestCaseData[] UnknownMarket => new[]
{
new TestCaseData("ETH-USD", SecurityType.Crypto, Market.USA)
};
}
}