487 lines
14 KiB
Dart
487 lines
14 KiB
Dart
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),
|
||
),
|
||
);
|