rwadurian/frontend/mining-app/lib/presentation/providers/trading_providers.dart

487 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/price_info.dart';
import '../../domain/entities/market_overview.dart';
import '../../domain/entities/trading_account.dart';
import '../../domain/entities/trade_order.dart';
import '../../domain/entities/kline.dart';
import '../../domain/repositories/trading_repository.dart';
import '../../data/models/trade_order_model.dart';
import '../../core/di/injection.dart';
// Repository Provider
final tradingRepositoryProvider = Provider<TradingRepository>((ref) {
return getIt<TradingRepository>();
});
// 买入功能开关 Provider (2分钟缓存)
final buyEnabledProvider = FutureProvider<bool>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getBuyEnabled();
ref.keepAlive();
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => false, // 获取失败时默认关闭
(enabled) => enabled,
);
});
// K线周期选择
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
// 周期字符串映射到API参数
String _periodToApiParam(String period) {
const periodMap = {
'1分': '1m',
'5分': '5m',
'15分': '15m',
'30分': '30m',
'1时': '1h',
'4时': '4h',
'': '1d',
};
return periodMap[period] ?? '1h';
}
/// K线数据状态
class KlinesState {
final List<Kline> klines;
final bool isLoading;
final bool isLoadingMore;
final bool hasMoreHistory;
final String? error;
final String period;
const KlinesState({
this.klines = const [],
this.isLoading = false,
this.isLoadingMore = false,
this.hasMoreHistory = true,
this.error,
this.period = '1h',
});
KlinesState copyWith({
List<Kline>? klines,
bool? isLoading,
bool? isLoadingMore,
bool? hasMoreHistory,
String? error,
String? period,
}) {
return KlinesState(
klines: klines ?? this.klines,
isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
hasMoreHistory: hasMoreHistory ?? this.hasMoreHistory,
error: error,
period: period ?? this.period,
);
}
}
/// K线数据管理器 - 支持累积数据和加载更多历史
class KlinesNotifier extends StateNotifier<KlinesState> {
final TradingRepository repository;
Timer? _refreshTimer;
KlinesNotifier({required this.repository}) : super(const KlinesState());
/// 初始加载K线数据
Future<void> loadKlines(String period) async {
final apiPeriod = _periodToApiParam(period);
// 如果周期变化,重置数据
if (state.period != apiPeriod) {
state = KlinesState(isLoading: true, period: apiPeriod);
} else {
state = state.copyWith(isLoading: true, error: null);
}
final result = await repository.getKlines(period: apiPeriod, limit: 100);
result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
},
(klines) {
state = state.copyWith(
klines: klines,
isLoading: false,
hasMoreHistory: true, // 默认还有更多历史,直到加载返回空数据
);
_startAutoRefresh(apiPeriod);
},
);
}
/// 加载更多历史数据(向左滑动到边界时触发)
Future<void> loadMoreHistory() async {
if (state.isLoadingMore || !state.hasMoreHistory || state.klines.isEmpty) {
return;
}
state = state.copyWith(isLoadingMore: true);
// 获取当前最早K线的时间作为before参数
final oldestKline = state.klines.first;
final beforeTime = oldestKline.time;
final result = await repository.getKlines(
period: state.period,
limit: 100,
before: beforeTime,
);
result.fold(
(failure) {
state = state.copyWith(isLoadingMore: false, error: failure.message);
},
(newKlines) {
if (newKlines.isEmpty) {
// 没有更多历史数据了
state = state.copyWith(isLoadingMore: false, hasMoreHistory: false);
} else {
// 将新数据插入到现有数据前面(按时间顺序)
// 需要去重,因为可能有重叠
final existingTimes = state.klines.map((k) => k.time.millisecondsSinceEpoch).toSet();
final uniqueNewKlines = newKlines.where(
(k) => !existingTimes.contains(k.time.millisecondsSinceEpoch)
).toList();
// 如果去重后没有新数据,说明已经没有更多历史了
if (uniqueNewKlines.isEmpty) {
state = state.copyWith(isLoadingMore: false, hasMoreHistory: false);
return;
}
final combinedKlines = [...uniqueNewKlines, ...state.klines];
// 按时间排序
combinedKlines.sort((a, b) => a.time.compareTo(b.time));
// 判断是否还有更多历史返回数据少于100条说明到底了
state = state.copyWith(
klines: combinedKlines,
isLoadingMore: false,
hasMoreHistory: newKlines.length >= 100,
);
}
},
);
}
/// 刷新最新数据(不清除历史)
Future<void> refreshLatest() async {
if (state.klines.isEmpty) {
return loadKlines(state.period);
}
final result = await repository.getKlines(period: state.period, limit: 100);
result.fold(
(failure) {
// 刷新失败不影响现有数据
},
(newKlines) {
if (newKlines.isEmpty) return;
// 合并新数据到现有数据末尾
final existingTimes = state.klines.map((k) => k.time.millisecondsSinceEpoch).toSet();
final uniqueNewKlines = newKlines.where(
(k) => !existingTimes.contains(k.time.millisecondsSinceEpoch)
).toList();
if (uniqueNewKlines.isNotEmpty) {
final combinedKlines = [...state.klines, ...uniqueNewKlines];
combinedKlines.sort((a, b) => a.time.compareTo(b.time));
state = state.copyWith(klines: combinedKlines);
}
// 更新最新K线的收盘价实时更新
if (newKlines.isNotEmpty && state.klines.isNotEmpty) {
final latestNew = newKlines.last;
final latestExisting = state.klines.last;
if (latestNew.time.millisecondsSinceEpoch == latestExisting.time.millisecondsSinceEpoch) {
// 更新最新K线
final updatedKlines = [...state.klines];
updatedKlines[updatedKlines.length - 1] = latestNew;
state = state.copyWith(klines: updatedKlines);
}
}
},
);
}
void _startAutoRefresh(String period) {
_refreshTimer?.cancel();
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (_) {
refreshLatest();
});
}
@override
void dispose() {
_refreshTimer?.cancel();
super.dispose();
}
}
/// K线数据 Provider支持累积和加载更多
final klinesNotifierProvider = StateNotifierProvider<KlinesNotifier, KlinesState>((ref) {
final repository = ref.watch(tradingRepositoryProvider);
return KlinesNotifier(repository: repository);
});
// 保留原有的简单 Provider 以保持向后兼容
final klinesProvider = FutureProvider<List<Kline>>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final selectedPeriod = ref.watch(selectedKlinePeriodProvider);
final apiPeriod = _periodToApiParam(selectedPeriod);
final result = await repository.getKlines(period: apiPeriod, limit: 100);
ref.keepAlive();
final timer = Timer(const Duration(minutes: 1), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(klines) => klines,
);
});
// 当前价格 Provider (15秒缓存 - 交易页面需要快速更新)
final currentPriceProvider = FutureProvider<PriceInfo?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getCurrentPrice();
ref.keepAlive();
final timer = Timer(const Duration(seconds: 15), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(priceInfo) => priceInfo,
);
});
// 市场概览 Provider (30秒缓存 - 市场数据相对稳定)
final marketOverviewProvider = FutureProvider<MarketOverview?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getMarketOverview();
ref.keepAlive();
final timer = Timer(const Duration(seconds: 30), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(overview) => overview,
);
});
// 交易账户 Provider (15秒缓存 - 交易后快速更新资产)
final tradingAccountProvider = FutureProvider.family<TradingAccount?, String>(
(ref, accountSequence) async {
if (accountSequence.isEmpty) return null;
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getTradingAccount(accountSequence);
ref.keepAlive();
final timer = Timer(const Duration(seconds: 15), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(account) => account,
);
},
);
// 订单列表 Provider (10秒缓存 - 交易后快速看到结果)
final ordersProvider = FutureProvider<OrdersPageModel?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getOrders(page: 1, pageSize: 10);
ref.keepAlive();
final timer = Timer(const Duration(seconds: 10), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(orders) => orders,
);
});
// 成交记录列表 Provider含手续费明细
final tradesProvider = FutureProvider<TradesPageModel?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getTrades(page: 1, pageSize: 50);
ref.keepAlive();
final timer = Timer(const Duration(seconds: 30), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(trades) => trades,
);
});
// 交易状态
class TradingState {
final bool isLoading;
final String? error;
final Map<String, dynamic>? lastOrderResult;
TradingState({
this.isLoading = false,
this.error,
this.lastOrderResult,
});
TradingState copyWith({
bool? isLoading,
String? error,
Map<String, dynamic>? lastOrderResult,
}) {
return TradingState(
isLoading: isLoading ?? this.isLoading,
error: error,
lastOrderResult: lastOrderResult ?? this.lastOrderResult,
);
}
}
class TradingNotifier extends StateNotifier<TradingState> {
final TradingRepository repository;
TradingNotifier({required this.repository}) : super(TradingState());
/// 创建买入订单
Future<bool> buyShares(String price, String quantity) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.createOrder(
type: 'BUY',
price: price,
quantity: quantity,
);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(orderResult) {
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
return true;
},
);
}
/// 创建卖出订单
Future<bool> sellShares(String price, String quantity) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.createOrder(
type: 'SELL',
price: price,
quantity: quantity,
);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(orderResult) {
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
return true;
},
);
}
/// 取消订单
Future<bool> cancelOrder(String orderNo) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.cancelOrder(orderNo);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
}
/// 划入积分股
Future<bool> transferIn(String amount) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.transferIn(amount);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
}
/// 划出积分股
Future<bool> transferOut(String amount) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.transferOut(amount);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
}
void clearError() {
state = state.copyWith(error: null);
}
}
final tradingNotifierProvider = StateNotifierProvider<TradingNotifier, TradingState>(
(ref) => TradingNotifier(
repository: ref.watch(tradingRepositoryProvider),
),
);