futures_provider.dart 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932
  1. import 'dart:async';
  2. import 'dart:math' as math;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:dio/dio.dart' show DioException, DioExceptionType;
  6. import '../core/network/api_response.dart';
  7. import '../core/network/dio_client.dart';
  8. import '../data/services/futures_service.dart';
  9. import 'app_provider.dart';
  10. import 'auth_provider.dart';
  11. import 'copy_trading_provider.dart';
  12. import 'market_detail_provider.dart';
  13. import 'ws_provider.dart';
  14. enum OrderSide { long, short }
  15. enum OrderType { market, limit, conditionalMarket, conditionalLimit }
  16. enum PositionMode { open, close }
  17. enum MarginMode { cross, isolated, split }
  18. enum AmountUnit { lots, usdt, btc }
  19. enum FuturesTab { positions, orders, assets }
  20. bool _isFuturesTabActive(int tabIndex) => tabIndex == 2 || tabIndex == 3;
  21. class FuturesPosition {
  22. final String id;
  23. final String symbol;
  24. final OrderSide side;
  25. final double size; // 持仓量(基础币)
  26. final double availableSize; // 可平仓位
  27. final double entryPrice;
  28. final double markPrice;
  29. final double leverage;
  30. final double unrealizedPnl;
  31. final double liquidationPrice;
  32. final double margin;
  33. final String marginMode;
  34. final double? profitPrice;
  35. final double? lossPrice;
  36. final int? cutEntrustId; // 止盈止损委托单 ID,有值时修改走 modify-cut
  37. final double contractSize; // 每张合约对应基础币数量
  38. final double? apiMarginRate;
  39. final double? apiCurrentMargin;
  40. final double realizedPnl;
  41. final double commissionFee; // 累计手续费(JSON: commissionFee)
  42. const FuturesPosition({
  43. required this.id,
  44. required this.symbol,
  45. required this.side,
  46. required this.size,
  47. required this.availableSize,
  48. required this.entryPrice,
  49. required this.markPrice,
  50. required this.leverage,
  51. required this.unrealizedPnl,
  52. required this.liquidationPrice,
  53. required this.margin,
  54. this.marginMode = '全仓',
  55. this.profitPrice,
  56. this.lossPrice,
  57. this.cutEntrustId,
  58. this.contractSize = 1.0,
  59. this.apiMarginRate,
  60. this.apiCurrentMargin,
  61. this.realizedPnl = 0.0,
  62. this.commissionFee = 0.0,
  63. });
  64. double get lots => contractSize > 0 ? size / contractSize : size;
  65. double get currentMargin => (size > 0 && markPrice > 0 && leverage > 0)
  66. ? (size * markPrice) / leverage
  67. : margin;
  68. double get roe {
  69. final denom = (apiCurrentMargin != null && apiCurrentMargin! > 0)
  70. ? apiCurrentMargin!
  71. : currentMargin;
  72. return denom > 0 ? (unrealizedPnl / denom) * 100 : 0;
  73. }
  74. // 保证金比率 = 保证金 / 合约账户权益 × 100,不超过100%
  75. double get marginRatio {
  76. if (apiMarginRate != null && apiMarginRate! > 0) return apiMarginRate!;
  77. if (margin > 0 && (margin + unrealizedPnl) > 0) {
  78. return (margin / (margin + unrealizedPnl) * 100).clamp(0.0, 100.0);
  79. }
  80. return 0;
  81. }
  82. FuturesPosition withMarkPrice(double price) {
  83. return FuturesPosition(
  84. id: id,
  85. symbol: symbol,
  86. side: side,
  87. size: size,
  88. availableSize: availableSize,
  89. entryPrice: entryPrice,
  90. markPrice: price,
  91. leverage: leverage,
  92. unrealizedPnl: unrealizedPnl,
  93. liquidationPrice: liquidationPrice,
  94. margin: margin,
  95. marginMode: marginMode,
  96. profitPrice: profitPrice,
  97. lossPrice: lossPrice,
  98. cutEntrustId: cutEntrustId,
  99. contractSize: contractSize,
  100. apiMarginRate: apiMarginRate,
  101. apiCurrentMargin: apiCurrentMargin,
  102. realizedPnl: realizedPnl,
  103. commissionFee: commissionFee,
  104. );
  105. }
  106. FuturesPosition withContractSize(double cs) => FuturesPosition(
  107. id: id,
  108. symbol: symbol,
  109. side: side,
  110. size: size,
  111. availableSize: availableSize,
  112. entryPrice: entryPrice,
  113. markPrice: markPrice,
  114. leverage: leverage,
  115. unrealizedPnl: unrealizedPnl,
  116. liquidationPrice: liquidationPrice,
  117. margin: margin,
  118. marginMode: marginMode,
  119. profitPrice: profitPrice,
  120. lossPrice: lossPrice,
  121. cutEntrustId: cutEntrustId,
  122. contractSize: cs,
  123. apiMarginRate: apiMarginRate,
  124. apiCurrentMargin: apiCurrentMargin,
  125. realizedPnl: realizedPnl,
  126. commissionFee: commissionFee,
  127. );
  128. /// 从 /swap/wallet-new/get-with-positions 的 currentPositionWithCutList 解析
  129. factory FuturesPosition.fromJson(Map<String, dynamic> json) {
  130. final coin = json['coin'] as Map<String, dynamic>? ?? {};
  131. final symbol = coin['symbol'] as String? ?? '';
  132. final direction = json['direction'] as String? ?? 'BUY';
  133. final side = direction == 'BUY' ? OrderSide.long : OrderSide.short;
  134. final size = _toDouble(json['currentPosition']);
  135. final frozen = _toDouble(json['frozenPosition']);
  136. final explicitAvail = _toDouble(json['availablePosition']);
  137. final available = explicitAvail > 0
  138. ? explicitAvail
  139. : (size - frozen).clamp(0.0, double.infinity);
  140. final entryPrice = _toDouble(json['openPrice']);
  141. final leverage = _toDouble(json['leverage']);
  142. final currentPrice = _toDouble(json['currentPrice'] ?? entryPrice);
  143. final pnl = json['currentProfit'] != null
  144. ? _toDouble(json['currentProfit'])
  145. : (side == OrderSide.long
  146. ? (currentPrice - entryPrice) * size
  147. : (entryPrice - currentPrice) * size);
  148. // type 1 → 分仓;否则 → 全仓
  149. final posType = json['type'];
  150. final String marginMode = (posType == 1 || posType == '1') ? '分仓' : '全仓';
  151. // cutList 为止盈止损委托单列表,取第一条作为回显数据源
  152. final cutList = json['cutList'];
  153. Map<String, dynamic>? cutOrder;
  154. if (cutList is List && cutList.isNotEmpty) {
  155. cutOrder = Map<String, dynamic>.from(cutList.first as Map);
  156. }
  157. final rawProfit = cutOrder != null
  158. ? _toDouble(cutOrder['profitPrice'])
  159. : _toDouble(json['profitPrice']);
  160. final rawLoss = cutOrder != null
  161. ? _toDouble(cutOrder['lossPrice'])
  162. : _toDouble(json['lossPrice']);
  163. final cutEntrustId = cutOrder != null
  164. ? (cutOrder['id'] is int
  165. ? cutOrder['id'] as int
  166. : int.tryParse('${cutOrder['id'] ?? ''}'))
  167. : null;
  168. final rawMarginRate = json['marginRate'];
  169. final apiMarginRate = rawMarginRate != null ? _toDouble(rawMarginRate) : null;
  170. final rawCurrentMargin = _toDouble(json['currentMargin']);
  171. final realizedPnl = _toDouble(
  172. json['usdtProfit'] ?? json['realizedPnl'] ?? json['realizedProfit'] ?? 0);
  173. return FuturesPosition(
  174. id: json['id']?.toString() ?? '',
  175. symbol: symbol,
  176. side: side,
  177. size: size,
  178. availableSize: available,
  179. entryPrice: entryPrice,
  180. markPrice: currentPrice,
  181. leverage: leverage,
  182. unrealizedPnl: pnl,
  183. liquidationPrice: _calcLiquidationPrice(
  184. apiValue: _toDouble(json['estimatedBlastPrice']),
  185. ),
  186. margin: _toDouble(json['principalAmount']),
  187. marginMode: marginMode,
  188. profitPrice: rawProfit > 0 ? rawProfit : null,
  189. lossPrice: rawLoss > 0 ? rawLoss : null,
  190. cutEntrustId: cutEntrustId,
  191. apiMarginRate: apiMarginRate != null && apiMarginRate > 0 ? apiMarginRate : null,
  192. apiCurrentMargin: rawCurrentMargin > 0 ? rawCurrentMargin : null,
  193. realizedPnl: realizedPnl,
  194. commissionFee: _toDouble(json['commissionFee']),
  195. );
  196. }
  197. /// 从 /swap/order/history 历史仓位记录解析
  198. factory FuturesPosition.fromHistoryJson(Map<String, dynamic> json) {
  199. final rawSym = (json['symbol'] ?? json['coinSymbol'] ?? '').toString();
  200. final direction = (json['direction'] ?? 'BUY').toString().toUpperCase();
  201. final side = direction == 'BUY' || direction == '0'
  202. ? OrderSide.long
  203. : OrderSide.short;
  204. final size = _toDouble(json['tradedVolume'] ?? json['volume'] ?? 0);
  205. final entryPrice = _toDouble(json['usdtOpenPrice'] ?? json['openPrice'] ?? 0);
  206. final closePrice = _toDouble(json['tradedPrice'] ?? json['closePrice'] ?? entryPrice);
  207. final leverage = _toDouble(json['leverage'] ?? 20);
  208. final realizedPnl = _toDouble(
  209. json['profitAndLoss'] ?? json['profit'] ??
  210. json['realizedPnl'] ?? json['realizedProfit'] ??
  211. json['closeProfitLoss'] ?? 0);
  212. final marginMode = _mapHistoryPositionType(json['positionType'], json['patterns']);
  213. return FuturesPosition(
  214. id: json['id']?.toString() ?? '',
  215. symbol: rawSym,
  216. side: side,
  217. size: size,
  218. availableSize: 0,
  219. entryPrice: entryPrice,
  220. markPrice: closePrice,
  221. leverage: leverage,
  222. unrealizedPnl: 0,
  223. liquidationPrice: 0,
  224. margin: _toDouble(json['principalAmount'] ?? 0),
  225. marginMode: marginMode,
  226. realizedPnl: realizedPnl,
  227. );
  228. }
  229. static String _mapHistoryPositionType(dynamic posType, dynamic patterns) {
  230. final pt = posType?.toString() ?? '';
  231. if (pt == '0') return '全仓';
  232. if (pt == '1') return '逐仓';
  233. final p = (patterns?.toString() ?? '').toUpperCase();
  234. if (p == 'ISOLATED') return '逐仓';
  235. return '全仓';
  236. }
  237. @override
  238. bool operator ==(Object other) =>
  239. identical(this, other) ||
  240. other is FuturesPosition &&
  241. id == other.id &&
  242. symbol == other.symbol &&
  243. side == other.side &&
  244. size == other.size &&
  245. entryPrice == other.entryPrice &&
  246. markPrice == other.markPrice &&
  247. leverage == other.leverage &&
  248. unrealizedPnl == other.unrealizedPnl &&
  249. liquidationPrice == other.liquidationPrice &&
  250. margin == other.margin &&
  251. marginMode == other.marginMode;
  252. @override
  253. int get hashCode => Object.hash(id, symbol, side, size, entryPrice,
  254. markPrice, leverage, unrealizedPnl, liquidationPrice, margin, marginMode);
  255. }
  256. class FuturesOrder {
  257. final String id;
  258. final String symbol;
  259. final OrderSide side;
  260. final OrderType type;
  261. final double price; // 委托价(entrustPrice)
  262. final double tradedPrice; // 成交均价(tradedPrice)
  263. final double triggerPrice; // 计划委托触发价
  264. final double size;
  265. final double filledSize;
  266. final String status;
  267. final String action; // 开多/开空/平多/平空
  268. final String marginMode;
  269. final double leverage;
  270. final DateTime? createTime;
  271. final DateTime? dealTime; // 成交时间
  272. final double? profitPrice; // 止盈价(>0 时有效)
  273. final double? lossPrice; // 止损价(>0 时有效)
  274. final double fee; // 手续费(openFee + closeFee)
  275. const FuturesOrder({
  276. required this.id,
  277. required this.symbol,
  278. required this.side,
  279. required this.type,
  280. required this.price,
  281. required this.size,
  282. required this.filledSize,
  283. required this.status,
  284. this.tradedPrice = 0,
  285. this.triggerPrice = 0,
  286. this.action = '开多',
  287. this.marginMode = '全仓',
  288. this.leverage = 20,
  289. this.createTime,
  290. this.dealTime,
  291. this.profitPrice,
  292. this.lossPrice,
  293. this.fee = 0,
  294. });
  295. /// 是否为开仓方向(开多/开空)
  296. bool get isOpenOrder => action.contains('开');
  297. /// 是否处于委托中(可撤销)状态
  298. bool get isPending => status == '委托中';
  299. /// 展示用类型标签
  300. String get typeLabel {
  301. switch (type) {
  302. case OrderType.market: return '市价';
  303. case OrderType.limit: return '限价';
  304. case OrderType.conditionalMarket:
  305. case OrderType.conditionalLimit: return '计划委托';
  306. }
  307. }
  308. String get priceDisplay {
  309. String fmt(double v) =>
  310. v == v.truncateToDouble() ? '${v.toInt()}' : v.toString();
  311. switch (type) {
  312. case OrderType.market:
  313. case OrderType.conditionalMarket:
  314. return '市价';
  315. case OrderType.limit:
  316. return price > 0 ? fmt(price) : '--';
  317. case OrderType.conditionalLimit:
  318. // 计划市价:entrustPrice=0;计划限价:entrustPrice=实际限价
  319. return price > 0 ? fmt(price) : '市价';
  320. }
  321. }
  322. /// 从 /swap/order/current 解析
  323. factory FuturesOrder.fromJson(Map<String, dynamic> json) {
  324. // direction 可能是字符串 "BUY"/"SELL" 或整数 0/1
  325. final dirRaw = json['direction'];
  326. final bool isLong;
  327. if (dirRaw is int) {
  328. isLong = dirRaw == 0; // 0=买多,1=卖空
  329. } else {
  330. isLong = (dirRaw as String? ?? 'BUY') == 'BUY';
  331. }
  332. final side = isLong ? OrderSide.long : OrderSide.short;
  333. // type 可能是字符串枚举或整数(0=市价,1=限价,2=计划委托)
  334. // 计划委托中 entrustPrice=0 为计划市价,entrustPrice>0 为计划限价
  335. // 注意:不能用 json['price'] 作为 fallback,否则市价单会被误判为限价单
  336. final typeRaw = json['type'];
  337. final entrustPriceRaw = _toDouble(json['entrustPrice']);
  338. final OrderType orderType;
  339. if (typeRaw is int) {
  340. orderType = switch (typeRaw) {
  341. 0 => OrderType.market,
  342. 2 => entrustPriceRaw > 0
  343. ? OrderType.conditionalLimit
  344. : OrderType.conditionalMarket,
  345. _ => OrderType.limit,
  346. };
  347. } else {
  348. final s = (typeRaw as String? ?? '').toUpperCase();
  349. if (s == 'SPOT_LIMIT' || s == 'PLAN') {
  350. orderType = entrustPriceRaw > 0
  351. ? OrderType.conditionalLimit
  352. : OrderType.conditionalMarket;
  353. } else {
  354. orderType = s == 'MARKET_PRICE' || s == 'MARKET'
  355. ? OrderType.market
  356. : OrderType.limit;
  357. }
  358. }
  359. // entrustType:0=OPEN(开仓),1=CLOSE(平仓)
  360. // 注意:positionType 是全仓/逐仓模式,不是开平仓标志,不要用它判断
  361. final entrustTypeRaw = json['entrustType'];
  362. final bool isClose;
  363. if (entrustTypeRaw is int) {
  364. isClose = entrustTypeRaw == 1;
  365. } else if (entrustTypeRaw is String) {
  366. final et = entrustTypeRaw.toUpperCase();
  367. isClose = et == '1' || et == 'CLOSE';
  368. } else {
  369. // entrustType 不存在时,看 closeType 是否有值(有值说明是平仓单)
  370. final closeTypeRaw = json['closeType'];
  371. isClose = closeTypeRaw != null;
  372. }
  373. final action = isClose
  374. ? (isLong ? '平多' : '平空')
  375. : (isLong ? '开多' : '开空');
  376. // createTime 为 13 位毫秒时间戳
  377. final ts = json['createTime'];
  378. DateTime? createTime;
  379. if (ts is int) {
  380. createTime = DateTime.fromMillisecondsSinceEpoch(ts);
  381. } else if (ts is String) {
  382. createTime = DateTime.tryParse(ts);
  383. }
  384. // dealTime:成交时间(Long 毫秒时间戳)
  385. final dealTs = json['dealTime'];
  386. DateTime? dealTime;
  387. if (dealTs is int && dealTs > 0) {
  388. dealTime = DateTime.fromMillisecondsSinceEpoch(dealTs);
  389. } else if (dealTs is String) {
  390. dealTime = DateTime.tryParse(dealTs);
  391. }
  392. // status 可能是字符串或整数
  393. final statusRaw = json['status'];
  394. final status = _mapStatus(statusRaw is String ? statusRaw : statusRaw?.toString() ?? '');
  395. // symbol 可能是直接字段(EntrustBean),也可能嵌套在 coin 中(position list)
  396. final coin = json['coin'] as Map<String, dynamic>? ?? {};
  397. final symbol = (json['symbol'] as String?)
  398. ?? (coin['symbol'] as String?)
  399. ?? '';
  400. // 止盈止损(>0 时有效)
  401. final rawProfit = _toDouble(json['profitPrice'] ?? json['stopProfitPrice']);
  402. final rawLoss = _toDouble(json['lossPrice'] ?? json['stopLossPrice']);
  403. return FuturesOrder(
  404. id: json['id']?.toString() ?? '',
  405. symbol: symbol,
  406. side: side,
  407. type: orderType,
  408. price: _toDouble(json['entrustPrice']),
  409. tradedPrice: _toDouble(json['tradedPrice']),
  410. triggerPrice: _toDouble(json['triggerPrice']),
  411. size: _toDouble(json['volume'] ?? json['size']),
  412. filledSize: _toDouble(json['tradedVolume'] ?? json['filledSize']),
  413. status: status,
  414. action: action,
  415. marginMode: _parsePatterns(json['patterns']),
  416. leverage: _toDouble(json['leverage'] ?? 20).toInt().toDouble(),
  417. createTime: createTime,
  418. dealTime: dealTime,
  419. profitPrice: rawProfit > 0 ? rawProfit : null,
  420. lossPrice: rawLoss > 0 ? rawLoss : null,
  421. fee: _toDouble(json['openFee']) + _toDouble(json['closeFee']),
  422. );
  423. }
  424. /// 从 /swap/order/history-open 历史委托记录解析
  425. factory FuturesOrder.fromHistoryJson(Map<String, dynamic> json) {
  426. final rawSym = (json['symbol'] ?? json['coinSymbol'] ?? '').toString();
  427. final dirRaw = json['direction'];
  428. final bool isLong;
  429. if (dirRaw is int) {
  430. isLong = dirRaw == 0;
  431. } else {
  432. final s = (dirRaw?.toString() ?? 'BUY').toUpperCase();
  433. isLong = s == 'BUY' || s == '0';
  434. }
  435. final side = isLong ? OrderSide.long : OrderSide.short;
  436. // 使用 type 字段判断委托类型;openType 是仓位开仓方式,平仓单中继承自仓位,不能用于判断平仓单类型
  437. final typeRaw = json['type'];
  438. final entrustPriceRaw = _toDouble(json['entrustPrice']);
  439. final OrderType orderType;
  440. if (typeRaw is int) {
  441. orderType = switch (typeRaw) {
  442. 0 => OrderType.market,
  443. 2 => entrustPriceRaw > 0
  444. ? OrderType.conditionalLimit
  445. : OrderType.conditionalMarket,
  446. _ => OrderType.limit,
  447. };
  448. } else {
  449. final s = (typeRaw?.toString() ?? '').toUpperCase();
  450. if (s == 'SPOT_LIMIT' || s == 'PLAN') {
  451. orderType = entrustPriceRaw > 0
  452. ? OrderType.conditionalLimit
  453. : OrderType.conditionalMarket;
  454. } else {
  455. orderType = (s == 'MARKET_PRICE' || s == 'MARKET')
  456. ? OrderType.market
  457. : OrderType.limit;
  458. }
  459. }
  460. // 历史记录同样用 entrustType 判断开平仓,fallback 看 closeType 是否有值
  461. final entrustTypeRaw = json['entrustType'];
  462. final bool isClose;
  463. if (entrustTypeRaw is int) {
  464. isClose = entrustTypeRaw == 1;
  465. } else if (entrustTypeRaw is String) {
  466. final et = entrustTypeRaw.toUpperCase();
  467. isClose = et == '1' || et == 'CLOSE';
  468. } else {
  469. isClose = json['closeType'] != null;
  470. }
  471. final action = isClose
  472. ? (isLong ? '平多' : '平空')
  473. : (isLong ? '开多' : '开空');
  474. final ts = json['createTime'] ?? json['usdtOpenTime'];
  475. DateTime? createTime;
  476. if (ts is int) {
  477. createTime = DateTime.fromMillisecondsSinceEpoch(ts);
  478. } else if (ts is String) {
  479. createTime = DateTime.tryParse(ts);
  480. }
  481. final dealTs = json['dealTime'];
  482. DateTime? dealTime;
  483. if (dealTs is int && dealTs > 0) {
  484. dealTime = DateTime.fromMillisecondsSinceEpoch(dealTs);
  485. } else if (dealTs is String) {
  486. dealTime = DateTime.tryParse(dealTs);
  487. }
  488. final statusRaw = json['status'];
  489. final status = _mapStatus(statusRaw is String ? statusRaw : statusRaw?.toString() ?? '');
  490. return FuturesOrder(
  491. id: json['id']?.toString() ?? '',
  492. symbol: rawSym,
  493. side: side,
  494. type: orderType,
  495. price: _toDouble(json['entrustPrice'] ?? 0),
  496. tradedPrice: _toDouble(json['tradedPrice'] ?? 0),
  497. triggerPrice: _toDouble(json['triggerPrice'] ?? 0),
  498. size: _toDouble(json['volume'] ?? json['size'] ?? 0),
  499. filledSize: _toDouble(json['tradedVolume'] ?? json['dealVolume'] ?? 0),
  500. status: status,
  501. action: action,
  502. marginMode: _parsePatterns(json['patterns']),
  503. leverage: _toDouble(json['leverage'] ?? 20).toInt().toDouble(),
  504. createTime: createTime,
  505. dealTime: dealTime,
  506. fee: _toDouble(json['openFee']) + _toDouble(json['closeFee']),
  507. );
  508. }
  509. static String _mapStatus(String raw) {
  510. return switch (raw) {
  511. 'ENTRUST_ING' => '委托中',
  512. 'ENTRUST_SUCCESS' => '已成交',
  513. 'ENTRUST_CANCEL' => '已撤销',
  514. _ => raw,
  515. };
  516. }
  517. /// 解析仓位模式:CROSSED=全仓,FIXED=分仓;ordinal 0=全仓,1=分仓
  518. static String _parsePatterns(dynamic raw) {
  519. if (raw == null) return '全仓';
  520. final s = raw.toString().toUpperCase();
  521. if (s == 'FIXED' || s == '1') return '分仓';
  522. return '全仓';
  523. }
  524. @override
  525. bool operator ==(Object other) =>
  526. identical(this, other) ||
  527. other is FuturesOrder &&
  528. id == other.id &&
  529. symbol == other.symbol &&
  530. side == other.side &&
  531. type == other.type &&
  532. price == other.price &&
  533. triggerPrice == other.triggerPrice &&
  534. size == other.size &&
  535. filledSize == other.filledSize &&
  536. status == other.status &&
  537. action == other.action &&
  538. marginMode == other.marginMode &&
  539. leverage == other.leverage &&
  540. profitPrice == other.profitPrice &&
  541. lossPrice == other.lossPrice;
  542. @override
  543. int get hashCode => Object.hash(id, symbol, side, type, price, triggerPrice,
  544. size, filledSize, status, action, marginMode, leverage,
  545. profitPrice, lossPrice);
  546. }
  547. class FuturesAccountInfo {
  548. final double totalBalance;
  549. final double availableMargin;
  550. final double usedMargin;
  551. final double unrealizedPnl;
  552. const FuturesAccountInfo({
  553. required this.totalBalance,
  554. required this.availableMargin,
  555. required this.usedMargin,
  556. required this.unrealizedPnl,
  557. });
  558. factory FuturesAccountInfo.fromJson(Map<String, dynamic> json,
  559. {double unrealizedPnl = 0, double positionsMargin = 0}) {
  560. // currentCapital = 账户当前权益(合约账户总余额)
  561. // 已用保证金 = 持仓 principalAmount 之和(最准确),其次 frozenMargin
  562. // 可用保证金 = 总余额 - 已用保证金
  563. final total = _toDouble(json['currentCapital'] ?? json['balance']);
  564. final frozen = _toDouble(json['frozenMargin'] ?? json['frozenBalance']);
  565. final usedMargin = positionsMargin > 0
  566. ? positionsMargin
  567. : frozen > 0
  568. ? frozen
  569. : 0.0;
  570. final available = (total - usedMargin).clamp(0.0, double.infinity);
  571. return FuturesAccountInfo(
  572. totalBalance: total,
  573. usedMargin: usedMargin,
  574. availableMargin: available,
  575. unrealizedPnl: unrealizedPnl,
  576. );
  577. }
  578. const FuturesAccountInfo.empty()
  579. : totalBalance = 0,
  580. availableMargin = 0,
  581. usedMargin = 0,
  582. unrealizedPnl = 0;
  583. @override
  584. bool operator ==(Object other) =>
  585. identical(this, other) ||
  586. other is FuturesAccountInfo &&
  587. totalBalance == other.totalBalance &&
  588. availableMargin == other.availableMargin &&
  589. usedMargin == other.usedMargin &&
  590. unrealizedPnl == other.unrealizedPnl;
  591. @override
  592. int get hashCode =>
  593. Object.hash(totalBalance, availableMargin, usedMargin, unrealizedPnl);
  594. }
  595. class FuturesState {
  596. final String symbol;
  597. final int contractCoinId;
  598. final double contractSize; // 每张对应基础币数量
  599. final int volScale; // 张数精度
  600. final String coinSymbol; // 基础资产名称
  601. final double lastPrice;
  602. final String? lastPriceStr; // WS 返回的原始价格字符串
  603. final double markPrice;
  604. final double change24h;
  605. final double leverage;
  606. final MarginMode marginMode;
  607. final PositionMode positionMode;
  608. final OrderSide orderSide;
  609. final OrderType orderType;
  610. final AmountUnit amountUnit;
  611. final double inputPrice;
  612. final double inputSize;
  613. final double sliderPercent;
  614. final bool isSliderInput; // true=滑块输入,false=手动输入
  615. final bool tpslEnabled;
  616. final double? tpPrice;
  617. final double? slPrice;
  618. final double fundingRate;
  619. final String fundingCountdown;
  620. final FuturesTab activeTab;
  621. final List<FuturesPosition> positions;
  622. final List<FuturesOrder> openOrders;
  623. final bool ordersHasMore;
  624. final int ordersPage;
  625. final bool ordersLoadingMore;
  626. final FuturesAccountInfo accountInfo;
  627. final bool isLoading; // 首屏骨架
  628. final bool isTabLoading; // 底部列表骨架
  629. final bool isSwitchingMode; // 模式切换中(切换完成前禁止下单)
  630. final String? errorMsg;
  631. final List<Map<String, dynamic>> orderBookAsks;
  632. final List<Map<String, dynamic>> orderBookBids;
  633. final bool hideOtherSymbols;
  634. final int pricePrecision;
  635. final int coinPrecision;
  636. final int usdtPrecision;
  637. final int leverageMin;
  638. final int leverageMax;
  639. final List<int> leverageOptions;
  640. final bool isDiscreteLeverage; // true=固定档位,false=区间任意值
  641. final double openFeeRate;
  642. const FuturesState({
  643. required this.symbol,
  644. this.contractCoinId = 0,
  645. this.contractSize = 1.0,
  646. this.volScale = 0,
  647. this.coinSymbol = '',
  648. required this.lastPrice,
  649. this.lastPriceStr,
  650. required this.markPrice,
  651. this.change24h = 0,
  652. this.leverage = 20,
  653. this.marginMode = MarginMode.cross,
  654. this.positionMode = PositionMode.open,
  655. this.orderSide = OrderSide.long,
  656. this.orderType = OrderType.market,
  657. this.amountUnit = AmountUnit.btc,
  658. this.inputPrice = 0,
  659. this.inputSize = 0,
  660. this.sliderPercent = 0,
  661. this.isSliderInput = false,
  662. this.tpslEnabled = false,
  663. this.tpPrice,
  664. this.slPrice,
  665. this.fundingRate = 0,
  666. this.fundingCountdown = '--:--:--',
  667. this.activeTab = FuturesTab.positions,
  668. required this.positions,
  669. required this.openOrders,
  670. this.ordersHasMore = false,
  671. this.ordersPage = 1,
  672. this.ordersLoadingMore = false,
  673. required this.accountInfo,
  674. this.isLoading = false,
  675. this.isTabLoading = false,
  676. this.isSwitchingMode = false,
  677. this.errorMsg,
  678. this.orderBookAsks = const [],
  679. this.orderBookBids = const [],
  680. this.hideOtherSymbols = false,
  681. this.pricePrecision = 2,
  682. this.coinPrecision = 4,
  683. this.usdtPrecision = 2,
  684. this.leverageMin = 1,
  685. this.leverageMax = 125,
  686. this.leverageOptions = const [],
  687. this.isDiscreteLeverage = false,
  688. this.openFeeRate = 0.0005,
  689. });
  690. /// 当前数量单位对应的精度
  691. int get currentAmountPrecision {
  692. switch (amountUnit) {
  693. case AmountUnit.lots:
  694. return 0;
  695. case AmountUnit.btc:
  696. return coinPrecision;
  697. case AmountUnit.usdt:
  698. return usdtPrecision;
  699. }
  700. }
  701. static String _norm(String s) => s.replaceAll('/', '').toUpperCase();
  702. List<FuturesPosition> get displayPositions {
  703. if (!hideOtherSymbols) return positions;
  704. final cur = _norm(symbol);
  705. return positions.where((p) => _norm(p.symbol) == cur).toList();
  706. }
  707. List<FuturesOrder> get displayOrders {
  708. if (!hideOtherSymbols) return openOrders;
  709. final cur = _norm(symbol);
  710. return openOrders.where((o) => _norm(o.symbol) == cur).toList();
  711. }
  712. String get orderTypeLabel {
  713. switch (orderType) {
  714. case OrderType.market:
  715. return '市价单';
  716. case OrderType.limit:
  717. return '限价单';
  718. case OrderType.conditionalMarket:
  719. return '市价条件委托单';
  720. case OrderType.conditionalLimit:
  721. return '限价条件委托单';
  722. }
  723. }
  724. String get marginModeLabel => switch (marginMode) {
  725. MarginMode.cross => '全仓',
  726. MarginMode.isolated => '逐仓',
  727. MarginMode.split => '分仓',
  728. };
  729. String get amountUnitLabel {
  730. switch (amountUnit) {
  731. case AmountUnit.lots:
  732. return '张';
  733. case AmountUnit.usdt:
  734. return 'USDT';
  735. case AmountUnit.btc:
  736. if (coinSymbol.isNotEmpty) return coinSymbol;
  737. final base = symbol.toUpperCase().replaceFirst(RegExp(r'USDT$'), '');
  738. return base.isNotEmpty ? base : 'BTC';
  739. }
  740. }
  741. bool get isConditionalOrder =>
  742. orderType == OrderType.conditionalMarket ||
  743. orderType == OrderType.conditionalLimit;
  744. bool get showPriceInput =>
  745. orderType == OrderType.limit || orderType == OrderType.conditionalLimit;
  746. FuturesState copyWith({
  747. String? symbol,
  748. int? contractCoinId,
  749. double? contractSize,
  750. int? volScale,
  751. String? coinSymbol,
  752. double? lastPrice,
  753. String? lastPriceStr,
  754. double? markPrice,
  755. double? change24h,
  756. double? leverage,
  757. MarginMode? marginMode,
  758. PositionMode? positionMode,
  759. OrderSide? orderSide,
  760. OrderType? orderType,
  761. AmountUnit? amountUnit,
  762. double? inputPrice,
  763. double? inputSize,
  764. double? sliderPercent,
  765. bool? isSliderInput,
  766. bool? tpslEnabled,
  767. double? tpPrice,
  768. double? slPrice,
  769. double? fundingRate,
  770. String? fundingCountdown,
  771. FuturesTab? activeTab,
  772. List<FuturesPosition>? positions,
  773. List<FuturesOrder>? openOrders,
  774. bool? ordersHasMore,
  775. int? ordersPage,
  776. bool? ordersLoadingMore,
  777. FuturesAccountInfo? accountInfo,
  778. bool? isLoading,
  779. bool? isTabLoading,
  780. bool? isSwitchingMode,
  781. String? errorMsg,
  782. List<Map<String, dynamic>>? orderBookAsks,
  783. List<Map<String, dynamic>>? orderBookBids,
  784. bool? hideOtherSymbols,
  785. int? pricePrecision,
  786. int? coinPrecision,
  787. int? usdtPrecision,
  788. int? leverageMin,
  789. int? leverageMax,
  790. List<int>? leverageOptions,
  791. bool? isDiscreteLeverage,
  792. double? openFeeRate,
  793. }) =>
  794. FuturesState(
  795. symbol: symbol ?? this.symbol,
  796. contractCoinId: contractCoinId ?? this.contractCoinId,
  797. contractSize: contractSize ?? this.contractSize,
  798. volScale: volScale ?? this.volScale,
  799. coinSymbol: coinSymbol ?? this.coinSymbol,
  800. lastPrice: lastPrice ?? this.lastPrice,
  801. lastPriceStr: lastPriceStr ?? this.lastPriceStr,
  802. markPrice: markPrice ?? this.markPrice,
  803. change24h: change24h ?? this.change24h,
  804. leverage: leverage ?? this.leverage,
  805. marginMode: marginMode ?? this.marginMode,
  806. positionMode: positionMode ?? this.positionMode,
  807. orderSide: orderSide ?? this.orderSide,
  808. orderType: orderType ?? this.orderType,
  809. amountUnit: amountUnit ?? this.amountUnit,
  810. inputPrice: inputPrice ?? this.inputPrice,
  811. inputSize: inputSize ?? this.inputSize,
  812. sliderPercent: sliderPercent ?? this.sliderPercent,
  813. isSliderInput: isSliderInput ?? this.isSliderInput,
  814. tpslEnabled: tpslEnabled ?? this.tpslEnabled,
  815. tpPrice: tpPrice ?? this.tpPrice,
  816. slPrice: slPrice ?? this.slPrice,
  817. fundingRate: fundingRate ?? this.fundingRate,
  818. fundingCountdown: fundingCountdown ?? this.fundingCountdown,
  819. activeTab: activeTab ?? this.activeTab,
  820. positions: positions ?? this.positions,
  821. openOrders: openOrders ?? this.openOrders,
  822. ordersHasMore: ordersHasMore ?? this.ordersHasMore,
  823. ordersPage: ordersPage ?? this.ordersPage,
  824. ordersLoadingMore: ordersLoadingMore ?? this.ordersLoadingMore,
  825. accountInfo: accountInfo ?? this.accountInfo,
  826. isLoading: isLoading ?? this.isLoading,
  827. isTabLoading: isTabLoading ?? this.isTabLoading,
  828. isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
  829. errorMsg: errorMsg,
  830. orderBookAsks: orderBookAsks ?? this.orderBookAsks,
  831. orderBookBids: orderBookBids ?? this.orderBookBids,
  832. hideOtherSymbols: hideOtherSymbols ?? this.hideOtherSymbols,
  833. pricePrecision: pricePrecision ?? this.pricePrecision,
  834. coinPrecision: coinPrecision ?? this.coinPrecision,
  835. usdtPrecision: usdtPrecision ?? this.usdtPrecision,
  836. leverageMin: leverageMin ?? this.leverageMin,
  837. leverageMax: leverageMax ?? this.leverageMax,
  838. leverageOptions: leverageOptions ?? this.leverageOptions,
  839. isDiscreteLeverage: isDiscreteLeverage ?? this.isDiscreteLeverage,
  840. openFeeRate: openFeeRate ?? this.openFeeRate,
  841. );
  842. }
  843. class FuturesNotifier extends AutoDisposeFamilyNotifier<FuturesState, String> {
  844. StreamSubscription<Map<String, dynamic>>? _tickerSub;
  845. StreamSubscription<Map<String, dynamic>>? _depthSub;
  846. StreamSubscription<Map<String, dynamic>>? _markSub;
  847. Timer? _fundingTimer;
  848. Timer? _pollTimer;
  849. bool _manuallyPaused = false; // 子页面推入时手动暂停,防止 tab 切换后误恢复
  850. int _nextSettleTimeMs = 0;
  851. final Map<String, int> _lastTickerUpdateMs = {}; // ticker 节流
  852. int _lastDepthUpdateMs = 0; // 盘口节流
  853. int _lastPositionTickUpdateMs = 0; // 持仓 mark price 节流(ticker 路径)
  854. Timer? _depthTimer;
  855. List<Map<String, dynamic>>? _pendingAsks;
  856. List<Map<String, dynamic>>? _pendingBids;
  857. // 切换到平仓模式时,若原 orderType 为计划委托类型则暂存,切回开仓时恢复
  858. OrderType? _savedConditionalType;
  859. // 路由 symbol(如 BTCUSDT)转 API 格式(BTC/USDT)
  860. static String _toApiSymbol(String symbol) {
  861. String s = symbol.replaceAll('-', '/');
  862. if (!s.contains('/')) {
  863. for (final base in ['USDT', 'BTC', 'ETH', 'BUSD']) {
  864. if (s.endsWith(base) && s.length > base.length) {
  865. return '${s.substring(0, s.length - base.length)}/$base';
  866. }
  867. }
  868. }
  869. return s;
  870. }
  871. @override
  872. FuturesState build(String symbol) {
  873. ref.onDispose(_dispose);
  874. // 读取该交易对上次用户选择的保证金模式(跨 symbol 切换时保留)
  875. final savedModeStr = ref.read(sharedPreferencesProvider)
  876. .getString('futures_margin_mode_$symbol');
  877. final savedMode = switch (savedModeStr) {
  878. 'split' => MarginMode.split,
  879. 'isolated' => MarginMode.isolated,
  880. _ => MarginMode.cross,
  881. };
  882. final initial = FuturesState(
  883. symbol: symbol,
  884. lastPrice: 0,
  885. markPrice: 0,
  886. positions: const [],
  887. openOrders: const [],
  888. accountInfo: const FuturesAccountInfo.empty(),
  889. isLoading: true,
  890. amountUnit: ref.read(futuresAmountUnitPrefProvider),
  891. hideOtherSymbols: ref.read(futuresHideOtherPrefProvider),
  892. marginMode: savedMode,
  893. leverage: 20,
  894. );
  895. Future.microtask(() => _init(symbol));
  896. // 监听 K 线行情数据,保持与 K 线页面价格一致
  897. // 注意:不能用 fireImmediately: true,build() 返回前 state 尚未初始化
  898. ref.listen<MarketStats?>(
  899. marketDetailProvider(MarketDetailKey(symbol: symbol, isFutures: true))
  900. .select((s) => s.stats),
  901. (_, stats) {
  902. if (stats == null || stats.lastPrice <= 0) return;
  903. state = state.copyWith(
  904. lastPrice: stats.lastPrice,
  905. change24h: stats.change24h,
  906. );
  907. },
  908. );
  909. // 监听 WS 重连:重连成功后若合约信息未加载则重新初始化
  910. ref.listen<AsyncValue<WsConnectionState>>(
  911. wsConnectionStateProvider,
  912. (prev, next) {
  913. final prevState = prev?.valueOrNull;
  914. final nextState = next.valueOrNull;
  915. // 任何 → connected 的转换都触发(reconnecting→connecting→connected,prev 是 connecting)
  916. if (nextState == WsConnectionState.connected &&
  917. prevState != WsConnectionState.connected) {
  918. if (state.contractCoinId == 0) {
  919. _init(symbol);
  920. }
  921. }
  922. },
  923. );
  924. // 监听登录状态:登录后自动加载持仓;退出后清空数据并停止轮询
  925. ref.listen<bool>(isLoggedInProvider, (prev, loggedIn) {
  926. if (loggedIn) {
  927. // 登录后刷新带单员身份(用于判断是否显示分仓选项)
  928. ref.read(copyTradingProvider.notifier).silentRefresh();
  929. _loadWallet(state.symbol);
  930. _loadCurrentOrders();
  931. // 在合约页时直接启动轮询,不区分子 tab
  932. if (_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
  933. _startPolling(state.symbol);
  934. }
  935. } else {
  936. _stopPolling();
  937. state = state.copyWith(
  938. positions: [],
  939. openOrders: [],
  940. accountInfo: const FuturesAccountInfo.empty(),
  941. );
  942. }
  943. });
  944. // 监听底部导航 tab 切换:离开合约页停止轮询,进入合约页直接启动轮询
  945. // 注意:若子页面手动暂停了轮询(_manuallyPaused),切回 tab 时不自动恢复
  946. ref.listen<int>(activeBottomTabProvider, (prev, tabIndex) {
  947. if (_isFuturesTabActive(tabIndex)) {
  948. if (ref.read(isLoggedInProvider) && !_manuallyPaused) {
  949. _loadWallet(state.symbol);
  950. _loadCurrentOrders();
  951. _startPolling(state.symbol);
  952. }
  953. } else {
  954. _stopPolling();
  955. }
  956. });
  957. // 监听带单员身份变化:若用户身份为带单员,强制切回全仓模式
  958. ref.listen<bool>(
  959. copyTradingProvider.select((s) => s.isTrader),
  960. (_, isTrader) {
  961. if (isTrader && state.marginMode == MarginMode.split) {
  962. state = state.copyWith(marginMode: MarginMode.cross);
  963. }
  964. },
  965. );
  966. return initial;
  967. }
  968. Future<void> _init(String symbol) async {
  969. await _loadContractInfo(symbol);
  970. _subscribeWebSocket(symbol);
  971. state = state.copyWith(isLoading: false, isTabLoading: true);
  972. if (ref.read(isLoggedInProvider)) {
  973. try {
  974. await Future.wait([_loadWallet(symbol), _loadCurrentOrders()]);
  975. } catch (_) {}
  976. if (_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
  977. _startPolling(symbol);
  978. }
  979. }
  980. state = state.copyWith(isTabLoading: false);
  981. }
  982. void _startPolling(String symbol) {
  983. _pollTimer?.cancel();
  984. _pollTimer = Timer.periodic(const Duration(seconds: 1), (_) {
  985. if (!_isFuturesTabActive(ref.read(activeBottomTabProvider))) {
  986. _stopPolling();
  987. return;
  988. }
  989. _loadWallet(symbol);
  990. _loadCurrentOrders();
  991. });
  992. }
  993. void _stopPolling() {
  994. _pollTimer?.cancel();
  995. _pollTimer = null;
  996. }
  997. /// 供页面跳转时外部调用:暂停轮询(标记手动暂停,防止 tab 切换后误恢复)
  998. void stopPolling() {
  999. _manuallyPaused = true;
  1000. _stopPolling();
  1001. }
  1002. /// 供页面返回时外部调用:恢复轮询(仅登录且在合约页时)
  1003. void resumePolling(String symbol) {
  1004. _manuallyPaused = false;
  1005. if (ref.read(isLoggedInProvider) &&
  1006. _isFuturesTabActive(ref.read(activeBottomTabProvider))) {
  1007. _startPolling(symbol);
  1008. }
  1009. }
  1010. Future<void> _loadContractInfo(String symbol) async {
  1011. try {
  1012. final svc = _service;
  1013. final apiSymbol = _toApiSymbol(symbol);
  1014. final info = await svc.getSymbolInfo(apiSymbol);
  1015. final id = (info['id'] as num?)?.toInt() ?? 0;
  1016. if (id == 0) return;
  1017. final pricePrecision = ((info['coinScale'] ?? info['minScale']) as num?)?.toInt() ?? 2;
  1018. const volScale = 0;
  1019. final coinPrecision = (info['minScale'] as num?)?.toInt() ?? 4;
  1020. final usdtPrecision = (info['baseCoinScale'] as num?)?.toInt() ?? 4;
  1021. final contractSize = _toDouble(
  1022. info['shareNumber'] ?? info['contractSize'] ?? info['shareSize'] ?? 1);
  1023. final coinObj = info['coin'] as Map?;
  1024. final rawSym = coinObj?['symbol'] as String? ?? apiSymbol;
  1025. final coinSymbol = rawSym.contains('/') ? rawSym.split('/').first : rawSym;
  1026. // 杠杆档位(逗号分隔字符串)
  1027. final rawLeverage = info['leverage']?.toString() ?? '';
  1028. final leverageOptions = rawLeverage.isNotEmpty
  1029. ? rawLeverage
  1030. .split(',')
  1031. .map((s) => int.tryParse(s.trim()))
  1032. .whereType<int>()
  1033. .where((v) => v > 0)
  1034. .toList()
  1035. : <int>[];
  1036. leverageOptions.sort();
  1037. final leverageMin = leverageOptions.isNotEmpty ? leverageOptions.first : 1;
  1038. final leverageMax = leverageOptions.isNotEmpty ? leverageOptions.last : 125;
  1039. // leverageType: 1=固定档位,2=区间任意值
  1040. final leverageType = (info['leverageType'] as num?)?.toInt() ?? 2;
  1041. final isDiscrete = leverageType == 1;
  1042. final openFeeRate = _toDouble(info['openFee'] ?? info['makerFee'] ?? 0.0005);
  1043. state = state.copyWith(
  1044. contractCoinId: id,
  1045. pricePrecision: pricePrecision,
  1046. volScale: volScale.toInt(),
  1047. coinPrecision: coinPrecision,
  1048. usdtPrecision: usdtPrecision,
  1049. contractSize: contractSize > 0 ? contractSize : 1.0,
  1050. coinSymbol: coinSymbol,
  1051. leverageMin: leverageMin,
  1052. leverageMax: leverageMax,
  1053. leverageOptions: leverageOptions,
  1054. isDiscreteLeverage: isDiscrete,
  1055. openFeeRate: openFeeRate > 0 ? openFeeRate : 0.0005,
  1056. );
  1057. try {
  1058. final leverage = await svc.getLeverage(id);
  1059. state = state.copyWith(leverage: leverage.toDouble());
  1060. } catch (_) {}
  1061. } catch (_) {}
  1062. }
  1063. Future<void> refreshWallet() => _loadWallet(state.symbol);
  1064. Future<void> _loadWallet(String symbol) async {
  1065. if (!ref.read(isLoggedInProvider)) return;
  1066. final data = await _service
  1067. .getWithPositions(symbol)
  1068. .catchError((_) => <String, dynamic>{});
  1069. final wallet = data;
  1070. final rawPositions = (data['currentPositionWithCutList'] as List<dynamic>? ?? [])
  1071. .cast<Map<String, dynamic>>();
  1072. List<FuturesPosition> positions = [];
  1073. final cs = state.contractSize > 0 ? state.contractSize : 1.0;
  1074. for (final item in rawPositions) {
  1075. try {
  1076. positions.add(FuturesPosition.fromJson(item).withContractSize(cs));
  1077. } catch (e) {
  1078. debugPrint('[FuturesPosition] parse error: $e\nitem: $item');
  1079. }
  1080. }
  1081. // 有匹配当前币对的持仓时,同步杠杆到下单表单
  1082. final currentSymbol = state.symbol.toUpperCase().replaceAll('-', '');
  1083. final matchedPos = positions.where((p) {
  1084. final ps = p.symbol.toUpperCase().replaceAll('-', '').replaceAll('/', '');
  1085. return ps.contains(currentSymbol) || currentSymbol.contains(ps);
  1086. }).toList();
  1087. if (matchedPos.isNotEmpty && matchedPos.first.leverage > 0) {
  1088. state = state.copyWith(
  1089. positions: positions,
  1090. leverage: matchedPos.first.leverage,
  1091. );
  1092. } else {
  1093. state = state.copyWith(positions: positions);
  1094. }
  1095. _subscribePositionTickers(positions);
  1096. try {
  1097. if (wallet.isNotEmpty) {
  1098. final totalPnl = positions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
  1099. final positionsMargin =
  1100. positions.fold(0.0, (sum, p) => sum + p.margin);
  1101. final accountInfo = FuturesAccountInfo.fromJson(
  1102. wallet,
  1103. unrealizedPnl: totalPnl,
  1104. positionsMargin: positionsMargin,
  1105. );
  1106. // marginMode 不从服务器同步(进入合约页默认全仓,用户主动切换才变更)
  1107. final isTrader = ref.read(copyTradingProvider).isTrader;
  1108. final syncedMode = (isTrader && state.marginMode == MarginMode.split)
  1109. ? MarginMode.cross
  1110. : null;
  1111. state = state.copyWith(
  1112. accountInfo: accountInfo,
  1113. marginMode: syncedMode ?? state.marginMode,
  1114. );
  1115. }
  1116. } catch (_) {}
  1117. }
  1118. Future<void> _loadCurrentOrders() async {
  1119. if (!ref.read(isLoggedInProvider)) return;
  1120. try {
  1121. final result = await _service.getCurrentOrders(pageNo: 1, pageSize: 10);
  1122. final orders = _parseOrders(result.items);
  1123. state = state.copyWith(
  1124. openOrders: orders,
  1125. ordersPage: 1,
  1126. ordersHasMore: result.hasMore,
  1127. );
  1128. } catch (_) {}
  1129. }
  1130. Future<void> loadMoreOrders() async {
  1131. if (state.ordersLoadingMore || !state.ordersHasMore) return;
  1132. state = state.copyWith(ordersLoadingMore: true);
  1133. try {
  1134. final nextPage = state.ordersPage + 1;
  1135. final result =
  1136. await _service.getCurrentOrders(pageNo: nextPage, pageSize: 10);
  1137. final more = _parseOrders(result.items);
  1138. state = state.copyWith(
  1139. openOrders: [...state.openOrders, ...more],
  1140. ordersPage: nextPage,
  1141. ordersHasMore: result.hasMore,
  1142. ordersLoadingMore: false,
  1143. );
  1144. } catch (e) {
  1145. state = state.copyWith(ordersLoadingMore: false);
  1146. }
  1147. }
  1148. static List<FuturesOrder> _parseOrders(List<Map<String, dynamic>> raw) {
  1149. final orders = <FuturesOrder>[];
  1150. for (final item in raw) {
  1151. try {
  1152. orders.add(FuturesOrder.fromJson(item));
  1153. } catch (_) {}
  1154. }
  1155. return orders;
  1156. }
  1157. void _subscribeWebSocket(String symbol) {
  1158. final wsSymbol = symbol.replaceAll('-', '').toLowerCase();
  1159. final ws = ref.read(wsClientProvider);
  1160. ws.subscribeTicker(wsSymbol);
  1161. ws.subscribeDepth(wsSymbol);
  1162. ws.subscribeMark(wsSymbol);
  1163. _markSub?.cancel();
  1164. _markSub = ws.markStream
  1165. .where((d) => (d['symbol'] as String? ?? '').toLowerCase() == wsSymbol)
  1166. .listen((data) {
  1167. final markPrice = (data['markPrice'] as num?)?.toDouble() ?? 0;
  1168. final fundingRate = (data['fundingRate'] as num?)?.toDouble() ?? 0;
  1169. final nextFundingTime = (data['nextFundingTime'] as num?)?.toInt() ?? 0;
  1170. if (markPrice > 0) {
  1171. state = state.copyWith(markPrice: markPrice);
  1172. }
  1173. // 新结算时间 > 当前记录时才更新(允许同一周期多次推送相同值)
  1174. if (nextFundingTime > 0 &&
  1175. nextFundingTime > DateTime.now().millisecondsSinceEpoch &&
  1176. nextFundingTime > _nextSettleTimeMs) {
  1177. _nextSettleTimeMs = nextFundingTime;
  1178. _startFundingCountdown();
  1179. }
  1180. state = state.copyWith(fundingRate: fundingRate);
  1181. });
  1182. _tickerSub = ws.tickerStream.listen((data) {
  1183. final s = (data['symbol'] as String? ?? '').toLowerCase();
  1184. final price = (data['price'] as num?)?.toDouble() ?? 0;
  1185. if (price <= 0) return;
  1186. final priceStr = data['priceStr'] as String? ?? '';
  1187. final now = DateTime.now().millisecondsSinceEpoch;
  1188. final isCurrentSymbol = s == wsSymbol;
  1189. // 非当前交易对节流 500ms
  1190. if (!isCurrentSymbol) {
  1191. final last = _lastTickerUpdateMs[s] ?? 0;
  1192. if (now - last < 500) return;
  1193. }
  1194. _lastTickerUpdateMs[s] = now;
  1195. final tickerApiSymbol = _toApiSymbol(s.toUpperCase());
  1196. final tSym = tickerApiSymbol.replaceAll('/', '').toUpperCase();
  1197. final hasMatch = state.positions.any(
  1198. (p) => p.symbol.replaceAll('/', '').toUpperCase() == tSym);
  1199. // 持仓 mark price 更新节流 1000ms,避免每次 WS 推送都重建 positions 数组
  1200. final canUpdatePositions =
  1201. now - _lastPositionTickUpdateMs >= 1000;
  1202. if (isCurrentSymbol) {
  1203. final change = (data['change24h'] as num?)?.toDouble() ?? 0;
  1204. if (hasMatch && canUpdatePositions) {
  1205. _lastPositionTickUpdateMs = now;
  1206. final updatedPositions = state.positions.map((p) {
  1207. return p.symbol.replaceAll('/', '').toUpperCase() == tSym
  1208. ? p.withMarkPrice(price)
  1209. : p;
  1210. }).toList();
  1211. final totalPnl =
  1212. updatedPositions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
  1213. final ai = state.accountInfo;
  1214. state = state.copyWith(
  1215. lastPrice: price,
  1216. lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
  1217. markPrice: price,
  1218. change24h: change,
  1219. positions: updatedPositions,
  1220. accountInfo: FuturesAccountInfo(
  1221. totalBalance: ai.totalBalance,
  1222. usedMargin: ai.usedMargin,
  1223. availableMargin: ai.availableMargin,
  1224. unrealizedPnl: totalPnl,
  1225. ),
  1226. );
  1227. } else {
  1228. state = state.copyWith(
  1229. lastPrice: price,
  1230. lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
  1231. markPrice: price,
  1232. change24h: change,
  1233. );
  1234. }
  1235. } else if (hasMatch && canUpdatePositions) {
  1236. _lastPositionTickUpdateMs = now;
  1237. final updatedPositions = state.positions.map((p) {
  1238. return p.symbol.replaceAll('/', '').toUpperCase() == tSym
  1239. ? p.withMarkPrice(price)
  1240. : p;
  1241. }).toList();
  1242. final totalPnl =
  1243. updatedPositions.fold(0.0, (sum, p) => sum + p.unrealizedPnl);
  1244. final ai = state.accountInfo;
  1245. state = state.copyWith(
  1246. positions: updatedPositions,
  1247. accountInfo: FuturesAccountInfo(
  1248. totalBalance: ai.totalBalance,
  1249. usedMargin: ai.usedMargin,
  1250. availableMargin: ai.availableMargin,
  1251. unrealizedPnl: totalPnl,
  1252. ),
  1253. );
  1254. }
  1255. });
  1256. _depthSub = ws.orderBookStream.listen((data) {
  1257. final s = (data['symbol'] as String? ?? '').toLowerCase();
  1258. if (s != wsSymbol) return;
  1259. // 盘口节流 120ms
  1260. final now = DateTime.now().millisecondsSinceEpoch;
  1261. if (now - _lastDepthUpdateMs < 120) return;
  1262. _lastDepthUpdateMs = now;
  1263. final rawAsks = data['asks'] as List? ?? [];
  1264. final rawBids = data['bids'] as List? ?? [];
  1265. _pendingAsks = rawAsks.whereType<Map<String, dynamic>>().toList();
  1266. _pendingBids = rawBids.whereType<Map<String, dynamic>>().toList();
  1267. if (_depthTimer == null || !_depthTimer!.isActive) {
  1268. _depthTimer = Timer(Duration.zero, () {
  1269. final asks = _pendingAsks;
  1270. final bids = _pendingBids;
  1271. _pendingAsks = null;
  1272. _pendingBids = null;
  1273. if (asks != null && bids != null) {
  1274. state = state.copyWith(orderBookAsks: asks, orderBookBids: bids);
  1275. }
  1276. });
  1277. }
  1278. });
  1279. }
  1280. void _subscribePositionTickers(List<FuturesPosition> positions) {
  1281. if (positions.isEmpty) return;
  1282. try {
  1283. final ws = ref.read(wsClientProvider);
  1284. final symbols = positions
  1285. .map((p) => p.symbol.replaceAll('/', '').toLowerCase())
  1286. .toSet()
  1287. .toList();
  1288. ws.subscribeTickerBatch(symbols);
  1289. } catch (_) {}
  1290. }
  1291. void _startFundingCountdown() {
  1292. _fundingTimer?.cancel();
  1293. _updateFundingCountdown();
  1294. _fundingTimer = Timer.periodic(const Duration(seconds: 1), (_) {
  1295. _updateFundingCountdown();
  1296. });
  1297. }
  1298. void _updateFundingCountdown() {
  1299. final remaining = _nextSettleTimeMs - DateTime.now().millisecondsSinceEpoch;
  1300. if (remaining <= 0) {
  1301. state = state.copyWith(fundingCountdown: '00:00:00');
  1302. _fundingTimer?.cancel();
  1303. // 下次结算时间由 WS mark 推送更新,无需 HTTP 拉取
  1304. return;
  1305. }
  1306. final secs = remaining ~/ 1000;
  1307. final h = secs ~/ 3600;
  1308. final m = (secs % 3600) ~/ 60;
  1309. final s = secs % 60;
  1310. state = state.copyWith(
  1311. fundingCountdown:
  1312. '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
  1313. );
  1314. }
  1315. void _dispose() {
  1316. _tickerSub?.cancel();
  1317. _tickerSub = null;
  1318. _depthSub?.cancel();
  1319. _depthSub = null;
  1320. _markSub?.cancel();
  1321. _markSub = null;
  1322. _fundingTimer?.cancel();
  1323. _fundingTimer = null;
  1324. _pollTimer?.cancel();
  1325. _pollTimer = null;
  1326. _depthTimer?.cancel();
  1327. _depthTimer = null;
  1328. }
  1329. void setOrderSide(OrderSide side) =>
  1330. state = state.copyWith(orderSide: side);
  1331. void setOrderType(OrderType type) =>
  1332. state = state.copyWith(orderType: type);
  1333. void setPositionMode(PositionMode mode) {
  1334. final isConditional = state.orderType == OrderType.conditionalMarket ||
  1335. state.orderType == OrderType.conditionalLimit;
  1336. if (mode == PositionMode.close && isConditional) {
  1337. // 平仓不支持计划委托:暂存类型,临时切换为市价
  1338. _savedConditionalType = state.orderType;
  1339. state = state.copyWith(positionMode: mode, orderType: OrderType.market);
  1340. } else if (mode == PositionMode.open && _savedConditionalType != null) {
  1341. // 切回开仓:恢复之前的计划委托类型
  1342. final restored = _savedConditionalType!;
  1343. _savedConditionalType = null;
  1344. state = state.copyWith(positionMode: mode, orderType: restored);
  1345. } else {
  1346. state = state.copyWith(positionMode: mode);
  1347. }
  1348. }
  1349. /// 切换仓位模式。
  1350. /// 全仓/分仓:必须等后端 modifyType 成功后才更新本地状态
  1351. /// 返回 null 表示成功;返回错误消息字符串表示失败(调用方负责展示)
  1352. Future<String?> setMarginMode(MarginMode mode) async {
  1353. // 全仓/分仓:先调后端,成功后再更新本地状态;期间禁止下单
  1354. final serverType = mode == MarginMode.split ? 1 : 0;
  1355. state = state.copyWith(isSwitchingMode: true);
  1356. try {
  1357. await _service.modifyPositionType(serverType);
  1358. state = state.copyWith(
  1359. marginMode: mode,
  1360. isSwitchingMode: false,
  1361. );
  1362. // 持久化该交易对的保证金模式,下次切回时自动恢复
  1363. final modeStr = mode == MarginMode.split ? 'split'
  1364. : mode == MarginMode.isolated ? 'isolated'
  1365. : 'cross';
  1366. ref.read(sharedPreferencesProvider)
  1367. .setString('futures_margin_mode_${state.symbol}', modeStr);
  1368. // 切换回普通模式后,重新从服务器获取当前杠杆倍数
  1369. if (state.contractCoinId > 0) {
  1370. try {
  1371. final lev = await _service.getLeverage(state.contractCoinId);
  1372. state = state.copyWith(leverage: lev.toDouble());
  1373. } catch (_) {}
  1374. }
  1375. return null;
  1376. } catch (e) {
  1377. // 切换失败:重新拉取真实状态,保持原有 marginMode 不变
  1378. state = state.copyWith(isSwitchingMode: false);
  1379. await _loadWallet(state.symbol);
  1380. final msg = e.toString();
  1381. return msg;
  1382. }
  1383. }
  1384. void setAmountUnit(AmountUnit unit) {
  1385. state = state.copyWith(amountUnit: unit);
  1386. ref.read(futuresAmountUnitPrefProvider.notifier).state = unit;
  1387. }
  1388. void toggleHideOtherSymbols() {
  1389. final next = !state.hideOtherSymbols;
  1390. state = state.copyWith(hideOtherSymbols: next);
  1391. ref.read(futuresHideOtherPrefProvider.notifier).state = next;
  1392. }
  1393. void setTab(FuturesTab tab) {
  1394. state = state.copyWith(activeTab: tab);
  1395. _loadWallet(state.symbol);
  1396. _loadCurrentOrders();
  1397. }
  1398. void setSliderPercent(double pct) =>
  1399. state = state.copyWith(sliderPercent: pct, isSliderInput: true);
  1400. void setSliderPercentFromInput(double pct) =>
  1401. state = state.copyWith(sliderPercent: pct, isSliderInput: false);
  1402. void toggleTpsl() =>
  1403. state = state.copyWith(tpslEnabled: !state.tpslEnabled);
  1404. String _errMsg(Object e) {
  1405. if (e is ApiException) return e.message;
  1406. if (e is DioException) {
  1407. final inner = e.error;
  1408. if (inner is ApiException) return inner.message;
  1409. final data = e.response?.data;
  1410. if (data is Map) {
  1411. final msg = data['message'] as String? ?? data['msg'] as String?;
  1412. if (msg != null && msg.isNotEmpty) return msg;
  1413. }
  1414. final errStr = inner?.toString() ?? '';
  1415. if (errStr.isNotEmpty) {
  1416. final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
  1417. .firstMatch(errStr);
  1418. if (m != null) return m.group(1)!.trim();
  1419. }
  1420. final dioStr = e.toString();
  1421. final dm = RegExp(r'Error:\s*ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
  1422. .firstMatch(dioStr);
  1423. if (dm != null) return dm.group(1)!.trim();
  1424. if (e.type == DioExceptionType.connectionTimeout ||
  1425. e.type == DioExceptionType.receiveTimeout ||
  1426. e.type == DioExceptionType.sendTimeout) {
  1427. return 'errTimeout';
  1428. }
  1429. if (e.type == DioExceptionType.connectionError) {
  1430. return 'errNetworkError';
  1431. }
  1432. }
  1433. final s = e.toString();
  1434. final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
  1435. .firstMatch(s);
  1436. if (m != null) return m.group(1)!.trim();
  1437. final lines = s.split('\n');
  1438. for (final line in lines.reversed) {
  1439. final t = line.trim();
  1440. if (t.isEmpty || t.startsWith('Uri:') || t.startsWith('DioException')) continue;
  1441. final idx = t.lastIndexOf(': ');
  1442. if (idx != -1 && idx < t.length - 2) return t.substring(idx + 2).trim();
  1443. return t;
  1444. }
  1445. return s;
  1446. }
  1447. Future<String?> setLeverage(double lev) async {
  1448. if (state.contractCoinId <= 0) {
  1449. await _loadContractInfo(state.symbol);
  1450. if (state.contractCoinId <= 0) return 'errServiceUnavailable';
  1451. }
  1452. try {
  1453. await _service.modifyLeverage(state.contractCoinId, lev.toInt());
  1454. state = state.copyWith(leverage: lev);
  1455. return null;
  1456. } catch (e) {
  1457. return _errMsg(e);
  1458. }
  1459. }
  1460. Future<String?> placeOpenOrder({
  1461. OrderSide? side,
  1462. double? entrustPrice,
  1463. double? triggerPrice,
  1464. double? volume,
  1465. double? tpPrice,
  1466. double? slPrice,
  1467. }) async {
  1468. // 模式切换 API 尚未完成,禁止下单,防止 positionType 不一致导致仓位错误
  1469. if (state.isSwitchingMode) return 'errSwitchingMode';
  1470. if (state.contractCoinId == 0) {
  1471. await _loadContractInfo(state.symbol);
  1472. if (state.contractCoinId == 0) return 'errServiceUnavailable';
  1473. }
  1474. final vol = volume ?? state.inputSize;
  1475. if (vol <= 0) return 'errEnterVolume';
  1476. final int type;
  1477. final double? ep;
  1478. switch (state.orderType) {
  1479. case OrderType.market:
  1480. type = 0;
  1481. ep = null;
  1482. case OrderType.limit:
  1483. type = 1;
  1484. ep = entrustPrice ?? state.inputPrice;
  1485. if (ep <= 0) return 'errEnterPrice';
  1486. case OrderType.conditionalMarket:
  1487. type = 2;
  1488. ep = null; // 计划市价不传委托价,后端以 entrustPrice=0 标识市价
  1489. if ((triggerPrice ?? 0) <= 0) return 'errEnterTriggerPrice';
  1490. case OrderType.conditionalLimit:
  1491. type = 2;
  1492. ep = entrustPrice;
  1493. if ((triggerPrice ?? 0) <= 0) return 'errEnterTriggerPrice';
  1494. }
  1495. final dir = (side ?? state.orderSide) == OrderSide.long ? 0 : 1;
  1496. // 滑块模式:市价/限价传百分比给后端;计划委托不支持百分比,需换算为实际张数
  1497. if (state.isSliderInput && state.sliderPercent > 0.0001 && type != 2) {
  1498. final pct = (state.sliderPercent * 100).round().clamp(1, 100);
  1499. try {
  1500. await _service.openOrder(
  1501. contractCoinId: state.contractCoinId,
  1502. type: type,
  1503. direction: dir,
  1504. volume: pct.toDouble(),
  1505. leverage: state.leverage.toInt(),
  1506. entrustPrice: ep,
  1507. triggerPrice: triggerPrice,
  1508. profitPrice: tpPrice ?? state.tpPrice,
  1509. lossPrice: slPrice ?? state.slPrice,
  1510. isPercentage: 1,
  1511. positionType: state.marginMode == MarginMode.split ? 1 : 0,
  1512. );
  1513. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1514. Future.delayed(const Duration(seconds: 2), () => _loadWallet(state.symbol));
  1515. return null;
  1516. } catch (e) {
  1517. return _errMsg(e);
  1518. }
  1519. }
  1520. if (state.contractSize <= 0) return 'errContractNotReady';
  1521. // 计划市价用触发价作为换算基准,计划限价用委托价,普通单用委托价/最新价
  1522. final double effectivePrice;
  1523. if (state.orderType == OrderType.conditionalMarket) {
  1524. effectivePrice = (triggerPrice != null && triggerPrice > 0)
  1525. ? triggerPrice
  1526. : state.lastPrice;
  1527. } else {
  1528. effectivePrice = ep ?? (state.lastPrice > 0 ? state.lastPrice : 0);
  1529. }
  1530. if (effectivePrice <= 0) return 'errPriceNotReady';
  1531. final double volumeInBtc;
  1532. switch (state.amountUnit) {
  1533. case AmountUnit.lots:
  1534. volumeInBtc = vol * state.contractSize / effectivePrice;
  1535. case AmountUnit.usdt:
  1536. volumeInBtc = vol / effectivePrice;
  1537. case AmountUnit.btc:
  1538. volumeInBtc = vol;
  1539. }
  1540. final factor = math.pow(10, state.coinPrecision).toDouble();
  1541. final double finalVolume = (volumeInBtc * factor).floorToDouble() / factor;
  1542. if (finalVolume <= 0) return 'errVolumeInsufficient';
  1543. try {
  1544. await _service.openOrder(
  1545. contractCoinId: state.contractCoinId,
  1546. type: type,
  1547. direction: dir,
  1548. volume: finalVolume,
  1549. leverage: state.leverage.toInt(),
  1550. entrustPrice: ep,
  1551. triggerPrice: triggerPrice,
  1552. profitPrice: tpPrice ?? state.tpPrice,
  1553. lossPrice: slPrice ?? state.slPrice,
  1554. positionType: state.marginMode == MarginMode.split ? 1 : 0,
  1555. );
  1556. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1557. Future.delayed(const Duration(seconds: 2), () => _loadWallet(state.symbol));
  1558. return null;
  1559. } catch (e) {
  1560. return _errMsg(e);
  1561. }
  1562. }
  1563. /// 市价平仓;volume 不传或 <=0 时全仓平
  1564. Future<String?> closeMarket(FuturesPosition position, {double? volume}) async {
  1565. try {
  1566. final vol = (volume != null && volume > 0)
  1567. ? volume.clamp(0.0, position.availableSize)
  1568. : position.availableSize;
  1569. await _service.closeMarket(
  1570. positionId: position.id,
  1571. volume: vol,
  1572. );
  1573. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1574. return null;
  1575. } catch (e) {
  1576. return _errMsg(e);
  1577. }
  1578. }
  1579. /// 限价平仓;volume 不传或 <=0 时全仓平
  1580. Future<String?> closeLimit(FuturesPosition position, double price, {double? volume}) async {
  1581. if (price <= 0) return 'errEnterClosePrice';
  1582. try {
  1583. final vol = (volume != null && volume > 0)
  1584. ? volume.clamp(0.0, position.availableSize)
  1585. : position.availableSize;
  1586. await _service.closeLimit(
  1587. positionId: position.id,
  1588. price: price,
  1589. volume: vol,
  1590. );
  1591. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1592. return null;
  1593. } catch (e) {
  1594. return _errMsg(e);
  1595. }
  1596. }
  1597. Future<String?> cancelOrder(FuturesOrder order) async {
  1598. final id = int.tryParse(order.id);
  1599. if (id == null) return 'errInvalidOrderId';
  1600. try {
  1601. await _service.cancelOrder(id);
  1602. await _loadCurrentOrders();
  1603. return null;
  1604. } catch (e) {
  1605. return _errMsg(e);
  1606. }
  1607. }
  1608. Future<String?> closeAllPositions() async {
  1609. try {
  1610. await _service.closeAllPositions();
  1611. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1612. return null;
  1613. } catch (e) {
  1614. return _errMsg(e);
  1615. }
  1616. }
  1617. // price 为 null/0 时市价,否则限价
  1618. Future<String?> closeByDirection(OrderSide side, {double? price, double? volume}) async {
  1619. final normSymbol = FuturesState._norm(_toApiSymbol(state.symbol));
  1620. final pos = state.positions.where(
  1621. (p) => p.side == side && FuturesState._norm(p.symbol) == normSymbol,
  1622. ).firstOrNull;
  1623. if (pos == null) return side == OrderSide.long ? 'errNoLongPosition' : 'errNoShortPosition';
  1624. final closeVol = (volume != null && volume > 0)
  1625. ? volume.clamp(0.0, pos.availableSize)
  1626. : pos.availableSize;
  1627. if (price != null && price > 0) {
  1628. return _closeLimitVolume(pos, price, closeVol);
  1629. } else {
  1630. return _closeMarketVolume(pos, closeVol);
  1631. }
  1632. }
  1633. Future<String?> closeConditionalByDirection(
  1634. OrderSide side, {
  1635. required double triggerPrice,
  1636. double? volume,
  1637. double entrustPrice = 0,
  1638. }) async {
  1639. final normSymbol = FuturesState._norm(_toApiSymbol(state.symbol));
  1640. final pos = state.positions.where(
  1641. (p) => p.side == side && FuturesState._norm(p.symbol) == normSymbol,
  1642. ).firstOrNull;
  1643. if (pos == null) return side == OrderSide.long ? 'errNoLongPosition' : 'errNoShortPosition';
  1644. final closeVol = (volume != null && volume > 0)
  1645. ? volume.clamp(0.0, pos.availableSize)
  1646. : pos.availableSize;
  1647. try {
  1648. await _service.closeConditional(
  1649. positionId: pos.id,
  1650. triggerPrice: triggerPrice,
  1651. volume: closeVol,
  1652. entrustPrice: entrustPrice,
  1653. );
  1654. await _loadCurrentOrders();
  1655. return null;
  1656. } catch (e) {
  1657. return _errMsg(e);
  1658. }
  1659. }
  1660. Future<String?> _closeMarketVolume(FuturesPosition position, double volume) async {
  1661. try {
  1662. await _service.closeMarket(positionId: position.id, volume: volume);
  1663. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1664. return null;
  1665. } catch (e) {
  1666. return _errMsg(e);
  1667. }
  1668. }
  1669. Future<String?> _closeLimitVolume(FuturesPosition position, double price, double volume) async {
  1670. if (price <= 0) return 'errEnterClosePrice';
  1671. try {
  1672. await _service.closeLimit(positionId: position.id, price: price, volume: volume);
  1673. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1674. return null;
  1675. } catch (e) {
  1676. return _errMsg(e);
  1677. }
  1678. }
  1679. Future<String?> cancelAllOrders() async {
  1680. final ids = state.openOrders
  1681. .map((o) => int.tryParse(o.id))
  1682. .whereType<int>()
  1683. .toList();
  1684. if (ids.isEmpty) return 'errNoOrdersToCancel';
  1685. try {
  1686. await _service.cancelOrdersByIds(ids);
  1687. await _loadCurrentOrders();
  1688. return null;
  1689. } catch (e) {
  1690. await _loadCurrentOrders();
  1691. return _errMsg(e);
  1692. }
  1693. }
  1694. Future<String?> setPositionTpsl(
  1695. FuturesPosition position, {
  1696. double? profitPrice,
  1697. double? lossPrice,
  1698. }) async {
  1699. try {
  1700. if (position.cutEntrustId != null) {
  1701. // 已有止盈止损委托单 → 修改
  1702. await _service.modifyCutOrder(
  1703. entrustId: position.cutEntrustId!,
  1704. profitPrice: profitPrice,
  1705. lossPrice: lossPrice,
  1706. );
  1707. } else {
  1708. // 首次设置,成功后刷新持仓以获取 cutEntrustId
  1709. await _service.setTpsl(
  1710. positionId: position.id,
  1711. profitPrice: profitPrice,
  1712. lossPrice: lossPrice,
  1713. );
  1714. await _loadWallet(state.symbol);
  1715. }
  1716. return null;
  1717. } catch (e) {
  1718. return _errMsg(e);
  1719. }
  1720. }
  1721. Future<String?> reversePosition(FuturesPosition position) async {
  1722. try {
  1723. await _service.reverseOpenPosition(position.id);
  1724. await Future.wait([_loadWallet(state.symbol), _loadCurrentOrders()]);
  1725. return null;
  1726. } catch (e) {
  1727. return _errMsg(e);
  1728. }
  1729. }
  1730. Future<void> refresh() async {
  1731. // 下拉刷新只更新数据,不触发 isLoading(骨架屏仅用于首屏)
  1732. await Future.wait([
  1733. _loadWallet(state.symbol),
  1734. _loadCurrentOrders(),
  1735. ]);
  1736. }
  1737. FuturesService get _service =>
  1738. FuturesService(ref.read(dioClientProvider));
  1739. }
  1740. final futuresProvider =
  1741. AutoDisposeNotifierProviderFamily<FuturesNotifier, FuturesState, String>(
  1742. FuturesNotifier.new,
  1743. );
  1744. final futuresAmountUnitPrefProvider =
  1745. StateProvider<AmountUnit>((ref) => AmountUnit.btc);
  1746. final futuresHideOtherPrefProvider = StateProvider<bool>((ref) => false);
  1747. final futuresActiveSymbolProvider = StateProvider<String>((ref) => '');
  1748. /// 当前底部导航栏选中的 tab 索引(0=首页, 1=行情, 2=交易, 3=合约, 4=跟单, 5=资产)
  1749. final activeBottomTabProvider = StateProvider<int>((ref) => 0);
  1750. /// 上次访问的交易路径(现货 /spot/:sym 或合约 /futures/:sym),用于恢复交易 tab
  1751. final lastTradingRouteProvider = StateProvider<String>((ref) => '/spot/BTCUSDT');
  1752. double _toDouble(dynamic v) {
  1753. if (v == null) return 0.0;
  1754. if (v is num) return v.toDouble();
  1755. return double.tryParse(v.toString()) ?? 0.0;
  1756. }
  1757. /// 预计强平价:直接使用后端返回值,<=0 或 -1 均视为无效(显示 '--')
  1758. double _calcLiquidationPrice({required double apiValue}) {
  1759. return apiValue > 0 ? apiValue : 0.0;
  1760. }