Skip to content

Instantly share code, notes, and snippets.

@JohanLarsson
Created April 7, 2021 15:41
Show Gist options
  • Save JohanLarsson/1735bd58821716232244d33deec7f7da to your computer and use it in GitHub Desktop.
Save JohanLarsson/1735bd58821716232244d33deec7f7da to your computer and use it in GitHub Desktop.
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));
}
}
}
}
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}"),
};
}
}
}
}
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));
}
}
}
}
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