├── CryptoTickerBot.Data
├── Domain
│ ├── UserRole.cs
│ ├── CryptoExchangeId.cs
│ ├── TelegramBotUser.cs
│ └── CryptoCoin.cs
├── Configs
│ ├── IConfig.cs
│ └── ConfigManager.cs
├── Extensions
│ ├── CryptoCoinExtensions.cs
│ ├── ListExtensions.cs
│ ├── ObjectExtensions.cs
│ └── StringExtensions.cs
├── Converters
│ ├── StringDateTimeConverter.cs
│ ├── DecimalConverter.cs
│ └── UriConverter.cs
├── NLog.config
├── CryptoTickerBot.Data.csproj
└── Helpers
│ └── PriceChange.cs
├── CryptoTickerBot.Arbitrage
├── Common
│ ├── Node.cs
│ ├── Edge.cs
│ ├── Cycle.cs
│ └── CycleMap.cs
├── Interfaces
│ ├── IEdge.cs
│ ├── ICycle.cs
│ ├── INode.cs
│ └── IGraph.cs
├── CryptoTickerBot.Arbitrage.csproj
├── Abstractions
│ ├── EdgeBase.cs
│ ├── CycleBase.cs
│ ├── GraphBase.cs
│ └── NodeBase.cs
├── Extensions.cs
└── IntraExchange
│ └── Graph.cs
├── NLog.Targets.Sentry
└── NLog.Targets.Sentry.csproj
├── CryptoTickerBot.CUI
├── CryptoTickerBot.CUI.csproj
└── ConsolePrintService.cs
├── CryptoTickerBot.Telegram
├── CryptoTickerBot.Telegram.csproj
├── Interfaces
│ ├── ITelegramBot.cs
│ ├── IPage.cs
│ └── IMenu.cs
├── Extensions
│ ├── StringExtensions.cs
│ ├── MessageExtensions.cs
│ ├── EnumerableExtensions.cs
│ └── CryptoExchangeExtensions.cs
├── TelegramBotService.cs
├── TelegramBotConfig.cs
├── Menus
│ ├── Pages
│ │ ├── MainPage.cs
│ │ ├── EditSubscriptionPage.cs
│ │ ├── SelectionPage.cs
│ │ └── ManageSubscriptionsPage.cs
│ ├── TelegramMenuManager.cs
│ ├── TelegramKeyboardMenu.cs
│ └── Abstractions
│ │ └── PageBase.cs
├── TelegramBotData.cs
├── TelegramBotBase.cs
└── Subscriptions
│ └── TelegramPercentChangeSubscription.cs
├── CryptoTickerBot.Core
├── Interfaces
│ ├── ICryptoExchangeSubscription.cs
│ ├── IBotService.cs
│ ├── IBot.cs
│ └── ICryptoExchange.cs
├── Helpers
│ ├── Utility.cs
│ └── FiatConverter.cs
├── Extensions
│ └── EnumerableExtensions.cs
├── CryptoTickerBot.Core.csproj
├── Exchanges
│ ├── CoinDeltaExchange.cs
│ ├── CoinbaseExchange.cs
│ ├── ZebpayExchange.cs
│ ├── KoinexExchange.cs
│ ├── KrakenExchange.cs
│ └── BitstampExchange.cs
├── Abstractions
│ ├── BotServiceBase.cs
│ └── CryptoExchangeSubscriptionBase.cs
├── Markets.cs
├── Subscriptions
│ └── PercentChangeSubscription.cs
└── CoreConfig.cs
├── CryptoTickerBot.Collections
├── CryptoTickerBot.Collections.csproj
└── Persistent
│ ├── Base
│ ├── IPersistentCollection.cs
│ ├── OpenCollections.cs
│ └── PersistentCollection.cs
│ ├── PersistentList.cs
│ ├── PersistentDictionary.cs
│ └── PersistentSet.cs
├── CryptoTickerBot.Runner
├── Properties
│ └── PublishProfiles
│ │ ├── Linux x64.pubxml
│ │ └── Win x86.pubxml
├── CryptoTickerBot.Runner.csproj
├── RunnerConfig.cs
└── Program.cs
├── CryptoTickerBot.GoogleSheets
├── CryptoTickerBot.GoogleSheets.csproj
├── SheetsConfig.cs
├── Utility.cs
└── GoogleSheetsUpdaterService.cs
├── .travis.yml
├── CryptoTickerBot.UnitTests
├── DictionaryTests.cs
├── ArbitrageTests
│ ├── EdgeTests.cs
│ ├── NodeTests.cs
│ └── CycleTests.cs
├── CryptoTickerBot.UnitTests.csproj
└── PersistentCollectionsTest.cs
├── .gitattributes
├── CryptoTickerBot.sln.DotSettings
├── CODE_OF_CONDUCT.md
├── .gitignore
└── CryptoTickerBot.sln
/CryptoTickerBot.Data/Domain/UserRole.cs:
--------------------------------------------------------------------------------
1 | namespace CryptoTickerBot.Data.Domain
2 | {
3 | public enum UserRole
4 | {
5 | Guest = 0,
6 | Registered = 20,
7 | Admin = 80,
8 | Owner = 100
9 | }
10 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Common/Node.cs:
--------------------------------------------------------------------------------
1 | using CryptoTickerBot.Arbitrage.Abstractions;
2 |
3 | namespace CryptoTickerBot.Arbitrage.Common
4 | {
5 | public class Node : NodeBase
6 | {
7 | public Node ( string symbol ) : base ( symbol )
8 | {
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Interfaces/IEdge.cs:
--------------------------------------------------------------------------------
1 | namespace CryptoTickerBot.Arbitrage.Interfaces
2 | {
3 | public interface IEdge
4 | {
5 | INode From { get; }
6 | INode To { get; }
7 | decimal OriginalCost { get; }
8 |
9 | double Weight { get; }
10 | void CopyFrom ( IEdge edge );
11 | }
12 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Domain/CryptoExchangeId.cs:
--------------------------------------------------------------------------------
1 | namespace CryptoTickerBot.Data.Domain
2 | {
3 | public enum CryptoExchangeId
4 | {
5 | Koinex = 1,
6 | BitBay,
7 | Binance,
8 | CoinDelta,
9 | Coinbase,
10 | Kraken,
11 | Bitstamp,
12 | Bitfinex,
13 | Poloniex,
14 | Zebpay
15 | }
16 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Common/Edge.cs:
--------------------------------------------------------------------------------
1 | using CryptoTickerBot.Arbitrage.Abstractions;
2 | using CryptoTickerBot.Arbitrage.Interfaces;
3 |
4 | namespace CryptoTickerBot.Arbitrage.Common
5 | {
6 | public class Edge : EdgeBase
7 | {
8 | public Edge ( INode from,
9 | INode to,
10 | decimal cost ) : base ( from, to, cost )
11 | {
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/CryptoTickerBot.Arbitrage.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Interfaces/ICycle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 |
4 | namespace CryptoTickerBot.Arbitrage.Interfaces
5 | {
6 | public interface ICycle : IEquatable> where TNode : INode
7 | {
8 | ImmutableList Path { get; }
9 | ImmutableList Edges { get; }
10 |
11 | int Length { get; }
12 | double Weight { get; }
13 |
14 | double UpdateWeight ( );
15 | }
16 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Common/Cycle.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using CryptoTickerBot.Arbitrage.Abstractions;
3 | using CryptoTickerBot.Arbitrage.Interfaces;
4 |
5 | namespace CryptoTickerBot.Arbitrage.Common
6 | {
7 | public class Cycle : CycleBase where TNode : INode
8 | {
9 | public Cycle ( IEnumerable path ) : base ( path )
10 | {
11 | }
12 |
13 | public Cycle ( params TNode[] path ) : base ( path )
14 | {
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/NLog.Targets.Sentry/NLog.Targets.Sentry.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Configs/IConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Newtonsoft.Json;
4 |
5 | namespace CryptoTickerBot.Data.Configs
6 | {
7 | public interface IConfig where TConfig : IConfig
8 | {
9 | [JsonIgnore]
10 | string ConfigFileName { get; }
11 |
12 | [JsonIgnore]
13 | string ConfigFolderName { get; }
14 |
15 | bool TryValidate ( out IList exceptions );
16 |
17 | TConfig RestoreDefaults ( );
18 | }
19 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.CUI/CryptoTickerBot.CUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/CryptoTickerBot.Telegram.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Interfaces/INode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CryptoTickerBot.Arbitrage.Interfaces
5 | {
6 | public interface INode : IEquatable, IComparable
7 | {
8 | string Symbol { get; }
9 | IReadOnlyDictionary EdgeTable { get; }
10 | IEnumerable Edges { get; }
11 |
12 | IEdge this [ string symbol ] { get; }
13 |
14 | bool AddOrUpdateEdge ( IEdge edge );
15 | bool HasEdge ( string symbol );
16 | }
17 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Interfaces/ICryptoExchangeSubscription.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CryptoTickerBot.Data.Domain;
3 | using Newtonsoft.Json;
4 |
5 | namespace CryptoTickerBot.Core.Interfaces
6 | {
7 | public interface ICryptoExchangeSubscription :
8 | IDisposable,
9 | IObserver,
10 | IEquatable
11 | {
12 | Guid Id { get; }
13 |
14 | [JsonIgnore]
15 | ICryptoExchange Exchange { get; }
16 |
17 | DateTime CreationTime { get; }
18 |
19 | [JsonIgnore]
20 | TimeSpan ActiveSince { get; }
21 |
22 | void Stop ( );
23 | }
24 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Interfaces/ITelegramBot.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Polly;
5 | using Telegram.Bot;
6 | using Telegram.Bot.Types;
7 |
8 | namespace CryptoTickerBot.Telegram.Interfaces
9 | {
10 | public interface ITelegramBot
11 | {
12 | TelegramBotClient Client { get; }
13 | User Self { get; }
14 | TelegramBotConfig Config { get; }
15 | Policy Policy { get; }
16 | DateTime StartTime { get; }
17 | CancellationToken CancellationToken { get; }
18 | Task StartAsync ( );
19 | void Stop ( );
20 | }
21 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/CryptoTickerBot.Collections.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Interfaces/IPage.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Telegram.Bot.Types;
4 | using Telegram.Bot.Types.ReplyMarkups;
5 |
6 | namespace CryptoTickerBot.Telegram.Interfaces
7 | {
8 | public interface IPage
9 | {
10 | string Title { get; }
11 | IEnumerable> Labels { get; }
12 | InlineKeyboardMarkup Keyboard { get; }
13 | IPage PreviousPage { get; }
14 | IMenu Menu { get; }
15 | Task HandleMessageAsync ( Message message );
16 | Task HandleQueryAsync ( CallbackQuery query );
17 | Task WaitForButtonPressAsync ( );
18 | }
19 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Interfaces/IBotService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using CryptoTickerBot.Data.Domain;
4 |
5 | namespace CryptoTickerBot.Core.Interfaces
6 | {
7 | public interface IBotService : IDisposable, IEquatable
8 | {
9 | Guid Guid { get; }
10 | IBot Bot { get; }
11 | bool IsAttached { get; }
12 |
13 | Task AttachToAsync ( IBot bot );
14 | Task DetachAsync ( );
15 |
16 | Task OnNextAsync ( ICryptoExchange exchange,
17 | CryptoCoin coin );
18 |
19 | Task OnChangedAsync ( ICryptoExchange exchange,
20 | CryptoCoin coin );
21 | }
22 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Extensions/CryptoCoinExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Diagnostics.Contracts;
3 | using CryptoTickerBot.Data.Domain;
4 |
5 | namespace CryptoTickerBot.Data.Extensions
6 | {
7 | public static class CryptoCoinExtensions
8 | {
9 | [DebuggerStepThrough]
10 | [Pure]
11 | public static decimal Buy ( this CryptoCoin coin,
12 | decimal amountInUsd ) => amountInUsd / coin.BuyPrice;
13 |
14 | [DebuggerStepThrough]
15 | [Pure]
16 | public static decimal Sell ( this CryptoCoin coin,
17 | decimal quantity ) => coin.SellPrice * quantity;
18 | }
19 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Runner/Properties/PublishProfiles/Linux x64.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | FileSystem
8 | Release
9 | Any CPU
10 | netcoreapp3.0
11 | bin\Release\netcoreapp3.0\publish\linux
12 | linux-x64
13 | true
14 | <_IsPortable>false
15 |
16 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Runner/Properties/PublishProfiles/Win x86.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | FileSystem
8 | Release
9 | Any CPU
10 | netcoreapp3.0
11 | bin\Release\netcoreapp3.0\publish\windows
12 | true
13 | <_IsPortable>false
14 | win-x86
15 |
16 |
--------------------------------------------------------------------------------
/CryptoTickerBot.GoogleSheets/CryptoTickerBot.GoogleSheets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Runner/CryptoTickerBot.Runner.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.2
6 | 7.3
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using Humanizer;
2 | using Telegram.Bot.Types.Enums;
3 | using Telegram.Bot.Types.InlineQueryResults;
4 | using Telegram.Bot.Types.ReplyMarkups;
5 |
6 | namespace CryptoTickerBot.Telegram.Extensions
7 | {
8 | public static class StringExtensions
9 | {
10 | public static string ToMarkdown ( this string str ) =>
11 | $"```\n{str.Truncate ( 4000 )}\n```";
12 |
13 | public static InputTextMessageContent ToMarkdownMessage ( this string str ) =>
14 | new InputTextMessageContent ( $"```\n{str.Truncate ( 4000 )}\n```" ) {ParseMode = ParseMode.Markdown};
15 |
16 | public static InlineKeyboardButton ToKeyboardButton ( this string label ) =>
17 | new InlineKeyboardButton
18 | {
19 | Text = label.Titleize ( ),
20 | CallbackData = label
21 | };
22 | }
23 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Runner/RunnerConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CryptoTickerBot.Data.Configs;
4 |
5 | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
6 |
7 | namespace CryptoTickerBot.Runner
8 | {
9 | public class RunnerConfig : IConfig
10 | {
11 | public string ConfigFileName { get; } = "Runner";
12 | public string ConfigFolderName { get; } = "Configs";
13 |
14 | public bool EnableConsoleService { get; set; } = false;
15 | public bool EnableGoogleSheetsService { get; set; } = true;
16 | public bool EnableTelegramService { get; set; } = true;
17 |
18 | public bool TryValidate ( out IList exceptions )
19 | {
20 | exceptions = new List ( );
21 | return true;
22 | }
23 |
24 | public RunnerConfig RestoreDefaults ( ) => this;
25 | }
26 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Abstractions/EdgeBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CryptoTickerBot.Arbitrage.Interfaces;
3 |
4 | namespace CryptoTickerBot.Arbitrage.Abstractions
5 | {
6 | public abstract class EdgeBase : IEdge
7 | {
8 | public virtual INode From { get; }
9 | public virtual INode To { get; }
10 | public decimal OriginalCost { get; protected set; }
11 |
12 | public virtual double Weight => -Math.Log ( (double) OriginalCost );
13 |
14 | protected EdgeBase ( INode from,
15 | INode to,
16 | decimal cost )
17 | {
18 | From = from;
19 | To = to;
20 | OriginalCost = cost;
21 | }
22 |
23 | public virtual void CopyFrom ( IEdge edge )
24 | {
25 | OriginalCost = edge.OriginalCost;
26 | }
27 |
28 | public override string ToString ( ) => $"{From.Symbol} -> {To.Symbol} {OriginalCost} {Weight}";
29 | }
30 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/TelegramBotService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using CryptoTickerBot.Core.Abstractions;
3 |
4 | namespace CryptoTickerBot.Telegram
5 | {
6 | public class TelegramBotService : BotServiceBase
7 | {
8 | public TelegramBot TelegramBot { get; set; }
9 | public TelegramBotConfig TelegramBotConfig { get; set; }
10 |
11 | public TelegramBotService ( TelegramBotConfig telegramBotConfig )
12 | {
13 | TelegramBotConfig = telegramBotConfig;
14 | }
15 |
16 | public override Task StartAsync ( )
17 | {
18 | TelegramBot = new TelegramBot ( TelegramBotConfig, Bot );
19 |
20 | TelegramBot.Ctb.Start += async bot =>
21 | await TelegramBot.StartAsync ( ).ConfigureAwait ( false );
22 |
23 | return Task.CompletedTask;
24 | }
25 |
26 | public override Task StopAsync ( )
27 | {
28 | TelegramBot.Stop ( );
29 | return Task.CompletedTask;
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Domain/TelegramBotUser.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace CryptoTickerBot.Data.Domain
4 | {
5 | public class TelegramBotUser
6 | {
7 | [JsonProperty ( Required = Required.Always )]
8 | public int Id { get; set; }
9 |
10 | [JsonProperty ( Required = Required.Always )]
11 | public bool IsBot { get; set; }
12 |
13 | [JsonProperty ( Required = Required.Always )]
14 | public string FirstName { get; set; }
15 |
16 | [JsonProperty ( DefaultValueHandling = DefaultValueHandling.Ignore )]
17 | public string LastName { get; set; }
18 |
19 | [JsonProperty ( DefaultValueHandling = DefaultValueHandling.Ignore )]
20 | public string Username { get; set; }
21 |
22 | [JsonProperty ( DefaultValueHandling = DefaultValueHandling.Ignore )]
23 | public string LanguageCode { get; set; }
24 |
25 | [JsonProperty ( Required = Required.Always )]
26 | public UserRole Role { get; set; }
27 | }
28 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Extensions/MessageExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Telegram.Bot.Types;
5 |
6 | namespace CryptoTickerBot.Telegram.Extensions
7 | {
8 | public static class MessageExtensions
9 | {
10 | public static (string command, List @params ) ExtractCommand ( this Message message,
11 | User self )
12 | {
13 | var text = message.Text;
14 | var command = text.Split ( ' ' ).First ( );
15 |
16 | var index = command.IndexOf ( $"@{self.Username}", StringComparison.OrdinalIgnoreCase );
17 | if ( index != -1 )
18 | command = command.Substring ( 0, index );
19 |
20 | var @params = text
21 | .Split ( new[] {' '}, StringSplitOptions.RemoveEmptyEntries )
22 | .Skip ( 1 )
23 | .ToList ( );
24 |
25 | return ( command, @params );
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Helpers/Utility.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net;
3 | using System.Threading.Tasks;
4 |
5 | namespace CryptoTickerBot.Core.Helpers
6 | {
7 | public static class Utility
8 | {
9 | static Utility ( )
10 | {
11 | ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
12 | }
13 |
14 | public static async Task DownloadWebPageAsync ( string url )
15 | {
16 | var client = new WebClient ( );
17 |
18 | client.Headers.Add ( "user-agent",
19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36Mozilla/5.0" );
20 |
21 | var data = await client.OpenReadTaskAsync ( url ).ConfigureAwait ( false );
22 |
23 | if ( data == null ) return null;
24 |
25 | var reader = new StreamReader ( data );
26 | var s = await reader.ReadToEndAsync ( ).ConfigureAwait ( false );
27 | return s;
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: csharp
2 | mono: none
3 | sudo: required
4 | dist: xenial
5 | dotnet: 2.2
6 | script:
7 | - dotnet restore
8 | - dotnet build
9 | - dotnet test CryptoTickerBot.UnitTests
10 | global:
11 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
12 | - DOTNET_CLI_TELEMETRY_OPTOUT=1
13 | notifications:
14 | slack:
15 | secure: Nd4AAHKwcxeF7T8ieUYzZ4dWyv9gSYeC4XPc8ajrZEWy7v73L15QSoOjELXgGUpnzXjs8ptTT+k61TADiT6UlV/iE5gY/S2tp1tZwDR3oWMZT7BMBRMNWFUakA8tQc4T9u+35isGC60Kc3Qb3fZt3JsABOmAbacHAt58FK6oGmZecMVzTZj62XrYpG6izornhEX891knNetnGI3tcZnaKaQSBM01WeSkzJOkRT2EypFfiNBzHXiwSuXL7rv6enR9gtJBPWlZyjKLgqs/8BoS0TrTbMZ6E7lWBe0WytMx62/ORo1RMh+smNOPOs8sl6xwmr+TWm1oDGix95x+AQ21tLdXX3paxnixpsRueLRTtN1xe6OqrAHXOsjh550CAedqlGGbdDZIwelZNw8kwaxBqGhUsr3P30BuC130JxFkvQxW7fidINVWtpbuHa30qzfXYNxr0fSS7Fr2VOYMpxrhclejTbxSu3V10oDO7S28/ovgtvWyRfZM9AfsGIDW+g33/GuIml2IrmPfME6tsx0fT+Ct3zNP3wJOvyoMkNLvC2tzwjQj2PaYW8lD+xmOXai7foPckoatRos1dY3VXp/Rbual5sWB/5sGL2jgwnlvo2xEzZP0kzbmRIWA4fejxGPmHdf2cCeFufQyGF7x8J0jiDxCqOTd11n575T/AAoCRv8=
16 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using MoreLinq;
4 | using Telegram.Bot.Types.ReplyMarkups;
5 |
6 | namespace CryptoTickerBot.Telegram.Extensions
7 | {
8 | public static class EnumerableExtensions
9 | {
10 | public static InlineKeyboardMarkup ToInlineKeyboardMarkup ( this IEnumerable labels ) =>
11 | new InlineKeyboardMarkup ( labels.Select ( x => x.ToKeyboardButton ( ) ) );
12 |
13 | public static InlineKeyboardMarkup ToInlineKeyboardMarkup ( this IEnumerable labels,
14 | int batchSize ) =>
15 | new InlineKeyboardMarkup ( labels.Select ( x => x.ToKeyboardButton ( ) ).Batch ( batchSize ) );
16 |
17 | public static InlineKeyboardMarkup ToInlineKeyboardMarkup ( this IEnumerable> labels ) =>
18 | new InlineKeyboardMarkup ( labels.Select ( x => x.Select ( y => y.ToKeyboardButton ( ) ) ).ToArray ( ) );
19 | }
20 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/Persistent/Base/IPersistentCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Newtonsoft.Json;
4 |
5 | namespace CryptoTickerBot.Collections.Persistent.Base
6 | {
7 | public interface IPersistentCollection : IDisposable
8 | {
9 | string FileName { get; }
10 | JsonSerializerSettings SerializerSettings { get; set; }
11 | TimeSpan FlushInterval { get; }
12 |
13 | event SaveDelegate OnSave;
14 | event LoadDelegate OnLoad;
15 | event ErrorDelegate OnError;
16 |
17 | void ForceSave ( );
18 | void Save ( );
19 | bool Load ( );
20 | }
21 |
22 | public interface IPersistentCollection : IPersistentCollection, ICollection
23 | {
24 | }
25 |
26 | public delegate void SaveDelegate ( IPersistentCollection collection );
27 |
28 | public delegate void LoadDelegate ( IPersistentCollection collection );
29 |
30 | public delegate void ErrorDelegate ( IPersistentCollection collection,
31 | Exception exception );
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.UnitTests/DictionaryTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Newtonsoft.Json;
5 | using NUnit.Framework;
6 |
7 | namespace CryptoTickerBot.UnitTests
8 | {
9 | [TestFixture]
10 | public class DictionaryTests
11 | {
12 | [Test]
13 | public void DictionaryRetainsInsertionOrder ( )
14 | {
15 | var random = new Random ( 42 );
16 | var normal = new Dictionary ( );
17 | var keys = new List ( 10000 );
18 | foreach ( var _ in Enumerable.Range ( 1, 10000 ) )
19 | {
20 | var value = random.Next ( 100, int.MaxValue );
21 | var key = $"{value}";
22 | keys.Add ( key );
23 | normal[key] = value;
24 | }
25 |
26 | var json = JsonConvert.SerializeObject ( normal );
27 | var rebuilt = JsonConvert.DeserializeObject> ( json );
28 | Assert.True ( new Dictionary ( normal ).Keys.SequenceEqual ( rebuilt.Keys ) );
29 | Assert.True ( keys.SequenceEqual ( rebuilt.Keys ) );
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.UnitTests/ArbitrageTests/EdgeTests.cs:
--------------------------------------------------------------------------------
1 | using CryptoTickerBot.Arbitrage.Common;
2 | using NUnit.Framework;
3 |
4 | namespace CryptoTickerBot.UnitTests.ArbitrageTests
5 | {
6 | [TestFixture]
7 | public class EdgeTests
8 | {
9 | [Test]
10 | public void EdgeCopyFromShouldNotChangeNodeReferences ( )
11 | {
12 | var nodeA = new Node ( "A" );
13 | var nodeB = new Node ( "B" );
14 | var nodeC = new Node ( "C" );
15 | var nodeD = new Node ( "D" );
16 | var edgeAb = new Edge ( nodeA, nodeB, 42m );
17 | var edgeCd = new Edge ( nodeC, nodeD, 0m );
18 |
19 | Assert.AreSame ( edgeCd.From, nodeC );
20 | Assert.AreSame ( edgeCd.To, nodeD );
21 | Assert.AreNotEqual ( edgeAb.To, edgeCd.To );
22 | Assert.AreEqual ( edgeAb.OriginalCost, 42m );
23 | Assert.AreEqual ( edgeCd.OriginalCost, 0m );
24 |
25 | edgeCd.CopyFrom ( edgeAb );
26 | Assert.AreSame ( edgeCd.From, nodeC );
27 | Assert.AreSame ( edgeCd.To, nodeD );
28 | Assert.AreEqual ( edgeAb.OriginalCost, 42m );
29 | Assert.AreEqual ( edgeCd.OriginalCost, 42m );
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/TelegramBotConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CryptoTickerBot.Data.Configs;
5 |
6 | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
7 |
8 | namespace CryptoTickerBot.Telegram
9 | {
10 | public class TelegramBotConfig : IConfig
11 | {
12 | public string ConfigFileName { get; } = "TelegramBot";
13 | public string ConfigFolderName { get; } = "Configs";
14 |
15 | public string BotToken { get; set; }
16 | public int RetryLimit { get; set; } = 5;
17 | public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds ( 5 );
18 |
19 | public TelegramBotConfig RestoreDefaults ( ) =>
20 | new TelegramBotConfig
21 | {
22 | BotToken = BotToken
23 | };
24 |
25 | public bool TryValidate ( out IList exceptions )
26 | {
27 | exceptions = new List ( );
28 |
29 | if ( string.IsNullOrEmpty ( BotToken ) )
30 | exceptions.Add ( new ArgumentException ( "Bot Token missing", nameof ( BotToken ) ) );
31 |
32 | return !exceptions.Any ( );
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.CUI/ConsolePrintService.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using System.Threading.Tasks;
3 | using Colorful;
4 | using CryptoTickerBot.Core.Abstractions;
5 | using CryptoTickerBot.Core.Interfaces;
6 | using CryptoTickerBot.Data.Domain;
7 |
8 | namespace CryptoTickerBot.CUI
9 | {
10 | public class ConsolePrintService : BotServiceBase
11 | {
12 | public StyleSheet StyleSheet { get; }
13 | private readonly object consoleLock = new object ( );
14 |
15 | public ConsolePrintService ( )
16 | {
17 | StyleSheet = new StyleSheet ( Color.White );
18 | StyleSheet.AddStyle ( @"Highest Bid = (0|[1-9][\d,]*)?(\.\d+)?(?<=\d)", Color.GreenYellow );
19 | StyleSheet.AddStyle ( @"Lowest Ask = (0|[1-9][\d,]*)?(\.\d+)?(?<=\d)", Color.OrangeRed );
20 | }
21 |
22 | public override Task OnChangedAsync ( ICryptoExchange exchange,
23 | CryptoCoin coin )
24 | {
25 | lock ( consoleLock )
26 | {
27 | Console.Write ( $"{exchange.Name,-12}", Color.DodgerBlue );
28 | Console.WriteLineStyled ( $" | {coin}", StyleSheet );
29 | }
30 |
31 | return Task.CompletedTask;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics.Contracts;
3 | using System.Linq;
4 | using CryptoTickerBot.Core.Interfaces;
5 |
6 | namespace CryptoTickerBot.Core.Extensions
7 | {
8 | public static class EnumerableExtensions
9 | {
10 | [Pure]
11 | public static decimal Product ( this IEnumerable enumerable ) =>
12 | enumerable.Aggregate ( 1m, ( cur,
13 | next ) => cur * next );
14 |
15 | [Pure]
16 | public static double Product ( this IEnumerable enumerable ) =>
17 | enumerable.Aggregate ( 1d, ( cur,
18 | next ) => cur * next );
19 |
20 | [Pure]
21 | public static int Product ( this IEnumerable enumerable ) =>
22 | enumerable.Aggregate ( 1, ( cur,
23 | next ) => cur * next );
24 |
25 | [Pure]
26 | public static IEnumerable ToTables (
27 | this IEnumerable exchanges,
28 | string fiat = "USD"
29 | ) =>
30 | exchanges.Select ( exchange => $"{exchange.Name}\n{exchange.ToTable ( fiat )}" );
31 | }
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/Persistent/Base/OpenCollections.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 |
3 | namespace CryptoTickerBot.Collections.Persistent.Base
4 | {
5 | internal static class OpenCollections
6 | {
7 | public static ImmutableDictionary Data { get; private set; } =
8 | ImmutableDictionary.Empty;
9 |
10 | public static bool Add ( IPersistentCollection collection )
11 | {
12 | if ( Data.ContainsKey ( collection.FileName ) )
13 | return false;
14 |
15 | Data = Data.Add ( collection.FileName, collection );
16 | return true;
17 | }
18 |
19 | public static bool TryOpen ( string fileName,
20 | out IPersistentCollection opened )
21 | {
22 | if ( Data.TryGetValue ( fileName, out var existing ) )
23 | {
24 | opened = existing;
25 | return true;
26 | }
27 |
28 | opened = null;
29 | return false;
30 | }
31 |
32 | public static bool Remove ( string fileName )
33 | {
34 | if ( !Data.ContainsKey ( fileName ) )
35 | return false;
36 |
37 | Data = Data.Remove ( fileName );
38 | return true;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Converters/StringDateTimeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 |
4 | namespace CryptoTickerBot.Data.Converters
5 | {
6 | public class StringDateTimeConverter : JsonConverter
7 | {
8 | public override bool CanConvert ( Type t ) => t == typeof ( DateTime ) || t == typeof ( DateTime? );
9 |
10 | public override object ReadJson ( JsonReader reader,
11 | Type t,
12 | object existingValue,
13 | JsonSerializer serializer )
14 | {
15 | if ( reader.TokenType == JsonToken.Null ) return null;
16 | var value = serializer.Deserialize ( reader );
17 |
18 | if ( long.TryParse ( value, out var l ) )
19 | return DateTimeOffset
20 | .FromUnixTimeSeconds ( l )
21 | .UtcDateTime;
22 |
23 | throw new ArgumentException ( "Cannot un-marshall type long" );
24 | }
25 |
26 | public override void WriteJson ( JsonWriter writer,
27 | object untypedValue,
28 | JsonSerializer serializer )
29 | {
30 | // Not implemented
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/NLog.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Extensions/ListExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics;
3 | using System.Diagnostics.Contracts;
4 | using System.Reflection;
5 |
6 | namespace CryptoTickerBot.Data.Extensions
7 | {
8 | public static class ListExtensions
9 | {
10 | [DebuggerStepThrough]
11 | [Pure]
12 | public static string Join ( this IEnumerable enumerable,
13 | string delimiter ) =>
14 | string.Join ( delimiter, enumerable );
15 |
16 | [DebuggerStepThrough]
17 | [Pure]
18 | public static T GetFieldValue ( this object obj,
19 | string name )
20 | {
21 | var field = obj.GetType ( )
22 | .GetField ( name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );
23 | return (T) field?.GetValue ( obj );
24 | }
25 |
26 | [DebuggerStepThrough]
27 | [Pure]
28 | public static object GetFieldValue ( this object obj,
29 | string name )
30 | {
31 | var field = obj.GetType ( )
32 | .GetField ( name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );
33 | return field?.GetValue ( obj );
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Interfaces/IGraph.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 |
3 | namespace CryptoTickerBot.Arbitrage.Interfaces
4 | {
5 | public interface IGraph where TNode : INode
6 | {
7 | ImmutableDictionary Nodes { get; }
8 |
9 | TNode this [ string symbol ] { get; }
10 | NodeBuilderDelegate NodeBuilder { get; }
11 | EdgeBuilderDelegate DefaultEdgeBuilder { get; }
12 |
13 | bool ContainsNode ( string symbol );
14 | TNode AddNode ( string symbol );
15 |
16 | IEdge UpsertEdge ( string from,
17 | string to,
18 | decimal cost );
19 |
20 | TEdge UpsertEdge ( string from,
21 | string to,
22 | decimal cost,
23 | EdgeBuilderDelegate edgeBuilder ) where TEdge : class, IEdge;
24 | }
25 |
26 | public delegate TNode NodeBuilderDelegate ( string symbol );
27 |
28 | public delegate TEdge EdgeBuilderDelegate ( TNode from,
29 | TNode to,
30 | decimal cost );
31 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/CryptoTickerBot.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 | AnyCPU
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Interfaces/IMenu.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Telegram.Bot.Types;
3 | using Telegram.Bot.Types.ReplyMarkups;
4 |
5 | namespace CryptoTickerBot.Telegram.Interfaces
6 | {
7 | public interface IMenu
8 | {
9 | User User { get; }
10 | Chat Chat { get; }
11 | TelegramBot TelegramBot { get; }
12 | IPage CurrentPage { get; }
13 | Message LastMessage { get; }
14 | bool IsOpen { get; }
15 |
16 | Task DeleteAsync ( );
17 | Task DisplayAsync ( IPage page );
18 |
19 | Task SwitchPageAsync ( IPage page,
20 | bool replaceOld = false );
21 |
22 | Task HandleMessageAsync ( Message message );
23 | Task HandleQueryAsync ( CallbackQuery query );
24 |
25 | Task SendTextBlockAsync (
26 | string text,
27 | int replyToMessageId = 0,
28 | bool disableWebPagePreview = false,
29 | bool disableNotification = true,
30 | IReplyMarkup replyMarkup = null
31 | );
32 |
33 | Task EditTextBlockAsync (
34 | int messageId,
35 | string text,
36 | InlineKeyboardMarkup markup = null
37 | );
38 |
39 | Task RequestReplyAsync (
40 | string text,
41 | bool disableWebPagePreview = false,
42 | bool disableNotification = true
43 | );
44 |
45 | Task WaitForMessageAsync ( );
46 | }
47 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Extensions/CryptoExchangeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using CryptoTickerBot.Core.Interfaces;
3 | using Humanizer;
4 | using Humanizer.Localisation;
5 | using Telegram.Bot.Types.InlineQueryResults;
6 |
7 | namespace CryptoTickerBot.Telegram.Extensions
8 | {
9 | public static class CryptoExchangeExtensions
10 | {
11 | public static InlineQueryResultArticle ToInlineQueryResult (
12 | this ICryptoExchange exchange,
13 | params string[] symbols
14 | ) =>
15 | new InlineQueryResultArticle (
16 | exchange.Name, exchange.Name,
17 | $"{exchange.Name}\n{exchange.ToTable ( symbols )}".ToMarkdownMessage ( )
18 | );
19 |
20 | public static string GetSummary ( this ICryptoExchange exchange )
21 | {
22 | var sb = new StringBuilder ( );
23 |
24 | sb.AppendLine ( $"Name: {exchange.Name}" );
25 | sb.AppendLine ( $"Is Running: {exchange.IsStarted}" );
26 | sb.AppendLine ( $"Url: {exchange.Url}" );
27 | sb.AppendLine ( $"Up Time: {exchange.UpTime.Humanize ( 2, minUnit: TimeUnit.Second )}" );
28 | sb.AppendLine ( $"Last Change: {exchange.LastChangeDuration.Humanize ( 2 )}" );
29 | sb.AppendLine ( $"Base Symbols: {exchange.BaseSymbols.Humanize ( )}" );
30 | sb.AppendLine ( $"Total Pairs: {exchange.Count}" );
31 |
32 | return sb.ToString ( );
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Extensions/ObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace CryptoTickerBot.Data.Extensions
4 | {
5 | public static class ObjectExtensions
6 | {
7 | public static string ToJson ( this T value ) =>
8 | JsonConvert.SerializeObject ( value );
9 |
10 | public static string ToJson ( this T value,
11 | Formatting formatting,
12 | JsonSerializerSettings settings ) =>
13 | JsonConvert.SerializeObject ( value, formatting, settings );
14 |
15 | public static string ToJson ( this T value,
16 | Formatting formatting,
17 | params JsonConverter[] converters ) =>
18 | JsonConvert.SerializeObject ( value, formatting, converters );
19 |
20 | public static string ToJson ( this T value,
21 | params JsonConverter[] converters ) =>
22 | JsonConvert.SerializeObject ( value, converters );
23 |
24 | public static string ToJson ( this T value,
25 | Formatting formatting ) =>
26 | JsonConvert.SerializeObject ( value, formatting );
27 |
28 | public static string ToJson ( this T value,
29 | JsonSerializerSettings settings ) =>
30 | JsonConvert.SerializeObject ( value, settings );
31 | }
32 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/CryptoTickerBot.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 7.3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Always
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Converters/DecimalConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Newtonsoft.Json;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace CryptoTickerBot.Data.Converters
7 | {
8 | public class DecimalConverter : JsonConverter
9 | {
10 | public override bool CanConvert ( Type objectType ) =>
11 | objectType == typeof ( decimal ) || objectType == typeof ( decimal? );
12 |
13 | public override object ReadJson ( JsonReader reader,
14 | Type objectType,
15 | object existingValue,
16 | JsonSerializer serializer )
17 | {
18 | var token = JToken.Load ( reader );
19 |
20 | if ( token.Type == JTokenType.Float || token.Type == JTokenType.Integer )
21 | return token.ToObject ( );
22 |
23 | if ( token.Type == JTokenType.String )
24 | return decimal.Parse ( token.ToString ( ), NumberStyles.Number | NumberStyles.AllowExponent );
25 |
26 | if ( token.Type == JTokenType.Null && objectType == typeof ( decimal? ) )
27 | return null;
28 |
29 | throw new JsonSerializationException ( $"Unexpected token type: {token.Type}" );
30 | }
31 |
32 | public override void WriteJson ( JsonWriter writer,
33 | object value,
34 | JsonSerializer serializer )
35 | {
36 | throw new NotImplementedException ( );
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.UnitTests/CryptoTickerBot.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 |
6 | false
7 |
8 | 7.3
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/Persistent/PersistentList.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CryptoTickerBot.Collections.Persistent.Base;
4 |
5 | namespace CryptoTickerBot.Collections.Persistent
6 | {
7 | public sealed class PersistentList : PersistentCollection>, IList
8 | {
9 | public T this [ int index ]
10 | {
11 | get => Collection[index];
12 | set
13 | {
14 | Collection[index] = value;
15 | Save ( );
16 | }
17 | }
18 |
19 | private PersistentList ( string fileName,
20 | TimeSpan flushInterval )
21 | : base ( fileName, DefaultSerializerSettings, flushInterval )
22 | {
23 | }
24 |
25 | public int IndexOf ( T item ) => Collection.IndexOf ( item );
26 |
27 | public void Insert ( int index,
28 | T item )
29 | {
30 | Collection.Insert ( index, item );
31 | Save ( );
32 | }
33 |
34 | public void RemoveAt ( int index )
35 | {
36 | Collection.RemoveAt ( index );
37 | Save ( );
38 | }
39 |
40 | public static PersistentList Build ( string fileName ) =>
41 | Build ( fileName, DefaultFlushInterval );
42 |
43 | public static PersistentList Build ( string fileName,
44 | TimeSpan flushInterval )
45 | {
46 | var collection = GetOpenCollection> ( fileName );
47 |
48 | return collection ?? new PersistentList ( fileName, flushInterval );
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Converters/UriConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 |
4 | namespace CryptoTickerBot.Data.Converters
5 | {
6 | public class UriConverter : JsonConverter
7 | {
8 | public override bool CanConvert ( Type objectType ) =>
9 | objectType == typeof ( Uri );
10 |
11 | public override object ReadJson ( JsonReader reader,
12 | Type objectType,
13 | object existingValue,
14 | JsonSerializer serializer )
15 | {
16 | if ( reader.TokenType == JsonToken.String )
17 | return new Uri ( (string) reader.Value );
18 |
19 | if ( reader.TokenType == JsonToken.Null )
20 | return null;
21 |
22 | throw new InvalidOperationException (
23 | "Unhandled case for UriConverter. Check to see if this converter has been applied to the wrong serialization type." );
24 | }
25 |
26 | public override void WriteJson ( JsonWriter writer,
27 | object value,
28 | JsonSerializer serializer )
29 | {
30 | if ( null == value )
31 | {
32 | writer.WriteNull ( );
33 | return;
34 | }
35 |
36 | if ( value is Uri uri )
37 | {
38 | writer.WriteValue ( uri.OriginalString );
39 | return;
40 | }
41 |
42 | throw new InvalidOperationException (
43 | "Unhandled case for UriConverter. Check to see if this converter has been applied to the wrong serialization type." );
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Interfaces/IBot.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using CryptoTickerBot.Data.Domain;
6 |
7 | namespace CryptoTickerBot.Core.Interfaces
8 | {
9 | public interface IBot : IDisposable
10 | {
11 | ICryptoExchange this [ CryptoExchangeId index ] { get; }
12 |
13 | CancellationTokenSource Cts { get; }
14 | ImmutableDictionary Exchanges { get; }
15 | bool IsInitialized { get; }
16 | bool IsRunning { get; }
17 | ImmutableHashSet Services { get; }
18 | DateTime StartTime { get; }
19 |
20 | event OnUpdateDelegate Changed;
21 | event OnUpdateDelegate Next;
22 | event TerminateDelegate Terminate;
23 | event StartDelegate Start;
24 |
25 | Task StartAsync ( CancellationTokenSource cts = null );
26 |
27 | Task StartAsync ( CancellationTokenSource cts = null,
28 | params CryptoExchangeId[] exchangeIds );
29 |
30 | Task StopAsync ( );
31 | void RestartExchangeMonitors ( );
32 |
33 | bool ContainsService ( IBotService service );
34 | Task AttachAsync ( IBotService service );
35 | Task DetachAsync ( IBotService service );
36 | Task DetachAllAsync ( ) where T : IBotService;
37 |
38 | bool TryGetExchange ( CryptoExchangeId exchangeId,
39 | out ICryptoExchange exchange );
40 |
41 | bool TryGetExchange ( string exchangeId,
42 | out ICryptoExchange exchange );
43 | }
44 |
45 | public delegate void TerminateDelegate ( Bot bot );
46 |
47 | public delegate Task StartDelegate ( Bot bot );
48 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Exchanges/CoinDeltaExchange.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using CryptoTickerBot.Core.Abstractions;
5 | using CryptoTickerBot.Data.Domain;
6 | using Flurl.Http;
7 | using Newtonsoft.Json;
8 |
9 | namespace CryptoTickerBot.Core.Exchanges
10 | {
11 | public class CoinDeltaExchange : CryptoExchangeBase
12 | {
13 | public CoinDeltaExchange ( ) : base ( CryptoExchangeId.CoinDelta )
14 | {
15 | }
16 |
17 | protected override async Task GetExchangeDataAsync ( CancellationToken ct )
18 | {
19 | while ( !ct.IsCancellationRequested )
20 | {
21 | var data = await TickerUrl.GetJsonAsync> ( ct ).ConfigureAwait ( false );
22 |
23 | foreach ( var datum in data )
24 | {
25 | var market = datum.MarketName.Replace ( "-", "" );
26 | Update ( datum, market.ToUpper ( ) );
27 | }
28 |
29 | await Task.Delay ( PollingRate, ct ).ConfigureAwait ( false );
30 | }
31 | }
32 |
33 | protected override void DeserializeData ( CoinDeltaCoin data,
34 | string id )
35 | {
36 | ExchangeData[id].LowestAsk = data.Ask;
37 | ExchangeData[id].HighestBid = data.Bid;
38 | ExchangeData[id].Rate = data.Last;
39 | }
40 |
41 | public class CoinDeltaCoin
42 | {
43 | [JsonProperty ( "Ask" )]
44 | public decimal Ask { get; set; }
45 |
46 | [JsonProperty ( "Bid" )]
47 | public decimal Bid { get; set; }
48 |
49 | [JsonProperty ( "MarketName" )]
50 | public string MarketName { get; set; }
51 |
52 | [JsonProperty ( "Last" )]
53 | public decimal Last { get; set; }
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Newtonsoft.Json;
4 |
5 | namespace CryptoTickerBot.Data.Extensions
6 | {
7 | public static class StringExtensions
8 | {
9 | public static int CaseInsensitiveHashCode ( this string str ) =>
10 | StringComparer.OrdinalIgnoreCase.GetHashCode ( str );
11 |
12 | public static T ToObject ( this string json ) =>
13 | JsonConvert.DeserializeObject ( json );
14 |
15 | public static T ToObject ( this string json,
16 | params JsonConverter[] converters ) =>
17 | JsonConvert.DeserializeObject ( json, converters );
18 |
19 | public static T ToObject ( this string json,
20 | JsonSerializerSettings settings ) =>
21 | JsonConvert.DeserializeObject ( json, settings );
22 |
23 | public static IEnumerable SplitOnLength ( this string input,
24 | int length )
25 | {
26 | var index = 0;
27 | while ( index < input.Length )
28 | {
29 | if ( index + length < input.Length )
30 | yield return input.Substring ( index, length );
31 | else
32 | yield return input.Substring ( index );
33 |
34 | index += length;
35 | }
36 | }
37 |
38 | public static string ReplaceLastOccurrence ( this string source,
39 | string find,
40 | string replace,
41 | StringComparison comparison = StringComparison.OrdinalIgnoreCase )
42 | {
43 | var place = source.LastIndexOf ( find, comparison );
44 |
45 | return place == -1 ? source : source.Remove ( place, find.Length ).Insert ( place, replace );
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Abstractions/CycleBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Linq;
5 | using CryptoTickerBot.Arbitrage.Interfaces;
6 | using MoreLinq;
7 |
8 | namespace CryptoTickerBot.Arbitrage.Abstractions
9 | {
10 | public abstract class CycleBase : ICycle where TNode : INode
11 | {
12 | public ImmutableList Path { get; }
13 | public ImmutableList Edges { get; }
14 | public int Length => Path.Count - 1;
15 | public double Weight { get; protected set; } = double.PositiveInfinity;
16 |
17 | protected CycleBase ( IEnumerable path )
18 | {
19 | Path = ImmutableList.Empty.AddRange ( path );
20 | Edges = ImmutableList.Empty.AddRange ( Path.Window ( 2 ).Select ( x => x[0][x[1].Symbol] ) );
21 |
22 | if ( Edges.Any ( x => x is null ) )
23 | throw new ArgumentOutOfRangeException ( );
24 | }
25 |
26 | public virtual double UpdateWeight ( ) =>
27 | Weight = Edges.Sum ( x => x.Weight );
28 |
29 | public override string ToString ( ) =>
30 | string.Join ( " -> ", Path.Select ( x => x.Symbol ) );
31 |
32 | #region Equality Comparers
33 |
34 | bool IEquatable>.Equals ( ICycle other ) =>
35 | other != null && other.Path.IsCyclicEquivalent ( Path );
36 |
37 | public override bool Equals ( object obj )
38 | {
39 | if ( obj is null ) return false;
40 | if ( ReferenceEquals ( this, obj ) ) return true;
41 |
42 | return obj is ICycle cycle && Equals ( cycle );
43 | }
44 |
45 | public override int GetHashCode ( )
46 | {
47 | if ( Path is null )
48 | return 0;
49 |
50 | return Path
51 | .Skip ( 1 )
52 | .Aggregate ( 0, ( current,
53 | node ) => current ^ node.GetHashCode ( ) );
54 | }
55 |
56 | #endregion
57 | }
58 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.GoogleSheets/SheetsConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CryptoTickerBot.Data.Configs;
5 |
6 | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
7 |
8 | namespace CryptoTickerBot.GoogleSheets
9 | {
10 | public class SheetsConfig : IConfig
11 | {
12 | public string ConfigFileName { get; } = "Sheets";
13 | public string ConfigFolderName { get; } = "Configs";
14 |
15 | public string SpreadSheetId { get; set; }
16 | public string SheetName { get; set; }
17 | public int SheetId { get; set; }
18 | public string ApplicationName { get; set; }
19 | public TimeSpan UpdateFrequency { get; set; } = TimeSpan.FromSeconds ( 6 );
20 | public TimeSpan CooldownPeriod { get; set; } = TimeSpan.FromSeconds ( 60 );
21 |
22 | public int StartingRow { get; set; } = 5;
23 | public char StartingColumn { get; set; } = 'A';
24 | public int ExchangeRowGap { get; set; } = 3;
25 |
26 | public SheetsConfig RestoreDefaults ( ) =>
27 | new SheetsConfig
28 | {
29 | SpreadSheetId = SpreadSheetId,
30 | SheetName = SheetName,
31 | SheetId = SheetId,
32 | ApplicationName = ApplicationName
33 | };
34 |
35 | public bool TryValidate ( out IList exceptions )
36 | {
37 | exceptions = new List ( );
38 |
39 | if ( string.IsNullOrEmpty ( SpreadSheetId ) )
40 | exceptions.Add ( new ArgumentException ( "SpreadSheet ID missing", nameof ( SpreadSheetId ) ) );
41 | if ( string.IsNullOrEmpty ( SheetName ) )
42 | exceptions.Add ( new ArgumentException ( "SheetName missing", nameof ( SheetName ) ) );
43 | if ( string.IsNullOrEmpty ( ApplicationName ) )
44 | exceptions.Add ( new ArgumentException ( "Application Name missing", nameof ( ApplicationName ) ) );
45 |
46 | return !exceptions.Any ( );
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Menus/Pages/MainPage.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Threading.Tasks;
3 | using CryptoTickerBot.Telegram.Extensions;
4 | using CryptoTickerBot.Telegram.Interfaces;
5 | using CryptoTickerBot.Telegram.Menus.Abstractions;
6 | using MoreLinq;
7 | using Telegram.Bot.Types;
8 |
9 | #pragma warning disable 1998
10 |
11 | namespace CryptoTickerBot.Telegram.Menus.Pages
12 | {
13 | internal class MainPage : PageBase
14 | {
15 | public MainPage ( IMenu menu ) :
16 | base ( "Main Menu", menu )
17 | {
18 | Labels = new[] {"status", "exchange info", "manage subscriptions"}
19 | .Batch ( 2 )
20 | .ToList ( );
21 | AddWideLabel ( "exit" );
22 |
23 | BuildKeyboard ( );
24 | AddHandlers ( );
25 | }
26 |
27 | private void AddHandlers ( )
28 | {
29 | AddHandler ( "status", StatusHandlerAsync );
30 | AddHandler ( "exchange info", ExchangeInfoHandlerAsync );
31 | AddHandler ( "manage subscriptions", ManageSubscriptionsHandlerAsync );
32 | AddHandler ( "exit", ExitHandler, "Cya!" );
33 | }
34 |
35 | private async Task StatusHandlerAsync ( CallbackQuery query )
36 | {
37 | await Menu.SendTextBlockAsync ( TelegramBot.GetStatusString ( ) ).ConfigureAwait ( false );
38 | await RedrawAsync ( ).ConfigureAwait ( false );
39 | }
40 |
41 | private async Task ExchangeInfoHandlerAsync ( CallbackQuery query )
42 | {
43 | var id = await RunExchangeSelectionPageAsync ( ).ConfigureAwait ( false );
44 |
45 | if ( id )
46 | {
47 | Ctb.TryGetExchange ( id, out var exchange );
48 |
49 | await Menu.SendTextBlockAsync ( exchange.GetSummary ( ) )
50 | .ConfigureAwait ( false );
51 | await RedrawAsync ( ).ConfigureAwait ( false );
52 | }
53 | }
54 |
55 | private async Task ManageSubscriptionsHandlerAsync ( CallbackQuery query )
56 | {
57 | await Menu.SwitchPageAsync ( new ManageSubscriptionsPage ( Menu, this ) ).ConfigureAwait ( false );
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Data/Helpers/PriceChange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CryptoTickerBot.Data.Domain;
3 |
4 | namespace CryptoTickerBot.Data.Helpers
5 | {
6 | public struct PriceChange : IComparable, IComparable
7 | {
8 | public decimal Value { get; private set; }
9 | public decimal Percentage { get; private set; }
10 | public TimeSpan TimeDiff { get; private set; }
11 | public DateTime AbsoluteTime { get; private set; }
12 |
13 | public static PriceChange Difference ( CryptoCoin newCoin,
14 | CryptoCoin oldCoin )
15 | => new PriceChange
16 | {
17 | Value = newCoin.Rate - oldCoin.Rate,
18 | Percentage = ( newCoin.Rate - oldCoin.Rate ) /
19 | ( newCoin.Rate + oldCoin.Rate ) *
20 | 2m,
21 | TimeDiff = newCoin.Time - oldCoin.Time,
22 | AbsoluteTime = newCoin.Time
23 | };
24 |
25 | public override string ToString ( ) => $"{Value:N} {Percentage:P}";
26 |
27 | public int CompareTo ( PriceChange other ) => Value.CompareTo ( other.Value );
28 |
29 | public int CompareTo ( object obj )
30 | {
31 | if ( obj is null ) return 1;
32 | return obj is PriceChange other
33 | ? CompareTo ( other )
34 | : throw new ArgumentException ( $"Object must be of type {nameof ( PriceChange )}" );
35 | }
36 |
37 | public static bool operator < ( PriceChange left,
38 | PriceChange right ) =>
39 | left.CompareTo ( right ) < 0;
40 |
41 | public static bool operator > ( PriceChange left,
42 | PriceChange right ) =>
43 | left.CompareTo ( right ) > 0;
44 |
45 | public static bool operator <= ( PriceChange left,
46 | PriceChange right ) =>
47 | left.CompareTo ( right ) <= 0;
48 |
49 | public static bool operator >= ( PriceChange left,
50 | PriceChange right ) =>
51 | left.CompareTo ( right ) >= 0;
52 | }
53 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/Persistent/PersistentDictionary.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CryptoTickerBot.Collections.Persistent.Base;
4 |
5 | namespace CryptoTickerBot.Collections.Persistent
6 | {
7 | public sealed class PersistentDictionary :
8 | PersistentCollection, Dictionary>,
9 | IDictionary
10 | {
11 | public TValue this [ TKey key ]
12 | {
13 | get => Collection[key];
14 | set
15 | {
16 | Collection[key] = value;
17 | Save ( );
18 | }
19 | }
20 |
21 | public ICollection Keys => Collection.Keys;
22 |
23 | public ICollection Values => Collection.Values;
24 |
25 | private PersistentDictionary ( string fileName,
26 | TimeSpan flushInterval )
27 | : base ( fileName, DefaultSerializerSettings, flushInterval )
28 | {
29 | }
30 |
31 | public void Add ( TKey key,
32 | TValue value )
33 | {
34 | Collection.Add ( key, value );
35 | Save ( );
36 | }
37 |
38 | public bool ContainsKey ( TKey key ) => Collection.ContainsKey ( key );
39 |
40 | public bool Remove ( TKey key )
41 | {
42 | var result = Collection.Remove ( key );
43 | Save ( );
44 |
45 | return result;
46 | }
47 |
48 | public bool TryGetValue ( TKey key,
49 | out TValue value ) =>
50 | Collection.TryGetValue ( key, out value );
51 |
52 | public static PersistentDictionary Build ( string fileName ) =>
53 | Build ( fileName, DefaultFlushInterval );
54 |
55 | public static PersistentDictionary Build ( string fileName,
56 | TimeSpan flushInterval )
57 | {
58 | var collection = GetOpenCollection> ( fileName );
59 |
60 | return collection ?? new PersistentDictionary ( fileName, flushInterval );
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Telegram/Menus/TelegramMenuManager.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CryptoTickerBot.Telegram.Interfaces;
5 | using Telegram.Bot.Types;
6 |
7 | namespace CryptoTickerBot.Telegram.Menus
8 | {
9 | internal class TelegramMenuManager
10 | {
11 | protected ConcurrentDictionary> Menus { get; }
12 |
13 | public IMenu this [ User user,
14 | long chatId ]
15 | {
16 | get
17 | {
18 | if ( Menus.TryGetValue ( user, out var userMenus ) )
19 | if ( userMenus.TryGetValue ( chatId, out var menu ) )
20 | return menu;
21 |
22 | return null;
23 | }
24 | }
25 |
26 | public IMenu this [ User user,
27 | Chat chat ] =>
28 | this[user, chat.Id];
29 |
30 | public IMenu this [ CallbackQuery query ] =>
31 | this[query.From, query.Message.Chat.Id];
32 |
33 | public IList this [ long chatId ] =>
34 | Menus.Values.Where ( x => x.ContainsKey ( chatId ) ).SelectMany ( x => x.Values ).ToList ( );
35 |
36 | public IList this [ Chat chat ] => this[chat.Id];
37 |
38 | public TelegramMenuManager ( )
39 | {
40 | Menus = new ConcurrentDictionary> ( );
41 | }
42 |
43 | public bool TryGetMenu ( User user,
44 | long chatId,
45 | out IMenu menu )
46 | {
47 | menu = null;
48 | return Menus.TryGetValue ( user, out var menus ) && menus.TryGetValue ( chatId, out menu );
49 | }
50 |
51 | public bool Remove ( User user,
52 | long chatId ) =>
53 | Menus.TryGetValue ( user, out var menus ) && menus.TryRemove ( chatId, out _ );
54 |
55 | public void AddOrUpdateMenu ( IMenu menu )
56 | {
57 | if ( Menus.TryGetValue ( menu.User, out var userMenus ) )
58 | userMenus[menu.Chat.Id] = menu;
59 | else
60 | Menus[menu.User] = new ConcurrentDictionary
61 | {
62 | [menu.Chat.Id] = menu
63 | };
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Abstractions/BotServiceBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using CryptoTickerBot.Core.Interfaces;
4 | using CryptoTickerBot.Data.Domain;
5 |
6 | namespace CryptoTickerBot.Core.Abstractions
7 | {
8 | public abstract class BotServiceBase : IBotService
9 | {
10 | public Guid Guid { get; } = Guid.NewGuid ( );
11 | public IBot Bot { get; protected set; }
12 | public bool IsAttached { get; protected set; }
13 |
14 | public virtual async Task AttachToAsync ( IBot bot )
15 | {
16 | if ( !bot.ContainsService ( this ) )
17 | {
18 | await bot.AttachAsync ( this ).ConfigureAwait ( false );
19 | return;
20 | }
21 |
22 | Bot = bot;
23 | IsAttached = true;
24 |
25 | await StartAsync ( ).ConfigureAwait ( false );
26 | }
27 |
28 | public virtual async Task DetachAsync ( )
29 | {
30 | if ( Bot.ContainsService ( this ) )
31 | {
32 | await Bot.DetachAsync ( this ).ConfigureAwait ( false );
33 | return;
34 | }
35 |
36 | IsAttached = false;
37 |
38 | await StopAsync ( ).ConfigureAwait ( false );
39 | }
40 |
41 | public virtual Task OnNextAsync ( ICryptoExchange exchange,
42 | CryptoCoin coin ) =>
43 | Task.CompletedTask;
44 |
45 | public virtual Task OnChangedAsync ( ICryptoExchange exchange,
46 | CryptoCoin coin ) =>
47 | Task.CompletedTask;
48 |
49 | public virtual void Dispose ( )
50 | {
51 | }
52 |
53 | public bool Equals ( IBotService other ) =>
54 | Guid.Equals ( other?.Guid );
55 |
56 | public virtual Task StartAsync ( ) =>
57 | Task.CompletedTask;
58 |
59 | public virtual Task StopAsync ( ) =>
60 | Task.CompletedTask;
61 |
62 | public override bool Equals ( object obj )
63 | {
64 | if ( obj is null )
65 | return false;
66 | if ( ReferenceEquals ( this, obj ) )
67 | return true;
68 | if ( obj is IBotService service )
69 | return Equals ( service );
70 | return false;
71 | }
72 |
73 | public override int GetHashCode ( ) => Guid.GetHashCode ( );
74 | }
75 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Core/Interfaces/ICryptoExchange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Diagnostics.Contracts;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using CryptoTickerBot.Data.Domain;
8 |
9 | // ReSharper disable UnusedMemberInSuper.Global
10 |
11 | namespace CryptoTickerBot.Core.Interfaces
12 | {
13 | public delegate Task OnUpdateDelegate ( ICryptoExchange exchange,
14 | CryptoCoin coin );
15 |
16 | public interface ICryptoExchange : IObservable, IDisposable
17 | {
18 | CryptoExchangeId Id { get; }
19 | string Name { get; }
20 | string Url { get; }
21 | string TickerUrl { get; }
22 | Dictionary SymbolMappings { get; }
23 | decimal BuyFees { get; }
24 | decimal SellFees { get; }
25 | TimeSpan PollingRate { get; }
26 | TimeSpan CooldownPeriod { get; }
27 | bool IsStarted { get; }
28 | DateTime StartTime { get; }
29 | TimeSpan UpTime { get; }
30 | DateTime LastUpdate { get; }
31 | TimeSpan LastUpdateDuration { get; }
32 | DateTime LastChange { get; }
33 | TimeSpan LastChangeDuration { get; }
34 | int Count { get; }
35 | ImmutableHashSet BaseSymbols { get; }
36 | Markets Markets { get; }
37 | IDictionary ExchangeData { get; }
38 | IDictionary DepositFees { get; }
39 | IDictionary WithdrawalFees { get; }
40 | ImmutableHashSet> Observers { get; }
41 |
42 | [Pure]
43 | CryptoCoin this [ string symbol ] { get; set; }
44 |
45 | [Pure]
46 | CryptoCoin this [ string baseSymbol,
47 | string symbol ] { get; }
48 |
49 | event OnUpdateDelegate Changed;
50 | event OnUpdateDelegate Next;
51 |
52 | Task StartReceivingAsync ( CancellationToken? ct = null );
53 | Task StopReceivingAsync ( );
54 |
55 | void Unsubscribe ( IObserver subscription );
56 |
57 | [Pure]
58 | CryptoCoin GetWithFees ( string symbol );
59 |
60 | [Pure]
61 | string ToTable ( params string[] symbols );
62 | }
63 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using CryptoTickerBot.Arbitrage.Common;
4 | using CryptoTickerBot.Arbitrage.Interfaces;
5 | using MoreLinq;
6 |
7 | namespace CryptoTickerBot.Arbitrage
8 | {
9 | public static class Extensions
10 | {
11 | public static bool IsCyclicEquivalent ( this IReadOnlyList cycle1,
12 | IReadOnlyList cycle2 )
13 | {
14 | return cycle1
15 | .Skip ( 1 )
16 | .Concat ( cycle1.Skip ( 1 ) )
17 | .Window ( cycle2.Count - 1 )
18 | .Any ( x => x.SequenceEqual ( cycle2.Skip ( 1 ) ) );
19 | }
20 |
21 | public static bool IsCyclicEquivalent ( this ICycle cycle1,
22 | ICycle cycle2 ) where TNode : INode =>
23 | IsCyclicEquivalent ( cycle1.Path, cycle2.Path );
24 |
25 | public static IEnumerable> GetTriangularCycles ( this TNode from,
26 | TNode to )
27 | where TNode : INode =>
28 | GetTriangularCycles ( ( from, to ) );
29 |
30 | public static IEnumerable> GetTriangularCycles ( this (TNode, TNode) pair )
31 | where TNode : INode
32 | {
33 | var (to, from) = pair;
34 |
35 | if ( !to.HasEdge ( from.Symbol ) )
36 | yield break;
37 |
38 | var nodes = from.Edges
39 | .Where ( x => x.To.EdgeTable.ContainsKey ( to.Symbol ) )
40 | .Select ( x => x.To )
41 | .OfType ( );
42 |
43 | foreach ( var node in nodes )
44 | yield return new Cycle ( from, node, to, from );
45 | }
46 |
47 |
48 | public static List> GetAllTriangularCycles ( this IGraph graph )
49 | where TNode : INode
50 | {
51 | var cycles = new List> ( );
52 |
53 | var pairs = graph.Nodes.Values
54 | .Subsets ( 2 )
55 | .SelectMany ( x => new[] {( x[0], x[1] ), ( x[1], x[0] )} )
56 | .Where ( x => x.Item2.HasEdge ( x.Item1.Symbol ) )
57 | .ToList ( );
58 |
59 | foreach ( var pair in pairs )
60 | cycles.AddRange ( GetTriangularCycles ( pair ) );
61 |
62 | return cycles.Distinct ( ).ToList ( );
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Abstractions/GraphBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using CryptoTickerBot.Arbitrage.Interfaces;
3 |
4 | namespace CryptoTickerBot.Arbitrage.Abstractions
5 | {
6 | public abstract class GraphBase : IGraph where TNode : class, INode
7 | {
8 | public ImmutableDictionary Nodes { get; protected set; } =
9 | ImmutableDictionary.Empty;
10 |
11 | public abstract EdgeBuilderDelegate DefaultEdgeBuilder { get; protected set; }
12 |
13 | public TNode this [ string symbol ] =>
14 | Nodes.TryGetValue ( symbol, out var value ) ? value : null;
15 |
16 | public NodeBuilderDelegate NodeBuilder { get; }
17 |
18 | protected GraphBase ( NodeBuilderDelegate nodeBuilder )
19 | {
20 | NodeBuilder = nodeBuilder;
21 | }
22 |
23 | public bool ContainsNode ( string symbol ) =>
24 | Nodes.ContainsKey ( symbol );
25 |
26 | public virtual TNode AddNode ( string symbol )
27 | {
28 | if ( Nodes.TryGetValue ( symbol, out var node ) )
29 | return node;
30 |
31 | node = NodeBuilder ( symbol );
32 | Nodes = Nodes.SetItem ( symbol, node );
33 |
34 | return node;
35 | }
36 |
37 | public virtual IEdge UpsertEdge ( string from,
38 | string to,
39 | decimal cost ) =>
40 | UpsertEdge ( from, to, cost, DefaultEdgeBuilder );
41 |
42 | public virtual TEdge UpsertEdge ( string from,
43 | string to,
44 | decimal cost,
45 | EdgeBuilderDelegate edgeBuilder )
46 | where TEdge : class, IEdge
47 | {
48 | var nodeFrom = AddNode ( from );
49 | var nodeTo = AddNode ( to );
50 |
51 | var edge = edgeBuilder ( nodeFrom, nodeTo, cost );
52 | if ( nodeFrom.AddOrUpdateEdge ( edge ) )
53 | OnEdgeInsert ( nodeFrom, nodeTo );
54 | else
55 | OnEdgeUpdate ( nodeFrom, nodeTo );
56 |
57 | return nodeFrom[to] as TEdge;
58 | }
59 |
60 | protected abstract void OnEdgeInsert ( TNode from,
61 | TNode to );
62 |
63 | protected abstract void OnEdgeUpdate ( TNode from,
64 | TNode to );
65 | }
66 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Collections/Persistent/PersistentSet.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CryptoTickerBot.Collections.Persistent.Base;
4 |
5 | namespace CryptoTickerBot.Collections.Persistent
6 | {
7 | public sealed class PersistentSet : PersistentCollection>, ISet
8 | {
9 | private PersistentSet ( string fileName,
10 | TimeSpan flushInterval )
11 | : base ( fileName, DefaultSerializerSettings, flushInterval )
12 | {
13 | }
14 |
15 | bool ISet.Add ( T item )
16 | {
17 | var result = Collection.Add ( item );
18 | Save ( );
19 |
20 | return result;
21 | }
22 |
23 | public void ExceptWith ( IEnumerable other )
24 | {
25 | Collection.ExceptWith ( other );
26 | }
27 |
28 | public void IntersectWith ( IEnumerable other )
29 | {
30 | Collection.IntersectWith ( other );
31 | }
32 |
33 | public bool IsProperSubsetOf ( IEnumerable other ) => Collection.IsProperSubsetOf ( other );
34 |
35 | public bool IsProperSupersetOf ( IEnumerable other ) => Collection.IsProperSupersetOf ( other );
36 |
37 | public bool IsSubsetOf ( IEnumerable other ) => Collection.IsSubsetOf ( other );
38 |
39 | public bool IsSupersetOf ( IEnumerable other ) => Collection.IsSupersetOf ( other );
40 |
41 | public bool Overlaps ( IEnumerable other ) => Collection.Overlaps ( other );
42 |
43 | public bool SetEquals ( IEnumerable other ) => Collection.SetEquals ( other );
44 |
45 | public void SymmetricExceptWith ( IEnumerable other )
46 | {
47 | Collection.SymmetricExceptWith ( other );
48 | }
49 |
50 | public void UnionWith ( IEnumerable other )
51 | {
52 | Collection.UnionWith ( other );
53 | }
54 |
55 | public static PersistentSet Build ( string fileName ) =>
56 | Build ( fileName, DefaultFlushInterval );
57 |
58 | public static PersistentSet Build ( string fileName,
59 | TimeSpan flushInterval )
60 | {
61 | var collection = GetOpenCollection> ( fileName );
62 |
63 | return collection ?? new PersistentSet ( fileName, flushInterval );
64 | }
65 |
66 | public bool AddOrUpdate ( T item )
67 | {
68 | var result = Collection.Remove ( item );
69 | Collection.Add ( item );
70 | Save ( );
71 |
72 | return !result;
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.Arbitrage/Common/CycleMap.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using CryptoTickerBot.Arbitrage.Interfaces;
5 |
6 | namespace CryptoTickerBot.Arbitrage.Common
7 | {
8 | using static ImmutableHashSet;
9 |
10 | public class CycleMap where TNode : INode
11 | {
12 | public ImmutableHashSet> this [ string from,
13 | string to ]
14 | {
15 | get
16 | {
17 | if ( !data.TryGetValue ( from, out var dict ) )
18 | return null;
19 | return dict.TryGetValue ( to, out var set ) ? set : null;
20 | }
21 | }
22 |
23 | public ImmutableHashSet> this [ INode from,
24 | INode to ] =>
25 | this[from.Symbol, to.Symbol];
26 |
27 | private readonly ConcurrentDictionary>>> data
30 | =
31 | new ConcurrentDictionary>>> ( );
34 |
35 | public bool AddCycle ( string from,
36 | string to,
37 | ICycle cycle )
38 | {
39 | if ( !data.TryGetValue ( from, out var dict ) )
40 | {
41 | data[from] = new ConcurrentDictionary>>
42 | {[to] = ImmutableHashSet>.Empty.Add ( cycle )};
43 | return true;
44 | }
45 |
46 | if ( dict.TryGetValue ( to, out var storedCycles ) )
47 | {
48 | var builder = storedCycles.ToBuilder ( );
49 | var result = builder.Add ( cycle );
50 | dict[to] = builder.ToImmutable ( );
51 | return result;
52 | }
53 |
54 | dict[to] = ImmutableHashSet>.Empty.Add ( cycle );
55 | return true;
56 | }
57 |
58 | public void AddCycles ( string from,
59 | string to,
60 | IEnumerable> cycles )
61 | {
62 | if ( !data.TryGetValue ( from, out var dict ) )
63 | {
64 | data[from] = new ConcurrentDictionary>>
65 | {[to] = cycles.ToImmutableHashSet ( )};
66 | return;
67 | }
68 |
69 | if ( dict.TryGetValue ( to, out var storedCycles ) )
70 | {
71 | var builder = storedCycles.ToBuilder ( );
72 | builder.UnionWith ( cycles );
73 | dict[to] = builder.ToImmutable ( );
74 | return;
75 | }
76 |
77 | dict[to] = cycles.ToImmutableHashSet ( );
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/CryptoTickerBot.GoogleSheets/Utility.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading;
6 | using CryptoTickerBot.Core.Interfaces;
7 | using CryptoTickerBot.Data.Domain;
8 | using Google.Apis.Auth.OAuth2;
9 | using Google.Apis.Sheets.v4;
10 | using Google.Apis.Util.Store;
11 | using NLog;
12 |
13 | namespace CryptoTickerBot.GoogleSheets
14 | {
15 | internal static class Utility
16 | {
17 | private static readonly Logger Logger = LogManager.GetCurrentClassLogger ( );
18 |
19 | public static UserCredential GetCredentials ( string clientSecretPath,
20 | string credentialsPath )
21 | {
22 | using ( var stream =
23 | new FileStream ( clientSecretPath, FileMode.Open, FileAccess.Read ) )
24 | {
25 | var fullPath = Path.GetFullPath ( credentialsPath );
26 |
27 | var credential = GoogleWebAuthorizationBroker.AuthorizeAsync (
28 | GoogleClientSecrets.Load ( stream ).Secrets,
29 | new[] {SheetsService.Scope.Spreadsheets},
30 | "user",
31 | CancellationToken.None,
32 | new FileDataStore ( fullPath, true ) ).Result;
33 | Logger.Info ( "Credential file saved to: " + fullPath );
34 |
35 | return credential;
36 | }
37 | }
38 |
39 | public static IList> ToSheetsRows ( this ICryptoExchange exchange )
40 | {
41 | return ToSheetsRows ( exchange,
42 | coin => new object[]
43 | {
44 | coin.LowestAsk,
45 | coin.HighestBid,
46 | coin.Rate,
47 | $"{coin.Time:G}",
48 | coin.Spread,
49 | coin.SpreadPercentage
50 | } );
51 | }
52 |
53 | public static IList> ToSheetsRows ( this ICryptoExchange exchange,
54 | Func> selector )
55 | {
56 | return exchange
57 | .Markets
58 | .BaseSymbols
59 | .OrderBy ( x => x )
60 | .SelectMany ( x => exchange
61 | .Markets
62 | .Data[x]
63 | .OrderBy ( y => y.Key )
64 | .Select ( y => (IList