Compare commits
93 Commits
Author | SHA1 | Date | |
---|---|---|---|
173fba0f03 | |||
a4ecd38b97 | |||
3e5875b873 | |||
2f50894b46 | |||
|
06be27d46c | ||
c453ff20e5 | |||
a455cdfc65 | |||
f317a93bfe | |||
|
14afb1400a | ||
0bab00f455 | |||
56e8017de7 | |||
7108fa2a56 | |||
6cf56ddf11 | |||
3a5ca91234 | |||
db8f73d5c6 | |||
7b65f6ff3f | |||
18b60bd608 | |||
0fec9abac0 | |||
c35bc35529 | |||
c4e1d8b586 | |||
db8377f0ef | |||
583ca98e51 | |||
b1305ca89d | |||
ef68ae0293 | |||
cae704b658 | |||
a69f3e495e | |||
40429fc70a | |||
c7e955f91e | |||
d0c087c3bf | |||
2367d1aef8 | |||
31abaa4052 | |||
d229c9cf2d | |||
a8b90182da | |||
48e47e34a8 | |||
469c421639 | |||
3bc7e7b496 | |||
a395f7d98d | |||
56baf83a77 | |||
8c159062f5 | |||
371b319e9d | |||
a0499ca157 | |||
33c56d8f6c | |||
e450395ad0 | |||
3ffec7b11b | |||
6c8800bef2 | |||
f46ebaa8a9 | |||
e9bb795ecf | |||
569f20709b | |||
c713e3283b | |||
0bf1deac48 | |||
c605f71f10 | |||
a6fcd29a34 | |||
8117986742 | |||
da2993ebf0 | |||
f41b9c7519 | |||
7504c840eb | |||
1682fe12cc | |||
177e3bc4c8 | |||
922fe0f027 | |||
38fb9ca7d0 | |||
0a113fdd8a | |||
9a71cdf355 | |||
66ad448516 | |||
49cebecb88 | |||
da0bfcbcb1 | |||
cad069d351 | |||
130f4e58e9 | |||
2ca6167c8b | |||
95e9bfd51c | |||
5512a647ad | |||
7e524ccf7a | |||
aea6bf9b57 | |||
68d854cb3f | |||
0d0b2121a3 | |||
2a8f5b4041 | |||
19523519ee | |||
41562f7e70 | |||
3189e50bd8 | |||
336cf41ca8 | |||
0f002f3478 | |||
79cd44d41f | |||
978566e0a8 | |||
c99ffe02d0 | |||
65f2e8434c | |||
e8be7e9efa | |||
49604a5ae9 | |||
b38a317b82 | |||
03a8045400 | |||
625c9228e9 | |||
3ec5b06e83 | |||
e8dbc16157 | |||
b246709603 | |||
09365c7957 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,3 +4,7 @@
|
|||||||
*egg-info
|
*egg-info
|
||||||
__pycache__
|
__pycache__
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
build
|
||||||
|
.coverage
|
||||||
|
.DS_store
|
@ -1,15 +0,0 @@
|
|||||||
# Fincal
|
|
||||||
This module simplified handling of time-series data
|
|
||||||
|
|
||||||
## The problem
|
|
||||||
Time series data often have missing data points. These missing points mess things up when you are trying to do a comparison between two sections of a time series.
|
|
||||||
|
|
||||||
To make things worse, most libraries don't allow comparison based on dates. Month to Month and year to year comparisons become difficult as they cannot be translated into number of days. However, these are commonly used metrics while looking at financial data.
|
|
||||||
|
|
||||||
## The Solution
|
|
||||||
Fincal aims to simplify things by allowing you to:
|
|
||||||
* Compare time-series data based on dates
|
|
||||||
* Easy way to work around missing dates by taking the closest data points
|
|
||||||
* Completing series with missing data points using forward fill and backward fill
|
|
||||||
|
|
||||||
## Examples
|
|
@ -1,233 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"id": "3f7938c0-98e3-43b8-86e8-4f000cda7ce5",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"import datetime\n",
|
|
||||||
"import pandas as pd\n",
|
|
||||||
"\n",
|
|
||||||
"from fincal.fincal import TimeSeries\n",
|
|
||||||
"from fincal.core import Series"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 2,
|
|
||||||
"id": "4b8ccd5f-dfff-4202-82c4-f66a30c122b6",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"CPU times: user 152 ms, sys: 284 ms, total: 436 ms\n",
|
|
||||||
"Wall time: 61.3 ms\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(2021, 5, 28, 0, 0), 249.679993),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 31, 0, 0), 310.980011)], frequency='D')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 2,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"dfd = pd.read_csv('test_files/msft.csv')\n",
|
|
||||||
"# dfd = dfd[dfd['amfi_code'] == 118825].reset_index(drop=True)\n",
|
|
||||||
"ts = TimeSeries([(i.date, i.nav) for i in dfd.itertuples()], frequency='D')\n",
|
|
||||||
"repr(ts)\n",
|
|
||||||
"ts[['2022-01-31', '2021-05-28']]"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 3,
|
|
||||||
"id": "a0232e05-27c7-4d2d-a4bc-5dcf42666983",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"ename": "TypeError",
|
|
||||||
"evalue": "Type List cannot be instantiated; use list() instead",
|
|
||||||
"output_type": "error",
|
|
||||||
"traceback": [
|
|
||||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
|
||||||
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
|
|
||||||
"Input \u001b[0;32mIn [3]\u001b[0m, in \u001b[0;36m<cell line: 7>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfincal\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Frequency\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m List, Tuple\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_test_data\u001b[39m(\n\u001b[1;32m 6\u001b[0m frequency: Frequency,\n\u001b[1;32m 7\u001b[0m num: \u001b[38;5;28mint\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1000\u001b[39m,\n\u001b[1;32m 8\u001b[0m skip_weekends: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[1;32m 9\u001b[0m mu: \u001b[38;5;28mfloat\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.1\u001b[39m,\n\u001b[1;32m 10\u001b[0m sigma: \u001b[38;5;28mfloat\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.05\u001b[39m,\n\u001b[1;32m 11\u001b[0m eomonth: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m---> 12\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[43mList\u001b[49m\u001b[43m(\u001b[49m\u001b[43mTuple\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124;03m\"\"\"Creates TimeSeries data\u001b[39;00m\n\u001b[1;32m 14\u001b[0m \n\u001b[1;32m 15\u001b[0m \u001b[38;5;124;03m Parameters:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[38;5;124;03m Returns a TimeSeries object\u001b[39;00m\n\u001b[1;32m 36\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 38\u001b[0m start_date \u001b[38;5;241m=\u001b[39m datetime\u001b[38;5;241m.\u001b[39mdatetime(\u001b[38;5;241m2017\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m1\u001b[39m)\n",
|
|
||||||
"File \u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/typing.py:941\u001b[0m, in \u001b[0;36m_BaseGenericAlias.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 939\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 940\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_inst:\n\u001b[0;32m--> 941\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mType \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m cannot be instantiated; \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 942\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124muse \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__origin__\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m() instead\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 943\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__origin__(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 944\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n",
|
|
||||||
"\u001b[0;31mTypeError\u001b[0m: Type List cannot be instantiated; use list() instead"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"from fincal.fincal import create_date_series\n",
|
|
||||||
"from fincal.core import Frequency\n",
|
|
||||||
"from typing import List, Tuple\n",
|
|
||||||
"\n",
|
|
||||||
"def create_test_data(\n",
|
|
||||||
" frequency: Frequency,\n",
|
|
||||||
" num: int = 1000,\n",
|
|
||||||
" skip_weekends: bool = False,\n",
|
|
||||||
" mu: float = 0.1,\n",
|
|
||||||
" sigma: float = 0.05,\n",
|
|
||||||
" eomonth: bool = False,\n",
|
|
||||||
") -> List[Tuple]:\n",
|
|
||||||
" \"\"\"Creates TimeSeries data\n",
|
|
||||||
"\n",
|
|
||||||
" Parameters:\n",
|
|
||||||
" -----------\n",
|
|
||||||
" frequency: Frequency\n",
|
|
||||||
" The frequency of the time series data to be generated.\n",
|
|
||||||
"\n",
|
|
||||||
" num: int\n",
|
|
||||||
" Number of date: value pairs to be generated.\n",
|
|
||||||
"\n",
|
|
||||||
" skip_weekends: bool\n",
|
|
||||||
" Whether weekends (saturday, sunday) should be skipped.\n",
|
|
||||||
" Gets used only if the frequency is daily.\n",
|
|
||||||
"\n",
|
|
||||||
" mu: float\n",
|
|
||||||
" Mean return for the values.\n",
|
|
||||||
"\n",
|
|
||||||
" sigma: float\n",
|
|
||||||
" standard deviation of the values.\n",
|
|
||||||
"\n",
|
|
||||||
" Returns:\n",
|
|
||||||
" --------\n",
|
|
||||||
" Returns a TimeSeries object\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
"\n",
|
|
||||||
" start_date = datetime.datetime(2017, 1, 1)\n",
|
|
||||||
" timedelta_dict = {\n",
|
|
||||||
" frequency.freq_type: int(\n",
|
|
||||||
" frequency.value * num * (7 / 5 if frequency == AllFrequencies.D and skip_weekends else 1)\n",
|
|
||||||
" )\n",
|
|
||||||
" }\n",
|
|
||||||
" end_date = start_date + relativedelta(**timedelta_dict)\n",
|
|
||||||
" dates = create_date_series(start_date, end_date, frequency.symbol, skip_weekends=skip_weekends, eomonth=eomonth)\n",
|
|
||||||
" values = create_prices(1000, mu, sigma, num)\n",
|
|
||||||
" ts = list(zip(dates, values))\n",
|
|
||||||
" return ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "53dbc8a6-d7b1-4d82-ac3d-ee3908ff086d",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"id": "aa1584d5-1df0-4661-aeeb-5e8c424de06d",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"from fincal import fincal\n",
|
|
||||||
"from fincal.core import FincalOptions\n",
|
|
||||||
"import csv"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 8,
|
|
||||||
"id": "7d51fca1-f731-47c8-99c9-6e199cfeca92",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"['date', 'nav']\n",
|
|
||||||
"CPU times: user 47.7 ms, sys: 3.16 ms, total: 50.9 ms\n",
|
|
||||||
"Wall time: 50.3 ms\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(1992, 2, 19, 0, 0), '2.398438'),\n",
|
|
||||||
"\t (datetime.datetime(1992, 2, 20, 0, 0), '2.447917'),\n",
|
|
||||||
"\t (datetime.datetime(1992, 2, 21, 0, 0), '2.385417')\n",
|
|
||||||
"\t ...\n",
|
|
||||||
"\t (datetime.datetime(2022, 2, 16, 0, 0), '299.5'),\n",
|
|
||||||
"\t (datetime.datetime(2022, 2, 17, 0, 0), '290.730011'),\n",
|
|
||||||
"\t (datetime.datetime(2022, 2, 18, 0, 0), '287.929993')], frequency='M')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 8,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"FincalOptions.date_format = '%Y-%m-%d'\n",
|
|
||||||
"fincal.read_csv('test_files/msft.csv', frequency='M')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 9,
|
|
||||||
"id": "b689f64c-6764-45b5-bccf-f23b351f6419",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"import pandas as pd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 11,
|
|
||||||
"id": "6c9b2dd7-9983-40cd-8ac4-3530a3892f17",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"CPU times: user 61.4 ms, sys: 2.35 ms, total: 63.7 ms\n",
|
|
||||||
"Wall time: 62.6 ms\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"dfd = pd.read_csv(\"test_files/msft.csv\")\n",
|
|
||||||
"ts = fincal.TimeSeries([(i.date, i.nav) for i in dfd.itertuples()], frequency=\"D\")"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "Python 3 (ipykernel)",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"codemirror_mode": {
|
|
||||||
"name": "ipython",
|
|
||||||
"version": 3
|
|
||||||
},
|
|
||||||
"file_extension": ".py",
|
|
||||||
"mimetype": "text/x-python",
|
|
||||||
"name": "python",
|
|
||||||
"nbconvert_exporter": "python",
|
|
||||||
"pygments_lexer": "ipython3",
|
|
||||||
"version": "3.10.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
248
README.md
248
README.md
@ -1,39 +1,253 @@
|
|||||||
# Fincal
|
# PyFacts
|
||||||
This module simplified handling of time-series data
|
|
||||||
|
PyFacts stands for Python library for Financial analysis and computations on time series. It is a library which makes it simple to work with time series data.
|
||||||
|
|
||||||
|
Most libraries, and languages like SQL, work with rows. Operations are performed by rows and not by dates. For instance, to calculate 1-year rolling returns in SQL, you are forced to use either a lag of 365/252 rows, leading to an approximation, or slow and cumbersome joins. PyFacts solves this by allowing you to work with dates and time intervals. Hence, to calculate 1-year returns, you will be specifying a lag of 1-year and the library will do the grunt work of finding the most appropriate observations to calculate these returns on.
|
||||||
|
|
||||||
## The problem
|
## The problem
|
||||||
Time series data often have missing data points. These missing points mess things up when you are trying to do a comparison between two sections of a time series.
|
|
||||||
|
|
||||||
To make things worse, most libraries don't allow comparison based on dates. Month to Month and year to year comparisons become difficult as they cannot be translated into number of days. However, these are commonly used metrics while looking at financial data.
|
Libraries and languages usually don't allow comparison based on dates. Calculating month on month or year on year returns are always cumbersome as users are forced to rely on row lags. However, data always have inconsistencies, especially financial data. Markets don't work on weekends, there are off days, data doesn't get released on a few days a year, data availability is patchy when dealing with 40-year old data. All these problems are exacerbated when you are forced to make calculations using lag.
|
||||||
|
|
||||||
## The Solution
|
## The Solution
|
||||||
Fincal aims to simplify things by allowing you to:
|
|
||||||
* Compare time-series data based on dates
|
|
||||||
* Easy way to work around missing dates by taking the closest data points
|
|
||||||
* Completing series with missing data points using forward fill and backward fill
|
|
||||||
|
|
||||||
## Examples
|
PyFacts aims to simplify things by allowing you to:
|
||||||
|
|
||||||
|
- Compare time-series data based on dates and time-period-based lag
|
||||||
|
- Easy way to work around missing dates by taking the closest data points
|
||||||
|
- Completing series with missing data points using forward fill and backward fill
|
||||||
|
- Use friendly dates everywhere written as a simple string
|
||||||
|
|
||||||
|
## Creating a time series
|
||||||
|
|
||||||
|
Time series data can be created from a dictionary, a list of lists/tuples/dicts, or by reading a csv file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> import pyfacts as pft
|
||||||
|
|
||||||
|
>>> time_series_data = [
|
||||||
|
... ('2021-01-01', 10),
|
||||||
|
... ('2021-02-01', 12),
|
||||||
|
... ('2021-03-01', 14),
|
||||||
|
... ('2021-04-01', 16),
|
||||||
|
... ('2021-05-01', 18),
|
||||||
|
... ('2021-06-01', 20)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(time_series_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample usage
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> ts.calculate_returns(as_on='2021-04-01', return_period_unit='months', return_period_value=3, annual_compounded_returns=False)
|
||||||
|
(datetime.datetime(2021, 4, 1, 0, 0), 0.6)
|
||||||
|
|
||||||
|
>>> ts.calculate_returns(as_on='2021-04-15', return_period_unit='months', return_period_value=3, annual_compounded_returns=False)
|
||||||
|
(datetime.datetime(2021, 4, 1, 0, 0), 0.6)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with dates
|
||||||
|
|
||||||
|
With PyFacts, you never have to go into the hassle of creating datetime objects for your time series. PyFacts will parse any date passed to it as string. The default format is ISO format, i.e., YYYY-MM-DD. However, you can use your preferred format simply by specifying it in the options in datetime library compatible format, after importing the library. For example, to use DD-MM-YYY format:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> import pyfacts as pft
|
||||||
|
>>> pft.PyfactsOptions.date_format = '%d-%m-%Y'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the library will automatically parse all dates as DD-MM-YYYY
|
||||||
|
|
||||||
|
If you happen to have any one situation where you need to use a different format, all methods accept a date_format parameter to override the default.
|
||||||
|
|
||||||
|
### Working with multiple time series
|
||||||
|
|
||||||
|
While working with time series data, you will often need to perform calculations on the data. PyFacts supports all kinds of mathematical operations on time series.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> import pyfacts as pft
|
||||||
|
|
||||||
|
>>> time_series_data = [
|
||||||
|
... ('2021-01-01', 10),
|
||||||
|
... ('2021-02-01', 12),
|
||||||
|
... ('2021-03-01', 14),
|
||||||
|
... ('2021-04-01', 16),
|
||||||
|
... ('2021-05-01', 18),
|
||||||
|
... ('2021-06-01', 20)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(time_series_data)
|
||||||
|
>>> print(ts/100)
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 0.1),
|
||||||
|
(datetime.datetime(2022, 1, 2, 0, 0), 0.12),
|
||||||
|
(datetime.datetime(2022, 1, 3, 0, 0), 0.14),
|
||||||
|
(datetime.datetime(2022, 1, 4, 0, 0), 0.16),
|
||||||
|
(datetime.datetime(2022, 1, 6, 0, 0), 0.18),
|
||||||
|
(datetime.datetime(2022, 1, 7, 0, 0), 0.2)], frequency='M')
|
||||||
|
```
|
||||||
|
|
||||||
|
Mathematical operations can also be done between time series as long as they have the same dates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> import pyfacts as pft
|
||||||
|
|
||||||
|
>>> time_series_data = [
|
||||||
|
... ('2021-01-01', 10),
|
||||||
|
... ('2021-02-01', 12),
|
||||||
|
... ('2021-03-01', 14),
|
||||||
|
... ('2021-04-01', 16),
|
||||||
|
... ('2021-05-01', 18),
|
||||||
|
... ('2021-06-01', 20)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(time_series_data)
|
||||||
|
>>> ts2 = pft.TimeSeries(time_series_data)
|
||||||
|
>>> print(ts/ts2)
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 1.0),
|
||||||
|
(datetime.datetime(2022, 1, 2, 0, 0), 1.0),
|
||||||
|
(datetime.datetime(2022, 1, 3, 0, 0), 1.0),
|
||||||
|
(datetime.datetime(2022, 1, 4, 0, 0), 1.0),
|
||||||
|
(datetime.datetime(2022, 1, 6, 0, 0), 1.0),
|
||||||
|
(datetime.datetime(2022, 1, 7, 0, 0), 1.0)], frequency='M')
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if the dates are not in sync, PyFacts provides convenience methods for syncronising dates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> import pyfacts as pft
|
||||||
|
|
||||||
|
>>> data1 = [
|
||||||
|
... ('2021-01-01', 10),
|
||||||
|
... ('2021-02-01', 12),
|
||||||
|
... ('2021-03-01', 14),
|
||||||
|
... ('2021-04-01', 16),
|
||||||
|
... ('2021-05-01', 18),
|
||||||
|
... ('2021-06-01', 20)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> data2 = [
|
||||||
|
... ("2022-15-01", 20),
|
||||||
|
... ("2022-15-02", 22),
|
||||||
|
... ("2022-15-03", 24),
|
||||||
|
... ("2022-15-04", 26),
|
||||||
|
... ("2022-15-06", 28),
|
||||||
|
... ("2022-15-07", 30)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(data, frequency='M', date_format='%Y-%d-%m')
|
||||||
|
>>> ts2 = pft.TimeSeries(data2, frequency='M', date_format='%Y-%d-%m')
|
||||||
|
>>> ts.sync(ts2, fill_method='bfill') # Sync ts2 with ts1
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 20.0),
|
||||||
|
(datetime.datetime(2022, 2, 1, 0, 0), 22.0),
|
||||||
|
(datetime.datetime(2022, 3, 1, 0, 0), 24.0),
|
||||||
|
(datetime.datetime(2022, 4, 1, 0, 0), 26.0),
|
||||||
|
(datetime.datetime(2022, 6, 1, 0, 0), 28.0),
|
||||||
|
(datetime.datetime(2022, 7, 1, 0, 0), 30.0)], frequency='M')
|
||||||
|
```
|
||||||
|
|
||||||
|
Even if you need to perform calculations on data with different frequencies, PyFacts will let you easily handle this with the expand and shrink methods.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> data = [
|
||||||
|
... ("2022-01-01", 10),
|
||||||
|
... ("2022-02-01", 12),
|
||||||
|
... ("2022-03-01", 14),
|
||||||
|
... ("2022-04-01", 16),
|
||||||
|
... ("2022-05-01", 18),
|
||||||
|
... ("2022-06-01", 20)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(data, 'M')
|
||||||
|
>>> ts.expand(to_frequency='W', method='ffill')
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10.0),
|
||||||
|
(datetime.datetime(2022, 1, 8, 0, 0), 10.0),
|
||||||
|
(datetime.datetime(2022, 1, 15, 0, 0), 10.0)
|
||||||
|
...
|
||||||
|
(datetime.datetime(2022, 5, 14, 0, 0), 18.0),
|
||||||
|
(datetime.datetime(2022, 5, 21, 0, 0), 18.0),
|
||||||
|
(datetime.datetime(2022, 5, 28, 0, 0), 18.0)], frequency='W')
|
||||||
|
|
||||||
|
>>> ts.shrink(to_frequency='Q', method='ffill')
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10.0),
|
||||||
|
(datetime.datetime(2022, 4, 1, 0, 0), 16.0)], frequency='Q')
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to shorten the timeframe of the data with an aggregation function, the transform method will help you out. Currently it supports sum and mean.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> data = [
|
||||||
|
... ("2022-01-01", 10),
|
||||||
|
... ("2022-02-01", 12),
|
||||||
|
... ("2022-03-01", 14),
|
||||||
|
... ("2022-04-01", 16),
|
||||||
|
... ("2022-05-01", 18),
|
||||||
|
... ("2022-06-01", 20),
|
||||||
|
... ("2022-07-01", 22),
|
||||||
|
... ("2022-08-01", 24),
|
||||||
|
... ("2022-09-01", 26),
|
||||||
|
... ("2022-10-01", 28),
|
||||||
|
... ("2022-11-01", 30),
|
||||||
|
... ("2022-12-01", 32)
|
||||||
|
...]
|
||||||
|
|
||||||
|
>>> ts = pft.TimeSeries(data, 'M')
|
||||||
|
>>> ts.transform(to_frequency='Q', method='sum')
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 36.0),
|
||||||
|
(datetime.datetime(2022, 4, 1, 0, 0), 54.0),
|
||||||
|
(datetime.datetime(2022, 7, 1, 0, 0), 72.0),
|
||||||
|
(datetime.datetime(2022, 10, 1, 0, 0), 90.0)], frequency='Q')
|
||||||
|
|
||||||
|
>>> ts.transform(to_frequency='Q', method='mean')
|
||||||
|
|
||||||
|
TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 12.0),
|
||||||
|
(datetime.datetime(2022, 4, 1, 0, 0), 18.0),
|
||||||
|
(datetime.datetime(2022, 7, 1, 0, 0), 24.0),
|
||||||
|
(datetime.datetime(2022, 10, 1, 0, 0), 30.0)], frequency='Q')
|
||||||
|
```
|
||||||
|
|
||||||
## To-do
|
## To-do
|
||||||
|
|
||||||
### Core features
|
### Core features
|
||||||
- [ ] Add __setitem__
|
|
||||||
|
- [x] Add **setitem**
|
||||||
- [ ] Create emtpy TimeSeries object
|
- [ ] Create emtpy TimeSeries object
|
||||||
- [x] Read from CSV
|
- [x] Read from CSV
|
||||||
- [ ] Write to CSV
|
- [ ] Write to CSV
|
||||||
- [ ] Convert to dict
|
- [x] Convert to dict
|
||||||
- [ ] Convert to list of dicts
|
- [x] Convert to list of tuples
|
||||||
### Fincal features
|
|
||||||
|
### pyfacts features
|
||||||
|
|
||||||
- [x] Sync two TimeSeries
|
- [x] Sync two TimeSeries
|
||||||
- [x] Average rolling return
|
- [x] Average rolling return
|
||||||
- [ ] Sharpe ratio
|
- [x] Sharpe ratio
|
||||||
- [ ] Jensen's Alpha
|
- [x] Jensen's Alpha
|
||||||
- [ ] Beta
|
- [x] Beta
|
||||||
|
- [x] Sortino ratio
|
||||||
|
- [x] Correlation & R-squared
|
||||||
|
- [ ] Treynor ratio
|
||||||
- [x] Max drawdown
|
- [x] Max drawdown
|
||||||
|
- [ ] Moving average
|
||||||
|
|
||||||
### Pending implementation
|
### Pending implementation
|
||||||
- [ ] Use limit parameter in ffill and bfill
|
|
||||||
|
- [x] Use limit parameter in ffill and bfill
|
||||||
- [x] Implementation of ffill and bfill may be incorrect inside expand, check and correct
|
- [x] Implementation of ffill and bfill may be incorrect inside expand, check and correct
|
||||||
- [ ] Implement interpolation in expand
|
- [ ] Implement interpolation in expand
|
29
dict_iter.py
29
dict_iter.py
@ -1,29 +0,0 @@
|
|||||||
import pandas
|
|
||||||
|
|
||||||
from fincal.fincal import TimeSeries
|
|
||||||
|
|
||||||
dfd = pandas.read_csv('test_files/nav_history_daily - Copy.csv')
|
|
||||||
dfm = pandas.read_csv('test_files/nav_history_monthly.csv')
|
|
||||||
|
|
||||||
data_d = [(i.date, i.nav) for i in dfd.itertuples() if i.amfi_code == 118825]
|
|
||||||
data_d.sort()
|
|
||||||
data_m = [{'date': i.date, 'value': i.nav} for i in dfm.itertuples()]
|
|
||||||
|
|
||||||
tsd = TimeSeries(data_d, frequency='D')
|
|
||||||
|
|
||||||
md = dict(data_d)
|
|
||||||
counter = 1
|
|
||||||
for i in iter(md):
|
|
||||||
print(i)
|
|
||||||
counter += 1
|
|
||||||
if counter >= 5: break
|
|
||||||
|
|
||||||
print('\n')
|
|
||||||
counter = 1
|
|
||||||
for i in reversed(md):
|
|
||||||
print('rev', i)
|
|
||||||
counter += 1
|
|
||||||
if counter >= 5: break
|
|
||||||
|
|
||||||
x = [next(i) for i in iter(md)]
|
|
||||||
print(x)
|
|
@ -1,3 +0,0 @@
|
|||||||
from .core import *
|
|
||||||
from .fincal import *
|
|
||||||
from .utils import *
|
|
@ -1,20 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
"""The main routine."""
|
|
||||||
if args is None:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
|
|
||||||
print("This is the main routine.")
|
|
||||||
print("It should do something interesting.")
|
|
||||||
|
|
||||||
print("This is the name of the script: ", sys.argv[0])
|
|
||||||
print("Number of arguments: ", len(sys.argv))
|
|
||||||
print("The arguments are: ", str(sys.argv))
|
|
||||||
|
|
||||||
# Do argument parsing here with argparse
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
449
fincal/core.py
449
fincal/core.py
@ -1,449 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import inspect
|
|
||||||
from collections import UserList
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from numbers import Number
|
|
||||||
from typing import Callable, Iterable, List, Literal, Mapping, Sequence, Type
|
|
||||||
|
|
||||||
from .utils import FincalOptions, _parse_date, _preprocess_timeseries
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Frequency:
|
|
||||||
name: str
|
|
||||||
freq_type: str
|
|
||||||
value: int
|
|
||||||
days: int
|
|
||||||
symbol: str
|
|
||||||
|
|
||||||
|
|
||||||
def date_parser(*pos):
|
|
||||||
"""Decorator to parse dates in any function
|
|
||||||
|
|
||||||
Accepts the 0-indexed position of the parameter for which date parsing needs to be done.
|
|
||||||
Works even if function is used with keyword arguments while not maintaining parameter order.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
--------
|
|
||||||
>>> @date_parser(2, 3)
|
|
||||||
>>> def calculate_difference(diff_units='days', return_type='int', date1, date2):
|
|
||||||
... diff = date2 - date1
|
|
||||||
... if return_type == 'int':
|
|
||||||
... return diff.days
|
|
||||||
... return diff
|
|
||||||
...
|
|
||||||
>>> calculate_difference(date1='2019-01-01', date2='2020-01-01')
|
|
||||||
datetime.timedelta(365)
|
|
||||||
|
|
||||||
Each of the dates is automatically parsed into a datetime.datetime object from string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def parse_dates(func):
|
|
||||||
def wrapper_func(*args, **kwargs):
|
|
||||||
date_format: str = kwargs.get("date_format", None)
|
|
||||||
args: list = list(args)
|
|
||||||
sig: inspect.Signature = inspect.signature(func)
|
|
||||||
params: list = [i[0] for i in sig.parameters.items()]
|
|
||||||
|
|
||||||
for j in pos:
|
|
||||||
kwarg: str = params[j]
|
|
||||||
date = kwargs.get(kwarg, None)
|
|
||||||
in_args: bool = False
|
|
||||||
if date is None:
|
|
||||||
try:
|
|
||||||
date = args[j]
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
in_args = True
|
|
||||||
|
|
||||||
if date is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parsed_date: datetime.datetime = _parse_date(date, date_format)
|
|
||||||
if not in_args:
|
|
||||||
kwargs[kwarg] = parsed_date
|
|
||||||
else:
|
|
||||||
args[j] = parsed_date
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper_func
|
|
||||||
|
|
||||||
return parse_dates
|
|
||||||
|
|
||||||
|
|
||||||
class AllFrequencies:
|
|
||||||
D = Frequency("daily", "days", 1, 1, "D")
|
|
||||||
W = Frequency("weekly", "days", 7, 7, "W")
|
|
||||||
M = Frequency("monthly", "months", 1, 30, "M")
|
|
||||||
Q = Frequency("quarterly", "months", 3, 91, "Q")
|
|
||||||
H = Frequency("half-yearly", "months", 6, 182, "H")
|
|
||||||
Y = Frequency("annual", "years", 1, 365, "Y")
|
|
||||||
|
|
||||||
|
|
||||||
class _IndexSlicer:
|
|
||||||
"""Class to create a slice using iloc in TimeSeriesCore"""
|
|
||||||
|
|
||||||
def __init__(self, parent_obj: object):
|
|
||||||
self.parent = parent_obj
|
|
||||||
|
|
||||||
def __getitem__(self, n):
|
|
||||||
if isinstance(n, int):
|
|
||||||
keys: list = [self.parent.dates[n]]
|
|
||||||
else:
|
|
||||||
keys: list = self.parent.dates[n]
|
|
||||||
item = [(key, self.parent.data[key]) for key in keys]
|
|
||||||
if len(item) == 1:
|
|
||||||
return item[0]
|
|
||||||
|
|
||||||
return self.parent.__class__(item, self.parent.frequency.symbol)
|
|
||||||
|
|
||||||
|
|
||||||
class Series(UserList):
|
|
||||||
"""Container for a series of objects, all objects must be of the same type"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
data: Sequence,
|
|
||||||
data_type: Literal["date", "number", "bool"],
|
|
||||||
date_format: str = None,
|
|
||||||
):
|
|
||||||
types_dict: dict = {
|
|
||||||
"date": datetime.datetime,
|
|
||||||
"datetime": datetime.datetime,
|
|
||||||
"datetime.datetime": datetime.datetime,
|
|
||||||
"float": float,
|
|
||||||
"int": float,
|
|
||||||
"number": float,
|
|
||||||
"bool": bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
if data_type not in types_dict.keys():
|
|
||||||
raise ValueError("Unsupported value for data type")
|
|
||||||
|
|
||||||
if not isinstance(data, Sequence):
|
|
||||||
raise TypeError("Series object can only be created using Sequence types")
|
|
||||||
|
|
||||||
if data_type in ["date", "datetime", "datetime.datetime"]:
|
|
||||||
data = [_parse_date(i, date_format) for i in data]
|
|
||||||
else:
|
|
||||||
func: Callable = types_dict[data_type]
|
|
||||||
data: list = [func(i) for i in data]
|
|
||||||
|
|
||||||
self.dtype: Type = types_dict[data_type]
|
|
||||||
self.data: Sequence = data
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__name__}({self.data}, data_type='{self.dtype.__name__}')"
|
|
||||||
|
|
||||||
def __getitem__(self, i):
|
|
||||||
if isinstance(i, slice):
|
|
||||||
return self.__class__(self.data[i], str(self.dtype.__name__))
|
|
||||||
else:
|
|
||||||
return self.data[i]
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
if self.dtype == bool:
|
|
||||||
raise TypeError("> not supported for boolean series")
|
|
||||||
|
|
||||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
||||||
other = _parse_date(other)
|
|
||||||
|
|
||||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
|
||||||
gt = Series([i > other for i in self.data], "bool")
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
||||||
|
|
||||||
return gt
|
|
||||||
|
|
||||||
def __ge__(self, other):
|
|
||||||
if self.dtype == bool:
|
|
||||||
raise TypeError(">= not supported for boolean series")
|
|
||||||
|
|
||||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
||||||
other = _parse_date(other)
|
|
||||||
|
|
||||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
|
||||||
ge = Series([i >= other for i in self.data], "bool")
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
||||||
|
|
||||||
return ge
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
if self.dtype == bool:
|
|
||||||
raise TypeError("< not supported for boolean series")
|
|
||||||
|
|
||||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
||||||
other = _parse_date(other)
|
|
||||||
|
|
||||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
|
||||||
lt = Series([i < other for i in self.data], "bool")
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
||||||
return lt
|
|
||||||
|
|
||||||
def __le__(self, other):
|
|
||||||
if self.dtype == bool:
|
|
||||||
raise TypeError("<= not supported for boolean series")
|
|
||||||
|
|
||||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
||||||
other = _parse_date(other)
|
|
||||||
|
|
||||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
|
||||||
le = Series([i <= other for i in self.data], "bool")
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
||||||
return le
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
||||||
other = _parse_date(other)
|
|
||||||
|
|
||||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
|
||||||
eq = Series([i == other for i in self.data], "bool")
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
||||||
return eq
|
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesCore:
|
|
||||||
"""Defines the core building blocks of a TimeSeries object"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ts_data: List[Iterable] | Mapping,
|
|
||||||
frequency: Literal["D", "W", "M", "Q", "H", "Y"],
|
|
||||||
date_format: str = "%Y-%m-%d",
|
|
||||||
):
|
|
||||||
"""Instantiate a TimeSeriesCore object
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ts_data : List[Iterable] | Mapping
|
|
||||||
Time Series data in the form of list of tuples or dictionary.
|
|
||||||
The first element of each tuple should be a date and second element should be a value.
|
|
||||||
In case of dictionary, the key should be the date.
|
|
||||||
|
|
||||||
frequency : str
|
|
||||||
The frequency of the time series.
|
|
||||||
Valid values are {D, W, M, Q, H, Y}
|
|
||||||
|
|
||||||
date_format : str, optional, default "%Y-%m-%d"
|
|
||||||
Specify the format of the date
|
|
||||||
Required only if the first argument of tuples is a string. Otherwise ignored.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ts_data = _preprocess_timeseries(ts_data, date_format=date_format)
|
|
||||||
|
|
||||||
self.data = dict(ts_data)
|
|
||||||
if len(self.data) != len(ts_data):
|
|
||||||
print("Warning: The input data contains duplicate dates which have been ignored.")
|
|
||||||
self.frequency: Frequency = getattr(AllFrequencies, frequency)
|
|
||||||
self.iter_num: int = -1
|
|
||||||
self._dates: list = None
|
|
||||||
self._values: list = None
|
|
||||||
self._start_date: datetime.datetime = None
|
|
||||||
self._end_date: datetime.datetime = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dates(self) -> Series:
|
|
||||||
"""Get a list of all the dates in the TimeSeries object"""
|
|
||||||
|
|
||||||
if self._dates is None or len(self._dates) != len(self.data):
|
|
||||||
self._dates = list(self.data.keys())
|
|
||||||
|
|
||||||
return Series(self._dates, "date")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def values(self) -> Series:
|
|
||||||
"""Get a list of all the Values in the TimeSeries object"""
|
|
||||||
|
|
||||||
if self._values is None or len(self._values) != len(self.data):
|
|
||||||
self._values = list(self.data.values())
|
|
||||||
|
|
||||||
return Series(self._values, "number")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def start_date(self) -> datetime.datetime:
|
|
||||||
"""The first date in the TimeSeries object"""
|
|
||||||
|
|
||||||
return self.dates[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def end_date(self) -> datetime.datetime:
|
|
||||||
"""The last date in the TimeSeries object"""
|
|
||||||
|
|
||||||
return self.dates[-1]
|
|
||||||
|
|
||||||
def _get_printable_slice(self, n: int):
|
|
||||||
"""Helper function for __repr__ and __str__
|
|
||||||
|
|
||||||
Returns a slice of the dataframe from beginning and end.
|
|
||||||
"""
|
|
||||||
|
|
||||||
printable = {}
|
|
||||||
iter_f = iter(self.data)
|
|
||||||
first_n = [next(iter_f) for i in range(n // 2)]
|
|
||||||
|
|
||||||
iter_b = reversed(self.data)
|
|
||||||
last_n = [next(iter_b) for i in range(n // 2)]
|
|
||||||
last_n.sort()
|
|
||||||
|
|
||||||
printable["start"] = [str((i, self.data[i])) for i in first_n]
|
|
||||||
printable["end"] = [str((i, self.data[i])) for i in last_n]
|
|
||||||
return printable
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if len(self.data) > 6:
|
|
||||||
printable = self._get_printable_slice(6)
|
|
||||||
printable_str = "{}([{}\n\t ...\n\t {}], frequency={})".format(
|
|
||||||
self.__class__.__name__,
|
|
||||||
",\n\t ".join(printable["start"]),
|
|
||||||
",\n\t ".join(printable["end"]),
|
|
||||||
repr(self.frequency.symbol),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
printable_str = "{}([{}], frequency={})".format(
|
|
||||||
self.__class__.__name__,
|
|
||||||
",\n\t".join([str(i) for i in self.data.items()]),
|
|
||||||
repr(self.frequency.symbol),
|
|
||||||
)
|
|
||||||
return printable_str
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if len(self.data) > 6:
|
|
||||||
printable = self._get_printable_slice(6)
|
|
||||||
printable_str = "[{}\n ...\n {}]".format(
|
|
||||||
",\n ".join(printable["start"]),
|
|
||||||
",\n ".join(printable["end"]),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
printable_str = "[{}]".format(",\n ".join([str(i) for i in self.data.items()]))
|
|
||||||
return printable_str
|
|
||||||
|
|
||||||
@date_parser(1)
|
|
||||||
def _get_item_from_date(self, date: str | datetime.datetime):
|
|
||||||
return date, self.data[date]
|
|
||||||
|
|
||||||
def _get_item_from_key(self, key: str | datetime.datetime):
|
|
||||||
if isinstance(key, int):
|
|
||||||
raise KeyError(f"{key}. \nHint: use .iloc[{key}] for index based slicing.")
|
|
||||||
|
|
||||||
if key in ["dates", "values"]:
|
|
||||||
return getattr(self, key)
|
|
||||||
|
|
||||||
return self._get_item_from_date(key)
|
|
||||||
|
|
||||||
def _get_item_from_list(self, date_list: Sequence[str | datetime.datetime]):
|
|
||||||
data_to_return = [self._get_item_from_key(key) for key in date_list]
|
|
||||||
return self.__class__(data_to_return, frequency=self.frequency.symbol)
|
|
||||||
|
|
||||||
def _get_item_from_series(self, series: Series):
|
|
||||||
if series.dtype == bool:
|
|
||||||
if len(series) != len(self.dates):
|
|
||||||
raise ValueError(f"Length of Series: {len(series)} did not match length of object: {len(self.dates)}")
|
|
||||||
dates_to_return = [self.dates[i] for i, j in enumerate(series) if j]
|
|
||||||
elif series.dtype == datetime.datetime:
|
|
||||||
dates_to_return = list(series)
|
|
||||||
else:
|
|
||||||
raise TypeError(f"Cannot slice {self.__class__.__name__} using a Series of {series.dtype.__name__}")
|
|
||||||
|
|
||||||
return self._get_item_from_list(dates_to_return)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if isinstance(key, (int, str, datetime.datetime, datetime.date)):
|
|
||||||
return self._get_item_from_key(key)
|
|
||||||
|
|
||||||
if isinstance(key, Series):
|
|
||||||
return self._get_item_from_series(key)
|
|
||||||
|
|
||||||
if isinstance(key, Sequence):
|
|
||||||
return self._get_item_from_list(key)
|
|
||||||
|
|
||||||
raise TypeError(f"Invalid type {repr(type(key).__name__)} for slicing.")
|
|
||||||
|
|
||||||
def __setitem__(self, key: str | datetime.datetime, value: Number) -> None:
|
|
||||||
key = _parse_date(key)
|
|
||||||
self.data.update({key: value})
|
|
||||||
self.data = dict(sorted(self.data.items()))
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
self.n = 0
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
if self.n >= len(self.dates):
|
|
||||||
raise StopIteration
|
|
||||||
else:
|
|
||||||
key = self.dates[self.n]
|
|
||||||
self.n += 1
|
|
||||||
return key, self.data[key]
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.data)
|
|
||||||
|
|
||||||
@date_parser(1)
|
|
||||||
def __contains__(self, key: object) -> bool:
|
|
||||||
return key in self.data
|
|
||||||
|
|
||||||
@date_parser(1)
|
|
||||||
def get(self, date: str | datetime.datetime, default=None, closest=None):
|
|
||||||
|
|
||||||
if closest is None:
|
|
||||||
closest = FincalOptions.get_closest
|
|
||||||
|
|
||||||
if closest == "exact":
|
|
||||||
try:
|
|
||||||
item = self._get_item_from_date(date)
|
|
||||||
return item
|
|
||||||
except KeyError:
|
|
||||||
return default
|
|
||||||
|
|
||||||
if closest == "previous":
|
|
||||||
delta = datetime.timedelta(-1)
|
|
||||||
elif closest == "next":
|
|
||||||
delta = datetime.timedelta(1)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid argument from closest {closest!r}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
item = self._get_item_from_date(date)
|
|
||||||
return item
|
|
||||||
except KeyError:
|
|
||||||
date += delta
|
|
||||||
|
|
||||||
@property
|
|
||||||
def iloc(self) -> Mapping:
|
|
||||||
"""Returns an item or a set of items based on index
|
|
||||||
|
|
||||||
supports slicing using numerical index.
|
|
||||||
Accepts integers or Python slice objects
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
>>> ts = TimeSeries(data, frequency='D')
|
|
||||||
>>> ts.iloc[0] # get the first value
|
|
||||||
>>> ts.iloc[-1] # get the last value
|
|
||||||
>>> ts.iloc[:3] # get the first 3 values
|
|
||||||
>>> ts.illoc[-3:] # get the last 3 values
|
|
||||||
>>> ts.iloc[5:10] # get five values starting from the fifth value
|
|
||||||
>>> ts.iloc[::2] # get every alternate date
|
|
||||||
"""
|
|
||||||
|
|
||||||
return _IndexSlicer(self)
|
|
||||||
|
|
||||||
def head(self, n: int = 6) -> TimeSeriesCore:
|
|
||||||
"""Returns the first n items of the TimeSeries object"""
|
|
||||||
|
|
||||||
return self.iloc[:n]
|
|
||||||
|
|
||||||
def tail(self, n: int = 6) -> TimeSeriesCore:
|
|
||||||
"""Returns the last n items of the TimeSeries object"""
|
|
||||||
|
|
||||||
return self.iloc[-n:]
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return self.data.items()
|
|
26
my_checks.py
Normal file
26
my_checks.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
import pandas
|
||||||
|
|
||||||
|
from pyfacts.pyfacts import AllFrequencies, TimeSeries, create_date_series
|
||||||
|
|
||||||
|
dfd = pandas.read_csv("test_files/msft.csv")
|
||||||
|
dfm = pandas.read_csv("test_files/nav_history_monthly.csv")
|
||||||
|
dfq = pandas.read_csv("test_files/nav_history_quarterly.csv")
|
||||||
|
|
||||||
|
data_d = [(i.date, i.nav) for i in dfd.itertuples()]
|
||||||
|
data_m = [{"date": i.date, "value": i.nav} for i in dfm.itertuples()]
|
||||||
|
data_q = {i.date: i.nav for i in dfq.itertuples()}
|
||||||
|
data_q.update({"14-02-2022": 93.7})
|
||||||
|
|
||||||
|
tsd = TimeSeries(data_d, frequency="D")
|
||||||
|
tsm = TimeSeries(data_m, frequency="M", date_format="%d-%m-%Y")
|
||||||
|
tsq = TimeSeries(data_q, frequency="Q", date_format="%d-%m-%Y")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
# ts.calculate_rolling_returns(datetime.datetime(2015, 1, 1), datetime.datetime(2022, 2, 1), years=1)
|
||||||
|
bdata = tsq.bfill()
|
||||||
|
# rr = tsd.calculate_rolling_returns(datetime.datetime(2022, 1, 1), datetime.datetime(2022, 2, 1), years=1)
|
||||||
|
print(time.time() - start)
|
26
my_test.py
26
my_test.py
@ -1,26 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import time
|
|
||||||
import timeit
|
|
||||||
|
|
||||||
import pandas
|
|
||||||
|
|
||||||
from fincal.fincal import AllFrequencies, TimeSeries, create_date_series
|
|
||||||
|
|
||||||
dfd = pandas.read_csv('test_files/msft.csv')
|
|
||||||
dfm = pandas.read_csv('test_files/nav_history_monthly.csv')
|
|
||||||
dfq = pandas.read_csv('test_files/nav_history_quarterly.csv')
|
|
||||||
|
|
||||||
data_d = [(i.date, i.nav) for i in dfd.itertuples()]
|
|
||||||
data_m = [{'date': i.date, 'value': i.nav} for i in dfm.itertuples()]
|
|
||||||
data_q = {i.date: i.nav for i in dfq.itertuples()}
|
|
||||||
data_q.update({'14-02-2022': 93.7})
|
|
||||||
|
|
||||||
tsd = TimeSeries(data_d, frequency='D')
|
|
||||||
tsm = TimeSeries(data_m, frequency='M', date_format='%d-%m-%Y')
|
|
||||||
tsq = TimeSeries(data_q, frequency='Q', date_format='%d-%m-%Y')
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
# ts.calculate_rolling_returns(datetime.datetime(2015, 1, 1), datetime.datetime(2022, 2, 1), years=1)
|
|
||||||
bdata = tsq.bfill()
|
|
||||||
# rr = tsd.calculate_rolling_returns(datetime.datetime(2022, 1, 1), datetime.datetime(2022, 2, 1), years=1)
|
|
||||||
print(time.time() - start)
|
|
27
pyfacts/__init__.py
Normal file
27
pyfacts/__init__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from .core import *
|
||||||
|
from .pyfacts import *
|
||||||
|
from .statistics import *
|
||||||
|
from .utils import *
|
||||||
|
|
||||||
|
__author__ = "Gourav Kumar"
|
||||||
|
__email__ = "gouravkr@outlook.in"
|
||||||
|
__version__ = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
__doc__ = """
|
||||||
|
PyFacts stands for Python library for Financial analysis and computations on time series.
|
||||||
|
It is a library which makes it simple to work with time series data.
|
||||||
|
|
||||||
|
Most libraries, and languages like SQL, work with rows. Operations are performed by rows
|
||||||
|
and not by dates. For instance, to calculate 1-year rolling returns in SQL, you are forced
|
||||||
|
to use either a lag of 365/252 rows, leading to an approximation, or slow and cumbersome
|
||||||
|
joins. PyFacts solves this by allowing you to work with dates and time intervals. Hence,
|
||||||
|
to calculate 1-year returns, you will be specifying a lag of 1-year and the library will
|
||||||
|
do the grunt work of finding the most appropriate observations to calculate these returns on.
|
||||||
|
|
||||||
|
PyFacts aims to simplify things by allowing you to:
|
||||||
|
* Compare time-series data based on dates and time-period-based lag
|
||||||
|
* Easy way to work around missing dates by taking the closest data points
|
||||||
|
* Completing series with missing data points using forward fill and backward fill
|
||||||
|
* Use friendly dates everywhere written as a simple string
|
||||||
|
"""
|
1017
pyfacts/core.py
Normal file
1017
pyfacts/core.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -13,9 +13,9 @@ class DateNotFoundError(Exception):
|
|||||||
class DateOutOfRangeError(Exception):
|
class DateOutOfRangeError(Exception):
|
||||||
"""Exception to be raised when provided date is outside the range of dates in the time series"""
|
"""Exception to be raised when provided date is outside the range of dates in the time series"""
|
||||||
|
|
||||||
def __init__(self, date: datetime.datetime, type: Literal['min', 'max']) -> None:
|
def __init__(self, date: datetime.datetime, type: Literal["min", "max"]) -> None:
|
||||||
if type == 'min':
|
if type == "min":
|
||||||
message = f"Provided date {date} is before the first date in the TimeSeries"
|
message = f"Provided date {date} is before the first date in the TimeSeries"
|
||||||
if type == 'max':
|
if type == "max":
|
||||||
message = f"Provided date {date} is after the last date in the TimeSeries"
|
message = f"Provided date {date} is after the last date in the TimeSeries"
|
||||||
super().__init__(message)
|
super().__init__(message)
|
@ -11,9 +11,10 @@ from dateutil.relativedelta import relativedelta
|
|||||||
|
|
||||||
from .core import AllFrequencies, Frequency, Series, TimeSeriesCore, date_parser
|
from .core import AllFrequencies, Frequency, Series, TimeSeriesCore, date_parser
|
||||||
from .utils import (
|
from .utils import (
|
||||||
FincalOptions,
|
PyfactsOptions,
|
||||||
_find_closest_date,
|
_find_closest_date,
|
||||||
_interval_to_years,
|
_interval_to_years,
|
||||||
|
_is_eomonth,
|
||||||
_preprocess_match_options,
|
_preprocess_match_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -75,31 +76,30 @@ def create_date_series(
|
|||||||
if eomonth and frequency.days < AllFrequencies.M.days:
|
if eomonth and frequency.days < AllFrequencies.M.days:
|
||||||
raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}")
|
raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}")
|
||||||
|
|
||||||
if ensure_coverage:
|
|
||||||
if frequency.days == 1 and skip_weekends and end_date.weekday() > 4:
|
|
||||||
extend_by_days = 7 - end_date.weekday()
|
|
||||||
end_date += relativedelta(days=extend_by_days)
|
|
||||||
|
|
||||||
# To-do: Add code to ensure coverage for other frequencies as well
|
|
||||||
|
|
||||||
datediff = (end_date - start_date).days / frequency.days + 1
|
|
||||||
dates = []
|
dates = []
|
||||||
|
counter = 0
|
||||||
for i in range(0, int(datediff)):
|
while counter < 100000:
|
||||||
diff = {frequency.freq_type: frequency.value * i}
|
diff = {frequency.freq_type: frequency.value * counter}
|
||||||
date = start_date + relativedelta(**diff)
|
date = start_date + relativedelta(**diff)
|
||||||
|
|
||||||
if eomonth:
|
if eomonth:
|
||||||
next_month = 1 if date.month == 12 else date.month + 1
|
date += relativedelta(months=1, day=1, days=-1)
|
||||||
date = date.replace(day=1).replace(month=next_month) - relativedelta(days=1)
|
|
||||||
|
|
||||||
if date <= end_date:
|
if date > end_date:
|
||||||
|
if not ensure_coverage:
|
||||||
|
break
|
||||||
|
elif dates[-1] >= end_date:
|
||||||
|
break
|
||||||
|
|
||||||
|
counter += 1
|
||||||
if frequency.days > 1 or not skip_weekends:
|
if frequency.days > 1 or not skip_weekends:
|
||||||
dates.append(date)
|
dates.append(date)
|
||||||
elif date.weekday() < 5:
|
elif date.weekday() < 5:
|
||||||
dates.append(date)
|
dates.append(date)
|
||||||
|
else:
|
||||||
|
raise ValueError("Cannot generate a series containing more than 100000 dates")
|
||||||
|
|
||||||
return Series(dates, data_type="date")
|
return Series(dates, dtype="date")
|
||||||
|
|
||||||
|
|
||||||
class TimeSeries(TimeSeriesCore):
|
class TimeSeries(TimeSeriesCore):
|
||||||
@ -117,26 +117,34 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
* List of dictionaries with 2 keys, first representing date & second representing value
|
* List of dictionaries with 2 keys, first representing date & second representing value
|
||||||
* Dictionary of key: value pairs
|
* Dictionary of key: value pairs
|
||||||
|
|
||||||
date_format : str, optional, default "%Y-%m-%d"
|
|
||||||
Specify the format of the date
|
|
||||||
Required only if the first argument of tuples is a string. Otherwise ignored.
|
|
||||||
|
|
||||||
frequency : str, optional, default "infer"
|
frequency : str, optional, default "infer"
|
||||||
The frequency of the time series. Default is infer.
|
The frequency of the time series. Default is infer.
|
||||||
The class will try to infer the frequency automatically and adjust to the closest member.
|
The class will try to infer the frequency automatically and adjust to the closest member.
|
||||||
Note that inferring frequencies can fail if the data is too irregular.
|
Note that inferring frequencies can fail if the data is too irregular.
|
||||||
Valid values are {D, W, M, Q, H, Y}
|
Valid values are {D, W, M, Q, H, Y}
|
||||||
|
|
||||||
|
validate_frequency: boolean, default True
|
||||||
|
Whether the provided frequency should be validated against the data.
|
||||||
|
When set to True, if the expected number of data points are not withint the expected limits,
|
||||||
|
it will raise an Exception and object creation will fail.
|
||||||
|
This parameter will be ignored if frequency is not provided.
|
||||||
|
refer core._validate_frequency for more details.
|
||||||
|
|
||||||
|
date_format : str, optional, default "%Y-%m-%d"
|
||||||
|
Specify the format of the date
|
||||||
|
Required only if the first argument of tuples is a string. Otherwise ignored.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: List[Iterable] | Mapping,
|
data: List[Iterable] | Mapping,
|
||||||
frequency: Literal["D", "W", "M", "Q", "H", "Y"],
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
validate_frequency: bool = True,
|
||||||
date_format: str = "%Y-%m-%d",
|
date_format: str = "%Y-%m-%d",
|
||||||
):
|
):
|
||||||
"""Instantiate a TimeSeriesCore object"""
|
"""Instantiate a TimeSeriesCore object"""
|
||||||
|
|
||||||
super().__init__(data, frequency, date_format)
|
super().__init__(data, frequency, validate_frequency, date_format)
|
||||||
|
|
||||||
def info(self) -> str:
|
def info(self) -> str:
|
||||||
"""Summary info about the TimeSeries object"""
|
"""Summary info about the TimeSeries object"""
|
||||||
@ -145,7 +153,9 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
res_string: str = "First date: {}\nLast date: {}\nNumber of rows: {}"
|
res_string: str = "First date: {}\nLast date: {}\nNumber of rows: {}"
|
||||||
return res_string.format(self.start_date, self.end_date, total_dates)
|
return res_string.format(self.start_date, self.end_date, total_dates)
|
||||||
|
|
||||||
def ffill(self, inplace: bool = False, limit: int = None, skip_weekends: bool = False) -> TimeSeries | None:
|
def ffill(
|
||||||
|
self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = None
|
||||||
|
) -> TimeSeries | None:
|
||||||
"""Forward fill missing dates in the time series
|
"""Forward fill missing dates in the time series
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -163,18 +173,24 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
-------
|
-------
|
||||||
Returns a TimeSeries object if inplace is False, otherwise None
|
Returns a TimeSeries object if inplace is False, otherwise None
|
||||||
"""
|
"""
|
||||||
|
if eomonth is None:
|
||||||
|
eomonth = _is_eomonth(self.dates)
|
||||||
|
|
||||||
eomonth: bool = True if self.frequency.days >= AllFrequencies.M.days else False
|
|
||||||
dates_to_fill = create_date_series(
|
dates_to_fill = create_date_series(
|
||||||
self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends
|
self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends
|
||||||
)
|
)
|
||||||
|
|
||||||
new_ts = dict()
|
new_ts = dict()
|
||||||
|
counter = 0
|
||||||
for cur_date in dates_to_fill:
|
for cur_date in dates_to_fill:
|
||||||
try:
|
try:
|
||||||
cur_val = self.get(cur_date, closest="previous")
|
new_val = self[cur_date]
|
||||||
|
cur_val = new_val
|
||||||
|
counter = 0
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
if counter >= limit:
|
||||||
|
continue
|
||||||
|
counter += 1
|
||||||
new_ts.update({cur_date: cur_val[1]})
|
new_ts.update({cur_date: cur_val[1]})
|
||||||
|
|
||||||
if inplace:
|
if inplace:
|
||||||
@ -183,7 +199,9 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
|
|
||||||
return self.__class__(new_ts, frequency=self.frequency.symbol)
|
return self.__class__(new_ts, frequency=self.frequency.symbol)
|
||||||
|
|
||||||
def bfill(self, inplace: bool = False, limit: int = None, skip_weekends: bool = False) -> TimeSeries | None:
|
def bfill(
|
||||||
|
self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = None
|
||||||
|
) -> TimeSeries | None:
|
||||||
"""Backward fill missing dates in the time series
|
"""Backward fill missing dates in the time series
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -201,21 +219,28 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
-------
|
-------
|
||||||
Returns a TimeSeries object if inplace is False, otherwise None
|
Returns a TimeSeries object if inplace is False, otherwise None
|
||||||
"""
|
"""
|
||||||
|
if eomonth is None:
|
||||||
|
eomonth = _is_eomonth(self.dates)
|
||||||
|
|
||||||
eomonth: bool = True if self.frequency.days >= AllFrequencies.M.days else False
|
|
||||||
dates_to_fill = create_date_series(
|
dates_to_fill = create_date_series(
|
||||||
self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends
|
self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends
|
||||||
)
|
)
|
||||||
dates_to_fill.append(self.end_date)
|
dates_to_fill.append(self.end_date)
|
||||||
|
|
||||||
bfill_ts = dict()
|
bfill_ts = dict()
|
||||||
|
counter = 0
|
||||||
for cur_date in reversed(dates_to_fill):
|
for cur_date in reversed(dates_to_fill):
|
||||||
try:
|
try:
|
||||||
cur_val = self.data[cur_date]
|
new_val = self[cur_date]
|
||||||
|
cur_val = new_val
|
||||||
|
counter = 0
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
if counter >= limit:
|
||||||
bfill_ts.update({cur_date: cur_val})
|
continue
|
||||||
new_ts = {k: bfill_ts[k] for k in reversed(bfill_ts)}
|
counter += 1
|
||||||
|
bfill_ts.update({cur_date: cur_val[1]})
|
||||||
|
# new_ts = {k: bfill_ts[k] for k in reversed(bfill_ts)}
|
||||||
|
new_ts = dict(list(reversed(bfill_ts.items())))
|
||||||
if inplace:
|
if inplace:
|
||||||
self.data = new_ts
|
self.data = new_ts
|
||||||
return None
|
return None
|
||||||
@ -236,7 +261,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
return_period_unit: Literal["years", "months", "days"] = "years",
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
return_period_value: int = 1,
|
return_period_value: int = 1,
|
||||||
date_format: str = None,
|
date_format: str = None,
|
||||||
) -> float:
|
) -> Tuple[datetime.datetime, float]:
|
||||||
"""Method to calculate returns for a certain time-period as on a particular date
|
"""Method to calculate returns for a certain time-period as on a particular date
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -269,7 +294,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
* fail: Raise a ValueError
|
* fail: Raise a ValueError
|
||||||
* nan: Return nan as the value
|
* nan: Return nan as the value
|
||||||
|
|
||||||
compounding : bool, optional
|
annual_compounded_returns : bool, optional
|
||||||
Whether the return should be compounded annually.
|
Whether the return should be compounded annually.
|
||||||
|
|
||||||
return_period_unit : 'years', 'months', 'days'
|
return_period_unit : 'years', 'months', 'days'
|
||||||
@ -295,18 +320,24 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
|
|
||||||
Example
|
Example
|
||||||
--------
|
--------
|
||||||
>>> calculate_returns(datetime.date(2020, 1, 1), years=1)
|
>>> ts.calculate_returns(datetime.date(2020, 1, 1), years=1)
|
||||||
(datetime.datetime(2020, 1, 1, 0, 0), .0567)
|
(datetime.datetime(2020, 1, 1, 0, 0), .0567)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
as_on_delta, prior_delta = _preprocess_match_options(as_on_match, prior_match, closest)
|
as_on_delta, prior_delta = _preprocess_match_options(as_on_match, prior_match, closest)
|
||||||
|
|
||||||
prev_date = as_on - relativedelta(**{return_period_unit: return_period_value})
|
current = _find_closest_date(self, as_on, closest_max_days, as_on_delta, if_not_found)
|
||||||
current = _find_closest_date(self.data, as_on, closest_max_days, as_on_delta, if_not_found)
|
|
||||||
if current[1] != str("nan"):
|
|
||||||
previous = _find_closest_date(self.data, prev_date, closest_max_days, prior_delta, if_not_found)
|
|
||||||
|
|
||||||
if current[1] == str("nan") or previous[1] == str("nan"):
|
prev_date = as_on - relativedelta(**{return_period_unit: return_period_value})
|
||||||
|
if current[1] != str("nan"):
|
||||||
|
previous = _find_closest_date(self, prev_date, closest_max_days, prior_delta, if_not_found)
|
||||||
|
|
||||||
|
if (
|
||||||
|
current[1] == str("nan")
|
||||||
|
or previous[1] == str("nan")
|
||||||
|
or current[0] == str("nan")
|
||||||
|
or previous[0] == str("nan")
|
||||||
|
):
|
||||||
return as_on, float("NaN")
|
return as_on, float("NaN")
|
||||||
|
|
||||||
returns = current[1] / previous[1]
|
returns = current[1] / previous[1]
|
||||||
@ -318,8 +349,8 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
@date_parser(1, 2)
|
@date_parser(1, 2)
|
||||||
def calculate_rolling_returns(
|
def calculate_rolling_returns(
|
||||||
self,
|
self,
|
||||||
from_date: datetime.date | str,
|
from_date: datetime.date | str = None,
|
||||||
to_date: datetime.date | str,
|
to_date: datetime.date | str = None,
|
||||||
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
as_on_match: str = "closest",
|
as_on_match: str = "closest",
|
||||||
prior_match: str = "closest",
|
prior_match: str = "closest",
|
||||||
@ -342,16 +373,16 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
End date for the returns calculation.
|
End date for the returns calculation.
|
||||||
|
|
||||||
frequency : str, optional
|
frequency : str, optional
|
||||||
Frequency at which the returns should be calcualated.
|
Frequency at which the returns should be calculated.
|
||||||
Valid values are {D, W, M, Q, H, Y}
|
Valid values are {D, W, M, Q, H, Y}
|
||||||
|
|
||||||
as_on_match : str, optional
|
as_on_match : str, optional
|
||||||
The match mode to be used for the as on date.
|
The match mode to be used for the as on date.
|
||||||
If not specified, the value for the closes parameter will be used.
|
If not specified, the value for the closest parameter will be used.
|
||||||
|
|
||||||
prior_match : str, optional
|
prior_match : str, optional
|
||||||
The match mode to be used for the prior date, i.e., the date against which the return will be calculated.
|
The match mode to be used for the prior date, i.e., the date against which the return will be calculated.
|
||||||
If not specified, the value for the closes parameter will be used.
|
If not specified, the value for the closest parameter will be used.
|
||||||
|
|
||||||
closest : previous | next | exact
|
closest : previous | next | exact
|
||||||
The default match mode for dates.
|
The default match mode for dates.
|
||||||
@ -369,7 +400,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
For instance, if the input date is before the starting of the first date of the time series,
|
For instance, if the input date is before the starting of the first date of the time series,
|
||||||
but match mode is set to previous. A DateOutOfRangeError will be raised in such cases.
|
but match mode is set to previous. A DateOutOfRangeError will be raised in such cases.
|
||||||
|
|
||||||
compounding : bool, optional
|
annual_compounded_returns : bool, optional
|
||||||
Should the returns be compounded annually.
|
Should the returns be compounded annually.
|
||||||
|
|
||||||
return_period_unit : years | month | days
|
return_period_unit : years | month | days
|
||||||
@ -384,7 +415,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Returs the rolling returns as a TimeSeries object.
|
Returns the rolling returns as a TimeSeries object.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@ -403,6 +434,13 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
frequency = getattr(AllFrequencies, frequency)
|
frequency = getattr(AllFrequencies, frequency)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError(f"Invalid argument for frequency {frequency}")
|
raise ValueError(f"Invalid argument for frequency {frequency}")
|
||||||
|
if from_date is None:
|
||||||
|
from_date = self.start_date + relativedelta(
|
||||||
|
days=math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365)
|
||||||
|
)
|
||||||
|
|
||||||
|
if to_date is None:
|
||||||
|
to_date = self.end_date
|
||||||
|
|
||||||
dates = create_date_series(from_date, to_date, frequency.symbol)
|
dates = create_date_series(from_date, to_date, frequency.symbol)
|
||||||
if frequency == AllFrequencies.D:
|
if frequency == AllFrequencies.D:
|
||||||
@ -422,7 +460,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
)
|
)
|
||||||
rolling_returns.append(returns)
|
rolling_returns.append(returns)
|
||||||
rolling_returns.sort()
|
rolling_returns.sort()
|
||||||
return self.__class__(rolling_returns, self.frequency.symbol)
|
return self.__class__(rolling_returns, frequency.symbol)
|
||||||
|
|
||||||
@date_parser(1, 2)
|
@date_parser(1, 2)
|
||||||
def volatility(
|
def volatility(
|
||||||
@ -443,7 +481,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
) -> float:
|
) -> float:
|
||||||
"""Calculates the volatility of the time series.add()
|
"""Calculates the volatility of the time series.add()
|
||||||
|
|
||||||
The volatility is calculated as the standard deviaion of periodic returns.
|
The volatility is calculated as the standard deviation of periodic returns.
|
||||||
The periodicity of returns is based on the periodicity of underlying data.
|
The periodicity of returns is based on the periodicity of underlying data.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@ -465,7 +503,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
Only used when annualizing volatility for a time series with daily frequency.
|
Only used when annualizing volatility for a time series with daily frequency.
|
||||||
If not provided, will use the value in FincalOptions.traded_days.
|
If not provided, will use the value in FincalOptions.traded_days.
|
||||||
|
|
||||||
Remaining options are passed on to rolling_return function.
|
Remaining options are passed on to calculate_rolling_returns function.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
-------
|
-------
|
||||||
@ -492,9 +530,12 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
from_date = self.start_date + relativedelta(**{return_period_unit: return_period_value})
|
from_date = self.start_date + relativedelta(**{return_period_unit: return_period_value})
|
||||||
if to_date is None:
|
if to_date is None:
|
||||||
to_date = self.end_date
|
to_date = self.end_date
|
||||||
|
years = _interval_to_years(return_period_unit, return_period_value)
|
||||||
if annual_compounded_returns is None:
|
if annual_compounded_returns is None:
|
||||||
annual_compounded_returns = False if frequency.days <= 366 else True
|
if years > 1:
|
||||||
|
annual_compounded_returns = True
|
||||||
|
else:
|
||||||
|
annual_compounded_returns = False
|
||||||
|
|
||||||
rolling_returns = self.calculate_rolling_returns(
|
rolling_returns = self.calculate_rolling_returns(
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
@ -511,12 +552,12 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
sd = statistics.stdev(rolling_returns.values)
|
sd = statistics.stdev(rolling_returns.values)
|
||||||
if annualize_volatility:
|
if annualize_volatility:
|
||||||
if traded_days is None:
|
if traded_days is None:
|
||||||
traded_days = FincalOptions.traded_days
|
traded_days = PyfactsOptions.traded_days
|
||||||
|
|
||||||
if return_period_unit == "months":
|
if return_period_unit == "months":
|
||||||
sd *= math.sqrt(12)
|
sd *= math.sqrt(12 / return_period_value)
|
||||||
elif return_period_unit == "days":
|
elif return_period_unit == "days":
|
||||||
sd *= math.sqrt(traded_days)
|
sd *= math.sqrt(traded_days / return_period_value)
|
||||||
|
|
||||||
return sd
|
return sd
|
||||||
|
|
||||||
@ -526,6 +567,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
kwargs: parameters to be passed to the calculate_rolling_returns() function
|
kwargs: parameters to be passed to the calculate_rolling_returns() function
|
||||||
|
Refer TimeSeries.calculate_rolling_returns() method for more details
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@ -536,19 +578,32 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
---------
|
---------
|
||||||
TimeSeries.calculate_rolling_returns()
|
TimeSeries.calculate_rolling_returns()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs["return_period_unit"] = kwargs.get("return_period_unit", self.frequency.freq_type)
|
kwargs["return_period_unit"] = kwargs.get("return_period_unit", self.frequency.freq_type)
|
||||||
kwargs["return_period_value"] = kwargs.get("return_period_value", 1)
|
kwargs["return_period_value"] = kwargs.get("return_period_value", 1)
|
||||||
kwargs["to_date"] = kwargs.get("to_date", self.end_date)
|
|
||||||
|
|
||||||
if kwargs.get("from_date", None) is None:
|
years = _interval_to_years(kwargs["return_period_unit"], kwargs["return_period_value"])
|
||||||
start_date = self.start_date + relativedelta(
|
if kwargs.get("annual_compounded_returns", True):
|
||||||
|
if years >= 1:
|
||||||
|
kwargs["annual_compounded_returns"] = True
|
||||||
|
annualise_returns = False
|
||||||
|
else:
|
||||||
|
kwargs["annual_compounded_returns"] = False
|
||||||
|
annualise_returns = True
|
||||||
|
elif not kwargs["annual_compounded_returns"]:
|
||||||
|
annualise_returns = False
|
||||||
|
|
||||||
|
if kwargs.get("from_date") is None:
|
||||||
|
kwargs["from_date"] = self.start_date + relativedelta(
|
||||||
**{kwargs["return_period_unit"]: kwargs["return_period_value"]}
|
**{kwargs["return_period_unit"]: kwargs["return_period_value"]}
|
||||||
)
|
)
|
||||||
kwargs["from_date"] = start_date
|
kwargs["to_date"] = kwargs.get("to_date", self.end_date)
|
||||||
|
|
||||||
rr = self.calculate_rolling_returns(**kwargs)
|
rr = self.calculate_rolling_returns(**kwargs)
|
||||||
return statistics.mean(rr.values)
|
mean_rr = statistics.mean(filter(lambda x: str(x) != "nan", rr.values))
|
||||||
|
if annualise_returns:
|
||||||
|
mean_rr = (1 + mean_rr) ** (1 / years) - 1
|
||||||
|
|
||||||
|
return mean_rr
|
||||||
|
|
||||||
def max_drawdown(self) -> MaxDrawdown:
|
def max_drawdown(self) -> MaxDrawdown:
|
||||||
"""Calculates the maximum fall the stock has taken between any two points.
|
"""Calculates the maximum fall the stock has taken between any two points.
|
||||||
@ -693,7 +748,10 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
)
|
)
|
||||||
|
|
||||||
closest: str = "previous" if method == "ffill" else "next"
|
closest: str = "previous" if method == "ffill" else "next"
|
||||||
new_ts: dict = {dt: self.get(dt, closest=closest)[1] for dt in new_dates}
|
new_ts = {}
|
||||||
|
for dt in new_dates:
|
||||||
|
new_ts.update({dt: self.get(dt, closest=closest)[1]})
|
||||||
|
# new_ts: dict = {dt: self.get(dt, closest=closest)[1] for dt in new_dates}
|
||||||
output_ts: TimeSeries = TimeSeries(new_ts, frequency=to_frequency.symbol)
|
output_ts: TimeSeries = TimeSeries(new_ts, frequency=to_frequency.symbol)
|
||||||
|
|
||||||
return output_ts
|
return output_ts
|
||||||
@ -704,14 +762,15 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
This will ensure that both time series have the same frequency and same set of dates.
|
This will ensure that both time series have the same frequency and same set of dates.
|
||||||
The frequency will be set to the higher of the two objects.
|
The frequency will be set to the higher of the two objects.
|
||||||
Dates will be taken from the class on which the method is called.
|
Dates will be taken from the class on which the method is called.
|
||||||
|
Values will be taken from the other class.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-----------
|
-----------
|
||||||
other: TimeSeries
|
other: TimeSeries
|
||||||
Another object of TimeSeries class whose dates need to be syncronized
|
Another object of TimeSeries class whose dates need to be synchronized
|
||||||
|
|
||||||
fill_method: ffill | bfill, default ffill
|
fill_method: ffill | bfill, default ffill
|
||||||
Method to use to fill missing values in time series when syncronizing
|
Method to use to fill missing values in time series when synchronizing
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
--------
|
--------
|
||||||
@ -740,8 +799,84 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
|
|
||||||
return self.__class__(new_other, frequency=other.frequency.symbol)
|
return self.__class__(new_other, frequency=other.frequency.symbol)
|
||||||
|
|
||||||
|
def mean(self) -> float:
|
||||||
|
"""Calculates the mean value of the time series data"""
|
||||||
|
|
||||||
def _preprocess_csv(file_path: str | pathlib.Path, delimiter: str = ",", encoding: str = "utf-8") -> List[list]:
|
return statistics.mean(self.values)
|
||||||
|
|
||||||
|
def transform(
|
||||||
|
self,
|
||||||
|
to_frequency: Literal["W", "M", "Q", "H", "Y"],
|
||||||
|
method: Literal["sum", "mean"],
|
||||||
|
eomonth: bool = False,
|
||||||
|
ensure_coverage: bool = True,
|
||||||
|
anchor_date=Literal["start", "end"],
|
||||||
|
) -> TimeSeries:
|
||||||
|
"""Transform a time series object into a lower frequency object with an aggregation function.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_frequency:
|
||||||
|
Frequency to which the time series needs to be transformed
|
||||||
|
|
||||||
|
method:
|
||||||
|
Aggregation method to be used. Can be either mean or sum
|
||||||
|
|
||||||
|
eomonth:
|
||||||
|
User end of month dates. Only applicable for frequencies monthly and lower.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Returns a TimeSeries object
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError:
|
||||||
|
* If invalid input is passed for frequency
|
||||||
|
* if invalid input is passed for method
|
||||||
|
* If to_frequency is higher than the current frequency
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
to_frequency: Frequency = getattr(AllFrequencies, to_frequency)
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(f"Invalid argument for to_frequency {to_frequency}")
|
||||||
|
|
||||||
|
if to_frequency.days <= self.frequency.days:
|
||||||
|
raise ValueError("TimeSeries can be only shrunk to a lower frequency")
|
||||||
|
|
||||||
|
if method not in ["sum", "mean"]:
|
||||||
|
raise ValueError(f"Method not recognised: {method}")
|
||||||
|
|
||||||
|
dates = create_date_series(
|
||||||
|
self.start_date,
|
||||||
|
self.end_date, # + relativedelta(days=to_frequency.days),
|
||||||
|
to_frequency.symbol,
|
||||||
|
ensure_coverage=ensure_coverage,
|
||||||
|
eomonth=eomonth,
|
||||||
|
)
|
||||||
|
# prev_date = dates[0]
|
||||||
|
|
||||||
|
new_ts_dict = {}
|
||||||
|
for idx, date in enumerate(dates):
|
||||||
|
if idx == 0:
|
||||||
|
cur_data = self[self.dates <= date]
|
||||||
|
else:
|
||||||
|
cur_data = self[(self.dates <= date) & (self.dates > dates[idx - 1])]
|
||||||
|
if method == "sum":
|
||||||
|
value = sum(cur_data.values)
|
||||||
|
elif method == "mean":
|
||||||
|
value = cur_data.mean()
|
||||||
|
|
||||||
|
new_ts_dict.update({date: value})
|
||||||
|
# prev_date = date
|
||||||
|
|
||||||
|
return self.__class__(new_ts_dict, to_frequency.symbol)
|
||||||
|
|
||||||
|
|
||||||
|
def _preprocess_csv(
|
||||||
|
file_path: str | pathlib.Path, delimiter: str = ",", encoding: str = "utf-8", **kwargs
|
||||||
|
) -> List[list]:
|
||||||
"""Preprocess csv data"""
|
"""Preprocess csv data"""
|
||||||
|
|
||||||
if isinstance(file_path, str):
|
if isinstance(file_path, str):
|
||||||
@ -751,7 +886,7 @@ def _preprocess_csv(file_path: str | pathlib.Path, delimiter: str = ",", encodin
|
|||||||
raise ValueError("File not found. Check the file path")
|
raise ValueError("File not found. Check the file path")
|
||||||
|
|
||||||
with open(file_path, "r", encoding=encoding) as file:
|
with open(file_path, "r", encoding=encoding) as file:
|
||||||
reader: csv.reader = csv.reader(file, delimiter=delimiter)
|
reader: csv.reader = csv.reader(file, delimiter=delimiter, **kwargs)
|
||||||
csv_data: list = list(reader)
|
csv_data: list = list(reader)
|
||||||
|
|
||||||
csv_data = [i for i in csv_data if i] # remove blank rows
|
csv_data = [i for i in csv_data if i] # remove blank rows
|
||||||
@ -772,8 +907,51 @@ def read_csv(
|
|||||||
nrows: int = -1,
|
nrows: int = -1,
|
||||||
delimiter: str = ",",
|
delimiter: str = ",",
|
||||||
encoding: str = "utf-8",
|
encoding: str = "utf-8",
|
||||||
) -> TimeSeriesCore:
|
**kwargs,
|
||||||
"""Reads Time Series data directly from a CSV file"""
|
) -> TimeSeries:
|
||||||
|
"""Reads Time Series data directly from a CSV file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
csv_file_pah:
|
||||||
|
path of the csv file to be read.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
frequency of the time series data.
|
||||||
|
|
||||||
|
date_format:
|
||||||
|
date format, specified as datetime compatible string
|
||||||
|
|
||||||
|
col_names:
|
||||||
|
specify the column headers to be read.
|
||||||
|
this parameter will allow you to read two columns from a CSV file which may have more columns.
|
||||||
|
this parameter overrides col_index parameter.
|
||||||
|
|
||||||
|
dol_index:
|
||||||
|
specify the column numbers to be read.
|
||||||
|
this parameter will allow you to read two columns from a CSV file which may have more columns.
|
||||||
|
if neither names nor index is specified, the first two columns from the csv file will be read,
|
||||||
|
with the first being treated as date.
|
||||||
|
|
||||||
|
has_header:
|
||||||
|
specify whether the file has a header row.
|
||||||
|
if true, the header row will be ignored while creating the time series data.
|
||||||
|
|
||||||
|
skip_rows:
|
||||||
|
the number of rows after the header which should be skipped.
|
||||||
|
|
||||||
|
nrows:
|
||||||
|
the number of rows to be read from the csv file.
|
||||||
|
|
||||||
|
delimiter:
|
||||||
|
specify the delimeter used in the csv file.
|
||||||
|
|
||||||
|
encoding:
|
||||||
|
specify the encoding of the csv file.
|
||||||
|
|
||||||
|
kwargs:
|
||||||
|
other keyword arguments to be passed on the csv.reader()
|
||||||
|
"""
|
||||||
|
|
||||||
data = _preprocess_csv(csv_file_path, delimiter, encoding)
|
data = _preprocess_csv(csv_file_path, delimiter, encoding)
|
||||||
|
|
||||||
@ -784,7 +962,7 @@ def read_csv(
|
|||||||
header = data[read_start_row]
|
header = data[read_start_row]
|
||||||
print(header)
|
print(header)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
# Black and pylance disagree on the foratting of the following line, hence formatting is disabled
|
# Black and pylance disagree on the formatting of the following line, hence formatting is disabled
|
||||||
data = data[(read_start_row + 1):read_end_row]
|
data = data[(read_start_row + 1):read_end_row]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
621
pyfacts/statistics.py
Normal file
621
pyfacts/statistics.py
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pyfacts.core import date_parser
|
||||||
|
|
||||||
|
from .pyfacts import TimeSeries, create_date_series
|
||||||
|
from .utils import _interval_to_years, _preprocess_from_to_date, covariance
|
||||||
|
|
||||||
|
# from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(3, 4)
|
||||||
|
def sharpe_ratio(
|
||||||
|
time_series_data: TimeSeries,
|
||||||
|
risk_free_data: TimeSeries = None,
|
||||||
|
risk_free_rate: float = None,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
|
return_period_value: int = 1,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> float:
|
||||||
|
"""Calculate the Sharpe ratio of any time series
|
||||||
|
|
||||||
|
Sharpe ratio is a measure of returns per unit of risk,
|
||||||
|
where risk is measured by the standard deviation of the returns.
|
||||||
|
|
||||||
|
The formula for Sharpe ratio is:
|
||||||
|
(average asset return - risk free rate)/volatility of asset returns
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
time_series_data:
|
||||||
|
The time series for which Sharpe ratio needs to be calculated
|
||||||
|
|
||||||
|
risk_free_data:
|
||||||
|
Risk free rates as time series data.
|
||||||
|
This should be the time series of risk free returns,
|
||||||
|
and not the underlying asset value.
|
||||||
|
|
||||||
|
risk_free_rate:
|
||||||
|
Risk free rate to be used.
|
||||||
|
Either risk_free_data or risk_free_rate needs to be provided.
|
||||||
|
If both are provided, the time series data will be used.
|
||||||
|
|
||||||
|
from_date:
|
||||||
|
Start date from which returns should be calculated.
|
||||||
|
Defaults to the first date of the series.
|
||||||
|
|
||||||
|
to_date:
|
||||||
|
End date till which returns should be calculated.
|
||||||
|
Defaults to the last date of the series.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
The frequency at which returns should be calculated.
|
||||||
|
|
||||||
|
return_period_unit: 'years', 'months', 'days'
|
||||||
|
The type of time period to use for return calculation.
|
||||||
|
|
||||||
|
return_period_value: int
|
||||||
|
The value of the specified interval type over which returns needs to be calculated.
|
||||||
|
|
||||||
|
as_on_match: str, optional
|
||||||
|
The mode of matching the as_on_date. Refer closest.
|
||||||
|
|
||||||
|
prior_match: str, optional
|
||||||
|
The mode of matching the prior_date. Refer closest.
|
||||||
|
|
||||||
|
closest: str, optional
|
||||||
|
The mode of matching the closest date.
|
||||||
|
Valid values are 'exact', 'previous', 'next' and next.
|
||||||
|
|
||||||
|
The date format to use for this operation.
|
||||||
|
Should be passed as a datetime library compatible string.
|
||||||
|
Sets the date format only for this operation. To set it globally, use FincalOptions.date_format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Value of Sharpe ratio as a float.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If risk free data or risk free rate is not provided.
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_days = math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365)
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
from_date = time_series_data.start_date + datetime.timedelta(days=interval_days)
|
||||||
|
if to_date is None:
|
||||||
|
to_date = time_series_data.end_date
|
||||||
|
|
||||||
|
if risk_free_data is None and risk_free_rate is None:
|
||||||
|
raise ValueError("At least one of risk_free_data or risk_free rate is required")
|
||||||
|
elif risk_free_data is not None:
|
||||||
|
risk_free_rate = risk_free_data.mean()
|
||||||
|
|
||||||
|
common_params = {
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"frequency": frequency,
|
||||||
|
"return_period_unit": return_period_unit,
|
||||||
|
"return_period_value": return_period_value,
|
||||||
|
"as_on_match": as_on_match,
|
||||||
|
"prior_match": prior_match,
|
||||||
|
"closest": closest,
|
||||||
|
"date_format": date_format,
|
||||||
|
}
|
||||||
|
average_rr = time_series_data.average_rolling_return(**common_params, annual_compounded_returns=True)
|
||||||
|
|
||||||
|
excess_returns = average_rr - risk_free_rate
|
||||||
|
sd = time_series_data.volatility(
|
||||||
|
**common_params,
|
||||||
|
annualize_volatility=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
sharpe_ratio_value = excess_returns / sd
|
||||||
|
return sharpe_ratio_value
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(2, 3)
|
||||||
|
def beta(
|
||||||
|
asset_data: TimeSeries,
|
||||||
|
market_data: TimeSeries,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
|
return_period_value: int = 1,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> float:
|
||||||
|
"""Beta is a measure of sensitivity of asset returns to market returns
|
||||||
|
|
||||||
|
The formula for beta is:
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset_data: TimeSeries
|
||||||
|
The time series data of the asset
|
||||||
|
|
||||||
|
market_data: TimeSeries
|
||||||
|
The time series data of the relevant market index
|
||||||
|
|
||||||
|
from_date:
|
||||||
|
Start date from which returns should be calculated.
|
||||||
|
Defaults to the first date of the series.
|
||||||
|
|
||||||
|
to_date:
|
||||||
|
End date till which returns should be calculated.
|
||||||
|
Defaults to the last date of the series.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
The frequency at which returns should be calculated.
|
||||||
|
|
||||||
|
return_period_unit: 'years', 'months', 'days'
|
||||||
|
The type of time period to use for return calculation.
|
||||||
|
|
||||||
|
return_period_value: int
|
||||||
|
The value of the specified interval type over which returns needs to be calculated.
|
||||||
|
|
||||||
|
as_on_match: str, optional
|
||||||
|
The mode of matching the as_on_date. Refer closest.
|
||||||
|
|
||||||
|
prior_match: str, optional
|
||||||
|
The mode of matching the prior_date. Refer closest.
|
||||||
|
|
||||||
|
closest: str, optional
|
||||||
|
The mode of matching the closest date.
|
||||||
|
Valid values are 'exact', 'previous', 'next' and next.
|
||||||
|
|
||||||
|
The date format to use for this operation.
|
||||||
|
Should be passed as a datetime library compatible string.
|
||||||
|
Sets the date format only for this operation. To set it globally, use FincalOptions.date_format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The value of beta as a float.
|
||||||
|
"""
|
||||||
|
interval_years = _interval_to_years(return_period_unit, return_period_value)
|
||||||
|
interval_days = math.ceil(interval_years * 365)
|
||||||
|
|
||||||
|
annual_compounded_returns = True if interval_years > 1 else False
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
from_date = asset_data.start_date + datetime.timedelta(days=interval_days)
|
||||||
|
if to_date is None:
|
||||||
|
to_date = asset_data.end_date
|
||||||
|
|
||||||
|
common_params = {
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"frequency": frequency,
|
||||||
|
"return_period_unit": return_period_unit,
|
||||||
|
"return_period_value": return_period_value,
|
||||||
|
"as_on_match": as_on_match,
|
||||||
|
"prior_match": prior_match,
|
||||||
|
"closest": closest,
|
||||||
|
"date_format": date_format,
|
||||||
|
"annual_compounded_returns": annual_compounded_returns,
|
||||||
|
}
|
||||||
|
|
||||||
|
asset_rr = asset_data.calculate_rolling_returns(**common_params)
|
||||||
|
market_rr = market_data.calculate_rolling_returns(**common_params)
|
||||||
|
|
||||||
|
cov = covariance(asset_rr.values, market_rr.values)
|
||||||
|
market_var = statistics.variance(market_rr.values)
|
||||||
|
|
||||||
|
beta = cov / market_var
|
||||||
|
return beta
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(4, 5)
|
||||||
|
def jensens_alpha(
|
||||||
|
asset_data: TimeSeries,
|
||||||
|
market_data: TimeSeries,
|
||||||
|
risk_free_data: TimeSeries = None,
|
||||||
|
risk_free_rate: float = None,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
|
return_period_value: int = 1,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
This function calculates the Jensen's alpha for a time series.
|
||||||
|
The formula for Jensen's alpha is:
|
||||||
|
Ri - Rf + B x (Rm - Rf)
|
||||||
|
where:
|
||||||
|
Ri = Realized return of the portfolio or investment
|
||||||
|
Rf = The risk free rate during the return time frame
|
||||||
|
B = Beta of the portfolio or investment
|
||||||
|
Rm = Realized return of the market index
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset_data: TimeSeries
|
||||||
|
The time series data of the asset
|
||||||
|
|
||||||
|
market_data: TimeSeries
|
||||||
|
The time series data of the relevant market index
|
||||||
|
|
||||||
|
risk_free_data:
|
||||||
|
Risk free rates as time series data.
|
||||||
|
This should be the time series of risk free returns,
|
||||||
|
and not the underlying asset value.
|
||||||
|
|
||||||
|
risk_free_rate:
|
||||||
|
Risk free rate to be used.
|
||||||
|
Either risk_free_data or risk_free_rate needs to be provided.
|
||||||
|
If both are provided, the time series data will be used.
|
||||||
|
|
||||||
|
from_date:
|
||||||
|
Start date from which returns should be calculated.
|
||||||
|
Defaults to the first date of the series.
|
||||||
|
|
||||||
|
to_date:
|
||||||
|
End date till which returns should be calculated.
|
||||||
|
Defaults to the last date of the series.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
The frequency at which returns should be calculated.
|
||||||
|
|
||||||
|
return_period_unit: 'years', 'months', 'days'
|
||||||
|
The type of time period to use for return calculation.
|
||||||
|
|
||||||
|
return_period_value: int
|
||||||
|
The value of the specified interval type over which returns needs to be calculated.
|
||||||
|
|
||||||
|
as_on_match: str, optional
|
||||||
|
The mode of matching the as_on_date. Refer closest.
|
||||||
|
|
||||||
|
prior_match: str, optional
|
||||||
|
The mode of matching the prior_date. Refer closest.
|
||||||
|
|
||||||
|
closest: str, optional
|
||||||
|
The mode of matching the closest date.
|
||||||
|
Valid values are 'exact', 'previous', 'next' and next.
|
||||||
|
|
||||||
|
The date format to use for this operation.
|
||||||
|
Should be passed as a datetime library compatible string.
|
||||||
|
Sets the date format only for this operation. To set it globally, use FincalOptions.date_format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The value of Jensen's alpha as a float.
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_years = _interval_to_years(return_period_unit, return_period_value)
|
||||||
|
interval_days = math.ceil(interval_years * 365)
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
from_date = asset_data.start_date + datetime.timedelta(days=interval_days)
|
||||||
|
if to_date is None:
|
||||||
|
to_date = asset_data.end_date
|
||||||
|
|
||||||
|
common_params = {
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"frequency": frequency,
|
||||||
|
"return_period_unit": return_period_unit,
|
||||||
|
"return_period_value": return_period_value,
|
||||||
|
"as_on_match": as_on_match,
|
||||||
|
"prior_match": prior_match,
|
||||||
|
"closest": closest,
|
||||||
|
"date_format": date_format,
|
||||||
|
}
|
||||||
|
|
||||||
|
num_days = (to_date - from_date).days
|
||||||
|
compound_realised_returns = True if num_days > 365 else False
|
||||||
|
realized_return = asset_data.calculate_returns(
|
||||||
|
as_on=to_date,
|
||||||
|
return_period_unit="days",
|
||||||
|
return_period_value=num_days,
|
||||||
|
annual_compounded_returns=compound_realised_returns,
|
||||||
|
as_on_match=as_on_match,
|
||||||
|
prior_match=prior_match,
|
||||||
|
closest=closest,
|
||||||
|
date_format=date_format,
|
||||||
|
)
|
||||||
|
market_return = market_data.calculate_returns(
|
||||||
|
as_on=to_date,
|
||||||
|
return_period_unit="days",
|
||||||
|
return_period_value=num_days,
|
||||||
|
annual_compounded_returns=compound_realised_returns,
|
||||||
|
as_on_match=as_on_match,
|
||||||
|
prior_match=prior_match,
|
||||||
|
closest=closest,
|
||||||
|
date_format=date_format,
|
||||||
|
)
|
||||||
|
beta_value = beta(asset_data=asset_data, market_data=market_data, **common_params)
|
||||||
|
|
||||||
|
if risk_free_data is None and risk_free_rate is None:
|
||||||
|
raise ValueError("At least one of risk_free_data or risk_free rate is required")
|
||||||
|
elif risk_free_data is not None:
|
||||||
|
risk_free_rate = risk_free_data.mean()
|
||||||
|
|
||||||
|
jensens_alpha = realized_return[1] - risk_free_rate + beta_value * (market_return[1] - risk_free_rate)
|
||||||
|
return jensens_alpha
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(2, 3)
|
||||||
|
def correlation(
|
||||||
|
data1: TimeSeries,
|
||||||
|
data2: TimeSeries,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
|
return_period_value: int = 1,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> float:
|
||||||
|
"""Calculate the correlation between two assets
|
||||||
|
|
||||||
|
correlation calculation is done based on rolling returns.
|
||||||
|
It must be noted that correlation is not calculated directly on the asset prices.
|
||||||
|
The asset prices used to calculate returns and correlation is then calculated based on these returns.
|
||||||
|
Hence this function requires all parameters for rolling returns calculations.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data1: TimeSeries
|
||||||
|
The first time series data
|
||||||
|
|
||||||
|
data2: TimeSeries
|
||||||
|
The second time series data
|
||||||
|
|
||||||
|
from_date:
|
||||||
|
Start date from which returns should be calculated.
|
||||||
|
Defaults to the first date of the series.
|
||||||
|
|
||||||
|
to_date:
|
||||||
|
End date till which returns should be calculated.
|
||||||
|
Defaults to the last date of the series.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
The frequency at which returns should be calculated.
|
||||||
|
|
||||||
|
return_period_unit: 'years', 'months', 'days'
|
||||||
|
The type of time period to use for return calculation.
|
||||||
|
|
||||||
|
return_period_value: int
|
||||||
|
The value of the specified interval type over which returns needs to be calculated.
|
||||||
|
|
||||||
|
as_on_match: str, optional
|
||||||
|
The mode of matching the as_on_date. Refer closest.
|
||||||
|
|
||||||
|
prior_match: str, optional
|
||||||
|
The mode of matching the prior_date. Refer closest.
|
||||||
|
|
||||||
|
closest: str, optional
|
||||||
|
The mode of matching the closest date.
|
||||||
|
Valid values are 'exact', 'previous', 'next' and next.
|
||||||
|
|
||||||
|
The date format to use for this operation.
|
||||||
|
Should be passed as a datetime library compatible string.
|
||||||
|
Sets the date format only for this operation. To set it globally, use FincalOptions.date_format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The value of beta as a float.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError:
|
||||||
|
* If frequency of both TimeSeries do not match
|
||||||
|
* If both time series do not have data between the from date and to date
|
||||||
|
"""
|
||||||
|
interval_years = _interval_to_years(return_period_unit, return_period_value)
|
||||||
|
interval_days = math.ceil(interval_years * 365)
|
||||||
|
|
||||||
|
annual_compounded_returns = True if interval_years > 1 else False
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
from_date = data1.start_date + datetime.timedelta(days=interval_days)
|
||||||
|
if to_date is None:
|
||||||
|
to_date = data1.end_date
|
||||||
|
|
||||||
|
if data1.frequency != data2.frequency:
|
||||||
|
raise ValueError("Correlation calculation requires both time series to be of same frequency")
|
||||||
|
|
||||||
|
if from_date < data2.start_date or to_date > data2.end_date:
|
||||||
|
raise ValueError("Data between from_date and to_date must be present in both time series")
|
||||||
|
|
||||||
|
common_params = {
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"frequency": frequency,
|
||||||
|
"return_period_unit": return_period_unit,
|
||||||
|
"return_period_value": return_period_value,
|
||||||
|
"as_on_match": as_on_match,
|
||||||
|
"prior_match": prior_match,
|
||||||
|
"closest": closest,
|
||||||
|
"date_format": date_format,
|
||||||
|
"annual_compounded_returns": annual_compounded_returns,
|
||||||
|
}
|
||||||
|
|
||||||
|
asset_rr = data1.calculate_rolling_returns(**common_params)
|
||||||
|
market_rr = data2.calculate_rolling_returns(**common_params)
|
||||||
|
|
||||||
|
cor = statistics.correlation(asset_rr.values, market_rr.values)
|
||||||
|
return cor
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(3, 4)
|
||||||
|
def sortino_ratio(
|
||||||
|
time_series_data: TimeSeries,
|
||||||
|
risk_free_data: TimeSeries = None,
|
||||||
|
risk_free_rate: float = None,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = "years",
|
||||||
|
return_period_value: int = 1,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> float:
|
||||||
|
"""Calculate the Sortino ratio of any time series
|
||||||
|
|
||||||
|
Sortino ratio is a variation of the Sharpe ratio,
|
||||||
|
where risk is measured as standard deviation of negative returns only.
|
||||||
|
Since deviation on the positive side is not undesirable, hence sortino ratio excludes positive deviations.
|
||||||
|
|
||||||
|
The formula for Sortino ratio is:
|
||||||
|
(average asset return - risk free rate)/volatility of negative asset returns
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
time_series_data:
|
||||||
|
The time series for which Sharpe ratio needs to be calculated
|
||||||
|
|
||||||
|
risk_free_data:
|
||||||
|
Risk free rates as time series data.
|
||||||
|
This should be the time series of risk free returns,
|
||||||
|
and not the underlying asset value.
|
||||||
|
|
||||||
|
risk_free_rate:
|
||||||
|
Risk free rate to be used.
|
||||||
|
Either risk_free_data or risk_free_rate needs to be provided.
|
||||||
|
If both are provided, the time series data will be used.
|
||||||
|
|
||||||
|
from_date:
|
||||||
|
Start date from which returns should be calculated.
|
||||||
|
Defaults to the first date of the series.
|
||||||
|
|
||||||
|
to_date:
|
||||||
|
End date till which returns should be calculated.
|
||||||
|
Defaults to the last date of the series.
|
||||||
|
|
||||||
|
frequency:
|
||||||
|
The frequency at which returns should be calculated.
|
||||||
|
|
||||||
|
return_period_unit: 'years', 'months', 'days'
|
||||||
|
The type of time period to use for return calculation.
|
||||||
|
|
||||||
|
return_period_value: int
|
||||||
|
The value of the specified interval type over which returns needs to be calculated.
|
||||||
|
|
||||||
|
as_on_match: str, optional
|
||||||
|
The mode of matching the as_on_date. Refer closest.
|
||||||
|
|
||||||
|
prior_match: str, optional
|
||||||
|
The mode of matching the prior_date. Refer closest.
|
||||||
|
|
||||||
|
closest: str, optional
|
||||||
|
The mode of matching the closest date.
|
||||||
|
Valid values are 'exact', 'previous', 'next' and next.
|
||||||
|
|
||||||
|
The date format to use for this operation.
|
||||||
|
Should be passed as a datetime library compatible string.
|
||||||
|
Sets the date format only for this operation. To set it globally, use FincalOptions.date_format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Value of Sortino ratio as a float.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If risk free data or risk free rate is not provided.
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_days = math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365)
|
||||||
|
|
||||||
|
# if from_date is None:
|
||||||
|
# from_date = time_series_data.start_date + relativedelta(**{return_period_unit: return_period_value})
|
||||||
|
# if to_date is None:
|
||||||
|
# to_date = time_series_data.end_date
|
||||||
|
from_date, to_date = _preprocess_from_to_date(
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
time_series_data,
|
||||||
|
False,
|
||||||
|
return_period_unit,
|
||||||
|
return_period_value,
|
||||||
|
as_on_match,
|
||||||
|
prior_match,
|
||||||
|
closest,
|
||||||
|
)
|
||||||
|
|
||||||
|
if risk_free_data is None and risk_free_rate is None:
|
||||||
|
raise ValueError("At least one of risk_free_data or risk_free rate is required")
|
||||||
|
elif risk_free_data is not None:
|
||||||
|
risk_free_rate = risk_free_data.mean()
|
||||||
|
|
||||||
|
common_params = {
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"frequency": frequency,
|
||||||
|
"return_period_unit": return_period_unit,
|
||||||
|
"return_period_value": return_period_value,
|
||||||
|
"as_on_match": as_on_match,
|
||||||
|
"prior_match": prior_match,
|
||||||
|
"closest": closest,
|
||||||
|
"date_format": date_format,
|
||||||
|
}
|
||||||
|
average_rr_ts = time_series_data.calculate_rolling_returns(
|
||||||
|
**common_params, annual_compounded_returns=False, if_not_found="nan"
|
||||||
|
)
|
||||||
|
average_rr = statistics.mean(filter(lambda x: str(x) != "nan", average_rr_ts.values))
|
||||||
|
annualized_average_rr = (1 + average_rr) ** (365 / interval_days) - 1
|
||||||
|
|
||||||
|
excess_returns = annualized_average_rr - risk_free_rate
|
||||||
|
my_list = [i for i in average_rr_ts.values if i < 0]
|
||||||
|
sd = statistics.stdev(my_list) # [i for i in average_rr_ts.values if i < 0])
|
||||||
|
sd *= math.sqrt(365 / interval_days)
|
||||||
|
|
||||||
|
sortino_ratio_value = excess_returns / sd
|
||||||
|
return sortino_ratio_value
|
||||||
|
|
||||||
|
|
||||||
|
@date_parser(3, 4)
|
||||||
|
def moving_average(
|
||||||
|
time_series_data: TimeSeries,
|
||||||
|
moving_average_period_unit: Literal["years", "months", "days"],
|
||||||
|
moving_average_period_value: int,
|
||||||
|
from_date: str | datetime.datetime = None,
|
||||||
|
to_date: str | datetime.datetime = None,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next"] = "previous",
|
||||||
|
date_format: str = None,
|
||||||
|
) -> TimeSeries:
|
||||||
|
|
||||||
|
from_date, to_date = _preprocess_from_to_date(
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
time_series_data,
|
||||||
|
False,
|
||||||
|
return_period_unit=moving_average_period_unit,
|
||||||
|
return_period_value=moving_average_period_value,
|
||||||
|
as_on_match=as_on_match,
|
||||||
|
prior_match=prior_match,
|
||||||
|
closest=closest,
|
||||||
|
)
|
||||||
|
|
||||||
|
dates = create_date_series(from_date, to_date, time_series_data.frequency.symbol)
|
||||||
|
|
||||||
|
for date in dates:
|
||||||
|
start_date = date - datetime.timedelta(**{moving_average_period_unit: moving_average_period_value})
|
||||||
|
time_series_data[start_date:date]
|
@ -1,14 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import statistics
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Literal, Mapping, Sequence, Tuple
|
from typing import List, Literal, Mapping, Sequence, Tuple
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from .exceptions import DateNotFoundError, DateOutOfRangeError
|
from .exceptions import DateNotFoundError, DateOutOfRangeError
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FincalOptions:
|
class PyfactsOptions:
|
||||||
date_format: str = "%Y-%m-%d"
|
date_format: str = "%Y-%m-%d"
|
||||||
closest: str = "before" # after
|
closest: str = "previous" # next
|
||||||
traded_days: int = 365
|
traded_days: int = 365
|
||||||
get_closest: str = "exact"
|
get_closest: str = "exact"
|
||||||
|
|
||||||
@ -40,7 +45,7 @@ def _parse_date(date: str, date_format: str = None) -> datetime.datetime:
|
|||||||
return datetime.datetime.fromordinal(date.toordinal())
|
return datetime.datetime.fromordinal(date.toordinal())
|
||||||
|
|
||||||
if date_format is None:
|
if date_format is None:
|
||||||
date_format = FincalOptions.date_format
|
date_format = PyfactsOptions.date_format
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date = datetime.datetime.strptime(date, date_format)
|
date = datetime.datetime.strptime(date, date_format)
|
||||||
@ -139,23 +144,63 @@ def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str)
|
|||||||
return as_on_delta, prior_delta
|
return as_on_delta, prior_delta
|
||||||
|
|
||||||
|
|
||||||
|
def _preprocess_from_to_date(
|
||||||
|
from_date: datetime.date | str,
|
||||||
|
to_date: datetime.date | str,
|
||||||
|
time_series: Mapping = None,
|
||||||
|
align_dates: bool = True,
|
||||||
|
return_period_unit: Literal["years", "months", "days"] = None,
|
||||||
|
return_period_value: int = None,
|
||||||
|
as_on_match: str = "closest",
|
||||||
|
prior_match: str = "closest",
|
||||||
|
closest: Literal["previous", "next", "exact"] = "previous",
|
||||||
|
) -> tuple:
|
||||||
|
|
||||||
|
as_on_match, prior_match = _preprocess_match_options(as_on_match, prior_match, closest)
|
||||||
|
|
||||||
|
if (from_date is None or to_date is None) and time_series is None:
|
||||||
|
raise ValueError("Provide either to_date and from_date or time_series data")
|
||||||
|
|
||||||
|
if time_series is not None and (return_period_unit is None or return_period_value is None):
|
||||||
|
raise ValueError("Provide return period for calculation of from_date")
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
expected_start_date = time_series.start_date + relativedelta(**{return_period_unit: return_period_value})
|
||||||
|
from_date = _find_closest_date(time_series, expected_start_date, 999, as_on_match, "fail")[0]
|
||||||
|
|
||||||
|
if to_date is None:
|
||||||
|
to_date = time_series.end_date
|
||||||
|
|
||||||
|
return from_date, to_date
|
||||||
|
|
||||||
|
|
||||||
def _find_closest_date(
|
def _find_closest_date(
|
||||||
data: Mapping[datetime.datetime, float],
|
data: Mapping[datetime.datetime, float],
|
||||||
date: datetime.datetime,
|
date: datetime.datetime,
|
||||||
limit_days: int,
|
limit_days: int,
|
||||||
delta: datetime.timedelta,
|
delta: datetime.timedelta,
|
||||||
if_not_found: Literal["fail", "nan"],
|
if_not_found: Literal["fail", "nan"],
|
||||||
):
|
) -> Tuple[datetime.datetime, float]:
|
||||||
"""Helper function to find data for the closest available date"""
|
"""Helper function to find data for the closest available date
|
||||||
|
|
||||||
if delta.days < 0 and date < min(data):
|
data:
|
||||||
|
TimeSeries data
|
||||||
|
"""
|
||||||
|
|
||||||
|
if delta.days < 0 and date < min(data.data):
|
||||||
|
if if_not_found == "nan":
|
||||||
|
return float("NaN"), float("NaN")
|
||||||
|
else:
|
||||||
raise DateOutOfRangeError(date, "min")
|
raise DateOutOfRangeError(date, "min")
|
||||||
if delta.days > 0 and date > max(data):
|
if delta.days > 0 and date > max(data.data):
|
||||||
|
if if_not_found == "nan":
|
||||||
|
return float("NaN"), float("NaN")
|
||||||
|
else:
|
||||||
raise DateOutOfRangeError(date, "max")
|
raise DateOutOfRangeError(date, "max")
|
||||||
|
|
||||||
row: tuple = data.get(date, None)
|
row: tuple = data.get(date, None)
|
||||||
if row is not None:
|
if row is not None:
|
||||||
return date, row
|
return row
|
||||||
|
|
||||||
if delta and limit_days != 0:
|
if delta and limit_days != 0:
|
||||||
return _find_closest_date(data, date + delta, limit_days - 1, delta, if_not_found)
|
return _find_closest_date(data, date + delta, limit_days - 1, delta, if_not_found)
|
||||||
@ -174,3 +219,47 @@ def _interval_to_years(interval_type: Literal["years", "months", "day"], interva
|
|||||||
year_conversion_factor: dict = {"years": 1, "months": 12, "days": 365}
|
year_conversion_factor: dict = {"years": 1, "months": 12, "days": 365}
|
||||||
years: float = interval_value / year_conversion_factor[interval_type]
|
years: float = interval_value / year_conversion_factor[interval_type]
|
||||||
return years
|
return years
|
||||||
|
|
||||||
|
|
||||||
|
def _is_eomonth(dates: Sequence[datetime.datetime], threshold: float = 0.7):
|
||||||
|
"""Checks if a series is should be treated as end of month date series or not.
|
||||||
|
|
||||||
|
If eomonth dates exceed threshold percentage, it will be treated as eomonth series.
|
||||||
|
This can be used for any frequency, but will work only for monthly and lower frequencies.
|
||||||
|
"""
|
||||||
|
eomonth_dates = [date.month != (date + relativedelta(days=1)).month for date in dates]
|
||||||
|
eomonth_proportion = sum(eomonth_dates) / len(dates)
|
||||||
|
return eomonth_proportion > threshold
|
||||||
|
|
||||||
|
|
||||||
|
def covariance(series1: list, series2: list) -> float:
|
||||||
|
"""Returns the covariance of two series
|
||||||
|
|
||||||
|
This is a compatibility function for Python versions prior to 3.10.
|
||||||
|
It will be replaced with statistics.covariance when support is dropped for versions <3.10.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
series1 : List
|
||||||
|
A list of numbers
|
||||||
|
series2 : list
|
||||||
|
A list of numbers
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Returns the covariance as a float value
|
||||||
|
"""
|
||||||
|
|
||||||
|
n = len(series1)
|
||||||
|
if len(series2) != n:
|
||||||
|
raise ValueError("Lenght of both series must be same for covariance calcualtion.")
|
||||||
|
if n < 2:
|
||||||
|
raise ValueError("At least two data poitns are required for covariance calculation.")
|
||||||
|
|
||||||
|
mean1 = statistics.mean(series1)
|
||||||
|
mean2 = statistics.mean(series2)
|
||||||
|
|
||||||
|
xy = sum([(x - mean1) * (y - mean2) for x, y in zip(series1, series2)])
|
||||||
|
|
||||||
|
return xy / n
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
12
setup.py
12
setup.py
@ -2,21 +2,17 @@ from setuptools import find_packages, setup
|
|||||||
|
|
||||||
license = open("LICENSE").read().strip()
|
license = open("LICENSE").read().strip()
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="Fincal",
|
name="pyfacts",
|
||||||
version='0.0.1',
|
version="0.0.1",
|
||||||
license=license,
|
license=license,
|
||||||
author="Gourav Kumar",
|
author="Gourav Kumar",
|
||||||
author_email="gouravkr@outlook.in",
|
author_email="gouravkr@outlook.in",
|
||||||
url="https://gouravkumar.com",
|
url="https://gouravkumar.com",
|
||||||
description="A library which makes handling time series data easier",
|
description="A Python library to perform financial analytics on Time Series data",
|
||||||
long_description=open("README.md").read().strip(),
|
long_description=open("README.md").read().strip(),
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=["python-dateutil"],
|
install_requires=["python-dateutil"],
|
||||||
test_suite="tests",
|
test_suite="tests",
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"fincal=fincal.__main__:main",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
29
test.py
29
test.py
@ -1,29 +0,0 @@
|
|||||||
# from fincal.core import FincalOptions
|
|
||||||
from fincal.fincal import TimeSeries
|
|
||||||
|
|
||||||
data = [
|
|
||||||
("2022-01-01", 10),
|
|
||||||
("2022-01-02", 12),
|
|
||||||
("2022-01-03", 14),
|
|
||||||
("2022-01-04", 16),
|
|
||||||
("2022-01-06", 18),
|
|
||||||
("2022-01-07", 20),
|
|
||||||
("2022-01-09", 22),
|
|
||||||
("2022-01-10", 24),
|
|
||||||
("2022-01-11", 26),
|
|
||||||
("2022-01-13", 28),
|
|
||||||
("2022-01-14", 30),
|
|
||||||
("2022-01-15", 32),
|
|
||||||
("2022-01-16", 34),
|
|
||||||
]
|
|
||||||
ts = TimeSeries(data, frequency="D")
|
|
||||||
print(ts)
|
|
||||||
|
|
||||||
data = [("2022-01-01", 220), ("2022-01-08", 230), ("2022-01-15", 240)]
|
|
||||||
ts2 = TimeSeries(data, frequency="W")
|
|
||||||
print(ts2)
|
|
||||||
|
|
||||||
synced_ts = ts.sync(ts2)
|
|
||||||
print("---------\n")
|
|
||||||
for i in synced_ts:
|
|
||||||
print(i)
|
|
52
test2.py
52
test2.py
@ -1,52 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from fincal.fincal import TimeSeries
|
|
||||||
|
|
||||||
# start = time.time()
|
|
||||||
# dfd = pd.read_csv("test_files/msft.csv") # , dtype=dict(nav=str))
|
|
||||||
# # dfd = dfd[dfd["amfi_code"] == 118825].reset_index(drop=True)
|
|
||||||
# print("instantiation took", round((time.time() - start) * 1000, 2), "ms")
|
|
||||||
# ts = TimeSeries([(i.date, i.nav) for i in dfd.itertuples()], frequency="D")
|
|
||||||
# print(repr(ts))
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
# mdd = ts.max_drawdown()
|
|
||||||
# print(mdd)
|
|
||||||
# print("max drawdown calc took", round((time.time() - start) * 1000, 2), "ms")
|
|
||||||
# # print(ts[['2022-01-31', '2021-05-28']])
|
|
||||||
|
|
||||||
# rr = ts.calculate_rolling_returns(
|
|
||||||
# from_date='2021-01-01',
|
|
||||||
# to_date='2022-01-01',
|
|
||||||
# frequency='D',
|
|
||||||
# interval_type='days',
|
|
||||||
# interval_value=30,
|
|
||||||
# compounding=False
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
data = [
|
|
||||||
("2022-01-01", 10),
|
|
||||||
# ("2022-01-08", 12),
|
|
||||||
("2022-01-15", 14),
|
|
||||||
("2022-01-22", 16)
|
|
||||||
# ("2020-02-07", 18),
|
|
||||||
# ("2020-02-14", 20),
|
|
||||||
# ("2020-02-21", 22),
|
|
||||||
# ("2020-02-28", 24),
|
|
||||||
# ("2020-03-01", 26),
|
|
||||||
# ("2020-03-01", 28),
|
|
||||||
# ("2020-03-01", 30),
|
|
||||||
# ("2020-03-01", 32),
|
|
||||||
# ("2021-03-01", 34),
|
|
||||||
]
|
|
||||||
|
|
||||||
ts = TimeSeries(data, "W")
|
|
||||||
# ts_expanded = ts.expand("D", "ffill", skip_weekends=True)
|
|
||||||
|
|
||||||
# for i in ts_expanded:
|
|
||||||
# print(i)
|
|
||||||
|
|
||||||
print(ts.get("2022-01-01"))
|
|
||||||
|
|
||||||
print(ts.ffill())
|
|
File diff suppressed because it is too large
Load Diff
@ -1,139 +0,0 @@
|
|||||||
amfi_code,date,nav
|
|
||||||
118825,01-11-2021,87.925
|
|
||||||
119528,02-11-2021,378.51
|
|
||||||
118825,02-11-2021,87.885
|
|
||||||
119528,03-11-2021,377.79
|
|
||||||
118825,03-11-2021,87.553
|
|
||||||
119528,08-11-2021,383.13
|
|
||||||
118825,08-11-2021,88.743
|
|
||||||
119528,09-11-2021,383.06
|
|
||||||
118825,09-11-2021,88.793
|
|
||||||
119528,10-11-2021,382.71
|
|
||||||
118825,10-11-2021,88.723
|
|
||||||
118825,10-11-2021,88.78
|
|
||||||
119528,11-11-2021,379.28
|
|
||||||
118825,11-11-2021,88.205
|
|
||||||
119528,12-11-2021,383.94
|
|
||||||
118825,12-11-2021,89.025
|
|
||||||
119528,15-11-2021,383.31
|
|
||||||
118825,15-11-2021,89.182
|
|
||||||
119528,16-11-2021,381.08
|
|
||||||
118825,16-11-2021,88.569
|
|
||||||
119528,17-11-2021,379.17
|
|
||||||
118825,17-11-2021,88.09
|
|
||||||
119528,18-11-2021,375.09
|
|
||||||
118825,18-11-2021,87.202
|
|
||||||
119528,22-11-2021,368.16
|
|
||||||
118825,22-11-2021,85.382
|
|
||||||
119528,23-11-2021,370.64
|
|
||||||
118825,23-11-2021,85.978
|
|
||||||
119528,24-11-2021,369.91
|
|
||||||
118825,24-11-2021,85.635
|
|
||||||
119528,25-11-2021,371.33
|
|
||||||
118825,25-11-2021,86.212
|
|
||||||
119528,26-11-2021,360.66
|
|
||||||
118825,26-11-2021,83.748
|
|
||||||
119528,29-11-2021,360.05
|
|
||||||
118825,29-11-2021,83.523
|
|
||||||
119528,30-11-2021,359.8
|
|
||||||
118825,30-11-2021,83.475
|
|
||||||
119528,01-12-2021,362.35
|
|
||||||
118825,01-12-2021,84.269
|
|
||||||
119528,02-12-2021,366.09
|
|
||||||
118825,02-12-2021,85.105
|
|
||||||
119528,03-12-2021,363.11
|
|
||||||
118825,03-12-2021,84.507
|
|
||||||
119528,06-12-2021,357.21
|
|
||||||
118825,06-12-2021,83.113
|
|
||||||
119528,07-12-2021,362.63
|
|
||||||
118825,07-12-2021,84.429
|
|
||||||
119528,08-12-2021,368.73
|
|
||||||
118825,08-12-2021,85.935
|
|
||||||
119528,09-12-2021,369.49
|
|
||||||
118825,09-12-2021,86.045
|
|
||||||
119528,10-12-2021,369.44
|
|
||||||
118825,10-12-2021,86.058
|
|
||||||
119528,13-12-2021,367.6
|
|
||||||
118825,13-12-2021,85.632
|
|
||||||
119528,14-12-2021,366.36
|
|
||||||
118825,14-12-2021,85.502
|
|
||||||
119528,15-12-2021,364.34
|
|
||||||
118825,15-12-2021,84.989
|
|
||||||
119528,16-12-2021,363.73
|
|
||||||
118825,16-12-2021,84.972
|
|
||||||
119528,17-12-2021,358.17
|
|
||||||
118825,17-12-2021,83.83
|
|
||||||
119528,20-12-2021,349.98
|
|
||||||
118825,20-12-2021,81.817
|
|
||||||
119528,21-12-2021,353.71
|
|
||||||
118825,21-12-2021,82.746
|
|
||||||
119528,22-12-2021,357.93
|
|
||||||
118825,22-12-2021,83.776
|
|
||||||
119528,23-12-2021,360.68
|
|
||||||
118825,23-12-2021,84.297
|
|
||||||
119528,24-12-2021,359.11
|
|
||||||
118825,24-12-2021,83.903
|
|
||||||
119528,27-12-2021,360.71
|
|
||||||
118825,27-12-2021,84.227
|
|
||||||
119528,28-12-2021,363.81
|
|
||||||
118825,28-12-2021,85.044
|
|
||||||
119528,29-12-2021,363.2
|
|
||||||
118825,29-12-2021,85.03
|
|
||||||
119528,30-12-2021,363.31
|
|
||||||
118825,30-12-2021,85.047
|
|
||||||
119528,31-12-2021,366.98
|
|
||||||
118825,31-12-2021,85.759
|
|
||||||
119528,03-01-2022,371.76
|
|
||||||
118825,03-01-2022,87.111
|
|
||||||
119528,04-01-2022,374.22
|
|
||||||
118825,04-01-2022,87.804
|
|
||||||
119528,05-01-2022,376.31
|
|
||||||
118825,05-01-2022,88.162
|
|
||||||
119528,06-01-2022,373.64
|
|
||||||
118825,06-01-2022,87.541
|
|
||||||
119528,07-01-2022,374.68
|
|
||||||
118825,07-01-2022,87.818
|
|
||||||
119528,10-01-2022,378.47
|
|
||||||
118825,10-01-2022,88.622
|
|
||||||
119528,11-01-2022,379.34
|
|
||||||
118825,11-01-2022,88.678
|
|
||||||
119528,12-01-2022,382.86
|
|
||||||
118825,12-01-2022,89.332
|
|
||||||
119528,13-01-2022,383.68
|
|
||||||
118825,13-01-2022,89.553
|
|
||||||
119528,14-01-2022,384.02
|
|
||||||
118825,14-01-2022,89.729
|
|
||||||
119528,17-01-2022,384.36
|
|
||||||
118825,17-01-2022,89.733
|
|
||||||
119528,18-01-2022,380
|
|
||||||
118825,18-01-2022,88.781
|
|
||||||
119528,19-01-2022,377.24
|
|
||||||
118825,19-01-2022,88.059
|
|
||||||
119528,20-01-2022,374.45
|
|
||||||
118825,20-01-2022,87.361
|
|
||||||
119528,21-01-2022,369.86
|
|
||||||
118825,21-01-2022,86.22
|
|
||||||
119528,24-01-2022,361.01
|
|
||||||
118825,24-01-2022,83.907
|
|
||||||
119528,25-01-2022,364.63
|
|
||||||
118825,25-01-2022,84.763
|
|
||||||
119528,27-01-2022,361.95
|
|
||||||
118825,27-01-2022,83.876
|
|
||||||
119528,28-01-2022,361.91
|
|
||||||
118825,28-01-2022,83.829
|
|
||||||
119528,31-01-2022,367.31
|
|
||||||
118825,31-01-2022,85.18
|
|
||||||
119528,04-02-2022,371.01
|
|
||||||
118825,04-02-2022,86.079
|
|
||||||
119528,07-02-2022,365.04
|
|
||||||
118825,07-02-2022,84.867
|
|
||||||
119528,08-02-2022,365.74
|
|
||||||
118825,08-02-2022,84.945
|
|
||||||
119528,09-02-2022,369.85
|
|
||||||
118825,09-02-2022,85.977
|
|
||||||
119528,10-02-2022,372.29
|
|
||||||
118825,10-02-2022,86.5
|
|
||||||
119528,11-02-2022,366.91
|
|
||||||
118825,11-02-2022,85.226
|
|
||||||
119528,14-02-2022,355.47
|
|
||||||
118825,14-02-2022,82.533
|
|
|
@ -1,11 +0,0 @@
|
|||||||
amfi_code,date,nav
|
|
||||||
118825,31-03-2021,70.69
|
|
||||||
118825,30-04-2021,70.39
|
|
||||||
118825,31-05-2021,74.85
|
|
||||||
118825,30-07-2021,78.335
|
|
||||||
118825,31-08-2021,83.691
|
|
||||||
118825,30-09-2021,86.128
|
|
||||||
118825,29-10-2021,86.612
|
|
||||||
118825,30-11-2021,83.475
|
|
||||||
118825,31-01-2022,85.18
|
|
||||||
118825,17-02-2022,84.33
|
|
|
7560
test_files/msft.csv
7560
test_files/msft.csv
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,139 +0,0 @@
|
|||||||
amfi_code,date,nav
|
|
||||||
118825,01-11-2021,87.925
|
|
||||||
119528,02-11-2021,378.51
|
|
||||||
118825,02-11-2021,87.885
|
|
||||||
119528,03-11-2021,377.79
|
|
||||||
118825,03-11-2021,87.553
|
|
||||||
119528,08-11-2021,383.13
|
|
||||||
118825,08-11-2021,88.743
|
|
||||||
119528,09-11-2021,383.06
|
|
||||||
118825,09-11-2021,88.793
|
|
||||||
119528,10-11-2021,382.71
|
|
||||||
118825,10-11-2021,88.723
|
|
||||||
118825,10-11-2021,88.78
|
|
||||||
119528,11-11-2021,379.28
|
|
||||||
118825,11-11-2021,88.205
|
|
||||||
119528,12-11-2021,383.94
|
|
||||||
118825,12-11-2021,89.025
|
|
||||||
119528,15-11-2021,383.31
|
|
||||||
118825,15-11-2021,89.182
|
|
||||||
119528,16-11-2021,381.08
|
|
||||||
118825,16-11-2021,88.569
|
|
||||||
119528,17-11-2021,379.17
|
|
||||||
118825,17-11-2021,88.09
|
|
||||||
119528,18-11-2021,375.09
|
|
||||||
118825,18-11-2021,87.202
|
|
||||||
119528,22-11-2021,368.16
|
|
||||||
118825,22-11-2021,85.382
|
|
||||||
119528,23-11-2021,370.64
|
|
||||||
118825,23-11-2021,85.978
|
|
||||||
119528,24-11-2021,369.91
|
|
||||||
118825,24-11-2021,85.635
|
|
||||||
119528,25-11-2021,371.33
|
|
||||||
118825,25-11-2021,86.212
|
|
||||||
119528,26-11-2021,360.66
|
|
||||||
118825,26-11-2021,83.748
|
|
||||||
119528,29-11-2021,360.05
|
|
||||||
118825,29-11-2021,83.523
|
|
||||||
119528,30-11-2021,359.8
|
|
||||||
118825,30-11-2021,83.475
|
|
||||||
119528,01-12-2021,362.35
|
|
||||||
118825,01-12-2021,84.269
|
|
||||||
119528,02-12-2021,366.09
|
|
||||||
118825,02-12-2021,85.105
|
|
||||||
119528,03-12-2021,363.11
|
|
||||||
118825,03-12-2021,84.507
|
|
||||||
119528,06-12-2021,357.21
|
|
||||||
118825,06-12-2021,83.113
|
|
||||||
119528,07-12-2021,362.63
|
|
||||||
118825,07-12-2021,84.429
|
|
||||||
119528,08-12-2021,368.73
|
|
||||||
118825,08-12-2021,85.935
|
|
||||||
119528,09-12-2021,369.49
|
|
||||||
118825,09-12-2021,86.045
|
|
||||||
119528,10-12-2021,369.44
|
|
||||||
118825,10-12-2021,86.058
|
|
||||||
119528,13-12-2021,367.6
|
|
||||||
118825,13-12-2021,85.632
|
|
||||||
119528,14-12-2021,366.36
|
|
||||||
118825,14-12-2021,85.502
|
|
||||||
119528,15-12-2021,364.34
|
|
||||||
118825,15-12-2021,84.989
|
|
||||||
119528,16-12-2021,363.73
|
|
||||||
118825,16-12-2021,84.972
|
|
||||||
119528,17-12-2021,358.17
|
|
||||||
118825,17-12-2021,83.83
|
|
||||||
119528,20-12-2021,349.98
|
|
||||||
118825,20-12-2021,81.817
|
|
||||||
119528,21-12-2021,353.71
|
|
||||||
118825,21-12-2021,82.746
|
|
||||||
119528,22-12-2021,357.93
|
|
||||||
118825,22-12-2021,83.776
|
|
||||||
119528,23-12-2021,360.68
|
|
||||||
118825,23-12-2021,84.297
|
|
||||||
119528,24-12-2021,359.11
|
|
||||||
118825,24-12-2021,83.903
|
|
||||||
119528,27-12-2021,360.71
|
|
||||||
118825,27-12-2021,84.227
|
|
||||||
119528,28-12-2021,363.81
|
|
||||||
118825,28-12-2021,85.044
|
|
||||||
119528,29-12-2021,363.2
|
|
||||||
118825,29-12-2021,85.03
|
|
||||||
119528,30-12-2021,363.31
|
|
||||||
118825,30-12-2021,85.047
|
|
||||||
119528,31-12-2021,366.98
|
|
||||||
118825,31-12-2021,85.759
|
|
||||||
119528,03-01-2022,371.76
|
|
||||||
118825,03-01-2022,87.111
|
|
||||||
119528,04-01-2022,374.22
|
|
||||||
118825,04-01-2022,87.804
|
|
||||||
119528,05-01-2022,376.31
|
|
||||||
118825,05-01-2022,88.162
|
|
||||||
119528,06-01-2022,373.64
|
|
||||||
118825,06-01-2022,87.541
|
|
||||||
119528,07-01-2022,374.68
|
|
||||||
118825,07-01-2022,87.818
|
|
||||||
119528,10-01-2022,378.47
|
|
||||||
118825,10-01-2022,88.622
|
|
||||||
119528,11-01-2022,379.34
|
|
||||||
118825,11-01-2022,88.678
|
|
||||||
119528,12-01-2022,382.86
|
|
||||||
118825,12-01-2022,89.332
|
|
||||||
119528,13-01-2022,383.68
|
|
||||||
118825,13-01-2022,89.553
|
|
||||||
119528,14-01-2022,384.02
|
|
||||||
118825,14-01-2022,89.729
|
|
||||||
119528,17-01-2022,384.36
|
|
||||||
118825,17-01-2022,89.733
|
|
||||||
119528,18-01-2022,380
|
|
||||||
118825,18-01-2022,88.781
|
|
||||||
119528,19-01-2022,377.24
|
|
||||||
118825,19-01-2022,88.059
|
|
||||||
119528,20-01-2022,374.45
|
|
||||||
118825,20-01-2022,87.361
|
|
||||||
119528,21-01-2022,369.86
|
|
||||||
118825,21-01-2022,86.22
|
|
||||||
119528,24-01-2022,361.01
|
|
||||||
118825,24-01-2022,83.907
|
|
||||||
119528,25-01-2022,364.63
|
|
||||||
118825,25-01-2022,84.763
|
|
||||||
119528,27-01-2022,361.95
|
|
||||||
118825,27-01-2022,83.876
|
|
||||||
119528,28-01-2022,361.91
|
|
||||||
118825,28-01-2022,83.829
|
|
||||||
119528,31-01-2022,367.31
|
|
||||||
118825,31-01-2022,85.18
|
|
||||||
119528,04-02-2022,371.01
|
|
||||||
118825,04-02-2022,86.079
|
|
||||||
119528,07-02-2022,365.04
|
|
||||||
118825,07-02-2022,84.867
|
|
||||||
119528,08-02-2022,365.74
|
|
||||||
118825,08-02-2022,84.945
|
|
||||||
119528,09-02-2022,369.85
|
|
||||||
118825,09-02-2022,85.977
|
|
||||||
119528,10-02-2022,372.29
|
|
||||||
118825,10-02-2022,86.5
|
|
||||||
119528,11-02-2022,366.91
|
|
||||||
118825,11-02-2022,85.226
|
|
||||||
119528,14-02-2022,355.47
|
|
||||||
118825,14-02-2022,82.533
|
|
|
@ -1,219 +0,0 @@
|
|||||||
"amfi_code","date","nav"
|
|
||||||
118825,2013-01-31,18.913
|
|
||||||
118825,2013-02-28,17.723
|
|
||||||
118825,2013-03-28,17.563
|
|
||||||
118825,2013-04-30,18.272
|
|
||||||
118825,2013-05-31,18.383
|
|
||||||
118825,2013-06-28,17.802
|
|
||||||
118825,2013-07-31,17.588
|
|
||||||
118825,2013-08-30,16.993
|
|
||||||
118825,2013-09-30,17.732
|
|
||||||
118825,2013-10-31,19.665
|
|
||||||
118825,2013-11-29,19.787
|
|
||||||
118825,2013-12-31,20.499
|
|
||||||
118825,2014-01-31,19.994
|
|
||||||
118825,2014-02-28,20.942
|
|
||||||
118825,2014-03-31,22.339
|
|
||||||
118825,2014-04-30,22.599
|
|
||||||
118825,2014-05-30,24.937
|
|
||||||
118825,2014-06-30,27.011
|
|
||||||
118825,2014-07-31,27.219
|
|
||||||
118825,2014-08-28,28.625
|
|
||||||
118825,2014-09-30,29.493
|
|
||||||
118825,2014-10-31,30.685
|
|
||||||
118825,2014-11-28,31.956
|
|
||||||
118825,2014-12-31,31.646
|
|
||||||
118825,2015-01-30,33.653
|
|
||||||
118825,2015-02-27,33.581
|
|
||||||
118825,2015-03-31,33.14
|
|
||||||
118825,2015-04-30,32.181
|
|
||||||
118825,2015-05-29,33.256
|
|
||||||
118825,2015-06-30,33.227
|
|
||||||
118825,2015-07-31,34.697
|
|
||||||
118825,2015-08-31,32.833
|
|
||||||
118825,2015-09-30,32.94
|
|
||||||
118825,2015-10-30,33.071
|
|
||||||
118825,2015-11-30,33.024
|
|
||||||
118825,2015-12-31,33.267
|
|
||||||
118825,2016-01-29,31.389
|
|
||||||
118825,2016-02-29,28.751
|
|
||||||
118825,2016-03-31,32.034
|
|
||||||
118825,2016-04-29,32.848
|
|
||||||
118825,2016-05-31,34.135
|
|
||||||
118825,2016-06-30,35.006
|
|
||||||
118825,2016-07-29,37.148
|
|
||||||
118825,2016-08-31,38.005
|
|
||||||
118825,2016-09-30,37.724
|
|
||||||
118825,2016-10-28,38.722
|
|
||||||
118825,2016-11-30,36.689
|
|
||||||
118825,2016-12-30,36.239
|
|
||||||
118825,2017-01-31,38.195
|
|
||||||
118825,2017-02-28,39.873
|
|
||||||
118825,2017-03-31,41.421
|
|
||||||
118825,2017-04-28,42.525
|
|
||||||
118825,2017-05-31,43.977
|
|
||||||
118825,2017-06-30,43.979
|
|
||||||
118825,2017-07-31,46.554
|
|
||||||
118825,2017-08-31,46.383
|
|
||||||
118825,2017-09-29,46.085
|
|
||||||
118825,2017-10-31,48.668
|
|
||||||
118825,2017-11-30,48.824
|
|
||||||
118825,2017-12-29,50.579
|
|
||||||
118825,2018-01-31,51.799
|
|
||||||
118825,2018-02-28,49.041
|
|
||||||
118825,2018-03-28,46.858
|
|
||||||
118825,2018-04-30,49.636
|
|
||||||
118825,2018-05-31,49.169
|
|
||||||
118825,2018-06-29,48.716
|
|
||||||
118825,2018-07-31,51.455
|
|
||||||
118825,2018-08-31,53.494
|
|
||||||
118825,2018-09-28,49.863
|
|
||||||
118825,2018-10-31,48.538
|
|
||||||
118825,2018-11-30,50.597
|
|
||||||
118825,2018-12-31,50.691
|
|
||||||
118825,2019-01-31,50.517
|
|
||||||
118825,2019-02-28,50.176
|
|
||||||
118825,2019-03-31,54.017
|
|
||||||
118825,2019-04-30,54.402
|
|
||||||
118825,2019-05-31,55.334
|
|
||||||
118825,2019-06-28,55.181
|
|
||||||
118825,2019-07-31,52.388
|
|
||||||
118825,2019-08-30,52.214
|
|
||||||
118825,2019-09-30,54.058
|
|
||||||
118825,2019-10-31,56.514
|
|
||||||
118825,2019-11-29,57.42
|
|
||||||
118825,2019-12-31,57.771
|
|
||||||
118825,2020-01-31,57.135
|
|
||||||
118825,2020-02-28,54.034
|
|
||||||
118825,2020-03-31,41.452
|
|
||||||
118825,2020-04-30,47.326
|
|
||||||
118825,2020-05-29,45.845
|
|
||||||
118825,2020-06-30,49.526
|
|
||||||
118825,2020-07-31,53.306000000000004
|
|
||||||
118825,2020-08-19,55.747
|
|
||||||
118825,2020-10-30,56.387
|
|
||||||
118825,2020-11-27,62.001000000000005
|
|
||||||
118825,2020-12-31,66.415
|
|
||||||
118825,2021-01-29,65.655
|
|
||||||
118825,2021-02-26,70.317
|
|
||||||
118825,2021-03-31,70.69
|
|
||||||
118825,2021-04-30,70.39
|
|
||||||
118825,2021-05-31,74.85
|
|
||||||
118825,2021-06-30,77.109
|
|
||||||
118825,2021-07-30,78.335
|
|
||||||
118825,2021-08-31,83.691
|
|
||||||
118825,2021-09-30,86.128
|
|
||||||
118825,2021-10-29,86.612
|
|
||||||
118825,2021-11-30,83.475
|
|
||||||
118825,2021-12-31,85.759
|
|
||||||
118825,2022-01-31,85.18
|
|
||||||
118825,2022-02-17,84.33
|
|
||||||
119528,2013-01-31,101.36
|
|
||||||
119528,2013-02-28,95.25
|
|
||||||
119528,2013-03-28,94.81
|
|
||||||
119528,2013-04-30,99.75
|
|
||||||
119528,2013-05-31,99.73
|
|
||||||
119528,2013-06-28,97.52
|
|
||||||
119528,2013-07-31,95.37
|
|
||||||
119528,2013-08-30,92.24
|
|
||||||
119528,2013-09-30,97.45
|
|
||||||
119528,2013-10-31,107.03
|
|
||||||
119528,2013-11-29,105.91
|
|
||||||
119528,2013-12-31,109.3
|
|
||||||
119528,2014-01-31,105.09
|
|
||||||
119528,2014-02-28,108.58
|
|
||||||
119528,2014-03-31,117.28
|
|
||||||
119528,2014-04-30,118.06
|
|
||||||
119528,2014-05-30,131.33
|
|
||||||
119528,2014-06-30,139.48
|
|
||||||
119528,2014-07-31,140.49
|
|
||||||
119528,2014-08-28,145.43
|
|
||||||
119528,2014-09-30,147.4
|
|
||||||
119528,2014-10-31,154.46
|
|
||||||
119528,2014-11-28,161.93
|
|
||||||
119528,2014-12-31,159.62
|
|
||||||
119528,2015-01-30,170.46
|
|
||||||
119528,2015-02-27,171.18
|
|
||||||
119528,2015-03-31,166.8
|
|
||||||
119528,2015-04-30,161.95
|
|
||||||
119528,2015-05-29,166.78
|
|
||||||
119528,2015-06-30,166.67
|
|
||||||
119528,2015-07-31,172.33
|
|
||||||
119528,2015-08-31,161.96
|
|
||||||
119528,2015-09-30,162.25
|
|
||||||
119528,2015-10-30,164.16
|
|
||||||
119528,2015-11-30,162.7
|
|
||||||
119528,2015-12-31,162.83
|
|
||||||
119528,2016-01-29,155.87
|
|
||||||
119528,2016-02-29,144.56
|
|
||||||
119528,2016-03-31,159.88
|
|
||||||
119528,2016-04-29,163.54
|
|
||||||
119528,2016-05-31,170.01
|
|
||||||
119528,2016-06-30,174.61
|
|
||||||
119528,2016-07-29,184.36
|
|
||||||
119528,2016-08-31,189.33
|
|
||||||
119528,2016-09-30,187.16
|
|
||||||
119528,2016-10-28,189.29
|
|
||||||
119528,2016-11-30,178.19
|
|
||||||
119528,2016-12-30,176.66
|
|
||||||
119528,2017-01-31,185.76
|
|
||||||
119528,2017-02-28,193.2
|
|
||||||
119528,2017-03-31,200.54
|
|
||||||
119528,2017-04-28,205.25
|
|
||||||
119528,2017-05-31,208.22
|
|
||||||
119528,2017-06-30,209.83
|
|
||||||
119528,2017-07-31,221.15
|
|
||||||
119528,2017-08-31,219.99
|
|
||||||
119528,2017-09-29,217.7
|
|
||||||
119528,2017-10-31,226.94
|
|
||||||
119528,2017-11-30,225.24
|
|
||||||
119528,2017-12-29,233.26
|
|
||||||
119528,2018-01-31,237.57
|
|
||||||
119528,2018-02-28,226.55
|
|
||||||
119528,2018-03-28,219.73
|
|
||||||
119528,2018-04-30,232.04
|
|
||||||
119528,2018-05-31,228.49
|
|
||||||
119528,2018-06-29,225.27
|
|
||||||
119528,2018-07-31,237.11
|
|
||||||
119528,2018-08-31,243.79
|
|
||||||
119528,2018-09-28,223.83
|
|
||||||
119528,2018-10-31,218.61
|
|
||||||
119528,2018-11-30,226.99
|
|
||||||
119528,2018-12-31,228.61
|
|
||||||
119528,2019-01-31,224.26
|
|
||||||
119528,2019-02-28,222.71
|
|
||||||
119528,2019-03-29,240.21
|
|
||||||
119528,2019-04-30,240.01
|
|
||||||
119528,2019-05-31,243.72
|
|
||||||
119528,2019-06-28,241.28
|
|
||||||
119528,2019-07-31,229.54
|
|
||||||
119528,2019-08-30,226.0
|
|
||||||
119528,2019-09-30,234.75
|
|
||||||
119528,2019-10-31,242.11
|
|
||||||
119528,2019-11-29,246.75
|
|
||||||
119528,2019-12-31,247.81
|
|
||||||
119528,2020-01-31,246.14
|
|
||||||
119528,2020-02-28,231.91
|
|
||||||
119528,2020-03-31,175.98
|
|
||||||
119528,2020-04-30,200.77
|
|
||||||
119528,2020-05-29,196.75
|
|
||||||
119528,2020-06-30,210.55
|
|
||||||
119528,2020-07-31,224.93
|
|
||||||
119528,2020-08-19,233.78
|
|
||||||
119528,2020-10-30,235.83
|
|
||||||
119528,2020-11-27,264.04
|
|
||||||
119528,2020-12-31,285.02
|
|
||||||
119528,2021-01-29,280.52
|
|
||||||
119528,2021-02-26,300.56
|
|
||||||
119528,2021-03-31,301.57
|
|
||||||
119528,2021-04-30,301.1
|
|
||||||
119528,2021-05-31,320.98
|
|
||||||
119528,2021-06-30,327.64
|
|
||||||
119528,2021-07-30,336.6
|
|
||||||
119528,2021-08-31,360.75
|
|
||||||
119528,2021-09-30,369.42
|
|
||||||
119528,2021-10-29,372.89
|
|
||||||
119528,2021-11-30,359.8
|
|
||||||
119528,2021-12-31,366.98
|
|
||||||
119528,2022-01-31,367.31
|
|
||||||
119528,2022-02-17,363.56
|
|
|
@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
amfi_code,date,nav
|
|
||||||
118825,31-03-2021,70.69
|
|
||||||
118825,30-04-2021,70.39
|
|
||||||
118825,31-05-2021,74.85
|
|
||||||
118825,30-07-2021,78.335
|
|
||||||
118825,31-08-2021,83.691
|
|
||||||
118825,30-09-2021,86.128
|
|
||||||
118825,29-10-2021,86.612
|
|
||||||
118825,30-11-2021,83.475
|
|
||||||
118825,31-01-2022,85.18
|
|
||||||
118825,17-02-2022,84.33
|
|
|
@ -1,71 +0,0 @@
|
|||||||
"amfi_code","date","nav"
|
|
||||||
118825,2013-03-28,17.563
|
|
||||||
118825,2013-06-28,17.802
|
|
||||||
118825,2013-09-30,17.732
|
|
||||||
118825,2013-12-31,20.499
|
|
||||||
118825,2014-03-31,22.339
|
|
||||||
118825,2014-06-30,27.011
|
|
||||||
118825,2014-09-30,29.493
|
|
||||||
118825,2014-12-31,31.646
|
|
||||||
118825,2015-03-31,33.14
|
|
||||||
118825,2015-06-30,33.227
|
|
||||||
118825,2015-09-30,32.94
|
|
||||||
118825,2015-12-31,33.267
|
|
||||||
118825,2016-03-31,32.034
|
|
||||||
118825,2016-06-30,35.006
|
|
||||||
118825,2016-09-30,37.724
|
|
||||||
118825,2016-12-30,36.239
|
|
||||||
118825,2017-03-31,41.421
|
|
||||||
118825,2017-06-30,43.979
|
|
||||||
118825,2017-09-29,46.085
|
|
||||||
118825,2017-12-29,50.579
|
|
||||||
118825,2018-03-28,46.858
|
|
||||||
118825,2018-06-29,48.716
|
|
||||||
118825,2018-09-28,49.863
|
|
||||||
118825,2018-12-31,50.691
|
|
||||||
118825,2019-03-31,54.017
|
|
||||||
118825,2019-06-28,55.181
|
|
||||||
118825,2019-09-30,54.058
|
|
||||||
118825,2019-12-31,57.771
|
|
||||||
118825,2020-03-31,41.452
|
|
||||||
118825,2020-06-30,49.526
|
|
||||||
118825,2020-12-31,66.415
|
|
||||||
118825,2021-03-31,70.69
|
|
||||||
118825,2021-06-30,77.109
|
|
||||||
118825,2021-09-30,86.128
|
|
||||||
118825,2021-12-31,85.759
|
|
||||||
119528,2013-03-28,94.81
|
|
||||||
119528,2013-06-28,97.52
|
|
||||||
119528,2013-09-30,97.45
|
|
||||||
119528,2013-12-31,109.3
|
|
||||||
119528,2014-03-31,117.28
|
|
||||||
119528,2014-06-30,139.48
|
|
||||||
119528,2014-09-30,147.4
|
|
||||||
119528,2014-12-31,159.62
|
|
||||||
119528,2015-03-31,166.8
|
|
||||||
119528,2015-06-30,166.67
|
|
||||||
119528,2015-09-30,162.25
|
|
||||||
119528,2015-12-31,162.83
|
|
||||||
119528,2016-03-31,159.88
|
|
||||||
119528,2016-06-30,174.61
|
|
||||||
119528,2016-09-30,187.16
|
|
||||||
119528,2016-12-30,176.66
|
|
||||||
119528,2017-03-31,200.54
|
|
||||||
119528,2017-06-30,209.83
|
|
||||||
119528,2017-09-29,217.7
|
|
||||||
119528,2017-12-29,233.26
|
|
||||||
119528,2018-03-28,219.73
|
|
||||||
119528,2018-06-29,225.27
|
|
||||||
119528,2018-09-28,223.83
|
|
||||||
119528,2018-12-31,228.61
|
|
||||||
119528,2019-03-29,240.21
|
|
||||||
119528,2019-06-28,241.28
|
|
||||||
119528,2019-09-30,234.75
|
|
||||||
119528,2019-12-31,247.81
|
|
||||||
119528,2020-03-31,175.98
|
|
||||||
119528,2020-06-30,210.55
|
|
||||||
119528,2020-12-31,285.02
|
|
||||||
119528,2021-03-31,301.57
|
|
||||||
119528,2021-06-30,327.64
|
|
||||||
119528,2021-09-30,369.42
|
|
||||||
119528,2021-12-31,366.98
|
|
|
@ -1,9 +0,0 @@
|
|||||||
amfi_code,date,nav
|
|
||||||
118825,31-03-2019,54.017
|
|
||||||
118825,28-06-2019,55.181
|
|
||||||
118825,31-12-2019,57.771
|
|
||||||
118825,31-03-2020,41.452
|
|
||||||
118825,30-06-2020,49.526
|
|
||||||
118825,30-06-2021,77.109
|
|
||||||
118825,30-09-2021,86.128
|
|
||||||
118825,31-12-2021,85.759
|
|
|
@ -1,25 +0,0 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
from fincal.core import Series
|
|
||||||
|
|
||||||
s1 = Series([2.5, 6.2, 5.6, 8.4, 7.4, 1.5, 9.6, 5])
|
|
||||||
|
|
||||||
dt_lst = [
|
|
||||||
datetime.datetime(2020, 12, 4, 0, 0),
|
|
||||||
datetime.datetime(2019, 5, 16, 0, 0),
|
|
||||||
datetime.datetime(2019, 9, 25, 0, 0),
|
|
||||||
datetime.datetime(2016, 2, 18, 0, 0),
|
|
||||||
datetime.datetime(2017, 8, 14, 0, 0),
|
|
||||||
datetime.datetime(2018, 1, 4, 0, 0),
|
|
||||||
datetime.datetime(2017, 5, 21, 0, 0),
|
|
||||||
datetime.datetime(2018, 7, 17, 0, 0),
|
|
||||||
datetime.datetime(2016, 4, 8, 0, 0),
|
|
||||||
datetime.datetime(2020, 1, 7, 0, 0),
|
|
||||||
datetime.datetime(2016, 12, 24, 0, 0),
|
|
||||||
datetime.datetime(2020, 6, 19, 0, 0),
|
|
||||||
datetime.datetime(2016, 3, 16, 0, 0),
|
|
||||||
datetime.datetime(2017, 4, 25, 0, 0),
|
|
||||||
datetime.datetime(2016, 7, 10, 0, 0)
|
|
||||||
]
|
|
||||||
|
|
||||||
s2 = Series(dt_lst)
|
|
390
testing.ipynb
390
testing.ipynb
@ -1,390 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"id": "e1ecfa55",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"import fincal as fc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 2,
|
|
||||||
"id": "ccac3896",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"fincal.fincal.TimeSeries"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 2,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"fc.TimeSeries"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 3,
|
|
||||||
"id": "a54bfbdf",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"data = [\n",
|
|
||||||
" (\"2022-01-01\", 10),\n",
|
|
||||||
" (\"2022-01-02\", 12),\n",
|
|
||||||
" (\"2022-01-03\", 14)\n",
|
|
||||||
" # (\"2022-01-04\", 16),\n",
|
|
||||||
" # (\"2022-01-06\", 18),\n",
|
|
||||||
" # (\"2022-01-07\", 20),\n",
|
|
||||||
" # (\"2022-01-09\", 22),\n",
|
|
||||||
" # (\"2022-01-10\", 24),\n",
|
|
||||||
" # (\"2022-01-11\", 26),\n",
|
|
||||||
" # (\"2022-01-12\", 28),\n",
|
|
||||||
" # (\"2023-01-01\", 30),\n",
|
|
||||||
" # (\"2023-01-02\", 32),\n",
|
|
||||||
" # (\"2023-01-03\", 34),\n",
|
|
||||||
"]"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 5,
|
|
||||||
"id": "fcc5f8f1",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 2, 0, 0), 12),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 3, 0, 0), 14)], frequency='M')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 5,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"ts = fc.TimeSeries(data, 'M')\n",
|
|
||||||
"ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 7,
|
|
||||||
"id": "c9e9cb1b",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 2, 0, 0), 12),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 3, 0, 0), 14),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 4, 0, 0), 15),\n",
|
|
||||||
"\t(datetime.datetime(2022, 1, 5, 0, 0), 16)], frequency='M')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 7,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"ts['2022-01-04'] = 15\n",
|
|
||||||
"ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 8,
|
|
||||||
"id": "8e812756",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10),\n",
|
|
||||||
"\t (datetime.datetime(2022, 1, 8, 0, 0), 20),\n",
|
|
||||||
"\t (datetime.datetime(2022, 1, 15, 0, 0), 28)\n",
|
|
||||||
"\t ...\n",
|
|
||||||
"\t (datetime.datetime(2022, 12, 17, 0, 0), 28),\n",
|
|
||||||
"\t (datetime.datetime(2022, 12, 24, 0, 0), 28),\n",
|
|
||||||
"\t (datetime.datetime(2022, 12, 31, 0, 0), 28)], frequency='W')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 8,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"ts.expand('W', 'ffill')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 12,
|
|
||||||
"id": "55918da9-2df6-4773-9ca0-e19b52c3ece2",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"TimeSeries([(datetime.datetime(2022, 1, 1, 0, 0), 10),\n",
|
|
||||||
"\t(datetime.datetime(2022, 4, 1, 0, 0), 28),\n",
|
|
||||||
"\t(datetime.datetime(2022, 7, 1, 0, 0), 28),\n",
|
|
||||||
"\t(datetime.datetime(2022, 10, 1, 0, 0), 28),\n",
|
|
||||||
"\t(datetime.datetime(2023, 1, 1, 0, 0), 30)], frequency='Q')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 12,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"ts.shrink('Q', 'ffill')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 13,
|
|
||||||
"id": "36eefec7-7dbf-4a28-ac50-2e502d9d6864",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"weekly_data = [('2017-01-01', 67),\n",
|
|
||||||
"('2017-01-08', 79),\n",
|
|
||||||
"('2017-01-15', 73),\n",
|
|
||||||
"('2017-01-22', 63),\n",
|
|
||||||
"('2017-01-29', 85),\n",
|
|
||||||
"('2017-02-05', 66),\n",
|
|
||||||
"('2017-02-12', 78),\n",
|
|
||||||
"('2017-02-19', 75),\n",
|
|
||||||
"('2017-02-26', 76),\n",
|
|
||||||
"('2017-03-05', 82),\n",
|
|
||||||
"('2017-03-12', 85),\n",
|
|
||||||
"('2017-03-19', 63),\n",
|
|
||||||
"('2017-03-26', 78),\n",
|
|
||||||
"('2017-04-02', 65),\n",
|
|
||||||
"('2017-04-09', 85),\n",
|
|
||||||
"('2017-04-16', 86),\n",
|
|
||||||
"('2017-04-23', 67),\n",
|
|
||||||
"('2017-04-30', 65),\n",
|
|
||||||
"('2017-05-07', 82),\n",
|
|
||||||
"('2017-05-14', 73),\n",
|
|
||||||
"('2017-05-21', 78),\n",
|
|
||||||
"('2017-05-28', 74),\n",
|
|
||||||
"('2017-06-04', 62),\n",
|
|
||||||
"('2017-06-11', 84),\n",
|
|
||||||
"('2017-06-18', 83)]"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 15,
|
|
||||||
"id": "39bd8598-ab0f-4c81-8428-ad8248e686d3",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"week_ts = fc.TimeSeries(weekly_data, 'W')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 22,
|
|
||||||
"id": "d64dd3c6-4295-4301-90e4-5c74ea23c4af",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"(datetime.datetime(2017, 1, 1, 0, 0), 67)\n",
|
|
||||||
"(datetime.datetime(2017, 2, 1, 0, 0), 85)\n",
|
|
||||||
"(datetime.datetime(2017, 3, 1, 0, 0), 76)\n",
|
|
||||||
"(datetime.datetime(2017, 4, 1, 0, 0), 78)\n",
|
|
||||||
"(datetime.datetime(2017, 5, 1, 0, 0), 65)\n",
|
|
||||||
"(datetime.datetime(2017, 6, 1, 0, 0), 74)\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"for i in week_ts.shrink('M', 'ffill', skip_weekends=True):\n",
|
|
||||||
" print(i)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 23,
|
|
||||||
"id": "a549c5c0-c89a-4cc3-b396-c4afa77a9879",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"ename": "OverflowError",
|
|
||||||
"evalue": "date value out of range",
|
|
||||||
"output_type": "error",
|
|
||||||
"traceback": [
|
|
||||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
|
||||||
"\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/core.py:405\u001b[0m, in \u001b[0;36mTimeSeriesCore.get\u001b[0;34m(self, date, default, closest)\u001b[0m\n\u001b[1;32m 404\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 405\u001b[0m item \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_item_from_date\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdate\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 406\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m item\n",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/core.py:69\u001b[0m, in \u001b[0;36mdate_parser.<locals>.parse_dates.<locals>.wrapper_func\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 68\u001b[0m args[j] \u001b[38;5;241m=\u001b[39m parsed_date\n\u001b[0;32m---> 69\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/core.py:328\u001b[0m, in \u001b[0;36mTimeSeriesCore._get_item_from_date\u001b[0;34m(self, date)\u001b[0m\n\u001b[1;32m 326\u001b[0m \u001b[38;5;129m@date_parser\u001b[39m(\u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 327\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_get_item_from_date\u001b[39m(\u001b[38;5;28mself\u001b[39m, date: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m|\u001b[39m datetime\u001b[38;5;241m.\u001b[39mdatetime):\n\u001b[0;32m--> 328\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m date, \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m[\u001b[49m\u001b[43mdate\u001b[49m\u001b[43m]\u001b[49m\n",
|
|
||||||
"\u001b[0;31mKeyError\u001b[0m: datetime.datetime(1, 1, 1, 0, 0)",
|
|
||||||
"\nDuring handling of the above exception, another exception occurred:\n",
|
|
||||||
"\u001b[0;31mOverflowError\u001b[0m Traceback (most recent call last)",
|
|
||||||
"Input \u001b[0;32mIn [23]\u001b[0m, in \u001b[0;36m<cell line: 1>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mweek_ts\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msync\u001b[49m\u001b[43m(\u001b[49m\u001b[43mts\u001b[49m\u001b[43m)\u001b[49m\n",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/fincal.py:733\u001b[0m, in \u001b[0;36mTimeSeries.sync\u001b[0;34m(self, other, fill_method)\u001b[0m\n\u001b[1;32m 731\u001b[0m new_other[dt] \u001b[38;5;241m=\u001b[39m other[dt][\u001b[38;5;241m1\u001b[39m]\n\u001b[1;32m 732\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 733\u001b[0m new_other[dt] \u001b[38;5;241m=\u001b[39m other\u001b[38;5;241m.\u001b[39mget(dt, closest\u001b[38;5;241m=\u001b[39mclosest)[\u001b[38;5;241m1\u001b[39m]\n\u001b[1;32m 735\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m(new_other, frequency\u001b[38;5;241m=\u001b[39mother\u001b[38;5;241m.\u001b[39mfrequency\u001b[38;5;241m.\u001b[39msymbol)\n",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/core.py:69\u001b[0m, in \u001b[0;36mdate_parser.<locals>.parse_dates.<locals>.wrapper_func\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 67\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 68\u001b[0m args[j] \u001b[38;5;241m=\u001b[39m parsed_date\n\u001b[0;32m---> 69\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
|
|
||||||
"File \u001b[0;32m~/Documents/projects/fincal/fincal/core.py:408\u001b[0m, in \u001b[0;36mTimeSeriesCore.get\u001b[0;34m(self, date, default, closest)\u001b[0m\n\u001b[1;32m 406\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m item\n\u001b[1;32m 407\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n\u001b[0;32m--> 408\u001b[0m date \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m delta\n",
|
|
||||||
"\u001b[0;31mOverflowError\u001b[0m: date value out of range"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"week_ts.sync(ts)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 1,
|
|
||||||
"id": "4755aea3-3655-4651-91d2-8e54c24303bc",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"import fincal as fc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 2,
|
|
||||||
"id": "bd9887b3-d98a-4c80-8f95-ef7b7f19ded4",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"['date', 'nav']\n",
|
|
||||||
"CPU times: user 56.9 ms, sys: 3.3 ms, total: 60.2 ms\n",
|
|
||||||
"Wall time: 60.2 ms\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"ts = fc.read_csv('test_files/msft.csv', frequency='D')"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 3,
|
|
||||||
"id": "b7c176d4-d89f-4bda-9d67-75463eb90468",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"(datetime.datetime(2022, 2, 9, 0, 0), 311.209991)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 10, 0, 0), 302.380005)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 11, 0, 0), 295.040009)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 14, 0, 0), 295.0)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 15, 0, 0), 300.470001)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 16, 0, 0), 299.5)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 17, 0, 0), 290.730011)\n",
|
|
||||||
"(datetime.datetime(2022, 2, 18, 0, 0), 287.929993)\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"for i in ts.tail(8):\n",
|
|
||||||
" print(i)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 4,
|
|
||||||
"id": "69c57754-a6fb-4881-9359-ba17c7fb8be5",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"CPU times: user 1.85 ms, sys: 143 µs, total: 1.99 ms\n",
|
|
||||||
"Wall time: 2 ms\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"ts['2022-02-12'] = 295"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 5,
|
|
||||||
"id": "7aa02023-406e-4700-801c-c06390ddf914",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"CPU times: user 3.7 ms, sys: 121 µs, total: 3.82 ms\n",
|
|
||||||
"Wall time: 3.84 ms\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"{'start_date': datetime.datetime(1999, 12, 27, 0, 0),\n",
|
|
||||||
" 'end_date': datetime.datetime(2009, 3, 9, 0, 0),\n",
|
|
||||||
" 'drawdown': -0.7456453305351521}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 5,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"%%time\n",
|
|
||||||
"ts.max_drawdown()"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "Python 3 (ipykernel)",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"codemirror_mode": {
|
|
||||||
"name": "ipython",
|
|
||||||
"version": 3
|
|
||||||
},
|
|
||||||
"file_extension": ".py",
|
|
||||||
"mimetype": "text/x-python",
|
|
||||||
"name": "python",
|
|
||||||
"nbconvert_exporter": "python",
|
|
||||||
"pygments_lexer": "ipython3",
|
|
||||||
"version": "3.10.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
42
tests/README.md
Normal file
42
tests/README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Testing Guidelines
|
||||||
|
PyFacts uses Pytest for unit testing.
|
||||||
|
|
||||||
|
All high level functions are expected to have tests written for them. Each file in the pyfacts module has a dedicated test file. All tests related to that file go within the respective test files.
|
||||||
|
|
||||||
|
Since this module needs test data for testing, a Pytest fixture has been defined to generate test data. Use this fixture to generate test data. The fixture uses the random module to generate random test data. A seed has been hardcoded for the random data generator to ensure it generates the same data all the time (if it didn't, tests for specific values would never pass).
|
||||||
|
WARNING! Do not change the seed for the random data generator. This will cause most tests to fail.
|
||||||
|
|
||||||
|
To use the fixture, just pass `create_test_data` as an argument to the test function and then use it within the function. Pytest will automatically locate the relevant function (it need not be imported into the test file).
|
||||||
|
|
||||||
|
## Writing tests
|
||||||
|
Tests are organised as follows:
|
||||||
|
- Each broad function/method has a Test Class
|
||||||
|
- All variations should be tested within this class using one or more functions
|
||||||
|
|
||||||
|
All test files should be named `test_<module_file_name>.py`.
|
||||||
|
For instance, test file for `core.py` is named `test_core.py`
|
||||||
|
|
||||||
|
All class names should begin with the word `Test`.
|
||||||
|
All function names should begin with the word `test_`.
|
||||||
|
|
||||||
|
It needs to be ensured that all test functions are independent of each other.
|
||||||
|
## Running tests
|
||||||
|
Skip this part if you already know how to run pytest.
|
||||||
|
|
||||||
|
Open the terminal. Make sure you are in the root pyfacts folder. Then run the following command:
|
||||||
|
`pytest tests`
|
||||||
|
|
||||||
|
This will run the entire test suite. This can take some time depending on the number of tests and speed of your computer. Hence you might want to run only a few tests.
|
||||||
|
|
||||||
|
To run tests within a particular file, say test_core.py, type the following command:
|
||||||
|
`pytest tests/test_core.py`
|
||||||
|
|
||||||
|
If you want to run only a particular class within a file, for instance `TestSetitem` within the `test_core.py` file, run them as follows:
|
||||||
|
`pytest tests/test_core.py::TestSetitem`
|
||||||
|
|
||||||
|
This will run only the specified class, making sure your tests don't take too long.
|
||||||
|
|
||||||
|
If you're using VS Code, you can make this whole process easier by configuring pytest within VS Code. It will identify all tests and allow you to run them individually from the testing pane on the left.
|
||||||
|
|
||||||
|
### Before you push your code
|
||||||
|
Before you push your code or raise a PR, ensure that all tests are passing. PRs where any of the tests are failing will not be merged. Any modifications to the code which require a modification to existing tests should be accompanied with a note in the PR as to the reasons existing tests had to be modified.
|
111
tests/conftest.py
Normal file
111
tests/conftest.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
import pyfacts as pft
|
||||||
|
|
||||||
|
|
||||||
|
def conf_add(n1, n2):
|
||||||
|
return n1 + n2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf_fun():
|
||||||
|
return conf_add
|
||||||
|
|
||||||
|
|
||||||
|
def create_prices(s0: float, mu: float, sigma: float, num_prices: int) -> list:
|
||||||
|
"""Generates a price following a geometric brownian motion process based on the input of the arguments.
|
||||||
|
|
||||||
|
Since this function is used only to generate data for tests, the seed is fixed as 1234.
|
||||||
|
Many of the tests rely on exact values generated using this seed.
|
||||||
|
If the seed is changed, those tests will fail.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
------------
|
||||||
|
s0: float
|
||||||
|
Asset inital price.
|
||||||
|
|
||||||
|
mu: float
|
||||||
|
Interest rate expressed annual terms.
|
||||||
|
|
||||||
|
sigma: float
|
||||||
|
Volatility expressed annual terms.
|
||||||
|
|
||||||
|
num_prices: int
|
||||||
|
number of prices to generate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
Returns a list of values generated using GBM algorithm
|
||||||
|
"""
|
||||||
|
|
||||||
|
random.seed(1234) # WARNING! Changing the seed will cause most tests to fail
|
||||||
|
all_values = []
|
||||||
|
for _ in range(num_prices):
|
||||||
|
s0 *= math.exp(
|
||||||
|
(mu - 0.5 * sigma**2) * (1.0 / 365.0) + sigma * math.sqrt(1.0 / 365.0) * random.gauss(mu=0, sigma=1)
|
||||||
|
)
|
||||||
|
all_values.append(round(s0, 2))
|
||||||
|
|
||||||
|
return all_values
|
||||||
|
|
||||||
|
|
||||||
|
def sample_data_generator(
|
||||||
|
frequency: pft.Frequency,
|
||||||
|
start_date: datetime.date = datetime.date(2017, 1, 1),
|
||||||
|
num: int = 1000,
|
||||||
|
skip_weekends: bool = False,
|
||||||
|
mu: float = 0.1,
|
||||||
|
sigma: float = 0.05,
|
||||||
|
eomonth: bool = False,
|
||||||
|
dates_as_string: bool = False,
|
||||||
|
) -> List[tuple]:
|
||||||
|
"""Creates TimeSeries data
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
frequency: Frequency
|
||||||
|
The frequency of the time series data to be generated.
|
||||||
|
|
||||||
|
num: int
|
||||||
|
Number of date: value pairs to be generated.
|
||||||
|
|
||||||
|
skip_weekends: bool
|
||||||
|
Whether weekends (saturday, sunday) should be skipped.
|
||||||
|
Gets used only if the frequency is daily.
|
||||||
|
|
||||||
|
mu: float
|
||||||
|
Mean return for the values.
|
||||||
|
|
||||||
|
sigma: float
|
||||||
|
standard deviation of the values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
Returns a TimeSeries object
|
||||||
|
"""
|
||||||
|
|
||||||
|
timedelta_dict = {
|
||||||
|
frequency.freq_type: int(
|
||||||
|
frequency.value * num * (7 / 5 if frequency == pft.AllFrequencies.D and skip_weekends else 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end_date = start_date + relativedelta(**timedelta_dict)
|
||||||
|
dates = pft.create_date_series(
|
||||||
|
start_date, end_date, frequency.symbol, skip_weekends=skip_weekends, eomonth=eomonth, ensure_coverage=False
|
||||||
|
)
|
||||||
|
if dates_as_string:
|
||||||
|
dates = [dt.strftime("%Y-%m-%d") for dt in dates]
|
||||||
|
values = create_prices(1000, mu, sigma, num)
|
||||||
|
ts = list(zip(dates, values))
|
||||||
|
return ts
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def create_test_data():
|
||||||
|
return sample_data_generator
|
@ -1,16 +1,15 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import random
|
import random
|
||||||
from typing import Literal, Sequence
|
from typing import Mapping
|
||||||
|
|
||||||
|
import pyfacts as pft
|
||||||
import pytest
|
import pytest
|
||||||
from fincal.core import AllFrequencies, Frequency, Series, TimeSeriesCore
|
from pyfacts.utils import PyfactsOptions
|
||||||
from fincal.fincal import create_date_series
|
|
||||||
from fincal.utils import FincalOptions
|
|
||||||
|
|
||||||
|
|
||||||
class TestFrequency:
|
class TestFrequency:
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
D = Frequency("daily", "days", 1, 1, "D")
|
D = pft.Frequency("daily", "days", 1, 1, "D")
|
||||||
assert D.days == 1
|
assert D.days == 1
|
||||||
assert D.symbol == "D"
|
assert D.symbol == "D"
|
||||||
assert D.name == "daily"
|
assert D.name == "daily"
|
||||||
@ -18,106 +17,103 @@ class TestFrequency:
|
|||||||
assert D.freq_type == "days"
|
assert D.freq_type == "days"
|
||||||
|
|
||||||
|
|
||||||
def create_test_data(
|
|
||||||
frequency: str,
|
|
||||||
eomonth: bool,
|
|
||||||
n: int,
|
|
||||||
gaps: float,
|
|
||||||
month_position: Literal["start", "middle", "end"],
|
|
||||||
date_as_str: bool,
|
|
||||||
as_outer_type: Literal["dict", "list"] = "list",
|
|
||||||
as_inner_type: Literal["dict", "list", "tuple"] = "tuple",
|
|
||||||
) -> Sequence[tuple]:
|
|
||||||
start_dates = {
|
|
||||||
"start": datetime.datetime(2016, 1, 1),
|
|
||||||
"middle": datetime.datetime(2016, 1, 15),
|
|
||||||
"end": datetime.datetime(2016, 1, 31),
|
|
||||||
}
|
|
||||||
end_date = datetime.datetime(2021, 12, 31)
|
|
||||||
dates = create_date_series(start_dates[month_position], end_date, frequency=frequency, eomonth=eomonth)
|
|
||||||
dates = dates[:n]
|
|
||||||
if gaps:
|
|
||||||
num_gaps = int(len(dates) * gaps)
|
|
||||||
to_remove = random.sample(dates, num_gaps)
|
|
||||||
for i in to_remove:
|
|
||||||
dates.remove(i)
|
|
||||||
if date_as_str:
|
|
||||||
dates = [i.strftime("%Y-%m-%d") for i in dates]
|
|
||||||
|
|
||||||
values = [random.randint(8000, 90000) / 100 for _ in dates]
|
|
||||||
|
|
||||||
data = list(zip(dates, values))
|
|
||||||
if as_outer_type == "list":
|
|
||||||
if as_inner_type == "list":
|
|
||||||
data = [list(i) for i in data]
|
|
||||||
elif as_inner_type == "dict[1]":
|
|
||||||
data = [dict((i,)) for i in data]
|
|
||||||
elif as_inner_type == "dict[2]":
|
|
||||||
data = [dict(date=i, value=j) for i, j in data]
|
|
||||||
elif as_outer_type == "dict":
|
|
||||||
data = dict(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class TestAllFrequencies:
|
class TestAllFrequencies:
|
||||||
def test_attributes(self):
|
def test_attributes(self):
|
||||||
assert hasattr(AllFrequencies, "D")
|
assert hasattr(pft.AllFrequencies, "D")
|
||||||
assert hasattr(AllFrequencies, "M")
|
assert hasattr(pft.AllFrequencies, "M")
|
||||||
assert hasattr(AllFrequencies, "Q")
|
assert hasattr(pft.AllFrequencies, "Q")
|
||||||
|
|
||||||
def test_days(self):
|
def test_days(self):
|
||||||
assert AllFrequencies.D.days == 1
|
assert pft.AllFrequencies.D.days == 1
|
||||||
assert AllFrequencies.M.days == 30
|
assert pft.AllFrequencies.M.days == 30
|
||||||
assert AllFrequencies.Q.days == 91
|
assert pft.AllFrequencies.Q.days == 91
|
||||||
|
|
||||||
def test_symbol(self):
|
def test_symbol(self):
|
||||||
assert AllFrequencies.H.symbol == "H"
|
assert pft.AllFrequencies.H.symbol == "H"
|
||||||
assert AllFrequencies.W.symbol == "W"
|
assert pft.AllFrequencies.W.symbol == "W"
|
||||||
|
|
||||||
def test_values(self):
|
def test_values(self):
|
||||||
assert AllFrequencies.H.value == 6
|
assert pft.AllFrequencies.H.value == 6
|
||||||
assert AllFrequencies.Y.value == 1
|
assert pft.AllFrequencies.Y.value == 1
|
||||||
|
|
||||||
def test_type(self):
|
def test_type(self):
|
||||||
assert AllFrequencies.Q.freq_type == "months"
|
assert pft.AllFrequencies.Q.freq_type == "months"
|
||||||
assert AllFrequencies.W.freq_type == "days"
|
assert pft.AllFrequencies.W.freq_type == "days"
|
||||||
|
|
||||||
|
|
||||||
class TestSeries:
|
class TestSeries:
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
series = Series([1, 2, 3, 4, 5, 6, 7], data_type="number")
|
series = pft.Series([1, 2, 3, 4, 5, 6, 7], dtype="number")
|
||||||
assert series.dtype == float
|
assert series.dtype == float
|
||||||
assert series[2] == 3
|
assert series[2] == 3
|
||||||
|
|
||||||
dates = create_date_series("2021-01-01", "2021-01-31", frequency="D")
|
dates = pft.create_date_series("2021-01-01", "2021-01-31", frequency="D")
|
||||||
series = Series(dates, data_type="date")
|
series = pft.Series(dates, dtype="date")
|
||||||
assert series.dtype == datetime.datetime
|
assert series.dtype == datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
class TestTimeSeriesCore:
|
class TestTimeSeriesCore:
|
||||||
data = [("2021-01-01", 220), ("2021-02-01", 230), ("2021-03-01", 240)]
|
data = [("2021-01-01", 220), ("2021-02-01", 230), ("2021-03-01", 240)]
|
||||||
|
|
||||||
def test_repr_str(self):
|
def test_repr_str(self, create_test_data):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert str(ts) in repr(ts).replace("\t", " ")
|
assert str(ts) in repr(ts).replace("\t", " ")
|
||||||
|
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True)
|
data = create_test_data(frequency=pft.AllFrequencies.D, eomonth=False, num=50, dates_as_string=True)
|
||||||
ts = TimeSeriesCore(data, frequency="D")
|
ts = pft.TimeSeriesCore(data, frequency="D")
|
||||||
assert "..." in str(ts)
|
assert "..." in str(ts)
|
||||||
assert "..." in repr(ts)
|
assert "..." in repr(ts)
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert isinstance(ts, TimeSeriesCore)
|
assert isinstance(ts, pft.TimeSeriesCore)
|
||||||
# assert isinstance(ts, Mapping)
|
assert isinstance(ts, Mapping)
|
||||||
|
|
||||||
|
def test_creation_no_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=300, frequency=pft.AllFrequencies.D)
|
||||||
|
ts = pft.TimeSeriesCore(data)
|
||||||
|
assert ts.frequency == pft.AllFrequencies.D
|
||||||
|
|
||||||
|
data = create_test_data(num=300, frequency=pft.AllFrequencies.M)
|
||||||
|
ts = pft.TimeSeriesCore(data)
|
||||||
|
assert ts.frequency == pft.AllFrequencies.M
|
||||||
|
|
||||||
|
def test_creation_no_freq_missing_data(self, create_test_data):
|
||||||
|
data = create_test_data(num=300, frequency=pft.AllFrequencies.D)
|
||||||
|
data = random.sample(data, 182)
|
||||||
|
ts = pft.TimeSeriesCore(data)
|
||||||
|
assert ts.frequency == pft.AllFrequencies.D
|
||||||
|
|
||||||
|
data = create_test_data(num=300, frequency=pft.AllFrequencies.D)
|
||||||
|
data = random.sample(data, 175)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts = pft.TimeSeriesCore(data)
|
||||||
|
|
||||||
|
data = create_test_data(num=100, frequency=pft.AllFrequencies.W)
|
||||||
|
data = random.sample(data, 70)
|
||||||
|
ts = pft.TimeSeriesCore(data)
|
||||||
|
assert ts.frequency == pft.AllFrequencies.W
|
||||||
|
|
||||||
|
data = create_test_data(num=100, frequency=pft.AllFrequencies.W)
|
||||||
|
data = random.sample(data, 68)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
pft.TimeSeriesCore(data)
|
||||||
|
|
||||||
|
def test_creation_wrong_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=100, frequency=pft.AllFrequencies.W)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
pft.TimeSeriesCore(data, frequency="D")
|
||||||
|
|
||||||
|
data = create_test_data(num=100, frequency=pft.AllFrequencies.D)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
pft.TimeSeriesCore(data, frequency="W")
|
||||||
|
|
||||||
|
|
||||||
class TestSlicing:
|
class TestSlicing:
|
||||||
data = [("2021-01-01", 220), ("2021-02-01", 230), ("2021-03-01", 240)]
|
data = [("2021-01-01", 220), ("2021-02-01", 230), ("2021-03-01", 240)]
|
||||||
|
|
||||||
def test_getitem(self):
|
def test_getitem(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert ts.dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
assert ts.dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
||||||
assert ts.values[0] == 220
|
assert ts.values[0] == 220
|
||||||
assert ts["2021-01-01"][1] == 220
|
assert ts["2021-01-01"][1] == 220
|
||||||
@ -129,11 +125,11 @@ class TestSlicing:
|
|||||||
ts["2021-02-03"]
|
ts["2021-02-03"]
|
||||||
subset_ts = ts[["2021-01-01", "2021-03-01"]]
|
subset_ts = ts[["2021-01-01", "2021-03-01"]]
|
||||||
assert len(subset_ts) == 2
|
assert len(subset_ts) == 2
|
||||||
assert isinstance(subset_ts, TimeSeriesCore)
|
assert isinstance(subset_ts, pft.TimeSeriesCore)
|
||||||
assert subset_ts.iloc[1][1] == 240
|
assert subset_ts.iloc[1][1] == 240
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert ts.dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
assert ts.dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
||||||
assert ts.values[0] == 220
|
assert ts.values[0] == 220
|
||||||
assert ts.get("2021-01-01")[1] == 220
|
assert ts.get("2021-01-01")[1] == 220
|
||||||
@ -141,44 +137,91 @@ class TestSlicing:
|
|||||||
assert ts.get("2021-02-23", -1) == -1
|
assert ts.get("2021-02-23", -1) == -1
|
||||||
assert ts.get("2021-02-10", closest="previous")[1] == 230
|
assert ts.get("2021-02-10", closest="previous")[1] == 230
|
||||||
assert ts.get("2021-02-10", closest="next")[1] == 240
|
assert ts.get("2021-02-10", closest="next")[1] == 240
|
||||||
FincalOptions.get_closest = "previous"
|
PyfactsOptions.get_closest = "previous"
|
||||||
assert ts.get("2021-02-10")[1] == 230
|
assert ts.get("2021-02-10")[1] == 230
|
||||||
FincalOptions.get_closest = "next"
|
PyfactsOptions.get_closest = "next"
|
||||||
assert ts.get("2021-02-10")[1] == 240
|
assert ts.get("2021-02-10")[1] == 240
|
||||||
|
|
||||||
def test_contains(self):
|
def test_contains(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert datetime.datetime(2021, 1, 1) in ts
|
assert datetime.datetime(2021, 1, 1) in ts
|
||||||
assert "2021-01-01" in ts
|
assert "2021-01-01" in ts
|
||||||
assert "2021-01-14" not in ts
|
assert "2021-01-14" not in ts
|
||||||
|
|
||||||
def test_items(self):
|
def test_items(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
for i, j in ts.items():
|
for i, j in ts.items():
|
||||||
assert j == self.data[0][1]
|
assert j == self.data[0][1]
|
||||||
break
|
break
|
||||||
|
|
||||||
def test_special_keys(self):
|
def test_special_keys(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
dates = ts["dates"]
|
dates = ts["dates"]
|
||||||
values = ts["values"]
|
values = ts["values"]
|
||||||
assert isinstance(dates, Series)
|
assert isinstance(dates, pft.Series)
|
||||||
assert isinstance(values, Series)
|
assert isinstance(values, pft.Series)
|
||||||
assert len(dates) == 3
|
assert len(dates) == 3
|
||||||
assert len(values) == 3
|
assert len(values) == 3
|
||||||
assert dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
assert dates[0] == datetime.datetime(2021, 1, 1, 0, 0)
|
||||||
assert values[0] == 220
|
assert values[0] == 220
|
||||||
|
|
||||||
def test_iloc_slicing(self):
|
def test_iloc_slicing(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert ts.iloc[0] == (datetime.datetime(2021, 1, 1), 220)
|
assert ts.iloc[0] == (datetime.datetime(2021, 1, 1), 220)
|
||||||
assert ts.iloc[-1] == (datetime.datetime(2021, 3, 1), 240)
|
assert ts.iloc[-1] == (datetime.datetime(2021, 3, 1), 240)
|
||||||
|
|
||||||
ts_slice = ts.iloc[0:2]
|
ts_slice = ts.iloc[0:2]
|
||||||
assert isinstance(ts_slice, TimeSeriesCore)
|
assert isinstance(ts_slice, pft.TimeSeriesCore)
|
||||||
assert len(ts_slice) == 2
|
assert len(ts_slice) == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestComparativeSlicing:
|
||||||
|
def test_date_gt_daily(self, create_test_data):
|
||||||
|
data = create_test_data(num=300, frequency=pft.AllFrequencies.D)
|
||||||
|
ts = pft.TimeSeries(data, "D")
|
||||||
|
ts_rr = ts.calculate_rolling_returns(return_period_unit="months")
|
||||||
|
assert len(ts_rr) == 269
|
||||||
|
subset = ts_rr[ts_rr.values < 0.1]
|
||||||
|
assert isinstance(subset, pft.TimeSeriesCore)
|
||||||
|
assert subset.frequency == pft.AllFrequencies.D
|
||||||
|
|
||||||
|
def test_date_gt_monthly(self, create_test_data):
|
||||||
|
data = create_test_data(num=60, frequency=pft.AllFrequencies.M)
|
||||||
|
ts = pft.TimeSeries(data, "M")
|
||||||
|
ts_rr = ts.calculate_rolling_returns(return_period_unit="months")
|
||||||
|
assert len(ts_rr) == 59
|
||||||
|
subset = ts_rr[ts_rr.values < 0.1]
|
||||||
|
assert isinstance(subset, pft.TimeSeriesCore)
|
||||||
|
assert subset.frequency == pft.AllFrequencies.M
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetitem:
|
||||||
|
data = [("2021-01-01", 220), ("2021-01-04", 230), ("2021-03-07", 240)]
|
||||||
|
|
||||||
|
def test_setitem(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
|
assert len(ts) == 3
|
||||||
|
|
||||||
|
ts["2021-01-02"] = 225
|
||||||
|
assert len(ts) == 4
|
||||||
|
assert ts["2021-01-02"][1] == 225
|
||||||
|
|
||||||
|
ts["2021-01-02"] = 227.6
|
||||||
|
assert len(ts) == 4
|
||||||
|
assert ts["2021-01-02"][1] == 227.6
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
ts["2021-01-03"] = "abc"
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
ts.iloc[4] = 4
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts["abc"] = 12
|
||||||
|
|
||||||
|
|
||||||
class TestTimeSeriesCoreHeadTail:
|
class TestTimeSeriesCoreHeadTail:
|
||||||
data = [
|
data = [
|
||||||
("2021-01-01", 220),
|
("2021-01-01", 220),
|
||||||
@ -196,24 +239,197 @@ class TestTimeSeriesCoreHeadTail:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def test_head(self):
|
def test_head(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert len(ts.head()) == 6
|
assert len(ts.head()) == 6
|
||||||
assert len(ts.head(3)) == 3
|
assert len(ts.head(3)) == 3
|
||||||
assert isinstance(ts.head(), TimeSeriesCore)
|
assert isinstance(ts.head(), pft.TimeSeriesCore)
|
||||||
head_ts = ts.head(6)
|
head_ts = ts.head(6)
|
||||||
assert head_ts.iloc[-1][1] == 270
|
assert head_ts.iloc[-1][1] == 270
|
||||||
|
|
||||||
def test_tail(self):
|
def test_tail(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
assert len(ts.tail()) == 6
|
assert len(ts.tail()) == 6
|
||||||
assert len(ts.tail(8)) == 8
|
assert len(ts.tail(8)) == 8
|
||||||
assert isinstance(ts.tail(), TimeSeriesCore)
|
assert isinstance(ts.tail(), pft.TimeSeriesCore)
|
||||||
tail_ts = ts.tail(6)
|
tail_ts = ts.tail(6)
|
||||||
assert tail_ts.iloc[0][1] == 280
|
assert tail_ts.iloc[0][1] == 280
|
||||||
|
|
||||||
def test_head_tail(self):
|
def test_head_tail(self):
|
||||||
ts = TimeSeriesCore(self.data, frequency="M")
|
ts = pft.TimeSeriesCore(self.data, frequency="M")
|
||||||
head_tail_ts = ts.head(8).tail(2)
|
head_tail_ts = ts.head(8).tail(2)
|
||||||
assert isinstance(head_tail_ts, TimeSeriesCore)
|
assert isinstance(head_tail_ts, pft.TimeSeriesCore)
|
||||||
assert "2021-07-01" in head_tail_ts
|
assert "2021-07-01" in head_tail_ts
|
||||||
assert head_tail_ts.iloc[1][1] == 290
|
assert head_tail_ts.iloc[1][1] == 290
|
||||||
|
|
||||||
|
|
||||||
|
class TestDelitem:
|
||||||
|
data = [
|
||||||
|
("2021-01-01", 220),
|
||||||
|
("2021-02-01", 230),
|
||||||
|
("2021-03-01", 240),
|
||||||
|
("2021-04-01", 250),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_deletion(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, "M")
|
||||||
|
assert len(ts) == 4
|
||||||
|
del ts["2021-03-01"]
|
||||||
|
assert len(ts) == 3
|
||||||
|
assert "2021-03-01" not in ts
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
del ts["2021-03-01"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeSeriesComparisons:
|
||||||
|
data1 = [
|
||||||
|
("2021-01-01", 220),
|
||||||
|
("2021-02-01", 230),
|
||||||
|
("2021-03-01", 240),
|
||||||
|
("2021-04-01", 250),
|
||||||
|
]
|
||||||
|
|
||||||
|
data2 = [
|
||||||
|
("2021-01-01", 240),
|
||||||
|
("2021-02-01", 210),
|
||||||
|
("2021-03-01", 240),
|
||||||
|
("2021-04-01", 270),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_number_comparison(self):
|
||||||
|
ts1 = pft.TimeSeriesCore(self.data1, "M")
|
||||||
|
assert isinstance(ts1 > 23, pft.TimeSeriesCore)
|
||||||
|
assert (ts1 > 230).values == pft.Series([0.0, 0.0, 1.0, 1.0], "float")
|
||||||
|
assert (ts1 >= 230).values == pft.Series([0.0, 1.0, 1.0, 1.0], "float")
|
||||||
|
assert (ts1 < 240).values == pft.Series([1.0, 1.0, 0.0, 0.0], "float")
|
||||||
|
assert (ts1 <= 240).values == pft.Series([1.0, 1.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 == 240).values == pft.Series([0.0, 0.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 != 240).values == pft.Series([1.0, 1.0, 0.0, 1.0], "float")
|
||||||
|
|
||||||
|
def test_series_comparison(self):
|
||||||
|
ts1 = pft.TimeSeriesCore(self.data1, "M")
|
||||||
|
ser = pft.Series([240, 210, 240, 270], dtype="int")
|
||||||
|
|
||||||
|
assert (ts1 > ser).values == pft.Series([0.0, 1.0, 0.0, 0.0], "float")
|
||||||
|
assert (ts1 >= ser).values == pft.Series([0.0, 1.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 < ser).values == pft.Series([1.0, 0.0, 0.0, 1.0], "float")
|
||||||
|
assert (ts1 <= ser).values == pft.Series([1.0, 0.0, 1.0, 1.0], "float")
|
||||||
|
assert (ts1 == ser).values == pft.Series([0.0, 0.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 != ser).values == pft.Series([1.0, 1.0, 0.0, 1.0], "float")
|
||||||
|
|
||||||
|
def test_tsc_comparison(self):
|
||||||
|
ts1 = pft.TimeSeriesCore(self.data1, "M")
|
||||||
|
ts2 = pft.TimeSeriesCore(self.data2, "M")
|
||||||
|
|
||||||
|
assert (ts1 > ts2).values == pft.Series([0.0, 1.0, 0.0, 0.0], "float")
|
||||||
|
assert (ts1 >= ts2).values == pft.Series([0.0, 1.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 < ts2).values == pft.Series([1.0, 0.0, 0.0, 1.0], "float")
|
||||||
|
assert (ts1 <= ts2).values == pft.Series([1.0, 0.0, 1.0, 1.0], "float")
|
||||||
|
assert (ts1 == ts2).values == pft.Series([0.0, 0.0, 1.0, 0.0], "float")
|
||||||
|
assert (ts1 != ts2).values == pft.Series([1.0, 1.0, 0.0, 1.0], "float")
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
ts1 = pft.TimeSeriesCore(self.data1, "M")
|
||||||
|
ts2 = pft.TimeSeriesCore(self.data2, "M")
|
||||||
|
ser = pft.Series([240, 210, 240], dtype="int")
|
||||||
|
ser2 = pft.Series(["2021-01-01", "2021-02-01", "2021-03-01", "2021-04-01"], dtype="date")
|
||||||
|
|
||||||
|
del ts2["2021-04-01"]
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
ts1 == "a"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts1 > ts2
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
ts1 == ser2
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts1 <= ser
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
ts2 < [23, 24, 25, 26]
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeSeriesArithmatic:
|
||||||
|
data = [
|
||||||
|
("2021-01-01", 220),
|
||||||
|
("2021-02-01", 230),
|
||||||
|
("2021-03-01", 240),
|
||||||
|
("2021-04-01", 250),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, "M")
|
||||||
|
ser = ts.values
|
||||||
|
|
||||||
|
num_add_ts = ts + 40
|
||||||
|
assert num_add_ts["2021-01-01"][1] == 260
|
||||||
|
assert num_add_ts["2021-04-01"][1] == 290
|
||||||
|
|
||||||
|
num_radd_ts = 40 + ts
|
||||||
|
assert num_radd_ts["2021-01-01"][1] == 260
|
||||||
|
assert num_radd_ts["2021-04-01"][1] == 290
|
||||||
|
|
||||||
|
ser_add_ts = ts + ser
|
||||||
|
assert ser_add_ts["2021-01-01"][1] == 440
|
||||||
|
assert ser_add_ts["2021-04-01"][1] == 500
|
||||||
|
|
||||||
|
ts_add_ts = ts + num_add_ts
|
||||||
|
assert ts_add_ts["2021-01-01"][1] == 480
|
||||||
|
assert ts_add_ts["2021-04-01"][1] == 540
|
||||||
|
|
||||||
|
def test_sub(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, "M")
|
||||||
|
ser = pft.Series([20, 30, 40, 50], "number")
|
||||||
|
|
||||||
|
num_sub_ts = ts - 40
|
||||||
|
assert num_sub_ts["2021-01-01"][1] == 180
|
||||||
|
assert num_sub_ts["2021-04-01"][1] == 210
|
||||||
|
|
||||||
|
num_rsub_ts = 240 - ts
|
||||||
|
assert num_rsub_ts["2021-01-01"][1] == 20
|
||||||
|
assert num_rsub_ts["2021-04-01"][1] == -10
|
||||||
|
|
||||||
|
ser_sub_ts = ts - ser
|
||||||
|
assert ser_sub_ts["2021-01-01"][1] == 200
|
||||||
|
assert ser_sub_ts["2021-04-01"][1] == 200
|
||||||
|
|
||||||
|
ts_sub_ts = ts - num_sub_ts
|
||||||
|
assert ts_sub_ts["2021-01-01"][1] == 40
|
||||||
|
assert ts_sub_ts["2021-04-01"][1] == 40
|
||||||
|
|
||||||
|
def test_truediv(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, "M")
|
||||||
|
ser = pft.Series([22, 23, 24, 25], "number")
|
||||||
|
|
||||||
|
num_div_ts = ts / 10
|
||||||
|
assert num_div_ts["2021-01-01"][1] == 22
|
||||||
|
assert num_div_ts["2021-04-01"][1] == 25
|
||||||
|
|
||||||
|
num_rdiv_ts = 1000 / ts
|
||||||
|
assert num_rdiv_ts["2021-04-01"][1] == 4
|
||||||
|
|
||||||
|
ser_div_ts = ts / ser
|
||||||
|
assert ser_div_ts["2021-01-01"][1] == 10
|
||||||
|
assert ser_div_ts["2021-04-01"][1] == 10
|
||||||
|
|
||||||
|
ts_div_ts = ts / num_div_ts
|
||||||
|
assert ts_div_ts["2021-01-01"][1] == 10
|
||||||
|
assert ts_div_ts["2021-04-01"][1] == 10
|
||||||
|
|
||||||
|
def test_floordiv(self):
|
||||||
|
ts = pft.TimeSeriesCore(self.data, "M")
|
||||||
|
ser = pft.Series([22, 23, 24, 25], "number")
|
||||||
|
|
||||||
|
num_div_ts = ts // 11
|
||||||
|
assert num_div_ts["2021-02-01"][1] == 20
|
||||||
|
assert num_div_ts["2021-04-01"][1] == 22
|
||||||
|
|
||||||
|
num_rdiv_ts = 1000 // ts
|
||||||
|
assert num_rdiv_ts["2021-01-01"][1] == 4
|
||||||
|
|
||||||
|
ser_div_ts = ts // ser
|
||||||
|
assert ser_div_ts["2021-01-01"][1] == 10
|
||||||
|
assert ser_div_ts["2021-04-01"][1] == 10
|
||||||
|
@ -1,101 +1,15 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import math
|
|
||||||
import random
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from fincal import (
|
from pyfacts import (
|
||||||
AllFrequencies,
|
AllFrequencies,
|
||||||
FincalOptions,
|
|
||||||
Frequency,
|
Frequency,
|
||||||
|
PyfactsOptions,
|
||||||
TimeSeries,
|
TimeSeries,
|
||||||
create_date_series,
|
create_date_series,
|
||||||
)
|
)
|
||||||
from fincal.exceptions import DateNotFoundError
|
from pyfacts.exceptions import DateNotFoundError
|
||||||
|
|
||||||
|
|
||||||
def create_prices(s0: float, mu: float, sigma: float, num_prices: int) -> list:
|
|
||||||
"""Generates a price following a geometric brownian motion process based on the input of the arguments.
|
|
||||||
|
|
||||||
Since this function is used only to generate data for tests, the seed is fixed as 1234.
|
|
||||||
Many of the tests rely on exact values generated using this seed.
|
|
||||||
If the seed is changed, those tests will fail.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
------------
|
|
||||||
s0: float
|
|
||||||
Asset inital price.
|
|
||||||
|
|
||||||
mu: float
|
|
||||||
Interest rate expressed annual terms.
|
|
||||||
|
|
||||||
sigma: float
|
|
||||||
Volatility expressed annual terms.
|
|
||||||
|
|
||||||
num_prices: int
|
|
||||||
number of prices to generate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
--------
|
|
||||||
Returns a list of values generated using GBM algorithm
|
|
||||||
"""
|
|
||||||
|
|
||||||
random.seed(1234) # WARNING! Changing the seed will cause most tests to fail
|
|
||||||
all_values = []
|
|
||||||
for _ in range(num_prices):
|
|
||||||
s0 *= math.exp(
|
|
||||||
(mu - 0.5 * sigma**2) * (1.0 / 365.0) + sigma * math.sqrt(1.0 / 365.0) * random.gauss(mu=0, sigma=1)
|
|
||||||
)
|
|
||||||
all_values.append(round(s0, 2))
|
|
||||||
|
|
||||||
return all_values
|
|
||||||
|
|
||||||
|
|
||||||
def create_test_data(
|
|
||||||
frequency: Frequency,
|
|
||||||
num: int = 1000,
|
|
||||||
skip_weekends: bool = False,
|
|
||||||
mu: float = 0.1,
|
|
||||||
sigma: float = 0.05,
|
|
||||||
eomonth: bool = False,
|
|
||||||
) -> List[tuple]:
|
|
||||||
"""Creates TimeSeries data
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
-----------
|
|
||||||
frequency: Frequency
|
|
||||||
The frequency of the time series data to be generated.
|
|
||||||
|
|
||||||
num: int
|
|
||||||
Number of date: value pairs to be generated.
|
|
||||||
|
|
||||||
skip_weekends: bool
|
|
||||||
Whether weekends (saturday, sunday) should be skipped.
|
|
||||||
Gets used only if the frequency is daily.
|
|
||||||
|
|
||||||
mu: float
|
|
||||||
Mean return for the values.
|
|
||||||
|
|
||||||
sigma: float
|
|
||||||
standard deviation of the values.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
--------
|
|
||||||
Returns a TimeSeries object
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_date = datetime.datetime(2017, 1, 1)
|
|
||||||
timedelta_dict = {
|
|
||||||
frequency.freq_type: int(
|
|
||||||
frequency.value * num * (7 / 5 if frequency == AllFrequencies.D and skip_weekends else 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
end_date = start_date + relativedelta(**timedelta_dict)
|
|
||||||
dates = create_date_series(start_date, end_date, frequency.symbol, skip_weekends=skip_weekends, eomonth=eomonth)
|
|
||||||
values = create_prices(1000, mu, sigma, num)
|
|
||||||
ts = list(zip(dates, values))
|
|
||||||
return ts
|
|
||||||
|
|
||||||
|
|
||||||
class TestDateSeries:
|
class TestDateSeries:
|
||||||
@ -116,7 +30,7 @@ class TestDateSeries:
|
|||||||
def test_monthly(self):
|
def test_monthly(self):
|
||||||
start_date = datetime.datetime(2020, 1, 1)
|
start_date = datetime.datetime(2020, 1, 1)
|
||||||
end_date = datetime.datetime(2020, 12, 31)
|
end_date = datetime.datetime(2020, 12, 31)
|
||||||
d = create_date_series(start_date, end_date, frequency="M")
|
d = create_date_series(start_date, end_date, frequency="M", ensure_coverage=False)
|
||||||
assert len(d) == 12
|
assert len(d) == 12
|
||||||
|
|
||||||
d = create_date_series(start_date, end_date, frequency="M", eomonth=True)
|
d = create_date_series(start_date, end_date, frequency="M", eomonth=True)
|
||||||
@ -161,14 +75,14 @@ class TestDateSeries:
|
|||||||
|
|
||||||
|
|
||||||
class TestTimeSeriesCreation:
|
class TestTimeSeriesCreation:
|
||||||
def test_creation_with_list_of_tuples(self):
|
def test_creation_with_list_of_tuples(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
||||||
ts = TimeSeries(ts_data, frequency="D")
|
ts = TimeSeries(ts_data, frequency="D")
|
||||||
assert len(ts) == 50
|
assert len(ts) == 50
|
||||||
assert isinstance(ts.frequency, Frequency)
|
assert isinstance(ts.frequency, Frequency)
|
||||||
assert ts.frequency.days == 1
|
assert ts.frequency.days == 1
|
||||||
|
|
||||||
def test_creation_with_string_dates(self):
|
def test_creation_with_string_dates(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
||||||
ts_data1 = [(dt.strftime("%Y-%m-%d"), val) for dt, val in ts_data]
|
ts_data1 = [(dt.strftime("%Y-%m-%d"), val) for dt, val in ts_data]
|
||||||
ts = TimeSeries(ts_data1, frequency="D")
|
ts = TimeSeries(ts_data1, frequency="D")
|
||||||
@ -186,19 +100,19 @@ class TestTimeSeriesCreation:
|
|||||||
ts = TimeSeries(ts_data1, frequency="D", date_format="%m-%d-%Y %H:%M")
|
ts = TimeSeries(ts_data1, frequency="D", date_format="%m-%d-%Y %H:%M")
|
||||||
datetime.datetime(2017, 1, 1, 0, 0) in ts
|
datetime.datetime(2017, 1, 1, 0, 0) in ts
|
||||||
|
|
||||||
def test_creation_with_list_of_dicts(self):
|
def test_creation_with_list_of_dicts(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
||||||
ts_data1 = [{"date": dt.strftime("%Y-%m-%d"), "value": val} for dt, val in ts_data]
|
ts_data1 = [{"date": dt.strftime("%Y-%m-%d"), "value": val} for dt, val in ts_data]
|
||||||
ts = TimeSeries(ts_data1, frequency="D")
|
ts = TimeSeries(ts_data1, frequency="D")
|
||||||
datetime.datetime(2017, 1, 1) in ts
|
datetime.datetime(2017, 1, 1) in ts
|
||||||
|
|
||||||
def test_creation_with_list_of_lists(self):
|
def test_creation_with_list_of_lists(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
||||||
ts_data1 = [[dt.strftime("%Y-%m-%d"), val] for dt, val in ts_data]
|
ts_data1 = [[dt.strftime("%Y-%m-%d"), val] for dt, val in ts_data]
|
||||||
ts = TimeSeries(ts_data1, frequency="D")
|
ts = TimeSeries(ts_data1, frequency="D")
|
||||||
datetime.datetime(2017, 1, 1) in ts
|
datetime.datetime(2017, 1, 1) in ts
|
||||||
|
|
||||||
def test_creation_with_dict(self):
|
def test_creation_with_dict(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50)
|
||||||
ts_data1 = [{dt.strftime("%Y-%m-%d"): val} for dt, val in ts_data]
|
ts_data1 = [{dt.strftime("%Y-%m-%d"): val} for dt, val in ts_data]
|
||||||
ts = TimeSeries(ts_data1, frequency="D")
|
ts = TimeSeries(ts_data1, frequency="D")
|
||||||
@ -206,7 +120,8 @@ class TestTimeSeriesCreation:
|
|||||||
|
|
||||||
|
|
||||||
class TestTimeSeriesBasics:
|
class TestTimeSeriesBasics:
|
||||||
def test_fill(self):
|
def test_fill(self, create_test_data):
|
||||||
|
PyfactsOptions.get_closest = "exact"
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.D, num=50, skip_weekends=True)
|
ts_data = create_test_data(frequency=AllFrequencies.D, num=50, skip_weekends=True)
|
||||||
ts = TimeSeries(ts_data, frequency="D")
|
ts = TimeSeries(ts_data, frequency="D")
|
||||||
ffill_data = ts.ffill()
|
ffill_data = ts.ffill()
|
||||||
@ -233,7 +148,7 @@ class TestTimeSeriesBasics:
|
|||||||
bf = ts.bfill()
|
bf = ts.bfill()
|
||||||
assert bf["2021-01-03"][1] == 240
|
assert bf["2021-01-03"][1] == 240
|
||||||
|
|
||||||
def test_fill_weekly(self):
|
def test_fill_weekly(self, create_test_data):
|
||||||
ts_data = create_test_data(frequency=AllFrequencies.W, num=10)
|
ts_data = create_test_data(frequency=AllFrequencies.W, num=10)
|
||||||
ts_data.pop(2)
|
ts_data.pop(2)
|
||||||
ts_data.pop(6)
|
ts_data.pop(6)
|
||||||
@ -250,9 +165,60 @@ class TestTimeSeriesBasics:
|
|||||||
assert "2017-01-15" in bf
|
assert "2017-01-15" in bf
|
||||||
assert bf["2017-01-15"][1] == bf["2017-01-22"][1]
|
assert bf["2017-01-15"][1] == bf["2017-01-22"][1]
|
||||||
|
|
||||||
|
def test_fill_monthly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(frequency=AllFrequencies.M, num=10)
|
||||||
|
ts_data.pop(2)
|
||||||
|
ts_data.pop(6)
|
||||||
|
ts = TimeSeries(ts_data, frequency="M")
|
||||||
|
assert len(ts) == 8
|
||||||
|
|
||||||
|
ff = ts.ffill()
|
||||||
|
assert len(ff) == 10
|
||||||
|
assert "2017-03-01" in ff
|
||||||
|
assert ff["2017-03-01"][1] == ff["2017-02-01"][1]
|
||||||
|
|
||||||
|
bf = ts.bfill()
|
||||||
|
assert len(bf) == 10
|
||||||
|
assert "2017-08-01" in bf
|
||||||
|
assert bf["2017-08-01"][1] == bf["2017-09-01"][1]
|
||||||
|
|
||||||
|
def test_fill_eomonthly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(frequency=AllFrequencies.M, num=10, eomonth=True)
|
||||||
|
ts_data.pop(2)
|
||||||
|
ts_data.pop(6)
|
||||||
|
ts = TimeSeries(ts_data, frequency="M")
|
||||||
|
assert len(ts) == 8
|
||||||
|
|
||||||
|
ff = ts.ffill()
|
||||||
|
assert len(ff) == 10
|
||||||
|
assert "2017-03-31" in ff
|
||||||
|
assert ff["2017-03-31"][1] == ff["2017-02-28"][1]
|
||||||
|
|
||||||
|
bf = ts.bfill()
|
||||||
|
assert len(bf) == 10
|
||||||
|
assert "2017-08-31" in bf
|
||||||
|
assert bf["2017-08-31"][1] == bf["2017-09-30"][1]
|
||||||
|
|
||||||
|
def test_fill_quarterly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(frequency=AllFrequencies.Q, num=10, eomonth=True)
|
||||||
|
ts_data.pop(2)
|
||||||
|
ts_data.pop(6)
|
||||||
|
ts = TimeSeries(ts_data, frequency="Q")
|
||||||
|
assert len(ts) == 8
|
||||||
|
|
||||||
|
ff = ts.ffill()
|
||||||
|
assert len(ff) == 10
|
||||||
|
assert "2017-07-31" in ff
|
||||||
|
assert ff["2017-07-31"][1] == ff["2017-04-30"][1]
|
||||||
|
|
||||||
|
bf = ts.bfill()
|
||||||
|
assert len(bf) == 10
|
||||||
|
assert "2018-10-31" in bf
|
||||||
|
assert bf["2018-10-31"][1] == bf["2019-01-31"][1]
|
||||||
|
|
||||||
|
|
||||||
class TestReturns:
|
class TestReturns:
|
||||||
def test_returns_calc(self):
|
def test_returns_calc(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
||||||
ts = TimeSeries(ts_data, "D")
|
ts = TimeSeries(ts_data, "D")
|
||||||
returns = ts.calculate_returns(
|
returns = ts.calculate_returns(
|
||||||
@ -283,12 +249,12 @@ class TestReturns:
|
|||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact")
|
ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact")
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact")
|
ts.calculate_returns("2020-04-08", return_period_unit="months", return_period_value=1, prior_match="exact")
|
||||||
|
|
||||||
def test_date_formats(self):
|
def test_date_formats(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
||||||
ts = TimeSeries(ts_data, "D")
|
ts = TimeSeries(ts_data, "D")
|
||||||
FincalOptions.date_format = "%d-%m-%Y"
|
PyfactsOptions.date_format = "%d-%m-%Y"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts.calculate_returns(
|
ts.calculate_returns(
|
||||||
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
||||||
@ -300,7 +266,7 @@ class TestReturns:
|
|||||||
returns2 = ts.calculate_returns("01-04-2020", return_period_unit="days", return_period_value=90)
|
returns2 = ts.calculate_returns("01-04-2020", return_period_unit="days", return_period_value=90)
|
||||||
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632
|
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632
|
||||||
|
|
||||||
FincalOptions.date_format = "%m-%d-%Y"
|
PyfactsOptions.date_format = "%m-%d-%Y"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts.calculate_returns(
|
ts.calculate_returns(
|
||||||
"2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
||||||
@ -312,61 +278,61 @@ class TestReturns:
|
|||||||
returns2 = ts.calculate_returns("04-01-2020", return_period_unit="days", return_period_value=90)
|
returns2 = ts.calculate_returns("04-01-2020", return_period_unit="days", return_period_value=90)
|
||||||
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632
|
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632
|
||||||
|
|
||||||
def test_limits(self):
|
def test_limits(self, create_test_data):
|
||||||
FincalOptions.date_format = "%Y-%m-%d"
|
PyfactsOptions.date_format = "%Y-%m-%d"
|
||||||
ts_data = create_test_data(AllFrequencies.D)
|
ts_data = create_test_data(AllFrequencies.D)
|
||||||
ts = TimeSeries(ts_data, "D")
|
ts = TimeSeries(ts_data, "D")
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
||||||
|
|
||||||
def test_rolling_returns(self):
|
def test_rolling_returns(self):
|
||||||
# Yet to be written
|
# To-do
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestExpand:
|
class TestExpand:
|
||||||
def test_weekly_to_daily(self):
|
def test_weekly_to_daily(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.W, 10)
|
ts_data = create_test_data(AllFrequencies.W, num=10)
|
||||||
ts = TimeSeries(ts_data, "W")
|
ts = TimeSeries(ts_data, "W")
|
||||||
expanded_ts = ts.expand("D", "ffill")
|
expanded_ts = ts.expand("D", "ffill")
|
||||||
assert len(expanded_ts) == 64
|
assert len(expanded_ts) == 64
|
||||||
assert expanded_ts.frequency.name == "daily"
|
assert expanded_ts.frequency.name == "daily"
|
||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
def test_weekly_to_daily_no_weekends(self):
|
def test_weekly_to_daily_no_weekends(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.W, 10)
|
ts_data = create_test_data(AllFrequencies.W, num=10)
|
||||||
ts = TimeSeries(ts_data, "W")
|
ts = TimeSeries(ts_data, "W")
|
||||||
expanded_ts = ts.expand("D", "ffill", skip_weekends=True)
|
expanded_ts = ts.expand("D", "ffill", skip_weekends=True)
|
||||||
assert len(expanded_ts) == 46
|
assert len(expanded_ts) == 46
|
||||||
assert expanded_ts.frequency.name == "daily"
|
assert expanded_ts.frequency.name == "daily"
|
||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
def test_monthly_to_daily(self):
|
def test_monthly_to_daily(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.M, 6)
|
ts_data = create_test_data(AllFrequencies.M, num=6)
|
||||||
ts = TimeSeries(ts_data, "M")
|
ts = TimeSeries(ts_data, "M")
|
||||||
expanded_ts = ts.expand("D", "ffill")
|
expanded_ts = ts.expand("D", "ffill")
|
||||||
assert len(expanded_ts) == 152
|
assert len(expanded_ts) == 152
|
||||||
assert expanded_ts.frequency.name == "daily"
|
assert expanded_ts.frequency.name == "daily"
|
||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
def test_monthly_to_daily_no_weekends(self):
|
def test_monthly_to_daily_no_weekends(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.M, 6)
|
ts_data = create_test_data(AllFrequencies.M, num=6)
|
||||||
ts = TimeSeries(ts_data, "M")
|
ts = TimeSeries(ts_data, "M")
|
||||||
expanded_ts = ts.expand("D", "ffill", skip_weekends=True)
|
expanded_ts = ts.expand("D", "ffill", skip_weekends=True)
|
||||||
assert len(expanded_ts) == 109
|
assert len(expanded_ts) == 109
|
||||||
assert expanded_ts.frequency.name == "daily"
|
assert expanded_ts.frequency.name == "daily"
|
||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
def test_monthly_to_weekly(self):
|
def test_monthly_to_weekly(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.M, 6)
|
ts_data = create_test_data(AllFrequencies.M, num=6)
|
||||||
ts = TimeSeries(ts_data, "M")
|
ts = TimeSeries(ts_data, "M")
|
||||||
expanded_ts = ts.expand("W", "ffill")
|
expanded_ts = ts.expand("W", "ffill")
|
||||||
assert len(expanded_ts) == 22
|
assert len(expanded_ts) == 23
|
||||||
assert expanded_ts.frequency.name == "weekly"
|
assert expanded_ts.frequency.name == "weekly"
|
||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
def test_yearly_to_monthly(self):
|
def test_yearly_to_monthly(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.Y, 5)
|
ts_data = create_test_data(AllFrequencies.Y, num=5)
|
||||||
ts = TimeSeries(ts_data, "Y")
|
ts = TimeSeries(ts_data, "Y")
|
||||||
expanded_ts = ts.expand("M", "ffill")
|
expanded_ts = ts.expand("M", "ffill")
|
||||||
assert len(expanded_ts) == 49
|
assert len(expanded_ts) == 49
|
||||||
@ -374,6 +340,100 @@ class TestExpand:
|
|||||||
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1]
|
||||||
|
|
||||||
|
|
||||||
|
class TestShrink:
|
||||||
|
def test_daily_to_smaller(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.D, num=1000)
|
||||||
|
ts = TimeSeries(ts_data, "D")
|
||||||
|
shrunk_ts_w = ts.shrink("W", "ffill")
|
||||||
|
shrunk_ts_m = ts.shrink("M", "ffill")
|
||||||
|
assert len(shrunk_ts_w) == 144
|
||||||
|
assert len(shrunk_ts_m) == 34
|
||||||
|
|
||||||
|
def test_weekly_to_smaller(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.W, num=300)
|
||||||
|
ts = TimeSeries(ts_data, "W")
|
||||||
|
tsm = ts.shrink("M", "ffill")
|
||||||
|
assert len(tsm) == 70
|
||||||
|
tsmeo = ts.shrink("M", "ffill", eomonth=True)
|
||||||
|
assert len(tsmeo) == 69
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts.shrink("D", "ffill")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMeanReturns:
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestReadCsv:
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransform:
|
||||||
|
def test_daily_to_weekly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.D, num=782, skip_weekends=True)
|
||||||
|
ts = TimeSeries(ts_data, "D")
|
||||||
|
tst = ts.transform("W", "mean", ensure_coverage=False)
|
||||||
|
assert isinstance(tst, TimeSeries)
|
||||||
|
assert len(tst) == 157
|
||||||
|
assert "2017-01-30" in tst
|
||||||
|
assert tst.iloc[4] == (datetime.datetime(2017, 1, 30), 1020.082)
|
||||||
|
|
||||||
|
def test_daily_to_monthly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.D, num=782, skip_weekends=False)
|
||||||
|
ts = TimeSeries(ts_data, "D")
|
||||||
|
tst = ts.transform("M", "mean")
|
||||||
|
assert isinstance(tst, TimeSeries)
|
||||||
|
assert len(tst) == 27
|
||||||
|
assert "2018-01-01" in tst
|
||||||
|
assert round(tst.iloc[12][1], 2) == 1146.91
|
||||||
|
|
||||||
|
def test_daily_to_yearly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.D, num=782, skip_weekends=True)
|
||||||
|
ts = TimeSeries(ts_data, "D")
|
||||||
|
tst = ts.transform("Y", "mean")
|
||||||
|
assert isinstance(tst, TimeSeries)
|
||||||
|
assert len(tst) == 4
|
||||||
|
assert "2019-01-02" in tst
|
||||||
|
assert tst.iloc[2] == (datetime.datetime(2019, 1, 2), 1157.2835632183908)
|
||||||
|
|
||||||
|
def test_weekly_to_monthly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.W, num=261)
|
||||||
|
ts = TimeSeries(ts_data, "W")
|
||||||
|
tst = ts.transform("M", "mean")
|
||||||
|
assert isinstance(tst, TimeSeries)
|
||||||
|
assert "2017-01-01" in tst
|
||||||
|
assert tst.iloc[1] == (datetime.datetime(2017, 2, 1), 1008.405)
|
||||||
|
|
||||||
|
def test_weekly_to_qty(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.W, num=261)
|
||||||
|
ts = TimeSeries(ts_data, "W")
|
||||||
|
tst = ts.transform("Q", "mean")
|
||||||
|
assert len(tst) == 21
|
||||||
|
assert "2018-01-01" in tst
|
||||||
|
assert round(tst.iloc[4][1], 2) == 1032.01
|
||||||
|
|
||||||
|
def test_weekly_to_yearly(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.W, num=261)
|
||||||
|
ts = TimeSeries(ts_data, "W")
|
||||||
|
tst = ts.transform("Y", "mean")
|
||||||
|
assert "2019-01-01" in tst
|
||||||
|
assert round(tst.iloc[2][1], 2) == 1053.70
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts.transform("D", "mean")
|
||||||
|
|
||||||
|
def test_monthly_to_qty(self, create_test_data):
|
||||||
|
ts_data = create_test_data(AllFrequencies.M, num=36)
|
||||||
|
ts = TimeSeries(ts_data, "M")
|
||||||
|
tst = ts.transform("Q", "mean")
|
||||||
|
assert len(tst) == 13
|
||||||
|
assert "2018-10-01" in tst
|
||||||
|
assert tst.iloc[7] == (datetime.datetime(2018, 10, 1), 1022.6466666666666)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ts.transform("M", "sum")
|
||||||
|
|
||||||
|
|
||||||
class TestReturnsAgain:
|
class TestReturnsAgain:
|
||||||
data = [
|
data = [
|
||||||
("2020-01-01", 10),
|
("2020-01-01", 10),
|
||||||
@ -424,7 +484,7 @@ class TestReturnsAgain:
|
|||||||
|
|
||||||
def test_date_formats(self):
|
def test_date_formats(self):
|
||||||
ts = TimeSeries(self.data, frequency="M")
|
ts = TimeSeries(self.data, frequency="M")
|
||||||
FincalOptions.date_format = "%d-%m-%Y"
|
PyfactsOptions.date_format = "%d-%m-%Y"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts.calculate_returns(
|
ts.calculate_returns(
|
||||||
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
||||||
@ -436,7 +496,7 @@ class TestReturnsAgain:
|
|||||||
returns2 = ts.calculate_returns("10-04-2020", return_period_unit="days", return_period_value=90)
|
returns2 = ts.calculate_returns("10-04-2020", return_period_unit="days", return_period_value=90)
|
||||||
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727
|
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727
|
||||||
|
|
||||||
FincalOptions.date_format = "%m-%d-%Y"
|
PyfactsOptions.date_format = "%m-%d-%Y"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts.calculate_returns(
|
ts.calculate_returns(
|
||||||
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
||||||
@ -450,13 +510,13 @@ class TestReturnsAgain:
|
|||||||
|
|
||||||
def test_limits(self):
|
def test_limits(self):
|
||||||
ts = TimeSeries(self.data, frequency="M")
|
ts = TimeSeries(self.data, frequency="M")
|
||||||
FincalOptions.date_format = "%Y-%m-%d"
|
PyfactsOptions.date_format = "%Y-%m-%d"
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-04-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
ts.calculate_returns("2020-04-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
||||||
|
|
||||||
|
|
||||||
class TestVolatility:
|
class TestVolatility:
|
||||||
def test_daily_ts(self):
|
def test_daily_ts(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.D)
|
ts_data = create_test_data(AllFrequencies.D)
|
||||||
ts = TimeSeries(ts_data, "D")
|
ts = TimeSeries(ts_data, "D")
|
||||||
assert len(ts) == 1000
|
assert len(ts) == 1000
|
||||||
@ -484,7 +544,7 @@ class TestVolatility:
|
|||||||
|
|
||||||
|
|
||||||
class TestDrawdown:
|
class TestDrawdown:
|
||||||
def test_daily_ts(self):
|
def test_daily_ts(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)
|
||||||
ts = TimeSeries(ts_data, "D")
|
ts = TimeSeries(ts_data, "D")
|
||||||
mdd = ts.max_drawdown()
|
mdd = ts.max_drawdown()
|
||||||
@ -498,7 +558,7 @@ class TestDrawdown:
|
|||||||
}
|
}
|
||||||
assert mdd == expeced_response
|
assert mdd == expeced_response
|
||||||
|
|
||||||
def test_weekly_ts(self):
|
def test_weekly_ts(self, create_test_data):
|
||||||
ts_data = create_test_data(AllFrequencies.W, mu=1, sigma=0.5)
|
ts_data = create_test_data(AllFrequencies.W, mu=1, sigma=0.5)
|
||||||
ts = TimeSeries(ts_data, "W")
|
ts = TimeSeries(ts_data, "W")
|
||||||
mdd = ts.max_drawdown()
|
mdd = ts.max_drawdown()
|
||||||
@ -514,7 +574,7 @@ class TestDrawdown:
|
|||||||
|
|
||||||
|
|
||||||
class TestSync:
|
class TestSync:
|
||||||
def test_weekly_to_daily(self):
|
def test_weekly_to_daily(self, create_test_data):
|
||||||
daily_data = create_test_data(AllFrequencies.D, num=15)
|
daily_data = create_test_data(AllFrequencies.D, num=15)
|
||||||
weekly_data = create_test_data(AllFrequencies.W, num=3)
|
weekly_data = create_test_data(AllFrequencies.W, num=3)
|
||||||
|
|
172
tests/test_stats.py
Normal file
172
tests/test_stats.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import pyfacts as pft
|
||||||
|
|
||||||
|
|
||||||
|
def test_conf(conf_fun):
|
||||||
|
conf_add = conf_fun
|
||||||
|
assert conf_add(2, 4) == 6
|
||||||
|
|
||||||
|
|
||||||
|
class TestSharpe:
|
||||||
|
def test_sharpe_daily_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=1305, frequency=pft.AllFrequencies.D, skip_weekends=True)
|
||||||
|
ts = pft.TimeSeries(data, "D")
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
from_date="2017-02-04",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 1.0502
|
||||||
|
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
from_date="2017-01-09",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="days",
|
||||||
|
return_period_value=7,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 1.0701
|
||||||
|
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
from_date="2018-01-02",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="years",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 1.4374
|
||||||
|
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
from_date="2017-07-03",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=6,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 0.8401
|
||||||
|
|
||||||
|
def test_sharpe_weekly_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=261, frequency=pft.AllFrequencies.W, mu=0.6, sigma=0.7)
|
||||||
|
ts = pft.TimeSeries(data, "W")
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.052,
|
||||||
|
from_date="2017-01-08",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="days",
|
||||||
|
return_period_value=7,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 0.4533
|
||||||
|
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.052,
|
||||||
|
from_date="2017-02-05",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 0.4898
|
||||||
|
|
||||||
|
sharpe_ratio = pft.sharpe_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.052,
|
||||||
|
from_date="2018-01-01",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=12,
|
||||||
|
)
|
||||||
|
assert round(sharpe_ratio, 4) == 0.3199
|
||||||
|
|
||||||
|
|
||||||
|
class TestSortino:
|
||||||
|
def test_sortino_daily_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=3600, frequency=pft.AllFrequencies.D, mu=0.12, sigma=0.12)
|
||||||
|
ts = pft.TimeSeries(data, "D")
|
||||||
|
sortino_ratio = pft.sortino_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06 / 12,
|
||||||
|
from_date="2017-02-02",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sortino_ratio, 4) == 1.625
|
||||||
|
|
||||||
|
sortino_ratio = pft.sortino_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
from_date="2018-01-02",
|
||||||
|
return_period_unit="years",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sortino_ratio, 4) == 1.2564
|
||||||
|
|
||||||
|
def test_sortino_weekly_freq(self, create_test_data):
|
||||||
|
data = create_test_data(num=500, frequency=pft.AllFrequencies.W, mu=0.12, sigma=0.06)
|
||||||
|
ts = pft.TimeSeries(data, "W")
|
||||||
|
sortino = pft.sortino_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.06,
|
||||||
|
return_period_unit="years",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sortino, 4) == -5.5233
|
||||||
|
|
||||||
|
sortino = pft.sortino_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.052,
|
||||||
|
from_date="2017-02-05",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=1,
|
||||||
|
)
|
||||||
|
assert round(sortino, 4) == -1.93
|
||||||
|
|
||||||
|
sortino = pft.sortino_ratio(
|
||||||
|
ts,
|
||||||
|
risk_free_rate=0.052,
|
||||||
|
from_date="2018-01-01",
|
||||||
|
to_date="2021-12-31",
|
||||||
|
return_period_unit="months",
|
||||||
|
return_period_value=12,
|
||||||
|
)
|
||||||
|
assert round(sortino, 4) == -3.9805
|
||||||
|
|
||||||
|
|
||||||
|
class TestBeta:
|
||||||
|
def test_beta_daily_freq(self, create_test_data):
|
||||||
|
market_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D)
|
||||||
|
stock_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D, mu=0.12, sigma=0.08)
|
||||||
|
sts = pft.TimeSeries(stock_data, "D")
|
||||||
|
mts = pft.TimeSeries(market_data, "D")
|
||||||
|
beta = pft.beta(sts, mts, frequency="D", return_period_unit="days", return_period_value=1)
|
||||||
|
assert round(beta, 4) == 1.5997
|
||||||
|
|
||||||
|
def test_beta_daily_freq_daily_returns(self, create_test_data):
|
||||||
|
market_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D)
|
||||||
|
stock_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D, mu=0.12, sigma=0.08)
|
||||||
|
sts = pft.TimeSeries(stock_data, "D")
|
||||||
|
mts = pft.TimeSeries(market_data, "D")
|
||||||
|
beta = pft.beta(sts, mts)
|
||||||
|
assert round(beta, 4) == 1.6287
|
||||||
|
|
||||||
|
def test_beta_monthly_freq(self, create_test_data):
|
||||||
|
market_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D)
|
||||||
|
stock_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D, mu=0.12, sigma=0.08)
|
||||||
|
sts = pft.TimeSeries(stock_data, "D")
|
||||||
|
mts = pft.TimeSeries(market_data, "D")
|
||||||
|
beta = pft.beta(sts, mts, frequency="M")
|
||||||
|
assert round(beta, 4) == 1.6131
|
||||||
|
|
||||||
|
def test_beta_monthly_freq_monthly_returns(self, create_test_data):
|
||||||
|
market_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D)
|
||||||
|
stock_data = create_test_data(num=3600, frequency=pft.AllFrequencies.D, mu=0.12, sigma=0.08)
|
||||||
|
sts = pft.TimeSeries(stock_data, "D")
|
||||||
|
mts = pft.TimeSeries(market_data, "D")
|
||||||
|
beta = pft.beta(sts, mts, frequency="M", return_period_unit="months", return_period_value=1)
|
||||||
|
assert round(beta, 4) == 1.5887
|
@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fincal.utils import _interval_to_years, _parse_date
|
from pyfacts.utils import _interval_to_years, _parse_date
|
||||||
|
|
||||||
|
|
||||||
class TestParseDate:
|
class TestParseDate:
|
||||||
|
Loading…
Reference in New Issue
Block a user