mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
feat(stocks): add insider transaction tracking to stock analysis panel (#2928)
* feat(stocks): add insider transaction tracking to stock analysis panel Shows 6-month insider buy/sell activity from Finnhub: total buys, sells, net value, and recent named-exec transactions. Gracefully skips when FINNHUB_API_KEY is unavailable. * fix: add cache tier entry for get-insider-transactions route * fix(stocks): add insider RPC to premium paths + fix empty/stale states * fix(stocks): add insider RPC to premium paths + fix empty/stale states - Add /api/market/v1/get-insider-transactions to PREMIUM_RPC_PATHS - Return unavailable:false with empty transactions when Finnhub has no data (panel shows "No insider transactions" instead of "unavailable") - Mark stale insider data on refresh failures to avoid showing outdated info - Update test to match new empty-data behavior * fix(stocks): unblock stock-analysis render and surface exercise-only insider activity - loadStockAnalysis no longer awaits loadInsiderDataForPanel before panel.renderAnalyses. The insider fetch now fires in parallel after the primary render at both the cached-snapshot and live-fetch call sites. When insider data arrives, loadInsiderDataForPanel re-renders the panel so the section fills in asynchronously without holding up the analyst report on a secondary Finnhub RPC. - Add transaction code 'M' (exercise / conversion of derivative) to the allowed set in get-insider-transactions so symbols whose only recent Form 4 activity is option/RSU exercises no longer appear as "No insider transactions in the last 6 months". Exercises do not contribute to buys/sells dollar totals because transactionPrice is the strike price, not a market transaction. - Panel table now uses a neutral (dim) color for non-buy/non-sell rows (M rows) instead of the buy/sell green/red binary. - Tests cover: exercise-only activity producing non-empty transactions with zero buys/sells, and blended P/S/M activity preserving all three rows. * fix(stocks): prevent cached insider fetch from clobbering live render - Cached-path insider enrichment only runs when no live fetch is coming - Added generation counter to guard against concurrent loadStockAnalysis calls - Stale insider fetches now no-op instead of reverting panel state * fix(stocks): hide transient insider-unavailable flash and zero out strike-derived values - renderInsiderSection returns empty string when insider data is not yet fetched, so the transient "Insider data unavailable" card no longer flashes on initial render before the RPC completes - Exercise rows (code M) now carry value: 0 on the server and render a dash placeholder in the Value cell, matching how the buy/sell totals already exclude strike-derived dollar amounts * fix(stocks): exclude non-market Form 4 codes (A/D/F) from insider buy/sell totals Form 4 codes A (grant/award), D (disposition to issuer), and F (tax/exercise payment) are not open-market trades and should not drive insider conviction totals. Only P (open-market purchase) and S (open-market sale) now feed the buy/sell aggregates. A/D/F rows are still surfaced in the transaction list alongside M (exercise) with value zeroed out so the panel does not look empty.
This commit is contained in:
32
proto/worldmonitor/market/v1/get_insider_transactions.proto
Normal file
32
proto/worldmonitor/market/v1/get_insider_transactions.proto
Normal file
@@ -0,0 +1,32 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package worldmonitor.market.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
import "sebuf/http/annotations.proto";
|
||||
|
||||
message InsiderTransaction {
|
||||
string name = 1;
|
||||
int64 shares = 2 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER];
|
||||
double value = 3;
|
||||
string transaction_code = 4;
|
||||
string transaction_date = 5;
|
||||
}
|
||||
|
||||
message GetInsiderTransactionsRequest {
|
||||
string symbol = 1 [(sebuf.http.query) = { name: "symbol" },
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 32
|
||||
];
|
||||
}
|
||||
|
||||
message GetInsiderTransactionsResponse {
|
||||
bool unavailable = 1;
|
||||
string symbol = 2;
|
||||
double total_buys = 3;
|
||||
double total_sells = 4;
|
||||
double net_value = 5;
|
||||
repeated InsiderTransaction transactions = 6;
|
||||
string fetched_at = 7;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import "worldmonitor/market/v1/list_other_tokens.proto";
|
||||
import "worldmonitor/market/v1/get_fear_greed_index.proto";
|
||||
import "worldmonitor/market/v1/list_earnings_calendar.proto";
|
||||
import "worldmonitor/market/v1/get_cot_positioning.proto";
|
||||
import "worldmonitor/market/v1/get_insider_transactions.proto";
|
||||
|
||||
// MarketService provides APIs for financial market data from Finnhub, Yahoo Finance, and CoinGecko.
|
||||
service MarketService {
|
||||
@@ -121,4 +122,9 @@ service MarketService {
|
||||
rpc GetCotPositioning(GetCotPositioningRequest) returns (GetCotPositioningResponse) {
|
||||
option (sebuf.http.config) = {path: "/get-cot-positioning", method: HTTP_METHOD_GET};
|
||||
}
|
||||
|
||||
// GetInsiderTransactions retrieves SEC insider buy/sell activity from Finnhub.
|
||||
rpc GetInsiderTransactions(GetInsiderTransactionsRequest) returns (GetInsiderTransactionsResponse) {
|
||||
option (sebuf.http.config) = {path: "/get-insider-transactions", method: HTTP_METHOD_GET};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user