pandas-market-calendars 4.1.3__py3-none-any.whl → 4.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/METADATA +200 -187
  2. pandas_market_calendars-4.2.0.dist-info/NOTICE +206 -0
  3. pandas_market_calendars-4.2.0.dist-info/RECORD +6 -0
  4. {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/WHEEL +1 -1
  5. pandas_market_calendars/__init__.py +0 -37
  6. pandas_market_calendars/calendar_registry.py +0 -47
  7. pandas_market_calendars/calendar_utils.py +0 -225
  8. pandas_market_calendars/class_registry.py +0 -111
  9. pandas_market_calendars/exchange_calendar_asx.py +0 -63
  10. pandas_market_calendars/exchange_calendar_bmf.py +0 -227
  11. pandas_market_calendars/exchange_calendar_bse.py +0 -409
  12. pandas_market_calendars/exchange_calendar_cboe.py +0 -115
  13. pandas_market_calendars/exchange_calendar_cme.py +0 -240
  14. pandas_market_calendars/exchange_calendar_cme_globex_agriculture.py +0 -109
  15. pandas_market_calendars/exchange_calendar_cme_globex_base.py +0 -106
  16. pandas_market_calendars/exchange_calendar_cme_globex_energy_and_metals.py +0 -146
  17. pandas_market_calendars/exchange_calendar_cme_globex_equities.py +0 -104
  18. pandas_market_calendars/exchange_calendar_cme_globex_fixed_income.py +0 -114
  19. pandas_market_calendars/exchange_calendar_cme_globex_fx.py +0 -78
  20. pandas_market_calendars/exchange_calendar_eurex.py +0 -119
  21. pandas_market_calendars/exchange_calendar_hkex.py +0 -408
  22. pandas_market_calendars/exchange_calendar_ice.py +0 -65
  23. pandas_market_calendars/exchange_calendar_iex.py +0 -98
  24. pandas_market_calendars/exchange_calendar_jpx.py +0 -98
  25. pandas_market_calendars/exchange_calendar_lse.py +0 -91
  26. pandas_market_calendars/exchange_calendar_nyse.py +0 -1127
  27. pandas_market_calendars/exchange_calendar_ose.py +0 -150
  28. pandas_market_calendars/exchange_calendar_sifma.py +0 -300
  29. pandas_market_calendars/exchange_calendar_six.py +0 -114
  30. pandas_market_calendars/exchange_calendar_sse.py +0 -290
  31. pandas_market_calendars/exchange_calendar_tase.py +0 -119
  32. pandas_market_calendars/exchange_calendar_tsx.py +0 -159
  33. pandas_market_calendars/exchange_calendars_mirror.py +0 -114
  34. pandas_market_calendars/holidays_cme.py +0 -341
  35. pandas_market_calendars/holidays_cme_globex.py +0 -169
  36. pandas_market_calendars/holidays_cn.py +0 -1436
  37. pandas_market_calendars/holidays_jp.py +0 -362
  38. pandas_market_calendars/holidays_nyse.py +0 -1474
  39. pandas_market_calendars/holidays_oz.py +0 -65
  40. pandas_market_calendars/holidays_sifma.py +0 -321
  41. pandas_market_calendars/holidays_uk.py +0 -177
  42. pandas_market_calendars/holidays_us.py +0 -364
  43. pandas_market_calendars/jpx_equinox.py +0 -147
  44. pandas_market_calendars/market_calendar.py +0 -770
  45. pandas_market_calendars-4.1.3.dist-info/RECORD +0 -45
  46. {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/LICENSE +0 -0
  47. {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/top_level.txt +0 -0
@@ -1,37 +0,0 @@
1
- # Fork of Zipline by Quantopian released under MIT license. Original Zipline license below.
2
- #
3
- # Copyright 2016 Quantopian, Inc.
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- import pkg_resources
18
-
19
- from .calendar_registry import get_calendar, get_calendar_names
20
- from .calendar_utils import convert_freq, date_range, merge_schedules
21
- # TODO: is the below needed? Can I replace all the imports on the calendars with ".market_calendar"
22
- from .market_calendar import MarketCalendar
23
-
24
- # if running in development there may not be a package
25
- try:
26
- __version__ = pkg_resources.get_distribution('pandas_market_calendars').version
27
- except pkg_resources.DistributionNotFound:
28
- __version__ = 'development'
29
-
30
- __all__ = [
31
- 'MarketCalendar',
32
- 'get_calendar',
33
- 'get_calendar_names',
34
- 'merge_schedules',
35
- 'date_range',
36
- 'convert_freq'
37
- ]
@@ -1,47 +0,0 @@
1
- from .market_calendar import MarketCalendar
2
- from .exchange_calendar_asx import ASXExchangeCalendar
3
- from .exchange_calendar_bmf import BMFExchangeCalendar
4
- from .exchange_calendar_cboe import CFEExchangeCalendar
5
- from .exchange_calendar_cme import \
6
- CMEEquityExchangeCalendar, \
7
- CMEBondExchangeCalendar
8
- from .exchange_calendar_cme_globex_base import CMEGlobexBaseExchangeCalendar
9
- from .exchange_calendar_cme_globex_agriculture import CMEGlobexAgricultureExchangeCalendar
10
- from .exchange_calendar_cme_globex_fx import CMEGlobexFXExchangeCalendar
11
- from .exchange_calendar_cme_globex_energy_and_metals import CMEGlobexEnergyAndMetalsExchangeCalendar
12
- from .exchange_calendar_cme_globex_equities import CMEGlobexEquitiesExchangeCalendar
13
- from .exchange_calendar_cme_globex_fixed_income import CMEGlobexFixedIncomeCalendar
14
- from .exchange_calendar_eurex import EUREXExchangeCalendar
15
- from .exchange_calendar_hkex import HKEXExchangeCalendar
16
- from .exchange_calendar_ice import ICEExchangeCalendar
17
- from .exchange_calendar_iex import IEXExchangeCalendar
18
- from .exchange_calendar_jpx import JPXExchangeCalendar
19
- from .exchange_calendar_lse import LSEExchangeCalendar
20
- from .exchange_calendar_nyse import NYSEExchangeCalendar
21
- from .exchange_calendar_ose import OSEExchangeCalendar
22
- from .exchange_calendar_sifma import SIFMAUSExchangeCalendar, SIFMAUKExchangeCalendar, SIFMAJPExchangeCalendar
23
- from .exchange_calendar_six import SIXExchangeCalendar
24
- from .exchange_calendar_sse import SSEExchangeCalendar
25
- from .exchange_calendar_tsx import TSXExchangeCalendar
26
- from .exchange_calendar_bse import BSEExchangeCalendar
27
- from .exchange_calendar_tase import TASEExchangeCalendar
28
- from .exchange_calendars_mirror import *
29
-
30
-
31
- def get_calendar(name, open_time=None, close_time=None):
32
- """
33
- Retrieves an instance of an MarketCalendar whose name is given.
34
-
35
- :param name: The name of the MarketCalendar to be retrieved.
36
- :param open_time: Market open time override as datetime.time object. If None then default is used.
37
- :param close_time: Market close time override as datetime.time object. If None then default is used.
38
- :return: MarketCalendar of the desired calendar.
39
- """
40
- return MarketCalendar.factory(name, open_time=open_time, close_time=close_time)
41
-
42
-
43
- def get_calendar_names():
44
- """All Market Calendar names and aliases that can be used in "factory"
45
- :return: list(str)
46
- """
47
- return MarketCalendar.calendar_names()
@@ -1,225 +0,0 @@
1
- """
2
- Utilities to use with market_calendars
3
- """
4
- import itertools
5
- import warnings
6
-
7
- import pandas as pd
8
- import numpy as np
9
-
10
- def merge_schedules(schedules, how='outer'):
11
- """
12
- Given a list of schedules will return a merged schedule. The merge method (how) will either return the superset
13
- of any datetime when any schedule is open (outer) or only the datetime where all markets are open (inner)
14
-
15
- CAVEATS:
16
- * This does not work for schedules with breaks, the break information will be lost.
17
- * Onlu "market_open" and "market_close" are considered, other market times are not yet supported.
18
-
19
- :param schedules: list of schedules
20
- :param how: outer or inner
21
- :return: schedule DataFrame
22
- """
23
- all_cols = [x.columns for x in schedules]
24
- all_cols = list(itertools.chain(*all_cols))
25
- if ('break_start' in all_cols) or ('break_end' in all_cols):
26
- warnings.warn('Merge schedules will drop the break_start and break_end from result.')
27
-
28
- result = schedules[0]
29
- for schedule in schedules[1:]:
30
- result = result.merge(schedule, how=how, right_index=True, left_index=True)
31
- if how == 'outer':
32
- result['market_open'] = result.apply(lambda x: min(x.market_open_x, x.market_open_y), axis=1)
33
- result['market_close'] = result.apply(lambda x: max(x.market_close_x, x.market_close_y), axis=1)
34
- elif how == 'inner':
35
- result['market_open'] = result.apply(lambda x: max(x.market_open_x, x.market_open_y), axis=1)
36
- result['market_close'] = result.apply(lambda x: min(x.market_close_x, x.market_close_y), axis=1)
37
- else:
38
- raise ValueError('how argument must be "inner" or "outer"')
39
- result = result[['market_open', 'market_close']]
40
- return result
41
-
42
-
43
- def convert_freq(index, frequency):
44
- """
45
- Converts a DateTimeIndex to a new lower frequency
46
-
47
- :param index: DateTimeIndex
48
- :param frequency: frequency string
49
- :return: DateTimeIndex
50
- """
51
- return pd.DataFrame(index=index).asfreq(frequency).index
52
-
53
- class _date_range:
54
- """
55
- This is a callable class that should be used by calling the already initiated instance: `date_range`.
56
- Given a schedule, it will return a DatetimeIndex with all of the valid datetimes at the frequency given.
57
-
58
- The schedule columns should all have the same time zone.
59
-
60
- The calculations will be made for each trading session. If the passed schedule-DataFrame doesn't have
61
- breaks, there is one trading session per day going from market_open to market_close, otherwise there are two,
62
- the first one going from market_open to break_start and the second one from break_end to market_close.
63
-
64
- *Any trading session where start == end is considered a 'no-trading session' and will always be dropped*
65
-
66
- CAVEATS:
67
- * Only "market_open", "market_close" (and, optionally, "breaak_start" and "break_end")
68
- are considered, other market times are not yet supported by this class.
69
-
70
- * If the difference between start and end of a trading session is smaller than an interval of the
71
- frequency, and closed= "right" and force_close = False, the whole session will disappear.
72
- This will also raise a warning.
73
-
74
-
75
- Signature:
76
- .__call__(self, schedule, frequency, closed='right', force_close=True, **kwargs)
77
-
78
- :param schedule: schedule of a calendar, which may or may not include break_start and break_end columns
79
- :param frequency: frequency string that is used by pd.Timedelta to calculate the timestamps
80
- this must be "1D" or higher frequency
81
- :param closed: the way the intervals are labeled
82
- 'right': use the end of the interval
83
- 'left': use the start of the interval
84
- None: (or 'both') use the end of the interval but include the start of the first interval (the open)
85
- :param force_close: how the last value of a trading session is handled
86
- True: guarantee that the close of the trading session is the last value
87
- False: guarantee that there is no value greater than the close of the trading session
88
- None: leave the last value as it is calculated based on the closed parameter
89
- :param kwargs: unused. Solely for compatibility.
90
-
91
-
92
- """
93
-
94
- def __init__(self, schedule = None, frequency= None, closed='right', force_close=True):
95
- if not closed in ("left", "right", "both", None):
96
- raise ValueError("closed must be 'left', 'right', 'both' or None.")
97
- elif not force_close in (True, False, None):
98
- raise ValueError("force_close must be True, False or None.")
99
-
100
- self.closed = closed
101
- self.force_close = force_close
102
- self.has_breaks = False
103
- if frequency is None: self.frequency = None
104
- else:
105
- self.frequency = pd.Timedelta(frequency)
106
- if self.frequency > pd.Timedelta("1D"):
107
- raise ValueError('Frequency must be 1D or higher frequency.')
108
-
109
- elif schedule.market_close.lt(schedule.market_open).any():
110
- raise ValueError("Schedule contains rows where market_close < market_open,"
111
- " please correct the schedule")
112
-
113
- if "break_start" in schedule:
114
- if not all([
115
- schedule.market_open.le(schedule.break_start).all(),
116
- schedule.break_start.le(schedule.break_end).all(),
117
- schedule.break_end.le(schedule.market_close).all()]):
118
- raise ValueError("Not all rows match the condition: "
119
- "market_open <= break_start <= break_end <= market_close, "
120
- "please correct the schedule")
121
- self.has_breaks = True
122
-
123
- def _check_overlap(self, schedule):
124
- """checks if calculated end times would overlap with the next start times.
125
- Only an issue when force_close is None and closed != left.
126
-
127
- :param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
128
- :raises ValueError:"""
129
- if self.force_close is None and self.closed != "left":
130
- num_bars = self._calc_num_bars(schedule)
131
- end_times = schedule.start + num_bars * self.frequency
132
-
133
- if end_times.gt(schedule.start.shift(-1)).any():
134
- raise ValueError(f"The chosen frequency will lead to overlaps in the calculated index. "
135
- f"Either choose a higher frequency or avoid setting force_close to None "
136
- f"when setting closed to 'right', 'both' or None.")
137
-
138
- def _check_disappearing_session(self, schedule):
139
- """checks if requested frequency and schedule would lead to lost trading sessions.
140
- Only necessary when force_close = False and closed = "right".
141
-
142
- :param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
143
- :raises UserWarning:"""
144
- if self.force_close is False and self.closed == "right":
145
-
146
- if (schedule.end- schedule.start).lt(self.frequency).any():
147
- warnings.warn("An interval of the chosen frequency is larger than some of the trading sessions, "
148
- "while closed== 'right' and force_close is False. This will make those trading sessions "
149
- "disappear. Use a higher frequency or change the values of closed/force_close, to "
150
- "keep this from happening.")
151
-
152
- def _calc_num_bars(self, schedule):
153
- """calculate the number of timestamps needed for each trading session.
154
-
155
- :param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
156
- :return: pd.Series of float64"""
157
- return np.ceil((schedule.end - schedule.start) / self.frequency)
158
-
159
-
160
- def _calc_time_series(self, schedule):
161
- """Method used by date_range to calculate the trading index.
162
-
163
- :param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
164
- :return: pd.Series of datetime64[ns, UTC]"""
165
- num_bars = self._calc_num_bars(schedule)
166
-
167
- # ---> calculate the desired timeseries:
168
- if self.closed == "left":
169
- opens = schedule.start.repeat(num_bars) # keep as is
170
- time_series = (opens.groupby(opens.index).cumcount()) * self.frequency + opens
171
- elif self.closed == "right":
172
- opens = schedule.start.repeat(num_bars) # dont add row but shift up
173
- time_series = (opens.groupby(opens.index).cumcount()+ 1) * self.frequency + opens
174
- else:
175
- num_bars += 1
176
- opens = schedule.start.repeat(num_bars) # add row but dont shift up
177
- time_series = (opens.groupby(opens.index).cumcount()) * self.frequency + opens
178
-
179
- if not self.force_close is None:
180
- time_series = time_series[time_series.le(schedule.end.repeat(num_bars))]
181
- if self.force_close:
182
- time_series = pd.concat([time_series, schedule.end]).sort_values()
183
-
184
- return time_series
185
-
186
-
187
- def __call__(self, schedule, frequency, closed='right', force_close=True, **kwargs):
188
- """
189
- See class docstring for more information.
190
-
191
- :param schedule: schedule of a calendar, which may or may not include break_start and break_end columns
192
- :param frequency: frequency string that is used by pd.Timedelta to calculate the timestamps
193
- this must be "1D" or higher frequency
194
- :param closed: the way the intervals are labeled
195
- 'right': use the end of the interval
196
- 'left': use the start of the interval
197
- None: (or 'both') use the end of the interval but include the start of the first interval
198
- :param force_close: how the last value of a trading session is handled
199
- True: guarantee that the close of the trading session is the last value
200
- False: guarantee that there is no value greater than the close of the trading session
201
- None: leave the last value as it is calculated based on the closed parameter
202
- :param kwargs: unused. Solely for compatibility.
203
- :return: pd.DatetimeIndex of datetime64[ns, UTC]
204
- """
205
- self.__init__(schedule, frequency, closed, force_close)
206
- if self.has_breaks:
207
- # rearrange the schedule, to make every row one session
208
- before = schedule[["market_open", "break_start"]].set_index(schedule["market_open"])
209
- after = schedule[["break_end", "market_close"]].set_index(schedule["break_end"])
210
- before.columns = after.columns = ["start", "end"]
211
- schedule = pd.concat([before, after]).sort_index()
212
-
213
- else:
214
- schedule = schedule.rename(columns= {"market_open": "start", "market_close": "end"})
215
-
216
- schedule = schedule[schedule.start.ne(schedule.end)] # drop the 'no-trading sessions'
217
- self._check_overlap(schedule)
218
- self._check_disappearing_session(schedule)
219
-
220
- time_series = self._calc_time_series(schedule)
221
-
222
- time_series.name = None
223
- return pd.DatetimeIndex(time_series.drop_duplicates())
224
-
225
- date_range = _date_range()
@@ -1,111 +0,0 @@
1
- import inspect
2
- from pprint import pformat
3
-
4
- def _regmeta_instance_factory(cls, name, *args, **kwargs):
5
- """
6
- :param cls(RegisteryMeta): registration meta class
7
- :param name(str): name of class that needs to be instantiated
8
- :param args(Optional(tuple)): instance positional arguments
9
- :param kwargs(Optional(dict)): instance named arguments
10
- :return: class instance
11
- """
12
- try:
13
- class_ = cls._regmeta_class_registry[name]
14
- except KeyError:
15
- raise RuntimeError(
16
- 'Class {} is not one of the registered classes: {}'.format(name, cls._regmeta_class_registry.keys()))
17
- return class_(*args, **kwargs)
18
-
19
- def _regmeta_register_class(cls, regcls, name):
20
- """
21
- :param cls(RegisteryMeta): registration base class
22
- :param regcls(class): class to be registered
23
- :param name(str): name of the class to be registered
24
- """
25
- if hasattr(regcls, 'aliases'):
26
- if regcls.aliases:
27
- for alias in regcls.aliases:
28
- cls._regmeta_class_registry[alias] = regcls
29
- else:
30
- cls._regmeta_class_registry[name] = regcls
31
- else:
32
- cls._regmeta_class_registry[name] = regcls
33
-
34
-
35
- class RegisteryMeta(type):
36
- """
37
- Metaclass used to register all classes inheriting from RegisteryMeta
38
- """
39
-
40
- def __new__(mcs, name, bases, attr):
41
- cls = super(RegisteryMeta, mcs).__new__(mcs, name, bases, attr)
42
- if not hasattr(cls, '_regmeta_class_registry'):
43
- cls._regmeta_class_registry = {}
44
- cls.factory = classmethod(_regmeta_instance_factory)
45
-
46
- return cls
47
-
48
- def __init__(cls, name, bases, attr):
49
- if not inspect.isabstract(cls):
50
- _regmeta_register_class(cls, cls, name)
51
- for b in bases:
52
- if hasattr(b, '_regmeta_class_registry'):
53
- _regmeta_register_class(b, cls, name)
54
-
55
- super(RegisteryMeta, cls).__init__(name, bases, attr)
56
-
57
- cls.regular_market_times = ProtectedDict(cls.regular_market_times)
58
- cls.open_close_map = ProtectedDict(cls.open_close_map)
59
-
60
- cls.special_market_open = cls.special_opens
61
- cls.special_market_open_adhoc = cls.special_opens_adhoc
62
-
63
- cls.special_market_close = cls.special_closes
64
- cls.special_market_close_adhoc = cls.special_closes_adhoc
65
-
66
-
67
- class ProtectedDict(dict):
68
-
69
- def __init__(self, *args, **kwargs):
70
- super().__init__(*args, **kwargs)
71
- # __init__ is bypassed when unpickling, which causes __setitem__ to fail
72
- # without the _INIT_RAN_NORMALLY flag
73
- self._INIT_RAN_NORMALLY = True
74
-
75
- def _set(self, key, value):
76
- return super().__setitem__(key, value)
77
-
78
- def _del(self, key):
79
- return super().__delitem__(key)
80
-
81
- def __setitem__(self, key, value):
82
- if not hasattr(self, "_INIT_RAN_NORMALLY"):
83
- return self._set(key, value)
84
-
85
- raise TypeError("You cannot set a value directly, you can change regular_market_times "
86
- "using .change_time, .add_time or .remove_time.")
87
-
88
- def __delitem__(self, key):
89
- if not hasattr(self, "_INIT_RAN_NORMALLY"):
90
- return self._del(key)
91
-
92
- raise TypeError("You cannot delete an item directly. You can change regular_market_times "
93
- "using .change_time, .add_time or .remove_time")
94
-
95
- def __repr__(self):
96
- return self.__class__.__name__+ "(" + super().__repr__() + ")"
97
-
98
- def __str__(self):
99
- try:
100
- formatted = pformat(dict(self), sort_dicts= False) # sort_dicts apparently not available < python3.8
101
- except TypeError:
102
- formatted = pformat(dict(self))
103
-
104
- return self.__class__.__name__+ "(\n" + formatted + "\n)"
105
-
106
- def copy(self):
107
- return self.__class__(super().copy())
108
-
109
-
110
-
111
-
@@ -1,63 +0,0 @@
1
- from datetime import time
2
-
3
- from pandas.tseries.holiday import AbstractHolidayCalendar, GoodFriday, EasterMonday
4
- from pytz import timezone
5
-
6
- from .holidays_oz import *
7
- from .market_calendar import MarketCalendar
8
-
9
- AbstractHolidayCalendar.start_date = '2011-01-01'
10
-
11
-
12
- class ASXExchangeCalendar(MarketCalendar):
13
- """
14
- Open Time: 10:00 AM, Australia/Sydney
15
- Close Time: 4:10 PM, Australia/Sydney
16
-
17
-
18
- Regularly-Observed Holidays:
19
- - New Year's Day (observed on Monday when Jan 1 is a Saturday or Sunday)
20
- - Australia Day (observed on Monday when Jan 26 is a Saturday or Sunday)
21
- - Good Friday (two days before Easter Sunday)
22
- - Easter Monday (the Monday after Easter Sunday)
23
- - ANZAC Day (April 25)
24
- - Queen's Birthday (second Monday in June)
25
- - Christmas Day (December 25, Saturday/Sunday to Monday)
26
- - Boxing Day (December 26, Saturday to Monday, Sunday to Tuesday)
27
-
28
-
29
- Regularly-Observed Early Closes:
30
- - Last Business Day before Christmas Day
31
- - Last Business Day of the Year
32
-
33
- """
34
- aliases = ['ASX']
35
- regular_market_times = {
36
- "market_open": ((None, time(10)),),
37
- "market_close": ((None, time(16,10)),)
38
- }
39
-
40
- @property
41
- def name(self):
42
- return "ASX"
43
-
44
- @property
45
- def tz(self):
46
- return timezone("Australia/Sydney")
47
-
48
- @property
49
- def regular_holidays(self):
50
- return AbstractHolidayCalendar(rules=[
51
- OZNewYearsDay,
52
- AustraliaDay,
53
- AnzacDay,
54
- QueensBirthday,
55
- Christmas,
56
- BoxingDay,
57
- GoodFriday,
58
- EasterMonday,
59
- ])
60
-
61
- @property
62
- def adhoc_holidays(self):
63
- return UniqueCloses