Created
April 7, 2021 15:41
-
-
Save JohanLarsson/1735bd58821716232244d33deec7f7da to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Snappy.AlphaVantage | |
{ | |
using System; | |
using System.Collections.Immutable; | |
using System.Net.Http; | |
using System.Threading; | |
using System.Threading.Tasks; | |
public sealed class AlphaVantageClient : IDisposable | |
{ | |
private readonly string apiKey; | |
private readonly HttpClient client; | |
private bool disposed; | |
public AlphaVantageClient(HttpMessageHandler messageHandler, string apiKey) | |
{ | |
this.apiKey = apiKey; | |
#pragma warning disable IDISP014 // Use a single instance of HttpClient. | |
this.client = new HttpClient(messageHandler) | |
{ | |
BaseAddress = new Uri("https://www.alphavantage.co", UriKind.Absolute), | |
}; | |
#pragma warning restore IDISP014 // Use a single instance of HttpClient. | |
} | |
public Task<ImmutableArray<Candle>> IntervalAsync(string symbol, Interval interval, OutputSize outputSize, CancellationToken cancellationToken = default) | |
{ | |
this.ThrowIfDisposed(); | |
return this.client.GetCandlesFromCsvAsync( | |
new Uri($"query?function=TIME_SERIES_INTRADAY&symbol={symbol}&interval={Interval()}&outputsize={OutputSize()}&datatype=csv&apikey={this.apiKey}", UriKind.Relative), | |
cancellationToken); | |
string Interval() => interval switch | |
{ | |
AlphaVantage.Interval.Minute => "1min", | |
AlphaVantage.Interval.FiveMinutes => "5min", | |
AlphaVantage.Interval.FifteenMinutes => "15min", | |
AlphaVantage.Interval.ThirtyMinutes => "30min", | |
AlphaVantage.Interval.Hour => "60min", | |
_ => throw new ArgumentOutOfRangeException(nameof(interval), interval, null) | |
}; | |
string OutputSize() => outputSize switch | |
{ | |
AlphaVantage.OutputSize.Full => "full", | |
AlphaVantage.OutputSize.Compact => "compact", | |
_ => throw new ArgumentOutOfRangeException(nameof(outputSize), outputSize, null) | |
}; | |
} | |
public Task<ImmutableArray<Candle>> IntervalExtendedAsync(string symbol, Interval interval, Slice slice, CancellationToken cancellationToken = default) | |
{ | |
this.ThrowIfDisposed(); | |
return this.client.GetCandlesFromCsvAsync( | |
#pragma warning disable CA1308 // Normalize strings to uppercase | |
new Uri($"query?function=TIME_SERIES_INTRADAY_EXTENDED&symbol={symbol}&interval={Interval()}&slice={slice.ToString().ToLowerInvariant()}&apikey={this.apiKey}", UriKind.Relative), | |
#pragma warning restore CA1308 // Normalize strings to uppercase | |
cancellationToken); | |
string Interval() => interval switch | |
{ | |
AlphaVantage.Interval.Minute => "1min", | |
AlphaVantage.Interval.FiveMinutes => "5min", | |
AlphaVantage.Interval.FifteenMinutes => "15min", | |
AlphaVantage.Interval.ThirtyMinutes => "30min", | |
AlphaVantage.Interval.Hour => "60min", | |
_ => throw new ArgumentOutOfRangeException(nameof(interval), interval, null) | |
}; | |
} | |
public Task<ImmutableArray<Candle>> DailyAsync(string symbol, OutputSize outputSize, CancellationToken cancellationToken = default) | |
{ | |
this.ThrowIfDisposed(); | |
return this.client.GetCandlesFromCsvAsync( | |
new Uri($"/query?function=TIME_SERIES_DAILY&symbol={symbol}&outputsize={OutputSize()}&datatype=csv&apikey={this.apiKey}", UriKind.Relative), | |
cancellationToken); | |
string OutputSize() | |
{ | |
return outputSize switch | |
{ | |
AlphaVantage.OutputSize.Full => "full", | |
AlphaVantage.OutputSize.Compact => "compact", | |
_ => throw new ArgumentOutOfRangeException(nameof(outputSize), outputSize, null) | |
}; | |
} | |
} | |
public Task<ImmutableArray<AdjustedCandle>> DailyAdjustedAsync(string symbol, OutputSize outputSize, CancellationToken cancellationToken = default) | |
{ | |
this.ThrowIfDisposed(); | |
return this.client.GetAdjustedCandleFromCsvAsync( | |
new Uri($"/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol={symbol}&outputsize={OutputSize()}&datatype=csv&apikey={this.apiKey}", UriKind.Relative), | |
cancellationToken); | |
string OutputSize() | |
{ | |
return outputSize switch | |
{ | |
AlphaVantage.OutputSize.Full => "full", | |
AlphaVantage.OutputSize.Compact => "compact", | |
_ => throw new ArgumentOutOfRangeException(nameof(outputSize), outputSize, null) | |
}; | |
} | |
} | |
public void Dispose() | |
{ | |
if (this.disposed) | |
{ | |
return; | |
} | |
this.disposed = true; | |
this.client.Dispose(); | |
} | |
private void ThrowIfDisposed() | |
{ | |
if (this.disposed) | |
{ | |
throw new ObjectDisposedException(nameof(AlphaVantageClient)); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Snappy | |
{ | |
using System; | |
using System.Collections.Immutable; | |
using System.Globalization; | |
using System.IO; | |
using System.Text; | |
using System.Threading.Tasks; | |
internal static class Csv | |
{ | |
internal static async Task<ImmutableArray<Candle>> ParseCandlesAsync(Stream content, Encoding encoding) | |
{ | |
using var reader = new CsvReader(content, encoding); | |
var header = await reader.ReadLineAsync().ConfigureAwait(false); | |
if (header != "timestamp,open,high,low,close,volume" && | |
header != "time,open,high,low,close,volume") | |
{ | |
throw new FormatException($"Unknown header {header}"); | |
} | |
var builder = ImmutableArray.CreateBuilder<Candle>(); | |
while (!reader.EndOfStream) | |
{ | |
var line = await reader.ReadLineAsync().ConfigureAwait(false) ?? throw new FormatException("Null line"); | |
var parts = line.Split(',', StringSplitOptions.RemoveEmptyEntries); | |
if (parts.Length != 6) | |
{ | |
throw new FormatException("Illegal CSV"); | |
} | |
builder.Add( | |
new Candle( | |
time: ReadDate(parts[0]), | |
open: float.Parse(parts[1], CultureInfo.InvariantCulture), | |
high: float.Parse(parts[2], CultureInfo.InvariantCulture), | |
low: float.Parse(parts[3], CultureInfo.InvariantCulture), | |
close: float.Parse(parts[4], CultureInfo.InvariantCulture), | |
volume: int.Parse(parts[5], CultureInfo.InvariantCulture))); | |
} | |
return builder.ToImmutable(); | |
static DateTimeOffset ReadDate(string text) | |
{ | |
return text switch | |
{ | |
{ Length: 10 } => DateTimeOffset.ParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), | |
{ Length: 19 } => DateTimeOffset.ParseExact(text, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), | |
_ => throw new FormatException($"Unknown date format {text}"), | |
}; | |
} | |
} | |
internal static async Task<ImmutableArray<AdjustedCandle>> ParseAdjustedCandlesAsync(Stream content, Encoding encoding) | |
{ | |
using var reader = new CsvReader(content, encoding); | |
var header = await reader.ReadLineAsync().ConfigureAwait(false); | |
if (header != "timestamp,open,high,low,close,adjusted_close,volume,dividend_amount,split_coefficient") | |
{ | |
throw new FormatException($"Unknown header {header}"); | |
} | |
var builder = ImmutableArray.CreateBuilder<AdjustedCandle>(); | |
while (!reader.EndOfStream) | |
{ | |
var line = await reader.ReadLineAsync().ConfigureAwait(false) ?? throw new FormatException("Null line"); | |
var parts = line.Split(',', StringSplitOptions.RemoveEmptyEntries); | |
if (parts.Length != 9) | |
{ | |
throw new FormatException("Illegal CSV"); | |
} | |
builder.Add( | |
new AdjustedCandle( | |
time: ReadDate(parts[0]), | |
open: float.Parse(parts[1], CultureInfo.InvariantCulture), | |
high: float.Parse(parts[2], CultureInfo.InvariantCulture), | |
low: float.Parse(parts[3], CultureInfo.InvariantCulture), | |
close: float.Parse(parts[4], CultureInfo.InvariantCulture), | |
adjustedClose: float.Parse(parts[5], CultureInfo.InvariantCulture), | |
volume: int.Parse(parts[6], CultureInfo.InvariantCulture), | |
dividend: float.Parse(parts[7], CultureInfo.InvariantCulture), | |
splitCoefficient: float.Parse(parts[8], CultureInfo.InvariantCulture))); | |
} | |
return builder.ToImmutable(); | |
static DateTimeOffset ReadDate(string text) | |
{ | |
return text switch | |
{ | |
{ Length: 10 } => DateTimeOffset.ParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), | |
{ Length: 19 } => DateTimeOffset.ParseExact(text, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), | |
_ => throw new FormatException($"Unknown date format {text}"), | |
}; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Snappy | |
{ | |
using System; | |
using System.IO; | |
using System.Text; | |
using System.Threading.Tasks; | |
internal sealed class CsvReader : IDisposable | |
{ | |
private readonly StreamReader reader; | |
private bool disposed; | |
internal CsvReader(Stream stream, Encoding encoding) | |
{ | |
this.reader = new StreamReader(stream, encoding); | |
} | |
internal bool EndOfStream => this.reader.EndOfStream; | |
public void Dispose() | |
{ | |
if (this.disposed) | |
{ | |
return; | |
} | |
this.disposed = true; | |
this.reader.Dispose(); | |
} | |
internal Task<string?> ReadLineAsync() => this.reader.ReadLineAsync(); | |
private void ThrowIfDisposed() | |
{ | |
if (this.disposed) | |
{ | |
throw new ObjectDisposedException(nameof(CsvReader)); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Snappy | |
{ | |
using System; | |
using System.Collections.Immutable; | |
using System.Net.Http; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
internal static class HttpClientExtensions | |
{ | |
internal static async Task<ImmutableArray<Candle>> GetCandlesFromCsvAsync(this HttpClient client, Uri requestUri, CancellationToken cancellationToken = default) | |
{ | |
using var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
var encoding = GetEncoding(response.Content.Headers.ContentType?.CharSet); | |
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); | |
return await Csv.ParseCandlesAsync(content, encoding).ConfigureAwait(false); | |
} | |
internal static async Task<ImmutableArray<AdjustedCandle>> GetAdjustedCandleFromCsvAsync(this HttpClient client, Uri requestUri, CancellationToken cancellationToken = default) | |
{ | |
using var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
var encoding = GetEncoding(response.Content.Headers.ContentType?.CharSet); | |
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); | |
return await Csv.ParseAdjustedCandlesAsync(content, encoding).ConfigureAwait(false); | |
} | |
internal static Encoding GetEncoding(string? charset) | |
{ | |
if (charset is null) | |
{ | |
return Encoding.UTF8; | |
} | |
// Remove at most a single set of quotes. | |
if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') | |
{ | |
return Encoding.GetEncoding(charset[1..^2]); | |
} | |
else | |
{ | |
return Encoding.GetEncoding(charset); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment