docs(stock): document dividend event buttons

This commit is contained in:
2026-07-21 18:20:14 +07:00
parent a739cdca06
commit 2ae3b505b6
3 changed files with 167 additions and 13 deletions
+37 -13
View File
@@ -47,20 +47,35 @@ Stock dividends are manual portfolio adjustments:
Ratios use `owned:new` exactly as written in the issuer notice. Equivalent
unreduced ratios are accepted and the entered ratio is preserved in the reply.
The bot validates syntax, tickers, and arithmetic safety, but does not look up
notices or prevent duplicate calls. The caller is responsible for verifying the
notice and avoiding accidental repeated adjustments.
The bot validates syntax, tickers, and arithmetic safety. `/stock_portfolio`
also checks SSI iBoard for cash and explicit share-dividend events published
since each holding's last successful check. The portfolio is always sent first;
each event then appears in its own message with an `Apply dividend` button. If
no relevant event exists, no additional message is sent.
Suggestions expire after 24 hours and are bound to the Telegram user who
requested the portfolio, the originating chat, and the event message. Another
group member cannot apply them. Acceptance calculates from the user's current
holding at click time, records the provider event atomically with the portfolio
change, and prevents the same SSI event from being applied twice. SSI iBoard is
an undocumented, best-effort source; failures do not prevent the portfolio from
being shown. The bot does not persist dated lots, so suggestions are not legal
record-date entitlement calculations. Users should verify the issuer notice.
The manual commands remain available, but they do not carry an SSI event ID.
Applying an event manually and then accepting its button can therefore record
the same dividend twice; use one method for a given event.
### Stock and coin P&L accounting
Stock and coin portfolios embed each open position under `assets.<symbol>`.
Both store `quantity` and total remaining `base`; stock positions additionally
store `dividendCheckedAt`. Stock cash is stored directly as `vnd`; coin cash
remains `usd`. Buys add their actual spend. Partial sells remove basis using the
weighted-average method and report realized P&L; full sells remove the position
and its basis. Stock share dividends add shares without adding cost, which
lowers the derived average price, while cash dividends do not change position
basis.
store `dividendCheckedAt` and an `openedAt` lifecycle marker. Stock cash is
stored directly as `vnd`; coin cash remains `usd`. Buys add their actual spend.
Partial sells remove basis using the weighted-average method and report realized
P&L; full sells remove the position and its basis. Stock share dividends add
shares without adding cost, which lowers the derived average price, while cash
dividends do not change position basis.
`/stock_portfolio` and `/coin_portfolio` show aligned monospace tables with
average entry price and unrealized P&L for each priced position. `Account P&L` remains the broader
@@ -68,11 +83,20 @@ account value minus all top-ups, so it also reflects realized proceeds,
dividend cash, and idle cash. If any current quote is unavailable, totals are
marked partial and numeric Account P&L is withheld.
For stock positions, `dividendCheckedAt` is a future dividend-event cursor. It
is initialized when a position is first bought, preserved across later buys and
sells, and advanced when a stock dividend is recorded. A full exit removes the
For stock positions, `dividendCheckedAt` is the dividend-event discovery cursor.
It is initialized when a position is first bought, preserved across later buys
and sells, and advanced after a successful event check or when a manual stock
dividend is recorded. Failed checks do not advance it. A full exit removes the
cursor; reopening the position starts it again. Coin positions do not store a
dividend cursor.
dividend cursor. Applied SSI event identities are retained in the stock
portfolio as `appliedDividendEvents.<hashed_provider_event_id> =
<applied_at_unix_milliseconds>` for idempotency and audit history. SSI queries
overlap the previous Asia/Saigon calendar day to avoid missing provider rows
whose publication time has only day precision; pending and applied provider IDs
suppress duplicate suggestions. The stock-only `assets.<ticker>.openedAt`
marker identifies the current position lifecycle and invalidates suggestion
buttons after a full sale and later repurchase. Existing positions adopt this
behavior without a startup migration.
## Layout
@@ -0,0 +1,73 @@
---
type: technical-journal
topic: stock-dividend-event-buttons
conducted_at: 2026-07-21T18:15:00+07:00
status: complete
---
# Stock Dividend Event Buttons Journal
## Context
`/stock_portfolio` needed to surface recent dividend events for held tickers
without obscuring the portfolio or silently changing financial state. SSI
iBoard was selected as a best-effort discovery source, while the existing
manual dividend commands remain available.
## What Happened
- The portfolio response is sent first. Each discovered cash or explicit share
dividend follows in its own message with an `Apply dividend` button; a ticker
with no relevant event produces no extra message.
- Added a replaceable SSI provider with pagination, local event classification,
stable event IDs, deduplication, and exact ratio handling.
- Added callback registration to the shared module framework and persisted
short-lived pending actions separately from portfolios.
- Successful checks advance `dividendCheckedAt`; failed checks leave the cursor
unchanged and never prevent the portfolio from being displayed.
- Added `openedAt` to distinguish the holding lifecycle and retained applied SSI
event identities for auditability and replay protection.
SSI discovery accepts only day-granularity date windows. Queries therefore use
a cursor overlap at the lower boundary, then locally filter and deduplicate the
returned events. This avoids missing events around a date boundary while stable
SSI event IDs prevent overlap from producing repeated actionable suggestions.
## Key Decisions
- Callback data carries an opaque random token, not trusted dividend values.
Pending actions expire after 24 hours and are bound to the requesting
Telegram user, originating chat, and event message.
- A callback reloads the current portfolio and calculates from the holding at
click time. Another group member, an expired action, or a holding sold and
reopened after suggestion creation cannot apply it.
- Applied SSI event IDs are recorded atomically with the portfolio mutation, so
duplicate clicks and retries cannot apply the same provider event twice.
- Event fetches occur outside the per-user mutation lock. Cursor persistence
reloads and merges under that lock, preserving concurrent buys, sells, and
manual dividends without holding a lock across network calls.
- Notification or provider failures do not advance the affected cursor. A later
portfolio request can safely retry rather than lose an event.
## Verification
- Focused stock and module tests passed.
- Full suite passed: `go test -count=1 ./...`.
- Stock race tests and 20-iteration concurrency stress tests passed.
- `go vet ./...` and `go build ./...` passed.
- `golangci-lint run` completed with zero issues.
- Independent testing, debugging, and code review found no blockers.
## Risks and Limitations
- SSI iBoard is undocumented and has no published stability or availability
guarantee; the provider can require replacement if its contract changes.
- The portfolio has no dated transaction lots, so current quantity is not proof
of record-date entitlement. Users must verify the issuer notice.
- Manual commands do not record an SSI event ID. Applying manually and later
accepting the related button can count the same dividend twice.
## Next
Observe SSI response stability and callback behavior in real use. Commit these
changes only if the user requests it.
@@ -0,0 +1,57 @@
---
type: project-status-report
topic: stock-dividend-event-buttons
reported_at: 2026-07-21T18:15:00+07:00
status: completed
---
# Stock Dividend Event Buttons Completion
## Summary
`/stock_portfolio` now checks SSI dividend events for held tickers after sending
the portfolio. Relevant events receive owner-bound, expiring Apply buttons;
successful checks with no events remain silent.
## Completed
- [x] Replaceable SSI provider with pagination, local classification, exact
share ratios, event-ID deduplication, and bounded date overlap.
- [x] Per-ticker cursor checks that preserve concurrent portfolio writes.
- [x] Opaque 24-hour pending actions bound to caller, chat, and message.
- [x] Current-holding cash/share application with replay-safe event ledger.
- [x] Position lifecycle binding that rejects buttons after full sale/reopen.
- [x] Shared callback registration, visibility validation, and authorization.
- [x] README and regression tests updated.
## Verification
| Gate | Result |
|---|---|
| Focused stock/modules tests | Passed |
| Full `go test -count=1 ./...` | Passed |
| Stock race and concurrency stress tests | Passed |
| `go vet ./...` | Passed |
| `golangci-lint run` | Passed, 0 issues |
| `go build ./...` | Passed |
| Independent final review | Passed, no blockers |
## Plan Sync
No active phase plan maps to this incremental request. Existing stock-dividend
and cost-basis plans were already completed before this work and remain
unchanged. Session task tracking is fully complete.
## Remaining Risks
- SSI iBoard is undocumented and best-effort.
- SSI publication timestamps are day-granularity; a bounded overlap prevents
same-day loss, while pending actions and applied IDs suppress duplicates.
- The portfolio has no dated-lot ledger, so acceptance uses holdings at click
time rather than legal record-date entitlement.
- Manual dividend commands cannot attach SSI event IDs; users must not apply
the same event both manually and through its button.
## Next Step
Commit only after explicit user approval.