| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932 |
- import 'dart:async';
- import 'dart:math' as math;
- import 'package:flutter/foundation.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:dio/dio.dart' show DioException, DioExceptionType;
- import '../core/network/api_response.dart';
- import '../core/network/dio_client.dart';
- import '../data/services/futures_service.dart';
- import 'app_provider.dart';
- import 'auth_provider.dart';
- import 'copy_trading_provider.dart';
- import 'market_detail_provider.dart';
- import 'ws_provider.dart';
- enum OrderSide { long, short }
- enum OrderType { market, limit, conditionalMarket, conditionalLimit }
- enum PositionMode { open, close }
- enum MarginMode { cross, isolated, split }
- enum AmountUnit { lots, usdt, btc }
- enum FuturesTab { positions, orders, assets }
- bool _isFuturesTabActive(int tabIndex) => tabIndex == 2 || tabIndex == 3;
- class FuturesPosition {
- final String id;
- final String symbol;
- final OrderSide side;
- final double size; // 持仓量(基础币)
- final double availableSize; // 可平仓位
- final double entryPrice;
- final double markPrice;
- final double leverage;
- final double unrealizedPnl;
- final double liquidationPrice;
- final double margin;
- final String marginMode;
- final double? profitPrice;
- final double? lossPrice;
- final int? cutEntrustId; // 止盈止损委托单 ID,有值时修改走 modify-cut
- final double contractSize; // 每张合约对应基础币数量
- final double? apiMarginRate;
- final double? apiCurrentMargin;
- final double realizedPnl;
- final double commissionFee; // 累计手续费(JSON: commissionFee)
- const FuturesPosition({
- required this.id,
- required this.symbol,
- required this.side,
- required this.size,
- required this.availableSize,
- required this.entryPrice,
- required this.markPrice,
- required this.leverage,
- required this.unrealizedPnl,
- required this.liquidationPrice,
- required this.margin,
- this.marginMode = '全仓',
- this.profitPrice,
- this.lossPrice,
- this.cutEntrustId,
- this.contractSize = 1.0,
- this.apiMarginRate,
- this.apiCurrentMargin,
- this.realizedPnl = 0.0,
- this.commissionFee = 0.0,
- });
- double get lots => contractSize > 0 ? size / contractSize : size;
- double get currentMargin => (size > 0 && markPrice > 0 && leverage > 0)
- ? (size * markPrice) / leverage
- : margin;
- double get roe {
- final denom = (apiCurrentMargin != null && apiCurrentMargin! > 0)
- ? apiCurrentMargin!
- : currentMargin;
- return denom > 0 ? (unrealizedPnl / denom) * 100 : 0;
- }
- // 保证金比率 = 保证金 / 合约账户权益 × 100,不超过100%
- double get marginRatio {
- if (apiMarginRate != null && apiMarginRate! > 0) return apiMarginRate!;
- if (margin > 0 && (margin + unrealizedPnl) > 0) {
- return (margin / (margin + unrealizedPnl) * 100).clamp(0.0, 100.0);
- }
- return 0;
- }
- FuturesPosition withMarkPrice(double price) {
- return FuturesPosition(
- id: id,
- symbol: symbol,
- side: side,
- size: size,
- availableSize: availableSize,
- entryPrice: entryPrice,
- markPrice: price,
- leverage: leverage,
- unrealizedPnl: unrealizedPnl,
- liquidationPrice: liquidationPrice,
- margin: margin,
- marginMode: marginMode,
- profitPrice: profitPrice,
- lossPrice: lossPrice,
- cutEntrustId: cutEntrustId,
- contractSize: contractSize,
- apiMarginRate: apiMarginRate,
- apiCurrentMargin: apiCurrentMargin,
- realizedPnl: realizedPnl,
- commissionFee: commissionFee,
- );
- }
- FuturesPosition withContractSize(double cs) => FuturesPosition(
- id: id,
- symbol: symbol,
- side: side,
- size: size,
- availableSize: availableSize,
- entryPrice: entryPrice,
- markPrice: markPrice,
- leverage: leverage,
- unrealizedPnl: unrealizedPnl,
- liquidationPrice: liquidationPrice,
- margin: margin,
- marginMode: marginMode,
- profitPrice: profitPrice,
- lossPrice: lossPrice,
- cutEntrustId: cutEntrustId,
- contractSize: cs,
- apiMarginRate: apiMarginRate,
- apiCurrentMargin: apiCurrentMargin,
- realizedPnl: realizedPnl,
- commissionFee: commissionFee,
- );
- /// 从 /swap/wallet-new/get-with-positions 的 currentPositionWithCutList 解析
- factory FuturesPosition.fromJson(Map<String, dynamic> json) {
- final coin = json['coin'] as Map<String, dynamic>? ?? {};
- final symbol = coin['symbol'] as String? ?? '';
- final direction = json['direction'] as String? ?? 'BUY';
- final side = direction == 'BUY' ? OrderSide.long : OrderSide.short;
- final size = _toDouble(json['currentPosition']);
- final frozen = _toDouble(json['frozenPosition']);
- final explicitAvail = _toDouble(json['availablePosition']);
- final available = explicitAvail > 0
- ? explicitAvail
- : (size - frozen).clamp(0.0, double.infinity);
- final entryPrice = _toDouble(json['openPrice']);
- final leverage = _toDouble(json['leverage']);
- final currentPrice = _toDouble(json['currentPrice'] ?? entryPrice);
- final pnl = json['currentProfit'] != null
- ? _toDouble(json['currentProfit'])
- : (side == OrderSide.long
- ? (currentPrice - entryPrice) * size
- : (entryPrice - currentPrice) * size);
- // type 1 → 分仓;否则 → 全仓
- final posType = json['type'];
- final String marginMode = (posType == 1 || posType == '1') ? '分仓' : '全仓';
- // cutList 为止盈止损委托单列表,取第一条作为回显数据源
- final cutList = json['cutList'];
- Map<String, dynamic>? cutOrder;
- if (cutList is List && cutList.isNotEmpty) {
- cutOrder = Map<String, dynamic>.from(cutList.first as Map);
- }
- final rawProfit = cutOrder != null
- ? _toDouble(cutOrder['profitPrice'])
- : _toDouble(json['profitPrice']);
- final rawLoss = cutOrder != null
- ? _toDouble(cutOrder['lossPrice'])
- : _toDouble(json['lossPrice']);
- final cutEntrustId = cutOrder != null
- ? (cutOrder['id'] is int
- ? cutOrder['id'] as int
- : int.tryParse('${cutOrder['id'] ?? ''}'))
- : null;
- final rawMarginRate = json['marginRate'];
- final apiMarginRate = rawMarginRate != null ? _toDouble(rawMarginRate) : null;
- final rawCurrentMargin = _toDouble(json['currentMargin']);
- final realizedPnl = _toDouble(
- json['usdtProfit'] ?? json['realizedPnl'] ?? json['realizedProfit'] ?? 0);
- return FuturesPosition(
- id: json['id']?.toString() ?? '',
- symbol: symbol,
- side: side,
- size: size,
- availableSize: available,
- entryPrice: entryPrice,
- markPrice: currentPrice,
- leverage: leverage,
- unrealizedPnl: pnl,
- liquidationPrice: _calcLiquidationPrice(
- apiValue: _toDouble(json['estimatedBlastPrice']),
- ),
- margin: _toDouble(json['principalAmount']),
- marginMode: marginMode,
- profitPrice: rawProfit > 0 ? rawProfit : null,
- lossPrice: rawLoss > 0 ? rawLoss : null,
- cutEntrustId: cutEntrustId,
- apiMarginRate: apiMarginRate != null && apiMarginRate > 0 ? apiMarginRate : null,
- apiCurrentMargin: rawCurrentMargin > 0 ? rawCurrentMargin : null,
- realizedPnl: realizedPnl,
- commissionFee: _toDouble(json['commissionFee']),
- );
- }
- /// 从 /swap/order/history 历史仓位记录解析
- factory FuturesPosition.fromHistoryJson(Map<String, dynamic> json) {
- final rawSym = (json['symbol'] ?? json['coinSymbol'] ?? '').toString();
- final direction = (json['direction'] ?? 'BUY').toString().toUpperCase();
- final side = direction == 'BUY' || direction == '0'
- ? OrderSide.long
- : OrderSide.short;
- final size = _toDouble(json['tradedVolume'] ?? json['volume'] ?? 0);
- final entryPrice = _toDouble(json['usdtOpenPrice'] ?? json['openPrice'] ?? 0);
- final closePrice = _toDouble(json['tradedPrice'] ?? json['closePrice'] ?? entryPrice);
- final leverage = _toDouble(json['leverage'] ?? 20);
- final realizedPnl = _toDouble(
- json['profitAndLoss'] ?? json['profit'] ??
- json['realizedPnl'] ?? json['realizedProfit'] ??
- json['closeProfitLoss'] ?? 0);
- final marginMode = _mapHistoryPositionType(json['positionType'], json['patterns']);
- return FuturesPosition(
- id: json['id']?.toString() ?? '',
- symbol: rawSym,
- side: side,
- size: size,
- availableSize: 0,
- entryPrice: entryPrice,
- markPrice: closePrice,
- leverage: leverage,
- unrealizedPnl: 0,
- liquidationPrice: 0,
- margin: _toDouble(json['principalAmount'] ?? 0),
- marginMode: marginMode,
- realizedPnl: realizedPnl,
- );
- }
- static String _mapHistoryPositionType(dynamic posType, dynamic patterns) {
- final pt = posType?.toString() ?? '';
- if (pt == '0') return '全仓';
- if (pt == '1') return '逐仓';
- final p = (patterns?.toString() ?? '').toUpperCase();
- if (p == 'ISOLATED') return '逐仓';
- return '全仓';
- }
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is FuturesPosition &&
- id == other.id &&
- symbol == other.symbol &&
- side == other.side &&
- size == other.size &&
- entryPrice == other.entryPrice &&
- markPrice == other.markPrice &&
- leverage == other.leverage &&
- unrealizedPnl == other.unrealizedPnl &&
- liquidationPrice == other.liquidationPrice &&
- margin == other.margin &&
- marginMode == other.marginMode;
- @override
- int get hashCode => Object.hash(id, symbol, side, size, entryPrice,
- markPrice, leverage, unrealizedPnl, liquidationPrice, margin, marginMode);
- }
- class FuturesOrder {
- final String id;
- final String symbol;
- final OrderSide side;
- final OrderType type;
- final double price; // 委托价(entrustPrice)
- final double tradedPrice; // 成交均价(tradedPrice)
- final double triggerPrice; // 计划委托触发价
- final double size;
- final double filledSize;
- final String status;
- final String action; // 开多/开空/平多/平空
- final String marginMode;
- final double leverage;
- final DateTime? createTime;
- final DateTime? dealTime; // 成交时间
- final double? profitPrice; // 止盈价(>0 时有效)
- final double? lossPrice; // 止损价(>0 时有效)
- final double fee; // 手续费(openFee + closeFee)
- const FuturesOrder({
- required this.id,
- required this.symbol,
- required this.side,
- required this.type,
- required this.price,
- required this.size,
- required this.filledSize,
- required this.status,
- this.tradedPrice = 0,
- this.triggerPrice = 0,
- this.action = '开多',
- this.marginMode = '全仓',
- this.leverage = 20,
- this.createTime,
- this.dealTime,
- this.profitPrice,
- this.lossPrice,
- this.fee = 0,
- });
- /// 是否为开仓方向(开多/开空)
- bool get isOpenOrder => action.contains('开');
- /// 是否处于委托中(可撤销)状态
- bool get isPending => status == '委托中';
- /// 展示用类型标签
- String get typeLabel {
- switch (type) {
- case OrderType.market: return '市价';
- case OrderType.limit: return '限价';
- case OrderType.conditionalMarket:
- case OrderType.conditionalLimit: return '计划委托';
- }
- }
- String get priceDisplay {
- String fmt(double v) =>
- v == v.truncateToDouble() ? '${v.toInt()}' : v.toString();
- switch (type) {
- case OrderType.market:
- case OrderType.conditionalMarket:
- return '市价';
- case OrderType.limit:
- return price > 0 ? fmt(price) : '--';
- case OrderType.conditionalLimit:
- // 计划市价:entrustPrice=0;计划限价:entrustPrice=实际限价
- return price > 0 ? fmt(price) : '市价';
- }
- }
- /// 从 /swap/order/current 解析
- factory FuturesOrder.fromJson(Map<String, dynamic> json) {
- // direction 可能是字符串 "BUY"/"SELL" 或整数 0/1
- final dirRaw = json['direction'];
- final bool isLong;
- if (dirRaw is int) {
- isLong = dirRaw == 0; // 0=买多,1=卖空
- } else {
- isLong = (dirRaw as String? ?? 'BUY') == 'BUY';
- }
- final side = isLong ? OrderSide.long : OrderSide.short;
- // type 可能是字符串枚举或整数(0=市价,1=限价,2=计划委托)
- // 计划委托中 entrustPrice=0 为计划市价,entrustPrice>0 为计划限价
- // 注意:不能用 json['price'] 作为 fallback,否则市价单会被误判为限价单
- final typeRaw = json['type'];
- final entrustPriceRaw = _toDouble(json['entrustPrice']);
- final OrderType orderType;
- if (typeRaw is int) {
- orderType = switch (typeRaw) {
- 0 => OrderType.market,
- 2 => entrustPriceRaw > 0
- ? OrderType.conditionalLimit
- : OrderType.conditionalMarket,
- _ => OrderType.limit,
- };
- } else {
- final s = (typeRaw as String? ?? '').toUpperCase();
- if (s == 'SPOT_LIMIT' || s == 'PLAN') {
- orderType = entrustPriceRaw > 0
- ? OrderType.conditionalLimit
- : OrderType.conditionalMarket;
- } else {
- orderType = s == 'MARKET_PRICE' || s == 'MARKET'
- ? OrderType.market
- : OrderType.limit;
- }
- }
- // entrustType:0=OPEN(开仓),1=CLOSE(平仓)
- // 注意:positionType 是全仓/逐仓模式,不是开平仓标志,不要用它判断
- final entrustTypeRaw = json['entrustType'];
- final bool isClose;
- if (entrustTypeRaw is int) {
- isClose = entrustTypeRaw == 1;
- } else if (entrustTypeRaw is String) {
- final et = entrustTypeRaw.toUpperCase();
- isClose = et == '1' || et == 'CLOSE';
- } else {
- // entrustType 不存在时,看 closeType 是否有值(有值说明是平仓单)
- final closeTypeRaw = json['closeType'];
- isClose = closeTypeRaw != null;
- }
- final action = isClose
- ? (isLong ? '平多' : '平空')
- : (isLong ? '开多' : '开空');
- // createTime 为 13 位毫秒时间戳
- final ts = json['createTime'];
- DateTime? createTime;
- if (ts is int) {
- createTime = DateTime.fromMillisecondsSinceEpoch(ts);
- } else if (ts is String) {
- createTime = DateTime.tryParse(ts);
- }
- // dealTime:成交时间(Long 毫秒时间戳)
- final dealTs = json['dealTime'];
- DateTime? dealTime;
- if (dealTs is int && dealTs > 0) {
- dealTime = DateTime.fromMillisecondsSinceEpoch(dealTs);
- } else if (dealTs is String) {
- dealTime = DateTime.tryParse(dealTs);
- }
- // status 可能是字符串或整数
- final statusRaw = json['status'];
- final status = _mapStatus(statusRaw is String ? statusRaw : statusRaw?.toString() ?? '');
- // symbol 可能是直接字段(EntrustBean),也可能嵌套在 coin 中(position list)
- final coin = json['coin'] as Map<String, dynamic>? ?? {};
- final symbol = (json['symbol'] as String?)
- ?? (coin['symbol'] as String?)
- ?? '';
- // 止盈止损(>0 时有效)
- final rawProfit = _toDouble(json['profitPrice'] ?? json['stopProfitPrice']);
- final rawLoss = _toDouble(json['lossPrice'] ?? json['stopLossPrice']);
- return FuturesOrder(
- id: json['id']?.toString() ?? '',
- symbol: symbol,
- side: side,
- type: orderType,
- price: _toDouble(json['entrustPrice']),
- tradedPrice: _toDouble(json['tradedPrice']),
- triggerPrice: _toDouble(json['triggerPrice']),
- size: _toDouble(json['volume'] ?? json['size']),
- filledSize: _toDouble(json['tradedVolume'] ?? json['filledSize']),
- status: status,
- action: action,
- marginMode: _parsePatterns(json['patterns']),
- leverage: _toDouble(json['leverage'] ?? 20).toInt().toDouble(),
- createTime: createTime,
- dealTime: dealTime,
- profitPrice: rawProfit > 0 ? rawProfit : null,
- lossPrice: rawLoss > 0 ? rawLoss : null,
- fee: _toDouble(json['openFee']) + _toDouble(json['closeFee']),
- );
- }
- /// 从 /swap/order/history-open 历史委托记录解析
- factory FuturesOrder.fromHistoryJson(Map<String, dynamic> json) {
- final rawSym = (json['symbol'] ?? json['coinSymbol'] ?? '').toString();
- final dirRaw = json['direction'];
- final bool isLong;
- if (dirRaw is int) {
- isLong = dirRaw == 0;
- } else {
- final s = (dirRaw?.toString() ?? 'BUY').toUpperCase();
- isLong = s == 'BUY' || s == '0';
- }
- final side = isLong ? OrderSide.long : OrderSide.short;
- // 使用 type 字段判断委托类型;openType 是仓位开仓方式,平仓单中继承自仓位,不能用于判断平仓单类型
- final typeRaw = json['type'];
- final entrustPriceRaw = _toDouble(json['entrustPrice']);
- final OrderType orderType;
- if (typeRaw is int) {
- orderType = switch (typeRaw) {
- 0 => OrderType.market,
- 2 => entrustPriceRaw > 0
- ? OrderType.conditionalLimit
- : OrderType.conditionalMarket,
- _ => OrderType.limit,
- };
- } else {
- final s = (typeRaw?.toString() ?? '').toUpperCase();
- if (s == 'SPOT_LIMIT' || s == 'PLAN') {
- orderType = entrustPriceRaw > 0
- ? OrderType.conditionalLimit
- : OrderType.conditionalMarket;
- } else {
- orderType = (s == 'MARKET_PRICE' || s == 'MARKET')
- ? OrderType.market
- : OrderType.limit;
- }
- }
- // 历史记录同样用 entrustType 判断开平仓,fallback 看 closeType 是否有值
- final entrustTypeRaw = json['entrustType'];
- final bool isClose;
- if (entrustTypeRaw is int) {
- isClose = entrustTypeRaw == 1;
- } else if (entrustTypeRaw is String) {
- final et = entrustTypeRaw.toUpperCase();
- isClose = et == '1' || et == 'CLOSE';
- } else {
- isClose = json['closeType'] != null;
- }
- final action = isClose
- ? (isLong ? '平多' : '平空')
- : (isLong ? '开多' : '开空');
- final ts = json['createTime'] ?? json['usdtOpenTime'];
- DateTime? createTime;
- if (ts is int) {
- createTime = DateTime.fromMillisecondsSinceEpoch(ts);
- } else if (ts is String) {
- createTime = DateTime.tryParse(ts);
- }
- final dealTs = json['dealTime'];
- DateTime? dealTime;
- if (dealTs is int && dealTs > 0) {
- dealTime = DateTime.fromMillisecondsSinceEpoch(dealTs);
- } else if (dealTs is String) {
- dealTime = DateTime.tryParse(dealTs);
- }
- final statusRaw = json['status'];
- final status = _mapStatus(statusRaw is String ? statusRaw : statusRaw?.toString() ?? '');
- return FuturesOrder(
- id: json['id']?.toString() ?? '',
- symbol: rawSym,
- side: side,
- type: orderType,
- price: _toDouble(json['entrustPrice'] ?? 0),
- tradedPrice: _toDouble(json['tradedPrice'] ?? 0),
- triggerPrice: _toDouble(json['triggerPrice'] ?? 0),
- size: _toDouble(json['volume'] ?? json['size'] ?? 0),
- filledSize: _toDouble(json['tradedVolume'] ?? json['dealVolume'] ?? 0),
- status: status,
- action: action,
- marginMode: _parsePatterns(json['patterns']),
- leverage: _toDouble(json['leverage'] ?? 20).toInt().toDouble(),
- createTime: createTime,
- dealTime: dealTime,
- fee: _toDouble(json['openFee']) + _toDouble(json['closeFee']),
- );
- }
- static String _mapStatus(String raw) {
- return switch (raw) {
- 'ENTRUST_ING' => '委托中',
- 'ENTRUST_SUCCESS' => '已成交',
- 'ENTRUST_CANCEL' => '已撤销',
- _ => raw,
- };
- }
- /// 解析仓位模式:CROSSED=全仓,FIXED=分仓;ordinal 0=全仓,1=分仓
- static String _parsePatterns(dynamic raw) {
- if (raw == null) return '全仓';
- final s = raw.toString().toUpperCase();
- if (s == 'FIXED' || s == '1') return '分仓';
- return '全仓';
- }
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is FuturesOrder &&
- id == other.id &&
- symbol == other.symbol &&
- side == other.side &&
- type == other.type &&
- price == other.price &&
- triggerPrice == other.triggerPrice &&
- size == other.size &&
- filledSize == other.filledSize &&
- status == other.status &&
- action == other.action &&
- marginMode == other.marginMode &&
- leverage == other.leverage &&
- profitPrice == other.profitPrice &&
- lossPrice == other.lossPrice;
- @override
- int get hashCode => Object.hash(id, symbol, side, type, price, triggerPrice,
- size, filledSize, status, action, marginMode, leverage,
- profitPrice, lossPrice);
- }
- class FuturesAccountInfo {
- final double totalBalance;
- final double availableMargin;
- final double usedMargin;
- final double unrealizedPnl;
- const FuturesAccountInfo({
- required this.totalBalance,
- required this.availableMargin,
- required this.usedMargin,
- required this.unrealizedPnl,
- });
- factory FuturesAccountInfo.fromJson(Map<String, dynamic> json,
- {double unrealizedPnl = 0, double positionsMargin = 0}) {
- // currentCapital = 账户当前权益(合约账户总余额)
- // 已用保证金 = 持仓 principalAmount 之和(最准确),其次 frozenMargin
- // 可用保证金 = 总余额 - 已用保证金
- final total = _toDouble(json['currentCapital'] ?? json['balance']);
- final frozen = _toDouble(json['frozenMargin'] ?? json['frozenBalance']);
- final usedMargin = positionsMargin > 0
- ? positionsMargin
- : frozen > 0
- ? frozen
- : 0.0;
- final available = (total - usedMargin).clamp(0.0, double.infinity);
- return FuturesAccountInfo(
- totalBalance: total,
- usedMargin: usedMargin,
- availableMargin: available,
- unrealizedPnl: unrealizedPnl,
- );
- }
- const FuturesAccountInfo.empty()
- : totalBalance = 0,
- availableMargin = 0,
- usedMargin = 0,
- unrealizedPnl = 0;
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is FuturesAccountInfo &&
- totalBalance == other.totalBalance &&
- availableMargin == other.availableMargin &&
- usedMargin == other.usedMargin &&
- unrealizedPnl == other.unrealizedPnl;
- @override
- int get hashCode =>
- Object.hash(totalBalance, availableMargin, usedMargin, unrealizedPnl);
- }
- class FuturesState {
- final String symbol;
- final int contractCoinId;
- final double contractSize; // 每张对应基础币数量
- final int volScale; // 张数精度
- final String coinSymbol; // 基础资产名称
- final double lastPrice;
- final String? lastPriceStr; // WS 返回的原始价格字符串
- final double markPrice;
- final double change24h;
- final double leverage;
- final MarginMode marginMode;
- final PositionMode positionMode;
- final OrderSide orderSide;
- final OrderType orderType;
- final AmountUnit amountUnit;
- final double inputPrice;
- final double inputSize;
- final double sliderPercent;
- final bool isSliderInput; // true=滑块输入,false=手动输入
- final bool tpslEnabled;
- final double? tpPrice;
- final double? slPrice;
- final double fundingRate;
- final String fundingCountdown;
- final FuturesTab activeTab;
- final List<FuturesPosition> positions;
- final List<FuturesOrder> openOrders;
- final bool ordersHasMore;
- final int ordersPage;
- final bool ordersLoadingMore;
- final FuturesAccountInfo accountInfo;
- final bool isLoading; // 首屏骨架
- final bool isTabLoading; // 底部列表骨架
- final bool isSwitchingMode; // 模式切换中(切换完成前禁止下单)
- final String? errorMsg;
- final List<Map<String, dynamic>> orderBookAsks;
- final List<Map<String, dynamic>> orderBookBids;
- final bool hideOtherSymbols;
- final int pricePrecision;
- final int coinPrecision;
- final int usdtPrecision;
- final int leverageMin;
- final int leverageMax;
- final List<int> leverageOptions;
- final bool isDiscreteLeverage; // true=固定档位,false=区间任意值
- final double openFeeRate;
- const FuturesState({
- required this.symbol,
- this.contractCoinId = 0,
- this.contractSize = 1.0,
- this.volScale = 0,
- this.coinSymbol = '',
- required this.lastPrice,
- this.lastPriceStr,
- required this.markPrice,
- this.change24h = 0,
- this.leverage = 20,
- this.marginMode = MarginMode.cross,
- this.positionMode = PositionMode.open,
- this.orderSide = OrderSide.long,
- this.orderType = OrderType.market,
- this.amountUnit = AmountUnit.btc,
- this.inputPrice = 0,
- this.inputSize = 0,
- this.sliderPercent = 0,
- this.isSliderInput = false,
- this.tpslEnabled = false,
- this.tpPrice,
- this.slPrice,
- this.fundingRate = 0,
- this.fundingCountdown = '--:--:--',
- this.activeTab = FuturesTab.positions,
- required this.positions,
- required this.openOrders,
- this.ordersHasMore = false,
- this.ordersPage = 1,
- this.ordersLoadingMore = false,
- required this.accountInfo,
- this.isLoading = false,
- this.isTabLoading = false,
- this.isSwitchingMode = false,
- this.errorMsg,
- this.orderBookAsks = const [],
- this.orderBookBids = const [],
- this.hideOtherSymbols = false,
- this.pricePrecision = 2,
- this.coinPrecision = 4,
- this.usdtPrecision = 2,
- this.leverageMin = 1,
- this.leverageMax = 125,
- this.leverageOptions = const [],
- this.isDiscreteLeverage = false,
- this.openFeeRate = 0.0005,
- });
- /// 当前数量单位对应的精度
- int get currentAmountPrecision {
- switch (amountUnit) {
- case AmountUnit.lots:
- return 0;
- case AmountUnit.btc:
- return coinPrecision;
- case AmountUnit.usdt:
- return usdtPrecision;
- }
- }
- static String _norm(String s) => s.replaceAll('/', '').toUpperCase();
- List<FuturesPosition> get displayPositions {
- if (!hideOtherSymbols) return positions;
- final cur = _norm(symbol);
- return positions.where((p) => _norm(p.symbol) == cur).toList();
- }
- List<FuturesOrder> get displayOrders {
- if (!hideOtherSymbols) return openOrders;
- final cur = _norm(symbol);
- return openOrders.where((o) => _norm(o.symbol) == cur).toList();
- }
- String get orderTypeLabel {
- switch (orderType) {
- case OrderType.market:
- return '市价单';
- case OrderType.limit:
- return '限价单';
- case OrderType.conditionalMarket:
- return '市价条件委托单';
- case OrderType.conditionalLimit:
- return '限价条件委托单';
- }
- }
- String get marginModeLabel => switch (marginMode) {
- MarginMode.cross => '全仓',
- MarginMode.isolated => '逐仓',
- MarginMode.split => '分仓',
- };
- String get amountUnitLabel {
- switch (amountUnit) {
- case AmountUnit.lots:
- return '张';
- case AmountUnit.usdt:
- return 'USDT';
- case AmountUnit.btc:
- if (coinSymbol.isNotEmpty) return coinSymbol;
- final base = symbol.toUpperCase().replaceFirst(RegExp(r'USDT$'), '');
- return base.isNotEmpty ? base : 'BTC';
- }
- }
- bool get isConditionalOrder =>
- orderType == OrderType.conditionalMarket ||
- orderType == OrderType.conditionalLimit;
- bool get showPriceInput =>
- orderType == OrderType.limit || orderType == OrderType.conditionalLimit;
- FuturesState copyWith({
- String? symbol,
- int? contractCoinId,
- double? contractSize,
- int? volScale,
- String? coinSymbol,
- double? lastPrice,
- String? lastPriceStr,
- double? markPrice,
- double? change24h,
- double? leverage,
- MarginMode? marginMode,
- PositionMode? positionMode,
- OrderSide? orderSide,
- OrderType? orderType,
- AmountUnit? amountUnit,
- double? inputPrice,
- double? inputSize,
- double? sliderPercent,
- bool? isSliderInput,
- bool? tpslEnabled,
- double? tpPrice,
- double? slPrice,
- double? fundingRate,
- String? fundingCountdown,
- FuturesTab? activeTab,
- List<FuturesPosition>? positions,
- List<FuturesOrder>? openOrders,
- bool? ordersHasMore,
- int? ordersPage,
- bool? ordersLoadingMore,
- FuturesAccountInfo? accountInfo,
- bool? isLoading,
- bool? isTabLoading,
- bool? isSwitchingMode,
- String? errorMsg,
- List<Map<String, dynamic>>? orderBookAsks,
- List<Map<String, dynamic>>? orderBookBids,
- bool? hideOtherSymbols,
- int? pricePrecision,
- int? coinPrecision,
- int? usdtPrecision,
- int? leverageMin,
- int? leverageMax,
- List<int>? leverageOptions,
- bool? isDiscreteLeverage,
- double? openFeeRate,
- }) =>
- FuturesState(
- symbol: symbol ?? this.symbol,
- contractCoinId: contractCoinId ?? this.contractCoinId,
- contractSize: contractSize ?? this.contractSize,
- volScale: volScale ?? this.volScale,
- coinSymbol: coinSymbol ?? this.coinSymbol,
- lastPrice: lastPrice ?? this.lastPrice,
- lastPriceStr: lastPriceStr ?? this.lastPriceStr,
- markPrice: markPrice ?? this.markPrice,
- change24h: change24h ?? this.change24h,
- leverage: leverage ?? this.leverage,
- marginMode: marginMode ?? this.marginMode,
- positionMode: positionMode ?? this.positionMode,
- orderSide: orderSide ?? this.orderSide,
- orderType: orderType ?? this.orderType,
- amountUnit: amountUnit ?? this.amountUnit,
- inputPrice: inputPrice ?? this.inputPrice,
- inputSize: inputSize ?? this.inputSize,
- sliderPercent: sliderPercent ?? this.sliderPercent,
- isSliderInput: isSliderInput ?? this.isSliderInput,
- tpslEnabled: tpslEnabled ?? this.tpslEnabled,
- tpPrice: tpPrice ?? this.tpPrice,
- slPrice: slPrice ?? this.slPrice,
- fundingRate: fundingRate ?? this.fundingRate,
- fundingCountdown: fundingCountdown ?? this.fundingCountdown,
- activeTab: activeTab ?? this.activeTab,
- positions: positions ?? this.positions,
- openOrders: openOrders ?? this.openOrders,
- ordersHasMore: ordersHasMore ?? this.ordersHasMore,
- ordersPage: ordersPage ?? this.ordersPage,
- ordersLoadingMore: ordersLoadingMore ?? this.ordersLoadingMore,
- accountInfo: accountInfo ?? this.accountInfo,
- isLoading: isLoading ?? this.isLoading,
- isTabLoading: isTabLoading ?? this.isTabLoading,
- isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
- errorMsg: errorMsg,
- orderBookAsks: orderBookAsks ?? this.orderBookAsks,
- orderBookBids: orderBookBids ?? this.orderBookBids,
- hideOtherSymbols: hideOtherSymbols ?? this.hideOtherSymbols,
- pricePrecision: pricePrecision ?? this.pricePrecision,
- coinPrecision: coinPrecision ?? this.coinPrecision,
- usdtPrecision: usdtPrecision ?? this.usdtPrecision,
- leverageMin: leverageMin ?? this.leverageMin,
- leverageMax: leverageMax ?? this.leverageMax,
- leverageOptions: leverageOptions ?? this.leverageOptions,
- isDiscreteLeverage: isDiscreteLeverage ?? this.isDiscreteLeverage,
- openFeeRate: openFeeRate ?? this.openFeeRate,
- );
- }
- class FuturesNotifier extends AutoDisposeFamilyNotifier<FuturesState, String> {
- StreamSubscription<Map<String, dynamic>>? _tickerSub;
- StreamSubscription<Map<String, dynamic>>? _depthSub;
- StreamSubscription<Map<String, dynamic>>? _markSub;
- Timer? _fundingTimer;
- Timer? _pollTimer;
- bool _manuallyPaused = false; // 子页面推入时手动暂停,防止 tab 切换后误恢复
- int _nextSettleTimeMs = 0;
- final Map<String, int> _lastTickerUpdateMs = {}; // ticker 节流
- int _lastDepthUpdateMs = 0; // 盘口节流
- int _lastPositionTickUpdateMs = 0; // 持仓 mark price 节流(ticker 路径)
- Timer? _depthTimer;
- List<Map<String, dynamic>>? _pendingAsks;
- List<Map<String, dynamic>>? _pendingBids;
- // 切换到平仓模式时,若原 orderType 为计划委托类型则暂存,切回开仓时恢复
- OrderType? _savedConditionalType;
- // 路由 symbol(如 BTCUSDT)转 API 格式(BTC/USDT)
- static String _toApiSymbol(String symbol) {
- String s = symbol.replaceAll('-', '/');
- if (!s.contains('/')) {
- for (final base in ['USDT', 'BTC', 'ETH', 'BUSD']) {
- if (s.endsWith(base) && s.length > base.length) {
- return '${s.substring(0, s.length - base.length)}/$base';
- }
- }
- }
- return s;
- }
- @override
- FuturesState build(String symbol) {
- ref.onDispose(_dispose);
- // 读取该交易对上次用户选择的保证金模式(跨 symbol 切换时保留)
- final savedModeStr = ref.read(sharedPreferencesProvider)
- .getString('futures_margin_mode_$symbol');
- final savedMode = switch (savedModeStr) {
- 'split' => MarginMode.split,
- 'isolated' => MarginMode.isolated,
- _ => MarginMode.cross,
- };
- final initial = FuturesState(
- symbol: symbol,
- lastPrice: 0,
- markPrice: 0,
- positions: const [],
- openOrders: const [],
- accountInfo: const FuturesAccountInfo.empty(),
- isLoading: true,
- amountUnit: ref.read(futuresAmountUnitPrefProvider),
- hideOtherSymbols: ref.read(futuresHideOtherPrefProvider),
- marginMode: savedMode,
- leverage: 20,
- );
- Future.microtask(() => _init(symbol));
- // 监听 K 线行情数据,保持与 K 线页面价格一致
- // 注意:不能用 fireImmediately: true,build() 返回前 state 尚未初始化
- ref.listen<MarketStats?>(
- marketDetailProvider(MarketDetailKey(symbol: symbol, isFutures: true))
- .select((s) => s.stats),
- (_, stats) {
- if (stats == null || stats.lastPrice <= 0) return;
- state = state.copyWith(
- lastPrice: stats.lastPrice,
- change24h: stats.change24h,
- );
- },
- );
- // 监听 WS 重连:重连成功后若合约信息未加载则重新初始化
- ref.listen<AsyncValue<WsConnectionState>>(
- wsConnectionStateProvider,
- (prev, next) {
- final prevState = prev?.valueOrNull;
- final nextState = next.valueOrNull;
- // 任何 → connected 的转换都触发(reconnecting→connecting→connected,prev 是 connecting)
- if (nextState == WsConnectionState.connected &&
- prevState != WsConnectionState.connected) {
- if (state.contractCoinId == 0) {
- _init(symbol);
- }
- }
- },
- );
- // 监听登录状态:登录后自动加载持仓;退出后清空数据并停止轮询
- ref.listen<bool>(isLoggedInProvider, (prev, loggedIn) {
- if (loggedIn) {
- // 登录后刷新带单员身份(用于判断是否显示分仓选项)
- ref.read(copyTradingProvider.notifier).silentRefresh();
- _loadWallet(state.symbol);
- _loadCurrentOrders();
- // 在合约页时直接启动轮询,不区分子 tab
- if (_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
- _startPolling(state.symbol);
- }
- } else {
- _stopPolling();
- state = state.copyWith(
- positions: [],
- openOrders: [],
- accountInfo: const FuturesAccountInfo.empty(),
- );
- }
- });
- // 监听底部导航 tab 切换:离开合约页停止轮询,进入合约页直接启动轮询
- // 注意:若子页面手动暂停了轮询(_manuallyPaused),切回 tab 时不自动恢复
- ref.listen<int>(activeBottomTabProvider, (prev, tabIndex) {
- if (_isFuturesTabActive(tabIndex)) {
- if (ref.read(isLoggedInProvider) && !_manuallyPaused) {
- _loadWallet(state.symbol);
- _loadCurrentOrders();
- _startPolling(state.symbol);
- }
- } else {
- _stopPolling();
- }
- });
- // 监听带单员身份变化:若用户身份为带单员,强制切回全仓模式
- ref.listen<bool>(
- copyTradingProvider.select((s) => s.isTrader),
- (_, isTrader) {
- if (isTrader && state.marginMode == MarginMode.split) {
- state = state.copyWith(marginMode: MarginMode.cross);
- }
- },
- );
- return initial;
- }
- Future<void> _init(String symbol) async {
- await _loadContractInfo(symbol);
- _subscribeWebSocket(symbol);
- state = state.copyWith(isLoading: false, isTabLoading: true);
- if (ref.read(isLoggedInProvider)) {
- try {
- await Future.wait([_loadWallet(symbol), _loadCurrentOrders()]);
- } catch (_) {}
- if (_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
- _startPolling(symbol);
- }
- }
- state = state.copyWith(isTabLoading: false);
- }
- void _startPolling(String symbol) {
- _pollTimer?.cancel();
- _pollTimer = Timer.periodic(const Duration(seconds: 1), (_) {
- if (!_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
- _stopPolling();
- return;
- }
- _loadWallet(symbol);
- _loadCurrentOrders();
- });
- }
- void _stopPolling() {
- _pollTimer?.cancel();
- _pollTimer = null;
- }
- /// 供页面跳转时外部调用:暂停轮询(标记手动暂停,防止 tab 切换后误恢复)
- void stopPolling() {
- _manuallyPaused = true;
- _stopPolling();
- }
- /// 供页面返回时外部调用:恢复轮询(仅登录且在合约页时)
- void resumePolling(String symbol) {
- _manuallyPaused = false;
- if (ref.read(isLoggedInProvider) &&
- _isFuturesTabActive(ref.read(activeBottomTabProvider))) {
- _startPolling(symbol);
- }
- }
- Future<void> _loadContractInfo(String symbol) async {
- try {
- final svc = _service;
- final apiSymbol = _toApiSymbol(symbol);
- final info = await svc.getSymbolInfo(apiSymbol);
- final id = (info['id'] as num?)?.toInt() ?? 0;
- if (id == 0) return;
- final pricePrecision = ((info['coinScale'] ?? info['minScale']) as num?)?.toInt() ?? 2;
- const volScale = 0;
- final coinPrecision = (info['minScale'] as num?)?.toInt() ?? 4;
- final usdtPrecision = (info['baseCoinScale'] as num?)?.toInt() ?? 4;
- final contractSize = _toDouble(
- info['shareNumber'] ?? info['contractSize'] ?? info['shareSize'] ?? 1);
- final coinObj = info['coin'] as Map?;
- final rawSym = coinObj?['symbol'] as String? ?? apiSymbol;
- final coinSymbol = rawSym.contains('/') ? rawSym.split('/').first : rawSym;
- // 杠杆档位(逗号分隔字符串)
- final rawLeverage = info['leverage']?.toString() ?? '';
- final leverageOptions = rawLeverage.isNotEmpty
- ? rawLeverage
- .split(',')
- .map((s) => int.tryParse(s.trim()))
- .whereType<int>()
- .where((v) => v > 0)
- .toList()
- : <int>[];
- leverageOptions.sort();
- final leverageMin = leverageOptions.isNotEmpty ? leverageOptions.first : 1;
- final leverageMax = leverageOptions.isNotEmpty ? leverageOptions.last : 125;
- // leverageType: 1=固定档位,2=区间任意值
- final leverageType = (info['leverageType'] as num?)?.toInt() ?? 2;
- final isDiscrete = leverageType == 1;
- final openFeeRate = _toDouble(info['openFee'] ?? info['makerFee'] ?? 0.0005);
- state = state.copyWith(
- contractCoinId: id,
- pricePrecision: pricePrecision,
- volScale: volScale.toInt(),
- coinPrecision: coinPrecision,
- usdtPrecision: usdtPrecision,
- contractSize: contractSize > 0 ? contractSize : 1.0,
- coinSymbol: coinSymbol,
- leverageMin: leverageMin,
- leverageMax: leverageMax,
- leverageOptions: leverageOptions,
- isDiscreteLeverage: isDiscrete,
- openFeeRate: openFeeRate > 0 ? openFeeRate : 0.0005,
- );
- try {
- final leverage = await svc.getLeverage(id);
- state = state.copyWith(leverage: leverage.toDouble());
- } catch (_) {}
- } catch (_) {}
- }
- Future<void> refreshWallet() => _loadWallet(state.symbol);
- Future<void> _loadWallet(String symbol) async {
- if (!ref.read(isLoggedInProvider)) return;
- final data = await _service
- .getWithPositions(symbol)
- .catchError((_) => <String, dynamic>{});
- final wallet = data;
- final rawPositions = (data['currentPositionWithCutList'] as List<dynamic>? ?? [])
- .cast<Map<String, dynamic>>();
- List<FuturesPosition> positions = [];
- final cs = state.contractSize > 0 ? state.contractSize : 1.0;
- for (final item in rawPositions) {
- try {
- positions.add(FuturesPosition.fromJson(item).withContractSize(cs));
- } catch (e) {
- debugPrint('[FuturesPosition] parse error: $e\nitem: $item');
- }
- }
- // 有匹配当前币对的持仓时,同步杠杆到下单表单
- final currentSymbol = state.symbol.toUpperCase().replaceAll('-', '');
- final matchedPos = positions.where((p) {
- final ps = p.symbol.toUpperCase().replaceAll('-', '').replaceAll('/', '');
- return ps.contains(currentSymbol) || currentSymbol.contains(ps);
- }).toList();
- if (matchedPos.isNotEmpty && matchedPos.first.leverage > 0) {
- state = state.copyWith(
- positions: positions,
- leverage: matchedPos.first.leverage,
- );
- } else {
- state = state.copyWith(positions: positions);
- }
- _subscribePositionTickers(positions);
- try {
- if (wallet.isNotEmpty) {
- final totalPnl = positions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
- final positionsMargin =
- positions.fold(0.0, (sum, p) => sum + p.margin);
- final accountInfo = FuturesAccountInfo.fromJson(
- wallet,
- unrealizedPnl: totalPnl,
- positionsMargin: positionsMargin,
- );
- // marginMode 不从服务器同步(进入合约页默认全仓,用户主动切换才变更)
- final isTrader = ref.read(copyTradingProvider).isTrader;
- final syncedMode = (isTrader && state.marginMode == MarginMode.split)
- ? MarginMode.cross
- : null;
- state = state.copyWith(
- accountInfo: accountInfo,
- marginMode: syncedMode ?? state.marginMode,
- );
- }
- } catch (_) {}
- }
- Future<void> _loadCurrentOrders() async {
- if (!ref.read(isLoggedInProvider)) return;
- try {
- final result = await _service.getCurrentOrders(pageNo: 1, pageSize: 10);
- final orders = _parseOrders(result.items);
- state = state.copyWith(
- openOrders: orders,
- ordersPage: 1,
- ordersHasMore: result.hasMore,
- );
- } catch (_) {}
- }
- Future<void> loadMoreOrders() async {
- if (state.ordersLoadingMore || !state.ordersHasMore) return;
- state = state.copyWith(ordersLoadingMore: true);
- try {
- final nextPage = state.ordersPage + 1;
- final result =
- await _service.getCurrentOrders(pageNo: nextPage, pageSize: 10);
- final more = _parseOrders(result.items);
- state = state.copyWith(
- openOrders: [...state.openOrders, ...more],
- ordersPage: nextPage,
- ordersHasMore: result.hasMore,
- ordersLoadingMore: false,
- );
- } catch (e) {
- state = state.copyWith(ordersLoadingMore: false);
- }
- }
- static List<FuturesOrder> _parseOrders(List<Map<String, dynamic>> raw) {
- final orders = <FuturesOrder>[];
- for (final item in raw) {
- try {
- orders.add(FuturesOrder.fromJson(item));
- } catch (_) {}
- }
- return orders;
- }
- void _subscribeWebSocket(String symbol) {
- final wsSymbol = symbol.replaceAll('-', '').toLowerCase();
- final ws = ref.read(wsClientProvider);
- ws.subscribeTicker(wsSymbol);
- ws.subscribeDepth(wsSymbol);
- ws.subscribeMark(wsSymbol);
- _markSub?.cancel();
- _markSub = ws.markStream
- .where((d) => (d['symbol'] as String? ?? '').toLowerCase() == wsSymbol)
- .listen((data) {
- final markPrice = (data['markPrice'] as num?)?.toDouble() ?? 0;
- final fundingRate = (data['fundingRate'] as num?)?.toDouble() ?? 0;
- final nextFundingTime = (data['nextFundingTime'] as num?)?.toInt() ?? 0;
- if (markPrice > 0) {
- state = state.copyWith(markPrice: markPrice);
- }
- // 新结算时间 > 当前记录时才更新(允许同一周期多次推送相同值)
- if (nextFundingTime > 0 &&
- nextFundingTime > DateTime.now().millisecondsSinceEpoch &&
- nextFundingTime > _nextSettleTimeMs) {
- _nextSettleTimeMs = nextFundingTime;
- _startFundingCountdown();
- }
- state = state.copyWith(fundingRate: fundingRate);
- });
- _tickerSub = ws.tickerStream.listen((data) {
- final s = (data['symbol'] as String? ?? '').toLowerCase();
- final price = (data['price'] as num?)?.toDouble() ?? 0;
- if (price <= 0) return;
- final priceStr = data['priceStr'] as String? ?? '';
- final now = DateTime.now().millisecondsSinceEpoch;
- final isCurrentSymbol = s == wsSymbol;
- // 非当前交易对节流 500ms
- if (!isCurrentSymbol) {
- final last = _lastTickerUpdateMs[s] ?? 0;
- if (now - last < 500) return;
- }
- _lastTickerUpdateMs[s] = now;
- final tickerApiSymbol = _toApiSymbol(s.toUpperCase());
- final tSym = tickerApiSymbol.replaceAll('/', '').toUpperCase();
- final hasMatch = state.positions.any(
- (p) => p.symbol.replaceAll('/', '').toUpperCase() == tSym);
- // 持仓 mark price 更新节流 1000ms,避免每次 WS 推送都重建 positions 数组
- final canUpdatePositions =
- now - _lastPositionTickUpdateMs >= 1000;
- if (isCurrentSymbol) {
- final change = (data['change24h'] as num?)?.toDouble() ?? 0;
- if (hasMatch && canUpdatePositions) {
- _lastPositionTickUpdateMs = now;
- final updatedPositions = state.positions.map((p) {
- return p.symbol.replaceAll('/', '').toUpperCase() == tSym
- ? p.withMarkPrice(price)
- : p;
- }).toList();
- final totalPnl =
- updatedPositions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
- final ai = state.accountInfo;
- state = state.copyWith(
- lastPrice: price,
- lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
- markPrice: price,
- change24h: change,
- positions: updatedPositions,
- accountInfo: FuturesAccountInfo(
- totalBalance: ai.totalBalance,
- usedMargin: ai.usedMargin,
- availableMargin: ai.availableMargin,
- unrealizedPnl: totalPnl,
- ),
- );
- } else {
- state = state.copyWith(
- lastPrice: price,
- lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
- markPrice: price,
- change24h: change,
- );
- }
- } else if (hasMatch && canUpdatePositions) {
- _lastPositionTickUpdateMs = now;
- final updatedPositions = state.positions.map((p) {
- return p.symbol.replaceAll('/', '').toUpperCase() == tSym
- ? p.withMarkPrice(price)
- : p;
- }).toList();
- final totalPnl =
- updatedPositions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
- final ai = state.accountInfo;
- state = state.copyWith(
- positions: updatedPositions,
- accountInfo: FuturesAccountInfo(
- totalBalance: ai.totalBalance,
- usedMargin: ai.usedMargin,
- availableMargin: ai.availableMargin,
- unrealizedPnl: totalPnl,
- ),
- );
- }
- });
- _depthSub = ws.orderBookStream.listen((data) {
- final s = (data['symbol'] as String? ?? '').toLowerCase();
- if (s != wsSymbol) return;
- // 盘口节流 120ms
- final now = DateTime.now().millisecondsSinceEpoch;
- if (now - _lastDepthUpdateMs < 120) return;
- _lastDepthUpdateMs = now;
- final rawAsks = data['asks'] as List? ?? [];
- final rawBids = data['bids'] as List? ?? [];
- _pendingAsks = rawAsks.whereType<Map<String, dynamic>>().toList();
- _pendingBids = rawBids.whereType<Map<String, dynamic>>().toList();
- if (_depthTimer == null || !_depthTimer!.isActive) {
- _depthTimer = Timer(Duration.zero, () {
- final asks = _pendingAsks;
- final bids = _pendingBids;
- _pendingAsks = null;
- _pendingBids = null;
- if (asks != null && bids != null) {
- state = state.copyWith(orderBookAsks: asks, orderBookBids: bids);
- }
- });
- }
- });
- }
- void _subscribePositionTickers(List<FuturesPosition> positions) {
- if (positions.isEmpty) return;
- try {
- final ws = ref.read(wsClientProvider);
- final symbols = positions
- .map((p) => p.symbol.replaceAll('/', '').toLowerCase())
- .toSet()
- .toList();
- ws.subscribeTickerBatch(symbols);
- } catch (_) {}
- }
- void _startFundingCountdown() {
- _fundingTimer?.cancel();
- _updateFundingCountdown();
- _fundingTimer = Timer.periodic(const Duration(seconds: 1), (_) {
- _updateFundingCountdown();
- });
- }
- void _updateFundingCountdown() {
- final remaining = _nextSettleTimeMs - DateTime.now().millisecondsSinceEpoch;
- if (remaining <= 0) {
- state = state.copyWith(fundingCountdown: '00:00:00');
- _fundingTimer?.cancel();
- // 下次结算时间由 WS mark 推送更新,无需 HTTP 拉取
- return;
- }
- final secs = remaining ~/ 1000;
- final h = secs ~/ 3600;
- final m = (secs % 3600) ~/ 60;
- final s = secs % 60;
- state = state.copyWith(
- fundingCountdown:
- '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
- );
- }
- void _dispose() {
- _tickerSub?.cancel();
- _tickerSub = null;
- _depthSub?.cancel();
- _depthSub = null;
- _markSub?.cancel();
- _markSub = null;
- _fundingTimer?.cancel();
- _fundingTimer = null;
- _pollTimer?.cancel();
- _pollTimer = null;
- _depthTimer?.cancel();
- _depthTimer = null;
- }
- void setOrderSide(OrderSide side) =>
- state = state.copyWith(orderSide: side);
- void setOrderType(OrderType type) =>
- state = state.copyWith(orderType: type);
- void setPositionMode(PositionMode mode) {
- final isConditional = state.orderType == OrderType.conditionalMarket ||
- state.orderType == OrderType.conditionalLimit;
- if (mode == PositionMode.close && isConditional) {
- // 平仓不支持计划委托:暂存类型,临时切换为市价
- _savedConditionalType = state.orderType;
- state = state.copyWith(positionMode: mode, orderType: OrderType.market);
- } else if (mode == PositionMode.open && _savedConditionalType != null) {
- // 切回开仓:恢复之前的计划委托类型
- final restored = _savedConditionalType!;
- _savedConditionalType = null;
- state = state.copyWith(positionMode: mode, orderType: restored);
- } else {
- state = state.copyWith(positionMode: mode);
- }
- }
- /// 切换仓位模式。
- /// 全仓/分仓:必须等后端 modifyType 成功后才更新本地状态
- /// 返回 null 表示成功;返回错误消息字符串表示失败(调用方负责展示)
- Future<String?> setMarginMode(MarginMode mode) async {
- // 全仓/分仓:先调后端,成功后再更新本地状态;期间禁止下单
- final serverType = mode == MarginMode.split ? 1 : 0;
- state = state.copyWith(isSwitchingMode: true);
- try {
- await _service.modifyPositionType(serverType);
- state = state.copyWith(
- marginMode: mode,
- isSwitchingMode: false,
- );
- // 持久化该交易对的保证金模式,下次切回时自动恢复
- final modeStr = mode == MarginMode.split ? 'split'
- : mode == MarginMode.isolated ? 'isolated'
- : 'cross';
- ref.read(sharedPreferencesProvider)
- .setString('futures_margin_mode_${state.symbol}', modeStr);
- // 切换回普通模式后,重新从服务器获取当前杠杆倍数
- if (state.contractCoinId > 0) {
- try {
- final lev = await _service.getLeverage(state.contractCoinId);
- state = state.copyWith(leverage: lev.toDouble());
- } catch (_) {}
- }
- return null;
- } catch (e) {
- // 切换失败:重新拉取真实状态,保持原有 marginMode 不变
- state = state.copyWith(isSwitchingMode: false);
- await _loadWallet(state.symbol);
- final msg = e.toString();
- return msg;
- }
- }
- void setAmountUnit(AmountUnit unit) {
- state = state.copyWith(amountUnit: unit);
- ref.read(futuresAmountUnitPrefProvider.notifier).state = unit;
- }
- void toggleHideOtherSymbols() {
- final next = !state.hideOtherSymbols;
- state = state.copyWith(hideOtherSymbols: next);
- ref.read(futuresHideOtherPrefProvider.notifier).state = next;
- }
- void setTab(FuturesTab tab) {
- state = state.copyWith(activeTab: tab);
- _loadWallet(state.symbol);
- _loadCurrentOrders();
- }
- void setSliderPercent(double pct) =>
- state = state.copyWith(sliderPercent: pct, isSliderInput: true);
- void setSliderPercentFromInput(double pct) =>
- state = state.copyWith(sliderPercent: pct, isSliderInput: false);
- void toggleTpsl() =>
- state = state.copyWith(tpslEnabled: !state.tpslEnabled);
- String _errMsg(Object e) {
- if (e is ApiException) return e.message;
- if (e is DioException) {
- final inner = e.error;
- if (inner is ApiException) return inner.message;
- final data = e.response?.data;
- if (data is Map) {
- final msg = data['message'] as String? ?? data['msg'] as String?;
- if (msg != null && msg.isNotEmpty) return msg;
- }
- final errStr = inner?.toString() ?? '';
- if (errStr.isNotEmpty) {
- final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
- .firstMatch(errStr);
- if (m != null) return m.group(1)!.trim();
- }
- final dioStr = e.toString();
- final dm = RegExp(r'Error:\s*ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
- .firstMatch(dioStr);
- if (dm != null) return dm.group(1)!.trim();
- if (e.type == DioExceptionType.connectionTimeout ||
- e.type == DioExceptionType.receiveTimeout ||
- e.type == DioExceptionType.sendTimeout) {
- return 'errTimeout';
- }
- if (e.type == DioExceptionType.connectionError) {
- return 'errNetworkError';
- }
- }
- final s = e.toString();
- final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
- .firstMatch(s);
- if (m != null) return m.group(1)!.trim();
- final lines = s.split('\n');
- for (final line in lines.reversed) {
- final t = line.trim();
- if (t.isEmpty || t.startsWith('Uri:') || t.startsWith('DioException')) continue;
- final idx = t.lastIndexOf(': ');
- if (idx != -1 && idx < t.length - 2) return t.substring(idx + 2).trim();
- return t;
- }
- return s;
- }
- Future<String?> setLeverage(double lev) async {
- if (state.contractCoinId <= 0) {
- await _loadContractInfo(state.symbol);
- if (state.contractCoinId <= 0) return 'errServiceUnavailable';
- }
- try {
- await _service.modifyLeverage(state.contractCoinId, lev.toInt());
- state = state.copyWith(leverage: lev);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> placeOpenOrder({
- OrderSide? side,
- double? entrustPrice,
- double? triggerPrice,
- double? volume,
- double? tpPrice,
- double? slPrice,
- }) async {
- // 模式切换 API 尚未完成,禁止下单,防止 positionType 不一致导致仓位错误
- if (state.isSwitchingMode) return 'errSwitchingMode';
- if (state.contractCoinId == 0) {
- await _loadContractInfo(state.symbol);
- if (state.contractCoinId == 0) return 'errServiceUnavailable';
- }
- final vol = volume ?? state.inputSize;
- if (vol <= 0) return 'errEnterVolume';
- final int type;
- final double? ep;
- switch (state.orderType) {
- case OrderType.market:
- type = 0;
- ep = null;
- case OrderType.limit:
- type = 1;
- ep = entrustPrice ?? state.inputPrice;
- if (ep <= 0) return 'errEnterPrice';
- case OrderType.conditionalMarket:
- type = 2;
- ep = null; // 计划市价不传委托价,后端以 entrustPrice=0 标识市价
- if ((triggerPrice ?? 0) <= 0) return 'errEnterTriggerPrice';
- case OrderType.conditionalLimit:
- type = 2;
- ep = entrustPrice;
- if ((triggerPrice ?? 0) <= 0) return 'errEnterTriggerPrice';
- }
- final dir = (side ?? state.orderSide) == OrderSide.long ? 0 : 1;
- // 滑块模式:市价/限价传百分比给后端;计划委托不支持百分比,需换算为实际张数
- if (state.isSliderInput && state.sliderPercent > 0.0001 && type != 2) {
- final pct = (state.sliderPercent * 100).round().clamp(1, 100);
- try {
- await _service.openOrder(
- contractCoinId: state.contractCoinId,
- type: type,
- direction: dir,
- volume: pct.toDouble(),
- leverage: state.leverage.toInt(),
- entrustPrice: ep,
- triggerPrice: triggerPrice,
- profitPrice: tpPrice ?? state.tpPrice,
- lossPrice: slPrice ?? state.slPrice,
- isPercentage: 1,
- positionType: state.marginMode == MarginMode.split ? 1 : 0,
- );
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- Future.delayed(const Duration(seconds: 2), () => _loadWallet(state.symbol));
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- if (state.contractSize <= 0) return 'errContractNotReady';
- // 计划市价用触发价作为换算基准,计划限价用委托价,普通单用委托价/最新价
- final double effectivePrice;
- if (state.orderType == OrderType.conditionalMarket) {
- effectivePrice = (triggerPrice != null && triggerPrice > 0)
- ? triggerPrice
- : state.lastPrice;
- } else {
- effectivePrice = ep ?? (state.lastPrice > 0 ? state.lastPrice : 0);
- }
- if (effectivePrice <= 0) return 'errPriceNotReady';
- final double volumeInBtc;
- switch (state.amountUnit) {
- case AmountUnit.lots:
- volumeInBtc = vol * state.contractSize / effectivePrice;
- case AmountUnit.usdt:
- volumeInBtc = vol / effectivePrice;
- case AmountUnit.btc:
- volumeInBtc = vol;
- }
- final factor = math.pow(10, state.coinPrecision).toDouble();
- final double finalVolume = (volumeInBtc * factor).floorToDouble() / factor;
- if (finalVolume <= 0) return 'errVolumeInsufficient';
- try {
- await _service.openOrder(
- contractCoinId: state.contractCoinId,
- type: type,
- direction: dir,
- volume: finalVolume,
- leverage: state.leverage.toInt(),
- entrustPrice: ep,
- triggerPrice: triggerPrice,
- profitPrice: tpPrice ?? state.tpPrice,
- lossPrice: slPrice ?? state.slPrice,
- positionType: state.marginMode == MarginMode.split ? 1 : 0,
- );
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- Future.delayed(const Duration(seconds: 2), () => _loadWallet(state.symbol));
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- /// 市价平仓;volume 不传或 <=0 时全仓平
- Future<String?> closeMarket(FuturesPosition position, {double? volume}) async {
- try {
- final vol = (volume != null && volume > 0)
- ? volume.clamp(0.0, position.availableSize)
- : position.availableSize;
- await _service.closeMarket(
- positionId: position.id,
- volume: vol,
- );
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- /// 限价平仓;volume 不传或 <=0 时全仓平
- Future<String?> closeLimit(FuturesPosition position, double price, {double? volume}) async {
- if (price <= 0) return 'errEnterClosePrice';
- try {
- final vol = (volume != null && volume > 0)
- ? volume.clamp(0.0, position.availableSize)
- : position.availableSize;
- await _service.closeLimit(
- positionId: position.id,
- price: price,
- volume: vol,
- );
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> cancelOrder(FuturesOrder order) async {
- final id = int.tryParse(order.id);
- if (id == null) return 'errInvalidOrderId';
- try {
- await _service.cancelOrder(id);
- await _loadCurrentOrders();
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> closeAllPositions() async {
- try {
- await _service.closeAllPositions();
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- // price 为 null/0 时市价,否则限价
- Future<String?> closeByDirection(OrderSide side, {double? price, double? volume}) async {
- final normSymbol = FuturesState._norm(_toApiSymbol(state.symbol));
- final pos = state.positions.where(
- (p) => p.side == side && FuturesState._norm(p.symbol) == normSymbol,
- ).firstOrNull;
- if (pos == null) return side == OrderSide.long ? 'errNoLongPosition' : 'errNoShortPosition';
- final closeVol = (volume != null && volume > 0)
- ? volume.clamp(0.0, pos.availableSize)
- : pos.availableSize;
- if (price != null && price > 0) {
- return _closeLimitVolume(pos, price, closeVol);
- } else {
- return _closeMarketVolume(pos, closeVol);
- }
- }
- Future<String?> closeConditionalByDirection(
- OrderSide side, {
- required double triggerPrice,
- double? volume,
- double entrustPrice = 0,
- }) async {
- final normSymbol = FuturesState._norm(_toApiSymbol(state.symbol));
- final pos = state.positions.where(
- (p) => p.side == side && FuturesState._norm(p.symbol) == normSymbol,
- ).firstOrNull;
- if (pos == null) return side == OrderSide.long ? 'errNoLongPosition' : 'errNoShortPosition';
- final closeVol = (volume != null && volume > 0)
- ? volume.clamp(0.0, pos.availableSize)
- : pos.availableSize;
- try {
- await _service.closeConditional(
- positionId: pos.id,
- triggerPrice: triggerPrice,
- volume: closeVol,
- entrustPrice: entrustPrice,
- );
- await _loadCurrentOrders();
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> _closeMarketVolume(FuturesPosition position, double volume) async {
- try {
- await _service.closeMarket(positionId: position.id, volume: volume);
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> _closeLimitVolume(FuturesPosition position, double price, double volume) async {
- if (price <= 0) return 'errEnterClosePrice';
- try {
- await _service.closeLimit(positionId: position.id, price: price, volume: volume);
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> cancelAllOrders() async {
- final ids = state.openOrders
- .map((o) => int.tryParse(o.id))
- .whereType<int>()
- .toList();
- if (ids.isEmpty) return 'errNoOrdersToCancel';
- try {
- await _service.cancelOrdersByIds(ids);
- await _loadCurrentOrders();
- return null;
- } catch (e) {
- await _loadCurrentOrders();
- return _errMsg(e);
- }
- }
- Future<String?> setPositionTpsl(
- FuturesPosition position, {
- double? profitPrice,
- double? lossPrice,
- }) async {
- try {
- if (position.cutEntrustId != null) {
- // 已有止盈止损委托单 → 修改
- await _service.modifyCutOrder(
- entrustId: position.cutEntrustId!,
- profitPrice: profitPrice,
- lossPrice: lossPrice,
- );
- } else {
- // 首次设置,成功后刷新持仓以获取 cutEntrustId
- await _service.setTpsl(
- positionId: position.id,
- profitPrice: profitPrice,
- lossPrice: lossPrice,
- );
- await _loadWallet(state.symbol);
- }
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<String?> reversePosition(FuturesPosition position) async {
- try {
- await _service.reverseOpenPosition(position.id);
- await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
- return null;
- } catch (e) {
- return _errMsg(e);
- }
- }
- Future<void> refresh() async {
- // 下拉刷新只更新数据,不触发 isLoading(骨架屏仅用于首屏)
- await Future.wait([
- _loadWallet(state.symbol),
- _loadCurrentOrders(),
- ]);
- }
- FuturesService get _service =>
- FuturesService(ref.read(dioClientProvider));
- }
- final futuresProvider =
- AutoDisposeNotifierProviderFamily<FuturesNotifier, FuturesState, String>(
- FuturesNotifier.new,
- );
- final futuresAmountUnitPrefProvider =
- StateProvider<AmountUnit>((ref) => AmountUnit.btc);
- final futuresHideOtherPrefProvider = StateProvider<bool>((ref) => false);
- final futuresActiveSymbolProvider = StateProvider<String>((ref) => '');
- /// 当前底部导航栏选中的 tab 索引(0=首页, 1=行情, 2=交易, 3=合约, 4=跟单, 5=资产)
- final activeBottomTabProvider = StateProvider<int>((ref) => 0);
- /// 上次访问的交易路径(现货 /spot/:sym 或合约 /futures/:sym),用于恢复交易 tab
- final lastTradingRouteProvider = StateProvider<String>((ref) => '/spot/BTCUSDT');
- double _toDouble(dynamic v) {
- if (v == null) return 0.0;
- if (v is num) return v.toDouble();
- return double.tryParse(v.toString()) ?? 0.0;
- }
- /// 预计强平价:直接使用后端返回值,<=0 或 -1 均视为无效(显示 '--')
- double _calcLiquidationPrice({required double apiValue}) {
- return apiValue > 0 ? apiValue : 0.0;
- }
|