feat(perps): add TradingView candle chart - #6513
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in TradingView candlestick chart and localized market descriptions to perps market details.
Changes:
- Adds TradingView chart rendering, attribution, and a debug preference toggle.
- Persists and displays localized perps market descriptions.
- Adds the Room migration and migration test for descriptions.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
build.gradle.kts |
Defines the chart library version. |
app/build.gradle.kts |
Adds the TradingView dependency. |
Constants.kt |
Defines the chart preference key. |
TradingViewCandleChart.kt |
Implements the TradingView chart. |
CandleChart.kt |
Selects the configured chart implementation. |
PerpsMarketDetailPage.kt |
Displays localized market information. |
PerpsMarket.kt |
Adds market descriptions. |
PerpsDatabase.kt |
Adds database migration 5→6. |
PerpsMigrationTest.kt |
Tests the migration. |
6.json |
Records Room schema version 6. |
LogAndDebugFragment.kt |
Manages the chart toggle. |
fragment_log_debug.xml |
Adds the toggle UI. |
AboutPage.kt |
Adds Compose attribution. |
AboutFragment.kt |
Adds legacy attribution behavior. |
fragment_about.xml |
Adds legacy attribution UI. |
values/strings.xml |
Adds English labels. |
values-zh-rCN/strings.xml |
Adds Simplified Chinese labels. |
values-zh-rTW/strings.xml |
Adds Traditional Chinese labels. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CandlestickSeriesOptions( | ||
| upColor = upColor.toArgb().toIntColor(), | ||
| downColor = downColor.toArgb().toIntColor(), | ||
| wickUpColor = upColor.toArgb().toIntColor(), | ||
| wickDownColor = downColor.toArgb().toIntColor(), | ||
| borderVisible = false, | ||
| lastValueVisible = false, | ||
| priceLineVisible = false, | ||
| ), |
| object : GestureDetector.SimpleOnGestureListener() { | ||
| override fun onLongPress(e: MotionEvent) { | ||
| longPressActive = true | ||
| targetView?.requestDisallowInterceptTouchEvent(true) | ||
| } | ||
| }, |
5029c67 to
72497c2
Compare
| <string name="a_video">a video</string> | ||
| <string name="About">About</string> | ||
| <string name="Use_TradingView_Candlestick_Chart">Use TradingView candlestick chart</string> | ||
| <string name="Charts_by_TradingView">Charts by TradingView</string> |
| <TextView | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_centerVertical="true" | ||
| android:layout_marginStart="@dimen/activity_horizontal_margin" | ||
| android:text="@string/Use_TradingView_Candlestick_Chart" | ||
| android:textColor="@color/colorBlue" | ||
| android:textSize="16sp" | ||
| tools:ignore="RelativeOverlap" /> |
Keep the existing renderer as default and expose TradingView through Log & Debug for comparison.
72497c2 to
ddacb27
Compare
| time = Time.Utc(item.timestamp), | ||
| open = open, | ||
| high = high, | ||
| low = low, | ||
| close = close, | ||
| ) | ||
| } | ||
| } | ||
| val candlesByTimestamp = | ||
| remember(candles) { | ||
| candles.firstOrNull()?.items.orEmpty().associateBy(CandleItem::timestamp) | ||
| } |
| ), | ||
| ) | ||
| subscribeOnChartStateChange { state -> | ||
| errorMessage = (state as? ChartsView.State.Error)?.exception?.localizedMessage |
| <androidx.appcompat.widget.SwitchCompat | ||
| android:id="@+id/trading_view_chart_sc" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_alignParentEnd="true" | ||
| android:layout_centerVertical="true" | ||
| android:layout_marginEnd="@dimen/activity_horizontal_margin" | ||
| tools:ignore="RelativeOverlap" /> |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:113
- The candle API timestamps are milliseconds (the iOS client consuming the same endpoint converts
item.timestamp / 1000), but lightweight-charts v4Time.Utcexpects epoch seconds. Passing the raw value places every candle tens of thousands of years in the future. Convert to seconds here and key the crosshair lookup by that converted value as well.
time = Time.Utc(item.timestamp),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:113
CandleItem.timestampis epoch milliseconds, but TradingView v4'sTime.Utcexpects epoch seconds. Passing the raw value makes chart dates and candle intervals 1,000× too large. Normalize both the series data and the crosshair lookup keys to seconds so tooltip selection still resolves the corresponding candle.
time = Time.Utc(item.timestamp),
| timeFrame: String, | ||
| ): String { | ||
| val millis = if (timestamp < MILLIS_TIMESTAMP_THRESHOLD) timestamp * 1000 else timestamp | ||
| val zoneId = ZoneId.systemDefault() |
| pinch = true | ||
| } | ||
| } | ||
| chartApi = api |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:113
Time.Utcin lightweight-charts v4 expects epoch seconds, while perps candle timestamps are epoch milliseconds. Passing milliseconds places every bar far in the future; normalize both the series time and lookup-map key so crosshair selections still resolve.
time = Time.Utc(item.timestamp),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:476
- This offset-based conversion collapses distinct candles during the daylight-saving fall-back hour (for example, 05:30Z at UTC-4 and 06:30Z at UTC-5 both become 01:30). That produces duplicate
Time.Utcvalues forsetData, andcandlesByTimestamp.associateByalso overwrites one candle. Keep normalized UTC epoch seconds as the chart/key identity and apply the local zone only when formatting labels.
val utcTimestamp = normalizeTradingViewTimestamp(timestamp)
val offsetSeconds = Instant.ofEpochSecond(utcTimestamp).atZone(zoneId).offset.totalSeconds
return utcTimestamp + offsetSeconds
app/src/main/res/layout/fragment_log_debug.xml:110
- The new switch has no accessible name: its sibling
TextViewis not associated with it vialabelFor, so TalkBack announces only an unlabeled switch state. Give the switch this string as its content description (or explicitly associate the label) so users know what it controls.
<androidx.appcompat.widget.SwitchCompat
| assertEquals( | ||
| utcTimestamp + 8 * 60 * 60, | ||
| tradingViewLocalTimestamp(utcTimestamp, ZoneOffset.ofHours(8)), | ||
| ) |
| assertEquals( | ||
| 1_721_600_000L + 8 * 60 * 60, | ||
| tradingViewLocalTimestamp(utcTimestampMillis, ZoneOffset.ofHours(8)), | ||
| ) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:475
- Adding each instant's zone offset makes timestamps collide during a daylight-saving fall-back. For example, consecutive 01:00 EDT and 01:00 EST candles both map to the same shifted epoch value;
setDatarequires strictly ascending candle times, andassociateByalso drops one of them. Preserve unique UTC timestamps and localize labels separately (or use another monotonic timezone strategy), with a DST-boundary test.
val utcTimestamp = normalizeTradingViewTimestamp(timestamp)
val offsetSeconds = Instant.ofEpochSecond(utcTimestamp).atZone(zoneId).offset.totalSeconds
return utcTimestamp + offsetSeconds
app/src/main/res/layout/fragment_log_debug.xml:117
- The switch has no accessible label, so a screen reader announces only an unlabeled on/off control. Associate the visible text with this switch or provide an equivalent content description.
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/trading_view_chart_sc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
tools:ignore="RelativeOverlap" />
| internal fun tradingViewTouchCoordinate( | ||
| touchX: Float, | ||
| density: Float, | ||
| ): Float = if (density > 0f) touchX / density else touchX |
…ndle-chart # Conflicts: # app/build.gradle.kts # build.gradle.kts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:475
- Converting every candle to a synthetic “local epoch” makes the series non-monotonic across a daylight-saving fall-back. For example, in New York, 05:00 UTC and 06:00 UTC can both become 01:00 after adding their respective offsets;
SeriesApi.setDatarequires ordered times, so 1h/intraday history spanning that transition can fail to render (and this map also drops duplicate keys). Keep the series timestamps monotonic UTC and localize only the displayed labels, or otherwise use a transformation that preserves ordering.
val utcTimestamp = normalizeTradingViewTimestamp(timestamp)
val offsetSeconds = Instant.ofEpochSecond(utcTimestamp).atZone(zoneId).offset.totalSeconds
return utcTimestamp + offsetSeconds
TradingView rejects duplicate timestamps and crashes on malformed series input, so normalize server candle responses before setData.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:460
- Adding the zone offset to the epoch makes timestamps collide during a daylight-saving fall-back (for example, 05:00 UTC at −04:00 and 06:00 UTC at −05:00 both become 01:00). The
associateBybelow then silently drops one candle—up to an hour of intraday data. KeepTime.Utcon the normalized UTC epoch and localize only the displayed labels; add a DST-overlap test.
val utcTimestamp = normalizeTradingViewTimestamp(timestamp)
val offsetSeconds = Instant.ofEpochSecond(utcTimestamp).atZone(zoneId).offset.totalSeconds
return utcTimestamp + offsetSeconds
| selectedCandle?.let { selection -> | ||
| val alignment = | ||
| if (selection.pointX < maxWidth.value / 2f) { | ||
| Alignment.TopEnd | ||
| } else { | ||
| Alignment.TopStart | ||
| } |
| LaunchedEffect(seriesApi, marketPrice, priceLineColor) { | ||
| val series = seriesApi ?: return@LaunchedEffect | ||
| val price = marketPrice?.toFloatOrNull() ?: return@LaunchedEffect | ||
| val options = | ||
| PriceLineOptions( | ||
| price = price, | ||
| color = priceLineColor.toArgb().toIntColor(), | ||
| lineStyle = LineStyle.DASHED, | ||
| lineVisible = true, | ||
| axisLabelVisible = true, | ||
| ) | ||
| marketPriceLine?.applyOptions(options) ?: run { | ||
| marketPriceLine = series.createPriceLine(options) | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:316
selection.pointXcomes from the Android chart/touch coordinate space (pixels), whilemaxWidth.valueis the raw dp value. On density > 1 devices this moves the midpoint far left, so a tooltip selected across much of the left half is aligned on the left and can cover the selected candle. ConvertmaxWidthto pixels before comparing.
val alignment =
if (selection.pointX < maxWidth.value / 2f) {
Alignment.TopEnd
} else {
Alignment.TopStart
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/res/values/strings.xml:16
- The copyright marker uses the Cyrillic letter
с, so the attribution displays a lookalike rather than the conventional(c)marker. Replace it with the Latin character.
<string name="trading_view_attribution" translatable="false">TradingView Lightweight Charts\nCopyright (с) 2022 TradingView, Inc. https://www.tradingview.com/</string>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradingViewCandleChart.kt:200
- If
marketPricebecomes null or non-numeric after a line has been created, this early return leaves the previous market price visible indefinitely. It also acceptsNaN/Infinity, unlike the candle conversion below, and can pass an invalid price into the chart. Remove or hide the existing line and clearmarketPriceLinewhen the parsed price is absent/non-finite; only create/update it for finite values.
LaunchedEffect(seriesApi, marketPrice, priceLineColor) {
val series = seriesApi ?: return@LaunchedEffect
val price = marketPrice?.toFloatOrNull() ?: return@LaunchedEffect
val options =
No description provided.