Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dates): add basic dates logic #17

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions example/example.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'dart:math';
import 'package:finances/finance.dart';
import 'package:finances/src/base/return.dart';

void main() {
// Return
final yearReturn = Return(nreturn: 0.05, period: oneyear);
final yearReturn = Return(
nreturn: 0.05,
returnPeriod: ReturnPeriod(tradingPeriod: Duration(days: 252 * 3)));

// Five year return, annualized
print(yearReturn.annualize.nreturn);
Expand Down
1 change: 1 addition & 0 deletions lib/finance.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'src/base/return.dart' show Return;
export 'src/base/return_stream.dart' show ReturnStream, ReturnStreamType;
export 'src/base/return_period.dart' show ReturnPeriod;
1 change: 1 addition & 0 deletions lib/src/base/base.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'return.dart';
export 'return_stream.dart';
export 'return_period.dart';
16 changes: 16 additions & 0 deletions lib/src/base/calc_trading_period.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'dart:core';

Duration calcTradingPeriod(DateTime startDate, DateTime endDate) {
// for each full week, subtract 2.
// for the remainder, find # of weekend days

int days = endDate.difference(startDate).inDays;

int tradingWeekDays = days - 2 * ((days + startDate.weekday) ~/ 7);
final totalDays = tradingWeekDays +
(startDate.weekday == 7 ? 1 : 0) +
(endDate.weekday == 7 ? 1 : 0);

//adjust for starting and ending on a Sunday:
return Duration(days: totalDays);
}
10 changes: 10 additions & 0 deletions lib/src/base/finance_constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:finances/src/base/return_period.dart';

class FiConstants {
/// A constant of one trading day
static const oneTradingDay = ReturnPeriod(tradingPeriod: Duration(days: 1));

/// A constant of 252 days for the American trading year
static const oneTradingYear =
ReturnPeriod(tradingPeriod: Duration(days: 252));
}
32 changes: 24 additions & 8 deletions lib/src/base/return.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import 'dart:core';
import 'dart:math';

const oneday = Duration(days: 1);
const oneyear = Duration(days: 252);
import 'package:finances/src/base/finance_constants.dart';
import 'package:finances/src/base/return_period.dart';
import 'package:finances/src/base/calc_trading_period.dart';

/// A core class for working with returns.
class Return {
/// The numeric return, expressed as `0.05` for a 5\% return.
final double nreturn;

/// The period over which the return occured. Defaults to one day, if not specified.
final Duration period;
final ReturnPeriod returnPeriod;

/// default `false`: the calculation method of the return.
/// Logarithmic if true, else Arithmetic.
Expand All @@ -19,24 +20,37 @@ class Return {
Return({
required this.nreturn,
this.isLog = false,
this.period = oneday,
this.returnPeriod = FiConstants.oneTradingDay,
});

Return.fromDates(
{required this.nreturn,
this.isLog = false,
required DateTime startDate,
required DateTime endDate})
: returnPeriod =
ReturnPeriod(tradingPeriod: calcTradingPeriod(startDate, endDate));

/// Rescales the return up or down over a given time period.
///
/// Warning: scaling returns up can be misleading.
Return scale({required Duration newPeriod}) {
double periodRatio = newPeriod.inSeconds / period.inSeconds;

double periodRatio =
newPeriod.inSeconds / returnPeriod.tradingPeriod.inSeconds;

if (periodRatio > 1.0) {
print('Warning: scaling returns up can be misleading.');
}

final newReturn = (pow(1.0 + this.toArithmetic.nreturn,
newPeriod.inSeconds / period.inSeconds)
newPeriod.inSeconds / returnPeriod.tradingPeriod.inSeconds)
.toDouble()) -
1.0;
return Return(nreturn: newReturn, period: newPeriod, isLog: false);
return Return(
nreturn: newReturn,
returnPeriod: ReturnPeriod(tradingPeriod: newPeriod),
isLog: false);
}

/// converts arithmetic return to log
Expand All @@ -54,6 +68,8 @@ class Return {
}

Return get annualize {
return scale(newPeriod: oneyear);

return scale(newPeriod: FiConstants.oneTradingYear.tradingPeriod);

}
}
31 changes: 31 additions & 0 deletions lib/src/base/return_period.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'dart:core';
import 'package:finances/src/base/calc_trading_period.dart';

/// ReturnPeriods are a denomination format that allow for easy
/// conversion between [TradingPeriod]s and [CalendarPeriod]s.
///
///
class ReturnPeriod {
final Duration tradingPeriod;

/// When the period the return was generated begins
final DateTime? startDate;

/// When the period the return was generated ends. Note it's exclusive,
/// so M-F is 4 days, M-S is 5 days.
final DateTime? endDate;

const ReturnPeriod(
{required this.tradingPeriod, this.startDate, this.endDate});

ReturnPeriod.fromDates({required this.startDate, required this.endDate})
: tradingPeriod = calcTradingPeriod(startDate!, endDate!);

Duration? get calendarPeriod {
if (endDate == null || startDate == null) {
// TODO(dpe): is it possible to construct?
return null;
} else
return endDate!.difference(startDate!);
}
}
28 changes: 18 additions & 10 deletions lib/src/base/return_stream.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:finances/src/base/return.dart';
import 'package:finances/src/base/return_period.dart';

/// Indicates whether a [ReturnStream] is `incremental` or `cumulative`.
///
Expand Down Expand Up @@ -33,9 +34,11 @@ class ReturnStream {
final totalReturn = nreturns.fold(
1.0, (double p, Return c) => (p * (1 + c.nreturn))) -
1.0;
final totalPeriod = nreturns.fold(
Duration(days: 0), (Duration p, Return c) => (p + c.period));
return Return(nreturn: totalReturn, period: totalPeriod);
final totalPeriod = nreturns.fold(Duration(days: 0),
(Duration p, Return c) => (p + c.returnPeriod.tradingPeriod));
return Return(
nreturn: totalReturn,
returnPeriod: ReturnPeriod(tradingPeriod: totalPeriod));
}
}
}
Expand All @@ -47,7 +50,7 @@ class ReturnStream {
return this;
case ReturnStreamType.incremental:
{
// #TODO: could be faster?
// #TODO(dpe): could be faster?
var totalReturn = 1.0;
var totalPeriod = Duration(days: 0);
var result = List<Return>.generate(
Expand All @@ -56,8 +59,10 @@ class ReturnStream {

for (var i = 0; i < nreturns.length; i++) {
totalReturn = (totalReturn * (1.0 + nreturns[i].nreturn));
totalPeriod = totalPeriod + nreturns[i].period;
result[i] = Return(nreturn: totalReturn, period: totalPeriod);
totalPeriod = totalPeriod + nreturns[i].returnPeriod.tradingPeriod;
result[i] = Return(
nreturn: totalReturn,
returnPeriod: ReturnPeriod(tradingPeriod: totalPeriod));
}
return ReturnStream(result, ReturnStreamType.cumulative);
}
Expand All @@ -73,7 +78,7 @@ class ReturnStream {
return this;
case ReturnStreamType.cumulative:
{
// #TODO: could be faster?
// #TODO(dpe): could be faster?
var thisReturn = 0.0;
var thisPeriod = Duration.zero;
var result = List<Return>.generate(
Expand All @@ -83,14 +88,17 @@ class ReturnStream {
for (var i = 0; i < nreturns.length; i++) {
if (i == 0) {
thisReturn = nreturns[i].nreturn - 1.0;
thisPeriod = nreturns[i].period;
thisPeriod = nreturns[i].returnPeriod.tradingPeriod;
} else {
thisReturn =
(nreturns[i].nreturn / nreturns[(i - 1)].nreturn) - 1.0;
thisPeriod = nreturns[i].period - nreturns[(i - 1)].period;
thisPeriod = nreturns[i].returnPeriod.tradingPeriod -
nreturns[(i - 1)].returnPeriod.tradingPeriod;
}

result[i] = Return(nreturn: thisReturn, period: thisPeriod);
result[i] = Return(
nreturn: thisReturn,
returnPeriod: ReturnPeriod(tradingPeriod: thisPeriod));
}
return ReturnStream(result, ReturnStreamType.incremental);
}
Expand Down
85 changes: 85 additions & 0 deletions test/base/return_period_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:finances/src/base/finance_constants.dart';
import 'package:finances/src/base/return_period.dart';
import 'package:test/test.dart';

void main() {
group('ReturnPeriod', () {
group('below one full week: ', () {
final dateMon = DateTime(2022, 08, 08, 00, 00);
final dateMon2 = DateTime(2022, 08, 15, 00, 00);
final dateFri = DateTime(2022, 08, 12, 00, 00);
final dateSat = DateTime(2022, 08, 13, 00, 00);
final dateSun = DateTime(2022, 08, 14, 00, 00);

test('normal week', () {
expect(
ReturnPeriod.fromDates(
startDate: dateMon,
endDate: dateFri,
).tradingPeriod,
Duration(days: 4),
);
});
test('weekends have no trading days', () {
expect(
ReturnPeriod.fromDates(
startDate: dateMon,
endDate: dateSat,
).tradingPeriod,
Duration(days: 5),
);
expect(
ReturnPeriod.fromDates(
startDate: dateMon,
endDate: dateSun,
).tradingPeriod,
Duration(days: 5),
);
expect(
ReturnPeriod.fromDates(
startDate: dateMon,
endDate: dateMon2,
).tradingPeriod,
Duration(days: 5),
);
});
test('weekends have no trading days', () {
expect(
ReturnPeriod.fromDates(
startDate: dateSat,
endDate: dateSun,
).tradingPeriod,
Duration(days: 0),
);
});
});
group('more than one full week: ', () {
final startDate = DateTime(2022, 08, 08, 00, 00); // Monday
final endDate = DateTime(2022, 08, 15, 00, 00); // Friday
final period = ReturnPeriod.fromDates(
startDate: startDate,
endDate: endDate,
);
test('weekends have no trading days', () {
expect(
period.tradingPeriod,
Duration(days: 5),
);
});
group('more than a year: ', () {
final startDate = DateTime(2000, 01, 01);
final endDate = DateTime(2005, 01, 01);
final period = ReturnPeriod.fromDates(
startDate: startDate,
endDate: endDate,
);
test('constants and constructor give same results', () {
expect(
period.tradingPeriod,
FiConstants.oneTradingYear.tradingPeriod * 5,
);
});
});
});
});
}
26 changes: 17 additions & 9 deletions test/base/return_stream_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ void main() {
final returnStreamVol = ReturnStream(
[
Return(nreturn: -0.01),
Return(nreturn: 0.02, period: Duration(days: 5)),
Return(nreturn: 0.02, period: Duration(days: 10)),
Return(
nreturn: 0.02,
returnPeriod: ReturnPeriod(tradingPeriod: Duration(days: 5))),
Return(
nreturn: 0.02,
returnPeriod: ReturnPeriod(tradingPeriod: Duration(days: 10))),
],
ReturnStreamType.incremental,
);
Expand All @@ -23,7 +27,7 @@ void main() {
expect(
returnStreamOne.cumulativeReturn.nreturn, closeTo(0.01, 0.000001));
expect(
returnStreamOne.cumulativeReturn.period,
returnStreamOne.cumulativeReturn.returnPeriod.tradingPeriod,
Duration(days: 3),
);
expect(
Expand Down Expand Up @@ -61,11 +65,14 @@ void main() {
});
test('cumulates time correctly', () {
expect(
returnStreamVolCumulative.nreturns[0].period, Duration(days: 1));
returnStreamVolCumulative.nreturns[0].returnPeriod.tradingPeriod,
Duration(days: 1));
expect(
returnStreamVolCumulative.nreturns[1].period, Duration(days: 6));
returnStreamVolCumulative.nreturns[1].returnPeriod.tradingPeriod,
Duration(days: 6));
expect(
returnStreamVolCumulative.nreturns[2].period, Duration(days: 16));
returnStreamVolCumulative.nreturns[2].returnPeriod.tradingPeriod,
Duration(days: 16));
});
});
group('incrementalReturnStream: ', () {
Expand All @@ -79,9 +86,10 @@ void main() {
closeTo(returnStreamVol.nreturns[2].nreturn, 0.00001));
});
test('increments time correctly', () {
expect(irs.nreturns[0].period, Duration(days: 1));
expect(irs.nreturns[1].period, Duration(days: 5));
expect(irs.nreturns[2].period, Duration(days: 10));
expect(irs.nreturns[0].returnPeriod.tradingPeriod, Duration(days: 1));
expect(irs.nreturns[1].returnPeriod.tradingPeriod, Duration(days: 5));
expect(
irs.nreturns[2].returnPeriod.tradingPeriod, Duration(days: 10));
});
});
});
Expand Down
Loading