2026-04-07 16:43:48 +08:00
|
|
|
import 'package:flutter/foundation.dart';
|
2026-04-07 22:35:39 +08:00
|
|
|
import 'package:flutter_chen_kchart/k_chart.dart';
|
2026-04-07 16:43:48 +08:00
|
|
|
import '../models/candle.dart';
|
|
|
|
|
|
|
|
|
|
/// 时间周期
|
|
|
|
|
enum ChartInterval {
|
|
|
|
|
min1('1分', '1m'),
|
|
|
|
|
min5('5分', '5m'),
|
|
|
|
|
min15('15分', '15m'),
|
|
|
|
|
hour1('1小时', '1h'),
|
|
|
|
|
hour4('4小时', '4h'),
|
|
|
|
|
day1('1天', '1d');
|
|
|
|
|
|
|
|
|
|
final String label;
|
|
|
|
|
final String value;
|
|
|
|
|
const ChartInterval(this.label, this.value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 21:27:23 +08:00
|
|
|
/// 技术指标设置
|
2026-04-07 16:43:48 +08:00
|
|
|
class IndicatorSettings {
|
|
|
|
|
final bool showMA;
|
|
|
|
|
final bool showEMA;
|
|
|
|
|
final bool showBOLL;
|
|
|
|
|
final bool showVOL;
|
2026-04-07 17:07:25 +08:00
|
|
|
final bool showMACD;
|
2026-04-07 21:27:23 +08:00
|
|
|
final bool showKDJ;
|
|
|
|
|
final bool showRSI;
|
2026-04-07 22:35:39 +08:00
|
|
|
final bool showWR;
|
|
|
|
|
final bool showCCI;
|
2026-04-07 16:43:48 +08:00
|
|
|
|
|
|
|
|
const IndicatorSettings({
|
|
|
|
|
this.showMA = true,
|
|
|
|
|
this.showEMA = false,
|
|
|
|
|
this.showBOLL = false,
|
|
|
|
|
this.showVOL = true,
|
2026-04-07 21:27:23 +08:00
|
|
|
this.showMACD = true,
|
|
|
|
|
this.showKDJ = false,
|
|
|
|
|
this.showRSI = false,
|
2026-04-07 22:35:39 +08:00
|
|
|
this.showWR = false,
|
|
|
|
|
this.showCCI = false,
|
2026-04-07 16:43:48 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
IndicatorSettings copyWith({
|
|
|
|
|
bool? showMA,
|
|
|
|
|
bool? showEMA,
|
|
|
|
|
bool? showBOLL,
|
|
|
|
|
bool? showVOL,
|
2026-04-07 17:07:25 +08:00
|
|
|
bool? showMACD,
|
2026-04-07 21:27:23 +08:00
|
|
|
bool? showKDJ,
|
|
|
|
|
bool? showRSI,
|
2026-04-07 22:35:39 +08:00
|
|
|
bool? showWR,
|
|
|
|
|
bool? showCCI,
|
2026-04-07 16:43:48 +08:00
|
|
|
}) {
|
|
|
|
|
return IndicatorSettings(
|
|
|
|
|
showMA: showMA ?? this.showMA,
|
|
|
|
|
showEMA: showEMA ?? this.showEMA,
|
|
|
|
|
showBOLL: showBOLL ?? this.showBOLL,
|
|
|
|
|
showVOL: showVOL ?? this.showVOL,
|
2026-04-07 17:07:25 +08:00
|
|
|
showMACD: showMACD ?? this.showMACD,
|
2026-04-07 21:27:23 +08:00
|
|
|
showKDJ: showKDJ ?? this.showKDJ,
|
|
|
|
|
showRSI: showRSI ?? this.showRSI,
|
2026-04-07 22:35:39 +08:00
|
|
|
showWR: showWR ?? this.showWR,
|
|
|
|
|
showCCI: showCCI ?? this.showCCI,
|
2026-04-07 16:43:48 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// K 线数据 Provider
|
|
|
|
|
class ChartProvider extends ChangeNotifier {
|
|
|
|
|
// 当前币种
|
|
|
|
|
String _symbol = 'BTC';
|
|
|
|
|
String get symbol => _symbol;
|
|
|
|
|
|
|
|
|
|
// 时间周期
|
|
|
|
|
ChartInterval _interval = ChartInterval.hour1;
|
|
|
|
|
ChartInterval get interval => _interval;
|
|
|
|
|
|
|
|
|
|
// 技术指标
|
|
|
|
|
IndicatorSettings _indicators = const IndicatorSettings();
|
|
|
|
|
IndicatorSettings get indicators => _indicators;
|
|
|
|
|
|
2026-04-07 21:27:23 +08:00
|
|
|
// K 线数据(原始)
|
2026-04-07 16:43:48 +08:00
|
|
|
List<Candle> _candles = [];
|
|
|
|
|
List<Candle> get candles => _candles;
|
|
|
|
|
|
2026-04-07 21:27:23 +08:00
|
|
|
// k_chart 需要的数据格式
|
|
|
|
|
List<KLineEntity> get klineData {
|
|
|
|
|
return _candles.map((c) {
|
|
|
|
|
return KLineEntity.fromCustom(
|
|
|
|
|
open: c.open,
|
|
|
|
|
high: c.high,
|
|
|
|
|
low: c.low,
|
|
|
|
|
close: c.close,
|
|
|
|
|
vol: c.volume,
|
|
|
|
|
time: c.timestamp,
|
|
|
|
|
);
|
|
|
|
|
}).toList();
|
2026-04-07 16:57:33 +08:00
|
|
|
}
|
2026-04-07 16:43:48 +08:00
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
bool _loading = false;
|
|
|
|
|
bool get loading => _loading;
|
|
|
|
|
|
|
|
|
|
String? _error;
|
|
|
|
|
String? get error => _error;
|
|
|
|
|
|
|
|
|
|
/// 切换币种
|
|
|
|
|
void setSymbol(String symbol) {
|
|
|
|
|
if (_symbol != symbol) {
|
|
|
|
|
_symbol = symbol;
|
2026-04-07 21:27:23 +08:00
|
|
|
loadCandles();
|
2026-04-07 16:43:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 切换时间周期
|
|
|
|
|
void setInterval(ChartInterval interval) {
|
|
|
|
|
if (_interval != interval) {
|
|
|
|
|
_interval = interval;
|
2026-04-07 21:27:23 +08:00
|
|
|
loadCandles(showLoading: false);
|
2026-04-07 16:43:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 更新指标设置
|
|
|
|
|
void updateIndicators(IndicatorSettings settings) {
|
|
|
|
|
_indicators = settings;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 加载 K 线数据
|
2026-04-07 17:00:45 +08:00
|
|
|
Future<void> loadCandles({bool showLoading = true}) async {
|
|
|
|
|
if (showLoading) {
|
|
|
|
|
_loading = true;
|
|
|
|
|
_error = null;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
2026-04-07 16:43:48 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// TODO: 对接真实 API
|
|
|
|
|
// 临时使用模拟数据
|
2026-04-07 17:00:45 +08:00
|
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
2026-04-07 16:43:48 +08:00
|
|
|
_candles = _generateMockCandles();
|
|
|
|
|
|
2026-04-07 17:00:45 +08:00
|
|
|
if (showLoading) {
|
|
|
|
|
_loading = false;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
} else {
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
2026-04-07 16:43:48 +08:00
|
|
|
} catch (e) {
|
2026-04-07 17:00:45 +08:00
|
|
|
if (showLoading) {
|
|
|
|
|
_loading = false;
|
|
|
|
|
}
|
2026-04-07 16:43:48 +08:00
|
|
|
_error = e.toString();
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 生成模拟 K 线数据
|
|
|
|
|
List<Candle> _generateMockCandles() {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final basePrice = _getBasePrice(_symbol);
|
|
|
|
|
final candles = <Candle>[];
|
|
|
|
|
|
|
|
|
|
for (int i = 100; i >= 0; i--) {
|
|
|
|
|
final time = now.subtract(Duration(
|
|
|
|
|
minutes: _interval == ChartInterval.min1
|
|
|
|
|
? i
|
|
|
|
|
: _interval == ChartInterval.min5
|
|
|
|
|
? i * 5
|
|
|
|
|
: _interval == ChartInterval.min15
|
|
|
|
|
? i * 15
|
|
|
|
|
: _interval == ChartInterval.hour1
|
|
|
|
|
? i * 60
|
|
|
|
|
: _interval == ChartInterval.hour4
|
|
|
|
|
? i * 240
|
|
|
|
|
: i * 1440,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
final random = (i * 17) % 100 / 100;
|
|
|
|
|
final change = basePrice * 0.02 * (random - 0.5);
|
|
|
|
|
final open = basePrice + change;
|
|
|
|
|
final close = open + basePrice * 0.01 * ((i * 13) % 100 / 100 - 0.5);
|
|
|
|
|
final high = open > close ? open * 1.005 : close * 1.005;
|
|
|
|
|
final low = open < close ? open * 0.995 : close * 0.995;
|
|
|
|
|
final volume = 1000 + (i * 37) % 5000;
|
|
|
|
|
|
|
|
|
|
candles.add(Candle(
|
|
|
|
|
timestamp: time.millisecondsSinceEpoch,
|
|
|
|
|
open: open,
|
|
|
|
|
high: high,
|
|
|
|
|
low: low,
|
|
|
|
|
close: close,
|
|
|
|
|
volume: volume.toDouble(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return candles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double _getBasePrice(String symbol) {
|
|
|
|
|
switch (symbol.toUpperCase()) {
|
|
|
|
|
case 'BTC':
|
|
|
|
|
return 42000.0;
|
|
|
|
|
case 'ETH':
|
|
|
|
|
return 2200.0;
|
|
|
|
|
case 'BNB':
|
|
|
|
|
return 300.0;
|
|
|
|
|
case 'SOL':
|
|
|
|
|
return 100.0;
|
|
|
|
|
case 'XRP':
|
|
|
|
|
return 0.5;
|
|
|
|
|
case 'DOGE':
|
|
|
|
|
return 0.08;
|
|
|
|
|
default:
|
|
|
|
|
return 100.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|