├── tests ├── __init__.py ├── data_test.py ├── data_hk_test.py ├── data_hk_half_day_test.py ├── half_day_weekend_test.py └── latest_year_weekends_test.py ├── cn_stock_holidays ├── tools │ ├── __init__.py │ └── cmd.py ├── __init__.py ├── zipline │ ├── __init__.py │ ├── default_calendar.py │ ├── exchange_calendar_hkex.py │ └── exchange_calendar_shsz.py ├── data.py ├── common.py ├── data_hk.py ├── data_hk.txt ├── data.txt └── meta_functions.py ├── .pre-commit-config.yaml ├── scripts ├── setup-dev.sh ├── ipython_config.py ├── dev_shell.py ├── test_dev_env.py ├── check_weekend_half_days.py ├── README.md └── quick_test.py ├── .github └── workflows │ └── ci.yml ├── docs └── TRUSTED_PUBLISHER_SETUP.md ├── UV_GUIDE.md ├── .gitignore ├── pyproject.toml ├── .cursorrules ├── Changelog-zh_CN.md ├── README-zh_CN.md ├── DATA_UPDATE_RULES.md ├── Changelog.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cn_stock_holidays/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cn_stock_holidays/__init__.py: -------------------------------------------------------------------------------- 1 | from .data import get_local, get_cached, get_remote_and_cache, check_expired, sync_data 2 | 3 | __all__ = [ 4 | "get_local", 5 | "get_cached", 6 | "get_remote_and_cache", 7 | "check_expired", 8 | "sync_data", 9 | ] 10 | -------------------------------------------------------------------------------- /cn_stock_holidays/zipline/__init__.py: -------------------------------------------------------------------------------- 1 | from cn_stock_holidays.zipline.exchange_calendar_hkex import HKExchangeCalendar 2 | from cn_stock_holidays.zipline.exchange_calendar_shsz import SHSZExchangeCalendar 3 | 4 | 5 | __all__ = [ 6 | "HKExchangeCalendar", 7 | "SHSZExchangeCalendar", 8 | ] 9 | -------------------------------------------------------------------------------- /cn_stock_holidays/zipline/default_calendar.py: -------------------------------------------------------------------------------- 1 | from zipline.utils.calendars import get_calendar, register_calendar 2 | from .exchange_calendar_shsz import SHSZExchangeCalendar 3 | 4 | register_calendar("SHSZ", SHSZExchangeCalendar(), force=True) 5 | 6 | 7 | # singleton in python 8 | shsz_calendar = get_calendar("SHSZ") 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - id: check-merge-conflict 10 | 11 | - repo: https://github.com/psf/black 12 | rev: 23.12.1 13 | hooks: 14 | - id: black 15 | language_version: python3 16 | 17 | - repo: https://github.com/pycqa/isort 18 | rev: 5.13.2 19 | hooks: 20 | - id: isort 21 | args: ["--profile", "black"] 22 | 23 | - repo: https://github.com/pycqa/flake8 24 | rev: 7.0.0 25 | hooks: 26 | - id: flake8 27 | args: [--max-line-length=88] 28 | 29 | - repo: https://github.com/pre-commit/mirrors-mypy 30 | rev: v1.8.0 31 | hooks: 32 | - id: mypy 33 | additional_dependencies: [types-requests] -------------------------------------------------------------------------------- /scripts/setup-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup development environment with uv 4 | 5 | echo "Setting up development environment with uv..." 6 | 7 | # Check if uv is installed 8 | if ! command -v uv &>/dev/null; then 9 | echo "uv is not installed. Installing uv..." 10 | curl -LsSf https://astral.sh/uv/install.sh | sh 11 | source ~/.bashrc # or source ~/.zshrc for zsh 12 | fi 13 | 14 | # Install dependencies 15 | echo "Installing dependencies..." 16 | uv sync --dev 17 | 18 | # Install pre-commit hooks 19 | echo "Installing pre-commit hooks..." 20 | uv run pre-commit install 21 | 22 | # Run initial tests 23 | echo "Running initial tests..." 24 | uv run pytest 25 | 26 | echo "Development environment setup complete!" 27 | echo "" 28 | echo "Common commands:" 29 | echo " uv run pytest # Run tests" 30 | echo " uv run black . # Format code" 31 | echo " uv run isort . # Sort imports" 32 | echo " uv run mypy cn_stock_holidays/ # Type checking" 33 | echo " uv run flake8 cn_stock_holidays/ # Linting" 34 | echo "" 35 | echo "Debugging commands:" 36 | echo " python scripts/dev_shell.py # Start IPython with modules pre-loaded" 37 | echo " python scripts/quick_test.py # Run quick functionality tests" 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test, Build & Publish 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | 9 | env: 10 | FORCE_COLOR: 1 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install uv 21 | uses: astral-sh/setup-uv@v1 22 | with: 23 | version: latest 24 | - name: Install dependencies 25 | run: uv sync --dev 26 | - name: Run tests 27 | run: uv run pytest 28 | - name: Check formatting 29 | run: uv run black --check . 30 | 31 | build: 32 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') 33 | runs-on: ubuntu-latest 34 | needs: test 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@v1 39 | with: 40 | version: latest 41 | - name: Build package 42 | run: uv build 43 | - name: Upload build artifacts 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: dist 47 | path: dist/ 48 | 49 | publish: 50 | if: startsWith(github.ref, 'refs/tags/') 51 | runs-on: ubuntu-latest 52 | needs: build 53 | permissions: 54 | id-token: write # Required for Trusted Publisher authentication 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Download build artifacts 58 | uses: actions/download-artifact@v4 59 | with: 60 | name: dist 61 | path: dist/ 62 | - name: Install uv 63 | uses: astral-sh/setup-uv@v1 64 | with: 65 | version: latest 66 | - name: Publish to PyPI 67 | run: | 68 | uv publish --trusted-publishing automatic 69 | -------------------------------------------------------------------------------- /tests/data_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cn_stock_holidays.data import * 4 | 5 | 6 | class TestData(unittest.TestCase): 7 | 8 | def test_get_cached(self): 9 | """ 10 | get cached related 11 | """ 12 | data = get_cached() 13 | data2 = get_cached() 14 | 15 | self.assertEqual(str(data), str(data2), "get_cached 2 times give some results") 16 | 17 | self.assertGreater(len(data), 0, "is greater then 0") 18 | 19 | self.assertIsInstance(list(data)[0], datetime.date, "is a date") 20 | 21 | self.assertTrue( 22 | datetime.date(1991, 2, 15) in data, 23 | "get datetime.date(1991, 2, 15) in cached data", 24 | ) 25 | 26 | def test_trading_days_between(self): 27 | data = list(trading_days_between(int_to_date(20170125), int_to_date(20170131))) 28 | 29 | self.assertEqual(len(data), 2) 30 | self.assertTrue(int_to_date(20170125) in data) 31 | self.assertTrue(int_to_date(20170126) in data) 32 | 33 | def test_is_trading_day(self): 34 | self.assertIsNotNone(is_trading_day(datetime.date.today())) 35 | 36 | def test_next_trading_day(self): 37 | data = next_trading_day(datetime.date.today()) 38 | self.assertGreater(data, datetime.date.today()) 39 | 40 | def test_previous_trading_day(self): 41 | data = previous_trading_day(datetime.date.today()) 42 | self.assertLess(data, datetime.date.today()) 43 | 44 | def test_cache_clear(self): 45 | data = get_cached() 46 | get_cached.cache_clear() 47 | data2 = get_cached() 48 | self.assertEqual(str(data), str(data2), "get_cached 2 times give some results") 49 | 50 | def test_loop_100000(self): 51 | trade_days = datetime.date.today() 52 | for i in range(100000): 53 | trade_days = previous_trading_day(trade_days) 54 | 55 | self.assertIsInstance(trade_days, datetime.date) 56 | -------------------------------------------------------------------------------- /cn_stock_holidays/data.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Help functions for python to get china stock exchange holidays 4 | """ 5 | 6 | from cn_stock_holidays.meta_functions import * 7 | 8 | DATA_FILE_FOR_SHSZ = "data.txt" 9 | 10 | get_local = meta_get_local(data_file_name=DATA_FILE_FOR_SHSZ) 11 | get_cache_path = meta_get_cache_path(data_file_name=DATA_FILE_FOR_SHSZ) 12 | 13 | 14 | @function_cache 15 | def get_cached(use_list=False): 16 | return meta_get_cached(get_local=get_local, get_cache_path=get_cache_path)( 17 | use_list=False 18 | ) 19 | 20 | 21 | get_remote_and_cache = meta_get_remote_and_cache( 22 | get_cached=get_cached, get_cache_path=get_cache_path 23 | ) 24 | check_expired = meta_check_expired(get_cached=get_cached) 25 | sync_data = meta_sync_data( 26 | check_expired=check_expired, get_remote_and_cache=get_remote_and_cache 27 | ) 28 | is_trading_day = meta_is_trading_day(get_cached=get_cached) 29 | previous_trading_day = meta_previous_trading_day(is_trading_day=is_trading_day) 30 | next_trading_day = meta_next_trading_day(is_trading_day=is_trading_day) 31 | trading_days_between = meta_trading_days_between(get_cached=get_cached) 32 | 33 | if __name__ == "__main__": 34 | data = check_expired() 35 | 36 | print("get datetime.date(1991, 2, 15) in cached data") 37 | data = get_cached() 38 | print_result(datetime.date(1991, 2, 15) in data) 39 | 40 | print("test trading_days_between 20170125 to 20170131") 41 | data = list(trading_days_between(int_to_date(20170125), int_to_date(20170131))) 42 | print_result(data) 43 | 44 | print("is trading day today?") 45 | data = is_trading_day(datetime.date.today()) 46 | print_result(data) 47 | 48 | print("next trading day after today?") 49 | data = next_trading_day(datetime.date.today()) 50 | print_result(data) 51 | 52 | print("previous trading day before today?") 53 | data = previous_trading_day(datetime.date.today()) 54 | print_result(data) 55 | 56 | print("Test loop 100000") 57 | trade_days = datetime.date.today() 58 | for i in range(100000): 59 | trade_days = previous_trading_day(trade_days) 60 | -------------------------------------------------------------------------------- /cn_stock_holidays/tools/cmd.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import click 4 | from cn_stock_holidays import data 5 | from cn_stock_holidays import data_hk 6 | import datetime 7 | import platform 8 | 9 | 10 | @click.command() 11 | @click.option("--market", "-m", default="cn", help="CN or Hk") 12 | @click.option( 13 | "--start", "-s", required=True, help="START DATE FORMAT YYYY-MM-DD or YYYYMMDD" 14 | ) 15 | @click.option( 16 | "--end", "-e", required=True, help="END DATE FORMAT YYYY-MM-DD or YYYYMMDD" 17 | ) 18 | @click.option("--output", "-o", default="-", help="Output file, - is stdout") 19 | @click.option( 20 | "--format", "-f", default="YYYY-MM-DD", help="output format ,YYYY-MM-DD or YYYYMMDD" 21 | ) 22 | @click.option("--daytype", "-d", default="workday", help="workday or holiday") 23 | def main(market, start, end, output, format, daytype): 24 | if market == "cn": 25 | holiday = data 26 | else: 27 | holiday = data_hk 28 | 29 | start_date = parse_date(start) 30 | end_date = parse_date(end) 31 | 32 | output_arr = [] 33 | cur_date = start_date 34 | 35 | while cur_date < end_date: 36 | if holiday.is_trading_day(cur_date): 37 | if daytype == "workday": 38 | output_arr.append(cur_date) 39 | else: 40 | if daytype == "holiday": 41 | output_arr.append(cur_date) 42 | 43 | cur_date = cur_date + datetime.timedelta(days=1) 44 | 45 | linesep = "\n\r" if platform.system() == "Windows" else "\n" 46 | 47 | if format == "YYYY-MM-DD": 48 | format_str = "%Y-%m-%d" 49 | else: 50 | format_str = "%Y%m%d" 51 | 52 | output_str = linesep.join([d.strftime(format_str) for d in output_arr]) 53 | 54 | if output == "-": 55 | print(output_str) 56 | else: 57 | with open(output, "w") as f: 58 | f.write(output_str) 59 | 60 | 61 | def parse_date(dstr): 62 | 63 | # handle YYYYMMDD 64 | if len(dstr) == 8: 65 | return data.int_to_date(dstr) 66 | else: 67 | # handle YYYY-MM-DD 68 | darr = dstr.split("-") 69 | if len(darr) != 3: 70 | raise Exception("start or end format is invalid") 71 | 72 | return datetime.date(year=int(darr[0]), month=int(darr[1]), day=int(darr[2])) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /docs/TRUSTED_PUBLISHER_SETUP.md: -------------------------------------------------------------------------------- 1 | # PyPI Trusted Publisher Setup 2 | 3 | This project uses PyPI Trusted Publisher for secure automated package publishing. This eliminates the need for long-lived API tokens and provides better security. 4 | 5 | ## What is Trusted Publisher? 6 | 7 | Trusted Publisher uses OpenID Connect (OIDC) to exchange short-lived identity tokens between GitHub Actions and PyPI. This provides: 8 | 9 | - **Better Security**: Tokens expire automatically after 15 minutes 10 | - **Easier Setup**: No need to manually create and manage API tokens 11 | - **Automated Authentication**: Seamless integration with CI/CD workflows 12 | 13 | ## Setup Instructions 14 | 15 | ### 1. Configure Trusted Publisher on PyPI 16 | 17 | 1. Go to your project page on PyPI 18 | 2. Navigate to "Settings" → "Trusted publishers" 19 | 3. Click "Add a new trusted publisher" 20 | 4. Configure the publisher with these settings: 21 | 22 | **Publisher name**: `github-actions` 23 | **Publisher specifier**: `github.com/rainx/cn_stock_holidays` 24 | **Environment**: `ref:refs/tags/*` 25 | **Workflow name**: `Test, Build & Publish` 26 | **Workflow filename**: `.github/workflows/ci.yml` 27 | 28 | ### 2. CI Configuration 29 | 30 | The CI workflow is already configured to use Trusted Publisher: 31 | 32 | ```yaml 33 | publish: 34 | permissions: 35 | id-token: write # Required for Trusted Publisher authentication 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Download build artifacts 39 | uses: actions/download-artifact@v4 40 | with: 41 | name: dist 42 | path: dist/ 43 | - name: Install uv 44 | uses: astral-sh/setup-uv@v1 45 | with: 46 | version: latest 47 | - name: Publish to PyPI 48 | run: uv publish --trusted-publishing automatic 49 | ``` 50 | 51 | ### 3. Publishing Process 52 | 53 | 1. Create and push a new tag: 54 | 55 | ```bash 56 | git tag v2.0.1 57 | git push origin v2.0.1 58 | ``` 59 | 60 | 2. The CI workflow will automatically: 61 | - Run tests 62 | - Build the package 63 | - Publish to PyPI using Trusted Publisher authentication 64 | 65 | ## Security Benefits 66 | 67 | - **Short-lived tokens**: Authentication tokens expire after 15 minutes 68 | - **No manual token management**: No need to create, store, or rotate API tokens 69 | - **Repository-specific**: Tokens are only valid for this specific repository 70 | - **Environment restrictions**: Can be limited to specific branches or tags 71 | 72 | ## Troubleshooting 73 | 74 | If publishing fails, check: 75 | 76 | 1. Trusted Publisher configuration on PyPI matches the repository 77 | 2. Workflow name and filename are correct 78 | 3. Environment specifier includes the tag pattern 79 | 4. Repository has the correct permissions 80 | 81 | ## References 82 | 83 | - [PyPI Trusted Publishers Documentation](https://docs.pypi.org/trusted-publishers/) 84 | - [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) 85 | -------------------------------------------------------------------------------- /scripts/ipython_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | IPython configuration for cn_stock_holidays development. 3 | 4 | This file provides custom IPython settings and startup commands 5 | for a better development experience. 6 | 7 | Usage: 8 | ipython --profile=cn_stock_holidays 9 | # or 10 | ipython -c "exec(open('scripts/ipython_config.py').read())" 11 | """ 12 | 13 | import sys 14 | from pathlib import Path 15 | 16 | # Add the project root to Python path 17 | project_root = Path(__file__).parent.parent 18 | if str(project_root) not in sys.path: 19 | sys.path.insert(0, str(project_root)) 20 | 21 | # IPython configuration 22 | try: 23 | c = get_config() 24 | except NameError: 25 | # This file is meant to be executed in IPython context 26 | # where get_config() is available 27 | print("This file should be executed in IPython context") 28 | c = None 29 | 30 | # Enable auto-reload for development 31 | c.InteractiveShellApp.exec_lines = [ 32 | "%load_ext autoreload", 33 | "%autoreload 2", 34 | 'print("=== cn_stock_holidays Development Environment ===")', 35 | 'print("Auto-reload enabled for development")', 36 | 'print("Available modules:")', 37 | 'print(" - cn_stock_holidays.data (Mainland China)")', 38 | 'print(" - cn_stock_holidays.data_hk (Hong Kong)")', 39 | 'print(" - cn_stock_holidays.common (Utilities)")', 40 | 'print(" - cn_stock_holidays.meta_functions (Cache)")', 41 | "print()", 42 | ] 43 | 44 | # Import commonly used modules 45 | c.InteractiveShellApp.exec_lines.extend( 46 | [ 47 | "import cn_stock_holidays", 48 | "import cn_stock_holidays.data as data", 49 | "import cn_stock_holidays.data_hk as data_hk", 50 | "import cn_stock_holidays.common as common", 51 | "import cn_stock_holidays.meta_functions as meta", 52 | "", 53 | "# Import commonly used functions", 54 | "from cn_stock_holidays.data import is_trading_day, is_holiday, trading_days_between, get_trading_days", 55 | "from cn_stock_holidays.data_hk import is_trading_day as is_trading_day_hk, is_half_day_trading_day", 56 | "from cn_stock_holidays.common import parse_date, format_date, is_weekend", 57 | "from cn_stock_holidays.meta_functions import get_cache_info, clear_cache", 58 | "", 59 | 'print("All modules imported successfully!")', 60 | 'print("Try: help(data) or help(data_hk) for more information")', 61 | "print()", 62 | ] 63 | ) 64 | 65 | # Enable rich display 66 | c.InteractiveShell.ast_node_interactivity = "all" 67 | 68 | # Enable auto-completion 69 | c.IPCompleter.use_jedi = True 70 | c.IPCompleter.greedy = True 71 | 72 | # Set up aliases for common operations 73 | c.AliasManager.user_aliases = [ 74 | ("test", "python scripts/quick_test.py"), 75 | ("shell", "python scripts/dev_shell.py"), 76 | ("cache", "get_cache_info()"), 77 | ("clear-cache", "clear_cache()"), 78 | ("hk-test", 'is_trading_day_hk("2024-12-24")'), 79 | ("cn-test", 'is_trading_day("2024-01-01")'), 80 | ] 81 | -------------------------------------------------------------------------------- /UV_GUIDE.md: -------------------------------------------------------------------------------- 1 | # UV Package Manager Guide 2 | 3 | This project now supports [uv](https://github.com/astral-sh/uv), a fast Python package installer and resolver. 4 | 5 | ## Installation 6 | 7 | First, install uv: 8 | 9 | ```bash 10 | # macOS and Linux 11 | curl -LsSf https://astral.sh/uv/install.sh | sh 12 | 13 | # Windows 14 | powershell -c "irm https://astral.sh/uv/install.ps1 | iex" 15 | 16 | # Or using pip 17 | pip install uv 18 | ``` 19 | 20 | ## Quick Start 21 | 22 | ### Install dependencies 23 | 24 | ```bash 25 | uv sync 26 | ``` 27 | 28 | ### Install in development mode 29 | 30 | ```bash 31 | uv sync --dev 32 | ``` 33 | 34 | ### Run tests 35 | 36 | ```bash 37 | uv run pytest 38 | ``` 39 | 40 | ### Run the CLI tool 41 | 42 | ```bash 43 | uv run get-day-list --help 44 | ``` 45 | 46 | ### Install the package 47 | 48 | ```bash 49 | uv pip install -e . 50 | ``` 51 | 52 | ## Common Commands 53 | 54 | ### Development 55 | 56 | ```bash 57 | # Install all dependencies including dev tools 58 | uv sync --dev 59 | 60 | # Run tests 61 | uv run pytest 62 | 63 | # Run tests with coverage 64 | uv run pytest --cov=cn_stock_holidays 65 | 66 | # Format code 67 | uv run black . 68 | 69 | # Sort imports 70 | uv run isort . 71 | 72 | # Type checking 73 | uv run mypy cn_stock_holidays/ 74 | 75 | # Linting 76 | uv run flake8 cn_stock_holidays/ 77 | ``` 78 | 79 | ### Package Management 80 | 81 | ```bash 82 | # Add a new dependency 83 | uv add package-name 84 | 85 | # Add a development dependency 86 | uv add --dev package-name 87 | 88 | # Remove a dependency 89 | uv remove package-name 90 | 91 | # Update dependencies 92 | uv sync --upgrade 93 | ``` 94 | 95 | ### Building and Publishing 96 | 97 | ```bash 98 | # Build the package 99 | uv build 100 | 101 | # Build and publish to PyPI 102 | uv publish 103 | ``` 104 | 105 | ## Benefits of UV 106 | 107 | 1. **Speed**: UV is significantly faster than pip and other package managers 108 | 2. **Reliability**: Better dependency resolution and lock file management 109 | 3. **Modern**: Uses `pyproject.toml` as the standard configuration format 110 | 4. **Compatible**: Works with existing Python tooling and workflows 111 | 5. **Cross-platform**: Works on Windows, macOS, and Linux 112 | 113 | ## Migration from setup.py 114 | 115 | The project now uses `pyproject.toml` instead of `setup.py`. The `setup.py` file has been removed as it's no longer needed. This provides: 116 | 117 | - Better dependency specification 118 | - Modern Python packaging standards 119 | - Improved tool integration 120 | - More flexible configuration 121 | - Cleaner project structure 122 | 123 | ## CI/CD Integration 124 | 125 | Update your GitHub Actions workflow to use uv: 126 | 127 | ```yaml 128 | - name: Install uv 129 | uses: astral-sh/setup-uv@v1 130 | 131 | - name: Install dependencies 132 | run: uv sync --dev 133 | 134 | - name: Run tests 135 | run: uv run pytest 136 | ``` 137 | 138 | ## Troubleshooting 139 | 140 | ### Common Issues 141 | 142 | 1. **UV not found**: Make sure uv is installed and in your PATH 143 | 2. **Lock file conflicts**: Delete `uv.lock` and run `uv sync` again 144 | 3. **Permission errors**: Use `uv sync --no-cache` to bypass cache issues 145 | 146 | ### Getting Help 147 | 148 | - [UV Documentation](https://docs.astral.sh/uv/) 149 | - [UV GitHub Repository](https://github.com/astral-sh/uv) 150 | - [UV Discord Community](https://discord.gg/astral-sh) 151 | -------------------------------------------------------------------------------- /cn_stock_holidays/common.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | import sys 4 | 5 | 6 | if sys.version_info.major == 2: 7 | 8 | def function_cache(function): 9 | memo = {} 10 | 11 | @wraps(function) 12 | def wrapper(*args, **kwargs): 13 | if args in memo: 14 | return memo[args] 15 | else: 16 | rv = function(*args, **kwargs) 17 | memo[args] = rv 18 | return rv 19 | 20 | def cache_clear(): 21 | global memo 22 | memo = {} 23 | 24 | wrapper.cache_clear = cache_clear 25 | return wrapper 26 | 27 | else: # suppose it is 3 or larger 28 | from functools import lru_cache 29 | 30 | function_cache = lru_cache(None, typed=True) 31 | 32 | 33 | def int_to_date(d): 34 | d = str(d) 35 | return datetime.date(int(d[:4]), int(d[4:6]), int(d[6:])) 36 | 37 | 38 | def date_to_str(da): 39 | return da.strftime("%Y%m%d") 40 | 41 | 42 | def str_to_int(s): 43 | return int(s) 44 | 45 | 46 | def date_to_int(da): 47 | return str_to_int(date_to_str(da)) 48 | 49 | 50 | def print_result(s): 51 | print("-" * 20) 52 | print("*" + str(s) + "*") 53 | print("-" * 20) 54 | print("") 55 | 56 | 57 | def _get_from_file(filename, use_list=False): 58 | with open(filename, "r") as f: 59 | data = f.readlines() 60 | # Filter out empty lines and comments 61 | filtered_data = [ 62 | i.rstrip("\n") for i in data if i.strip() and not i.strip().startswith("#") 63 | ] 64 | # Process dates, handling both regular and half-day format 65 | processed_data = [] 66 | for i in filtered_data: 67 | if i.endswith(",h"): 68 | # For half-day trading days, treat them as regular holidays for backward compatibility 69 | processed_data.append(i[:-2]) # Remove ',h' suffix 70 | else: 71 | processed_data.append(i) 72 | 73 | if use_list: 74 | return [int_to_date(str_to_int(i)) for i in processed_data] 75 | else: 76 | return set([int_to_date(str_to_int(i)) for i in processed_data]) 77 | if use_list: 78 | return [] 79 | else: 80 | return set([]) 81 | 82 | 83 | def _get_from_file_with_half_day(filename, use_list=False): 84 | """ 85 | Read data from file with support for half-day trading format. 86 | Lines with 'h' suffix (e.g., '20251225,h') indicate half-day trading days. 87 | Returns a tuple: (holidays_set, half_day_set) 88 | """ 89 | holidays = set() 90 | half_days = set() 91 | 92 | try: 93 | with open(filename, "r") as f: 94 | for line in f: 95 | line = line.strip() 96 | if not line or line.startswith("#"): 97 | continue 98 | 99 | if line.endswith(",h"): 100 | # Half-day trading day 101 | date_str = line[:-2] # Remove ',h' suffix 102 | half_days.add(int_to_date(str_to_int(date_str))) 103 | else: 104 | # Regular holiday 105 | holidays.add(int_to_date(str_to_int(line))) 106 | except FileNotFoundError: 107 | pass 108 | 109 | if use_list: 110 | return list(holidays), list(half_days) 111 | else: 112 | return holidays, half_days 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python,pycharm 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | ### PyCharm ### 95 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 96 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 97 | 98 | # User-specific stuff: 99 | .idea/workspace.xml 100 | .idea/tasks.xml 101 | .idea/dictionaries 102 | .idea/vcs.xml 103 | .idea/jsLibraryMappings.xml 104 | 105 | # Sensitive or high-churn files: 106 | .idea/dataSources.ids 107 | .idea/dataSources.xml 108 | .idea/dataSources.local.xml 109 | .idea/sqlDataSources.xml 110 | .idea/dynamic.xml 111 | .idea/uiDesigner.xml 112 | 113 | # Gradle: 114 | .idea/gradle.xml 115 | .idea/libraries 116 | 117 | # Mongo Explorer plugin: 118 | .idea/mongoSettings.xml 119 | 120 | ## File-based project format: 121 | *.iws 122 | 123 | ## Plugin-specific files: 124 | 125 | # IntelliJ 126 | /out/ 127 | 128 | # mpeltonen/sbt-idea plugin 129 | .idea_modules/ 130 | 131 | # JIRA plugin 132 | atlassian-ide-plugin.xml 133 | 134 | # Crashlytics plugin (for Android Studio and IntelliJ) 135 | com_crashlytics_export_strings.xml 136 | crashlytics.properties 137 | crashlytics-build.properties 138 | fabric.properties 139 | 140 | ### PyCharm Patch ### 141 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 142 | 143 | # *.iml 144 | # modules.xml 145 | # .idea/misc.xml 146 | # *.ipr 147 | 148 | hki.pickle 149 | 150 | # uv 151 | .uv/ 152 | 153 | # Additional Python patterns 154 | *.py,cover 155 | .pytest_cache/ 156 | cover/ 157 | 158 | # pipenv 159 | Pipfile.lock 160 | 161 | # poetry 162 | poetry.lock 163 | 164 | # pdm 165 | .pdm.toml 166 | 167 | # PEP 582 168 | __pypackages__/ 169 | 170 | # mypy 171 | .mypy_cache/ 172 | .dmypy.json 173 | dmypy.json 174 | 175 | # Pyre type checker 176 | .pyre/ 177 | 178 | # pytype static type analyzer 179 | .pytype/ 180 | 181 | # Cython debug symbols 182 | cython_debug/ 183 | 184 | # VS Code 185 | .vscode/ 186 | 187 | # macOS 188 | .DS_Store 189 | 190 | # Windows 191 | Thumbs.db 192 | ehthumbs.db 193 | Desktop.ini 194 | -------------------------------------------------------------------------------- /scripts/dev_shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Development shell for cn_stock_holidays. 4 | 5 | A simple script to start IPython with all project modules pre-loaded 6 | for convenient development and debugging. 7 | 8 | Usage: 9 | python scripts/dev_shell.py 10 | # or 11 | uv run python scripts/dev_shell.py 12 | """ 13 | 14 | import sys 15 | from pathlib import Path 16 | 17 | # Add the project root to Python path 18 | project_root = Path(__file__).parent.parent 19 | sys.path.insert(0, str(project_root)) 20 | 21 | 22 | def main(): 23 | """Start IPython with project modules pre-loaded.""" 24 | try: 25 | from IPython import start_ipython 26 | 27 | # Pre-import modules 28 | import cn_stock_holidays 29 | import cn_stock_holidays.data 30 | import cn_stock_holidays.data_hk 31 | import cn_stock_holidays.common 32 | import cn_stock_holidays.meta_functions 33 | 34 | # Import commonly used functions 35 | from cn_stock_holidays.data import ( 36 | is_trading_day, 37 | is_holiday, 38 | trading_days_between, 39 | get_trading_days, 40 | get_holidays, 41 | sync_data, 42 | ) 43 | 44 | from cn_stock_holidays.data_hk import ( 45 | is_trading_day as is_trading_day_hk, 46 | is_half_day_trading_day, 47 | trading_days_between as trading_days_between_hk, 48 | get_half_day_trading_days, 49 | sync_data as sync_data_hk, 50 | ) 51 | 52 | from cn_stock_holidays.common import ( 53 | int_to_date, 54 | date_to_str, 55 | date_to_int, 56 | parse_date, 57 | format_date, 58 | is_weekend, 59 | ) 60 | 61 | from cn_stock_holidays.meta_functions import ( 62 | get_cache_info, 63 | clear_cache, 64 | is_cache_expired, 65 | get_half_day_cache_info, 66 | clear_half_day_cache, 67 | is_half_day_cache_expired, 68 | ) 69 | 70 | # Start IPython with comprehensive configuration 71 | start_ipython( 72 | argv=[], 73 | user_ns={ 74 | # Mainland China functions 75 | "is_trading_day": is_trading_day, 76 | "is_holiday": is_holiday, 77 | "trading_days_between": trading_days_between, 78 | "get_trading_days": get_trading_days, 79 | "get_holidays": get_holidays, 80 | "sync_data": sync_data, 81 | # Hong Kong functions 82 | "is_trading_day_hk": is_trading_day_hk, 83 | "is_half_day_trading_day": is_half_day_trading_day, 84 | "trading_days_between_hk": trading_days_between_hk, 85 | "get_half_day_trading_days": get_half_day_trading_days, 86 | "sync_data_hk": sync_data_hk, 87 | # Common utilities 88 | "int_to_date": int_to_date, 89 | "date_to_str": date_to_str, 90 | "date_to_int": date_to_int, 91 | "parse_date": parse_date, 92 | "format_date": format_date, 93 | "is_weekend": is_weekend, 94 | # Cache management 95 | "get_cache_info": get_cache_info, 96 | "clear_cache": clear_cache, 97 | "is_cache_expired": is_cache_expired, 98 | "get_half_day_cache_info": get_half_day_cache_info, 99 | "clear_half_day_cache": clear_half_day_cache, 100 | "is_half_day_cache_expired": is_half_day_cache_expired, 101 | }, 102 | ) 103 | 104 | except ImportError: 105 | print("IPython not found. Installing...") 106 | print("Run: uv add --dev ipython") 107 | print("Then run this script again.") 108 | sys.exit(1) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "cn-stock-holidays" 7 | version = "2.1.5" 8 | description = "A List of china stock exchange holidays" 9 | readme = "README.md" 10 | license = { text = "MIT" } 11 | authors = [{ name = "rainx", email = "i@rainx.cc" }] 12 | keywords = [ 13 | "china", 14 | "stock", 15 | "holiday", 16 | "exchange", 17 | "shanghai", 18 | "shenzhen", 19 | "hongkong", 20 | ] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Intended Audience :: Financial and Insurance Industry", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Topic :: Office/Business :: Financial", 33 | "Topic :: Software Development :: Libraries :: Python Modules", 34 | ] 35 | requires-python = ">=3.8.1" 36 | dependencies = ["requests>=2.25.0", "click>=8.0.0"] 37 | 38 | [project.urls] 39 | Homepage = "https://github.com/rainx/cn_stock_holidays" 40 | Repository = "https://github.com/rainx/cn_stock_holidays.git" 41 | Issues = "https://github.com/rainx/cn_stock_holidays/issues" 42 | 43 | [project.scripts] 44 | cn-stock-holiday-sync = "cn_stock_holidays.data:sync_data" 45 | cn-stock-holiday-sync-hk = "cn_stock_holidays.data_hk:sync_data" 46 | get-day-list = "cn_stock_holidays.tools.cmd:main" 47 | 48 | [tool.hatch.build.targets.wheel] 49 | packages = ["cn_stock_holidays"] 50 | 51 | [tool.hatch.build.targets.wheel.sources] 52 | "cn_stock_holidays" = "cn_stock_holidays" 53 | 54 | [tool.hatch.build.targets.wheel.include] 55 | "cn_stock_holidays/*.txt" = "cn_stock_holidays" 56 | 57 | # Development dependencies 58 | [project.optional-dependencies] 59 | dev = [ 60 | "pytest>=7.0.0", 61 | "pytest-cov>=4.0.0", 62 | "black>=23.0.0", 63 | "isort>=5.12.0", 64 | "flake8>=6.0.0", 65 | "mypy>=1.0.0", 66 | "pre-commit>=3.0.0", 67 | "ipython>=8.0.0", 68 | ] 69 | 70 | test = ["pytest>=7.0.0", "pytest-cov>=4.0.0"] 71 | 72 | # uv specific configuration 73 | [tool.uv] 74 | dev-dependencies = [ 75 | "pytest>=7.0.0", 76 | "pytest-cov>=4.0.0", 77 | "black>=23.0.0", 78 | "isort>=5.12.0", 79 | "flake8>=6.0.0", 80 | "mypy>=1.0.0", 81 | "pre-commit>=3.0.0", 82 | "ipython>=8.0.0", 83 | ] 84 | 85 | # Testing configuration 86 | [tool.pytest.ini_options] 87 | testpaths = ["tests"] 88 | python_files = ["*_test.py"] 89 | python_classes = ["Test*"] 90 | python_functions = ["test_*"] 91 | addopts = [ 92 | "--strict-markers", 93 | "--strict-config", 94 | "--cov=cn_stock_holidays", 95 | "--cov-report=term-missing", 96 | "--cov-report=html", 97 | ] 98 | 99 | # Code formatting 100 | [tool.black] 101 | line-length = 88 102 | target-version = ['py38'] 103 | include = '\.pyi?$' 104 | extend-exclude = ''' 105 | /( 106 | # directories 107 | \.eggs 108 | | \.git 109 | | \.hg 110 | | \.mypy_cache 111 | | \.tox 112 | | \.venv 113 | | build 114 | | dist 115 | )/ 116 | ''' 117 | 118 | [tool.isort] 119 | profile = "black" 120 | multi_line_output = 3 121 | line_length = 88 122 | known_first_party = ["cn_stock_holidays"] 123 | 124 | # Type checking 125 | [tool.mypy] 126 | python_version = "3.8.1" 127 | warn_return_any = true 128 | warn_unused_configs = true 129 | disallow_untyped_defs = true 130 | disallow_incomplete_defs = true 131 | check_untyped_defs = true 132 | disallow_untyped_decorators = true 133 | no_implicit_optional = true 134 | warn_redundant_casts = true 135 | warn_unused_ignores = true 136 | warn_no_return = true 137 | warn_unreachable = true 138 | strict_equality = true 139 | 140 | [[tool.mypy.overrides]] 141 | module = ["tests.*"] 142 | disallow_untyped_defs = false 143 | disallow_incomplete_defs = false 144 | -------------------------------------------------------------------------------- /tests/data_hk_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import unittest 4 | 5 | from cn_stock_holidays.data_hk import * 6 | 7 | 8 | class TestHkData(unittest.TestCase): 9 | def test_get_cached(self): 10 | """ 11 | get cached related 12 | """ 13 | data = get_cached() 14 | data2 = get_cached() 15 | 16 | self.assertEqual(str(data), str(data2), "get_cached 2 times give some results") 17 | 18 | self.assertGreater(len(data), 0, "is greater then 0") 19 | 20 | self.assertIsInstance(list(data)[0], datetime.date, "is a date") 21 | 22 | self.assertTrue( 23 | datetime.date(2000, 12, 25) in data, 24 | "get datetime.date(2000, 12, 25) in cached data", 25 | ) 26 | 27 | def test_trading_days_between(self): 28 | is_trading_day.cache_clear() 29 | get_cached_with_half_day.cache_clear() 30 | 31 | # Debug: check what holidays and half_days contain 32 | holidays, half_days = get_cached_with_half_day() 33 | print("DEBUG holidays containing 20170126:", int_to_date(20170126) in holidays) 34 | print( 35 | "DEBUG half_days containing 20170126:", int_to_date(20170126) in half_days 36 | ) 37 | 38 | data = list(trading_days_between(int_to_date(20170125), int_to_date(20170131))) 39 | 40 | print("DEBUG trading_days_between result:", data) 41 | print("DEBUG is_trading_day(20170125):", is_trading_day(int_to_date(20170125))) 42 | print("DEBUG is_trading_day(20170126):", is_trading_day(int_to_date(20170126))) 43 | print("DEBUG is_trading_day(20170127):", is_trading_day(int_to_date(20170127))) 44 | print("DEBUG is_trading_day(20170130):", is_trading_day(int_to_date(20170130))) 45 | print("DEBUG is_trading_day(20170131):", is_trading_day(int_to_date(20170131))) 46 | 47 | self.assertEqual(len(data), 3) 48 | self.assertTrue(int_to_date(20170125) in data) 49 | self.assertTrue(int_to_date(20170126) in data) 50 | self.assertTrue(int_to_date(20170127) in data) 51 | 52 | def test_is_trading_day(self): 53 | self.assertIsNotNone(is_trading_day(datetime.date.today())) 54 | 55 | def test_next_trading_day(self): 56 | data = next_trading_day(datetime.date.today()) 57 | self.assertGreater(data, datetime.date.today()) 58 | 59 | def test_previous_trading_day(self): 60 | data = previous_trading_day(datetime.date.today()) 61 | self.assertLess(data, datetime.date.today()) 62 | 63 | def test_cache_clear(self): 64 | data = get_cached() 65 | get_cached.cache_clear() 66 | data2 = get_cached() 67 | self.assertEqual(str(data), str(data2), "get_cached 2 times give some results") 68 | 69 | def test_loop_100000(self): 70 | trade_days = datetime.date.today() 71 | for i in range(100000): 72 | trade_days = previous_trading_day(trade_days) 73 | 74 | self.assertIsInstance(trade_days, datetime.date) 75 | 76 | def test_half_day_trading_functionality(self): 77 | """ 78 | Test the new half-day trading functionality 79 | """ 80 | import cn_stock_holidays.data_hk as data_hk 81 | 82 | self.assertTrue(hasattr(data_hk, "is_half_day_trading_day")) 83 | 84 | # Test with today's date 85 | result = is_half_day_trading_day(datetime.date.today()) 86 | self.assertIsInstance(result, bool) 87 | 88 | # Test with a datetime object 89 | result = is_half_day_trading_day(datetime.datetime.now()) 90 | self.assertIsInstance(result, bool) 91 | 92 | # Test that half-day trading days are also trading days 93 | holidays, half_days = get_cached_with_half_day() 94 | if half_days: 95 | sample_half_day = list(half_days)[0] 96 | print(f"DEBUG sample_half_day: {sample_half_day}") 97 | print(f"DEBUG is_trading_day meta info: {is_trading_day.__code__}") 98 | self.assertTrue(is_trading_day(sample_half_day)) 99 | self.assertTrue(is_half_day_trading_day(sample_half_day)) 100 | -------------------------------------------------------------------------------- /scripts/test_dev_env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify the development environment is working correctly. 4 | 5 | This script tests that all the debugging tools and IPython setup are working. 6 | """ 7 | 8 | import sys 9 | from pathlib import Path 10 | 11 | # Add the project root to Python path 12 | project_root = Path(__file__).parent.parent 13 | sys.path.insert(0, str(project_root)) 14 | 15 | 16 | def test_imports(): 17 | """Test that all modules can be imported.""" 18 | print("Testing imports...") 19 | 20 | try: 21 | import cn_stock_holidays 22 | 23 | print("✓ cn_stock_holidays imported") 24 | 25 | import cn_stock_holidays.data 26 | 27 | print("✓ cn_stock_holidays.data imported") 28 | 29 | import cn_stock_holidays.data_hk 30 | 31 | print("✓ cn_stock_holidays.data_hk imported") 32 | 33 | import cn_stock_holidays.common 34 | 35 | print("✓ cn_stock_holidays.common imported") 36 | 37 | import cn_stock_holidays.meta_functions 38 | 39 | print("✓ cn_stock_holidays.meta_functions imported") 40 | 41 | return True 42 | except ImportError as e: 43 | print(f"✗ Import error: {e}") 44 | return False 45 | 46 | 47 | def test_functions(): 48 | """Test that key functions work.""" 49 | print("\nTesting functions...") 50 | 51 | try: 52 | from cn_stock_holidays.data import is_trading_day 53 | from cn_stock_holidays.data_hk import is_half_day_trading_day 54 | from cn_stock_holidays.common import int_to_date 55 | 56 | from datetime import date 57 | 58 | # Test mainland China 59 | result = is_trading_day(date(2024, 1, 1)) 60 | print(f"✓ is_trading_day(2024-01-01) = {result}") 61 | 62 | # Test Hong Kong half-day 63 | result = is_half_day_trading_day(date(2024, 12, 24)) 64 | print(f"✓ is_half_day_trading_day(2024-12-24) = {result}") 65 | 66 | # Test date conversion 67 | result = int_to_date(20240101) 68 | print(f"✓ int_to_date(20240101) = {result}") 69 | 70 | return True 71 | except Exception as e: 72 | print(f"✗ Function error: {e}") 73 | return False 74 | 75 | 76 | def test_ipython(): 77 | """Test that IPython can be imported.""" 78 | print("\nTesting IPython...") 79 | 80 | try: 81 | from IPython import start_ipython 82 | 83 | print("✓ IPython can be imported") 84 | return True 85 | except ImportError as e: 86 | print(f"✗ IPython import error: {e}") 87 | return False 88 | 89 | 90 | def test_scripts(): 91 | """Test that all scripts exist and are executable.""" 92 | print("\nTesting scripts...") 93 | 94 | scripts = [ 95 | "scripts/quick_test.py", 96 | "scripts/dev_shell.py", 97 | "scripts/ipython_config.py", 98 | ] 99 | 100 | all_exist = True 101 | for script in scripts: 102 | if Path(script).exists(): 103 | print(f"✓ {script} exists") 104 | else: 105 | print(f"✗ {script} missing") 106 | all_exist = False 107 | 108 | return all_exist 109 | 110 | 111 | def main(): 112 | """Run all tests.""" 113 | print("cn_stock_holidays Development Environment Test") 114 | print("=" * 50) 115 | 116 | tests = [ 117 | test_imports, 118 | test_functions, 119 | test_ipython, 120 | test_scripts, 121 | ] 122 | 123 | passed = 0 124 | total = len(tests) 125 | 126 | for test in tests: 127 | if test(): 128 | passed += 1 129 | 130 | print("\n" + "=" * 50) 131 | print(f"Tests passed: {passed}/{total}") 132 | 133 | if passed == total: 134 | print("🎉 All tests passed! Development environment is ready.") 135 | print("\nYou can now use:") 136 | print(" python scripts/quick_test.py # Quick functionality test") 137 | print(" python scripts/dev_shell.py # Start IPython with modules") 138 | print(" uv run python scripts/dev_shell.py # Using uv environment") 139 | else: 140 | print("❌ Some tests failed. Please check the errors above.") 141 | 142 | return passed == total 143 | 144 | 145 | if __name__ == "__main__": 146 | success = main() 147 | sys.exit(0 if success else 1) 148 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # Cursor Rules for cn_stock_holidays 2 | 3 | ## Project Overview 4 | 5 | This is a Python package providing China stock exchange holiday data for both Shanghai/Shenzhen (SHSZ) and Hong Kong (HKEX) markets. The project uses modern Python packaging standards with uv as the package manager. 6 | 7 | ## Code Style & Standards 8 | 9 | ### Language Requirements 10 | 11 | - **All code comments and commit messages must be in English** 12 | - **Documentation**: README.md is in English, README-zh_CN.md is the Chinese translation 13 | - **Code**: All variable names, function names, and comments must be in English 14 | 15 | ### Python Standards 16 | 17 | - Use Python 3.8+ features 18 | - Follow PEP 8 style guidelines 19 | - Use type hints where appropriate 20 | - Use f-strings for string formatting 21 | - Prefer list comprehensions over map/filter when readable 22 | 23 | ### Package Management 24 | 25 | - Use `uv` as the primary package manager 26 | - Dependencies are managed in `pyproject.toml` 27 | - Development dependencies include: pytest, black, isort, flake8, mypy, pre-commit 28 | 29 | ### Code Quality 30 | 31 | - All code must pass black formatting 32 | - All imports must be sorted with isort 33 | - All code must pass flake8 linting 34 | - All code must pass mypy type checking 35 | - Use pre-commit hooks for automated quality checks 36 | 37 | ### Testing 38 | 39 | - Write tests for all new functionality 40 | - Use pytest as the testing framework 41 | - Aim for high test coverage 42 | - Test files should be named `*_test.py` 43 | 44 | ### Documentation 45 | 46 | - Keep README.md and README-zh_CN.md in sync 47 | - Update Changelog.md for all version releases 48 | - Document all public APIs 49 | - Include usage examples in docstrings 50 | 51 | ### Changelog Format 52 | 53 | - **File naming**: Use `Changelog.md` (English) and `Changelog-zh_CN.md` (Chinese) 54 | - **Version titles**: Use format `## cn-stock-holidays x.y.z (YYYY-MM-DD)` 55 | - **Categories**: Use ### headers to categorize changes: 56 | - `### New Features` - New functionality added 57 | - `### Bug Fixes` - Bug fixes and corrections 58 | - `### Improvements` - Enhancements and updates 59 | - `### Breaking Changes` - Breaking changes (if any) 60 | - `### Historical` - For legacy/version 0.x entries 61 | - **Content**: All changelog entries must be in English for Changelog.md 62 | - **Translation**: Keep Changelog.md and Changelog-zh_CN.md synchronized 63 | 64 | ### Git Workflow 65 | 66 | - Use conventional commit messages 67 | - All commits must be in English 68 | - Create feature branches for new development 69 | - Ensure CI passes before merging 70 | 71 | ### Project Structure 72 | 73 | ``` 74 | cn_stock_holidays/ 75 | ├── cn_stock_holidays/ # Main package 76 | │ ├── __init__.py 77 | │ ├── data.py # Shanghai/Shenzhen market data 78 | │ ├── data_hk.py # Hong Kong market data 79 | │ ├── common.py # Shared utilities 80 | │ ├── meta_functions.py # Meta-function patterns 81 | │ ├── zipline/ # Zipline integration 82 | │ └── tools/ # Command line tools 83 | ├── tests/ # Test files 84 | ├── scripts/ # Development scripts 85 | ├── .github/workflows/ # CI/CD workflows 86 | │ └── ci.yml # Test, Build & Publish workflow 87 | ├── pyproject.toml # Project configuration 88 | ├── README.md # English documentation 89 | ├── README-zh_CN.md # Chinese documentation 90 | ├── Changelog.md # English changelog 91 | ├── Changelog-zh_CN.md # Chinese changelog 92 | └── .cursorrules # This file 93 | ``` 94 | 95 | ### Key Principles 96 | 97 | 1. **Internationalization**: Support both English and Chinese users 98 | 2. **Modern Python**: Use latest Python packaging standards 99 | 3. **Performance**: Optimize for speed with caching mechanisms 100 | 4. **Reliability**: Ensure data accuracy and availability 101 | 5. **Maintainability**: Keep code clean and well-documented 102 | 103 | ### When Making Changes 104 | 105 | 1. Update both README.md and README-zh_CN.md if documentation changes 106 | 2. Add tests for new functionality 107 | 3. Update Changelog.md and Changelog-zh_CN.md for version releases 108 | 4. Ensure all CI checks pass 109 | 5. Follow the established code style and patterns 110 | 6. Use the standardized changelog format with proper categorization 111 | -------------------------------------------------------------------------------- /cn_stock_holidays/data_hk.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | Help functions for python to get Hongkong stock exchange holidays 5 | """ 6 | 7 | from cn_stock_holidays.meta_functions import * 8 | 9 | DATA_FILE_FOR_HK = "data_hk.txt" 10 | 11 | get_local = meta_get_local(data_file_name=DATA_FILE_FOR_HK) 12 | get_cache_path = meta_get_cache_path(data_file_name=DATA_FILE_FOR_HK) 13 | 14 | # Half-day trading support 15 | get_local_with_half_day = meta_get_local_with_half_day(data_file_name=DATA_FILE_FOR_HK) 16 | 17 | 18 | @function_cache 19 | def get_cached(use_list=False): 20 | return meta_get_cached(get_local=get_local, get_cache_path=get_cache_path)( 21 | use_list=False 22 | ) 23 | 24 | 25 | @function_cache 26 | def get_cached_with_half_day(use_list=False): 27 | return meta_get_cached_with_half_day( 28 | get_local_with_half_day=get_local_with_half_day, get_cache_path=get_cache_path 29 | )(use_list=False) 30 | 31 | 32 | get_remote_and_cache = meta_get_remote_and_cache( 33 | get_cached=get_cached, get_cache_path=get_cache_path 34 | ) 35 | check_expired = meta_check_expired(get_cached=get_cached) 36 | sync_data = meta_sync_data( 37 | check_expired=check_expired, get_remote_and_cache=get_remote_and_cache 38 | ) 39 | is_trading_day = meta_is_trading_day(get_cached=get_cached) 40 | previous_trading_day = meta_previous_trading_day(is_trading_day=is_trading_day) 41 | next_trading_day = meta_next_trading_day(is_trading_day=is_trading_day) 42 | 43 | # Half-day trading functions 44 | get_remote_and_cache_with_half_day = meta_get_remote_and_cache_with_half_day( 45 | get_cached_with_half_day=get_cached_with_half_day, 46 | get_cache_path=get_cache_path, 47 | data_file_name=DATA_FILE_FOR_HK, 48 | ) 49 | check_expired_with_half_day = meta_check_expired_with_half_day( 50 | get_cached_with_half_day=get_cached_with_half_day 51 | ) 52 | sync_data_with_half_day = meta_sync_data_with_half_day( 53 | check_expired_with_half_day=check_expired_with_half_day, 54 | get_remote_and_cache_with_half_day=get_remote_and_cache_with_half_day, 55 | ) 56 | is_half_day_trading_day = meta_is_half_day_trading_day( 57 | get_cached_with_half_day=get_cached_with_half_day 58 | ) 59 | 60 | 61 | # Override is_trading_day for HK to treat both normal and half-day trading days as trading days 62 | def is_trading_day(dt): 63 | if type(dt) is datetime.datetime: 64 | dt = dt.date() 65 | if dt.weekday() >= 5: 66 | return False 67 | holidays, _half_days = get_cached_with_half_day() 68 | if dt in holidays: 69 | return False 70 | return True 71 | 72 | 73 | is_trading_day.cache_clear = lambda: get_cached_with_half_day.cache_clear() 74 | 75 | 76 | # Override trading_days_between for HK to treat both normal and half-day trading days as trading days 77 | def trading_days_between(start, end): 78 | if type(start) is datetime.datetime: 79 | start = start.date() 80 | if type(end) is datetime.datetime: 81 | end = end.date() 82 | holidays, _half_days = get_cached_with_half_day() 83 | if start > end: 84 | return 85 | curdate = start 86 | while curdate <= end: 87 | if curdate.weekday() < 5 and curdate not in holidays: 88 | yield curdate 89 | curdate = curdate + datetime.timedelta(days=1) 90 | 91 | 92 | if __name__ == "__main__": 93 | data = check_expired() 94 | 95 | print("get datetime.date(1991, 2, 15) in cached data") 96 | data = get_cached() 97 | print_result(datetime.date(1991, 2, 15) in data) 98 | 99 | print("test trading_days_between 20170125 to 20170131") 100 | data = list(trading_days_between(int_to_date(20170125), int_to_date(20170131))) 101 | print_result(data) 102 | 103 | print("is trading day today?") 104 | data = is_trading_day(datetime.date.today()) 105 | print_result(data) 106 | 107 | print("next trading day after today?") 108 | data = next_trading_day(datetime.date.today()) 109 | print_result(data) 110 | 111 | print("previous trading day before today?") 112 | data = previous_trading_day(datetime.date.today()) 113 | print_result(data) 114 | 115 | print("Test loop 100000") 116 | trade_days = datetime.date.today() 117 | for i in range(100000): 118 | trade_days = previous_trading_day(trade_days) 119 | 120 | # Test half-day trading functionality 121 | print("test half-day trading functionality") 122 | holidays, half_days = get_cached_with_half_day() 123 | print_result(f"Total holidays: {len(holidays)}, Total half-days: {len(half_days)}") 124 | 125 | if half_days: 126 | sample_half_day = list(half_days)[0] 127 | print(f"Sample half-day: {sample_half_day}") 128 | print_result(is_half_day_trading_day(sample_half_day)) 129 | -------------------------------------------------------------------------------- /scripts/check_weekend_half_days.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Check which half-day trading dates fall on weekends and need to be removed. 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | from datetime import datetime, date 9 | 10 | # Add the project root to Python path 11 | project_root = Path(__file__).parent.parent 12 | sys.path.insert(0, str(project_root)) 13 | 14 | from cn_stock_holidays.common import int_to_date 15 | 16 | 17 | def check_weekend_half_days(): 18 | """Check which half-day trading dates fall on weekends.""" 19 | data_file = Path("cn_stock_holidays/data_hk.txt") 20 | 21 | weekend_half_days = [] 22 | valid_half_days = [] 23 | 24 | with open(data_file, "r") as f: 25 | for line in f: 26 | line = line.strip() 27 | if line.endswith(",h"): 28 | date_str = line[:-2] # Remove ',h' suffix 29 | try: 30 | date_obj = int_to_date(int(date_str)) 31 | weekday = date_obj.weekday() # 0=Monday, 6=Sunday 32 | 33 | if weekday >= 5: # Saturday (5) or Sunday (6) 34 | weekend_half_days.append((date_str, date_obj, weekday)) 35 | else: 36 | valid_half_days.append((date_str, date_obj, weekday)) 37 | except ValueError as e: 38 | print(f"Error parsing date {date_str}: {e}") 39 | 40 | print("=== Weekend Half-Day Trading Days (Need to be removed) ===") 41 | for date_str, date_obj, weekday in weekend_half_days: 42 | weekday_name = [ 43 | "Monday", 44 | "Tuesday", 45 | "Wednesday", 46 | "Thursday", 47 | "Friday", 48 | "Saturday", 49 | "Sunday", 50 | ][weekday] 51 | print(f"{date_str} -> {date_obj} ({weekday_name})") 52 | 53 | print(f"\nTotal weekend half-days: {len(weekend_half_days)}") 54 | 55 | print("\n=== Valid Half-Day Trading Days ===") 56 | for date_str, date_obj, weekday in valid_half_days[:10]: # Show first 10 57 | weekday_name = [ 58 | "Monday", 59 | "Tuesday", 60 | "Wednesday", 61 | "Thursday", 62 | "Friday", 63 | "Saturday", 64 | "Sunday", 65 | ][weekday] 66 | print(f"{date_str} -> {date_obj} ({weekday_name})") 67 | 68 | if len(valid_half_days) > 10: 69 | print(f"... and {len(valid_half_days) - 10} more") 70 | 71 | print(f"\nTotal valid half-days: {len(valid_half_days)}") 72 | 73 | return weekend_half_days, valid_half_days 74 | 75 | 76 | def remove_weekend_half_days(): 77 | """Remove weekend half-day trading days from the data file.""" 78 | data_file = Path("cn_stock_holidays/data_hk.txt") 79 | backup_file = Path("cn_stock_holidays/data_hk.txt.backup") 80 | 81 | # Create backup 82 | import shutil 83 | 84 | shutil.copy2(data_file, backup_file) 85 | print(f"Created backup: {backup_file}") 86 | 87 | weekend_half_days, valid_half_days = check_weekend_half_days() 88 | 89 | if not weekend_half_days: 90 | print("No weekend half-days found. No changes needed.") 91 | return 92 | 93 | # Read all lines 94 | with open(data_file, "r") as f: 95 | lines = f.readlines() 96 | 97 | # Filter out weekend half-days 98 | weekend_half_day_strings = {f"{date_str},h" for date_str, _, _ in weekend_half_days} 99 | 100 | filtered_lines = [] 101 | removed_count = 0 102 | 103 | for line in lines: 104 | line_stripped = line.strip() 105 | if line_stripped in weekend_half_day_strings: 106 | removed_count += 1 107 | print(f"Removing: {line_stripped}") 108 | else: 109 | filtered_lines.append(line) 110 | 111 | # Write back the filtered content 112 | with open(data_file, "w") as f: 113 | f.writelines(filtered_lines) 114 | 115 | print(f"\nRemoved {removed_count} weekend half-day trading days.") 116 | print(f"Updated {data_file}") 117 | 118 | 119 | if __name__ == "__main__": 120 | print("Checking weekend half-day trading days...") 121 | print("=" * 60) 122 | 123 | weekend_half_days, valid_half_days = check_weekend_half_days() 124 | 125 | if weekend_half_days: 126 | print( 127 | f"\nFound {len(weekend_half_days)} weekend half-days that need to be removed." 128 | ) 129 | response = input("\nDo you want to remove them? (y/N): ") 130 | if response.lower() in ["y", "yes"]: 131 | remove_weekend_half_days() 132 | else: 133 | print("No changes made.") 134 | else: 135 | print("\nNo weekend half-days found. Data is already correct.") 136 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Development Scripts 2 | 3 | This directory contains various scripts to help with development and debugging of the `cn_stock_holidays` package. 4 | 5 | ## Quick Start 6 | 7 | ### 1. Setup Development Environment 8 | 9 | ```bash 10 | # Install all development dependencies including IPython 11 | ./scripts/setup-dev.sh 12 | ``` 13 | 14 | ### 2. Start Development Shell 15 | 16 | ```bash 17 | # Start IPython with all modules pre-loaded 18 | python scripts/dev_shell.py 19 | # or 20 | uv run python scripts/dev_shell.py 21 | ``` 22 | 23 | ## Available Scripts 24 | 25 | ### `dev_shell.py` - Development Shell 26 | 27 | The main development environment script that starts IPython with: 28 | 29 | - All project modules pre-imported 30 | - Auto-reload enabled for development 31 | - Commonly used functions available 32 | - Helpful examples and documentation 33 | 34 | **Usage:** 35 | 36 | ```bash 37 | python scripts/dev_shell.py 38 | ``` 39 | 40 | **Features:** 41 | 42 | - Auto-reloads modules when you make changes 43 | - Pre-imports all main functions 44 | - Provides helpful examples 45 | - Syntax highlighting and auto-completion 46 | 47 | ### `quick_test.py` - Quick Functionality Test 48 | 49 | A simple script to quickly test the package functionality without starting IPython. 50 | 51 | **Usage:** 52 | 53 | ```bash 54 | python scripts/quick_test.py 55 | ``` 56 | 57 | **Tests:** 58 | 59 | - Mainland China market functions 60 | - Hong Kong market functions (including half-day trading) 61 | - Cache management 62 | - Common utilities 63 | 64 | ## IPython Configuration 65 | 66 | ### `ipython_config.py` - IPython Configuration 67 | 68 | Advanced IPython configuration file with custom settings. 69 | 70 | **Usage:** 71 | 72 | ```bash 73 | # In IPython 74 | exec(open('scripts/ipython_config.py').read()) 75 | ``` 76 | 77 | **Features:** 78 | 79 | - Custom aliases for common operations 80 | - Syntax highlighting 81 | - Auto-completion 82 | - History search 83 | 84 | ## Common Debugging Tasks 85 | 86 | ### 1. Test Trading Day Functions 87 | 88 | ```python 89 | # In IPython or debug shell 90 | is_trading_day('2024-01-01') # Mainland China 91 | is_trading_day_hk('2024-12-24') # Hong Kong 92 | is_half_day_trading_day('2024-12-24') # Hong Kong half-day 93 | ``` 94 | 95 | ### 2. Check Cache Status 96 | 97 | ```python 98 | get_cache_info() # Main cache 99 | get_half_day_cache_info() # Half-day cache 100 | clear_cache() # Clear main cache 101 | clear_half_day_cache() # Clear half-day cache 102 | ``` 103 | 104 | ### 3. Get Trading Days 105 | 106 | ```python 107 | # Get trading days between dates 108 | trading_days_between('2024-01-01', '2024-01-31') # Mainland 109 | trading_days_between_hk('2024-12-01', '2024-12-31') # Hong Kong 110 | 111 | # Get half-day trading days 112 | get_half_day_trading_days('2024-12-01', '2024-12-31') 113 | ``` 114 | 115 | ### 4. Test Date Utilities 116 | 117 | ```python 118 | parse_date('2024-01-01') # Parse date string 119 | format_date(date(2024, 1, 1)) # Format date object 120 | is_weekend('2024-01-06') # Check if weekend 121 | ``` 122 | 123 | ## Development Workflow 124 | 125 | 1. **Start Development Shell:** 126 | 127 | ```bash 128 | python scripts/dev_shell.py 129 | ``` 130 | 131 | 2. **Test Functions:** 132 | 133 | ```python 134 | # Test basic functionality 135 | is_trading_day('2024-01-01') 136 | is_half_day_trading_day('2024-12-24') 137 | ``` 138 | 139 | 3. **Make Changes:** 140 | 141 | - Edit source files 142 | - Changes auto-reload in IPython 143 | 144 | 4. **Run Quick Tests:** 145 | 146 | ```bash 147 | python scripts/quick_test.py 148 | ``` 149 | 150 | 5. **Run Full Tests:** 151 | ```bash 152 | uv run pytest 153 | ``` 154 | 155 | ## Troubleshooting 156 | 157 | ### IPython Not Found 158 | 159 | If you get an error about IPython not being found: 160 | 161 | ```bash 162 | # Install IPython 163 | uv add --dev ipython 164 | 165 | # Then run the script again 166 | python scripts/dev_shell.py 167 | ``` 168 | 169 | ### Import Errors 170 | 171 | If you get import errors, make sure you're running from the project root: 172 | 173 | ```bash 174 | cd /path/to/cn_stock_holidays 175 | python scripts/dev_shell.py 176 | ``` 177 | 178 | ### Auto-reload Issues 179 | 180 | If auto-reload isn't working: 181 | 182 | ```python 183 | # In IPython 184 | %load_ext autoreload 185 | %autoreload 2 186 | ``` 187 | 188 | ## Tips 189 | 190 | 1. **Use Auto-reload:** The development shell automatically enables auto-reload, so your changes are immediately available. 191 | 192 | 2. **Check Cache:** Use `get_cache_info()` to see if data is cached and when it expires. 193 | 194 | 3. **Test Both Markets:** Remember to test both mainland China (`data`) and Hong Kong (`data_hk`) functions. 195 | 196 | 4. **Use Help:** In IPython, use `help(function_name)` to get detailed documentation. 197 | 198 | 5. **Quick Tests:** Use `python scripts/quick_test.py` for a quick sanity check before running full tests. 199 | -------------------------------------------------------------------------------- /cn_stock_holidays/zipline/exchange_calendar_hkex.py: -------------------------------------------------------------------------------- 1 | from datetime import time 2 | from cn_stock_holidays.data_hk import get_cached 3 | from pandas import Timestamp, date_range, DatetimeIndex 4 | import pytz 5 | from zipline.utils.memoize import remember_last, lazyval 6 | import warnings 7 | 8 | from zipline.utils.calendars import TradingCalendar 9 | from zipline.utils.calendars.trading_calendar import days_at_time, NANOS_IN_MINUTE 10 | import numpy as np 11 | import pandas as pd 12 | 13 | # lunch break for shanghai and shenzhen exchange 14 | lunch_break_start = time(12, 30) 15 | lunch_break_end = time(14, 31) 16 | 17 | start_default = pd.Timestamp("2000-12-25", tz="UTC") 18 | end_base = pd.Timestamp("today", tz="UTC") 19 | end_default = end_base + pd.Timedelta(days=365) 20 | 21 | 22 | class HKExchangeCalendar(TradingCalendar): 23 | """ 24 | Exchange calendar for Shanghai and Shenzhen (China Market) 25 | Open Time 9:31 AM, Asia/Shanghai 26 | Close Time 3:00 PM, Asia/Shanghai 27 | 28 | One big difference between china and us exchange is china exchange has a lunch break , so I handle it 29 | 30 | Sample Code in ipython: 31 | 32 | > from zipline.utils.calendars import * 33 | > from cn_stock_holidays.zipline.exchange_calendar_hkex import HKExchangeCalendar 34 | > register_calendar("HKEX", HKExchangeCalendar(), force=True) 35 | > c=get_calendar("HKEX") 36 | 37 | for the guy need to keep updating about holiday file, try to add `cn-stock-holiday-sync-hk` command to crontab 38 | """ 39 | 40 | def __init__(self, start=start_default, end=end_default): 41 | with warnings.catch_warnings(): 42 | warnings.simplefilter("ignore") 43 | _all_days = date_range(start, end, freq=self.day, tz="UTC") 44 | 45 | self._lunch_break_starts = days_at_time( 46 | _all_days, lunch_break_start, self.tz, 0 47 | ) 48 | self._lunch_break_ends = days_at_time(_all_days, lunch_break_end, self.tz, 0) 49 | 50 | TradingCalendar.__init__(self, start=start_default, end=end_default) 51 | 52 | @property 53 | def name(self): 54 | return "HKEX" 55 | 56 | @property 57 | def tz(self): 58 | return pytz.timezone("Asia/Shanghai") 59 | 60 | @property 61 | def open_time(self): 62 | return time(10, 1) 63 | 64 | @property 65 | def close_time(self): 66 | return time(16, 0) 67 | 68 | @property 69 | def adhoc_holidays(self): 70 | return [Timestamp(t, tz=pytz.UTC) for t in get_cached(use_list=True)] 71 | 72 | @property 73 | @remember_last 74 | def all_minutes(self): 75 | """ 76 | Returns a DatetimeIndex representing all the minutes in this calendar. 77 | """ 78 | opens_in_ns = self._opens.values.astype("datetime64[ns]") 79 | 80 | closes_in_ns = self._closes.values.astype("datetime64[ns]") 81 | 82 | lunch_break_start_in_ns = self._lunch_break_starts.values.astype( 83 | "datetime64[ns]" 84 | ) 85 | lunch_break_ends_in_ns = self._lunch_break_ends.values.astype("datetime64[ns]") 86 | 87 | deltas_before_lunch = lunch_break_start_in_ns - opens_in_ns 88 | deltas_after_lunch = closes_in_ns - lunch_break_ends_in_ns 89 | 90 | daily_before_lunch_sizes = (deltas_before_lunch / NANOS_IN_MINUTE) + 1 91 | daily_after_lunch_sizes = (deltas_after_lunch / NANOS_IN_MINUTE) + 1 92 | 93 | daily_sizes = daily_before_lunch_sizes + daily_after_lunch_sizes 94 | 95 | num_minutes = np.sum(daily_sizes).astype(np.int64) 96 | 97 | # One allocation for the entire thing. This assumes that each day 98 | # represents a contiguous block of minutes. 99 | all_minutes = np.empty(num_minutes, dtype="datetime64[ns]") 100 | 101 | idx = 0 102 | for day_idx, size in enumerate(daily_sizes): 103 | # lots of small allocations, but it's fast enough for now. 104 | 105 | # size is a np.timedelta64, so we need to int it 106 | size_int = int(size) 107 | 108 | before_lunch_size_int = int(daily_before_lunch_sizes[day_idx]) 109 | after_lunch_size_int = int(daily_after_lunch_sizes[day_idx]) 110 | 111 | # print("idx:{}, before_lunch_size_int: {}".format(idx, before_lunch_size_int)) 112 | all_minutes[idx : (idx + before_lunch_size_int)] = np.arange( 113 | opens_in_ns[day_idx], 114 | lunch_break_start_in_ns[day_idx] + NANOS_IN_MINUTE, 115 | NANOS_IN_MINUTE, 116 | ) 117 | 118 | all_minutes[(idx + before_lunch_size_int) : (idx + size_int)] = np.arange( 119 | lunch_break_ends_in_ns[day_idx], 120 | closes_in_ns[day_idx] + NANOS_IN_MINUTE, 121 | NANOS_IN_MINUTE, 122 | ) 123 | 124 | idx += size_int 125 | return DatetimeIndex(all_minutes).tz_localize("UTC") 126 | 127 | 128 | if __name__ == "__main__": 129 | HKExchangeCalendar() 130 | -------------------------------------------------------------------------------- /Changelog-zh_CN.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | 版本遵循语义化版本控制 (..)。 4 | 5 | ## cn-stock-holidays 2.1.5 (2025-12-07) 6 | 7 | ### 改进 8 | 9 | - **更新 2026 年香港股市节假日**: 从香港交易所官方通告添加完整的 2026 年节假日数据 10 | - 全日休市:元旦(1月1日)、农历新年(2月17-19日)、耶稣受难节(4月3日)、清明节翌日(4月6日)、复活节星期一翌日(4月7日)、劳动节(5月1日)、佛诞翌日(5月25日)、端午节(6月19日)、香港特别行政区成立纪念日(7月1日)、国庆日(10月1日)、重阳节翌日(10月19日)、圣诞节(12月25日) 11 | - 半日交易:农历新年前夕(2月16日)、圣诞节前夕(12月24日)、新年前夕(12月31日) 12 | - 数据来源:香港交易所通告 CT/075/25 () 13 | 14 | ## cn-stock-holidays 2.1.4 (2025-12-07) 15 | 16 | ### 改进 17 | 18 | - **更新 2026 年股市节假日**: 从通达信官方数据源添加完整的 2026 年节假日数据 19 | - 元旦:1月1-2日 20 | - 春节:2月16-20日、23日 21 | - 清明节:4月6日 22 | - 劳动节:5月1日、4-5日 23 | - 端午节:6月19日 24 | - 中秋节:9月25日 25 | - 国庆节:10月1-2日、5-7日 26 | - 数据来源: 27 | 28 | ## cn-stock-holidays 2.1.3 (2025-01-27) 29 | 30 | ### 改进 31 | 32 | - **代码清理和维护**: 移除过时和无用的代码以提高项目可维护性 33 | - 移除 `wind_holidays.py` - 过时的 Wind API 集成脚本 34 | - 移除 `tools/in/` 目录中的临时数据处理工具 35 | - 移除已废弃的 `utils/` 目录及其无用的数据获取脚本 36 | - 通过将 `debug.py` 合并到 `dev_shell.py` 来整合开发脚本 37 | - 简化 `ipython_config.py` 配置 38 | - 更新文档和脚本引用 39 | - 在保持 100% 测试覆盖率的同时减少约 500+ 行无用代码 40 | 41 | ## cn-stock-holidays 2.1.2 (2025-01-27) 42 | 43 | ### 错误修复 44 | 45 | - 修复了 2019 年劳动节假期安排的历史数据 - 确认 2019 年 5 月 2 日和 3 日休市,符合上交所公告 ([Issue #13](https://github.com/rainx/cn_stock_holidays/issues/13), [上交所参考](https://www.sse.com.cn/disclosure/announcement/general/c/c_20190418_4771364.shtml)) 46 | 47 | ## cn-stock-holidays 2.1.1 (2025-01-27) 48 | 49 | ### 错误修复 50 | 51 | - 修复了 2024-02-09(除夕)的历史数据 - 更新为反映该日期市场休市 ([Issue #16](https://github.com/rainx/cn_stock_holidays/issues/16)) 52 | 53 | ## cn-stock-holidays 2.1.0 (2024-12-19) 54 | 55 | ### 新功能 56 | 57 | - **香港市场半日交易支持**: 为香港证券交易所添加了全面的半日交易支持 58 | - 新增 `is_half_day_trading_day()` 函数用于检测半日交易日 59 | - 增强数据格式支持,使用 `,h` 后缀标记半日交易日期(例如:`20251225,h`) 60 | - 保持对现有上海/深圳市场数据的向后兼容性 61 | - 半日交易日仍被 `is_trading_day()`、`next_trading_day()`、`previous_trading_day()` 和 `trading_days_between()` 视为交易日 62 | - 更新香港市场数据,包含截至 2025 年的半日交易日期 63 | - 添加了半日交易功能的综合测试套件 64 | 65 | ### 改进 66 | 67 | - 将香港市场节假日数据扩展至 2025 年 68 | - 添加对常见半日交易模式的支持: 69 | - 平安夜(12 月 24 日) 70 | - 除夕(12 月 31 日) 71 | - 农历除夕 72 | - 主要节假日前一天(清明节、国庆节) 73 | - 通过 `_get_from_file_with_half_day()` 函数增强数据解析 74 | - 添加新的元函数以支持半日交易,同时保持现有 API 兼容性 75 | 76 | ## cn-stock-holidays 2.0.0 (2024-12-19) 77 | 78 | ### 新功能 79 | 80 | - **重大更新**: 项目现代化改造 81 | - 引入 uv 作为现代化的 Python 包管理工具 82 | - 迁移到 pyproject.toml 配置,移除 setup.py 83 | - 添加完整的开发工具链:black, isort, mypy, flake8, pre-commit 84 | - 更新 CI/CD 工作流,使用 uv 进行测试、构建和发布 85 | - 将 CI 工作流文件从 test.yml 重命名为 ci.yml,提高清晰度 86 | - 迁移到 PyPI Trusted Publisher 实现安全的自动化发布 87 | - 修复已弃用的 GitHub Actions upload-artifact,从 v3 升级到 v4 88 | - 修复 uv publish 命令,移除不支持的 --yes 标志并添加 trusted-publishing 89 | - 修复发布作业,添加 download-artifact 步骤以访问构建的包 90 | - 添加代码质量检查和自动化格式化 91 | - 支持现代 Python 打包标准 (PEP 517/518) 92 | - 改进项目结构和文档 93 | 94 | ## cn-stock-holidays 1.12 (2024-12-03) 95 | 96 | ### 改进 97 | 98 | - 更新了 2024 年国内的股市节假日 99 | 100 | ## cn-stock-holidays 1.11 (2023-12-25) 101 | 102 | ### 改进 103 | 104 | - 更新了 2024 年国内的股市节假日 105 | 106 | ## cn-stock-holidays 1.10 (2022-12-16) 107 | 108 | ### 改进 109 | 110 | - 更新了 2023 年国内的股市节假日 111 | 112 | ## cn-stock-holidays 1.9 (2021-12-30) 113 | 114 | ### 改进 115 | 116 | - 更新了 2022 年国内股市节假日 117 | 118 | ## cn-stock-holidays 1.8 (2020-12-25) 119 | 120 | ### 改进 121 | 122 | - 更新了 2021 年国内股市节假日 123 | 124 | ## cn-stock-holidays 1.7 (2020-01-31) 125 | 126 | ### 错误修复 127 | 128 | - 受 2019-nCoV 影响,变更 2020 年的股市日历, 追加 2020 年 1 月 31 日 129 | 130 | ## cn-stock-holidays 1.6 (2019-12-06) 131 | 132 | ### 错误修复 133 | 134 | - 修复一处错误 20090101 -> 20190101, thanks @liuyug #8 135 | 136 | ## cn-stock-holidays 1.5 (2019-11-26) 137 | 138 | ### 改进 139 | 140 | - 更新 2020 年中国市场休假数据 ref: 141 | 142 | ``` 143 | $("table.table tr td:first-child").map((i, e)=>e.innerText).toArray().filter(e => /[\d.]/.test(e)).map(e=>e.replace(/\./g, "")).join("\n") 144 | ``` 145 | 146 | - 更新 2019,2020 年香港股市休市数据 ref: 147 | 148 | ``` 149 | $("table.table tr td:first-child").map((i, e)=>e.innerText).toArray().filter(e => /[\d.]/.test(e)).map(e=>e.replace(/\./g, "")).filter(e=>e.includes("/")).map(e=> moment(e, "D/M/YYYY").format("YYYYMMDD")).join("\n") 150 | ``` 151 | 152 | ## cn-stock-holidays 1.4 (2019-01-08) 153 | 154 | ### 改进 155 | 156 | - 更新 2019 年中国市场假期数据 157 | 158 | ## cn-stock-holidays 1.3 (2018-04-17) 159 | 160 | ### 改进 161 | 162 | - update hk 2018 holiday data 163 | 164 | ## cn-stock-holidays 1.2 (2017-12-20) 165 | 166 | ### 新功能 167 | 168 | - 增加 get-day-list 命令,用于获取一段周期内的工作日或者休息日列表 169 | 170 | ## cn-stock-holidays 1.1 (2017-11-27) 171 | 172 | ### 新功能 173 | 174 | - merge pr #2 from @JaysonAlbert 175 | - 增加 minutes per session 176 | - 从 wind 获取假日信息代码 177 | 178 | ## cn-stock-holidays 1.0 (2017-11-06) 179 | 180 | ### 新功能 181 | 182 | - 增加对香港交易所的支持 183 | 184 | ## cn-stock-holidays 0.x 185 | 186 | ### 历史版本 187 | 188 | - 史前版本,历史记录还未整理 189 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # cn_stock_holidays 2 | 3 | ![CI Status](https://github.com/rainx/cn_stock_holidays/actions/workflows/ci.yml/badge.svg) 4 | 5 | 一个全面的 Python 包,为中国大陆(沪深)和香港股票交易所提供节假日数据。该包为需要确定交易日的金融应用程序提供可靠的数据源和实用工具库。 6 | 7 | ## 功能特性 8 | 9 | - **双市场支持**:涵盖中国大陆和香港市场 10 | - **多数据源**:本地文件、缓存数据和远程获取 11 | - **Zipline 集成**:为算法交易提供交易所日历 12 | - **命令行工具**:数据提取的命令行实用工具 13 | - **缓存机制**:LRU 缓存优化性能 14 | - **全面的 API**:交易日计算函数 15 | 16 | ## 数据文件 17 | 18 | ### 沪深市场 19 | 20 | ``` 21 | cn_stock_holidays/data.txt 22 | ``` 23 | 24 | ### 香港市场 25 | 26 | ``` 27 | cn_stock_holidays/data_hk.txt 28 | ``` 29 | 30 | ### 通过 URL 获取数据 31 | 32 | ```bash 33 | # 沪深数据 34 | wget https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/data.txt 35 | 36 | # 或使用 curl 37 | curl https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/data.txt 38 | ``` 39 | 40 | ## 数据格式 41 | 42 | 数据文件存储中国股票交易所的所有节假日(不包括周六周日的常规休市),每行一个日期,格式为: 43 | 44 | ``` 45 | YYYYMMDD 46 | ``` 47 | 48 | ## 安装 49 | 50 | ### 使用 uv(推荐) 51 | 52 | 本项目支持 [uv](https://github.com/astral-sh/uv),一个快速的 Python 包安装器: 53 | 54 | ```bash 55 | # 首先安装 uv 56 | curl -LsSf https://astral.sh/uv/install.sh | sh 57 | 58 | # 安装包 59 | uv pip install cn-stock-holidays 60 | ``` 61 | 62 | ### 使用 pip 63 | 64 | ```bash 65 | pip install cn-stock-holidays 66 | ``` 67 | 68 | ### 从源码安装 69 | 70 | ```bash 71 | git clone https://github.com/rainx/cn_stock_holidays.git 72 | cd cn_stock_holidays 73 | uv sync --dev # 使用 uv 安装 74 | # 或 75 | pip install -e . # 使用 pip 安装 76 | ``` 77 | 78 | ## 使用方法 79 | 80 | ### 导入 81 | 82 | ```python 83 | # 沪深市场 84 | import cn_stock_holidays.data as shsz 85 | 86 | # 香港市场 87 | import cn_stock_holidays.data_hk as hkex 88 | ``` 89 | 90 | ### 核心函数 91 | 92 | ```python 93 | # 获取节假日数据 94 | holidays = shsz.get_cached() # 从缓存或本地文件获取 95 | holidays = shsz.get_local() # 从包数据文件读取 96 | holidays = shsz.get_remote_and_cache() # 从网络获取并缓存 97 | 98 | # 交易日操作 99 | is_trading = shsz.is_trading_day(date) # 检查是否为交易日 100 | prev_day = shsz.previous_trading_day(date) # 获取前一个交易日 101 | next_day = shsz.next_trading_day(date) # 获取下一个交易日 102 | 103 | # 获取日期范围内的交易日 104 | for trading_day in shsz.trading_days_between(start_date, end_date): 105 | print(trading_day) 106 | 107 | # 数据同步 108 | shsz.sync_data() # 如果过期则同步数据 109 | shsz.check_expired() # 检查数据是否需要更新 110 | ``` 111 | 112 | ### 函数详情 113 | 114 | ```python 115 | Help on module cn_stock_holidays.data: 116 | 117 | FUNCTIONS 118 | check_expired() 119 | 检查本地或缓存数据是否需要更新 120 | :return: True/False 121 | 122 | get_cached() 123 | 从缓存版本获取,如果不存在,使用包数据中的 txt 文件 124 | :return: 包含所有节假日数据的集合/列表,元素为 datetime.date 格式 125 | 126 | get_local() 127 | 从包数据文件读取数据 128 | :return: 包含所有节假日数据的列表,元素为 datetime.date 格式 129 | 130 | get_remote_and_cache() 131 | 从网络获取最新数据文件并在本地机器上缓存 132 | :return: 包含所有节假日数据的列表,元素为 datetime.date 格式 133 | 134 | is_trading_day(dt) 135 | :param dt: datetime.datetime 或 datetime.date 136 | :return: 如果是交易日返回 True,否则返回 False 137 | 138 | next_trading_day(dt) 139 | :param dt: datetime.datetime 或 datetime.date 140 | :return: 下一个交易日,格式为 datetime.date 141 | 142 | previous_trading_day(dt) 143 | :param dt: datetime.datetime 或 datetime.date 144 | :return: 前一个交易日,格式为 datetime.date 145 | 146 | sync_data() 147 | 如果过期则同步数据 148 | 149 | trading_days_between(start, end) 150 | :param start, end: 开始和结束时间,datetime.datetime 或 datetime.date 151 | :return: 中国市场可用交易日的生成器 152 | ``` 153 | 154 | ### 缓存管理 155 | 156 | 从版本 0.10 开始,我们在 `get_cached` 上使用 `functools.lru_cache` 以获得更好的性能。如果需要,可以使用以下方式清除缓存: 157 | 158 | ```python 159 | get_cached.cache_clear() 160 | ``` 161 | 162 | ## 命令行工具 163 | 164 | ### 数据同步 165 | 166 | ```bash 167 | # 同步沪深数据 168 | cn-stock-holiday-sync 169 | 170 | # 同步香港数据 171 | cn-stock-holiday-sync-hk 172 | ``` 173 | 174 | ### 获取交易日列表 175 | 176 | ```bash 177 | # 获取日期范围内的交易日 178 | get-day-list --start 2024-01-01 --end 2024-01-31 --daytype workday 179 | 180 | # 获取日期范围内的节假日 181 | get-day-list --start 2024-01-01 --end 2024-01-31 --daytype holiday 182 | 183 | # 香港市场 184 | get-day-list --market hk --start 2024-01-01 --end 2024-01-31 --daytype workday 185 | ``` 186 | 187 | ## 保持数据更新 188 | 189 | 该包包含检查数据过期并从网络获取更新的脚本。您可以使用 cron 设置自动更新: 190 | 191 | ```crontab 192 | # 每天午夜同步 193 | 0 0 * * * /usr/local/bin/cn-stock-holiday-sync > /tmp/cn_stock_holiday_sync.log 194 | ``` 195 | 196 | 查找同步命令的绝对路径: 197 | 198 | ```bash 199 | # 沪深 200 | which cn-stock-holiday-sync 201 | 202 | # 香港 203 | which cn-stock-holiday-sync-hk 204 | ``` 205 | 206 | ## Zipline 集成 207 | 208 | 用于 Zipline 算法交易: 209 | 210 | ```python 211 | from cn_stock_holidays.zipline import SHSZExchangeCalendar, HKExchangeCalendar 212 | 213 | # 在 Zipline 中使用 214 | calendar = SHSZExchangeCalendar() # 沪深 215 | calendar = HKExchangeCalendar() # 香港 216 | ``` 217 | 218 | ## 开发 219 | 220 | ### 设置开发环境 221 | 222 | ```bash 223 | # 克隆并设置 224 | git clone https://github.com/rainx/cn_stock_holidays.git 225 | cd cn_stock_holidays 226 | 227 | # 使用 uv 安装(推荐) 228 | uv sync --dev 229 | 230 | # 或使用 pip 231 | pip install -e .[dev] 232 | ``` 233 | 234 | ### 运行测试 235 | 236 | ```bash 237 | # 运行所有测试 238 | uv run pytest 239 | 240 | # 运行覆盖率测试 241 | uv run pytest --cov=cn_stock_holidays 242 | 243 | # 格式化代码 244 | uv run black . 245 | 246 | # 类型检查 247 | uv run mypy cn_stock_holidays/ 248 | ``` 249 | 250 | ## 贡献 251 | 252 | 1. Fork 仓库 253 | 2. 创建功能分支 254 | 3. 进行更改 255 | 4. 运行测试并确保代码质量 256 | 5. 提交拉取请求 257 | 258 | ## 许可证 259 | 260 | 本项目采用 MIT 许可证 - 详情请参阅 [LICENSE](LICENSE) 文件。 261 | 262 | ## 链接 263 | 264 | - [GitHub 仓库](https://github.com/rainx/cn_stock_holidays) 265 | - [PyPI 包](https://pypi.org/project/cn-stock-holidays/) 266 | - [UV 包管理器](https://github.com/astral-sh/uv) 267 | -------------------------------------------------------------------------------- /tests/data_hk_half_day_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import unittest 4 | import datetime 5 | from cn_stock_holidays.data_hk import ( 6 | get_cached_with_half_day, 7 | is_half_day_trading_day, 8 | is_trading_day, 9 | sync_data_with_half_day, 10 | check_expired_with_half_day, 11 | ) 12 | from cn_stock_holidays.common import int_to_date 13 | 14 | 15 | class TestHkHalfDayTrading(unittest.TestCase): 16 | def test_get_cached_with_half_day(self): 17 | """ 18 | Test getting cached data with half-day trading support 19 | """ 20 | holidays, half_days = get_cached_with_half_day() 21 | 22 | # Both should be sets 23 | self.assertIsInstance(holidays, set) 24 | self.assertIsInstance(half_days, set) 25 | 26 | # Both should contain datetime.date objects 27 | if holidays: 28 | self.assertIsInstance(list(holidays)[0], datetime.date) 29 | if half_days: 30 | self.assertIsInstance(list(half_days)[0], datetime.date) 31 | 32 | # Should be greater than 0 33 | self.assertGreater(len(holidays), 0) 34 | 35 | # Clear cache and test again 36 | get_cached_with_half_day.cache_clear() 37 | holidays2, half_days2 = get_cached_with_half_day() 38 | self.assertEqual(holidays, holidays2) 39 | self.assertEqual(half_days, half_days2) 40 | 41 | def test_is_half_day_trading_day(self): 42 | """ 43 | Test half-day trading day detection 44 | """ 45 | # Test with datetime.date 46 | today = datetime.date.today() 47 | result = is_half_day_trading_day(today) 48 | self.assertIsInstance(result, bool) 49 | 50 | # Test with datetime.datetime 51 | now = datetime.datetime.now() 52 | result = is_half_day_trading_day(now) 53 | self.assertIsInstance(result, bool) 54 | 55 | # Test weekend (should be False) 56 | weekend = datetime.date(2024, 1, 6) # Saturday 57 | self.assertFalse(is_half_day_trading_day(weekend)) 58 | 59 | # Test Sunday 60 | sunday = datetime.date(2024, 1, 7) # Sunday 61 | self.assertFalse(is_half_day_trading_day(sunday)) 62 | 63 | def test_half_day_trading_consistency(self): 64 | """ 65 | Test that half-day trading days are also considered trading days 66 | """ 67 | is_trading_day.cache_clear() 68 | holidays, half_days = get_cached_with_half_day() 69 | 70 | # Only test half-day trading days that are weekdays 71 | weekday_half_days = [d for d in half_days if d.weekday() < 5] 72 | print("DEBUG weekday_half_days:", weekday_half_days) 73 | for half_day in weekday_half_days[:5]: # Test first 5 weekday half-days 74 | print("DEBUG is_trading_day:", half_day, is_trading_day(half_day)) 75 | self.assertTrue(is_trading_day(half_day)) 76 | self.assertTrue(is_half_day_trading_day(half_day)) 77 | 78 | def test_holiday_not_half_day(self): 79 | """ 80 | Test that regular holidays are not considered half-day trading days 81 | """ 82 | holidays, half_days = get_cached_with_half_day() 83 | 84 | # Test a few regular holidays if they exist 85 | for holiday in list(holidays)[:5]: # Test first 5 holidays 86 | # Regular holidays should not be trading days 87 | self.assertFalse(is_trading_day(holiday)) 88 | # Regular holidays should not be half-day trading days 89 | self.assertFalse(is_half_day_trading_day(holiday)) 90 | 91 | def test_weekday_not_half_day(self): 92 | """ 93 | Test that regular weekdays are not considered half-day trading days 94 | """ 95 | # Test a few regular weekdays 96 | test_dates = [ 97 | datetime.date(2024, 1, 2), # Tuesday 98 | datetime.date(2024, 1, 3), # Wednesday 99 | datetime.date(2024, 1, 4), # Thursday 100 | datetime.date(2024, 1, 5), # Friday 101 | ] 102 | 103 | for test_date in test_dates: 104 | # Regular weekdays should be trading days (unless they're holidays) 105 | if is_trading_day(test_date): 106 | # If it's a trading day, it should not be a half-day unless it's marked as such 107 | # This test assumes the test dates are not actual half-days in the data 108 | pass # We can't guarantee this without knowing the exact data 109 | 110 | def test_check_expired_with_half_day(self): 111 | """ 112 | Test checking if data is expired with half-day trading support 113 | """ 114 | result = check_expired_with_half_day() 115 | self.assertIsInstance(result, bool) 116 | 117 | def test_sync_data_with_half_day(self): 118 | """ 119 | Test syncing data with half-day trading support 120 | """ 121 | # This test just ensures the function can be called without error 122 | # The actual sync behavior depends on network and cache state 123 | try: 124 | sync_data_with_half_day() 125 | except Exception as e: 126 | # If there's a network error, that's acceptable for testing 127 | self.assertIsInstance(e, Exception) 128 | 129 | def test_data_format_compatibility(self): 130 | """ 131 | Test that the new format is backward compatible with existing data 132 | """ 133 | # Get data using both old and new methods 134 | old_data = get_cached_with_half_day() 135 | new_data = get_cached_with_half_day() 136 | 137 | # Both should return the same result 138 | self.assertEqual(old_data, new_data) 139 | 140 | 141 | if __name__ == "__main__": 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /cn_stock_holidays/zipline/exchange_calendar_shsz.py: -------------------------------------------------------------------------------- 1 | from datetime import time 2 | from cn_stock_holidays.data import get_cached 3 | from pandas import Timestamp, date_range, DatetimeIndex 4 | import pytz 5 | from zipline.utils.memoize import remember_last, lazyval 6 | import warnings 7 | 8 | from zipline.utils.calendars import TradingCalendar 9 | from zipline.utils.calendars.trading_calendar import days_at_time, NANOS_IN_MINUTE 10 | import numpy as np 11 | import pandas as pd 12 | 13 | # lunch break for shanghai and shenzhen exchange 14 | lunch_break_start = time(11, 30) 15 | lunch_break_end = time(13, 1) 16 | 17 | start_default = pd.Timestamp("1990-12-19", tz="UTC") 18 | end_base = pd.Timestamp("today", tz="UTC") 19 | end_default = end_base + pd.Timedelta(days=365) 20 | 21 | 22 | class SHSZExchangeCalendar(TradingCalendar): 23 | """ 24 | Exchange calendar for Shanghai and Shenzhen (China Market) 25 | Open Time 9:31 AM, Asia/Shanghai 26 | Close Time 3:00 PM, Asia/Shanghai 27 | 28 | One big difference between china and us exchange is china exchange has a lunch break , so I handle it 29 | 30 | Sample Code in ipython: 31 | 32 | > from zipline.utils.calendars import * 33 | > from cn_stock_holidays.zipline.exchange_calendar_shsz import SHSZExchangeCalendar 34 | > register_calendar("SHSZ", SHSZExchangeCalendar(), force=True) 35 | > c=get_calendar("SHSZ") 36 | 37 | for the guy need to keep updating about holiday file, try to add `cn-stock-holiday-sync` command to crontab 38 | """ 39 | 40 | def __init__(self, start=start_default, end=end_default): 41 | with warnings.catch_warnings(): 42 | warnings.simplefilter("ignore") 43 | _all_days = date_range(start, end, freq=self.day, tz="UTC") 44 | 45 | self._lunch_break_starts = days_at_time( 46 | _all_days, lunch_break_start, self.tz, 0 47 | ) 48 | self._lunch_break_ends = days_at_time(_all_days, lunch_break_end, self.tz, 0) 49 | 50 | TradingCalendar.__init__(self, start=start_default, end=end_default) 51 | 52 | self.schedule = pd.DataFrame( 53 | index=_all_days, 54 | columns=[ 55 | "market_open", 56 | "market_close", 57 | "lunch_break_start", 58 | "lunch_break_end", 59 | ], 60 | data={ 61 | "market_open": self._opens, 62 | "market_close": self._closes, 63 | "lunch_break_start": self._lunch_break_starts, 64 | "lunch_break_end": self._lunch_break_ends, 65 | }, 66 | dtype="datetime64[ns]", 67 | ) 68 | 69 | @property 70 | def name(self): 71 | return "SHSZ" 72 | 73 | @property 74 | def tz(self): 75 | return pytz.timezone("Asia/Shanghai") 76 | 77 | @property 78 | def open_time(self): 79 | return time(9, 31) 80 | 81 | @property 82 | def close_time(self): 83 | return time(15, 0) 84 | 85 | @property 86 | def adhoc_holidays(self): 87 | return [Timestamp(t, tz=pytz.UTC) for t in get_cached(use_list=True)] 88 | 89 | @lazyval 90 | def _minutes_per_session(self): 91 | diff = ( 92 | self.schedule.lunch_break_start.values.astype("datetime64[m]") 93 | - self.schedule.market_open.values.astype("datetime64[m]") 94 | ) + ( 95 | self.schedule.market_close.values.astype("datetime64[m]") 96 | - self.schedule.lunch_break_end.values.astype("datetime64[m]") 97 | ) 98 | diff = diff.astype(np.int64) 99 | return diff + 2 100 | 101 | @property 102 | @remember_last 103 | def all_minutes(self): 104 | """ 105 | Returns a DatetimeIndex representing all the minutes in this calendar. 106 | """ 107 | opens_in_ns = self._opens.values.astype("datetime64[ns]") 108 | 109 | closes_in_ns = self._closes.values.astype("datetime64[ns]") 110 | 111 | lunch_break_start_in_ns = self._lunch_break_starts.values.astype( 112 | "datetime64[ns]" 113 | ) 114 | lunch_break_ends_in_ns = self._lunch_break_ends.values.astype("datetime64[ns]") 115 | 116 | deltas_before_lunch = lunch_break_start_in_ns - opens_in_ns 117 | deltas_after_lunch = closes_in_ns - lunch_break_ends_in_ns 118 | 119 | daily_before_lunch_sizes = (deltas_before_lunch / NANOS_IN_MINUTE) + 1 120 | daily_after_lunch_sizes = (deltas_after_lunch / NANOS_IN_MINUTE) + 1 121 | 122 | daily_sizes = daily_before_lunch_sizes + daily_after_lunch_sizes 123 | 124 | num_minutes = np.sum(daily_sizes).astype(np.int64) 125 | 126 | # One allocation for the entire thing. This assumes that each day 127 | # represents a contiguous block of minutes. 128 | all_minutes = np.empty(num_minutes, dtype="datetime64[ns]") 129 | 130 | idx = 0 131 | for day_idx, size in enumerate(daily_sizes): 132 | # lots of small allocations, but it's fast enough for now. 133 | 134 | # size is a np.timedelta64, so we need to int it 135 | size_int = int(size) 136 | 137 | before_lunch_size_int = int(daily_before_lunch_sizes[day_idx]) 138 | after_lunch_size_int = int(daily_after_lunch_sizes[day_idx]) 139 | 140 | all_minutes[idx : (idx + before_lunch_size_int)] = np.arange( 141 | opens_in_ns[day_idx], 142 | lunch_break_start_in_ns[day_idx] + NANOS_IN_MINUTE, 143 | NANOS_IN_MINUTE, 144 | ) 145 | 146 | all_minutes[(idx + before_lunch_size_int) : (idx + size_int)] = np.arange( 147 | lunch_break_ends_in_ns[day_idx], 148 | closes_in_ns[day_idx] + NANOS_IN_MINUTE, 149 | NANOS_IN_MINUTE, 150 | ) 151 | 152 | idx += size_int 153 | return DatetimeIndex(all_minutes).tz_localize("UTC") 154 | -------------------------------------------------------------------------------- /scripts/quick_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Quick test script for cn_stock_holidays development. 4 | 5 | This script provides a simple way to test the package functionality 6 | without starting a full IPython session. 7 | 8 | Usage: 9 | python scripts/quick_test.py 10 | # or 11 | uv run python scripts/quick_test.py 12 | """ 13 | 14 | import sys 15 | from pathlib import Path 16 | from datetime import datetime, date, timedelta 17 | 18 | # Add the project root to Python path 19 | project_root = Path(__file__).parent.parent 20 | sys.path.insert(0, str(project_root)) 21 | 22 | 23 | def test_mainland_china(): 24 | """Test mainland China market functions.""" 25 | print("=== Testing Mainland China Market ===") 26 | 27 | from cn_stock_holidays.data import ( 28 | is_trading_day, 29 | trading_days_between, 30 | ) 31 | 32 | # Test dates 33 | test_dates = [ 34 | date(2024, 1, 1), # New Year's Day (holiday) 35 | date(2024, 1, 2), # Regular trading day 36 | date(2024, 1, 6), # Saturday (weekend) 37 | date(2024, 1, 7), # Sunday (weekend) 38 | date(2024, 2, 10), # Chinese New Year (holiday) 39 | date(2024, 5, 1), # Labor Day (holiday) 40 | ] 41 | 42 | for test_date in test_dates: 43 | is_trading = is_trading_day(test_date) 44 | print(f"{test_date}: Trading Day={is_trading}") 45 | 46 | # Test trading days between 47 | start_date = date(2024, 1, 1) 48 | end_date = date(2024, 1, 31) 49 | trading_days = list(trading_days_between(start_date, end_date)) 50 | print( 51 | f"\nTrading days between {start_date} and {end_date}: {len(trading_days)} days" 52 | ) 53 | print(f"First 5 trading days: {trading_days[:5]}") 54 | 55 | 56 | def test_hong_kong(): 57 | """Test Hong Kong market functions.""" 58 | print("\n=== Testing Hong Kong Market ===") 59 | 60 | from cn_stock_holidays.data_hk import ( 61 | is_trading_day, 62 | is_half_day_trading_day, 63 | trading_days_between, 64 | ) 65 | 66 | # Test dates 67 | test_dates = [ 68 | date(2024, 1, 1), # New Year's Day (holiday) 69 | date(2024, 1, 2), # Regular trading day 70 | date(2024, 1, 6), # Saturday (weekend) 71 | date(2024, 1, 7), # Sunday (weekend) 72 | date(2024, 2, 9), # Chinese New Year Eve (half-day) 73 | date(2024, 2, 10), # Chinese New Year (holiday) 74 | date(2024, 12, 24), # Christmas Eve (half-day) 75 | date(2024, 12, 25), # Christmas (holiday) 76 | date(2024, 12, 31), # New Year's Eve (half-day) 77 | ] 78 | 79 | for test_date in test_dates: 80 | is_trading = is_trading_day(test_date) 81 | is_half = is_half_day_trading_day(test_date) 82 | print(f"{test_date}: Trading Day={is_trading}, Half-day={is_half}") 83 | 84 | # Test half-day trading days 85 | start_date = date(2024, 12, 1) 86 | end_date = date(2024, 12, 31) 87 | # Get half-day trading days manually 88 | half_days = [] 89 | current = start_date 90 | while current <= end_date: 91 | if is_half_day_trading_day(current): 92 | half_days.append(current) 93 | current += timedelta(days=1) 94 | print(f"\nHalf-day trading days in December 2024: {half_days}") 95 | 96 | 97 | def test_cache(): 98 | """Test cache management functions.""" 99 | print("\n=== Testing Cache Management ===") 100 | 101 | from cn_stock_holidays.data import get_cached, check_expired 102 | from cn_stock_holidays.data_hk import ( 103 | get_cached_with_half_day, 104 | check_expired_with_half_day, 105 | ) 106 | 107 | # Test main cache 108 | try: 109 | main_data = get_cached() 110 | main_expired = check_expired() 111 | print(f"Main cache: {len(main_data)} holidays, expired: {main_expired}") 112 | except Exception as e: 113 | print(f"Main cache error: {e}") 114 | 115 | # Test half-day cache 116 | try: 117 | hk_holidays, hk_half_days = get_cached_with_half_day() 118 | hk_expired = check_expired_with_half_day() 119 | print( 120 | f"HK cache: {len(hk_holidays)} holidays, {len(hk_half_days)} half-days, expired: {hk_expired}" 121 | ) 122 | except Exception as e: 123 | print(f"HK cache error: {e}") 124 | 125 | 126 | def test_common_utils(): 127 | """Test common utility functions.""" 128 | print("\n=== Testing Common Utilities ===") 129 | 130 | from cn_stock_holidays.common import int_to_date, date_to_str, date_to_int 131 | 132 | # Test date conversion functions 133 | test_date_int = 20240101 134 | test_date = date(2024, 1, 1) 135 | 136 | # Test int_to_date 137 | converted_date = int_to_date(test_date_int) 138 | print(f"Int {test_date_int} -> Date: {converted_date}") 139 | 140 | # Test date_to_str 141 | date_str = date_to_str(test_date) 142 | print(f"Date {test_date} -> String: {date_str}") 143 | 144 | # Test date_to_int 145 | date_int = date_to_int(test_date) 146 | print(f"Date {test_date} -> Int: {date_int}") 147 | 148 | # Test weekend check 149 | weekend_date = date(2024, 1, 6) # Saturday 150 | weekday_date = date(2024, 1, 8) # Monday 151 | print(f"{weekend_date} is weekend: {weekend_date.weekday() >= 5}") 152 | print(f"{weekday_date} is weekend: {weekday_date.weekday() >= 5}") 153 | 154 | 155 | def main(): 156 | """Run all tests.""" 157 | print("cn_stock_holidays Quick Test") 158 | print("=" * 50) 159 | 160 | try: 161 | test_mainland_china() 162 | test_hong_kong() 163 | test_cache() 164 | test_common_utils() 165 | 166 | print("\n" + "=" * 50) 167 | print("All tests completed successfully!") 168 | 169 | except Exception as e: 170 | print(f"\nError during testing: {e}") 171 | import traceback 172 | 173 | traceback.print_exc() 174 | 175 | 176 | if __name__ == "__main__": 177 | main() 178 | -------------------------------------------------------------------------------- /DATA_UPDATE_RULES.md: -------------------------------------------------------------------------------- 1 | # Data Update Rules for cn_stock_holidays 2 | 3 | This document defines the rules for updating holiday data in this project. It is designed to be read by both humans and LLMs for automated updates. 4 | 5 | ## Core Principles 6 | 7 | ### 1. Data File Format 8 | 9 | - **File**: `cn_stock_holidays/data.txt` 10 | - **Format**: One date per line in `YYYYMMDD` format 11 | - **Encoding**: UTF-8 12 | - **Line Ending**: Unix style (LF) 13 | - **No Trailing Content**: Each line contains only the date, no comments or metadata 14 | 15 | ### 2. What to Include 16 | 17 | **ONLY include dates that meet ALL of these criteria:** 18 | 19 | 1. ✅ **Non-trading days** (stock market is closed) 20 | 2. ✅ **Weekdays only** (Monday-Friday) 21 | 3. ✅ **Statutory holidays and their makeup days** (法定节假日及调休日) 22 | 23 | **DO NOT include:** 24 | 25 | 1. ❌ **Weekends** (Saturday and Sunday) - These are automatically handled by the code 26 | 2. ❌ **Regular weekdays** - Only include special non-trading weekdays 27 | 3. ❌ **Duplicate dates** 28 | 29 | ### 3. Why Weekends Are Excluded 30 | 31 | The stock market is always closed on weekends. The code automatically identifies weekends using `datetime.date.weekday()`: 32 | - If `weekday() >= 5` (Saturday=5, Sunday=6), it's automatically a non-trading day 33 | - No need to store weekends in data.txt 34 | - This keeps the data file small and focused on exceptional non-trading days 35 | 36 | ## Data Sources 37 | 38 | ### Primary Source 39 | - **TDX (通达信)**: https://www.tdx.com.cn/url/holiday/ 40 | - Look for "中国" (China) tab for Shanghai/Shenzhen market data 41 | - This is the official and most reliable source 42 | 43 | ### Backup Sources 44 | - Shanghai Stock Exchange: https://www.sse.com.cn/ 45 | - Shenzhen Stock Exchange: https://www.szse.cn/ 46 | 47 | ## Annual Update Process 48 | 49 | ### When to Update 50 | - **Timing**: December of each year (when next year's official calendar is announced) 51 | - **Trigger**: Check TDX website for next year's holiday arrangements 52 | 53 | ### Step-by-Step Process 54 | 55 | 1. **Fetch Data** 56 | ``` 57 | Visit: https://www.tdx.com.cn/url/holiday/ 58 | Select: 中国 (China) tab 59 | Extract: All holiday dates for the target year 60 | ``` 61 | 62 | 2. **Filter Weekend Dates** 63 | ```python 64 | # Pseudo-code for validation 65 | for each date in new_dates: 66 | if date.weekday() >= 5: # Saturday or Sunday 67 | REJECT - "Weekend dates must not be included" 68 | else: 69 | ACCEPT 70 | ``` 71 | 72 | 3. **Verify Data Structure** 73 | - Each holiday period should include all non-trading weekdays 74 | - Example: If Spring Festival is Feb 10-16 (Mon-Sun): 75 | - Include: Feb 10, 11, 12, 13, 14 (Mon-Fri) 76 | - Exclude: Feb 15, 16 (Sat-Sun) - automatically handled 77 | - Include makeup working days if they fall on normally-closed days 78 | 79 | 4. **Validate Format** 80 | - All dates must be 8 digits: YYYYMMDD 81 | - All dates must be in chronological order 82 | - No blank lines or comments 83 | - File must end with the last date (no trailing newline after last date) 84 | 85 | 5. **Run Tests** 86 | ```bash 87 | uv run pytest tests/ 88 | uv run pytest tests/test_latest_year_weekends.py # Automated weekend check 89 | ``` 90 | 91 | 6. **Update Metadata** 92 | - Update version in `pyproject.toml` 93 | - Update both `Changelog.md` and `Changelog-zh_CN.md` 94 | - Document data source URL 95 | 96 | ## Chinese Stock Market Holidays 97 | 98 | Typical annual holidays (dates vary each year): 99 | 100 | 1. **元旦 (New Year's Day)**: Usually Jan 1 + makeup days 101 | 2. **春节 (Spring Festival / Chinese New Year)**: 7 days, including makeup days 102 | 3. **清明节 (Qingming Festival / Tomb Sweeping Day)**: Usually 1-3 days 103 | 4. **劳动节 (Labor Day)**: Usually May 1 + makeup days 104 | 5. **端午节 (Dragon Boat Festival)**: Usually 1-3 days 105 | 6. **中秋节 (Mid-Autumn Festival)**: Usually 1-3 days 106 | 7. **国庆节 (National Day)**: Usually 7 days (Oct 1-7), including makeup days 107 | 108 | ### Makeup Days (调休) 109 | - When holidays fall near weekends, the government may: 110 | - Extend the holiday by converting adjacent weekdays to holidays 111 | - Require working on adjacent weekends to compensate 112 | - **Only include the weekdays that become holidays** in data.txt 113 | - Do NOT include the makeup working days (those are just regular weekends becoming workdays) 114 | 115 | ## Validation Rules 116 | 117 | ### Automated Checks (in tests) 118 | 119 | 1. **No Weekends**: All dates in data.txt must be weekdays (Mon-Fri) 120 | 2. **Chronological Order**: Dates must be sorted in ascending order 121 | 3. **Valid Dates**: All dates must be valid calendar dates 122 | 4. **Format Check**: All dates must match `\d{8}` pattern 123 | 5. **Trading Day Logic**: All dates in data.txt must return `False` for `is_trading_day()` 124 | 125 | ### Manual Checks 126 | 127 | 1. **Cross-reference**: Compare with official announcements 128 | 2. **Complete Coverage**: Ensure all announced holidays are included 129 | 3. **No Duplicates**: Each date appears only once 130 | 4. **Year Completeness**: Verify all major holidays for the year are present 131 | 132 | ## Example: Adding 2027 Data (Template) 133 | 134 | ```python 135 | # Step 1: Fetch from TDX 136 | # Visit https://www.tdx.com.cn/url/holiday/ 137 | # Extract dates for 2027 138 | 139 | # Step 2: Filter (pseudo-code) 140 | dates_2027 = [ 141 | "20270101", # New Year - Friday ✓ 142 | "20270211", # Spring Festival - Thursday ✓ 143 | # ... more dates 144 | ] 145 | 146 | # Step 3: Validate each date is a weekday 147 | for date_str in dates_2027: 148 | date_obj = datetime.strptime(date_str, "%Y%m%d") 149 | assert date_obj.weekday() < 5, f"{date_str} is a weekend!" 150 | 151 | # Step 4: Append to data.txt (in chronological order) 152 | # Step 5: Run tests 153 | # Step 6: Update version and changelog 154 | ``` 155 | 156 | ## Common Mistakes to Avoid 157 | 158 | 1. ❌ **Including Saturdays/Sundays**: Weekend dates should NEVER be in data.txt 159 | 2. ❌ **Missing makeup holidays**: If a weekday is declared a holiday, include it 160 | 3. ❌ **Wrong format**: Must be YYYYMMDD, not YYYY-MM-DD or other formats 161 | 4. ❌ **Unsorted data**: Dates must be in chronological order 162 | 5. ❌ **Trailing newlines**: File should end immediately after the last date 163 | 164 | ## Testing Your Changes 165 | 166 | After updating data.txt, always run: 167 | 168 | ```bash 169 | # Run all tests 170 | uv run pytest 171 | 172 | # Specifically test the latest year 173 | uv run pytest tests/test_latest_year_weekends.py -v 174 | 175 | # Verify specific dates 176 | uv run python -c " 177 | from cn_stock_holidays import data 178 | import datetime 179 | # Test a specific holiday 180 | print(data.is_trading_day(datetime.date(2026, 1, 1))) # Should be False 181 | print(data.is_trading_day(datetime.date(2026, 1, 6))) # Should be True (if not holiday) 182 | " 183 | ``` 184 | 185 | ## Version Update Guidelines 186 | 187 | When adding a new year's data: 188 | - Increment patch version (e.g., 2.1.3 → 2.1.4) 189 | - Update changelog with: 190 | - All holiday dates added 191 | - Data source reference 192 | - Date of update 193 | 194 | --- 195 | 196 | **Last Updated**: 2025-12-07 197 | **For Questions**: https://github.com/rainx/cn_stock_holidays/issues 198 | -------------------------------------------------------------------------------- /cn_stock_holidays/data_hk.txt: -------------------------------------------------------------------------------- 1 | 20001225 2 | 20001226 3 | 20010101 4 | 20010124 5 | 20010125 6 | 20010126 7 | 20010405 8 | 20010413 9 | 20010416 10 | 20010430 11 | 20010501 12 | 20010625 13 | 20010702 14 | 20010706 15 | 20010725 16 | 20011001 17 | 20011002 18 | 20011225 19 | 20011226 20 | 20020101 21 | 20020212 22 | 20020213 23 | 20020214 24 | 20020329 25 | 20020401 26 | 20020405 27 | 20020501 28 | 20020520 29 | 20020701 30 | 20021001 31 | 20021014 32 | 20021225 33 | 20021226 34 | 20030101 35 | 20030203 36 | 20030418 37 | 20030421 38 | 20030501 39 | 20030508 40 | 20030604 41 | 20030701 42 | 20030912 43 | 20031001 44 | 20031225 45 | 20031226 46 | 20040101 47 | 20040122 48 | 20040123 49 | 20040405 50 | 20040409 51 | 20040412 52 | 20040526 53 | 20040622 54 | 20040701 55 | 20040929 56 | 20041001 57 | 20041022 58 | 20041227 59 | 20050209 60 | 20050210 61 | 20050211 62 | 20050325 63 | 20050328 64 | 20050405 65 | 20050502 66 | 20050516 67 | 20050701 68 | 20050919 69 | 20051011 70 | 20051226 71 | 20051227 72 | 20060102 73 | 20060130 74 | 20060131 75 | 20060414 76 | 20060417 77 | 20060501 78 | 20060505 79 | 20060531 80 | 20061225 81 | 20061226 82 | 20070101 83 | 20070220 84 | 20070405 85 | 20070406 86 | 20070409 87 | 20070702 88 | 20071225 89 | 20071226 90 | 20080101 91 | 20080207 92 | 20080208 93 | 20080321 94 | 20080609 95 | 20080701 96 | 20080806 97 | 20080915 98 | 20081001 99 | 20081225 100 | 20090101 101 | 20090126 102 | 20090127 103 | 20090128 104 | 20090410 105 | 20090528 106 | 20090701 107 | 20091225 108 | 20100101 109 | 20100215 110 | 20100216 111 | 20100405 112 | 20100521 113 | 20100616 114 | 20100923 115 | 20101001 116 | 20101227 117 | 20110203 118 | 20110204 119 | 20110405 120 | 20110422 121 | 20110425 122 | 20110502 123 | 20110510 124 | 20110606 125 | 20110701 126 | 20110913 127 | 20110929 128 | 20111005 129 | 20111226 130 | 20111227 131 | 20120102 132 | 20120123 133 | 20120124 134 | 20120125 135 | 20120404 136 | 20120406 137 | 20120409 138 | 20120501 139 | 20120702 140 | 20121001 141 | 20121002 142 | 20121023 143 | 20121225 144 | 20121226 145 | 20130101 146 | 20130211 147 | 20130212 148 | 20130213 149 | 20130329 150 | 20130401 151 | 20130404 152 | 20130501 153 | 20130517 154 | 20130612 155 | 20130701 156 | 20130814 157 | 20130920 158 | 20131001 159 | 20131014 160 | 20131225 161 | 20131226 162 | 20140101 163 | 20140131 164 | 20140203 165 | 20140418 166 | 20140421 167 | 20140501 168 | 20140506 169 | 20140602 170 | 20140701 171 | 20140909 172 | 20141001 173 | 20141002 174 | 20141225 175 | 20141226 176 | 20150101 177 | 20150219 178 | 20150220 179 | 20150403 180 | 20150406 181 | 20150407 182 | 20150501 183 | 20150525 184 | 20150701 185 | 20150903 186 | 20150928 187 | 20151001 188 | 20151021 189 | 20151225 190 | 20160101 191 | 20160208 192 | 20160209 193 | 20160210 194 | 20160325 195 | 20160328 196 | 20160404 197 | 20160502 198 | 20160609 199 | 20160701 200 | 20160802 201 | 20160916 202 | 20161010 203 | 20161021 204 | 20161226 205 | 20161227 206 | 20170102 207 | 20170130 208 | 20170131 209 | 20170404 210 | 20170414 211 | 20170417 212 | 20170501 213 | 20170503 214 | 20170530 215 | 20171002 216 | 20171005 217 | 20171225 218 | 20171226 219 | 20180101 220 | 20180213 221 | 20180214 222 | 20180216 223 | 20180330 224 | 20180402 225 | 20180403 226 | 20180405 227 | 20180426 228 | 20180427 229 | 20180430 230 | 20180501 231 | 20180522 232 | 20180618 233 | 20180702 234 | 20180920 235 | 20180921 236 | 20180924 237 | 20180925 238 | 20180927 239 | 20181001 240 | 20181002 241 | 20181003 242 | 20181004 243 | 20181005 244 | 20181017 245 | 20181225 246 | 20181226 247 | 20190101 248 | 20190205 249 | 20190206 250 | 20190207 251 | 20190405 252 | 20190419 253 | 20190422 254 | 20190501 255 | 20190513 256 | 20190607 257 | 20190701 258 | 20191001 259 | 20191007 260 | 20191225 261 | 20191226 262 | 20200101 263 | 20200127 264 | 20200128 265 | 20200410 266 | 20200413 267 | 20200430 268 | 20200501 269 | 20200625 270 | 20200701 271 | 20201001 272 | 20201002 273 | 20201026 274 | 20201225 275 | 20201228 276 | 20210101 277 | 20210211 278 | 20210212 279 | 20210215 280 | 20210216 281 | 20210217 282 | 20210402 283 | 20210405 284 | 20210406 285 | 20210501 286 | 20210519 287 | 20210614 288 | 20210701 289 | 20210922 290 | 20211001 291 | 20211014 292 | 20211227 293 | 20220101 294 | 20220201 295 | 20220202 296 | 20220203 297 | 20220204 298 | 20220405 299 | 20220415 300 | 20220418 301 | 20220502 302 | 20220509 303 | 20220603 304 | 20220701 305 | 20220912 306 | 20221001 307 | 20221004 308 | 20221226 309 | 20221227 310 | 20230101 311 | 20230123 312 | 20230124 313 | 20230125 314 | 20230126 315 | 20230127 316 | 20230405 317 | 20230407 318 | 20230410 319 | 20230501 320 | 20230526 321 | 20230622 322 | 20230701 323 | 20230929 324 | 20231002 325 | 20231023 326 | 20231225 327 | 20231226 328 | 20240101 329 | 20240210 330 | 20240211 331 | 20240212 332 | 20240213 333 | 20240214 334 | 20240215 335 | 20240216 336 | 20240404 337 | 20240405 338 | 20240410 339 | 20240501 340 | 20240610 341 | 20240701 342 | 20240917 343 | 20241001 344 | 20241002 345 | 20241003 346 | 20241004 347 | 20241225 348 | 20241226 349 | 20250101 350 | 20250129 351 | 20250130 352 | 20250131 353 | 20250203 354 | 20250204 355 | 20250205 356 | 20250206 357 | 20250207 358 | 20250404 359 | 20250407 360 | 20250418 361 | 20250501 362 | 20250530 363 | 20250701 364 | 20250916 365 | 20251001 366 | 20251002 367 | 20251003 368 | 20251006 369 | 20251007 370 | 20251225 371 | 20251226 372 | 20260101 373 | 20260217 374 | 20260218 375 | 20260219 376 | 20260403 377 | 20260406 378 | 20260407 379 | 20260501 380 | 20260525 381 | 20260619 382 | 20260701 383 | 20261001 384 | 20261019 385 | 20261225 386 | # Half-day trading days (Christmas Eve, New Year's Eve, and day before major holidays) 387 | 20011224,h 388 | 20021224,h 389 | 20031224,h 390 | 20041224,h 391 | 20071224,h 392 | 20081224,h 393 | 20091224,h 394 | 20101224,h 395 | 20121224,h 396 | 20131224,h 397 | 20141224,h 398 | 20151224,h 399 | 20181224,h 400 | 20191224,h 401 | 20201224,h 402 | 20211224,h 403 | 20241224,h 404 | 20251224,h 405 | 20261224,h 406 | # New Year's Eve half-days 407 | 20001229,h 408 | 20011231,h 409 | 20021231,h 410 | 20031231,h 411 | 20041231,h 412 | 20051230,h 413 | 20061229,h 414 | 20071231,h 415 | 20081231,h 416 | 20091231,h 417 | 20101231,h 418 | 20111230,h 419 | 20121231,h 420 | 20131231,h 421 | 20141231,h 422 | 20151231,h 423 | 20161230,h 424 | 20170127,h 425 | 20181231,h 426 | 20191231,h 427 | 20201231,h 428 | 20211231,h 429 | 20221230,h 430 | 20231229,h 431 | 20241231,h 432 | 20251231,h 433 | 20261231,h 434 | # Lunar New Year's Eve half-days 435 | 20010123,h 436 | 20020211,h 437 | 20030131,h 438 | 20040121,h 439 | 20050208,h 440 | 20070216,h 441 | 20080206,h 442 | 20100212,h 443 | 20110202,h 444 | 20130208,h 445 | 20140130,h 446 | 20150218,h 447 | 20180215,h 448 | 20190204,h 449 | 20200124,h 450 | 20210210,h 451 | 20230120,h 452 | 20240209,h 453 | 20250128,h 454 | 20260216,h 455 | # Day before major holidays 456 | 20010404,h 457 | 20020404,h 458 | 20030404,h 459 | 20050404,h 460 | 20060404,h 461 | 20070404,h 462 | 20080403,h 463 | 20090403,h 464 | 20110404,h 465 | 20120403,h 466 | 20130403,h 467 | 20140403,h 468 | 20150402,h 469 | 20160331,h 470 | 20180404,h 471 | 20190404,h 472 | 20200403,h 473 | 20220404,h 474 | 20230404,h 475 | 20240403,h 476 | 20250403,h 477 | # Day before National Day 478 | 20020930,h 479 | 20030930,h 480 | 20040930,h 481 | 20050930,h 482 | 20060929,h 483 | 20070928,h 484 | 20080930,h 485 | 20090930,h 486 | 20100930,h 487 | 20110930,h 488 | 20120928,h 489 | 20130930,h 490 | 20140930,h 491 | 20150930,h 492 | 20160930,h 493 | 20180928,h 494 | 20190930,h 495 | 20200930,h 496 | 20210930,h 497 | 20220930,h 498 | 20230928,h 499 | 20240930,h 500 | 20250930,h 501 | -------------------------------------------------------------------------------- /tests/half_day_weekend_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import unittest 4 | from datetime import date, datetime 5 | 6 | from cn_stock_holidays.data_hk import ( 7 | is_half_day_trading_day, 8 | is_trading_day, 9 | get_cached_with_half_day, 10 | ) 11 | from cn_stock_holidays.common import int_to_date 12 | 13 | 14 | class TestHalfDayWeekendLogic(unittest.TestCase): 15 | """Test that half-day trading logic correctly handles weekends.""" 16 | 17 | def setUp(self): 18 | """Clear cache before each test.""" 19 | from cn_stock_holidays.data_hk import get_cached_with_half_day 20 | 21 | get_cached_with_half_day.cache_clear() 22 | 23 | def test_weekend_half_days_are_excluded(self): 24 | """Test that half-day trading days on weekends are correctly excluded.""" 25 | # These dates were previously marked as half-days but fall on weekends 26 | weekend_half_days = [ 27 | (2000, 12, 24), # Sunday 28 | (2005, 12, 24), # Saturday 29 | (2006, 12, 24), # Sunday 30 | (2011, 12, 24), # Saturday 31 | (2016, 12, 24), # Saturday 32 | (2022, 12, 24), # Saturday 33 | (2023, 12, 24), # Sunday 34 | (2006, 1, 28), # Saturday 35 | (2009, 1, 25), # Sunday 36 | (2012, 1, 22), # Sunday 37 | (2016, 2, 7), # Sunday 38 | (2022, 1, 30), # Sunday 39 | (2004, 4, 4), # Sunday 40 | (2001, 9, 30), # Sunday 41 | ] 42 | 43 | for year, month, day in weekend_half_days: 44 | test_date = date(year, month, day) 45 | with self.subTest(date=test_date): 46 | # Should not be a half-day trading day (because it's weekend) 47 | self.assertFalse( 48 | is_half_day_trading_day(test_date), 49 | f"{test_date} should not be a half-day trading day (weekend)", 50 | ) 51 | # Should not be a trading day (because it's weekend) 52 | self.assertFalse( 53 | is_trading_day(test_date), 54 | f"{test_date} should not be a trading day (weekend)", 55 | ) 56 | 57 | def test_valid_half_days_are_included(self): 58 | """Test that valid half-day trading days (weekdays) are correctly included.""" 59 | # These are valid half-day trading days (weekdays) 60 | valid_half_days = [ 61 | (2001, 12, 24), # Monday 62 | (2002, 12, 24), # Tuesday 63 | (2003, 12, 24), # Wednesday 64 | (2004, 12, 24), # Friday 65 | (2007, 12, 24), # Monday 66 | (2008, 12, 24), # Wednesday 67 | (2009, 12, 24), # Thursday 68 | (2010, 12, 24), # Friday 69 | (2012, 12, 24), # Monday 70 | (2013, 12, 24), # Tuesday 71 | ] 72 | 73 | for year, month, day in valid_half_days: 74 | test_date = date(year, month, day) 75 | with self.subTest(date=test_date): 76 | # Should be a half-day trading day 77 | self.assertTrue( 78 | is_half_day_trading_day(test_date), 79 | f"{test_date} should be a half-day trading day", 80 | ) 81 | # Should also be a trading day (half-days are trading days) 82 | self.assertTrue( 83 | is_trading_day(test_date), 84 | f"{test_date} should be a trading day (half-day)", 85 | ) 86 | 87 | def test_weekend_dates_are_not_trading_days(self): 88 | """Test that weekend dates are never trading days.""" 89 | # Test some weekend dates 90 | weekend_dates = [ 91 | (2024, 1, 6), # Saturday 92 | (2024, 1, 7), # Sunday 93 | (2024, 1, 13), # Saturday 94 | (2024, 1, 14), # Sunday 95 | ] 96 | 97 | for year, month, day in weekend_dates: 98 | test_date = date(year, month, day) 99 | with self.subTest(date=test_date): 100 | # Should not be a half-day trading day 101 | self.assertFalse( 102 | is_half_day_trading_day(test_date), 103 | f"{test_date} should not be a half-day trading day (weekend)", 104 | ) 105 | # Should not be a trading day 106 | self.assertFalse( 107 | is_trading_day(test_date), 108 | f"{test_date} should not be a trading day (weekend)", 109 | ) 110 | 111 | def test_regular_weekdays_are_trading_days(self): 112 | """Test that regular weekdays are trading days (unless they're holidays).""" 113 | # Test some regular weekdays 114 | weekday_dates = [ 115 | (2024, 1, 2), # Tuesday 116 | (2024, 1, 3), # Wednesday 117 | (2024, 1, 4), # Thursday 118 | (2024, 1, 5), # Friday 119 | (2024, 1, 8), # Monday 120 | ] 121 | 122 | for year, month, day in weekday_dates: 123 | test_date = date(year, month, day) 124 | with self.subTest(date=test_date): 125 | # Should be a trading day (unless it's a holiday) 126 | # Note: We don't assert True here because some might be holidays 127 | result = is_trading_day(test_date) 128 | self.assertIsInstance( 129 | result, bool, f"{test_date} should return boolean" 130 | ) 131 | 132 | def test_half_day_data_integrity(self): 133 | """Test that half-day data is properly loaded and contains no weekends.""" 134 | holidays, half_days = get_cached_with_half_day() 135 | 136 | # Check that half_days is not empty 137 | self.assertGreater(len(half_days), 0, "Half-days data should not be empty") 138 | 139 | # Check that no half-day is on a weekend 140 | for half_day in half_days: 141 | with self.subTest(half_day=half_day): 142 | weekday = half_day.weekday() # 0=Monday, 6=Sunday 143 | self.assertLess( 144 | weekday, 145 | 5, 146 | f"{half_day} should not be a weekend (weekday {weekday})", 147 | ) 148 | 149 | def test_datetime_objects_are_handled(self): 150 | """Test that datetime objects are properly handled.""" 151 | # Test with datetime objects 152 | test_datetime = datetime(2024, 12, 24, 10, 30) # Tuesday 153 | test_date = date(2024, 12, 24) 154 | 155 | # Both should return the same result 156 | datetime_result = is_half_day_trading_day(test_datetime) 157 | date_result = is_half_day_trading_day(test_date) 158 | 159 | self.assertEqual(datetime_result, date_result) 160 | self.assertIsInstance(datetime_result, bool) 161 | 162 | def test_edge_cases(self): 163 | """Test edge cases and boundary conditions.""" 164 | # Test with None (should raise TypeError) 165 | with self.assertRaises(TypeError): 166 | is_half_day_trading_day(None) 167 | 168 | # Test with invalid date string (should raise TypeError) 169 | with self.assertRaises(TypeError): 170 | is_half_day_trading_day("invalid_date") 171 | 172 | # Test with integer (should raise TypeError) 173 | with self.assertRaises(TypeError): 174 | is_half_day_trading_day(20241224) 175 | 176 | 177 | if __name__ == "__main__": 178 | unittest.main() 179 | -------------------------------------------------------------------------------- /tests/latest_year_weekends_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Automated test to verify that the latest year in data.txt does 5 | not contain weekend dates. 6 | 7 | This test automatically detects the latest year in the data file 8 | and validates: 9 | 1. All weekend dates in that year are NOT in data.txt 10 | 2. All weekend dates are correctly identified as non-trading days 11 | 3. No weekend dates were accidentally added to the data file 12 | 13 | This ensures data integrity whenever new year data is added. 14 | """ 15 | 16 | import unittest 17 | from datetime import date, timedelta 18 | from typing import List, Set 19 | 20 | from cn_stock_holidays.data import get_cached, is_trading_day 21 | 22 | 23 | class TestLatestYearWeekends(unittest.TestCase): 24 | """Test that the latest year in data.txt contains no weekend dates.""" 25 | 26 | @classmethod 27 | def setUpClass(cls): 28 | """Get the latest year from data.txt once for all tests.""" 29 | # Read all dates from data.txt 30 | holidays = get_cached() 31 | 32 | if not holidays: 33 | raise ValueError("No holiday data found in data.txt") 34 | 35 | # Find the latest year 36 | cls.latest_year = max(d.year for d in holidays) 37 | 38 | # Get all dates from data.txt for the latest year 39 | cls.latest_year_dates: Set[date] = { 40 | d for d in holidays if d.year == cls.latest_year 41 | } 42 | 43 | # Generate all weekend dates for the latest year 44 | cls.weekend_dates: List[date] = [] 45 | current_date = date(cls.latest_year, 1, 1) 46 | end_date = date(cls.latest_year, 12, 31) 47 | 48 | while current_date <= end_date: 49 | if current_date.weekday() >= 5: # Saturday=5, Sunday=6 50 | cls.weekend_dates.append(current_date) 51 | current_date += timedelta(days=1) 52 | 53 | print(f"\n{'='*70}") 54 | print(f"Testing latest year: {cls.latest_year}") 55 | print( 56 | f"Total dates in data.txt for {cls.latest_year}: " 57 | f"{len(cls.latest_year_dates)}" 58 | ) 59 | print(f"Total weekend dates in {cls.latest_year}: {len(cls.weekend_dates)}") 60 | print(f"{'='*70}\n") 61 | 62 | def test_no_weekends_in_data_file(self): 63 | """Verify that no weekend dates are in data.txt for the latest year.""" 64 | weekends_in_data = [] 65 | 66 | for weekend_date in self.weekend_dates: 67 | if weekend_date in self.latest_year_dates: 68 | weekends_in_data.append(weekend_date) 69 | 70 | if weekends_in_data: 71 | error_msg = ( 72 | f"\n❌ Found {len(weekends_in_data)} weekend date(s) in data.txt " 73 | f"for year {self.latest_year}:\n" 74 | ) 75 | for d in weekends_in_data: 76 | weekday_name = d.strftime("%A") 77 | error_msg += f" - {d} ({weekday_name})\n" 78 | error_msg += ( 79 | "\nWeekend dates should NOT be in data.txt as they are " 80 | "automatically handled by the code.\n" 81 | "Please remove these dates from cn_stock_holidays/data.txt" 82 | ) 83 | self.fail(error_msg) 84 | else: 85 | print(f"✓ No weekend dates found in data.txt for {self.latest_year}") 86 | 87 | def test_all_weekends_are_non_trading_days(self): 88 | """Verify all weekend dates are non-trading days.""" 89 | failed_dates = [] 90 | 91 | for weekend_date in self.weekend_dates: 92 | if is_trading_day(weekend_date): 93 | failed_dates.append(weekend_date) 94 | 95 | if failed_dates: 96 | error_msg = ( 97 | f"\n❌ {len(failed_dates)} weekend date(s) incorrectly identified " 98 | f"as trading days:\n" 99 | ) 100 | for d in failed_dates: 101 | weekday_name = d.strftime("%A") 102 | error_msg += f" - {d} ({weekday_name})\n" 103 | self.fail(error_msg) 104 | else: 105 | print( 106 | f"✓ All {len(self.weekend_dates)} weekend dates in {self.latest_year} " 107 | f"are correctly non-trading days" 108 | ) 109 | 110 | def test_all_data_file_dates_are_weekdays(self): 111 | """Verify that all dates in data.txt for the latest year are weekdays.""" 112 | weekend_dates_in_file = [] 113 | 114 | for d in self.latest_year_dates: 115 | if d.weekday() >= 5: # Saturday or Sunday 116 | weekend_dates_in_file.append(d) 117 | 118 | if weekend_dates_in_file: 119 | error_msg = ( 120 | f"\n❌ Found {len(weekend_dates_in_file)} weekend date(s) " 121 | f"in data.txt for {self.latest_year}:\n" 122 | ) 123 | for d in weekend_dates_in_file: 124 | weekday_name = d.strftime("%A") 125 | error_msg += f" - {d.strftime('%Y%m%d')} ({weekday_name})\n" 126 | error_msg += ( 127 | "\nAll dates in data.txt must be weekdays (Monday-Friday).\n" 128 | "Please remove weekend dates from cn_stock_holidays/data.txt" 129 | ) 130 | self.fail(error_msg) 131 | else: 132 | print( 133 | f"✓ All {len(self.latest_year_dates)} dates in data.txt " 134 | f"for {self.latest_year} are weekdays" 135 | ) 136 | 137 | def test_data_file_dates_are_non_trading(self): 138 | """Verify that all dates in data.txt are correctly non-trading days.""" 139 | incorrectly_trading = [] 140 | 141 | for d in self.latest_year_dates: 142 | if is_trading_day(d): 143 | incorrectly_trading.append(d) 144 | 145 | if incorrectly_trading: 146 | error_msg = ( 147 | f"\n❌ Found {len(incorrectly_trading)} date(s) in data.txt " 148 | f"that are incorrectly identified as trading days:\n" 149 | ) 150 | for d in incorrectly_trading: 151 | weekday_name = d.strftime("%A") 152 | error_msg += f" - {d} ({weekday_name})\n" 153 | self.fail(error_msg) 154 | else: 155 | print( 156 | f"✓ All {len(self.latest_year_dates)} dates in data.txt " 157 | f"for {self.latest_year} are non-trading days" 158 | ) 159 | 160 | def test_weekend_count_is_reasonable(self): 161 | """Verify the number of weekends is reasonable for a year.""" 162 | # A year should have approximately 104 weekend days (52 weeks * 2 days) 163 | # Allow range of 102-106 to account for how the year starts/ends 164 | min_weekends = 102 165 | max_weekends = 106 166 | 167 | actual_count = len(self.weekend_dates) 168 | 169 | self.assertGreaterEqual( 170 | actual_count, 171 | min_weekends, 172 | f"Too few weekend dates detected ({actual_count}). " 173 | f"Expected at least {min_weekends}.", 174 | ) 175 | self.assertLessEqual( 176 | actual_count, 177 | max_weekends, 178 | f"Too many weekend dates detected ({actual_count}). " 179 | f"Expected at most {max_weekends}.", 180 | ) 181 | 182 | print( 183 | f"✓ Weekend count for {self.latest_year} is reasonable: " 184 | f"{actual_count} days ({min_weekends}-{max_weekends} expected)" 185 | ) 186 | 187 | 188 | if __name__ == "__main__": 189 | unittest.main(verbosity=2) 190 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Versions follow Semantic Versioning (..). 4 | 5 | ## cn-stock-holidays 2.1.5 (2025-12-07) 6 | 7 | ### Improvements 8 | 9 | - **Updated 2026 Hong Kong Stock Market Holidays**: Added complete holiday data for 2026 from HKEX official circular 10 | - Full-day closures: New Year's Day (Jan 1), Lunar New Year (Feb 17-19), Good Friday (Apr 3), Day after Ching Ming (Apr 6), Day after Easter Monday (Apr 7), Labour Day (May 1), Day after Buddha's Birthday (May 25), Dragon Boat Festival (Jun 19), HKSAR Day (Jul 1), National Day (Oct 1), Day after Chung Yeung Festival (Oct 19), Christmas (Dec 25) 11 | - Half-day trading: Lunar New Year's Eve (Feb 16), Christmas Eve (Dec 24), New Year's Eve (Dec 31) 12 | - Data source: HKEX Circular CT/075/25 () 13 | 14 | ## cn-stock-holidays 2.1.4 (2025-12-07) 15 | 16 | ### Improvements 17 | 18 | - **Updated 2026 Stock Market Holidays**: Added complete holiday data for 2026 from TDX official source 19 | - New Year's Day: January 1-2 20 | - Spring Festival: February 16-20, 23 21 | - Qingming Festival: April 6 22 | - Labor Day: May 1, 4-5 23 | - Dragon Boat Festival: June 19 24 | - Mid-Autumn Festival: September 25 25 | - National Day: October 1-2, 5-7 26 | - Data source: 27 | 28 | ## cn-stock-holidays 2.1.3 (2025-01-27) 29 | 30 | ### Improvements 31 | 32 | - **Code Cleanup and Maintenance**: Removed outdated and unused code to improve project maintainability 33 | - Removed `wind_holidays.py` - outdated Wind API integration script 34 | - Removed temporary data processing tools in `tools/in/` directory 35 | - Removed deprecated `utils/` directory with unused data fetching scripts 36 | - Consolidated development scripts by merging `debug.py` into `dev_shell.py` 37 | - Simplified `ipython_config.py` configuration 38 | - Updated documentation and script references 39 | - Reduced codebase by ~500+ lines of unused code while maintaining 100% test coverage 40 | 41 | ## cn-stock-holidays 2.1.2 (2025-01-27) 42 | 43 | ### Bug Fixes 44 | 45 | - Fixed historical data for 2019 Labor Day holiday arrangement - confirmed May 2nd and 3rd, 2019 were closed as per SSE announcement ([Issue #13](https://github.com/rainx/cn_stock_holidays/issues/13), [SSE Reference](https://www.sse.com.cn/disclosure/announcement/general/c/c_20190418_4771364.shtml)) 46 | 47 | ## cn-stock-holidays 2.1.1 (2025-01-27) 48 | 49 | ### Bug Fixes 50 | 51 | - Fixed historical data for 2024-02-09 (Chinese New Year's Eve) - updated to reflect that markets were closed on this date ([Issue #16](https://github.com/rainx/cn_stock_holidays/issues/16)) 52 | 53 | ## cn-stock-holidays 2.1.0 (2024-12-19) 54 | 55 | ### New Features 56 | 57 | - **Half-Day Trading Support for Hong Kong Market**: Added comprehensive support for Hong Kong stock exchange half-day trading 58 | - New `is_half_day_trading_day()` function to detect half-day trading days 59 | - Enhanced data format support with `,h` suffix for half-day trading dates (e.g., `20251225,h`) 60 | - Backward compatibility maintained for existing Shanghai/Shenzhen market data 61 | - Half-day trading days are still considered trading days by `is_trading_day()`, `next_trading_day()`, `previous_trading_day()`, and `trading_days_between()` 62 | - Updated Hong Kong market data to include half-day trading dates through 2025 63 | - Added comprehensive test suite for half-day trading functionality 64 | 65 | ### Improvements 66 | 67 | - Extended Hong Kong market holiday data through 2025 68 | - Added support for common half-day trading patterns: 69 | - Christmas Eve (December 24) 70 | - New Year's Eve (December 31) 71 | - Lunar New Year's Eve 72 | - Day before major holidays (Qingming Festival, National Day) 73 | - Enhanced data parsing with `_get_from_file_with_half_day()` function 74 | - Added new meta functions for half-day trading support while maintaining existing API compatibility 75 | 76 | ## cn-stock-holidays 2.0.0 (2024-12-19) 77 | 78 | ### New Features 79 | 80 | - **Major Update**: Project modernization 81 | - Introduced uv as a modern Python package manager 82 | - Migrated to pyproject.toml configuration, removed setup.py 83 | - Added complete development toolchain: black, isort, mypy, flake8, pre-commit 84 | - Updated CI/CD workflows to use uv for testing, building and publishing 85 | - Renamed CI workflow file from test.yml to ci.yml for better clarity 86 | - Migrated to PyPI Trusted Publisher for secure automated publishing 87 | - Fixed deprecated GitHub Actions upload-artifact from v3 to v4 88 | - Fixed uv publish command by removing unsupported --yes flag and adding trusted-publishing 89 | - Fixed publish job by adding download-artifact step to access built packages 90 | - Added code quality checks and automated formatting 91 | - Support for modern Python packaging standards (PEP 517/518) 92 | - Improved project structure and documentation 93 | 94 | ## cn-stock-holidays 1.12 (2024-12-03) 95 | 96 | ### Improvements 97 | 98 | - Updated 2024 domestic stock market holidays 99 | 100 | ## cn-stock-holidays 1.11 (2023-12-25) 101 | 102 | ### Improvements 103 | 104 | - Updated 2024 domestic stock market holidays 105 | 106 | ## cn-stock-holidays 1.10 (2022-12-16) 107 | 108 | ### Improvements 109 | 110 | - Updated 2023 domestic stock market holidays 111 | 112 | ## cn-stock-holidays 1.9 (2021-12-30) 113 | 114 | ### Improvements 115 | 116 | - Updated 2022 domestic stock market holidays 117 | 118 | ## cn-stock-holidays 1.8 (2020-12-25) 119 | 120 | ### Improvements 121 | 122 | - Updated 2021 domestic stock market holidays 123 | 124 | ## cn-stock-holidays 1.7 (2020-01-31) 125 | 126 | ### Bug Fixes 127 | 128 | - Changed 2020 stock market calendar due to 2019-nCoV impact, added 2020-01-31 129 | 130 | ## cn-stock-holidays 1.6 (2019-12-06) 131 | 132 | ### Bug Fixes 133 | 134 | - Fixed an error: 20090101 -> 20190101, thanks @liuyug #8 135 | 136 | ## cn-stock-holidays 1.5 (2019-11-26) 137 | 138 | ### Improvements 139 | 140 | - Updated 2020 China market holiday data ref: 141 | 142 | ``` 143 | $("table.table tr td:first-child").map((i, e)=>e.innerText).toArray().filter(e => /[\d.]/.test(e)).map(e=>e.replace(/\./g, "")).join("\n") 144 | ``` 145 | 146 | - Updated 2019, 2020 Hong Kong stock market holiday data ref: 147 | 148 | ``` 149 | $("table.table tr td:first-child").map((i, e)=>e.innerText).toArray().filter(e => /[\d.]/.test(e)).map(e=>e.replace(/\./g, "")).filter(e=>e.includes("/")).map(e=> moment(e, "D/M/YYYY").format("YYYYMMDD")).join("\n") 150 | ``` 151 | 152 | ## cn-stock-holidays 1.4 (2019-01-08) 153 | 154 | ### Improvements 155 | 156 | - Updated 2019 China market holiday data 157 | 158 | ## cn-stock-holidays 1.3 (2018-04-17) 159 | 160 | ### Improvements 161 | 162 | - Updated HK 2018 holiday data 163 | 164 | ## cn-stock-holidays 1.2 (2017-12-20) 165 | 166 | ### New Features 167 | 168 | - Added get-day-list command for obtaining workday or holiday lists within a period 169 | 170 | ## cn-stock-holidays 1.1 (2017-11-27) 171 | 172 | ### New Features 173 | 174 | - Merged PR #2 from @JaysonAlbert 175 | - Added minutes per session 176 | - Added code for obtaining holiday information from Wind 177 | 178 | ## cn-stock-holidays 1.0 (2017-11-06) 179 | 180 | ### New Features 181 | 182 | - Added support for Hong Kong Exchange 183 | 184 | ## cn-stock-holidays 0.x 185 | 186 | ### Historical 187 | 188 | - Prehistoric versions, historical records not yet organized 189 | -------------------------------------------------------------------------------- /cn_stock_holidays/data.txt: -------------------------------------------------------------------------------- 1 | 19910101 2 | 19910215 3 | 19910218 4 | 19910501 5 | 19911001 6 | 19911002 7 | 19920101 8 | 19920204 9 | 19920205 10 | 19920206 11 | 19920501 12 | 19921001 13 | 19921002 14 | 19930101 15 | 19930125 16 | 19930126 17 | 19931001 18 | 19940207 19 | 19940208 20 | 19940209 21 | 19940210 22 | 19940211 23 | 19940502 24 | 19941003 25 | 19941004 26 | 19950102 27 | 19950130 28 | 19950131 29 | 19950201 30 | 19950202 31 | 19950203 32 | 19950501 33 | 19951002 34 | 19951003 35 | 19960101 36 | 19960219 37 | 19960220 38 | 19960221 39 | 19960222 40 | 19960223 41 | 19960226 42 | 19960227 43 | 19960228 44 | 19960229 45 | 19960301 46 | 19960501 47 | 19960930 48 | 19961001 49 | 19961002 50 | 19970101 51 | 19970203 52 | 19970204 53 | 19970205 54 | 19970206 55 | 19970207 56 | 19970210 57 | 19970211 58 | 19970212 59 | 19970213 60 | 19970214 61 | 19970501 62 | 19970502 63 | 19970630 64 | 19970701 65 | 19971001 66 | 19971002 67 | 19971003 68 | 19980101 69 | 19980102 70 | 19980126 71 | 19980127 72 | 19980128 73 | 19980129 74 | 19980130 75 | 19980202 76 | 19980203 77 | 19980204 78 | 19980205 79 | 19980206 80 | 19980501 81 | 19981001 82 | 19981002 83 | 19990101 84 | 19990210 85 | 19990211 86 | 19990212 87 | 19990215 88 | 19990216 89 | 19990217 90 | 19990218 91 | 19990219 92 | 19990222 93 | 19990223 94 | 19990224 95 | 19990225 96 | 19990226 97 | 19990503 98 | 19991001 99 | 19991004 100 | 19991005 101 | 19991006 102 | 19991007 103 | 19991220 104 | 19991231 105 | 20000103 106 | 20000131 107 | 20000201 108 | 20000202 109 | 20000203 110 | 20000204 111 | 20000207 112 | 20000208 113 | 20000209 114 | 20000210 115 | 20000211 116 | 20000501 117 | 20000502 118 | 20000503 119 | 20000504 120 | 20000505 121 | 20001002 122 | 20001003 123 | 20001004 124 | 20001005 125 | 20001006 126 | 20010101 127 | 20010122 128 | 20010123 129 | 20010124 130 | 20010125 131 | 20010126 132 | 20010129 133 | 20010130 134 | 20010131 135 | 20010201 136 | 20010202 137 | 20010501 138 | 20010502 139 | 20010503 140 | 20010504 141 | 20010507 142 | 20011001 143 | 20011002 144 | 20011003 145 | 20011004 146 | 20011005 147 | 20020101 148 | 20020102 149 | 20020103 150 | 20020211 151 | 20020212 152 | 20020213 153 | 20020214 154 | 20020215 155 | 20020218 156 | 20020219 157 | 20020220 158 | 20020221 159 | 20020222 160 | 20020501 161 | 20020502 162 | 20020503 163 | 20020506 164 | 20020507 165 | 20020930 166 | 20021001 167 | 20021002 168 | 20021003 169 | 20021004 170 | 20021007 171 | 20030101 172 | 20030130 173 | 20030131 174 | 20030203 175 | 20030204 176 | 20030205 177 | 20030206 178 | 20030207 179 | 20030501 180 | 20030502 181 | 20030505 182 | 20030506 183 | 20030507 184 | 20030508 185 | 20030509 186 | 20031001 187 | 20031002 188 | 20031003 189 | 20031006 190 | 20031007 191 | 20040101 192 | 20040119 193 | 20040120 194 | 20040121 195 | 20040122 196 | 20040123 197 | 20040126 198 | 20040127 199 | 20040128 200 | 20040503 201 | 20040504 202 | 20040505 203 | 20040506 204 | 20040507 205 | 20041001 206 | 20041004 207 | 20041005 208 | 20041006 209 | 20041007 210 | 20050103 211 | 20050207 212 | 20050208 213 | 20050209 214 | 20050210 215 | 20050211 216 | 20050214 217 | 20050215 218 | 20050502 219 | 20050503 220 | 20050504 221 | 20050505 222 | 20050506 223 | 20051003 224 | 20051004 225 | 20051005 226 | 20051006 227 | 20051007 228 | 20060102 229 | 20060103 230 | 20060126 231 | 20060127 232 | 20060130 233 | 20060131 234 | 20060201 235 | 20060202 236 | 20060203 237 | 20060501 238 | 20060502 239 | 20060503 240 | 20060504 241 | 20060505 242 | 20061002 243 | 20061003 244 | 20061004 245 | 20061005 246 | 20061006 247 | 20070101 248 | 20070102 249 | 20070103 250 | 20070219 251 | 20070220 252 | 20070221 253 | 20070222 254 | 20070223 255 | 20070501 256 | 20070502 257 | 20070503 258 | 20070504 259 | 20070507 260 | 20071001 261 | 20071002 262 | 20071003 263 | 20071004 264 | 20071005 265 | 20071231 266 | 20080101 267 | 20080206 268 | 20080207 269 | 20080208 270 | 20080211 271 | 20080212 272 | 20080404 273 | 20080501 274 | 20080502 275 | 20080609 276 | 20080915 277 | 20080929 278 | 20080930 279 | 20081001 280 | 20081002 281 | 20081003 282 | 20090101 283 | 20090102 284 | 20090126 285 | 20090127 286 | 20090128 287 | 20090129 288 | 20090130 289 | 20090406 290 | 20090501 291 | 20090528 292 | 20090529 293 | 20091001 294 | 20091002 295 | 20091005 296 | 20091006 297 | 20091007 298 | 20091008 299 | 20100101 300 | 20100215 301 | 20100216 302 | 20100217 303 | 20100218 304 | 20100219 305 | 20100405 306 | 20100503 307 | 20100614 308 | 20100615 309 | 20100616 310 | 20100922 311 | 20100923 312 | 20100924 313 | 20101001 314 | 20101004 315 | 20101005 316 | 20101006 317 | 20101007 318 | 20110103 319 | 20110202 320 | 20110203 321 | 20110204 322 | 20110207 323 | 20110208 324 | 20110404 325 | 20110405 326 | 20110502 327 | 20110606 328 | 20110912 329 | 20111003 330 | 20111004 331 | 20111005 332 | 20111006 333 | 20111007 334 | 20120102 335 | 20120103 336 | 20120123 337 | 20120124 338 | 20120125 339 | 20120126 340 | 20120127 341 | 20120402 342 | 20120403 343 | 20120404 344 | 20120430 345 | 20120501 346 | 20120622 347 | 20121001 348 | 20121002 349 | 20121003 350 | 20121004 351 | 20121005 352 | 20130101 353 | 20130102 354 | 20130103 355 | 20130211 356 | 20130212 357 | 20130213 358 | 20130214 359 | 20130215 360 | 20130404 361 | 20130405 362 | 20130429 363 | 20130430 364 | 20130501 365 | 20130610 366 | 20130611 367 | 20130612 368 | 20130919 369 | 20130920 370 | 20131001 371 | 20131002 372 | 20131003 373 | 20131004 374 | 20131007 375 | 20140101 376 | 20140131 377 | 20140203 378 | 20140204 379 | 20140205 380 | 20140206 381 | 20140407 382 | 20140501 383 | 20140502 384 | 20140602 385 | 20140908 386 | 20141001 387 | 20141002 388 | 20141003 389 | 20141006 390 | 20141007 391 | 20150101 392 | 20150102 393 | 20150218 394 | 20150219 395 | 20150220 396 | 20150223 397 | 20150224 398 | 20150406 399 | 20150501 400 | 20150622 401 | 20150903 402 | 20150904 403 | 20151001 404 | 20151002 405 | 20151005 406 | 20151006 407 | 20151007 408 | 20160101 409 | 20160208 410 | 20160209 411 | 20160210 412 | 20160211 413 | 20160212 414 | 20160404 415 | 20160502 416 | 20160609 417 | 20160610 418 | 20160915 419 | 20160916 420 | 20161003 421 | 20161004 422 | 20161005 423 | 20161006 424 | 20161007 425 | 20170102 426 | 20170127 427 | 20170130 428 | 20170131 429 | 20170201 430 | 20170202 431 | 20170403 432 | 20170404 433 | 20170501 434 | 20170529 435 | 20170530 436 | 20171002 437 | 20171003 438 | 20171004 439 | 20171005 440 | 20171006 441 | 20180101 442 | 20180215 443 | 20180216 444 | 20180219 445 | 20180220 446 | 20180221 447 | 20180405 448 | 20180406 449 | 20180430 450 | 20180501 451 | 20180618 452 | 20180924 453 | 20181001 454 | 20181002 455 | 20181003 456 | 20181004 457 | 20181005 458 | 20190101 459 | 20190204 460 | 20190205 461 | 20190206 462 | 20190207 463 | 20190208 464 | 20190405 465 | 20190501 466 | 20190502 467 | 20190503 468 | 20190607 469 | 20190913 470 | 20191001 471 | 20191002 472 | 20191003 473 | 20191004 474 | 20191007 475 | 20200101 476 | 20200124 477 | 20200127 478 | 20200128 479 | 20200129 480 | 20200130 481 | 20200131 482 | 20200406 483 | 20200501 484 | 20200504 485 | 20200505 486 | 20200625 487 | 20200626 488 | 20201001 489 | 20201002 490 | 20201005 491 | 20201006 492 | 20201007 493 | 20201008 494 | 20210101 495 | 20210211 496 | 20210212 497 | 20210215 498 | 20210216 499 | 20210217 500 | 20210405 501 | 20210503 502 | 20210504 503 | 20210505 504 | 20210614 505 | 20210920 506 | 20210921 507 | 20211001 508 | 20211004 509 | 20211005 510 | 20211006 511 | 20211007 512 | 20220103 513 | 20220131 514 | 20220201 515 | 20220202 516 | 20220203 517 | 20220204 518 | 20220404 519 | 20220405 520 | 20220502 521 | 20220503 522 | 20220504 523 | 20220603 524 | 20220912 525 | 20221003 526 | 20221004 527 | 20221005 528 | 20221006 529 | 20221007 530 | 20230102 531 | 20230123 532 | 20230124 533 | 20230125 534 | 20230126 535 | 20230127 536 | 20230405 537 | 20230501 538 | 20230502 539 | 20230503 540 | 20230622 541 | 20230623 542 | 20230929 543 | 20231002 544 | 20231003 545 | 20231004 546 | 20231005 547 | 20231006 548 | 20240101 549 | 20240209 550 | 20240212 551 | 20240213 552 | 20240214 553 | 20240215 554 | 20240216 555 | 20240404 556 | 20240405 557 | 20240501 558 | 20240502 559 | 20240503 560 | 20240610 561 | 20240916 562 | 20240917 563 | 20241001 564 | 20241002 565 | 20241003 566 | 20241004 567 | 20241007 568 | 20250101 569 | 20250128 570 | 20250129 571 | 20250130 572 | 20250131 573 | 20250203 574 | 20250204 575 | 20250404 576 | 20250501 577 | 20250502 578 | 20250505 579 | 20250602 580 | 20251001 581 | 20251002 582 | 20251003 583 | 20251006 584 | 20251007 585 | 20251008 586 | 20260101 587 | 20260102 588 | 20260216 589 | 20260217 590 | 20260218 591 | 20260219 592 | 20260220 593 | 20260223 594 | 20260406 595 | 20260501 596 | 20260504 597 | 20260505 598 | 20260619 599 | 20260925 600 | 20261001 601 | 20261002 602 | 20261005 603 | 20261006 604 | 20261007 605 | -------------------------------------------------------------------------------- /cn_stock_holidays/meta_functions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | import datetime 5 | import logging 6 | import os 7 | import requests 8 | from cn_stock_holidays.common import ( 9 | function_cache, 10 | int_to_date, 11 | print_result, 12 | _get_from_file, 13 | _get_from_file_with_half_day, 14 | ) 15 | 16 | 17 | # meta func is not a good design, but for backward compatibility for data version and create similar logic for hk, 18 | # we did it 19 | 20 | 21 | def meta_get_local(data_file_name="data.txt"): 22 | def get_local(use_list=False): 23 | """ 24 | read data from package data file 25 | :return: a list contains all holiday data, element with datatime.date format 26 | """ 27 | datafilepath = os.path.join(os.path.dirname(__file__), data_file_name) 28 | return _get_from_file(datafilepath, use_list) 29 | 30 | return get_local 31 | 32 | 33 | def meta_get_local_with_half_day(data_file_name="data.txt"): 34 | def get_local_with_half_day(use_list=False): 35 | """ 36 | read data from package data file with half-day trading support 37 | :return: a tuple (holidays, half_days) where both are sets/lists of datetime.date 38 | """ 39 | datafilepath = os.path.join(os.path.dirname(__file__), data_file_name) 40 | return _get_from_file_with_half_day(datafilepath, use_list) 41 | 42 | return get_local_with_half_day 43 | 44 | 45 | def meta_get_cached(get_local, get_cache_path): 46 | # @function_cache 47 | def get_cached(use_list=False): 48 | """ 49 | get from cache version , if it is not exising , use txt file in package data 50 | :return: a list/set contains all holiday data, element with datatime.date format 51 | """ 52 | cache_path = get_cache_path() 53 | 54 | if os.path.isfile(cache_path): 55 | return _get_from_file(cache_path, use_list) 56 | else: 57 | return get_local(use_list=False) 58 | 59 | return get_cached 60 | 61 | 62 | def meta_get_cached_with_half_day(get_local_with_half_day, get_cache_path): 63 | # @function_cache 64 | def get_cached_with_half_day(use_list=False): 65 | """ 66 | get from cache version with half-day trading support, if it is not existing, use txt file in package data 67 | :return: a tuple (holidays, half_days) where both are sets/lists of datetime.date 68 | """ 69 | cache_path = get_cache_path() 70 | 71 | if os.path.isfile(cache_path): 72 | return _get_from_file_with_half_day(cache_path, use_list) 73 | else: 74 | return get_local_with_half_day(use_list=False) 75 | 76 | return get_cached_with_half_day 77 | 78 | 79 | def meta_get_remote_and_cache(get_cached, get_cache_path): 80 | def get_remote_and_cache(): 81 | """ 82 | get newest data file from network and cache on local machine 83 | :return: a list contains all holiday data, element with datatime.date format 84 | """ 85 | response = requests.get( 86 | "https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/data.txt" 87 | ) 88 | cache_path = get_cache_path() 89 | 90 | with open(cache_path, "wb") as f: 91 | f.write(response.content) 92 | 93 | get_cached.cache_clear() 94 | 95 | return get_cached() 96 | 97 | return get_remote_and_cache 98 | 99 | 100 | def meta_get_remote_and_cache_with_half_day( 101 | get_cached_with_half_day, get_cache_path, data_file_name="data.txt" 102 | ): 103 | def get_remote_and_cache_with_half_day(): 104 | """ 105 | get newest data file from network and cache on local machine with half-day trading support 106 | :return: a tuple (holidays, half_days) where both are sets of datetime.date 107 | """ 108 | response = requests.get( 109 | f"https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/{data_file_name}" 110 | ) 111 | cache_path = get_cache_path() 112 | 113 | with open(cache_path, "wb") as f: 114 | f.write(response.content) 115 | 116 | get_cached_with_half_day.cache_clear() 117 | 118 | return get_cached_with_half_day() 119 | 120 | return get_remote_and_cache_with_half_day 121 | 122 | 123 | def meta_check_expired(get_cached): 124 | def check_expired(): 125 | """ 126 | check if local or cached data need update 127 | :return: true/false 128 | """ 129 | data = get_cached() 130 | now = datetime.datetime.now().date() 131 | for d in data: 132 | if d > now: 133 | return False 134 | return True 135 | 136 | return check_expired 137 | 138 | 139 | def meta_check_expired_with_half_day(get_cached_with_half_day): 140 | def check_expired_with_half_day(): 141 | """ 142 | check if local or cached data need update with half-day trading support 143 | :return: true/false 144 | """ 145 | holidays, half_days = get_cached_with_half_day() 146 | now = datetime.datetime.now().date() 147 | 148 | # Check holidays 149 | for d in holidays: 150 | if d > now: 151 | return False 152 | 153 | # Check half-days 154 | for d in half_days: 155 | if d > now: 156 | return False 157 | return True 158 | 159 | return check_expired_with_half_day 160 | 161 | 162 | def meta_sync_data(check_expired, get_remote_and_cache): 163 | def sync_data(): 164 | logging.basicConfig(level=logging.INFO) 165 | if check_expired(): 166 | logging.info("trying to fetch data...") 167 | get_remote_and_cache() 168 | logging.info("done") 169 | else: 170 | logging.info("local data is not exipired, do not fetch new data") 171 | 172 | return sync_data 173 | 174 | 175 | def meta_sync_data_with_half_day( 176 | check_expired_with_half_day, get_remote_and_cache_with_half_day 177 | ): 178 | def sync_data_with_half_day(): 179 | logging.basicConfig(level=logging.INFO) 180 | if check_expired_with_half_day(): 181 | logging.info("trying to fetch data...") 182 | get_remote_and_cache_with_half_day() 183 | logging.info("done") 184 | else: 185 | logging.info("local data is not expired, do not fetch new data") 186 | 187 | return sync_data_with_half_day 188 | 189 | 190 | def meta_get_cache_path(data_file_name="data.txt"): 191 | def get_cache_path(): 192 | usr_home = os.path.expanduser("~") 193 | cache_dir = os.path.join(usr_home, ".cn_stock_holidays") 194 | if not (os.path.isdir(cache_dir)): 195 | os.mkdir(cache_dir) 196 | return os.path.join(cache_dir, data_file_name) 197 | 198 | return get_cache_path 199 | 200 | 201 | def meta_is_trading_day(get_cached): 202 | def is_trading_day(dt): 203 | if dt is None: 204 | raise TypeError("Date cannot be None") 205 | 206 | if not isinstance(dt, (datetime.date, datetime.datetime)): 207 | raise TypeError("Date must be datetime.date or datetime.datetime") 208 | 209 | if type(dt) is datetime.datetime: 210 | dt = dt.date() 211 | 212 | if dt.weekday() >= 5: 213 | return False 214 | holidays = get_cached() 215 | if dt in holidays: 216 | return False 217 | return True 218 | 219 | return is_trading_day 220 | 221 | 222 | def meta_is_half_day_trading_day(get_cached_with_half_day): 223 | def is_half_day_trading_day(dt): 224 | """ 225 | Check if a given date is a half-day trading day 226 | :param dt: datetime.date or datetime.datetime 227 | :return: True if it's a half-day trading day, False otherwise 228 | """ 229 | if dt is None: 230 | raise TypeError("Date cannot be None") 231 | 232 | if not isinstance(dt, (datetime.date, datetime.datetime)): 233 | raise TypeError("Date must be datetime.date or datetime.datetime") 234 | 235 | if type(dt) is datetime.datetime: 236 | dt = dt.date() 237 | 238 | if dt.weekday() >= 5: 239 | return False 240 | 241 | holidays, half_days = get_cached_with_half_day() 242 | if dt in holidays: 243 | return False 244 | 245 | return dt in half_days 246 | 247 | return is_half_day_trading_day 248 | 249 | 250 | def meta_previous_trading_day(is_trading_day): 251 | def previous_trading_day(dt): 252 | if type(dt) is datetime.datetime: 253 | dt = dt.date() 254 | 255 | while True: 256 | dt = dt - datetime.timedelta(days=1) 257 | if is_trading_day(dt): 258 | return dt 259 | 260 | return previous_trading_day 261 | 262 | 263 | def meta_next_trading_day(is_trading_day): 264 | def next_trading_day(dt): 265 | if type(dt) is datetime.datetime: 266 | dt = dt.date() 267 | 268 | while True: 269 | dt = dt + datetime.timedelta(days=1) 270 | if is_trading_day(dt): 271 | return dt 272 | 273 | return next_trading_day 274 | 275 | 276 | def meta_trading_days_between(get_cached): 277 | def trading_days_between(start, end): 278 | if type(start) is datetime.datetime: 279 | start = start.date() 280 | 281 | if type(end) is datetime.datetime: 282 | end = end.date() 283 | 284 | dataset = get_cached() 285 | if start > end: 286 | return 287 | curdate = start 288 | while curdate <= end: 289 | if curdate.weekday() < 5 and not (curdate in dataset): 290 | yield curdate 291 | curdate = curdate + datetime.timedelta(days=1) 292 | 293 | return trading_days_between 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cn_stock_holidays 2 | 3 | ![CI Status](https://github.com/rainx/cn_stock_holidays/actions/workflows/ci.yml/badge.svg) 4 | 5 | A comprehensive Python package providing China stock exchange holiday data for both Shanghai/Shenzhen (SHSZ) and Hong Kong (HKEX) markets. This package serves as a reliable data source and utility library for financial applications that need to determine trading days. 6 | 7 | ## Features 8 | 9 | - **Dual Market Support**: Covers both mainland China and Hong Kong markets 10 | - **Multiple Data Sources**: Local files, cached data, and remote fetching 11 | - **Zipline Integration**: Provides exchange calendars for algorithmic trading 12 | - **CLI Tools**: Command-line utilities for data extraction 13 | - **Caching Mechanism**: LRU cache for performance optimization 14 | - **Comprehensive API**: Functions for trading day calculations 15 | 16 | ## Data Files 17 | 18 | ### Shanghai/Shenzhen Market 19 | 20 | ``` 21 | cn_stock_holidays/data.txt 22 | ``` 23 | 24 | ### Hong Kong Market 25 | 26 | ``` 27 | cn_stock_holidays/data_hk.txt 28 | ``` 29 | 30 | ### Fetch Data via URL 31 | 32 | ```bash 33 | # Shanghai/Shenzhen data 34 | wget https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/data.txt 35 | 36 | # Or using curl 37 | curl https://raw.githubusercontent.com/rainx/cn_stock_holidays/main/cn_stock_holidays/data.txt 38 | ``` 39 | 40 | ## Data Format 41 | 42 | ### Shanghai/Shenzhen Market 43 | 44 | The data files store all holidays for China stock exchanges (excluding regular weekend closures on Saturday and Sunday), with one date per line in the format: 45 | 46 | ``` 47 | YYYYMMDD 48 | ``` 49 | 50 | ### Hong Kong Market 51 | 52 | Hong Kong market data supports both regular holidays and half-day trading days. The format is: 53 | 54 | ``` 55 | YYYYMMDD # Regular holiday 56 | YYYYMMDD,h # Half-day trading day 57 | ``` 58 | 59 | **Half-day trading days** are days when the market is open for only part of the day (typically morning session only). Common half-day trading days in Hong Kong include: 60 | 61 | - Christmas Eve (December 24) 62 | - New Year's Eve (December 31) 63 | - Lunar New Year's Eve 64 | - Day before major holidays (Qingming Festival, National Day) 65 | 66 | **Important**: Half-day trading days are still considered trading days by all standard functions (`is_trading_day()`, `next_trading_day()`, etc.). Use `is_half_day_trading_day()` to specifically detect half-day trading days. 67 | 68 | ## Installation 69 | 70 | ### Using uv (Recommended) 71 | 72 | This project supports [uv](https://github.com/astral-sh/uv), a fast Python package installer: 73 | 74 | ```bash 75 | # Install uv first 76 | curl -LsSf https://astral.sh/uv/install.sh | sh 77 | 78 | # Install the package 79 | uv pip install cn-stock-holidays 80 | ``` 81 | 82 | ### Using pip 83 | 84 | ```bash 85 | pip install cn-stock-holidays 86 | ``` 87 | 88 | ### From source 89 | 90 | ```bash 91 | git clone https://github.com/rainx/cn_stock_holidays.git 92 | cd cn_stock_holidays 93 | uv sync --dev # Install with uv 94 | # or 95 | pip install -e . # Install with pip 96 | ``` 97 | 98 | ## Usage 99 | 100 | ### Import 101 | 102 | ```python 103 | # For Shanghai/Shenzhen market 104 | import cn_stock_holidays.data as shsz 105 | 106 | # For Hong Kong market 107 | import cn_stock_holidays.data_hk as hkex 108 | ``` 109 | 110 | ### Core Functions 111 | 112 | ```python 113 | # Get holiday data 114 | holidays = shsz.get_cached() # Get from cache or local file 115 | holidays = shsz.get_local() # Read from package data file 116 | holidays = shsz.get_remote_and_cache() # Fetch from network and cache 117 | 118 | # Trading day operations 119 | is_trading = shsz.is_trading_day(date) # Check if date is a trading day 120 | prev_day = shsz.previous_trading_day(date) # Get previous trading day 121 | next_day = shsz.next_trading_day(date) # Get next trading day 122 | 123 | # Get trading days in range 124 | for trading_day in shsz.trading_days_between(start_date, end_date): 125 | print(trading_day) 126 | 127 | # Data synchronization 128 | shsz.sync_data() # Sync data if expired 129 | shsz.check_expired() # Check if data needs update 130 | ``` 131 | 132 | ### Hong Kong Market with Half-Day Trading Support 133 | 134 | ```python 135 | # Import Hong Kong market functions 136 | import cn_stock_holidays.data_hk as hkex 137 | 138 | # Standard trading day functions (same as Shanghai/Shenzhen) 139 | is_trading = hkex.is_trading_day(date) 140 | prev_day = hkex.previous_trading_day(date) 141 | next_day = hkex.next_trading_day(date) 142 | 143 | # Half-day trading detection (Hong Kong market only) 144 | is_half_day = hkex.is_half_day_trading_day(date) # Check if date is a half-day trading day 145 | 146 | # Get data with half-day trading support 147 | holidays, half_days = hkex.get_cached_with_half_day() # Returns (holidays_set, half_days_set) 148 | 149 | # Data synchronization with half-day support 150 | hkex.sync_data_with_half_day() # Sync data if expired 151 | hkex.check_expired_with_half_day() # Check if data needs update 152 | ``` 153 | 154 | ### Function Details 155 | 156 | ```python 157 | Help on module cn_stock_holidays.data: 158 | 159 | FUNCTIONS 160 | check_expired() 161 | Check if local or cached data needs update 162 | :return: True/False 163 | 164 | get_cached() 165 | Get from cache version, if not existing, use txt file in package data 166 | :return: A set/list contains all holiday data, elements with datetime.date format 167 | 168 | get_local() 169 | Read data from package data file 170 | :return: A list contains all holiday data, elements with datetime.date format 171 | 172 | get_remote_and_cache() 173 | Get newest data file from network and cache on local machine 174 | :return: A list contains all holiday data, elements with datetime.date format 175 | 176 | is_trading_day(dt) 177 | :param dt: datetime.datetime or datetime.date 178 | :return: True if trading day, False otherwise 179 | 180 | next_trading_day(dt) 181 | :param dt: datetime.datetime or datetime.date 182 | :return: Next trading day as datetime.date 183 | 184 | previous_trading_day(dt) 185 | :param dt: datetime.datetime or datetime.date 186 | :return: Previous trading day as datetime.date 187 | 188 | sync_data() 189 | Synchronize data if expired 190 | 191 | trading_days_between(start, end) 192 | :param start, end: Start and end time, datetime.datetime or datetime.date 193 | :return: A generator for available trading dates in Chinese market 194 | ``` 195 | 196 | ### Cache Management 197 | 198 | From version 0.10 onwards, we use `functools.lru_cache` on `get_cached` for better performance. If needed, you can clear the cache using: 199 | 200 | ```python 201 | get_cached.cache_clear() 202 | ``` 203 | 204 | ## Command Line Tools 205 | 206 | ### Data Synchronization 207 | 208 | ```bash 209 | # Sync Shanghai/Shenzhen data 210 | cn-stock-holiday-sync 211 | 212 | # Sync Hong Kong data 213 | cn-stock-holiday-sync-hk 214 | ``` 215 | 216 | ### Get Trading Days List 217 | 218 | ```bash 219 | # Get trading days between dates 220 | get-day-list --start 2024-01-01 --end 2024-01-31 --daytype workday 221 | 222 | # Get holidays between dates 223 | get-day-list --start 2024-01-01 --end 2024-01-31 --daytype holiday 224 | 225 | # For Hong Kong market 226 | get-day-list --market hk --start 2024-01-01 --end 2024-01-31 --daytype workday 227 | ``` 228 | 229 | ## Keeping Data Up-to-Date 230 | 231 | The package includes scripts to check data expiration and fetch updates from the web. You can set up automatic updates using cron: 232 | 233 | ```crontab 234 | # Daily sync at midnight 235 | 0 0 * * * /usr/local/bin/cn-stock-holiday-sync > /tmp/cn_stock_holiday_sync.log 236 | ``` 237 | 238 | Find the absolute path of sync commands: 239 | 240 | ```bash 241 | # Shanghai/Shenzhen 242 | which cn-stock-holiday-sync 243 | 244 | # Hong Kong 245 | which cn-stock-holiday-sync-hk 246 | ``` 247 | 248 | ## Zipline Integration 249 | 250 | For algorithmic trading with Zipline: 251 | 252 | ```python 253 | from cn_stock_holidays.zipline import SHSZExchangeCalendar, HKExchangeCalendar 254 | 255 | # Use in Zipline 256 | calendar = SHSZExchangeCalendar() # Shanghai/Shenzhen 257 | calendar = HKExchangeCalendar() # Hong Kong 258 | ``` 259 | 260 | ## Development 261 | 262 | ### Setup Development Environment 263 | 264 | ```bash 265 | # Clone and setup 266 | git clone https://github.com/rainx/cn_stock_holidays.git 267 | cd cn_stock_holidays 268 | 269 | # Install with uv (recommended) 270 | uv sync --dev 271 | 272 | # Or with pip 273 | pip install -e .[dev] 274 | ``` 275 | 276 | ### Run Tests 277 | 278 | ```bash 279 | # Run all tests 280 | uv run pytest 281 | 282 | # Run with coverage 283 | uv run pytest --cov=cn_stock_holidays 284 | 285 | # Format code 286 | uv run black . 287 | 288 | # Type checking 289 | uv run mypy cn_stock_holidays/ 290 | ``` 291 | 292 | ### Publishing 293 | 294 | This project uses [PyPI Trusted Publisher](https://docs.pypi.org/trusted-publishers/) for secure automated publishing. The CI workflow automatically publishes to PyPI when a new tag is pushed. 295 | 296 | **To publish a new version:** 297 | 298 | 1. Update version in `pyproject.toml` 299 | 2. Create and push a new tag: 300 | ```bash 301 | git tag v2.0.1 302 | git push origin v2.0.1 303 | ``` 304 | 3. The CI workflow will automatically test, build, and publish to PyPI 305 | 306 | **Security Benefits:** 307 | 308 | - No need to manage long-lived API tokens 309 | - Short-lived authentication tokens (15 minutes) 310 | - Repository-specific permissions 311 | - Automated OIDC authentication 312 | 313 | See [Trusted Publisher Setup](docs/TRUSTED_PUBLISHER_SETUP.md) for detailed configuration instructions. 314 | 315 | ## Contributing 316 | 317 | 1. Fork the repository 318 | 2. Create a feature branch 319 | 3. Make your changes 320 | 4. Run tests and ensure code quality 321 | 5. Submit a pull request 322 | 323 | ## License 324 | 325 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 326 | 327 | ## Links 328 | 329 | - [GitHub Repository](https://github.com/rainx/cn_stock_holidays) 330 | - [PyPI Package](https://pypi.org/project/cn-stock-holidays/) 331 | - [UV Package Manager](https://github.com/astral-sh/uv) 332 | --------------------------------------------------------------------------------