Remove the concept of "Rate multiplier" and replace it with the concept of "Spread"

This commit is contained in:
nicolas.dorier
2018-08-01 18:38:46 +09:00
parent 5c8e03dcbf
commit 83304de1c6
11 changed files with 85 additions and 51 deletions

View File

@@ -46,8 +46,8 @@ namespace BTCPayServer.Tests
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
rules.GlobalMultiplier = 2.32m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
rules.Spread = 0.2m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
@@ -113,22 +113,22 @@ namespace BTCPayServer.Tests
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
builder.AppendLine("DOGE_BTC = 2000");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rules.GlobalMultiplier = 1.1m;
rules.Spread = 0.1m;
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.BidAsk.Bid);
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.BidAsk.Bid);
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
////////
// Make sure kraken is not converted to CurrencyPair

View File

@@ -982,8 +982,8 @@ namespace BTCPayServer.Tests
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@@ -997,7 +997,11 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
}
}
@@ -1079,7 +1083,7 @@ namespace BTCPayServer.Tests
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(1.0, rateVm.RateMultiplier);
Assert.Equal(0.0, rateVm.Spread);
Assert.Null(rateVm.TestRateRules);
rateVm.PreferredExchange = "bitflyer";
@@ -1088,13 +1092,13 @@ namespace BTCPayServer.Tests
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
rateVm.RateMultiplier = 1.1;
rateVm.Spread = 10;
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.NotNull(rateVm.TestRateRules);
Assert.Equal(2, rateVm.TestRateRules.Count);
Assert.False(rateVm.TestRateRules[0].Error);
Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.True(rateVm.TestRateRules[1].Error);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
@@ -1107,19 +1111,19 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_JPY";
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.ShowScripting);
Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);";
rateVm.RateMultiplier = 0.5;
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(0.5, rateVm.RateMultiplier);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.82</Version>
<Version>1.0.2.83</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
@@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
model.AvailableExchanges = GetSupportedExchanges();
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.Spread = (decimal)model.Spread / 100.0m;
if (!model.ShowScripting)
{

View File

@@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateRules.GlobalMultiplier = 1.0m;
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);

View File

@@ -281,23 +281,9 @@ namespace BTCPayServer.Data
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule_Obsolete>();
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public decimal Spread { get; set; } = 0.0m;
[Obsolete]
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
@@ -344,7 +330,7 @@ namespace BTCPayServer.Data
}
else
{
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}
}
@@ -370,7 +356,7 @@ namespace BTCPayServer.Data
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}

View File

@@ -53,6 +53,12 @@ namespace BTCPayServer.HostedServices
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertMultiplierToSpread)
{
await ConvertMultiplierToSpread();
settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@@ -61,6 +67,30 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertMultiplierToSpread()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0612 // Type or member is obsolete
decimal multiplier = 1.0m;
if (blob.RateRules != null && blob.RateRules.Count != 0)
{
foreach (var rule in blob.RateRules)
{
multiplier = rule.Apply(null, multiplier);
}
}
blob.RateRules = null;
blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m)));
#pragma warning restore CS0612 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private Task UnreachableStoreCheck()
{
return _StoreRepository.CleanUnreachableStores();

View File

@@ -44,9 +44,9 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
[Display(Name = "Add a spread on exchange rate of ... %")]
[Range(0.0, 100.0)]
public double Spread
{
get;
set;

View File

@@ -99,7 +99,20 @@ namespace BTCPayServer.Rating
SyntaxNode root;
RuleList ruleList;
public decimal GlobalMultiplier { get; set; } = 1.0m;
decimal _Spread;
public decimal Spread
{
get
{
return _Spread;
}
set
{
if (value > 1.0m || value < 0.0m)
throw new ArgumentOutOfRangeException(paramName: nameof(value), message: "The spread should be between 0 and 1");
_Spread = value;
}
}
RateRules(SyntaxNode root)
{
@@ -136,9 +149,9 @@ namespace BTCPayServer.Rating
if (currencyPair.Left == currencyPair.Right)
return new RateRule(this, currencyPair, CreateExpression("1.0"));
var candidate = FindBestCandidate(currencyPair);
if (GlobalMultiplier != decimal.One)
if (Spread != decimal.Zero)
{
candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}");
candidate = CreateExpression($"({candidate}) * ({(1.0m - Spread).ToString(CultureInfo.InvariantCulture)}, {(1.0m + Spread).ToString(CultureInfo.InvariantCulture)})");
}
return new RateRule(this, currencyPair, candidate);
}
@@ -257,7 +270,7 @@ namespace BTCPayServer.Rating
{
case SyntaxKind.UnaryMinusExpression:
var v = Values.Pop();
if(v.Bid == v.Ask)
if (v.Bid == v.Ask)
{
Values.Push(-v);
}
@@ -338,7 +351,7 @@ namespace BTCPayServer.Rating
{
_TupleValues = new Stack<decimal>();
base.VisitTupleExpression(node);
if(_TupleValues.Count != 2)
if (_TupleValues.Count != 2)
{
Errors.Add(RateRulesErrors.MissingArgument);
}

View File

@@ -9,5 +9,6 @@ namespace BTCPayServer.Services
{
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
}
}

View File

@@ -132,9 +132,9 @@
</div>
}
<div class="form-group">
<label asp-for="RateMultiplier"></label>
<input asp-for="RateMultiplier" class="form-control" />
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
<label asp-for="Spread"></label>
<input asp-for="Spread" class="form-control" />
<span asp-validation-for="Spread" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Testing</h5>