Skip to content

Commit ecca3f9

Browse files
authored
Async ExchangeAPI init (#668)
* Make exchange initialization async * Fix test * Async logger * Fix buy option * Update readme
1 parent a01ebdd commit ecca3f9

27 files changed

+186
-129
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ The following cryptocurrency services are supported:
6262

6363
Exchange constructors are private, to get access to an exchange in code use:
6464

65-
`ExchangeAPI.GetExchangeAPI`.
65+
`ExchangeAPI.GetExchangeAPIAsync`.
6666

6767
### Installing the CLI
6868

@@ -121,7 +121,7 @@ e.g.
121121
public static async Task Main(string[] args)
122122
{
123123
// create a web socket connection to Binance. Note you can Dispose the socket anytime to shut it down.
124-
using var api = ExchangeAPI.GetExchangeAPI<ExchangeBinanceAPI>();
124+
using var api = await ExchangeAPI.GetExchangeAPIAsync<ExchangeBinanceAPI>();
125125
// the web socket will handle disconnects and attempt to re-connect automatically.
126126
using var socket = await api.GetTickersWebSocket(tickers =>
127127
{

examples/ExchangeSharpWinForms/MainForm.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Text;
33
using System.Windows.Forms;
44
using ExchangeSharp;
@@ -28,7 +28,7 @@ private async void FetchTickers()
2828
this.UseWaitCursor = true;
2929
try
3030
{
31-
var api = ExchangeAPI.GetExchangeAPI(cmbExchange.SelectedItem as string);
31+
var api = await ExchangeAPI.GetExchangeAPIAsync(cmbExchange.SelectedItem as string);
3232
var tickers = await api.GetTickersAsync();
3333
StringBuilder b = new StringBuilder();
3434
foreach (var ticker in tickers)
@@ -52,10 +52,10 @@ public MainForm()
5252
InitializeComponent();
5353
}
5454

55-
protected override void OnShown(EventArgs e)
55+
protected override async void OnShown(EventArgs e)
5656
{
5757
base.OnShown(e);
58-
foreach (var exchange in ExchangeAPI.GetExchangeAPIs())
58+
foreach (var exchange in await ExchangeAPI.GetExchangeAPIsAsync())
5959
{
6060
cmbExchange.Items.Add(exchange.Name);
6161
}

src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ public abstract partial class ExchangeAPI : BaseAPI, IExchangeAPI
4545
#region Private methods
4646

4747
private static readonly IReadOnlyCollection<Type> exchangeTypes = typeof(ExchangeAPI).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ExchangeAPI)) && !type.IsAbstract).ToArray();
48-
private static readonly ConcurrentDictionary<Type, IExchangeAPI> apis = new ConcurrentDictionary<Type, IExchangeAPI>();
48+
private static readonly ConcurrentDictionary<Type, Task<IExchangeAPI>> apis = new ConcurrentDictionary<Type, Task<IExchangeAPI>>();
49+
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
4950

5051
private bool initialized;
5152
private bool disposed;
5253

53-
private static IExchangeAPI InitializeAPI(Type? type, Type? knownType = null)
54+
private static async Task<IExchangeAPI> InitializeAPIAsync(Type? type, Type? knownType = null)
5455
{
5556
if (type is null)
5657
{
@@ -64,30 +65,23 @@ private static IExchangeAPI InitializeAPI(Type? type, Type? knownType = null)
6465
}
6566

6667
const int retryCount = 3;
67-
Exception? ex = null;
68-
68+
6969
// try up to 3 times to init
70-
for (int i = 1; i <= 3; i++)
70+
for (int i = 1; i <= retryCount; i++)
7171
{
7272
try
7373
{
74-
api.InitializeAsync().Sync();
75-
ex = null;
76-
break;
74+
await api.InitializeAsync();
7775
}
78-
catch (Exception _ex)
76+
catch (Exception)
7977
{
80-
ex = _ex;
81-
if (i != retryCount)
78+
if (i == retryCount)
8279
{
83-
Thread.Sleep(5000);
80+
throw;
8481
}
85-
}
86-
}
8782

88-
if (ex != null)
89-
{
90-
throw ex;
83+
Thread.Sleep(5000);
84+
}
9185
}
9286

9387
return api;
@@ -365,7 +359,7 @@ public void Dispose()
365359
Cache?.Dispose();
366360

367361
// take out of global api dictionary if disposed and we are the current exchange in the dictionary
368-
if (apis.TryGetValue(GetType(), out IExchangeAPI existing) && this == existing)
362+
if (apis.TryGetValue(GetType(), out Task<IExchangeAPI> existing) && this == existing.Result)
369363
{
370364
apis.TryRemove(GetType(), out _);
371365
}
@@ -386,9 +380,17 @@ private async Task InitializeAsync()
386380
initialized = true;
387381
}
388382

389-
private static IExchangeAPI CreateExchangeAPI(Type? type)
383+
/// <summary>
384+
/// Create an exchange api, by-passing any cache. Use this method for cases
385+
/// where you need multiple instances of the same exchange, for example
386+
/// multiple credentials.
387+
/// </summary>
388+
/// <typeparam name="T">Type of exchange api to create</typeparam>
389+
/// <returns>Created exchange api</returns>
390+
[Obsolete("Use the async version")]
391+
public static T CreateExchangeAPI<T>() where T : ExchangeAPI
390392
{
391-
return InitializeAPI(type);
393+
return CreateExchangeAPIAsync<T>().Result;
392394
}
393395

394396
/// <summary>
@@ -398,75 +400,117 @@ private static IExchangeAPI CreateExchangeAPI(Type? type)
398400
/// </summary>
399401
/// <typeparam name="T">Type of exchange api to create</typeparam>
400402
/// <returns>Created exchange api</returns>
401-
public static T CreateExchangeAPI<T>() where T : ExchangeAPI
403+
public static async Task<T> CreateExchangeAPIAsync<T>() where T : ExchangeAPI
402404
{
403-
return (T)CreateExchangeAPI(typeof(T));
405+
return (T)await InitializeAPIAsync(typeof(T));
404406
}
405407

406408
/// <summary>
407409
/// Get a cached exchange API given an exchange name (see ExchangeName class)
408410
/// </summary>
409411
/// <param name="exchangeName">Exchange name. Must match the casing of the ExchangeName class name exactly.</param>
410412
/// <returns>Exchange API or null if not found</returns>
413+
[Obsolete("Use the async version")]
411414
public static IExchangeAPI GetExchangeAPI(string exchangeName)
415+
{
416+
return GetExchangeAPIAsync(exchangeName).Result;
417+
}
418+
419+
/// <summary>
420+
/// Get a cached exchange API given an exchange name (see ExchangeName class)
421+
/// </summary>
422+
/// <param name="exchangeName">Exchange name. Must match the casing of the ExchangeName class name exactly.</param>
423+
/// <returns>Exchange API or null if not found</returns>
424+
public static Task<IExchangeAPI> GetExchangeAPIAsync(string exchangeName)
412425
{
413426
Type type = ExchangeName.GetExchangeType(exchangeName);
414-
return GetExchangeAPI(type);
427+
return GetExchangeAPIAsync(type);
415428
}
416429

417430
/// <summary>
418431
/// Get a cached exchange API given a type
419432
/// </summary>
420433
/// <typeparam name="T">Type of exchange to get</typeparam>
421434
/// <returns>Exchange API or null if not found</returns>
435+
[Obsolete("Use the async version")]
422436
public static IExchangeAPI GetExchangeAPI<T>() where T : ExchangeAPI
437+
{
438+
return GetExchangeAPIAsync<T>().Result;
439+
}
440+
441+
public static Task<IExchangeAPI> GetExchangeAPIAsync<T>() where T : ExchangeAPI
423442
{
424443
// note: this method will be slightly slow (milliseconds) the first time it is called due to cache miss and initialization
425444
// subsequent calls with cache hits will be nanoseconds
426445
Type type = typeof(T)!;
427-
return GetExchangeAPI(type);
446+
return GetExchangeAPIAsync(type);
428447
}
429448

430449
/// <summary>
431450
/// Get a cached exchange API given a type
432451
/// </summary>
433452
/// <param name="type">Type of exchange</param>
434453
/// <returns>Exchange API or null if not found</returns>
454+
[Obsolete("Use the async version")]
435455
public static IExchangeAPI GetExchangeAPI(Type type)
436456
{
437-
// note: this method will be slightly slow (milliseconds) the first time it is called due to cache miss and initialization
438-
// subsequent calls with cache hits will be nanoseconds
439-
return apis.GetOrAdd(type, _exchangeName =>
457+
return GetExchangeAPIAsync(type).Result;
458+
}
459+
460+
/// <summary>
461+
/// Get a cached exchange API given a type
462+
/// </summary>
463+
/// <param name="type">Type of exchange</param>
464+
/// <returns>Exchange API or null if not found</returns>
465+
public static async Task<IExchangeAPI> GetExchangeAPIAsync(Type type)
466+
{
467+
if (apis.TryGetValue(type, out var result)) return await result;
468+
469+
await semaphore.WaitAsync();
470+
try
440471
{
441-
// find the api type
442-
Type? foundType = exchangeTypes.FirstOrDefault(t => t == type);
443-
return InitializeAPI(foundType, type);
444-
});
472+
// try again inside semaphore
473+
if (apis.TryGetValue(type, out result)) return await result;
474+
475+
// still not found, initialize it
476+
var foundType = exchangeTypes.FirstOrDefault(t => t == type);
477+
return await (apis[type] = InitializeAPIAsync(foundType, type));
478+
}
479+
finally
480+
{
481+
semaphore.Release();
482+
}
445483
}
446484

447485
/// <summary>
448486
/// Get all cached versions of exchange APIs
449487
/// </summary>
450488
/// <returns>All APIs</returns>
489+
[Obsolete("Use the async version")]
451490
public static IExchangeAPI[] GetExchangeAPIs()
452491
{
453-
foreach (Type type in exchangeTypes)
492+
return GetExchangeAPIsAsync().Result;
493+
}
494+
495+
/// <summary>
496+
/// Get all cached versions of exchange APIs
497+
/// </summary>
498+
/// <returns>All APIs</returns>
499+
public static async Task<IExchangeAPI[]> GetExchangeAPIsAsync()
500+
{
501+
var apiList = new List<IExchangeAPI>();
502+
foreach (var kv in apis.ToArray())
454503
{
455-
List<IExchangeAPI> apiList = new List<IExchangeAPI>();
456-
foreach (var kv in apis.ToArray())
504+
if (kv.Value == null)
457505
{
458-
if (kv.Value == null)
459-
{
460-
apiList.Add(GetExchangeAPI(kv.Key));
461-
}
462-
else
463-
{
464-
apiList.Add(kv.Value);
465-
}
506+
apiList.Add(await GetExchangeAPIAsync(kv.Key));
507+
}
508+
else
509+
{
510+
apiList.Add(await kv.Value);
466511
}
467-
return apiList.ToArray();
468512
}
469-
return apis.Values.ToArray();
513+
return apiList.ToArray();
470514
}
471515

472516
/// <summary>

src/ExchangeSharp/API/Exchanges/_Base/ExchangeLogger.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,29 @@ public static BinaryReader OpenLogReader(string basePath)
195195
return new BinaryReader(new System.IO.Compression.GZipStream(File.OpenRead(basePath + ".gz"), System.IO.Compression.CompressionMode.Decompress, false));
196196
}
197197

198+
/// <summary>
199+
/// Begins logging exchanges - writes errors to console. You should block the app using Console.ReadLine.
200+
/// </summary>
201+
/// <param name="path">Path to write files to</param>
202+
/// <param name="intervalSeconds">Interval in seconds in between each log calls for each exchange</param>
203+
/// <param name="terminateAction">Call this when the process is about to exit, like a WM_CLOSE message on Windows.</param>
204+
/// <param name="compress">Whether to compress the log files</param>
205+
/// <param name="exchangeNamesAndSymbols">Exchange names and symbols to log</param>
206+
[Obsolete("Use the async version")]
207+
public static void LogExchanges(string path, float intervalSeconds, out Action terminateAction, bool compress, params string[] exchangeNamesAndSymbols)
208+
{
209+
terminateAction = LogExchangesAsync(path, intervalSeconds, compress, exchangeNamesAndSymbols).Result;
210+
}
211+
198212
/// <summary>
199213
/// Begins logging exchanges - writes errors to console. You should block the app using Console.ReadLine.
200214
/// </summary>
201215
/// <param name="path">Path to write files to</param>
202216
/// <param name="intervalSeconds">Interval in seconds in between each log calls for each exchange</param>
203-
/// <param name="terminateAction">Call this when the process is about to exit, like a WM_CLOSE message on Windows.</param>
204217
/// <param name="compress">Whether to compress the log files</param>
205218
/// <param name="exchangeNamesAndSymbols">Exchange names and symbols to log</param>
206-
public static void LogExchanges(string path, float intervalSeconds, out Action terminateAction, bool compress, params string[] exchangeNamesAndSymbols)
219+
/// <returns">Call this when the process is about to exit, like a WM_CLOSE message on Windows.</returns>
220+
public static async Task<Action> LogExchangesAsync(string path, float intervalSeconds, bool compress, params string[] exchangeNamesAndSymbols)
207221
{
208222
bool terminating = false;
209223
Action terminator = null;
@@ -212,7 +226,7 @@ public static void LogExchanges(string path, float intervalSeconds, out Action t
212226
List<ExchangeLogger> loggers = new List<ExchangeLogger>();
213227
for (int i = 0; i < exchangeNamesAndSymbols.Length;)
214228
{
215-
loggers.Add(new ExchangeLogger(ExchangeAPI.GetExchangeAPI(exchangeNamesAndSymbols[i++]), exchangeNamesAndSymbols[i++], intervalSeconds, path, compress));
229+
loggers.Add(new ExchangeLogger(await ExchangeAPI.GetExchangeAPIAsync(exchangeNamesAndSymbols[i++]), exchangeNamesAndSymbols[i++], intervalSeconds, path, compress));
216230
};
217231
foreach (ExchangeLogger logger in loggers)
218232
{
@@ -244,7 +258,6 @@ public static void LogExchanges(string path, float intervalSeconds, out Action t
244258
loggers.Clear();
245259
}
246260
};
247-
terminateAction = terminator;
248261

249262
// make sure to close properly
250263
Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e)
@@ -256,6 +269,8 @@ public static void LogExchanges(string path, float intervalSeconds, out Action t
256269
terminator();
257270
};
258271
Logger.Info("Loggers \"{0}\" started, press ENTER or CTRL-C to terminate.", string.Join(", ", loggers.Select(l => l.API.Name)));
272+
273+
return terminator;
259274
}
260275

261276
/// <summary>

src/ExchangeSharpConsole/ExchangeSharpConsole.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>

src/ExchangeSharpConsole/Options/BaseOption.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ protected void WaitInteractively()
117117
Console.CursorLeft = 0;
118118
}
119119

120-
protected IExchangeAPI GetExchangeInstance(string exchangeName)
120+
protected Task<IExchangeAPI> GetExchangeInstanceAsync(string exchangeName)
121121
{
122-
return ExchangeAPI.GetExchangeAPI(exchangeName);
122+
return ExchangeAPI.GetExchangeAPIAsync(exchangeName);
123123
}
124124

125125
protected async Task RunWebSocket(string exchangeName, Func<IExchangeAPI, Task<IWebSocket>> getWebSocket)
126126
{
127-
using var api = ExchangeAPI.GetExchangeAPI(exchangeName);
127+
using var api = await ExchangeAPI.GetExchangeAPIAsync(exchangeName);
128128

129129
Console.WriteLine("Connecting web socket to {0}...", api.Name);
130130

src/ExchangeSharpConsole/Options/BuyOption.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public override async Task RunCommand()
2020

2121
protected async Task AddOrder(bool isBuyOrder)
2222
{
23-
using var api = GetExchangeInstance(ExchangeName);
23+
using var api = await GetExchangeInstanceAsync(ExchangeName);
2424

2525
var exchangeOrderRequest = GetExchangeOrderRequest(isBuyOrder, api);
2626

src/ExchangeSharpConsole/Options/CancelOrderOption.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class CancelOrderOption : BaseOption,
1111
{
1212
public override async Task RunCommand()
1313
{
14-
using var api = GetExchangeInstance(ExchangeName);
14+
using var api = await GetExchangeInstanceAsync(ExchangeName);
1515

1616
api.LoadAPIKeys(KeyPath);
1717

src/ExchangeSharpConsole/Options/CandlesOption.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class CandlesOption : BaseOption, IOptionPerExchange, IOptionPerMarketSym
1111
{
1212
public override async Task RunCommand()
1313
{
14-
using var api = GetExchangeInstance(ExchangeName);
14+
using var api = await GetExchangeInstanceAsync(ExchangeName);
1515

1616
var candles = await api.GetCandlesAsync(
1717
MarketSymbol,

src/ExchangeSharpConsole/Options/CurrenciesOption.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class CurrenciesOption : BaseOption, IOptionPerExchange
1313

1414
public override async Task RunCommand()
1515
{
16-
using var api = GetExchangeInstance(ExchangeName);
16+
using var api = await GetExchangeInstanceAsync(ExchangeName);
1717

1818
Authenticate(api);
1919

0 commit comments

Comments
 (0)