Skip to content

Commit affc9ee

Browse files
committed
v0.4.0-rc.1: ExposureSummary typed POCO + 100% field-coverage integration test
- Add ExposureSummaryResponse + 5 sub-types (ExposureSummaryExposures, ExposureSummaryInterpretation, ExposureSummaryHedgingMove, ExposureSummaryHedgingEstimate, ExposureSummaryZeroDte) mirroring the live SDK's pattern (ZeroDteResponse). Field-level nullability aligned with C#/Go/Java defensive convention. - Integration test now references every leaf field declared in the POCO. - Bump version to 0.4.0-rc.1 (PEP 440: 0.4.0rc1; npm/NuGet: 0.4.0-rc.1; Maven: 0.4.0-RC1). API behaviour confirmed via live + historical probes: - regime ∈ {positive_gamma, negative_gamma, neutral, undetermined} (`neutral` was previously missing from the type set). - direction is lowercase "buy"/"sell" on BOTH summary and zero-dte (live API doc shows uppercase but actual response is lowercase). - as_of_requested exists on /exposure/{gex,dex,narrative} but NOT on /exposure/summary; removed from this type.
1 parent bdd9fc2 commit affc9ee

3 files changed

Lines changed: 155 additions & 3 deletions

File tree

src/FlashAlpha.Historical/FlashAlpha.Historical.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<RootNamespace>FlashAlpha.Historical</RootNamespace>
1010

1111
<PackageId>FlashAlpha.Historical</PackageId>
12-
<Version>0.1.0</Version>
12+
<Version>0.4.0-rc.1</Version>
1313
<Authors>FlashAlpha</Authors>
1414
<Company>FlashAlpha</Company>
1515
<Description>Official .NET SDK for the FlashAlpha Historical API — point-in-time replay of GEX, gamma flip, VRP, narrative, max pain, 0DTE, and the full stock summary at any minute back to 2018-04-16. Backtest options strategies against 6.7B+ option rows.</Description>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace FlashAlpha.Historical.Models;
4+
5+
/// <summary>
6+
/// Typed response model for <c>GET /v1/exposure/summary/{symbol}?at=...</c>.
7+
///
8+
/// <para>The Historical API returns the same response shape as the live API;
9+
/// the only difference is every analytics endpoint requires an <c>at</c> query
10+
/// parameter (string or <see cref="System.DateTime"/>).</para>
11+
///
12+
/// <para><b>Direction casing:</b> confirmed via live probe — both
13+
/// <c>/v1/exposure/summary/</c> and <c>/v1/exposure/zero-dte/</c> return
14+
/// lowercase <c>"buy"</c>/<c>"sell"</c>. Casing is consistent across
15+
/// summary and zero-DTE endpoints.</para>
16+
/// </summary>
17+
public sealed class ExposureSummaryResponse
18+
{
19+
[JsonPropertyName("symbol")]
20+
public string? Symbol { get; set; }
21+
22+
[JsonPropertyName("underlying_price")]
23+
public double? UnderlyingPrice { get; set; }
24+
25+
/// <summary>The as-of stamp the API actually used (snapped to the available minute).</summary>
26+
[JsonPropertyName("as_of")]
27+
public string? AsOf { get; set; }
28+
29+
// Note: as_of_requested exists on /v1/exposure/{gex,dex,narrative} but
30+
// NOT on /v1/exposure/summary. Don't add it to this type even though it
31+
// would be defensive — the field genuinely isn't returned for this endpoint.
32+
33+
[JsonPropertyName("gamma_flip")]
34+
public double? GammaFlip { get; set; }
35+
36+
/// <summary>
37+
/// One of <c>"positive_gamma"</c>, <c>"negative_gamma"</c>,
38+
/// <c>"neutral"</c>, or <c>"undetermined"</c>.
39+
/// </summary>
40+
[JsonPropertyName("regime")]
41+
public string? Regime { get; set; }
42+
43+
[JsonPropertyName("exposures")]
44+
public ExposureSummaryExposures? Exposures { get; set; }
45+
46+
[JsonPropertyName("interpretation")]
47+
public ExposureSummaryInterpretation? Interpretation { get; set; }
48+
49+
[JsonPropertyName("hedging_estimate")]
50+
public ExposureSummaryHedgingEstimate? HedgingEstimate { get; set; }
51+
52+
[JsonPropertyName("zero_dte")]
53+
public ExposureSummaryZeroDte? ZeroDte { get; set; }
54+
}
55+
56+
/// <summary>Net exposure totals across the entire chain.</summary>
57+
public sealed class ExposureSummaryExposures
58+
{
59+
[JsonPropertyName("net_gex")]
60+
public double? NetGex { get; set; }
61+
62+
[JsonPropertyName("net_dex")]
63+
public double? NetDex { get; set; }
64+
65+
[JsonPropertyName("net_vex")]
66+
public double? NetVex { get; set; }
67+
68+
[JsonPropertyName("net_chex")]
69+
public double? NetChex { get; set; }
70+
}
71+
72+
/// <summary>Verbal interpretation of the gamma/vanna/charm regimes.</summary>
73+
public sealed class ExposureSummaryInterpretation
74+
{
75+
[JsonPropertyName("gamma")]
76+
public string? Gamma { get; set; }
77+
78+
[JsonPropertyName("vanna")]
79+
public string? Vanna { get; set; }
80+
81+
[JsonPropertyName("charm")]
82+
public string? Charm { get; set; }
83+
}
84+
85+
/// <summary>One side (up or down) of a dealer-hedging estimate.</summary>
86+
public sealed class ExposureSummaryHedgingMove
87+
{
88+
[JsonPropertyName("dealer_shares_to_trade")]
89+
public double? DealerSharesToTrade { get; set; }
90+
91+
/// <summary>
92+
/// <c>"buy"</c> or <c>"sell"</c> (lowercase on both this endpoint and
93+
/// zero-dte).
94+
/// </summary>
95+
[JsonPropertyName("direction")]
96+
public string? Direction { get; set; }
97+
98+
[JsonPropertyName("notional_usd")]
99+
public double? NotionalUsd { get; set; }
100+
}
101+
102+
/// <summary>Estimated dealer hedging flow at +/- 1% spot moves.</summary>
103+
public sealed class ExposureSummaryHedgingEstimate
104+
{
105+
[JsonPropertyName("spot_up_1pct")]
106+
public ExposureSummaryHedgingMove? SpotUp1Pct { get; set; }
107+
108+
[JsonPropertyName("spot_down_1pct")]
109+
public ExposureSummaryHedgingMove? SpotDown1Pct { get; set; }
110+
}
111+
112+
/// <summary>Same-day-expiration contribution to total GEX.</summary>
113+
public sealed class ExposureSummaryZeroDte
114+
{
115+
[JsonPropertyName("net_gex")]
116+
public double? NetGex { get; set; }
117+
118+
[JsonPropertyName("pct_of_total_gex")]
119+
public double? PctOfTotalGex { get; set; }
120+
121+
[JsonPropertyName("expiration")]
122+
public string? Expiration { get; set; }
123+
}

tests/FlashAlpha.Historical.Tests/IntegrationTests.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class IntegrationTests
2121
private const double SpotTol = 1.0;
2222

2323
private static readonly HashSet<string> Regimes =
24-
new() { "positive_gamma", "negative_gamma", "transition" };
24+
new() { "positive_gamma", "negative_gamma", "neutral", "undetermined" };
2525

2626
private static readonly string? ApiKey = Environment.GetEnvironmentVariable("FLASHALPHA_API_KEY");
2727

@@ -125,22 +125,51 @@ public async Task OptionQuote_AllFilters_ReturnsSingleObjectWithGreeks()
125125
// ── Exposure ───────────────────────────────────────────────────────────
126126

127127
[SkippableFact]
128-
public async Task ExposureSummary_ShapeAndInvariants()
128+
public async Task ExposureSummary_EveryFieldDeclaredInPocoMustBeReferenced()
129129
{
130130
Skip.IfNot(HasKey, SkipReason);
131131
using var client = MakeClient();
132132
var s = await client.ExposureSummaryAsync("SPY", SpyAt);
133+
// ── top-level scalars ──
133134
Assert.Equal("SPY", s.GetProperty("symbol").GetString());
134135
Assert.True(Math.Abs(s.GetProperty("underlying_price").GetDouble() - ExpectedSpot) < SpotTol);
136+
Assert.Equal(JsonValueKind.String, s.GetProperty("as_of").ValueKind);
137+
Assert.False(string.IsNullOrEmpty(s.GetProperty("as_of").GetString()));
138+
Assert.Equal(SpyAt, s.GetProperty("as_of").GetString()); // historical snaps to requested minute
135139
Assert.Contains(s.GetProperty("regime").GetString()!, Regimes);
136140
Assert.Equal(JsonValueKind.Number, s.GetProperty("gamma_flip").ValueKind);
141+
// ── exposures block (4 fields) ──
137142
var e = s.GetProperty("exposures");
138143
foreach (var k in new[] { "net_gex", "net_dex", "net_vex", "net_chex" })
139144
Assert.Equal(JsonValueKind.Number, e.GetProperty(k).ValueKind);
145+
// ── interpretation block (3 fields) ──
146+
var interp = s.GetProperty("interpretation");
147+
foreach (var k in new[] { "gamma", "vanna", "charm" })
148+
{
149+
Assert.Equal(JsonValueKind.String, interp.GetProperty(k).ValueKind);
150+
Assert.False(string.IsNullOrEmpty(interp.GetProperty(k).GetString()));
151+
}
152+
// ── hedging_estimate (every leaf on both sides) ──
140153
var h = s.GetProperty("hedging_estimate");
154+
foreach (var sideKey in new[] { "spot_up_1pct", "spot_down_1pct" })
155+
{
156+
var side = h.GetProperty(sideKey);
157+
Assert.Contains(side.GetProperty("direction").GetString(), new[] { "buy", "sell" });
158+
Assert.Equal(JsonValueKind.Number, side.GetProperty("dealer_shares_to_trade").ValueKind);
159+
Assert.Equal(JsonValueKind.Number, side.GetProperty("notional_usd").ValueKind);
160+
Assert.NotEqual(0, side.GetProperty("notional_usd").GetInt64());
161+
}
141162
var up = h.GetProperty("spot_up_1pct").GetProperty("dealer_shares_to_trade").GetInt64();
142163
var down = h.GetProperty("spot_down_1pct").GetProperty("dealer_shares_to_trade").GetInt64();
143164
Assert.Equal(up, -down);
165+
// ── zero_dte block (3 fields) ──
166+
var z = s.GetProperty("zero_dte");
167+
Assert.True(z.TryGetProperty("net_gex", out var zng));
168+
Assert.True(zng.ValueKind == JsonValueKind.Null || zng.ValueKind == JsonValueKind.Number);
169+
Assert.True(z.TryGetProperty("pct_of_total_gex", out var zpct));
170+
Assert.True(zpct.ValueKind == JsonValueKind.Null || zpct.ValueKind == JsonValueKind.Number);
171+
Assert.True(z.TryGetProperty("expiration", out var zexp));
172+
Assert.True(zexp.ValueKind == JsonValueKind.Null || zexp.ValueKind == JsonValueKind.String);
144173
}
145174

146175
[SkippableFact]

0 commit comments

Comments
 (0)