Skip to content

DCA Sell Extension + kritické opravy z revize#82

Merged
Crynners merged 75 commits into
mainfrom
feature/dca-sell-extension
Jun 12, 2026
Merged

DCA Sell Extension + kritické opravy z revize#82
Crynners merged 75 commits into
mainfrom
feature/dca-sell-extension

Conversation

@nehasvit

Copy link
Copy Markdown
Collaborator

Co PR obsahuje

DCA Sell Extension

Kompletní podpora prodejů: limit/ladder sell wizard, sledování a rušení open orderů, realizované P&L a net P&L v portfoliu, notifikace na fill, filtr historie podle plánu, import transakcí per connection. Včetně JVM test harness (Robolectric + in-memory Room).

Hardening proti runaway buys

Původní fix (atomický claim + reconcile-before-retry + circuit breaker) rozšířen o zbývající vektory z hloubkové revize:

  • OkHttp retryOnConnectionFailure(false) - transport vrstva už nikdy potichu nepřepošle order POST (duplicitní nákup pod úrovní reconcile logiky)
  • Force-run cesty (Koupit nyní / catch-up) serializované přes unique work, jištěné circuit breakerem (s tolerancí pro user-initiated catch-up) a checkpointované v DB - replay po zabití procesu nenakoupí dávku znovu
  • Alarm work KEEP místo REPLACE - znovu vystřelený alarm nezruší worker uprostřed nákupu

Další opravy

  • Kraken market buy: odstraněný flag viqc (Kraken ho z API vyřadil - každý nákup selhal); objem se počítá z aktuální ceny v base měně s rezervou na fee
  • Biometrický zámek: oprava bypass přes saved instance state po zabití procesu + re-lock po 30 s v pozadí (rychlé přepnutí aplikací se neptá)
  • Graf portfolia: odstraněné buy/sell trojúhelníky (vizuální šum, při více akcích agregované do nečitelných shluků)

Testy

  • ./gradlew testDebugUnitTest - zelené
  • Ručně ověřeno na zařízení (SM-G970F, Android 12)

🤖 Generated with Claude Code

vitnehasil and others added 30 commits April 23, 2026 21:50
Opt-in trading mode rozsirujici DCA plany o limitni prodejni prikazy,
P&L tracking a volitelny cil zisku. Globalni settings gate + per-plan
allowSells flag. MVP support Coinmate + Binance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DB je aktualne na version 20 (po merge multi-connection-envelopes
byla zabrana migrace 19->20 pro plan name + displayOrder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
34 tasku v 9 fazich. Datovy model (migrace v20->v21), rozsireni
ExchangeApi (Coinmate + Binance limit sell), use cases, UI
(settings + plan detail + wizard + chart/history/portfolio).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…y and DcaPlanEntity

- TransactionSide enum (BUY, SELL) + TypeConverter
- TransactionEntity: side, limitPrice, requestedCryptoAmount (all nullable/defaulted)
- DcaPlanEntity: allowSells (default false), targetProfitAmount (nullable)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- TransactionSide enum (BUY/SELL) v domain layer + Converter
- TransactionEntity + Transaction: side, limitPrice, requestedCryptoAmount
- DcaPlanEntity + DcaPlan: allowSells, targetProfitAmount
- Room migration 20->21 pro nova pole + idx_tx_plan_side_status
- EntityMappers: toEntity/toDomain pro DcaPlan + Transaction
- DAO: getResolvablePendingTransactions (PENDING+PARTIAL),
  countOpenSells, observeOpenSellsForPlan, observeAllOpenSells,
  updateResolvedTransaction (guarded update pro race s cancelem)
- Backup models v3: allowSells, targetProfitAmount, side,
  limitPrice, requestedCryptoAmount (backward-kompatibilni)
- Backup collector + restorer rozsireni pro nova pole

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…elOrder hooks

Task 8 + 9 z Faze 2:
- novy data class OrderStatusResult zastupujici Transaction? ve vraceni getOrderStatus
- getOrderStatus dostava nove (orderId, crypto, fiat) - Binance potrebuje symbol
- pridany interface defaulty: limitSell, cancelOrder, supportsLimitSell
- CoinbaseApi.getOrderStatus a KrakenApi.getOrderStatus prepsany na novy tvar

ResolvePendingTransactionsUseCase zatim nebuildi - oprava je soucasti Faze 3.
…tatus

Task 10:
- supportsLimitSell = true
- limitSell: POST /sellLimit s amount/price/currencyPair, sdili stejny signing
  pattern jako marketBuy (clientId + publicKey + nonce + signature)
- cancelOrder: POST /cancelOrder s orderId
- getOrderStatus: POST /orderById, mapuje OPEN/FILLED/PARTIALLY_FILLED/CANCELLED/EXPIRED
  -> TransactionStatus, filledAmount = originalAmount - remainingAmount
- Coinmate nevraci aggregate fee na orderById, takze fee=null v OrderStatusResult
  (resolver muze fetchnout z tradeHistory pokud bude potreba)
…atus

Task 11:
- supportsLimitSell = true
- limitSell: POST /api/v3/order se side=SELL, type=LIMIT, timeInForce=GTC,
  quantity+price; sdili stejny signing pattern jako marketBuy
- cancelOrder: DELETE /api/v3/order s symbol+orderId
- getOrderStatus: GET /api/v3/order, mapuje NEW/PARTIALLY_FILLED/FILLED/CANCELED/
  EXPIRED/REJECTED/PENDING_* -> TransactionStatus
- avgFillPrice = cummulativeQuoteQty / executedQty (pokud > 0)
- Binance vraci fee per-fill, ne na order level; pro MVP vraci null v
  OrderStatusResult (dohledani pres /myTrades je volitelne v budoucnu)
…(Task 12)

Fix broken build: switch to getResolvablePendingTransactions() +
guarded updateResolvedTransaction() so the resolver handles both
PENDING/PARTIAL BUY and SELL transactions and never clobbers a
concurrent user cancel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PlaceLimitSellUseCase: places a limit sell via ExchangeApi, inserts
  PENDING SELL transaction, best-effort resolves for instant-fill case
- CancelSellOrderUseCase: cancels on exchange, marks local as PARTIAL
  if partially filled or FAILED otherwise; re-resolves on cancel failure
- PlanPnL model + CalculatePlanPnLUseCase: per-plan realized /
  unrealized / net PnL + target progress
- ValidateSellOrderUseCase: amount/limitPrice/min-order/available-crypto
  hard errors + instant-fill and far-from-market warnings
- Add suspend TransactionDao.getTransactionsByPlanSync for PnL +
  validation (existing Flow variant stays for UI consumers)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds persisted prefs for the upcoming sell extension: a master
isTradingEnabled switch (default off) and a group for periodic sell-order
polling (enabled flag, DcaFrequency with cron + schedule-config for CUSTOM).
All sell-polling fields are written atomically via setPeriodicSellPolling
to avoid readers seeing a half-applied state. Enum parsing falls back to
HOURLY on unknown names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a self-perpetuating one-shot WorkManager chain for resolving
PENDING sell orders in the background. The scheduler reads frequency and
cron from UserPreferences, computes the next fire (via CronUtils for
CUSTOM), and enqueues a unique work request with REPLACE policy. Worker
short-circuits when there are no open sells, then always reschedules
itself (even on failure) so transient errors don't stop the chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds lifecycle-process dep and hooks a ProcessLifecycleOwner observer in
AccBotApplication. On every app onStart we fire-and-forget a call to
ResolvePendingTransactionsUseCase from an app-scope SupervisorJob so users
see their filled sells reflect immediately when they open the app,
independent of the periodic worker cadence. Errors are logged and
swallowed - the use case is a no-op when there are no open sells.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add trading master switch (setTradingEnabled) that also cancels sell
  polling when turned off.
- Add background sell-order polling toggle with frequency picker
  (reuses FrequencyDropdown) and CUSTOM cron text field.
- Disabling trading atomically clears all sell-polling prefs and
  cancels the SellPollingScheduler to avoid orphan work.
- Settings strings localized for cs/en.

Note: For DAILY/WEEKLY/CUSTOM the worker relies on
DcaFrequency.intervalMinutes / cron (same as SellPollingScheduler
already handles). The richer visual schedule builder used by
AddPlanScreen is intentionally not reused here to keep the task
scoped - a future refactor can extract it as a shared composable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extend CreateDcaPlanUseCase with allowSells + targetProfitAmount
  params (both default off, opt-in only).
- Extend PlanFormDelegate state/setters with sell fields and validation
  (empty = no target, non-positive number rejected).
- PlanFormContent gains a gated "Prodeje (volitelne)" section rendered
  only when hosting screen passes showSellSection = true.
- AddPlanViewModel reads the global tradingEnabled snapshot from
  UserPreferences and passes it to the form; plan creation scopes
  allowSells with the global switch (double-gate belt-and-suspenders).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inject TransactionDao into EditPlanViewModel and snapshot
  tradingEnabled at construction.
- Load allowSells + targetProfitAmount from the plan entity into the
  shared PlanFormDelegate.
- Save now persists allowSells / targetProfitAmount; the persisted
  flag is AND-gated with the global trading switch as a safety net.
- Toggle-off guard: when user tries to disable allowSells while any
  open sell orders exist for the plan (observeOpenSellsForPlan count),
  show a confirmation dialog warning that orders remain on the exchange
  and must be cancelled manually.
- hasChanges now also tracks allowSells + targetProfitAmount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend PlanDetailsViewModel with planPnL/openSells/sellUiVisible flows and a
cancelSell action backed by CancelSellOrderUseCase. PnL is recomputed on each
transaction change and spot-price refresh; open sells come from the DAO flow
observeOpenSellsForPlan. sellUiVisible is gated by plan.allowSells, the global
trading switch and the exchange's supportsLimitSell capability.

Two new composables under plans/components/:
- PnLCard: held / avg buy / realized / unrealized / net rows with green/red
  coloring, plus optional target-profit progress bar.
- OpenSellsList: per-row pending/partial sell with confirm-dialog cancel.

Wired into PlanDetailsScreen between the exchange-balance card and the
transaction list, plus a "+ Vytvorit prodejni prikaz" button that launches
the wizard (implemented in Tasks 24-25).
New SellWizardViewModel backing a two-step limit-sell flow. init() loads the
plan, computes available crypto (held minus reservations from other open
sells), fetches a best-effort spot price and derives the avg buy price via
CalculatePlanPnLUseCase. Inputs trigger live revalidation through
ValidateSellOrderUseCase; minOrderSize is set per-exchange (Binance
LOT_SIZE, default 0.00001 otherwise).

SellWizardBottomSheet is a ModalBottomSheet with the INPUT step wired up:
- 25/50/75/100% amount chips
- Trzni / Breakeven / +10% / +25% price chips (disabled when inputs missing)
- live Ziskate + Zisk-vs-prum summary with green/red coloring
- validation banners (HardError / InstantFillInfo / FarFromMarketWarning)
- Pokracovat button enabled only when no hard errors and both fields parse

The CONFIRM step currently falls through to INPUT; Task 25 adds the real
confirm + submit UI.
Replace the placeholder confirm branch with SellConfirmStep: a read-only
summary (burza / plan / smer / mnozstvi / limitni cena / ziskate) plus a
warning banner, Zpet / Odeslat buttons and a loading indicator while
PlaceLimitSellUseCase runs. submit() applies the 15s timeout in
SellWizardViewModel; on timeout we show an AlertDialog telling the user to
verify on the exchange to avoid duplicate orders.
Add ChartTradeMarker model and an optional tradeMarkers parameter to
PortfolioLineChart so callers (per-plan / portfolio screens) can pass
BUY/SELL trade points through. Visual rendering on top of the Vico
CartesianChartHost is intentionally deferred - it requires custom
decoration components and an Instant -> chart-pixel x-coordinate solver
because Vico's x-axis is index-based (epochDay buckets), not time-based.
The API is wired now so a follow-up patch can drop in the renderer
without touching call sites.

DONE_WITH_CONCERNS per plan: marker rendering deferred.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add HistorySideFilter (ALL / BUYS / SELLS / PENDING) as a first-class
  row of chips at the top of HistoryScreen, persisted via HistoryFilter
  in the ViewModel. PENDING matches status IN (PENDING, PARTIAL).
- TransactionCard now shows a TrendingUp (red) / TrendingDown (green)
  badge next to the status icon based on transaction side.
- Amount signs flip for SELL: crypto -, fiat +. Colors match direction.
- clearFilter preserves the side chip since it's a persistent mode, not
  a one-off filter like crypto/exchange/date.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- TransactionDetailsViewModel now injects CancelSellOrderUseCase and
  exposes cancelOrder(txId) with snackbar error reporting and an
  isCancelling guard to prevent double-taps.
- TransactionDetailsScreen renders a new SellDetailsCard for SELL
  transactions showing limit price, fill progress (X / Y crypto, %),
  and avg fill price when any amount has been filled.
- For PENDING / PARTIAL sells the screen also shows a red Cancel order
  button gated by a confirmation AlertDialog. Localized CS/EN strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DAO:
- Filter all aggregate "invested / accumulated" SUM queries to side='BUY'
  so existing dashboard / portfolio / chart totals don't get inflated by
  SELL transactions. SELL fiatAmount is reported separately as realized.
- Add getRealizedFiatByFiat(fiat) and getRealizedFiatByPlan(planId) for
  the new portfolio summary rows. PARTIAL is included because partial
  fills have already booked their fiatAmount.

PortfolioViewModel:
- Add showTradingMetrics (gated by UserPreferences.isTradingEnabled),
  totalRealized, and netPnL state. recomputeTradingMetrics runs after
  each chart load and uses planId scope on per-plan pages and fiat scope
  on aggregate pages.

PortfolioScreen:
- TradingMetricsRows composable rendering "Realizováno" and "Čistý P&L"
  rows under the existing KPI grid, in both portrait and landscape
  variants. Hidden when trading is off or there are no realized sells
  (avoids showing 0 rows by default).

DONE_WITH_CONCERNS scope per plan: charts and per-plan series remain
BUY-only. Realized P&L is surfaced in summary text rows only, no chart
overlay. The sell-extension chart series can be added later without
touching the existing pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DashboardViewModel:
- Add openSellsByPlan StateFlow fed by transactionDao.observeAllOpenSells.
  Subscription only starts when global trading is enabled, so users without
  the feature opt-in never see the cards.
- Reactive: cards update when an order fills, the user cancels, or a new
  sell wizard submits a fresh order.

DashboardScreen:
- New OpenSellsSummaryCard composable showing "{plan name}: N open sell
  order(s)" + a preview of the most recent sell ({amount} {crypto} @
  {price} {fiat}). Tapping deep-links to plan detail.
- Wired into both portrait and landscape layouts between Holdings and
  Market Pulse. Hidden when the map is empty (natural gating).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FilterBottomSheet Apply button was constructing a fresh HistoryFilter
from scratch, which silently reset the new BUY/SELL/PENDING side chip to
ALL every time the user touched any other filter. Using currentFilter.copy
keeps the chip selection intact across filter-sheet applies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PlanDetailsViewModel.deletePlan now checks observeOpenSellsForPlan
before issuing the cascade delete. When open sells exist, it surfaces
deleteBlockedOpenSells in UI state instead of dropping the plan; the
screen shows an AlertDialog telling the user to cancel the orders on
the exchange first. Without this guard the FK link to the order rows
would be lost and subsequent fill polling would silently fail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Material3 PullToRefreshBox wraps the LazyColumn. Pull triggers
ResolvePendingTransactionsUseCase to poll exchange for fill status of
any pending/partial orders for the plan. Underlying Flow collectors
push updates back to UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Room schema-validator mismatches caused IllegalStateException at
app start:

1. Custom index 'idx_tx_plan_side_status' was created by migration but
   not declared in entity -> Room flagged as extra index. Added
   Index(value = ["planId","side","status"]) to TransactionEntity and
   renamed migration index to Room convention
   (index_transactions_planId_side_status).

2. Migration's `DEFAULT NULL` on nullable columns mismatched entity's
   defaultValue=undefined. Removed `DEFAULT NULL` clauses for
   limitPrice, requestedCryptoAmount, targetProfitAmount.

3. Migration's `DEFAULT 0` / `DEFAULT 'BUY'` on NOT NULL columns
   mismatched entity (no @ColumnInfo). Added @ColumnInfo(defaultValue=...)
   to allowSells and side fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dashboard portrait LazyColumn iterates both `openSellsByPlan` (Long
planId keys) and `activePlans` (Long plan.id keys) in the same
LazyColumn. When a plan has open sells AND is in active plans, the
same Long key is used twice -> Compose crashes with
IllegalArgumentException("Key X was already used").

Prefix the open-sells item key with "open-sells-" string to namespace
it away from plan ids.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vitnehasil and others added 29 commits May 9, 2026 12:50
PlaceLadderSellUseCase iterates orders sequentially, returning AllPlaced
or PartialFailure on first error. No auto-rollback - caller surfaces
the partial result and lets the user decide.

LadderGenerator.generate produces N orders linearly distributed across
[from..to] with two amount modes: EQUAL_CRYPTO (each order sells
total/N) or EQUAL_FIAT (each order generates same gross fiat,
amounts rescaled to match total). 3 unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checkbox toggles between single and ladder modes. Ladder UI shows From/To
range with toggle (Profit % vs Price), order count, amount-mode toggle
(Equal crypto vs Equal fiat), plus a live preview table with per-order
amount, price, profit % and net fiat. Submit dispatches via
PlaceLadderSellUseCase; partial-success dialog reports placed/total/reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LadderPreviewTable extracted as a reusable composable so the CONFIRM
step can render the ladder preview without exposing edit handlers from
the input-step LadderControls. Single mode keeps amount/limit/proceeds
SummaryRows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ModalBottomSheet uses MaterialTheme surface (was default surfaceContainerLow,
  rendered gray instead of the app's dark navy in dark mode).
- 'Amount' heading now reads 'Amount to sell' / 'Mnozstvi k prodeji'.
- 'Breakeven' chip now uses sell_wizard_chip_breakeven string
  (cs: 'Bez ztraty', en: 'Breakeven').
- All percent values get a space before the % per Czech typography
  (e.g. '+10 %' instead of '+10%'). Affects amount chips, price chips,
  net presets, fee display, profit pct in summary and ladder preview,
  and target progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds maxLines=1 + softWrap=false to all preset chip Text composables
so '+100 %' / 'Vse' / etc. don't wrap. Removes the Breakeven chip
from the price row - the +X % chips already cover that intent and the
button was redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace ModalBottomSheet with Dialog (usePlatformDefaultWidth=false,
  dismissOnClickOutside=false) so the wizard fills the screen and
  cannot be dismissed accidentally by swipe.
- Ladder checkbox row is fully clickable (toggles via Modifier.clickable).
- Fiat number fields (avg buy, limit price, net proceeds, ladder from/to
  in price mode) get a ThousandSeparator VisualTransformation that
  inserts a space every 3 digits in the integer part. Cursor mapping
  preserves typing position.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty inputs gave proceeds=0 and the summary rendered 'Ziskate: 0 CZK'
which is noise. Now the whole Souhrn section shows only when proceeds
> 0 in single mode. In ladder mode the preview table is the summary,
so the single-mode summary block is hidden entirely (loss banner too).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DarkColorScheme and SandboxDarkColorScheme didn't define
primaryContainer / tertiaryContainer / errorContainer, so M3 fell back
to its purple/violet defaults. Anywhere those tokens were used
(InfoBanner, WarningBanner, AssistChip selected state) rendered with a
palette that didn't match AccBot's teal/dark-blue look.

New container colors are derived from the existing palette: dark teal
for primary, dark amber for tertiary/warnings, dark red for errors.
Sandbox dark theme reuses the amber tones since its primary is already
orange. Light schemes already had container colors so they're unchanged.

Sell wizard ladder chips revert to using primaryContainer directly now
that it's themed correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Czech diacritics back to all validation messages
('Mnozstvi' -> 'Mnozstvi musi byt' -> 'Mnozstvi musi byt')
and tags each HardError with the input field it relates to
(AMOUNT / PRICE / NET / GENERIC). The wizard renders field-tagged
errors as supportingText (with red border) directly under the matching
OutlinedTextField, keeping only GENERIC errors in the bottom list.
Same diacritic fix in the ladder hard-error strings on the VM side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tweak primaryContainer to a mint that shares Primary's exact hue
  (Primary at ~50% brightness) instead of the forest-green it was.
- Ladder Od/Do now show isError=true and the validation message renders
  directly below the row instead of two sections lower.
- imePadding() on the wizard scrollable Column so the keyboard pushes
  content up; the user can now scroll to fields below the keyboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coinmate's minimum is denominated in fiat (50 CZK / 2 EUR), not crypto.
Previously the wizard hardcoded 0.00001 BTC as the min for non-Binance
exchanges, which let users submit sub-50 CZK orders that the exchange
then rejected.

Now SellWizardViewModel injects MinOrderSizeRepository and fetches the
fiat min in init(). ValidateSellOrderUseCase compares
'cryptoAmount * limitPrice' against minOrderFiat instead of
'cryptoAmount < minCrypto'. Ladder validation does the same per
smallest-order. Validation message now reads 'Minimalni hodnota orderu
je 50 CZK (zvys mnozstvi nebo cenu)'.

Same change benefits Binance (NOTIONAL filter) - the repo already
fetches minNotional from /exchangeInfo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mode

- setLadderRangeMode no longer clears From/To. When toggling between
  Profit % and Price it converts the values via the avg buy price
  (price = avg * (1 + pct/100), pct = (price - avg)/avg * 100).
  If avg is unknown the fields clear (no other option).
- Net fiat field is now visible in ladder mode too, but read-only.
  Value = sum of per-order nets across the preview, so the user has
  the same '3 fields' visual structure as in single mode and sees the
  total expected proceeds at a glance. Profit-target chips stay hidden
  in ladder mode (they only make sense for single orders).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Net field in ladder mode is editable: typing a target net total adjusts
  the upper bound (To) so the linear-distributed ladder hits that total
  given current amount + From. Math:
  total_net = amount * (from+to)/2 * (1-fee) -> to = 2*net/(amount*(1-fee)) - from.
  Works in both Profit % and Price modes (converts From through avg buy
  when needed).
- recomputeLadderPreview optionally syncs netInput from total. Edits to
  amount/from/to/count/mode sync net = preview total; edits to net itself
  skip the sync so the user-typed value stays.
- Amount field shows '= X % z dostupnych' as supportingText so the user
  sees the ratio whether they typed manually or it was computed via the
  3-field calculator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add TransactionStatus.CANCELLED for user-cancelled or exchange-side
  cancelled orders (no fills). FAILED stays for actual rejections.
- OpenSellsList: every string is now a stringResource (diacritics in
  cs), title 'Otevrene prodejni prikazy', cancel dialog/button use
  proper Czech. Adds 'Zrusit vse' header button (visible when >1 open
  sell) that confirms then cancels in sequence with aggregate snackbar.
- Ladder dialog title 'Vysledek' instead of repeating the checkbox
  label. Czech ladder strings drop English ('ladder', 'order'): now
  'rozdelit na vice prikazu', 'pocet prikazu', 'Zisk %'.
- cancelSell error message has diacritics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Single -> Ladder: single limit price becomes the midpoint of the
  range, From/To = price * 0.90 .. price * 1.10. With default count 5,
  the middle order matches the price the user already set; the rest
  spread +-10 % around it. Range fields populated in whatever mode
  (Cena / Zisk %) is currently active.
- Ladder -> Single: keep total net from the ladder preview, derive
  the single limit price as net / (amount * (1 - fee)). User keeps
  the same expected proceeds with one order instead of N.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cs: Zisk / Vynos, en: Profit / Net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Targeted fixes from independent code review:

Correctness:
- Fix race in revalidate(): cancel previous Job before launching next
  so a slower earlier validation can't overwrite newer keystroke results.
- Fix avg-buy "manual" flag wrongly true after prefill (compared 8dp auto
  vs 2dp display - always reported manual).
- Fix LadderPreviewTable totalNet shadowing: was showing profit when avg
  known, but label said "total net". Now consistently net.
- Convert lossPct from Double to BigDecimal (no Double in money math).

Cleanup:
- Drop SellValidation.Ok singleton (empty list = valid, simpler API).
- Delete dead code: setPriceBreakeven, setPriceSpotPlus.
- Remove unused imports (TransactionSide, TransactionStatus, Exchange).
- Magic timeouts -> named constants (INIT/SUBMIT_SINGLE/SUBMIT_LADDER).
- PlaceLadderSellUseCase: silent catch -> Log.w on resolvePending failure.
- Rename `drobky` -> `remainder` (English consistency).
- Drop orphaned strings sell_wizard_chip_breakeven, sell_wizard_profit_vs_avg.

DRY:
- Extract LadderGenerator.priceToProfitPct / profitPctToPrice -
  same formula was duplicated 5x across setLadderEnabled,
  setLadderRangeMode, setLadderNetTarget, recomputeLadderPreview.
… locale-aware grouping

Localization (CZ strings out of code):
- SellValidation.HardError -> sealed subclasses (AmountMustBePositive,
  PriceMustBePositive, MinOrderTooLow, InsufficientInventory). UI resolves
  text via Composable HardError.localizedText().
- New sealed LadderError (AvgRequired, AmountMustBePositive, CountMin2,
  ToMustExceedFrom, Insufficient, BelowMin) replaces String? ladderHardError.
- @ApplicationContext in SellWizardViewModel for "Unknown error" fallback.
- Hardcoded CZ strings (validation, ladder, "= X % z dostupných") moved
  to strings.xml (EN+CS).

Reuse:
- ValidateSellOrderUseCase now uses CalculatePlanCostBasisUseCase for the
  available-to-sell check. Single source of truth, drops 25 lines of
  duplicated held/openSellsRequested logic.
- LadderControls uses SelectableChip (matches HistoryScreen, ScheduleBuilder)
  instead of custom AssistChip styling.
- ThousandSeparator visual transform now locale-aware via DecimalFormatSymbols
  (CS: nbsp, EN: ',') instead of hardcoded ASCII space.
- New SellSummary data class on UiState computes proceeds, fee, net,
  costBasis, profit, profitPct, amountPct, totalProgress, targetProgressPct
  in one place. UI reads state.summary instead of reparsing on each render.
- SellInputStep (385 lines) split into 8 focused composables:
  SellWizardHeader, SellInfoBlock, AvgBuyField, AmountField, LadderModeRow,
  PriceField, NetField, OrderSummary, LossBannerSection, ValidationsList.
- Drop Double from progress percentage (was Double-divide for display);
  targetProgressPct is now an Int computed on BigDecimal.
- Collapse 3 identical priceAvgPlus AssistChips into a single forEach.
Notifications:
- New NotificationType.SELL_FILLED + NotificationTemplateArgs.SellFilled.
- ResolvePendingTransactionsUseCase fires showSellFilledNotification when
  a SELL transitions to COMPLETED in this poll cycle. Per-transaction ID so
  multiple ladder fills produce separate notifications.
- Localized title/text in EN+CS (TrendingUp icon, success color).

Chart trade markers (Task 26 - was a stub):
- New Daos.getCompletedSellsOrdered() (BUY-only chart series invariant
  preserved by keeping completedTransactions BUY-only and adding a
  separate completedSells cache).
- PortfolioViewModel computes ChartTradeMarker list per page in
  computeChartData; pushed via UiState.tradeMarkers.
- New private TradeMarkersDecoration in ChartComponents.kt: implements
  Vico Decoration, draws BUY up-triangles (green, bottom of plot) and
  SELL down-triangles (red, top of plot) at each trade's chart-bucket
  index. Pixel x = layerBounds.left + startPadding + (modelDelta * xSpacing) - scroll.
- Markers shown only in FIAT denomination (CRYPTO mode hides them).
- key() bumped to invalidate chart when markers change.
No JVM unit tests existed before - only instrumentation/screenshot tests.
Adds the test source set, Robolectric (pinned to SDK 34), in-memory Room
builder, a configurable FakeExchangeApi, and a sanity test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trade-history import left transactions.connectionId NULL, which broke
per-connection aggregation and backup-restore dedup (keyed on
orderId+connectionId). Now resolves the plan's connectionId on import,
plus migration 21->22 backfills existing NULL rows from their plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'View all transactions' from plan detail now filters to that plan, the
active plan filter shows as a clearable chip, and the filter sheet gains
a plan picker. Threads planId through the History route, ViewModel and
the getFilteredTransactions DAO query.

Note: Daos.kt/strings.xml in this commit also carry additive helpers
(countCompletedBuysSinceSync, resetNetworkRetrySync, runaway notification
string) used by the follow-up runaway-buy fix - they share these files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Market buys are not idempotent: /buyInstant places the order before the
client confirms it, so a slow network made withTimeoutOrNull fire after
the order was placed, and the worker retried it (and re-armed every 5 min)
- draining accounts while reporting 'failed, no internet'.

- Reconcile against exchange trade history before any retry; a timed-out
  buy that actually went through is recorded, never re-issued.
- Treat reconciliation uncertainty conservatively (never retry on unknown).
- Cap the 5-minute network-retry loop; runaway circuit breaker auto-disables
  a plan that buys far beyond its schedule (counts reconciled buys too).
- OkHttp callTimeout so calls abort deterministically.
- Don't surface CancellationException as an error notification.
- Note the same orphan-order caveat on the (non-retrying) sell path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the runaway-buy fix surfaced residual duplicate-order
windows, all now closed:

- Order placement could outlive the worker's coroutine timeout (30s) while
  OkHttp's callTimeout was 40s, so reconciliation could run against an
  in-flight order -> false 'not found' -> duplicate. Now callTimeout(30s)
  < worker timeout(45s) so OkHttp always aborts first.
- Reconciliation now aggregates trade-history fills by orderId before the
  amount check (partial fills no longer look too small to match).
- Honor the lookback buffer in the in-memory filter too (exchange clock
  skew no longer hides a just-placed order).
- Dedup against the whole connection, not just the plan (two plans on one
  account can't double-claim an order).
- Longer settlement window (3x2s) for slow-network fills to appear.
- Import attributes connectionId only when > 0 (matches the migration).
- History plan picker uses the ordered query; export Room schemas for
  future migration tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce runs)

- disable OkHttp retryOnConnectionFailure: silent transparent POST re-send on
  stale pooled connections could place two real orders below the worker's
  reconcile logic
- serialize all forceRun executions (runNow/runPlan/runMissedPurchases) through
  one unique-work queue (APPEND_OR_REPLACE) - forced runs bypass the per-plan
  claim, so they must never run concurrently
- apply the runaway circuit breaker to forced runs too, with a wider allowance
  (+repeatCount) for user-initiated catch-ups
- checkpoint catch-up progress in missedPurchaseCount (decrement per completed
  buy) so a worker replayed after process death resumes instead of re-buying
  the whole batch
- forced runs return failure instead of retry on unexpected errors (no
  automatic replay of user-initiated buys)
- alarm work: KEEP instead of REPLACE so a re-fired alarm cannot cancel a
  worker mid-buy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase-volume sizing

Kraken removed the volume-in-quote (viqc) oflag from the spot API, so every
buy failed. Size the order in base currency from the current ticker price,
reserving the taker fee so cost + fee stays within the plan's fiat amount.
A failed price fetch returns a retryable error (no order was placed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fter background grace

- isUnlocked uses plain remember: rememberSaveable survived process death and
  restored the unlocked state, silently skipping the lock when the task was
  reopened from recents
- re-lock when the app spends more than 30s in background (lifecycle observer
  with monotonic clock); quick app switches don't re-prompt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o chart

Visual clutter, and with many transactions the markers were aggregated into
meaningless clusters. Removes the feature end-to-end: decoration + data class
(ChartComponents), marker computation and completedSells cache (ViewModel),
call sites (Screen). Chart recreation key no longer depends on markers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Crynners Crynners merged commit 1092088 into main Jun 12, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants