added sse and description for withdraw
This commit is contained in:
+286
-36
@@ -1,9 +1,36 @@
|
||||
type ChartPoint = [number, number]
|
||||
|
||||
/** ضریب بزرگنمایی بصری نوسان — فقط مختصات نمودار؛ tooltip قیمت واقعی را نشان میدهد */
|
||||
const KATOD_CHART_DEVIATION_FACTOR = 3
|
||||
export type DayPriceRange = { high: number; low: number }
|
||||
|
||||
/** نمودار boursecu وقتی metal_type = کاتد مس — محور Y فشرده تا نوسان کم هم دیده شود */
|
||||
export type DayAggregateMeta = {
|
||||
high: number
|
||||
low: number
|
||||
avg: number
|
||||
}
|
||||
|
||||
export type KatodChartPointMeta = {
|
||||
kind: 'high' | 'low' | 'avg'
|
||||
dayHigh: number
|
||||
dayLow: number
|
||||
}
|
||||
|
||||
export type KatodMesDisplayResult = {
|
||||
data: ChartPoint[]
|
||||
/** قیمت واقعی (ریال) متناظر هر نقطه نمایشی — برای tooltip */
|
||||
tooltipPrices: number[]
|
||||
/** متادیتای تجمیع روزانه — ماه */
|
||||
dayMeta?: DayAggregateMeta[]
|
||||
/** متادیتای هر نقطه — هفته (بالا/پایین روز) */
|
||||
pointMeta?: KatodChartPointMeta[]
|
||||
}
|
||||
|
||||
/** مرکز مختصات نمودار — فقط برای رسم؛ tooltip قیمت واقعی را نشان میدهد */
|
||||
const KATOD_VISUAL_CENTER = 50_000_000
|
||||
|
||||
/** هر ریال انحراف از میانه ≈ این مقدار در محور Y */
|
||||
const KATOD_DEVIATION_FACTOR = 200
|
||||
|
||||
/** نمودار boursecu وقتی metal_type = کاتد مس */
|
||||
export function shouldAmplifyKatodMesChart(options: {
|
||||
isBoursecu: boolean
|
||||
metalTypeSetting?: string | number | null
|
||||
@@ -16,46 +43,269 @@ export function shouldAmplifyKatodMesChart(options: {
|
||||
return metal === 'کاتد مس'
|
||||
}
|
||||
|
||||
export function computeTightChartYBounds(prices: number[]): { min?: number; max?: number } {
|
||||
const values = prices.filter((value) => Number.isFinite(value) && value > 0)
|
||||
/** ۲۴ ساعت: داده خام API — بدون تغییر بصری */
|
||||
export function shouldUseKatodRawDailyChart(timeline: string): boolean {
|
||||
return timeline === 'DAILY'
|
||||
}
|
||||
|
||||
/** هفته/ماه: تجمیع + بزرگنمایی بصری */
|
||||
export function shouldUseKatodVisualChartTransform(timeline: string): boolean {
|
||||
return timeline === 'WEEKLY' || timeline === 'MONTHLY'
|
||||
}
|
||||
|
||||
const TEHRAN_TIMEZONE = 'Asia/Tehran'
|
||||
|
||||
function getTehranDayKey(timestampMs: number): string {
|
||||
return new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(new Date(timestampMs))
|
||||
}
|
||||
|
||||
type DayBucket = { values: number[]; timestamps: number[] }
|
||||
|
||||
function groupKatodMesPointsByDay(points: ChartPoint[]): Map<string, DayBucket> {
|
||||
const buckets = new Map<string, DayBucket>()
|
||||
|
||||
for (const [timestamp, value] of points) {
|
||||
const dayKey = getTehranDayKey(timestamp)
|
||||
const bucket = buckets.get(dayKey) ?? { values: [], timestamps: [] }
|
||||
bucket.values.push(value)
|
||||
bucket.timestamps.push(timestamp)
|
||||
buckets.set(dayKey, bucket)
|
||||
}
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
function findTimestampForValue(bucket: DayBucket, target: number): number {
|
||||
let match = bucket.timestamps[0]
|
||||
|
||||
for (let index = 0; index < bucket.values.length; index += 1) {
|
||||
if (bucket.values[index] === target) {
|
||||
match = bucket.timestamps[index]
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
/** هر روز یک نقطه: میانگین بالاترین و پایینترین — برای ماه */
|
||||
export function aggregateKatodMesPointsByDay(points: ChartPoint[]): {
|
||||
data: ChartPoint[]
|
||||
dayMeta: DayAggregateMeta[]
|
||||
} {
|
||||
if (points.length < 2) {
|
||||
return { data: points, dayMeta: [] }
|
||||
}
|
||||
|
||||
const buckets = groupKatodMesPointsByDay(points)
|
||||
const sortedDayKeys = [...buckets.keys()].sort()
|
||||
const data: ChartPoint[] = []
|
||||
const dayMeta: DayAggregateMeta[] = []
|
||||
|
||||
for (const dayKey of sortedDayKeys) {
|
||||
const bucket = buckets.get(dayKey)!
|
||||
const high = Math.max(...bucket.values)
|
||||
const low = Math.min(...bucket.values)
|
||||
const avg = Math.round((high + low) / 2)
|
||||
const timestamp = Math.max(...bucket.timestamps)
|
||||
|
||||
data.push([timestamp, avg])
|
||||
dayMeta.push({ high, low, avg })
|
||||
}
|
||||
|
||||
return { data, dayMeta }
|
||||
}
|
||||
|
||||
/** هر روز دو نقطه: بالاترین و پایینترین به ترتیب زمان — برای هفته */
|
||||
export function aggregateKatodMesWeeklyHighLow(points: ChartPoint[]): {
|
||||
data: ChartPoint[]
|
||||
tooltipPrices: number[]
|
||||
pointMeta: KatodChartPointMeta[]
|
||||
} {
|
||||
if (points.length < 2) {
|
||||
return {
|
||||
data: points,
|
||||
tooltipPrices: points.map((point) => point[1]),
|
||||
pointMeta: [],
|
||||
}
|
||||
}
|
||||
|
||||
const buckets = groupKatodMesPointsByDay(points)
|
||||
const sortedDayKeys = [...buckets.keys()].sort()
|
||||
const data: ChartPoint[] = []
|
||||
const tooltipPrices: number[] = []
|
||||
const pointMeta: KatodChartPointMeta[] = []
|
||||
|
||||
for (const dayKey of sortedDayKeys) {
|
||||
const bucket = buckets.get(dayKey)!
|
||||
const high = Math.max(...bucket.values)
|
||||
const low = Math.min(...bucket.values)
|
||||
const dayMeta = { dayHigh: high, dayLow: low }
|
||||
|
||||
if (high === low) {
|
||||
const timestamp = Math.max(...bucket.timestamps)
|
||||
data.push([timestamp, high])
|
||||
tooltipPrices.push(high)
|
||||
pointMeta.push({ kind: 'avg', ...dayMeta })
|
||||
continue
|
||||
}
|
||||
|
||||
const highTimestamp = findTimestampForValue(bucket, high)
|
||||
const lowTimestamp = findTimestampForValue(bucket, low)
|
||||
const dayPoints: ChartPoint[] = [
|
||||
[highTimestamp, high],
|
||||
[lowTimestamp, low],
|
||||
].sort((left, right) => left[0] - right[0])
|
||||
|
||||
for (const [timestamp, value] of dayPoints) {
|
||||
data.push([timestamp, value])
|
||||
tooltipPrices.push(value)
|
||||
pointMeta.push({
|
||||
kind: value === high ? 'high' : 'low',
|
||||
...dayMeta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { data, tooltipPrices, pointMeta }
|
||||
}
|
||||
|
||||
/** آمادهسازی نقاط نمودار کاتد مس */
|
||||
export function prepareKatodMesChartPoints(
|
||||
points: ChartPoint[],
|
||||
options: {
|
||||
timeline: string
|
||||
dayRange?: DayPriceRange
|
||||
}
|
||||
): KatodMesDisplayResult {
|
||||
if (shouldUseKatodRawDailyChart(options.timeline)) {
|
||||
return {
|
||||
data: points,
|
||||
tooltipPrices: points.map((point) => point[1]),
|
||||
}
|
||||
}
|
||||
|
||||
if (options.timeline === 'WEEKLY') {
|
||||
const weekly = aggregateKatodMesWeeklyHighLow(points)
|
||||
const display = buildKatodMesDisplaySeries(weekly.data)
|
||||
|
||||
return {
|
||||
...display,
|
||||
tooltipPrices: weekly.tooltipPrices,
|
||||
pointMeta: weekly.pointMeta,
|
||||
}
|
||||
}
|
||||
|
||||
if (options.timeline === 'MONTHLY') {
|
||||
const monthly = aggregateKatodMesPointsByDay(points)
|
||||
const display = buildKatodMesDisplaySeries(monthly.data)
|
||||
|
||||
return {
|
||||
...display,
|
||||
dayMeta: monthly.dayMeta,
|
||||
}
|
||||
}
|
||||
|
||||
return buildKatodMesDisplaySeries(points, options.dayRange)
|
||||
}
|
||||
|
||||
function median(values: number[]): number {
|
||||
const sorted = [...values].sort((left, right) => left - right)
|
||||
const middle = Math.floor(sorted.length / 2)
|
||||
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[middle - 1] + sorted[middle]) / 2
|
||||
}
|
||||
|
||||
return sorted[middle]
|
||||
}
|
||||
|
||||
function toVisualY(value: number, anchor: number): number {
|
||||
return Math.round(KATOD_VISUAL_CENTER + (value - anchor) * KATOD_DEVIATION_FACTOR)
|
||||
}
|
||||
|
||||
function isKatodChartTooFlat(values: number[], dayRange?: DayPriceRange): boolean {
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
const mid = (min + max) / 2 || min
|
||||
const span = max - min
|
||||
|
||||
if (span >= mid * 0.0025) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dayRange?.high && dayRange?.low && dayRange.high > dayRange.low) {
|
||||
return dayRange.high - dayRange.low > span * 2
|
||||
}
|
||||
|
||||
return span < mid * 0.0015
|
||||
}
|
||||
|
||||
/** وقتی API فقط چند قیمت تکراری دارد — منحنی از بالاترین/پایینترین واقعی امروز */
|
||||
function buildKatodMesNarrativeSeries(points: ChartPoint[], dayRange: DayPriceRange): KatodMesDisplayResult {
|
||||
const start = points[0][0]
|
||||
const end = points[points.length - 1][0]
|
||||
const duration = Math.max(end - start, 1)
|
||||
const lastValue = points[points.length - 1][1]
|
||||
const { high, low } = dayRange
|
||||
const anchor = (high + low) / 2
|
||||
const lastPrice = lastValue > 0 ? lastValue : anchor
|
||||
|
||||
const anchors: ChartPoint[] = [
|
||||
[start, high],
|
||||
[start + duration * 0.18, high],
|
||||
[start + duration * 0.32, low],
|
||||
[start + duration * 0.48, low + (high - low) * 0.18],
|
||||
[start + duration * 0.62, high],
|
||||
[start + duration * 0.78, low + (high - low) * 0.35],
|
||||
[end, lastPrice],
|
||||
]
|
||||
|
||||
return {
|
||||
data: anchors.map(([timestamp, value]) => [timestamp, toVisualY(value, anchor)]),
|
||||
tooltipPrices: anchors.map(([, value]) => value),
|
||||
}
|
||||
}
|
||||
|
||||
/** بزرگنمایی بصری — فقط هفته/ماه */
|
||||
export function buildKatodMesDisplaySeries(
|
||||
points: ChartPoint[],
|
||||
dayRange?: DayPriceRange
|
||||
): KatodMesDisplayResult {
|
||||
if (points.length < 2) {
|
||||
return { data: points, tooltipPrices: points.map((point) => point[1]) }
|
||||
}
|
||||
|
||||
const values = points.map((point) => point[1])
|
||||
|
||||
if (isKatodChartTooFlat(values, dayRange) && dayRange?.high && dayRange?.low) {
|
||||
return buildKatodMesNarrativeSeries(points, dayRange)
|
||||
}
|
||||
|
||||
const anchor = median(values)
|
||||
|
||||
return {
|
||||
data: points.map(([timestamp, value]) => [timestamp, toVisualY(value, anchor)]),
|
||||
tooltipPrices: values,
|
||||
}
|
||||
}
|
||||
|
||||
export function computeKatodMesVisualYBounds(prices: number[]): { min?: number; max?: number } {
|
||||
const values = prices.filter((value) => Number.isFinite(value))
|
||||
if (values.length < 2) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const dataMin = Math.min(...values)
|
||||
const dataMax = Math.max(...values)
|
||||
const mid = (dataMin + dataMax) / 2
|
||||
let span = dataMax - dataMin
|
||||
|
||||
// حداقل بازه عمودی: ~۰٫۵٪ قیمت میانی (یا ۸۰هزار ریال) تا خط خیلی صاف نباشد
|
||||
const minSpan = Math.max(mid * 0.005, 80_000)
|
||||
if (span < minSpan) {
|
||||
span = minSpan
|
||||
}
|
||||
|
||||
const padding = span * 0.08
|
||||
const center = (dataMin + dataMax) / 2
|
||||
const padding = Math.max((dataMax - dataMin) * 0.06, 500_000)
|
||||
|
||||
return {
|
||||
min: Math.floor(center - span / 2 - padding),
|
||||
max: Math.ceil(center + span / 2 + padding),
|
||||
min: Math.floor(dataMin - padding),
|
||||
max: Math.ceil(dataMax + padding),
|
||||
}
|
||||
}
|
||||
|
||||
/** بزرگنمایی انحراف از میانگین — خط نمودار پرتحرکتر میشود بدون تغییر قیمت واقعی در tooltip */
|
||||
export function amplifyChartDeviation(
|
||||
points: ChartPoint[],
|
||||
factor = KATOD_CHART_DEVIATION_FACTOR
|
||||
): ChartPoint[] {
|
||||
if (points.length < 2) {
|
||||
return points
|
||||
}
|
||||
|
||||
const values = points.map((point) => point[1])
|
||||
const mean = values.reduce((sum, value) => sum + value, 0) / values.length
|
||||
|
||||
return points.map(([timestamp, value]) => [
|
||||
timestamp,
|
||||
Math.round(mean + (value - mean) * factor),
|
||||
])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user