Skip to content

Commit

Permalink
Merge pull request #172 from lyskouski/RF-146
Browse files Browse the repository at this point in the history
[#146] [RF] Improve Forecast Chart. Add Monte Carlo prediction
  • Loading branch information
lyskouski authored Aug 20, 2023
2 parents f1272f1 + 210b63a commit 2181d86
Show file tree
Hide file tree
Showing 23 changed files with 161 additions and 77 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
- Simple grouping via `/` (in name) for the main page
- Bills, Transfers, Incomes
- Exchange rates, Default Currency for Summary
- Metrics: Budget Forecast
- Metrics: Budget Forecast (with Monte Carlo simulation)
- Goals Definition
- Recovery via WebDav
- Import from `.csv`-files
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/math/abstract_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/structure/currency/exchange.dart';

Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/math/account_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/math/abstract_recalculation.dart';
import 'package:app_finance/_classes/structure/account_app_data.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/math/bill_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/math/abstract_recalculation.dart';
import 'package:app_finance/_classes/structure/account_app_data.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/math/budget_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/math/abstract_recalculation.dart';
import 'package:app_finance/_classes/structure/budget_app_data.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/math/goal_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/math/abstract_recalculation.dart';
import 'package:app_finance/_classes/structure/goal_app_data.dart';
Expand Down
51 changes: 51 additions & 0 deletions lib/_classes/math/monte_carlo_simulation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:math';

import 'package:flutter/material.dart';

class MonteCarloSimulation {
final Random random = Random();
final int cycles;
final double coercion;

MonteCarloSimulation({this.cycles = 30, this.coercion = 1});

List<Offset> generate(List<Offset> scope, double step, double max) {
final List<List<double>> distribution = [];
for (int i = 0; i < scope.length; i++) {
final state = mcNormal(scope[i].dy, coercion, cycles);
for (int j = 0; j < state.length; j++) {
if (j >= distribution.length) {
distribution.add([]);
}
distribution[j].add(state[j]);
}
}
double posX = scope.last.dx + step;
List<Offset> result = [];
int idx = 0;
while (posX <= max) {
result.add(Offset(posX, distribution[idx][distribution[idx].length * random.nextDouble() ~/ 1]));
posX += step;
idx++;
}
return result;
}

List<double> mcNormal(double mean, double stdDev, int samples) {
List<double> results = [];
for (int i = 0; i < samples; i++) {
results.add(_normalRandom(mean, stdDev));
}
return results;
}

double _normalRandom(double mean, double stdDev) {
double u1 = random.nextDouble();
double u2 = random.nextDouble();
double z0 = sqrt(-2.0 * log(u1)) * cos(2 * pi * u2);
return mean + stdDev * z0;
}
}
3 changes: 1 addition & 2 deletions lib/_classes/math/total_recalculation.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:collection';

Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/storage/transaction_log.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:io';
import 'dart:convert';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/structure/abstract_app_data.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:convert';
import 'package:app_finance/_classes/storage/transaction_log.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/structure/currency/currency_provider.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:collection';
import 'package:app_finance/_classes/structure/currency/crypto_list.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_classes/structure/currency/exchange.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/structure/currency/currency_provider.dart';
import 'package:app_finance/_classes/structure/currency_app_data.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/_configs/custom_text_theme.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down
14 changes: 11 additions & 3 deletions lib/charts/forecast_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@ import 'package:app_finance/charts/painter/forecast_chart_painter.dart';
import 'package:app_finance/charts/painter/foreground_chart_painter.dart';
import 'package:flutter/material.dart';

class ForecastData {
final List<Offset> data;
final Color color;

ForecastData(this.data, {this.color = Colors.red});
}

class ForecastChart extends StatefulWidget {
final double width;
final double height;
final double indent;
final String tooltip;
final List<Offset> data;
final List<ForecastData> data;
final double yMax;

const ForecastChart({
super.key,
required this.data,
required this.yMax,
required this.width,
required this.height,
this.indent = 0.0,
this.tooltip = '',
});
Expand All @@ -29,7 +38,7 @@ class ForecastChartState extends State<ForecastChart> {
@override
Widget build(BuildContext context) {
final now = DateTime.now();
final size = Size(widget.width, widget.width / 1.9);
final size = Size(widget.width, widget.height);
final bgColor = Theme.of(context).colorScheme.onBackground;
final xMin = DateTime(now.year, now.month);
final xMax = DateTime(now.year, now.month + 1);
Expand All @@ -51,7 +60,6 @@ class ForecastChartState extends State<ForecastChart> {
child: CustomPaint(
size: size,
painter: ForecastChartPainter(
color: Colors.red,
indent: bg.shift,
size: size,
data: widget.data,
Expand Down
93 changes: 58 additions & 35 deletions lib/charts/painter/forecast_chart_painter.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/math/monte_carlo_simulation.dart';
import 'package:app_finance/charts/forecast_chart.dart';
import 'package:flutter/material.dart';

class ForecastChartPainter extends CustomPainter {
final double indent;
final Color color;
final Size? size;
final List<Offset> data;
final List<ForecastData> data;
final double xMax;
final double xMin;
final double yMax;

ForecastChartPainter({
required this.indent,
required this.color,
required this.data,
this.size,
this.xMax = 1.0,
Expand All @@ -27,65 +27,88 @@ class ForecastChartPainter extends CustomPainter {
return false;
}

List<dynamic> _bind(Offset data, Size size, double total) {
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) {
return;
}
size = this.size ?? size;
double usDay = 86400000000;
for (final scope in data) {
final total = _paint(canvas, scope.data, size, scope.color);
final dx = scope.data.last.dx;
if (scope.data.length > 2 && dx < xMax) {
final cycles = (xMax - dx) ~/ usDay;
final forecast = [scope.data.last];
forecast.addAll(MonteCarloSimulation(cycles: cycles).generate(scope.data, usDay, xMax - 2 * usDay));
_paint(canvas, forecast, size, scope.color.withBlue(200).withOpacity(0.4), total);
}
}
}

List<dynamic> _bind(Offset point, Size size, double total) {
return [
total + data.dy,
_getValue(data, size, total),
total + point.dy,
_getValue(point, size, total),
];
}

Offset _getValue(Offset data, Size size, [double dy = 0]) {
Offset _getValue(Offset point, Size size, [double dy = 0]) {
return Offset(
(data.dx - xMin) / (xMax - xMin) * size.width + indent,
(1 - (data.dy + dy) / yMax) * size.height - indent,
(point.dx - xMin) / (xMax - xMin) * size.width + indent,
(1 - (point.dy + dy) / yMax) * size.height - indent,
);
}

double _sumY(List<Offset> data) {
return data.fold(0.0, (v, e) => v + e.dy);
double _sumY(List<Offset> scope) {
return scope.fold(0.0, (v, e) => v + e.dy);
}

Offset _getMedian(List<Offset> data) {
Offset _getMedian(List<Offset> scope) {
return Offset(
(data.last.dx + data.first.dx) / 2,
_sumY(data.sublist(0, data.length ~/ 2)),
(scope.last.dx + scope.first.dx) / 2,
_sumY(scope.sublist(0, scope.length ~/ 2)),
);
}

@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) {
return;
}
size = this.size ?? size;
double total = 0.0;
double _paint(Canvas canvas, List<Offset> scope, Size size, Color color, [double total = 0.0]) {
Offset startPoint;
[_, startPoint] = _bind(data.first, size, total);
[_, startPoint] = _bind(scope.first, size, total);
int i = 0;
Offset point;
[i, point] = _paintDots(canvas, scope, size, color, total);
int third = i ~/ 3;
Offset startBezier = _getValue(_getMedian(scope.sublist(0, third)), size, total);
total += _sumY(scope.sublist(0, third));
Offset middleBezier = _getValue(_getMedian(scope.sublist(third, 2 * third)), size, total);
total += _sumY(scope.sublist(third, 2 * third));
Offset endBezier = _getValue(_getMedian(scope.sublist(2 * third, i)), size, total);
_paintCurve(canvas, startPoint, startBezier, middleBezier, color);
_paintCurve(canvas, middleBezier, endBezier, point, color);
return total + point.dy;
}

List<dynamic> _paintDots(Canvas canvas, List<Offset> scope, Size size, Color color, double total) {
Offset point = const Offset(0, 0);
int i = 0;
for (i; i < data.length; i++) {
[total, point] = _bind(data[i], size, total);
_paintDot(canvas, point);
for (i; i < scope.length; i++) {
[total, point] = _bind(scope[i], size, total);
if (point.dy < 0) {
point = Offset(point.dx, 0);
_paintDot(canvas, point, color);
break;
}
_paintDot(canvas, point, color);
}
int third = i ~/ 3;
Offset startBezier = _getValue(_getMedian(data.sublist(0, third)), size);
total = _sumY(data.sublist(0, third));
Offset middleBezier = _getValue(_getMedian(data.sublist(third, 2 * third)), size, total);
total += _sumY(data.sublist(third, 2 * third));
Offset endBezier = _getValue(_getMedian(data.sublist(2 * third, i)), size, total);
_paintCurve(canvas, startPoint, startBezier, middleBezier);
_paintCurve(canvas, middleBezier, endBezier, point);
return [i - 1, point];
}

void _paintDot(Canvas canvas, Offset point) {
void _paintDot(Canvas canvas, Offset point, Color color) {
final dot = Paint()..color = color;
canvas.drawCircle(point, 2.2, dot);
}

_paintCurve(Canvas canvas, Offset startPoint, Offset controlPoint, Offset endPoint) {
_paintCurve(Canvas canvas, Offset startPoint, Offset controlPoint, Offset endPoint, Color color) {
final line = Paint()
..color = color
..style = PaintingStyle.stroke
Expand Down
4 changes: 2 additions & 2 deletions lib/charts/painter/foreground_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class ForegroundChartPainter extends CustomPainter {
}) {
_setTextArea();
shift = textArea * 1.2;
yDiv = 12 * size!.width ~/ 640;
xDiv = 12 * size!.height ~/ 240;
yDiv = 12 * size!.height ~/ 400;
xDiv = 12 * size!.width ~/ 640;
}

void _setTextArea() {
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"accountType": "Account Type",
"acknowledgeTooltip": "Acknowledge",
"activate": "Activate",
"actualData": "Historical Data",
"addAccountTooltip": "Add Account",
"addBudgetTooltip": "Add new Budget Category",
"addGoalTooltip": "Add new Goal",
Expand Down Expand Up @@ -118,6 +119,7 @@
"expenseDateTime": "Billed At",
"expenseHeadline": "Expense",
"expenseTransfer": "Amount of Transfer",
"forecastData": "Forecast",
"from": "from",
"goNextTooltip": "Go Next",
"goalHeadline": "Goals",
Expand Down
3 changes: 1 addition & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/herald/app_locale.dart';
import 'package:app_finance/_classes/herald/app_theme.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/routes/start_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/storage/app_data.dart';
import 'package:app_finance/_classes/herald/app_locale.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/widgets/_forms/currency_exchange_input.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/herald/app_locale.dart';
import 'package:app_finance/_classes/structure/currency/currency_provider.dart';
Expand Down
3 changes: 1 addition & 2 deletions lib/widgets/_forms/list_account_selector.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be
// found in the LICENSE file.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/storage/app_data.dart';
import 'package:app_finance/widgets/_forms/list_selector.dart';
Expand Down
Loading

0 comments on commit 2181d86

Please sign in to comment.