diff --git a/Framework/DiscordRewards/GiveRewardsCommand.cs b/Framework/DiscordRewards/GiveRewardsCommand.cs index 03bc835..0fd7a0f 100644 --- a/Framework/DiscordRewards/GiveRewardsCommand.cs +++ b/Framework/DiscordRewards/GiveRewardsCommand.cs @@ -3,6 +3,7 @@ public class GiveRewardsCommand { public RewardUsersCommand[] Rewards { get; set; } = Array.Empty(); + public MarketAverage[] Averages { get; set; } = Array.Empty(); } public class RewardUsersCommand @@ -10,4 +11,15 @@ public ulong RewardId { get; set; } public string[] UserAddresses { get; set; } = Array.Empty(); } + + public class MarketAverage + { + public int NumberOfFinished { get; set; } + public TimeSpan TimeRange { get; set; } + public float Price { get; set; } + public float Size { get; set; } + public float Duration { get; set; } + public float Collateral { get; set; } + public float ProofProbability { get; set; } + } } diff --git a/Tools/BiblioTech/Commands/MarketCommand.cs b/Tools/BiblioTech/Commands/MarketCommand.cs new file mode 100644 index 0000000..9289181 --- /dev/null +++ b/Tools/BiblioTech/Commands/MarketCommand.cs @@ -0,0 +1,57 @@ +using BiblioTech.Options; +using DiscordRewards; +using System.Globalization; +using Utils; + +namespace BiblioTech.Commands +{ + public class MarketCommand : BaseCommand + { + public override string Name => "market"; + public override string StartingMessage => RandomBusyMessage.Get(); + public override string Description => "Fetch some insights about current market conditions."; + + protected override async Task Invoke(CommandContext context) + { + await context.Followup(GetInsights()); + } + + private string[] GetInsights() + { + var result = Program.Averages.SelectMany(GetInsight).ToArray(); + if (result.Length > 0) + { + result = new[] + { + "No market insights available." + }; + } + return result; + } + + private string[] GetInsight(MarketAverage avg) + { + var headerLine = $"[Last {Time.FormatDuration(avg.TimeRange)}] ({avg.NumberOfFinished} Contracts finished)"; + + if (avg.NumberOfFinished == 0) + { + return new[] { headerLine }; + } + + return new[] + { + headerLine, + $"Price: {Format(avg.Price)}", + $"Size: {Format(avg.Size)}", + $"Duration: {Format(avg.Duration)}", + $"Collateral: {Format(avg.Collateral)}", + $"ProofProbability: {Format(avg.ProofProbability)}" + }; + } + + private string Format(float f) + { + return f.ToString("F3", CultureInfo.InvariantCulture); + } + } +} diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 8f7c98c..a604812 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -3,6 +3,7 @@ using BiblioTech.Commands; using BiblioTech.Rewards; using Discord; using Discord.WebSocket; +using DiscordRewards; using Logging; namespace BiblioTech @@ -16,6 +17,7 @@ namespace BiblioTech public static AdminChecker AdminChecker { get; private set; } = null!; public static IDiscordRoleDriver RoleDriver { get; set; } = null!; public static ILog Log { get; private set; } = null!; + public static MarketAverage[] Averages { get; set; } = Array.Empty(); public static Task Main(string[] args) { @@ -49,7 +51,8 @@ namespace BiblioTech sprCommand, associateCommand, notifyCommand, - new AdminCommand(sprCommand) + new AdminCommand(sprCommand), + new MarketCommand() ); await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); diff --git a/Tools/BiblioTech/Rewards/RewardController.cs b/Tools/BiblioTech/Rewards/RewardController.cs index c9a19de..6bf7b9f 100644 --- a/Tools/BiblioTech/Rewards/RewardController.cs +++ b/Tools/BiblioTech/Rewards/RewardController.cs @@ -23,6 +23,7 @@ namespace BiblioTech.Rewards { try { + Program.Averages = cmd.Averages; await Program.RoleDriver.GiveRewards(cmd); } catch (Exception ex) diff --git a/Tools/TestNetRewarder/ChainState.cs b/Tools/TestNetRewarder/ChainState.cs index 40e4bf6..b57c39b 100644 --- a/Tools/TestNetRewarder/ChainState.cs +++ b/Tools/TestNetRewarder/ChainState.cs @@ -15,7 +15,7 @@ namespace TestNetRewarder historicState.UpdateStorageRequests(contracts); StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray(); - FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFininshed).ToArray(); + FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFinished).ToArray(); RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange); RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange); SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange); diff --git a/Tools/TestNetRewarder/Configuration.cs b/Tools/TestNetRewarder/Configuration.cs index d19edb3..ed885d1 100644 --- a/Tools/TestNetRewarder/Configuration.cs +++ b/Tools/TestNetRewarder/Configuration.cs @@ -19,6 +19,9 @@ namespace TestNetRewarder [Uniform("check-history", "ch", "CHECKHISTORY", true, "Unix epoc timestamp of a moment in history on which processing begins. Required for hosting rewards. Should be 'launch of the testnet'.")] public int CheckHistoryTimestamp { get; set; } = 0; + [Uniform("market-insights", "mi", "MARKETINSIGHTS", false, "Semi-colon separated integers. Each represents a multiple of intervals, for which a market insights average will be generated.")] + public string MarketInsights { get; set; } = "1;96"; + public string LogPath { get @@ -26,5 +29,13 @@ namespace TestNetRewarder return Path.Combine(DataPath, "logs"); } } + + public TimeSpan Interval + { + get + { + return TimeSpan.FromMinutes(IntervalMinutes); + } + } } } diff --git a/Tools/TestNetRewarder/HistoricState.cs b/Tools/TestNetRewarder/HistoricState.cs index b1ac699..53fd1ba 100644 --- a/Tools/TestNetRewarder/HistoricState.cs +++ b/Tools/TestNetRewarder/HistoricState.cs @@ -33,7 +33,7 @@ namespace TestNetRewarder public EthAddress[] Hosts { get; private set; } public RequestState State { get; private set; } public bool RecentlyStarted { get; private set; } - public bool RecentlyFininshed { get; private set; } + public bool RecentlyFinished { get; private set; } public void Update(ICodexContracts contracts) { @@ -45,7 +45,7 @@ namespace TestNetRewarder State == RequestState.New && newState == RequestState.Started; - RecentlyFininshed = + RecentlyFinished = State == RequestState.Started && newState == RequestState.Finished; diff --git a/Tools/TestNetRewarder/MarketTracker.cs b/Tools/TestNetRewarder/MarketTracker.cs new file mode 100644 index 0000000..47957c8 --- /dev/null +++ b/Tools/TestNetRewarder/MarketTracker.cs @@ -0,0 +1,122 @@ +using CodexContractsPlugin.Marketplace; +using DiscordRewards; +using System.Numerics; + +namespace TestNetRewarder +{ + public class MarketTracker + { + private readonly List buffer = new List(); + + public MarketAverage[] ProcessChainState(ChainState chainState) + { + var intervalCounts = GetInsightCounts(); + if (!intervalCounts.Any()) return Array.Empty(); + + UpdateBuffer(chainState, intervalCounts.Max()); + var result = intervalCounts + .Select(GenerateMarketAverage) + .Where(a => a != null) + .Cast() + .ToArray(); + + if (!result.Any()) result = Array.Empty(); + return result; + } + + private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals) + { + buffer.Add(chainState); + while (buffer.Count > maxNumberOfIntervals) + { + buffer.RemoveAt(0); + } + } + + private MarketAverage? GenerateMarketAverage(int numberOfIntervals) + { + var states = SelectStates(numberOfIntervals); + return CreateAverage(states); + } + + private ChainState[] SelectStates(int numberOfIntervals) + { + if (numberOfIntervals < 1) return Array.Empty(); + return buffer.TakeLast(numberOfIntervals).ToArray(); + } + + private MarketAverage? CreateAverage(ChainState[] states) + { + try + { + return new MarketAverage + { + NumberOfFinished = CountNumberOfFinishedRequests(states), + TimeRange = GetTotalTimeRange(states), + Price = Average(states, s => s.Request.Ask.Reward), + Duration = Average(states, s => s.Request.Ask.Duration), + Size = Average(states, s => GetTotalSize(s.Request.Ask)), + Collateral = Average(states, s => s.Request.Ask.Collateral), + ProofProbability = Average(states, s => s.Request.Ask.ProofProbability) + }; + } + catch (Exception ex) + { + Program.Log.Error($"Exception in CreateAverage: {ex}"); + return null; + } + } + + private int GetTotalSize(Ask ask) + { + var nSlots = Convert.ToInt32(ask.Slots); + var slotSize = Convert.ToInt32(ask.SlotSize); + return nSlots * slotSize; + } + + private float Average(ChainState[] states, Func getValue) + { + return Average(states, s => Convert.ToInt32(getValue(s))); + } + + private float Average(ChainState[] states, Func getValue) + { + var sum = 0.0f; + var count = 0.0f; + foreach (var state in states) + { + foreach (var finishedRequest in state.FinishedRequests) + { + sum += getValue(finishedRequest); + count++; + } + } + + return sum / count; + } + + private TimeSpan GetTotalTimeRange(ChainState[] states) + { + return Program.Config.Interval * states.Length; + } + + private int CountNumberOfFinishedRequests(ChainState[] states) + { + return states.Sum(s => s.FinishedRequests.Length); + } + + private int[] GetInsightCounts() + { + try + { + var tokens = Program.Config.MarketInsights.Split(';').ToArray(); + return tokens.Select(t => Convert.ToInt32(t)).ToArray(); + } + catch (Exception ex) + { + Program.Log.Error($"Exception when parsing MarketInsights config parameters: {ex}"); + } + return Array.Empty(); + } + } +} diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index d710a90..8530330 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -10,6 +10,7 @@ namespace TestNetRewarder { private static readonly HistoricState historicState = new HistoricState(); private static readonly RewardRepo rewardRepo = new RewardRepo(); + private static readonly MarketTracker marketTracker = new MarketTracker(); private readonly ILog log; private BlockInterval? lastBlockRange; @@ -63,21 +64,30 @@ namespace TestNetRewarder ProcessReward(outgoingRewards, reward, chainState); } - log.Log($"Found {outgoingRewards.Count} rewards to send."); + var marketAverages = GetMarketAverages(chainState); + + log.Log($"Found {outgoingRewards.Count} rewards to send. Found {marketAverages.Length} market averages."); + if (outgoingRewards.Any()) { - if (!await SendRewardsCommand(outgoingRewards)) + if (!await SendRewardsCommand(outgoingRewards, marketAverages)) { log.Error("Failed to send reward command."); } } } - private async Task SendRewardsCommand(List outgoingRewards) + private MarketAverage[] GetMarketAverages(ChainState chainState) + { + return marketTracker.ProcessChainState(chainState); + } + + private async Task SendRewardsCommand(List outgoingRewards, MarketAverage[] marketAverages) { var cmd = new GiveRewardsCommand { - Rewards = outgoingRewards.ToArray() + Rewards = outgoingRewards.ToArray(), + Averages = marketAverages.ToArray() }; log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd)); diff --git a/Tools/TestNetRewarder/TimeSegmenter.cs b/Tools/TestNetRewarder/TimeSegmenter.cs index d14bcc7..44062f8 100644 --- a/Tools/TestNetRewarder/TimeSegmenter.cs +++ b/Tools/TestNetRewarder/TimeSegmenter.cs @@ -13,10 +13,10 @@ namespace TestNetRewarder { this.log = log; - if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 15; + if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1; if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet."); - segmentSize = TimeSpan.FromMinutes(configuration.IntervalMinutes); + segmentSize = configuration.Interval; start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; log.Log("Starting time segments at " + start);