├── .gitattributes ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── media │ └── ethusdt_orderbook.gif └── src ├── BinanceBot.Market ├── Abstracts │ ├── BaseMarketBot.cs │ ├── IMarketBot.cs │ ├── IMarketDepthPublisher.cs │ └── IMarketStrategy.cs ├── BinanceBot.Market.csproj ├── Configurations │ └── MarketStrategyConfiguration.cs ├── Core │ ├── MarketDepth.cs │ ├── MarketDepthPair.cs │ └── Quote.cs ├── CreateOrderRequest.cs ├── MarketDepthManager.cs ├── MarketMakerBot.cs ├── Strategies │ └── NaiveMarketMakerStrategy.cs └── Utility │ ├── DescDecimalComparer.cs │ └── QuoteExtensions.cs ├── BinanceBot.MarketBot.Console ├── BinanceBot.MarketBot.Console.csproj ├── NLog.config └── Program.cs ├── BinanceBot.MarketViewer.Console ├── BinanceBot.MarketViewer.Console.csproj ├── NLog.config └── Program.cs ├── BinanceBot.sln └── BinanceBot.sln.DotSettings /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute? 2 | 3 | *Welcome on board!* 4 | 5 | If U have useful stuff related to this project's Objectives, feel free to create a branch(es) from `main`. Then just add content and [Pull request](https://github.com/codez0mb1e/BinanceBot/pulls) changes. 6 | 7 | Thanks in advance :tada:! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dmitry Petukhov. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Market Bot for Binance 2 | 3 | [![Status](https://img.shields.io/badge/status-in_active_development-green.svg)](https://github.com/codez0mb1e/BinanceBot/projects/1) 4 | [![Contributors Welcome](https://img.shields.io/badge/contributing-welcome-blue.svg)](CONTRIBUTING.md) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 6 | 7 | Naive _Market Maker Bot_ for Binance exchange. 8 | 9 | Solution contains two console projects: 10 | 11 | - The `BinanceBot.MarketViewer.Console` project: __Order book updating in near-real time__ (via _Binance WebSocket API_). 12 | - The `BinanceBot.MarketBot.Console` project: __Create and cancel orders__ (via _Binance REST API_) depends on current Market Depth. 13 | 14 | ![alt text](/docs/media/ethusdt_orderbook.gif) 15 | 16 | In picture below _BinanceBot create order to Order Book only if price spread by ETH/BTC greater than 0.2%_. 17 | 18 | __Warn:__ BinanceBot uses _test order create_ API by default (without real order creation). 19 | Turn off `TEST_ORDER_CREATION_MODE` compilation symbol in [MarketMakerBot.cs](src/BinanceBot.Market/MarketMakerBot.cs) to _create real order_ in order book. 20 | 21 | ## Requirements 22 | 23 | - .NET 7.0 24 | - Binance Account. 25 | 26 | ## References 27 | 28 | 1. [Binance official API docs](https://github.com/binance-exchange/binance-official-api-docs). 29 | 1. [Official C# Wrapper for the Binance exchange API](https://github.com/glitch100/BinanceDotNet). 30 | -------------------------------------------------------------------------------- /docs/media/ethusdt_orderbook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez0mb1e/BinanceBot/be94d43e43bd0ad47586566f17216ea7e90451d5/docs/media/ethusdt_orderbook.gif -------------------------------------------------------------------------------- /src/BinanceBot.Market/Abstracts/BaseMarketBot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Binance.Net.Objects.Models.Spot; 5 | using NLog; 6 | 7 | namespace BinanceBot.Market 8 | { 9 | /// 10 | /// Base Market Bot 11 | /// 12 | /// 13 | public abstract class BaseMarketBot : 14 | IMarketBot, IDisposable 15 | where TStrategy : class, IMarketStrategy 16 | { 17 | protected readonly Logger Logger; 18 | 19 | protected readonly TStrategy MarketStrategy; 20 | 21 | 22 | protected BaseMarketBot(string symbol, TStrategy marketStrategy, Logger logger) 23 | { 24 | Symbol = symbol ?? throw new ArgumentNullException(nameof(symbol)); 25 | MarketStrategy = marketStrategy ?? throw new ArgumentNullException(nameof(marketStrategy)); 26 | Logger = logger ?? LogManager.GetCurrentClassLogger(); 27 | } 28 | 29 | 30 | public string Symbol { get; } 31 | 32 | 33 | public abstract Task RunAsync(); 34 | 35 | public abstract void Stop(); 36 | 37 | public abstract Task ValidateServerTimeAsync(); 38 | 39 | public abstract Task> GetOpenedOrdersAsync(string symbol); 40 | 41 | public abstract Task CancelOrdersAsync(IEnumerable orders); 42 | 43 | public abstract Task CreateOrderAsync(CreateOrderRequest order); 44 | 45 | 46 | public abstract void Dispose(); 47 | } 48 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Abstracts/IMarketBot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Binance.Net.Objects.Models.Spot; 4 | 5 | 6 | namespace BinanceBot.Market 7 | { 8 | /// 9 | /// Market Bot Interface 10 | /// 11 | public interface IMarketBot 12 | { 13 | /// 14 | /// Symbol 15 | /// 16 | string Symbol { get; } 17 | 18 | 19 | /// 20 | /// Run bot 21 | /// 22 | Task RunAsync(); 23 | 24 | /// 25 | /// Stop bot 26 | /// 27 | void Stop(); 28 | 29 | 30 | /// 31 | /// Validate connection w/ stock 32 | /// 33 | Task ValidateServerTimeAsync(); 34 | 35 | /// 36 | /// Get currently opened orders 37 | /// 38 | /// 39 | Task> GetOpenedOrdersAsync(string symbol); 40 | 41 | /// 42 | /// Create new order 43 | /// 44 | /// 45 | Task CreateOrderAsync(CreateOrderRequest order); 46 | 47 | /// 48 | /// Cancel orders 49 | /// 50 | /// 51 | Task CancelOrdersAsync(IEnumerable orders); 52 | } 53 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Abstracts/IMarketDepthPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using BinanceBot.Market.Core; 4 | 5 | namespace BinanceBot.Market 6 | { 7 | /// 8 | /// Publisher of events 9 | /// 10 | public interface IMarketDepthPublisher 11 | { 12 | /// 13 | /// Order book was changed 14 | /// 15 | event EventHandler MarketDepthChanged; 16 | 17 | /// 18 | /// Best was changed 19 | /// 20 | event EventHandler MarketBestPairChanged; 21 | } 22 | 23 | 24 | 25 | /// 26 | /// Order book changed event args 27 | /// 28 | public sealed class MarketBestPairChangedEventArgs : EventArgs 29 | { 30 | public MarketBestPairChangedEventArgs(MarketDepthPair marketBestPair) 31 | { 32 | MarketBestPair = marketBestPair ?? throw new ArgumentNullException(nameof(marketBestPair)); 33 | } 34 | 35 | public MarketDepthPair MarketBestPair { get; } 36 | } 37 | 38 | 39 | /// 40 | /// Best changed event args 41 | /// 42 | public sealed class MarketDepthChangedEventArgs : EventArgs 43 | { 44 | public MarketDepthChangedEventArgs(IEnumerable asks, IEnumerable bids, long updateTime) 45 | { 46 | if (updateTime <= 0) throw new ArgumentOutOfRangeException(nameof(updateTime)); 47 | 48 | Asks = asks; 49 | Bids = bids; 50 | UpdateTime = updateTime; 51 | } 52 | 53 | 54 | public IEnumerable Asks { get; } 55 | 56 | public IEnumerable Bids { get; } 57 | 58 | public long UpdateTime { get; } 59 | } 60 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Abstracts/IMarketStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace BinanceBot.Market 2 | { 3 | 4 | /// 5 | /// MarketStrategy interface 6 | /// 7 | /// 8 | /// As simple as possible now 9 | /// 10 | public interface IMarketStrategy { } 11 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/BinanceBot.Market.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/BinanceBot.Market/Configurations/MarketStrategyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BinanceBot.Market.Configurations 4 | { 5 | /// 6 | /// Market Strategy configuration 7 | /// 8 | /// 9 | /// limits must not contradict Stock limits. 10 | /// Binance limits: 11 | /// 12 | public record MarketStrategyConfiguration 13 | { 14 | /// 15 | /// Start trading when spread greater than that values (in percentage point) 16 | /// 17 | public decimal TradeWhenSpreadGreaterThan { get; set; } 18 | 19 | 20 | #region Order limits 21 | /// 22 | /// Minimal order volume 23 | /// 24 | public decimal MinOrderVolume { get; set; } 25 | 26 | /// 27 | /// Maximum order volume 28 | /// 29 | public decimal MaxOrderVolume { get; set; } 30 | 31 | 32 | /// 33 | /// Precision of the base asset 34 | /// 35 | public int BaseAssetPrecision { get; set; } 36 | 37 | /// 38 | /// Price precision 39 | /// 40 | public int PricePrecision { get; set; } 41 | #endregion 42 | 43 | 44 | #region Day limits (not usage now, but usefull in future) 45 | /// 46 | /// Minimal order volume 47 | /// 48 | public decimal MinVolumePerDay { get; set; } 49 | 50 | /// 51 | /// Maximum order volume 52 | /// 53 | public decimal MaxVolumePerDay { get; set; } 54 | #endregion 55 | 56 | 57 | #region Behaviour settings (not usage now, but usefull in future) 58 | public bool CancelOrdersWhenStopping { get; set; } = true; 59 | 60 | public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5); 61 | 62 | public (TimeSpan From, TimeSpan To) WorkingTime { get; set; } 63 | #endregion 64 | } 65 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Core/MarketDepth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Binance.Net.Enums; 5 | using Binance.Net.Objects.Models; 6 | using BinanceBot.Market.Utility; 7 | 8 | namespace BinanceBot.Market.Core 9 | { 10 | /// 11 | /// Order book 12 | /// 13 | public class MarketDepth : IMarketDepthPublisher 14 | { 15 | public MarketDepth(string symbol) 16 | { 17 | if (string.IsNullOrEmpty(symbol)) 18 | throw new ArgumentException("Invalid symbol value", nameof(symbol)); 19 | 20 | Symbol = symbol; 21 | } 22 | 23 | 24 | public string Symbol { get; } 25 | 26 | 27 | #region Ask section 28 | private readonly IDictionary _asks = new SortedDictionary(); 29 | 30 | /// 31 | /// Get prices that a seller is willing to receive a symbol. 32 | /// Asks sorted by ascending price. The first (best) ask will be the minimum price. 33 | /// 34 | public IEnumerable Asks => _asks.ToQuotes(OrderSide.Sell); 35 | 36 | /// 37 | /// The best ask. If the order book does not contain asks, will be returned . 38 | /// 39 | public Quote BestAsk => Asks.FirstOrDefault(); 40 | #endregion 41 | 42 | 43 | #region Bid section 44 | private readonly IDictionary _bids = new SortedDictionary(new DescendingDecimalComparer()); 45 | 46 | /// 47 | /// Get prices that a buyer is willing to pay for a symbol. 48 | /// Bids sorted by descending price. The first (best) bid will be the maximum price. 49 | /// 50 | public IEnumerable Bids => _bids.ToQuotes(OrderSide.Buy); 51 | 52 | /// 53 | /// The best bid. If the order book does not contain bids, will be returned . 54 | /// 55 | public Quote BestBid => Bids.FirstOrDefault(); 56 | #endregion 57 | 58 | 59 | /// 60 | /// The best pair. If the order book is empty, will be returned . 61 | /// 62 | public MarketDepthPair BestPair => LastUpdateTime.HasValue 63 | ? new MarketDepthPair(BestAsk, BestBid, LastUpdateTime.Value) 64 | : null; 65 | 66 | /// 67 | /// Last update of market depth 68 | /// 69 | public long? LastUpdateTime { get; private set; } 70 | 71 | 72 | 73 | #region Update depth section 74 | private const decimal IgnoreVolumeValue = 1e-11M; 75 | 76 | /// 77 | /// Update market depth 78 | /// 79 | /// 80 | /// How to manage a local order book correctly [1]: 81 | /// 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth 82 | /// 2. Buffer the events you receive from the stream 83 | /// 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 84 | /// -> 4. Drop any event where u is less or equal lastUpdateId in the snapshot 85 | /// 5. The first processed should have U less or equal lastUpdateId+1 AND u equal or greater lastUpdateId+1 86 | /// -> 6. While listening to the stream, each new event's U should be equal to the previous event's u+1 87 | /// -> 7. The data in each event is the absolute quantity for a price level 88 | /// -> 8. If the quantity is 0, remove the price level 89 | /// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal. 90 | /// Reference: 91 | /// 1. https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#how-to-manage-a-local-order-book-correctly 92 | /// 93 | public void UpdateDepth(IEnumerable asks, IEnumerable bids, long updateTime) 94 | { 95 | if (updateTime <= 0) 96 | throw new ArgumentOutOfRangeException(nameof(updateTime)); 97 | 98 | // if nothing was changed then return 99 | if (updateTime <= LastUpdateTime) return; 100 | if (asks == null && bids == null) return; 101 | 102 | 103 | static void UpdateOrderBook(IEnumerable updates, IDictionary orders) 104 | { 105 | if (orders == null) throw new ArgumentNullException(nameof(orders)); 106 | if (updates == null) return; 107 | 108 | // WARN: clean orders in cases when connector received orderbook snapshots instead of orderbook updates 109 | // orders.Clear(); 110 | 111 | // update order book 112 | foreach (BinanceOrderBookEntry t in updates) 113 | { 114 | if (t.Quantity > IgnoreVolumeValue) 115 | orders[t.Price] = t.Quantity; 116 | else 117 | if (orders.ContainsKey(t.Price)) orders.Remove(t.Price); 118 | } 119 | } 120 | 121 | // save prev BestPair to OnMarketBestPairChanged raise event 122 | MarketDepthPair prevBestPair = BestPair; 123 | // update asks market depth 124 | UpdateOrderBook(asks, _asks); 125 | UpdateOrderBook(bids, _bids); 126 | // set new update time 127 | LastUpdateTime = updateTime; 128 | 129 | // raise events 130 | OnMarketDepthChanged(new MarketDepthChangedEventArgs(Asks, Bids, LastUpdateTime.Value)); 131 | if(!BestPair.Equals(prevBestPair)) 132 | OnMarketBestPairChanged(new MarketBestPairChangedEventArgs(BestPair)); 133 | } 134 | #endregion 135 | 136 | 137 | #region Market Depth events 138 | public event EventHandler MarketDepthChanged; 139 | 140 | protected virtual void OnMarketDepthChanged(MarketDepthChangedEventArgs e) 141 | { 142 | EventHandler handler = MarketDepthChanged; 143 | handler?.Invoke(this, e); 144 | } 145 | 146 | 147 | public event EventHandler MarketBestPairChanged; 148 | 149 | protected virtual void OnMarketBestPairChanged(MarketBestPairChangedEventArgs e) 150 | { 151 | EventHandler handler = MarketBestPairChanged; 152 | handler?.Invoke(this, e); 153 | } 154 | #endregion 155 | } 156 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Core/MarketDepthPair.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BinanceBot.Market.Core 4 | { 5 | /// 6 | /// Order book's ask-bid pair 7 | /// 8 | public record MarketDepthPair 9 | { 10 | public MarketDepthPair(Quote ask, Quote bid, long updateTime) 11 | { 12 | if (ask == null) 13 | throw new ArgumentNullException(nameof(ask)); 14 | if (bid == null) 15 | throw new ArgumentNullException(nameof(bid)); 16 | if (ask.Price < bid.Price) 17 | throw new ArgumentNullException(nameof(bid), "Best sell price (ask) cannot be less the best buy price (bid)"); 18 | if (updateTime <= 0) 19 | throw new ArgumentOutOfRangeException(nameof(updateTime)); 20 | 21 | Ask = ask; 22 | Bid = bid; 23 | UpdateTime = updateTime; 24 | } 25 | 26 | 27 | public Quote Ask { get; } 28 | 29 | public Quote Bid { get; } 30 | 31 | public long UpdateTime { get; } 32 | 33 | /// 34 | /// Flag that has orders on 2 sides 35 | /// 36 | public bool IsFull => Ask != null && Bid != null; 37 | 38 | public decimal? PriceSpread => IsFull ? Ask.Price - Bid.Price : default; 39 | 40 | public decimal? VolumeSpread => IsFull ? Math.Abs(Ask.Volume - Bid.Volume) : default; 41 | 42 | public decimal? MediumPrice => IsFull ? (Ask.Price + Bid.Price)/2 : default; 43 | } 44 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Core/Quote.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Binance.Net.Enums; 3 | 4 | namespace BinanceBot.Market.Core 5 | { 6 | /// 7 | /// quote representing bid or ask 8 | /// 9 | public record Quote 10 | { 11 | public Quote(decimal price, decimal volume, OrderSide direction) 12 | { 13 | if (price <= 0) throw new ArgumentOutOfRangeException(nameof(price)); 14 | if (volume <= 0) throw new ArgumentOutOfRangeException(nameof(volume)); 15 | 16 | Price = price; 17 | Volume = volume; 18 | Direction = direction; 19 | } 20 | 21 | 22 | /// 23 | /// Quote price 24 | /// 25 | public decimal Price { get; } 26 | 27 | /// 28 | /// Quote volume 29 | /// 30 | public decimal Volume { get; } 31 | 32 | 33 | /// 34 | /// Direction (buy or sell) 35 | /// 36 | public OrderSide Direction { get; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/BinanceBot.Market/CreateOrderRequest.cs: -------------------------------------------------------------------------------- 1 | using Binance.Net.Enums; 2 | 3 | namespace BinanceBot.Market 4 | { 5 | /// 6 | /// Request object used to create a new Binance order 7 | /// 8 | public class CreateOrderRequest 9 | { 10 | public string Symbol { get; set; } 11 | 12 | public OrderSide Side { get; set; } 13 | 14 | public SpotOrderType OrderType { get; set; } 15 | 16 | public TimeInForce? TimeInForce { get; set; } 17 | 18 | public decimal Quantity { get; set; } 19 | 20 | public decimal? Price { get; set; } 21 | 22 | public string NewClientOrderId { get; set; } 23 | 24 | public decimal? StopPrice { get; set; } 25 | 26 | public decimal? IcebergQuantity { get; set; } 27 | 28 | public int? RecvWindow { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/MarketDepthManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Binance.Net.Interfaces.Clients; 4 | using Binance.Net.Objects.Models.Spot; 5 | using BinanceBot.Market.Core; 6 | using CryptoExchange.Net.Objects; 7 | 8 | 9 | namespace BinanceBot.Market 10 | { 11 | /// 12 | /// manager 13 | /// 14 | /// 15 | /// How to manage a local order book correctly [1]: 16 | /// 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth 17 | /// 2. Buffer the events you receive from the stream 18 | /// -> 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 19 | /// 4. Drop any event where u is less or equal lastUpdateId in the snapshot 20 | /// 5. The first processed should have U less or equal lastUpdateId+1 AND u equal or greater lastUpdateId+1 21 | /// 6. While listening to the stream, each new event's U should be equal to the previous event's u+1 22 | /// 7. The data in each event is the absolute quantity for a price level 23 | /// 8. If the quantity is 0, remove the price level 24 | /// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal. 25 | /// Reference: 26 | /// 1. https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#how-to-manage-a-local-order-book-correctly 27 | /// 28 | public class MarketDepthManager 29 | { 30 | private readonly IBinanceClient _restClient; 31 | private readonly IBinanceSocketClient _webSocketClient; 32 | 33 | 34 | /// 35 | /// Create instance of 36 | /// 37 | /// Binance REST client 38 | /// Binance WebSocket client 39 | /// cannot be 40 | /// cannot be 41 | public MarketDepthManager(IBinanceClient binanceRestClient, IBinanceSocketClient webSocketClient) 42 | { 43 | _restClient = binanceRestClient ?? throw new ArgumentNullException(nameof(binanceRestClient)); 44 | _webSocketClient = webSocketClient ?? throw new ArgumentNullException(nameof(webSocketClient)); 45 | } 46 | 47 | 48 | /// 49 | /// Build 50 | /// 51 | /// Market depth 52 | /// Limit of returned orders count 53 | public async Task BuildAsync(MarketDepth marketDepth, short limit = 10) 54 | { 55 | if (marketDepth == null) 56 | throw new ArgumentNullException(nameof(marketDepth)); 57 | if (limit <= 0) 58 | throw new ArgumentOutOfRangeException(nameof(limit)); 59 | 60 | WebCallResult response = await _restClient.SpotApi.ExchangeData.GetOrderBookAsync(marketDepth.Symbol, limit); 61 | BinanceOrderBook orderBook = response.Data; 62 | 63 | marketDepth.UpdateDepth(orderBook.Asks, orderBook.Bids, orderBook.LastUpdateId); 64 | } 65 | 66 | 67 | /// 68 | /// Stream updates 69 | /// 70 | /// Market depth 71 | /// 72 | public void StreamUpdates(MarketDepth marketDepth, TimeSpan? updateInterval = default) 73 | { 74 | if (marketDepth == null) 75 | throw new ArgumentNullException(nameof(marketDepth)); 76 | 77 | _webSocketClient.SpotStreams.SubscribeToOrderBookUpdatesAsync( 78 | marketDepth.Symbol, 79 | (int)updateInterval?.TotalMilliseconds, 80 | marketData => marketDepth.UpdateDepth(marketData.Data.Asks, marketData.Data.Bids, marketData.Data.LastUpdateId)); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/MarketMakerBot.cs: -------------------------------------------------------------------------------- 1 | #define TEST_ORDER_CREATION_MODE // Test order flag. See details: https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#test-new-order-trade 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Binance.Net.Enums; 7 | using Binance.Net.Interfaces.Clients; 8 | using Binance.Net.Objects.Models.Spot; 9 | using BinanceBot.Market.Core; 10 | using BinanceBot.Market.Strategies; 11 | using CryptoExchange.Net.Objects; 12 | using NLog; 13 | 14 | 15 | namespace BinanceBot.Market 16 | { 17 | /// 18 | /// Market Maker Bot 19 | /// 20 | public class MarketMakerBot : BaseMarketBot 21 | { 22 | private readonly IBinanceClient _binanceRestClient; 23 | private readonly IBinanceSocketClient _webSocketClient; 24 | private readonly MarketDepth _marketDepth; 25 | 26 | 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// cannot be 33 | /// cannot be 34 | /// cannot be 35 | /// cannot be 36 | public MarketMakerBot( 37 | string symbol, 38 | NaiveMarketMakerStrategy marketStrategy, 39 | IBinanceClient binanceRestClient, 40 | IBinanceSocketClient webSocketClient, 41 | Logger logger) : 42 | base(symbol, marketStrategy, logger) 43 | { 44 | _marketDepth = new MarketDepth(symbol); 45 | _binanceRestClient = binanceRestClient ?? throw new ArgumentNullException(nameof(binanceRestClient)); 46 | _webSocketClient = webSocketClient ?? throw new ArgumentNullException(nameof(webSocketClient)); 47 | } 48 | 49 | 50 | 51 | public override async Task ValidateServerTimeAsync() 52 | { 53 | CallResult testConnectResponse = await _binanceRestClient.SpotApi.ExchangeData.PingAsync().ConfigureAwait(false); 54 | 55 | if (testConnectResponse.Error != null) 56 | Logger.Error(testConnectResponse.Error.Message); 57 | else 58 | { 59 | string msg = $"Connection was established successfully. Approximate ping time: {testConnectResponse.Data} ms"; 60 | if (testConnectResponse.Data > 1000) 61 | Logger.Warn(msg); 62 | else 63 | Logger.Info(msg); 64 | } 65 | } 66 | 67 | 68 | public override async Task> GetOpenedOrdersAsync(string symbol) 69 | { 70 | if (string.IsNullOrEmpty(symbol)) 71 | throw new ArgumentException("Invalid symbol value", nameof(symbol)); 72 | 73 | var response = await _binanceRestClient.SpotApi.Trading.GetOpenOrdersAsync(symbol).ConfigureAwait(false); 74 | return response.Data; 75 | } 76 | 77 | 78 | public override async Task CancelOrdersAsync(IEnumerable orders) 79 | { 80 | if (orders == null) 81 | throw new ArgumentNullException(nameof(orders)); 82 | 83 | foreach (var order in orders) 84 | await _binanceRestClient.SpotApi.Trading.CancelOrderAsync(orderId: order.Id, origClientOrderId: order.ClientOrderId, symbol: order.Symbol).ConfigureAwait(false); 85 | } 86 | 87 | 88 | public override async Task CreateOrderAsync(CreateOrderRequest order) 89 | { 90 | if (order == null) throw new ArgumentNullException(nameof(order)); 91 | 92 | #if TEST_ORDER_CREATION_MODE 93 | WebCallResult response = await _binanceRestClient.SpotApi.Trading.PlaceTestOrderAsync( 94 | // general 95 | order.Symbol, 96 | order.Side, 97 | order.OrderType, 98 | // price-quantity 99 | price: order.Price, 100 | quantity: order.Quantity, 101 | // metadata 102 | newClientOrderId: order.NewClientOrderId, 103 | timeInForce: order.TimeInForce, 104 | receiveWindow: order.RecvWindow) 105 | .ConfigureAwait(false); 106 | #else 107 | WebCallResult response = await _binanceRestClient.SpotApi.Trading.PlaceOrderAsync( 108 | // general 109 | order.Symbol, 110 | order.Side, 111 | order.OrderType, 112 | // price-quantity 113 | price: order.Price, 114 | quantity: order.Quantity, 115 | // metadata 116 | newClientOrderId: order.NewClientOrderId, 117 | timeInForce: order.TimeInForce, 118 | receiveWindow: order.RecvWindow) 119 | .ConfigureAwait(false); 120 | #endif 121 | 122 | if (response.Error != null) 123 | Logger.Error(response.Error.Message); 124 | 125 | return response.Data; 126 | } 127 | 128 | 129 | 130 | #region Run bot section 131 | public override async Task RunAsync() 132 | { 133 | // validate connection w/ stock 134 | await ValidateServerTimeAsync(); 135 | 136 | // subscribe on order book updates 137 | _marketDepth.MarketBestPairChanged += async (s, e) => await OnMarketBestPairChanged(s, e); 138 | 139 | 140 | var marketDepthManager = new MarketDepthManager(_binanceRestClient, _webSocketClient); 141 | 142 | // stream order book updates 143 | marketDepthManager.StreamUpdates(_marketDepth, TimeSpan.FromMilliseconds(1000)); 144 | // build order book 145 | await marketDepthManager.BuildAsync(_marketDepth, 100); 146 | } 147 | 148 | 149 | private async Task OnMarketBestPairChanged(object sender, MarketBestPairChangedEventArgs e) 150 | { 151 | if (e == null) 152 | throw new ArgumentNullException(nameof(e)); 153 | 154 | // get current opened orders by token 155 | var openOrdersResponse = await GetOpenedOrdersAsync(Symbol); 156 | 157 | // cancel already opened orders (if necessary) 158 | if (openOrdersResponse != null) await CancelOrdersAsync(openOrdersResponse); 159 | 160 | // find new market position 161 | Quote q = MarketStrategy.Process(e.MarketBestPair); 162 | // if position found then create order 163 | if (q != null) 164 | { 165 | var newOrderRequest = new CreateOrderRequest 166 | { 167 | // general 168 | Symbol = Symbol, 169 | Side = q.Direction, 170 | OrderType = SpotOrderType.Limit, 171 | // price-quantity 172 | Price = Decimal.Round(q.Price, decimals: MarketStrategy.Config.PricePrecision), 173 | Quantity = Decimal.Round(q.Volume, decimals: MarketStrategy.Config.BaseAssetPrecision), 174 | // metadata 175 | NewClientOrderId = $"market-bot-{Guid.NewGuid():N}".Substring(0, 36), 176 | TimeInForce = TimeInForce.GoodTillCanceled, 177 | RecvWindow = (int)MarketStrategy.Config.ReceiveWindow.TotalMilliseconds 178 | }; 179 | 180 | var createOrderResponse = await CreateOrderAsync(newOrderRequest); 181 | if (createOrderResponse != null) 182 | Logger.Warn($"Limit order was created. Price: {createOrderResponse.Price}. Volume: {createOrderResponse.Quantity}"); 183 | } 184 | } 185 | #endregion 186 | 187 | 188 | #region Stop/dispose bot section 189 | public override void Stop() 190 | { 191 | Logger.Warn("Bot was stopped"); 192 | Dispose(); 193 | } 194 | 195 | 196 | public override void Dispose() 197 | { 198 | Dispose(true); 199 | GC.SuppressFinalize(this); 200 | } 201 | 202 | 203 | protected virtual void Dispose(bool disposing) 204 | { 205 | if (disposing) 206 | _webSocketClient.Dispose(); 207 | } 208 | #endregion 209 | } 210 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Strategies/NaiveMarketMakerStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Binance.Net.Enums; 3 | using BinanceBot.Market.Configurations; 4 | using BinanceBot.Market.Core; 5 | using NLog; 6 | 7 | namespace BinanceBot.Market.Strategies 8 | { 9 | /// 10 | /// Market Maker strategy (naive version) 11 | /// 12 | public class NaiveMarketMakerStrategy : IMarketStrategy 13 | { 14 | private readonly MarketStrategyConfiguration _marketStrategyConfig; 15 | private readonly Logger _logger; 16 | 17 | 18 | public NaiveMarketMakerStrategy(MarketStrategyConfiguration marketStrategyConfig, Logger logger) 19 | { 20 | _marketStrategyConfig = marketStrategyConfig ?? throw new ArgumentNullException(nameof(marketStrategyConfig)); 21 | _logger = logger ?? LogManager.GetCurrentClassLogger(); 22 | } 23 | 24 | 25 | public MarketStrategyConfiguration Config => _marketStrategyConfig; 26 | 27 | 28 | /// 29 | /// Process new best 30 | /// 31 | /// Best ask-bid pair 32 | /// Recommended price-volume pair or 33 | public Quote Process(MarketDepthPair marketPair) 34 | { 35 | if (marketPair == null) 36 | throw new ArgumentNullException(nameof(marketPair)); 37 | if (!marketPair.IsFull) 38 | return null; 39 | 40 | 41 | Quote quote = null; 42 | 43 | _logger.Info($"Best ask / bid: {marketPair.Ask.Price} / {marketPair.Bid.Price}. Update Id: {marketPair.UpdateTime}."); 44 | 45 | // get price spreads (in percent) 46 | decimal actualSpread = marketPair.PriceSpread!.Value/marketPair.MediumPrice!.Value * 100; // spread_relative = spread_absolute/price * 100 47 | decimal expectedSpread = _marketStrategyConfig.TradeWhenSpreadGreaterThan; 48 | 49 | _logger.Info($"Spread absolute (relative): {marketPair.PriceSpread} ({actualSpread/100:P}). Update Id: {marketPair.UpdateTime}."); 50 | 51 | 52 | if (actualSpread >= expectedSpread) 53 | { 54 | // compute new order price 55 | decimal extra = marketPair.MediumPrice.Value * (actualSpread - expectedSpread)/100; // extra = medium_price * (spread_actual - spread_expected) 56 | decimal orderPrice = marketPair.Bid.Price + extra; // new_price = best_bid + extra 57 | 58 | // compute order volume 59 | decimal volumeSpread = marketPair.VolumeSpread!.Value; 60 | decimal orderVolume = volumeSpread > _marketStrategyConfig.MaxOrderVolume ? 61 | _marketStrategyConfig.MaxOrderVolume : // max volume restriction 62 | (volumeSpread < _marketStrategyConfig.MinOrderVolume ? _marketStrategyConfig.MinOrderVolume : volumeSpread); // min volume restriction 63 | 64 | // return new price-volume pair 65 | quote = new Quote(orderPrice, orderVolume, OrderSide.Buy); 66 | } 67 | 68 | 69 | return quote; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Utility/DescDecimalComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BinanceBot.Market.Utility 4 | { 5 | /// 6 | /// Descending decimal comparer 7 | /// 8 | internal class DescendingDecimalComparer : IComparer 9 | { 10 | public int Compare(decimal x, decimal y) => 11 | decimal.Compare(x, y) * -1; 12 | } 13 | } -------------------------------------------------------------------------------- /src/BinanceBot.Market/Utility/QuoteExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Binance.Net.Enums; 4 | using BinanceBot.Market.Core; 5 | 6 | namespace BinanceBot.Market.Utility 7 | { 8 | internal static class QuoteExtensions 9 | { 10 | public static IEnumerable ToQuotes(this IDictionary source, OrderSide direction) 11 | { 12 | return source? 13 | .Select(s => new Quote(s.Key, s.Value, direction)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/BinanceBot.MarketBot.Console/BinanceBot.MarketBot.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net7.0 6 | 0.3.0 7 | Dmitry Petukhov 8 | BinanceBot 9 | (c) 2023, Dmitry Petukhov. 10 | http://0xcode.in/ 11 | https://github.com/codez0mb1e/BinanceBot 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/BinanceBot.MarketBot.Console/NLog.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/BinanceBot.MarketBot.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Binance.Net.Clients; 5 | using Binance.Net.Enums; 6 | using Binance.Net.Interfaces.Clients; 7 | using Binance.Net.Objects; 8 | using BinanceBot.Market; 9 | using BinanceBot.Market.Configurations; 10 | using BinanceBot.Market.Strategies; 11 | 12 | using static System.Console; 13 | 14 | 15 | namespace BinanceBot.MarketBot.Console 16 | { 17 | internal static class Program 18 | { 19 | #region Bot Settings 20 | // WARN: set your credentials here here 21 | private const string ApiKey = "***"; 22 | private const string Secret = "***"; 23 | 24 | // WARN: set necessary token here 25 | private const string Symbol = "SOLUSDT"; 26 | private static readonly TimeSpan ReceiveWindow = TimeSpan.FromMilliseconds(1000); 27 | #endregion 28 | 29 | private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 30 | 31 | 32 | static async Task Main(string[] args) 33 | { 34 | // 1. create connections with exchange 35 | var credentials = new BinanceApiCredentials(ApiKey, Secret); 36 | using IBinanceClient binanceRestClient = new BinanceClient(new BinanceClientOptions { ApiCredentials = credentials }); 37 | using IBinanceSocketClient binanceSocketClient = new BinanceSocketClient(new BinanceSocketClientOptions { ApiCredentials = credentials }); 38 | 39 | 40 | // 2. test connection 41 | Logger.Info("Testing connection..."); 42 | var pingAsyncResult = await binanceRestClient.SpotApi.ExchangeData.PingAsync(); 43 | Logger.Info($"Ping {pingAsyncResult.Data} ms"); 44 | 45 | 46 | // 2.1. check permissions 47 | var permissionsResponse = await binanceRestClient.SpotApi.Account.GetAPIKeyPermissionsAsync(); 48 | if (!permissionsResponse.Success) 49 | { 50 | Logger.Error($"{permissionsResponse.Error?.Message}"); 51 | ReadLine(); 52 | } 53 | else if (permissionsResponse.Data.IpRestrict | !permissionsResponse.Data.EnableSpotAndMarginTrading) 54 | { 55 | Logger.Error("Insufficient API permissions"); 56 | ReadLine(); 57 | } 58 | 59 | 60 | // 3. set bot strategy config 61 | var exchangeInfoResult = binanceRestClient.SpotApi.ExchangeData.GetExchangeInfoAsync(Symbol); 62 | 63 | var symbolInfo = exchangeInfoResult.Result.Data.Symbols 64 | .Single(s => s.Name.Equals(Symbol, StringComparison.InvariantCultureIgnoreCase)); 65 | 66 | if (!(symbolInfo.Status == SymbolStatus.Trading && symbolInfo.OrderTypes.Contains(SpotOrderType.Market))) 67 | { 68 | Logger.Error($"Symbol {symbolInfo.Name} doesn't suitable for this strategy"); 69 | return; 70 | } 71 | 72 | if (symbolInfo.LotSizeFilter == null) 73 | { 74 | Logger.Error($"Cannot define risks strategy for {symbolInfo.Name}"); 75 | return; 76 | } 77 | 78 | if (symbolInfo.PriceFilter == null) 79 | { 80 | Logger.Error($"Cannot define price precision for {symbolInfo.Name}. Please define it manually."); 81 | return; 82 | } 83 | int pricePrecision = (int)Math.Abs(Math.Log10(Convert.ToDouble(symbolInfo.PriceFilter.TickSize))); 84 | 85 | 86 | // WARN: set thresholds for strategy here 87 | var strategyConfig = new MarketStrategyConfiguration 88 | { 89 | TradeWhenSpreadGreaterThan = .05M, // or 0.05%, (price spread*min_volume) should be greater than broker's commissions for trade 90 | MinOrderVolume = symbolInfo.LotSizeFilter.MinQuantity*10, 91 | MaxOrderVolume = symbolInfo.LotSizeFilter.MinQuantity*100, 92 | BaseAssetPrecision = symbolInfo.BaseAssetPrecision, 93 | PricePrecision = pricePrecision, 94 | ReceiveWindow = ReceiveWindow 95 | }; 96 | 97 | var marketStrategy = new NaiveMarketMakerStrategy(strategyConfig, Logger); 98 | 99 | 100 | // 3. start bot 101 | IMarketBot bot = new MarketMakerBot(Symbol, marketStrategy, binanceRestClient, binanceSocketClient, Logger); 102 | 103 | try 104 | { 105 | await bot.RunAsync(); 106 | 107 | WriteLine($"Press Enter to stop {Symbol} bot..."); 108 | ReadLine(); 109 | } 110 | finally 111 | { 112 | bot.Stop(); 113 | } 114 | 115 | WriteLine("Press Enter to exit..."); 116 | ReadLine(); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/BinanceBot.MarketViewer.Console/BinanceBot.MarketViewer.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net7.0 6 | 0.3.0 7 | Dmitry Petukhov 8 | BinanceBot 9 | (c) 2023, Dmitry Petukhov. 10 | http://0xcode.in/ 11 | https://github.com/codez0mb1e/BinanceBot 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/BinanceBot.MarketViewer.Console/NLog.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/BinanceBot.MarketViewer.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Binance.Net.Clients; 8 | using Binance.Net.Interfaces.Clients; 9 | using Binance.Net.Objects; 10 | using BinanceBot.Market; 11 | using BinanceBot.Market.Core; 12 | using Spectre.Console; 13 | 14 | using static System.Console; 15 | 16 | 17 | namespace BinanceBot.MarketViewer.Console 18 | { 19 | internal static class Program 20 | { 21 | #region Bot Settings 22 | // WARN: Set your credentials here here 23 | private const string ApiKey = "***"; 24 | private const string Secret = "***"; 25 | 26 | // WARN: Set necessary token here 27 | private const string Symbol = "ETHUSDT"; 28 | private const int OrderBookDepth = 10; 29 | private static readonly TimeSpan? OrderBookUpdateLimit = TimeSpan.FromMilliseconds(1000); 30 | #endregion 31 | 32 | private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 33 | 34 | 35 | static async Task Main(string[] args) 36 | { 37 | // 1. create connections with exchange 38 | var credentials = new BinanceApiCredentials(ApiKey, Secret); 39 | using IBinanceClient binanceRestClient = new BinanceClient(new BinanceClientOptions { ApiCredentials = credentials }); 40 | using IBinanceSocketClient binanceSocketClient = new BinanceSocketClient(new BinanceSocketClientOptions { ApiCredentials = credentials }); 41 | 42 | 43 | // 2. test connection 44 | await AnsiConsole.Status() 45 | .StartAsync("Testing connection...", async ctx => 46 | { 47 | var pingResult = await binanceRestClient.SpotApi.ExchangeData.PingAsync(); 48 | AnsiConsole.MarkupLine($"Ping time: [yellow]{pingResult.Data} ms[/]"); 49 | 50 | Task.Delay(1000).Wait(); 51 | }); 52 | 53 | // 3. get order book 54 | var marketDepthManager = new MarketDepthManager(binanceRestClient, binanceSocketClient); 55 | var marketDepth = new MarketDepth(Symbol); 56 | 57 | 58 | // 4. Render order book 59 | var orderBookTable = new Table 60 | { 61 | Title = new TableTitle($"{Symbol} Quotes") 62 | }; 63 | 64 | foreach (var column in new[] { "Asks (volume)", "Price", "Bid (volume)" }) 65 | orderBookTable.AddColumn(column); 66 | 67 | static IEnumerable<(string price, string volume)> GetValues(IEnumerable data) 68 | { 69 | return data 70 | .OrderByDescending(q => q.Price) 71 | .Select(q => (price: q.Price.ToString(CultureInfo.InvariantCulture), volume: q.Volume.ToString(CultureInfo.InvariantCulture)) ); 72 | } 73 | 74 | marketDepth.MarketDepthChanged += (sender, e) => 75 | { 76 | var asks = e.Asks.OrderBy(q => q.Price).Take(OrderBookDepth).ToImmutableArray(); 77 | var bids = e.Bids.OrderByDescending(q => q.Price).Take(OrderBookDepth).ToImmutableArray(); 78 | 79 | 80 | orderBookTable.Rows.Clear(); 81 | 82 | foreach (var row in GetValues(asks)) 83 | orderBookTable.AddRow(row.volume, $"[red]{row.price}[/]", String.Empty); 84 | 85 | foreach (var row in GetValues(bids)) 86 | orderBookTable.AddRow(String.Empty, $"[green]{row.price}[/]", row.volume); 87 | 88 | orderBookTable.Caption = new TableTitle( 89 | $"Spread: {e.Asks.Select(q => q.Price).Min() - e.Bids.Select(q => q.Price).Max()}. " + 90 | $"Last updated as {DateTimeOffset.FromUnixTimeSeconds(e.UpdateTime):T}\n" 91 | ); 92 | 93 | var dominanceChart = new BreakdownChart() 94 | .ShowTagValues() 95 | .AddItem("Asks", (double) asks.Select(q => q.Volume).Sum(), Color.Red) 96 | .AddItem("Bids", (double) bids.Select(q => q.Volume).Sum(), Color.Green); 97 | 98 | 99 | AnsiConsole.Clear(); 100 | 101 | AnsiConsole.Write(orderBookTable); 102 | AnsiConsole.Write(dominanceChart); 103 | }; 104 | 105 | 106 | // build order book 107 | await marketDepthManager.BuildAsync(marketDepth, OrderBookDepth); 108 | // stream order book updates 109 | marketDepthManager.StreamUpdates(marketDepth, OrderBookUpdateLimit); 110 | 111 | 112 | WriteLine("Press Enter to exit..."); 113 | ReadLine(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/BinanceBot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31613.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinanceBot.Market", "BinanceBot.Market\BinanceBot.Market.csproj", "{AD93580C-2CEC-4695-89E7-2029F6E44D32}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinanceBot.MarketBot.Console", "BinanceBot.MarketBot.Console\BinanceBot.MarketBot.Console.csproj", "{F2327623-18BC-4176-AA9C-D7A5F1A0255D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinanceBot.MarketViewer.Console", "BinanceBot.MarketViewer.Console\BinanceBot.MarketViewer.Console.csproj", "{7912A437-0EC4-4939-B244-9379927AB0D1}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B6FD43FF-5A63-4978-8444-5F06FCB9C5C0}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\README.md = ..\README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {AD93580C-2CEC-4695-89E7-2029F6E44D32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {AD93580C-2CEC-4695-89E7-2029F6E44D32}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {AD93580C-2CEC-4695-89E7-2029F6E44D32}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {AD93580C-2CEC-4695-89E7-2029F6E44D32}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {F2327623-18BC-4176-AA9C-D7A5F1A0255D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {F2327623-18BC-4176-AA9C-D7A5F1A0255D}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {F2327623-18BC-4176-AA9C-D7A5F1A0255D}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {F2327623-18BC-4176-AA9C-D7A5F1A0255D}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {7912A437-0EC4-4939-B244-9379927AB0D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {7912A437-0EC4-4939-B244-9379927AB0D1}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {7912A437-0EC4-4939-B244-9379927AB0D1}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {7912A437-0EC4-4939-B244-9379927AB0D1}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {743FF889-DA4B-4AED-A0B8-6559562BA95F} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /src/BinanceBot.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True --------------------------------------------------------------------------------