-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathyc_date.py
233 lines (192 loc) · 7.36 KB
/
yc_date.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Copyright © 2017 Ondrej Martinsky, All rights reserved
# http://github.com/omartinsky/pybor
from dateutil.relativedelta import relativedelta
import dateutil.parser
import enum, calendar, datetime
import datetime as dt
import numpy as np
class Tenor:
def __init__(self, s):
try:
assert isinstance(s, str)
self.string = s
self.n = s[:-1]
self.n = int(self.n) if self.n != "" else 0
self.unit = s[-1:]
assert self.unit in ['F', 'D', 'M', 'Q', 'Y']
except BaseException as ex:
raise BaseException("Unable to parse tenor %s" % s) from ex
def __eq__(self, other):
return self.string == other.string
def __neg__(self):
return Tenor("-%s" % self.string) if self.string[0] != "-" else Tenor(self.string[1:])
def __str__(self):
return self.string
class RollType(enum.Enum):
NONE = 0
FOLLOWING = 1
PRECEDING = 2
MODIFIED_FOLLOWING = 3
MODIFIED_PRECEDING = 4
class StubType(enum.Enum):
STUB_NOT_ALLOWED = 0
FRONT_STUB_SHORT = 1
FRONT_STUB_LONG = 2
BACK_STUB_SHORT = 3
BACK_STUB_LONG = 4
STUB_NOT_ALLOWED = StubType.STUB_NOT_ALLOWED
FRONT_STUB_SHORT = StubType.FRONT_STUB_SHORT
FRONT_STUB_LONG = StubType.FRONT_STUB_LONG
BACK_STUB_SHORT = StubType.BACK_STUB_SHORT
BACK_STUB_LONG = StubType.BACK_STUB_LONG
excelBaseDate = datetime.date(1899, 12, 30)
def pydate_to_exceldate(d: dt.date) -> int:
xldate = int((d - excelBaseDate).days)
assert xldate >= 61, "Do not allow dates below 1 March 1900, Excel incorrectly assumes 1900 is a leap year"
return xldate
def exceldate_to_pydate(d: int) -> dt.date:
assert d >= 61, "Do not allow dates below 1 March 1900, Excel incorrectly assumes 1900 is a leap year"
return excelBaseDate + relativedelta(days=d)
def create_relativedelta(n: int, unit: str) -> relativedelta:
if unit == 'M':
return relativedelta(months=n)
elif unit == 'D':
return relativedelta(days=n)
elif unit == 'Y':
return relativedelta(years=n)
elif unit == 'Q':
return relativedelta(months=3 * n)
else:
raise BaseException("Unknown unit %s" % unit)
def next_imm_date(d: dt.date) -> dt.date:
assert isinstance(d, dt.date)
def third_wednesday(d: dt.date) -> int:
d = d.replace(day=1)
x0 = d.weekday() - 2 # How many days since last wednesday
x1 = 7 - x0 # how many days till next (1st wednesday)
return (d + relativedelta(days=x1 + 14 if x0 >= 1 else x1 + 7)).day
if d.month in [3, 6, 9, 12]:
wed = third_wednesday(d)
return next_imm_date(d + relativedelta(months=1)) if wed <= d.day else d.replace(day=wed)
else:
d = d.replace(day=1).replace(month=int((d.month - 1) / 3 + 1) * 3)
return d.replace(day=third_wednesday(d))
def date_step(date: int, tenor: Tenor, preserve_eom: bool = False):
assert tenor.unit != 'E'
pydate = exceldate_to_pydate(date)
if tenor.unit == 'F':
pydate2 = pydate
for i in range(tenor.n):
pydate2 = next_imm_date(pydate2)
else:
pydate2 = pydate + create_relativedelta(tenor.n, tenor.unit)
if preserve_eom:
assert tenor.unit not in ['F']
lastDay = calendar.monthrange(pydate.year, pydate.month)[1]
if pydate.day == lastDay:
d2 = calendar.monthrange(pydate2.year, pydate2.month)[1]
pydate2 = datetime.date(pydate2.year, pydate2.month, d2)
date2 = pydate_to_exceldate(pydate2)
return date2
def date_roll(date, roll_type, calendar):
assert isinstance(date, int)
assert isinstance(roll_type, RollType)
if roll_type == RollType.FOLLOWING:
while calendar.is_holiday(date): date += 1
return date
elif roll_type == RollType.PRECEDING:
while calendar.is_holiday(date): date -= 1
return date
else:
raise BaseException("Roll type %s not implemented", roll_type)
def calculate_spot_date(trade_date, spot_offset, calendar):
assert not calendar.is_holiday(trade_date)
spot_date = trade_date
tenor1D = Tenor('1D')
for i in range(spot_offset):
spot_date = date_step(spot_date, tenor1D, preserve_eom=False)
spot_date = date_roll(spot_date, RollType.FOLLOWING, calendar)
assert not calendar.is_holiday(spot_date)
return spot_date
def create_py_date(arg, reference_date=None):
return exceldate_to_pydate(create_excel_date(arg, reference_date))
def create_excel_date(arg, reference_date=None): # Creates excel date
if isinstance(arg, int):
return arg
elif isinstance(arg, datetime.date):
return pydate_to_exceldate(arg)
elif isinstance(arg, str) and arg[0:4].isdigit():
return pydate_to_exceldate(dateutil.parser.parse(arg).date())
elif isinstance(arg, str):
assert reference_date is not None
ret = reference_date
tenors = arg.split("+")
for t in tenors:
if t == 'E': continue
ret = date_step(ret, Tenor(t))
return ret
elif isinstance(arg, Tenor):
if arg.unit == 'E':
return create_excel_date(reference_date)
return date_step(reference_date, arg)
assert False, (type(arg), arg)
def calculate_dcfs(dates, dcc):
numerator = dates[1:] - dates[:-1]
return numerator / dcc.get_denominator()
def calculate_dcf(date0, date1, dcc):
numerator = date1 - date0
return numerator / dcc.get_denominator()
def generate_schedule(start: int, end: int, step: Tenor, stub_type: StubType = FRONT_STUB_SHORT):
if stub_type == StubType.STUB_NOT_ALLOWED:
d = start
out = []
while d <= end:
out.append(d)
d = date_step(d, step)
mismatch = out[-1] - end
if mismatch != 0:
raise BaseException(
"Function generate_schedule for start=%s, end=%s, step=%s results in unallowed stub (mismatch %i days)" %
(start, end, step.string, mismatch))
return np.array(out)
if stub_type == StubType.BACK_STUB_SHORT:
d = start
out = []
while d < end:
out.append(d)
d = date_step(d, step)
if out[-1] != end:
out.append(end)
return np.array(out)
elif stub_type == StubType.BACK_STUB_LONG:
d = start
out = []
while date_step(d, step) <= end:
out.append(d)
d = date_step(d, step)
if out[-1] != end:
out.append(end)
return np.array(out)
elif stub_type == StubType.FRONT_STUB_SHORT:
d = end
out = []
stepinv = -step
while d > start:
out.append(d)
d = date_step(d, stepinv)
if out[-1] != start:
out.append(start)
return np.array(out[::-1])
elif stub_type == StubType.FRONT_STUB_LONG:
d = end
out = []
stepinv = -step
while date_step(d, stepinv) >= start:
out.append(d)
d = date_step(d, stepinv)
if out[-1] != start:
out.append(start)
return np.array(out[::-1])
else:
raise BaseException("Other stub types not supported")
# endregion