WorkWingman — Equity Compensation Valuation (Technical)¶
Informational only — not financial or investment advice. Current price is not a guarantee of realized value; past volatility does not predict future movement. Consult a licensed advisor. This disclaimer is carried on every
EquityValuationresponse (EquityValuation.Disclaimer), on the model doc comments, and inline on every UI surface that shows a number from this feature.
Many job offers include RSUs or stock options. This feature helps you see the current market value, the vesting timeline, and the historical volatility of that equity — factual data only. It never recommends, never predicts, and never says an offer or a security is "good" or "bad."
Hard rules¶
- No investment advice, no recommendations, no predictions. Nothing in this feature ever outputs "this is a good/bad equity package," "take/skip this offer," or a forward-looking price forecast.
HistoricalVolatilityPctis backward-looking only. It's the annualized standard deviation of past daily log-returns — a factual description of what already happened, never a forecast.- No Robinhood, no brokerage-account APIs. See "Sources evaluated" below for the full verdict.
- Local-only, like all profile data. The equity grant profile never leaves the machine. The only network calls this feature makes are read-only, factual stock-price lookups (no authentication, no personal brokerage credentials, ever).
- Never fabricates a price. A private company with no user-supplied 409A/last-round price, or
a public ticker with no reachable quote, returns
EquityValuation.UnpriceableReasoninstead of a number.
Sources evaluated¶
| Source | Verdict | Why |
|---|---|---|
Stooq (StooqSource) |
Used — primary, free | Public CSV export endpoints (stooq.com/q/l/… for a current quote, stooq.com/q/d/l/… for daily history), no API key, no sign-up, long-used by open-source/hobbyist tooling (pandas_datareader, QuantStart) for personal/non-commercial use. Operational caveat, confirmed live at the time this was written: Stooq currently fronts these endpoints with a JavaScript proof-of-work bot-challenge that a server-side .NET HTTP client cannot solve — so real-world calls from StooqSource are expected to fail closed (return null/empty) until Stooq's export is reachable without a browser again. That failure is silent and graceful by design: StockQuoteService falls through to the next configured source exactly like it would for any other outage. The adapter and its CSV-parsing contract are fully implemented and tested against fixture CSV bodies; only live reachability is currently blocked upstream. |
Alpha Vantage (AlphaVantageSource) |
Used — secondary, config-gated, confirmed live | Free-tier REST API, requires a free key from alphavantage.co/support/#api-key. GLOBAL_QUOTE for current price, TIME_SERIES_DAILY for history — confirmed working during research for this feature. Free tier is limited (roughly 25 requests/day, 5/minute), so this is a secondary/fallback source, never primary. Reads the key from the ALPHA_VANTAGE_API_KEY environment variable; with no key set, IsConfigured is false and the adapter is skipped entirely — no call, no error. |
Barchart OnDemand (BarchartSource) |
Config-stub only — paid | Barchart's quote API (getQuote.json) is a commercial, paid product — pricing is quotation-based, contact-form gated, no public self-serve tier. This adapter exists so the shape is ready if a paid key is ever supplied (reads BARCHART_API_KEY from the environment), but WorkWingman ships with no Barchart key, makes no Barchart calls by default, and does not depend on it. Historical-EOD lookups aren't wired for this stub (GetHistoryAsync returns empty) since a full historical entitlement is a separate paid tier. |
| Robinhood | Excluded — no legal path | Robinhood has no official public market-data API. Its only public API surface is a separate Crypto Trading API; stocks/options have no supported public endpoint. Unofficial libraries (robin_stocks and similar) work by reverse-engineering Robinhood's private, internal, authenticated app endpoints — which requires the user's own brokerage login/session credentials. That violates Robinhood's Terms of Service for unauthorized/automated access, risks account suspension, and hands brokerage credentials to third-party code for a feature (current price only) that Stooq/Alpha Vantage already serve legitimately and without any credential risk. WorkWingman does not build on this and never will. |
Data flow¶
EquityGrant (profile, optional)
│
▼
IStockQuoteService.GetQuoteAsync/GetHistoryAsync(ticker)
│ tries each IStockQuoteSourceAdapter in DI registration order:
│ Stooq → AlphaVantage → Barchart
│ (skips any adapter where IsConfigured == false; degrades past any
│ adapter that throws or returns null/empty — never blocks the rest)
▼
StockQuote? / IReadOnlyList<DailyPricePoint>
│
▼
IEquityValuationService.Value(grant, quote, history) ← pure, deterministic, no network
│
▼
EquityValuation { CurrentGrantValue, VestedValueToDate, UnvestedValue,
PerYearVestSchedule[], HistoricalVolatilityPct?, Disclaimer, DataVintage }
Models¶
EquityGrant (src/WorkWingman.Core/Models/EquityGrant.cs) —
the optional profile input, every field nullable/defaulted:
public class EquityGrant
{
public string? Ticker { get; set; }
public bool IsPublic { get; set; } = true;
public EquityGrantType GrantType { get; set; } = EquityGrantType.Unknown; // Unknown | Rsu | Iso | Nso
public decimal? ShareCount { get; set; }
public decimal? StrikePrice { get; set; } // options only
public decimal? GrantValueAtOffer { get; set; } // the offer letter's own quoted number, informational
public int VestingYears { get; set; } = 4;
public int CliffMonths { get; set; } = 12;
public decimal? PrivateSharePrice { get; set; } // user-supplied 409A/last-round price
public DateOnly? GrantDate { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
StockQuote / DailyPricePoint (src/WorkWingman.Core/Models/StockQuote.cs):
public class StockQuote
{
public string Ticker { get; set; }
public decimal CurrentPrice { get; set; }
public DateTimeOffset AsOf { get; set; }
public StockQuoteSource Source { get; set; } // Stooq | AlphaVantage | Barchart
public string SourceUrl { get; set; }
}
public class DailyPricePoint
{
public DateOnly Date { get; set; }
public decimal Close { get; set; }
}
EquityValuation (src/WorkWingman.Core/Models/EquityValuation.cs) —
the pure-math output:
public class EquityValuation
{
public const string StandardDisclaimer =
"Informational only — not financial or investment advice. Current price is not a " +
"guarantee of realized value; past volatility does not predict future movement. " +
"Consult a licensed advisor.";
public decimal? CurrentGrantValue { get; set; }
public decimal? VestedValueToDate { get; set; }
public decimal? UnvestedValue { get; set; }
public List<VestSchedulePoint> PerYearVestSchedule { get; set; } = [];
public double? HistoricalVolatilityPct { get; set; } // annualized stddev of daily log-returns — BACKWARD-LOOKING
public int? VolatilitySampleDays { get; set; }
public string? UnpriceableReason { get; set; }
public string Disclaimer { get; set; } = StandardDisclaimer;
public DateTimeOffset? DataVintage { get; set; }
}
Valuation math (EquityValuationService)¶
Pure, deterministic, no network — src/WorkWingman.Infrastructure/Equity/EquityValuationService.cs.
- RSUs are valued at the full current share price.
- Options (ISO/NSO) are valued at intrinsic value only:
max(0, currentPrice - strikePrice) * shareCount. Time value (the Black-Scholes premium for time-to-expiry/volatility) is deliberately ignored — every surface that shows an option's value notes this. - Vesting follows the standard cliff-then-linear-monthly shape: zero vested before
CliffMonths, then equal monthly tranches up to 100% atVestingYears * 12months fromGrantDate.PerYearVestSchedulereports cumulative shares/value at each year anniversary;VestedValueToDateinterpolates the exact fraction elapsed as of today (not just the last full year boundary). - Private companies: if
IsPublicisfalse, the grant is valued againstPrivateSharePriceif the user supplied one; otherwiseUnpriceableReasonis set to"Unpriceable (private company) — no 409A/last-round price on file."— never a fabricated number. - Public companies with no reachable quote:
UnpriceableReasonis set to"Unpriceable — no current market quote available for this ticker." - Historical volatility: annualized standard deviation of daily log-returns
(
ln(close[t] / close[t-1])) over the supplied price history, annualized via the conventionalsqrt(252)trading-days-per-year factor, expressed as a percent. Requires at least 2 valid consecutive closes; fewer leavesHistoricalVolatilityPctnull. This number describes what the security's price did in the past — it is never framed as, and must never be used as, a forecast.
Source adapters (Infrastructure/Equity/)¶
public interface IStockQuoteSourceAdapter
{
StockQuoteSource Source { get; }
bool IsConfigured { get; } // config-stub adapters report false with no key present
Task<StockQuote?> GetQuoteAsync(string ticker, CancellationToken ct = default);
Task<IReadOnlyList<DailyPricePoint>> GetHistoryAsync(string ticker, CancellationToken ct = default);
}
StockQuoteService (src/WorkWingman.Infrastructure/Equity/StockQuoteService.cs)
tries every registered adapter in DI order, skips any with IsConfigured == false, and degrades
past any adapter that throws or returns null/empty — the same per-source try/catch graceful-
degradation pattern as CompanyLayoffService (see job-signals.md). Cancellation
propagates immediately; it is never swallowed by the per-source catch.
Wiring (Program.cs)¶
builder.Services.AddSingleton<IStockQuoteSourceAdapter, StooqSource>();
builder.Services.AddSingleton<IStockQuoteSourceAdapter, AlphaVantageSource>();
builder.Services.AddSingleton<IStockQuoteSourceAdapter, BarchartSource>();
builder.Services.AddSingleton<IStockQuoteService, StockQuoteService>();
builder.Services.AddSingleton<IEquityValuationService, EquityValuationService>();
API¶
GET/PUT /api/profile/equity— the optionalEquityGrantprofile section, following the sameProfileControllerpattern asintake/story(ownLocalJsonStorekey,"equity").GET /api/equity/valuation— computes and returns the currentEquityValuationfor the saved grant: looks up the ticker's quote + history (if public), then callsIEquityValuationService.Value(...).
The IEquityValuation integration seam¶
src/WorkWingman.Core/Interfaces/IEquityValuationService.cs
also defines a narrow seam:
public interface IEquityValuation
{
decimal? CurrentGrantValue { get; }
bool IsPriced { get; }
string Disclaimer { get; }
}
EquityValuationAdapter (src/WorkWingman.Infrastructure/Equity/EquityValuationAdapter.cs)
is a thin projection of a full EquityValuation onto this seam. This is stub wiring only —
nothing constructs or registers it in DI today. It exists so feature-financial-profile's
JobImpactService can later fold equity into its total-comp comparison without taking a
dependency on this feature's richer types; the integration point is the seam interface, not a
concrete class reference.
Tests¶
StooqSourceTests.cs,AlphaVantageSourceTests.cs,BarchartSourceTests.cs— FlurlHttpTestfixture CSV/JSON, zero real network, zero real keys. Includes a case simulating Stooq's current bot-challenge HTML response to prove the graceful-degradation path.StockQuoteServiceTests.cs— priority-order fallthrough, unconfigured-source skipping, and per-source failure isolation against mockedIStockQuoteSourceAdapters.EquityValuationServiceTests.cs— exhaustive pure-math coverage: RSU valuation, option intrinsic value (including underwater/at- strike/null-strike edge cases), cliff/vesting-schedule math at multiple points in time, private- company unpriceable handling, and historical volatility (including zero-variance, out-of-order history, and skip-bad-data-point cases).ProfileServiceTests.cs— equity section round-trips through its ownLocalJsonStorekey, independent ofintake/story.- Bogus (
TestData.EquityFaker) generates realistic fixture grants for round-trip tests. - Stryker.NET:
**/Equity/*.csis added tostryker-config.json'smutatearray — it's a new folder, not covered by any pre-existing glob.
Related docs¶
- Plain-language version: ../plain/equity-comp.md
- Job-signal enrichment (the pattern this feature follows): job-signals.md
- Testing philosophy and tooling: testing.md