Sisyphus is a Flutter-based crypto app that displays real-time candlestick charts using Binance WebSockets APIs. The app provides a seamless experience for traders and crypto enthusiasts to monitor market trends efficiently.
- 📊 Real-time candlestick charts.
- 🔄 WebSocket integration for live updates.
- 📈 Supports multiple cryptocurrencies.
- 🌓 Light and Dark theme modes.
- 📱 Responsive UI for both mobile and tablet.
- Flutter: For building cross-platform mobile apps.
- WebSockets: For live financial data.
- Riverpod: For state management.
- Candlesticks: Custom implementation using charts using the 'candlesticks' package
-
Clone the repository:
git clone https://github.com/Raks-Javac/Sisyphus.git cd Sisyphus
-
Install dependencies:
flutter pub get
-
Run the app:
flutter run
-
Ensure you are on VPN or a solid network to allow connection to binance websocket(Optional)
The project methodology adopted is a project based method, while feature based isolates feature, the reason project based was chosen for this was because it was just to demonstrates binance websockets with flutter, while "Keeping It Simple Solid"
Sisyphus/
├── lib/
│ ├── main.dart # Entry point
│ ├── models/ # Data models
│ ├── services/ # WebSocket and API handling
│ ├── widgets/ # UI components
│ ├── screens/ # Screens for different sections
├── assets/ # containing svgs, images and fonts
├── pubspec.yaml
For assets, textstyle and color configurations, you can check the resources folder /res, where "ThemeExtensions" was set up for dynamic theming.
The app uses WebSocket APIs to fetch live market data. Below is a sample implementation:
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:sisyphus/models/candlestick.dart';
import 'package:sisyphus/models/orderbook.dart';
import 'package:sisyphus/services/websocket_service.dart';
class BinanceApiService {
final WebSocketService _webSocketService;
final String _baseUrl = 'https://api.binance.com/api/v3';
final String _wsBaseUrl = 'wss://stream.binance.com:9443/ws';
BinanceApiService(this._webSocketService);
Future<void> connectToWebSocket() async {
await _webSocketService.connect(_wsBaseUrl);
}
void subscribeToCandlesticks(String symbol, String interval,
Function(List<SisyphusCandlestick>) onData) {
final streamName = '${symbol.toLowerCase()}@kline_$interval';
_webSocketService.subscribe(streamName, (data) {
if (data != null && data.containsKey('k')) {
final kline = data['k'];
final candlestick = SisyphusCandlestick.fromJson(kline);
onData([candlestick]);
}
});
_webSocketService.send(jsonEncode({
'method': 'SUBSCRIBE',
'params': [streamName],
'id': 1
}));
}
void subscribeToOrderbook(String symbol, Function(Orderbook) onData) {
final streamName = '${symbol.toLowerCase()}@depth20@100ms';
_webSocketService.subscribe(streamName, (data) {
if (data != null) {
final orderbook = Orderbook.fromJson(data);
onData(orderbook);
}
});
_webSocketService.send(jsonEncode({
'method': 'SUBSCRIBE',
'params': [streamName],
'id': 2
}));
}
void unsubscribeFromCandlesticks(String symbol, String interval) {
final streamName = '${symbol.toLowerCase()}@kline_$interval';
_webSocketService.send(jsonEncode({
'method': 'UNSUBSCRIBE',
'params': [streamName],
'id': 3
}));
}
void unsubscribeFromOrderbook(String symbol) {
final streamName = '${symbol.toLowerCase()}@depth20@100ms';
_webSocketService.send(jsonEncode({
'method': 'UNSUBSCRIBE',
'params': [streamName],
'id': 4
}));
}
Future<List<SisyphusCandlestick>> getHistoricalCandlesticks(
String symbol, String interval,
{int limit = 500}) async {
final url =
'$_baseUrl/klines?symbol=$symbol&interval=$interval&limit=$limit';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((item) => SisyphusCandlestick.fromList(item)).toList();
} else {
throw Exception('Failed to load historical candlesticks');
}
}
Future<Orderbook> getOrderbook(String symbol, {int limit = 20}) async {
final url = '$_baseUrl/depth?symbol=$symbol&limit=$limit';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return Orderbook.fromJson(data);
} else {
throw Exception('Failed to load orderbook');
}
}
void dispose() {
_webSocketService.disconnect();
}
}
Custom candlestick charts were implemented using Flutter widgets. Key components include:
- OHLC (Open, High, Low, Close) data parsing.
- Dynamic scaling for different timeframes.
- Touch and zoom interactions.
import 'package:candlesticks/candlesticks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:sisyphus/models/candlestick.dart';
import 'package:sisyphus/res/assets.dart';
import 'package:sisyphus/res/gap.dart';
import 'package:sisyphus/res/theme/app_theme.dart';
import 'package:sisyphus/widgets/chart_widgets.dart';
import 'package:sisyphus/widgets/render_assets.dart';
class CryptoChart extends StatefulWidget {
final List<SisyphusCandlestick> candles;
final String symbol;
final Color upColor;
final Color downColor;
final Color backgroundColor;
final Color gridColor;
final Color textColor;
final bool showVolume;
final bool showHeader;
final bool showFooter;
final Function()? onLoadMoreCandles;
const CryptoChart({
super.key,
required this.candles,
this.symbol = 'BTC/USD',
this.upColor = const Color(0xFF25C26E),
this.downColor = const Color(0xFFFF6B4A),
this.backgroundColor = const Color(0xFF1E2029),
this.gridColor = const Color(0x33FFFFFF),
this.textColor = Colors.white,
this.showVolume = true,
this.showHeader = true,
this.showFooter = true,
this.onLoadMoreCandles,
});
@override
State<CryptoChart> createState() => _CryptoChartState();
}
class _CryptoChartState extends State<CryptoChart> {
late List<Candle> _convertedCandles;
@override
void initState() {
super.initState();
_updateCandles();
}
@override
void didUpdateWidget(CryptoChart oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.candles != oldWidget.candles) {
_updateCandles();
}
}
void _updateCandles() {
_convertedCandles = widget.candles
.map((c) => Candle(
date: DateTime.fromMillisecondsSinceEpoch(c.openTime),
high: c.high,
low: c.low,
open: c.open,
close: c.close,
volume: c.volume))
.toList();
}
@override
Widget build(BuildContext context) {
if (widget.candles.isEmpty) {
return Center(
child: Text(
'No data available',
style: TextStyle(color: widget.textColor),
),
);
}
return Container(
color: widget.backgroundColor,
child: Stack(
children: [
Column(
children: [
// Main chart area
Expanded(
child: Candlesticks(
candles: _convertedCandles,
),
),
// Footer with volume info
const SizedBox(height: 16),
Padding(
padding: EdgeInsetsDirectional.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Vol(BTC): ${widget.candles.last.volume.toStringAsFixed(4)}K',
style: context.textStyles.caption.copyWith(
color: context.colors.chartGreen,
),
),
Text(
'Vol(USDT): ${widget.candles.last.quoteAssetVolume.toStringAsFixed(4)}B',
style: context.textStyles.caption.copyWith(
color: context.colors.chartRed,
),
),
],
),
),
const SizedBox(height: 16),
],
),
Positioned(
top: 30,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
addHorizontalSpacing(15),
SWidgetRenderSvg(svgPath: borderedDropDown),
addHorizontalSpacing(5),
Row(
children: [
Text(
'BTC/USD',
style: context.textStyles.body2.copyWith(
fontSize: 10.sp,
),
),
addHorizontalSpacing(20),
PriceInfo(
label: 'O',
color: widget.candles.last.open > 0
? context.colors.positiveGreen
: context.colors.negativeRed,
value: widget.candles.last.open.toStringAsFixed(2),
),
addHorizontalSpacing(20),
PriceInfo(
label: 'H',
color: widget.candles.last.high > 0
? context.colors.positiveGreen
: context.colors.negativeRed,
value: widget.candles.last.high.toStringAsFixed(2),
),
addHorizontalSpacing(20),
PriceInfo(
label: 'L',
color: widget.candles.last.low > 0
? context.colors.positiveGreen
: context.colors.negativeRed,
value: widget.candles.last.low.toStringAsFixed(2),
),
addHorizontalSpacing(20),
PriceInfo(
label: 'C',
color: widget.candles.last.close > 0
? context.colors.positiveGreen
: context.colors.negativeRed,
value: widget.candles.last.close.toStringAsFixed(2),
),
],
)
],
),
),
),
],
),
);
}
}
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please open an issue or submit a pull request.
For support, reach out to Rufai Kudus Adeboye at rufaikudus2014@gmail.com.
⭐ Don't forget to star the repo if you find it useful! ⭐