Skip to content

Android wallet/perps portfolio#616

Open
gemdev111 wants to merge 17 commits into
mainfrom
android-portfolio-perps
Open

Android wallet/perps portfolio#616
gemdev111 wants to merge 17 commits into
mainfrom
android-portfolio-perps

Conversation

@gemdev111

@gemdev111 gemdev111 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

closes #403

images:

Wallet home
Both entry points — balance change + Perpetuals header
Portfolio chart
Line + area, high/low bounds, ATH/ATL stats
Scrub
Selected point value + date
Perpetuals portfolio
PnL chart + perps stats
Chart-type switcher
Value / PnL
Value chart
Same scene, Value mode

gemdev111 added 8 commits July 2, 2026 19:11
Backend endpoint + coordinator for the wallet portfolio value history:
- GemDeviceApiClient: POST /v2/devices/portfolio/assets
- GetPortfolioData reads held balances, POSTs them, applies the fiat rate,
  and returns the generated PortfolioData (mirrors iOS PortfolioDataService)
- Hilt binding + GetPortfolioDataImplTest
Prepare the shared chart components so the portfolio scene can reuse them:
- PeriodsPanel/ChartStateView take an optional periods list (defaults to all
  entries) so a chart can restrict the selector; asset and perpetual charts
  are unchanged
- allTimeProperties is now internal and drops a dead asset parameter, so it
  can render the portfolio all-time high/low rows too
PortfolioChartViewModel + PortfolioChartScene showing the wallet value
history plus all-time high/low:
- ViewModel holds the selected period in-memory (default All, matching iOS
  PortfolioState); maps PortfolioData's ChartDateValue series onto ChartUIModel
  via a new date-value overload (no seconds/millis round-trip); timeout and
  min-points are named constants
- Scene reuses ChartStateView / GemLineChart / chartHeader / allTimeProperties
  and renders the generated PortfolioStatistic; the period set is a local constant
- PortfolioChartViewModelTest keeps a collector on the WhileSubscribed flow
  and awaits the refetched data, the way the scene subscribes via
  collectAsStateWithLifecycle
- PortfolioChartRoute + portfolioChartScreen entry (Navigation3)
- navigator.openPortfolioChart(), mirroring openAssetChart
- register the entry in WalletNavGraph
- AmountListHead gains an optional onSubtitleClick (Role.Button); the clip,
  ripple, and a show-chart icon only apply when it is set, so asset/
  perpetual/confirm headers are unchanged. The icon mirrors iOS
  WalletHeaderViewModel.subtitleImage (chart.line.uptrend.xyaxis in footnote
  size and secondary color)
- AssetsHead threads it through as onPortfolio, dispatched as
  AssetsAction.Portfolio through the assets screen's sealed action and wired
  to navigator.openPortfolioChart in MainScreen, mirroring iOS
  WalletHeaderView.onSubtitleAction
- PerpetualService.getPortfolio(address) -> gateway.getPerpetualPortfolio,
  with GemPerpetualPortfolio/timeframe/summary/date-value DTO mappers
- GetPortfolioData takes a PortfolioType and dispatches wallet vs perpetuals,
  like iOS PortfolioDataService.getPortfoliData(input:); the perpetuals side
  mirrors iOS getPerpetualData (pnl + value charts, unrealizedPnl/leverage/
  margin/allTimePnl/volume statistics, availablePeriods from the account
  timeframes)
- Failures surface like iOS: a gateway error propagates and a missing
  hyperliquid account throws, so the chart shows the error state instead of
  a silent empty portfolio
- The wallet side now returns data-driven availablePeriods
- Hilt DI + coordinator test updated; ViewModel still requests Wallet
  (perpetuals UI lands in the follow-up commits)
- selectedType (wallet/perpetuals) + selectedChartType (value/pnl) drive the
  coordinator fetch; the chart renders the selected chartType's series and
  perpetuals values format in USD like iOS's perpetualFormatter (the gateway
  data carries no fiat rate)
- showSegmentedControl gated on ObservePerpetualWallet (mirrors iOS
  showPerpetuals(for:)); showChartTypePicker when perpetuals is selected
- availablePeriods sourced from the data per type; when the fetched data does
  not offer the selected period it resets to the first available one, like
  iOS PortfolioSceneViewModel.fetch (a new perpetuals account has no all-time
  history, so Year/All disappear)
- chartState lives in one place: fewer than two points or a flat series with
  no variation is Empty (iOS ChartValues.hasVariation), a failed request is
  Error, and toggling the chart type recomputes it for the newly selected
  series
- ViewModel tests cover switching, the period reset, the flat series, and the
  error state
- Scene title slot shows the Portfolio/Perpetuals segmented control when
  perpetuals is enabled; actions slot shows the value/PnL chart-type menu;
  periods are data-driven (viewModel.availablePeriods)
- One portfolioStatistics renderer mirrors iOS's single statistics list under
  an Info section header: the wallet all-time high/low reuse the existing
  allTimeProperties rows, and the perpetual rows (unrealized/all-time PnL as
  signed amounts colored by direction, leverage, margin usage with an
  unsigned percent, volume) reuse the shared property rows and formatters,
  matching iOS statisticModel
@gemdev111 gemdev111 changed the title Android portfolio perps Android wallet/perps porfolio Jul 2, 2026
gemdev111 added 9 commits July 2, 2026 19:47
Header: build a priceChange-style header (ChartValueType.PriceChange) like
iOS ChartValuesViewModel instead of reusing the asset price header. It shows
the absolute value as the big header value, the signed change amount colored
by the amount's sign (so a negative perpetuals PnL reads red), and the
percentage as an unsigned value in parentheses that is hidden when there is
no header value (perpetuals PnL). showHeaderValue follows iOS: wallet, or the
value chart type. The change amount uses the raw first point as the base so
the perpetuals PnL change equals the full PnL, matching iOS.

Footer: render the statistics section only when the chart is loaded (ready
or empty), mirroring iOS which sources statistics from the data state, rather
than leaving the previous type's rows visible during a refetch.

The header-model change is additive: the asset price chart passes no type
(defaults to Price) and renders exactly as before.
Mirror iOS: tapping the balance on the perpetuals screen opens the
portfolio with the Perpetuals type preselected (PerpetualsScene
balanceActionType -> .portfolio(.perpetuals)), while the wallet balance
subtitle keeps opening it on Wallet.

- PortfolioChartRoute carries a PortfolioType (default Wallet) passed to
  the ViewModel via the Type route argument, like WalletPhraseRoute
- PerpetualMarketScene dispatches OpenPortfolio from AmountListHead onClick
  through the screen's sealed action
- PortfolioChartViewModel takes the initial type via SavedStateHandle with
  the same two-constructor pattern as ChartViewModel
- Scene title follows the selected type when the segmented control is
  hidden, matching iOS typeTitle(for:)
The portfolio chart duplicated the asset Chart composable body (period-keyed
selection state, the loading fallback while a new period's data arrives, and
the ChartStateView/GemLineChart wiring). Extract that block as ChartSection
with the periods and the header builder as parameters; Chart and
PortfolioChart are now thin wrappers that collect their ViewModel and choose
the header.
The Portfolio/Perpetuals switch was the app's only Material3 SegmentedButton;
replace it with the shared TabsBar the buy/sell switch already uses in its
scene title. The chart-type menu label matches the wallet selector in
AssetsTopBar: onSurface text with an ExpandMore affordance instead of a bare
primary-colored label.
Review findings: viewState was a MutableStateFlow written from five call
sites, with the ready/empty rule computed from a stale _selectedChartType
read inside mapLatest and re-derived in setChartType from a lagging
portfolio.value; the fetch result also carried no request identity, so a
type switch could pair Ready with the previous type's points, and the
cancelled block's finally hid the refresh indicator while the replacing
fetch was still loading.

The fetch now emits tagged with its request (type + period): the chart
state, model, and statistics derive reactively from the tag matching the
current selection, the period reset uses compareAndSet so a cancelled fetch
cannot override a newer user selection, and the refresh flag clears only
when a fetch actually finishes. showChartTypePicker/showHeaderValue
StateFlows are gone - the picker derives from selectedType at the call site
and the header value is baked into ChartUIModel like iOS
ChartValuesViewModel.headerValue, which also removes their duplicated
initial-value predicates. The chart percentage base now uses the raw first
point like iOS ChartValuesViewModel.priceChange (was first non-zero).
Review findings: the PnL rows hand-built ListItem/PropertyTitleText/
PropertyDataText although PropertyItem(title, data, dataColor) renders the
same row and is how TransactionDetailsScene shows PnL today; leverage was
formatted with a locale-fragile String.format while gemcore owns leverage
display (formatLeverage), which gains a locale-aware Double overload with
iOS's two fraction digits; and the all-time/perpetual split was an either/or
branch that would silently drop non-all-time statistics from a mixed list -
the statistics now partition so both kinds render.
Review findings: the wallet fetch read the currency rate only after the
portfolio POST and mapped a missing rate to an empty result, wasting the
round-trip and diverging from iOS, which reads the rate first and fails into
the error state - the rate is now checked before the request and throws;
seconds-to-millis conversions used TimeUnit and a magic multiply instead of
the shared secondsToMillis extension; the wallet period list existed twice
(coordinator constant + ViewModel fallback) and is now one constant beside
the GetPortfolioData interface; the portfolio sub-mappers follow the file's
private-mapper precedent; new imports are sorted into their blocks.
The tagged-fetch pattern from the portfolio ViewModel moves to ui-models as
ChartFetch(request, data) with a shared viewState derivation - the Android
counterpart of iOS StateViewType<PortfolioData>, where the fetch result and
its identity are one value. The asset ChartViewModel adopts it, deleting its
imperative viewState writes, which had the same cancelled-refresh and
unordered Ready-vs-data races as the portfolio chart. The candlestick chart
keeps its streaming flow: flatMapLatest already binds candles to their
period, so there is no identity to tag.

The chart type picker flag returns as a named value in the scene, and the
duplicated PnL PropertyItem calls collapse into a shared PnlPropertyItem
next to the other property rows.
Port iOS StateViewType (noData/loading/data/error) to ui-models and make it
the single chart state primitive: ChartViewState and the interim ChartFetch
are gone, and the state now carries its payload the way iOS
StateViewType<PortfolioData> does.

Each chart fetch is a transformLatest that emits its request state first and
the result after, so a stale result can never render under a newer selection
and no imperative state writes remain. The request snapshot is a feature
model like iOS Types/PortfolioState: PortfolioState(type, period, currency,
data) with displayCurrency computed on it, and AssetChartState for the asset
chart - the currency rides in the request, not beside the result. The
portfolio and asset chart ViewModels expose one ChartUIModel.State(period,
chart); the candlestick flow emits its states in-band on the stream that
merges websocket candles; ChartStateView is generic over the payload. The
chart ViewModels share the named StopTimeoutMillis instead of a raw 5_000.
@gemdev111 gemdev111 force-pushed the android-portfolio-perps branch from 4cd5efd to 4534e25 Compare July 2, 2026 17:01
@gemdev111 gemdev111 self-assigned this Jul 2, 2026
@gemdev111 gemdev111 marked this pull request as ready for review July 2, 2026 18:54
@gemdev111 gemdev111 changed the title Android wallet/perps porfolio Android wallet/perps portfolio Jul 3, 2026
StateViewType.Data(ChartUIModel.from(it, priceInfo, state.period, state.currency))
},
)
}.stateIn(viewModelScope, SharingStarted.Eagerly, ChartUIModel.State())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use WhileSubscribed?

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.

Implement Portfolio

2 participants