DCA Sell Extension + kritické opravy z revize#82
Merged
Conversation
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>
…English) in user-facing strings
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
retryOnConnectionFailure(false)- transport vrstva už nikdy potichu nepřepošle order POST (duplicitní nákup pod úrovní reconcile logiky)KEEPmístoREPLACE- znovu vystřelený alarm nezruší worker uprostřed nákupuDalší opravy
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 feeTesty
./gradlew testDebugUnitTest- zelené🤖 Generated with Claude Code