Skip to content

Commit 83ed67e

Browse files
committed
Added interfaces for market holidays and market status
1 parent 6029122 commit 83ed67e

11 files changed

Lines changed: 330 additions & 3 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ The solution includes `src`, `tests`, and `utilities` as solution folders for ID
2323

2424
## Install
2525

26+
NuGet package: [FinnHubSharp](https://www.nuget.org/packages/FinnHubSharp/)
27+
2628
```bash
2729
dotnet add package FinnHubSharp
2830
```
@@ -60,6 +62,8 @@ Available REST methods:
6062
- `GetQuoteAsync(string symbol)`
6163
- `GetSymbolInfoAsync(string symbolOrSecurityName)`
6264
- `GetAllSymbolsAsync(string exchange)`
65+
- `GetMarketStatusAsync(string exchange)`
66+
- `GetMarketHolidaysAsync(string exchange)`
6367

6468
### Streaming Client
6569

src/FinnHubSharp/FinnHubSharp.csproj

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>netstandard2.1</TargetFramework>
55
<PackageId>FinnHubSharp</PackageId>
6-
<Version>1.0.3</Version>
6+
<Version>1.0.4</Version>
77
<Authors>Shravan Jambukesan</Authors>
88
<Company>Pseudo Markets</Company>
99
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@@ -12,13 +12,18 @@
1212
<PackageProjectUrl>https://github.com/pseudomarkets/FinnHubSharp</PackageProjectUrl>
1313
<PackageTags>FinnHub</PackageTags>
1414
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
1516
<PackageReleaseNotes>
16-
* Initial Release for nuget.org
17+
* Added interfaces for market status and market holidays
1718
</PackageReleaseNotes>
1819
</PropertyGroup>
1920

2021
<ItemGroup>
2122
<PackageReference Include="System.Text.Json" Version="10.0.0" />
2223
</ItemGroup>
2324

25+
<ItemGroup>
26+
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
27+
</ItemGroup>
28+
2429
</Project>

src/FinnHubSharp/Implementations/FinnHubClient.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,63 @@ public async Task<FinnHubListedSymbols> GetAllSymbolsAsync(string exchange)
115115

116116
return listedSymbols;
117117
}
118+
119+
public async Task<FinnHubMarketStatus> GetMarketStatusAsync(string exchange)
120+
{
121+
var marketStatus = new FinnHubMarketStatus();
122+
123+
try
124+
{
125+
var endpoint = $"{_baseUrl}/stock/market-status?exchange={exchange}&token={_apiKey}";
126+
var response = await _client.GetAsync(endpoint);
127+
128+
string responseString = await response.Content.ReadAsStringAsync();
129+
marketStatus.ResponseCode = (int)response.StatusCode;
130+
131+
if (!response.IsSuccessStatusCode)
132+
{
133+
marketStatus.ErrorMessage = responseString;
134+
return marketStatus;
135+
}
136+
137+
var jsonResponse = JsonSerializer.Deserialize<MarketStatus>(responseString);
138+
marketStatus.MarketStatus = jsonResponse;
139+
}
140+
catch (Exception e)
141+
{
142+
marketStatus.ErrorMessage = e.ToString();
143+
}
144+
145+
return marketStatus;
146+
}
147+
148+
public async Task<FinnHubMarketHoliday> GetMarketHolidaysAsync(string exchange)
149+
{
150+
var marketHoliday = new FinnHubMarketHoliday();
151+
152+
try
153+
{
154+
var endpoint = $"{_baseUrl}/stock/market-holiday?exchange={exchange}&token={_apiKey}";
155+
var response = await _client.GetAsync(endpoint);
156+
157+
string responseString = await response.Content.ReadAsStringAsync();
158+
marketHoliday.ResponseCode = (int)response.StatusCode;
159+
160+
if (!response.IsSuccessStatusCode)
161+
{
162+
marketHoliday.ErrorMessage = responseString;
163+
return marketHoliday;
164+
}
165+
166+
var jsonResponse = JsonSerializer.Deserialize<MarketHoliday>(responseString);
167+
marketHoliday.MarketHoliday = jsonResponse;
168+
}
169+
catch (Exception e)
170+
{
171+
marketHoliday.ErrorMessage = e.ToString();
172+
}
173+
174+
return marketHoliday;
175+
}
118176
}
119177
}

src/FinnHubSharp/Interfaces/IFinnHubClient.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ public interface IFinnHubClient
1010
Task<FinnHubSymbolInfo> GetSymbolInfoAsync(string symbolOrSecurityName);
1111

1212
Task<FinnHubListedSymbols> GetAllSymbolsAsync(string exchange);
13+
14+
Task<FinnHubMarketStatus> GetMarketStatusAsync(string exchange);
15+
16+
Task<FinnHubMarketHoliday> GetMarketHolidaysAsync(string exchange);
1317
}
14-
}
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using FinnHubSharp.Models.Response.Raw;
2+
3+
namespace FinnHubSharp.Models.Response.FinnHub
4+
{
5+
public class FinnHubMarketHoliday : FinnHubResponseBase
6+
{
7+
public MarketHoliday MarketHoliday { get; set; }
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using FinnHubSharp.Models.Response.Raw;
2+
3+
namespace FinnHubSharp.Models.Response.FinnHub
4+
{
5+
public class FinnHubMarketStatus : FinnHubResponseBase
6+
{
7+
public MarketStatus MarketStatus { get; set; }
8+
}
9+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace FinnHubSharp.Models.Response.Raw
5+
{
6+
public class MarketHoliday
7+
{
8+
[JsonPropertyName("exchange")]
9+
public string Exchange { get; set; }
10+
11+
[JsonPropertyName("data")]
12+
public List<MarketHolidayData> Data { get; set; }
13+
14+
[JsonPropertyName("timezone")]
15+
public string Timezone { get; set; }
16+
}
17+
18+
public class MarketHolidayData
19+
{
20+
[JsonPropertyName("eventName")]
21+
public string EventName { get; set; }
22+
23+
[JsonPropertyName("atDate")]
24+
public string AtDate { get; set; }
25+
26+
[JsonPropertyName("tradingHour")]
27+
public string TradingHour { get; set; }
28+
}
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace FinnHubSharp.Models.Response.Raw
4+
{
5+
public class MarketStatus
6+
{
7+
[JsonPropertyName("exchange")]
8+
public string Exchange { get; set; }
9+
10+
[JsonPropertyName("holiday")]
11+
public string Holiday { get; set; }
12+
13+
[JsonPropertyName("isOpen")]
14+
public bool IsOpen { get; set; }
15+
16+
[JsonPropertyName("session")]
17+
public string Session { get; set; }
18+
19+
[JsonPropertyName("state")]
20+
public string State { get; set; }
21+
22+
[JsonPropertyName("timezone")]
23+
public string Timezone { get; set; }
24+
25+
[JsonPropertyName("sessionOpen")]
26+
public string SessionOpen { get; set; }
27+
28+
[JsonPropertyName("sessionClose")]
29+
public string SessionClose { get; set; }
30+
31+
[JsonPropertyName("t")]
32+
public long Timestamp { get; set; }
33+
}
34+
}

tests/FinnHubSharp.Tests/FinnHubClientTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,119 @@ public async Task GetAllSymbolsAsync_WhenHttpClientThrows_CapturesExceptionMessa
257257
handler.VerifyAll();
258258
}
259259

260+
[Test]
261+
public async Task GetMarketStatusAsync_WhenResponseIsSuccessful_RequestsMarketStatusEndpointAndMapsResponse()
262+
{
263+
var mockHttp = new MockHttpMessageHandler();
264+
mockHttp.Expect(HttpMethod.Get, $"{BaseUrl}/stock/market-status?exchange=US&token={ApiKey}")
265+
.Respond("application/json", """
266+
{
267+
"exchange": "US",
268+
"holiday": "",
269+
"isOpen": true,
270+
"session": "regular",
271+
"state": "open",
272+
"timezone": "America/New_York",
273+
"sessionOpen": "2026-05-06 09:30:00",
274+
"sessionClose": "2026-05-06 16:00:00",
275+
"t": 1778088600
276+
}
277+
""");
278+
var client = CreateClient(mockHttp);
279+
280+
var result = await client.GetMarketStatusAsync("US");
281+
282+
result.ResponseCode.ShouldBe((int)HttpStatusCode.OK);
283+
result.ErrorMessage.ShouldBeNull();
284+
result.MarketStatus.ShouldNotBeNull();
285+
result.MarketStatus.Exchange.ShouldBe("US");
286+
result.MarketStatus.Holiday.ShouldBe("");
287+
result.MarketStatus.IsOpen.ShouldBeTrue();
288+
result.MarketStatus.Session.ShouldBe("regular");
289+
result.MarketStatus.State.ShouldBe("open");
290+
result.MarketStatus.Timezone.ShouldBe("America/New_York");
291+
result.MarketStatus.SessionOpen.ShouldBe("2026-05-06 09:30:00");
292+
result.MarketStatus.SessionClose.ShouldBe("2026-05-06 16:00:00");
293+
result.MarketStatus.Timestamp.ShouldBe(1778088600);
294+
mockHttp.VerifyNoOutstandingExpectation();
295+
}
296+
297+
[Test]
298+
public async Task GetMarketStatusAsync_WhenResponseIsUnsuccessful_SetsStatusCodeAndRawErrorMessage()
299+
{
300+
const string errorBody = """{"error":"Exchange not found"}""";
301+
var mockHttp = new MockHttpMessageHandler();
302+
mockHttp.Expect(HttpMethod.Get, $"{BaseUrl}/stock/market-status?exchange=NOPE&token={ApiKey}")
303+
.Respond(HttpStatusCode.BadRequest, "application/json", errorBody);
304+
var client = CreateClient(mockHttp);
305+
306+
var result = await client.GetMarketStatusAsync("NOPE");
307+
308+
result.ResponseCode.ShouldBe((int)HttpStatusCode.BadRequest);
309+
result.ErrorMessage.ShouldBe(errorBody);
310+
result.MarketStatus.ShouldBeNull();
311+
mockHttp.VerifyNoOutstandingExpectation();
312+
}
313+
314+
[Test]
315+
public async Task GetMarketHolidaysAsync_WhenResponseIsSuccessful_RequestsMarketHolidayEndpointAndMapsResponse()
316+
{
317+
var mockHttp = new MockHttpMessageHandler();
318+
mockHttp.Expect(HttpMethod.Get, $"{BaseUrl}/stock/market-holiday?exchange=US&token={ApiKey}")
319+
.Respond("application/json", """
320+
{
321+
"exchange": "US",
322+
"data": [
323+
{
324+
"eventName": "Memorial Day",
325+
"atDate": "2026-05-25",
326+
"tradingHour": ""
327+
},
328+
{
329+
"eventName": "Independence Day",
330+
"atDate": "2026-07-03",
331+
"tradingHour": "09:30-13:00"
332+
}
333+
],
334+
"timezone": "America/New_York"
335+
}
336+
""");
337+
var client = CreateClient(mockHttp);
338+
339+
var result = await client.GetMarketHolidaysAsync("US");
340+
341+
result.ResponseCode.ShouldBe((int)HttpStatusCode.OK);
342+
result.ErrorMessage.ShouldBeNull();
343+
result.MarketHoliday.ShouldNotBeNull();
344+
result.MarketHoliday.Exchange.ShouldBe("US");
345+
result.MarketHoliday.Timezone.ShouldBe("America/New_York");
346+
result.MarketHoliday.Data.Count.ShouldBe(2);
347+
result.MarketHoliday.Data[0].EventName.ShouldBe("Memorial Day");
348+
result.MarketHoliday.Data[0].AtDate.ShouldBe("2026-05-25");
349+
result.MarketHoliday.Data[0].TradingHour.ShouldBe("");
350+
result.MarketHoliday.Data[1].EventName.ShouldBe("Independence Day");
351+
result.MarketHoliday.Data[1].AtDate.ShouldBe("2026-07-03");
352+
result.MarketHoliday.Data[1].TradingHour.ShouldBe("09:30-13:00");
353+
mockHttp.VerifyNoOutstandingExpectation();
354+
}
355+
356+
[Test]
357+
public async Task GetMarketHolidaysAsync_WhenResponseIsUnsuccessful_SetsStatusCodeAndRawErrorMessage()
358+
{
359+
const string errorBody = """{"error":"Exchange not found"}""";
360+
var mockHttp = new MockHttpMessageHandler();
361+
mockHttp.Expect(HttpMethod.Get, $"{BaseUrl}/stock/market-holiday?exchange=NOPE&token={ApiKey}")
362+
.Respond(HttpStatusCode.BadRequest, "application/json", errorBody);
363+
var client = CreateClient(mockHttp);
364+
365+
var result = await client.GetMarketHolidaysAsync("NOPE");
366+
367+
result.ResponseCode.ShouldBe((int)HttpStatusCode.BadRequest);
368+
result.ErrorMessage.ShouldBe(errorBody);
369+
result.MarketHoliday.ShouldBeNull();
370+
mockHttp.VerifyNoOutstandingExpectation();
371+
}
372+
260373
private static FinnHubClient CreateClient(HttpMessageHandler handler) =>
261374
new(new HttpClient(handler), new FinnHubSharpConfiguration
262375
{

tests/FinnHubSharp.Tests/JsonSerializationTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,59 @@ public void StreamingQuote_DeserializesUsingFinnHubWireNames()
124124
data.Volume.ShouldBe(0.5);
125125
data.TradeCondition.ShouldBe("regular");
126126
}
127+
128+
[Test]
129+
public void MarketStatus_DeserializesUsingFinnHubWireNames()
130+
{
131+
var marketStatus = JsonSerializer.Deserialize<MarketStatus>("""
132+
{
133+
"exchange": "US",
134+
"holiday": "",
135+
"isOpen": true,
136+
"session": "regular",
137+
"state": "open",
138+
"timezone": "America/New_York",
139+
"sessionOpen": "2026-05-06 09:30:00",
140+
"sessionClose": "2026-05-06 16:00:00",
141+
"t": 1778088600
142+
}
143+
""");
144+
145+
marketStatus.ShouldNotBeNull();
146+
marketStatus.Exchange.ShouldBe("US");
147+
marketStatus.Holiday.ShouldBe("");
148+
marketStatus.IsOpen.ShouldBeTrue();
149+
marketStatus.Session.ShouldBe("regular");
150+
marketStatus.State.ShouldBe("open");
151+
marketStatus.Timezone.ShouldBe("America/New_York");
152+
marketStatus.SessionOpen.ShouldBe("2026-05-06 09:30:00");
153+
marketStatus.SessionClose.ShouldBe("2026-05-06 16:00:00");
154+
marketStatus.Timestamp.ShouldBe(1778088600);
155+
}
156+
157+
[Test]
158+
public void MarketHoliday_DeserializesUsingFinnHubWireNames()
159+
{
160+
var marketHoliday = JsonSerializer.Deserialize<MarketHoliday>("""
161+
{
162+
"exchange": "US",
163+
"data": [
164+
{
165+
"eventName": "Memorial Day",
166+
"atDate": "2026-05-25",
167+
"tradingHour": ""
168+
}
169+
],
170+
"timezone": "America/New_York"
171+
}
172+
""");
173+
174+
marketHoliday.ShouldNotBeNull();
175+
marketHoliday.Exchange.ShouldBe("US");
176+
marketHoliday.Timezone.ShouldBe("America/New_York");
177+
var holiday = marketHoliday.Data.ShouldHaveSingleItem();
178+
holiday.EventName.ShouldBe("Memorial Day");
179+
holiday.AtDate.ShouldBe("2026-05-25");
180+
holiday.TradingHour.ShouldBe("");
181+
}
127182
}

0 commit comments

Comments
 (0)