Source code for vistock.tw

"""
tw.py - Module for handling Taiwan stock market data.

This module provides functionalities for interacting with various Taiwan Stock
markets,including the Taiwan Stock Exchange (TWSE), Taipei Exchange (TPEx), and
Emerging Stock Board (ESB). It supports converting stock symbols, fetching
stock data, and retrieving ticker lists from these markets.

Main Features:
--------------
- Convert Taiwan stock symbols to yfinance-compatible format.
- Fetch historical and real-time stock data from TWSE, TPEx, and ESB.
- Retrieve lists of tickers for TWSE, TPEx, and ESB.
- Find stocks similar to a given name or code.

Usage Examples:
---------------
::

    import tw

    # Get the stock name from its code
    name = tw.stock_name('2330.TW')

    # Convert a stock symbol to yfinance format
    yf_symbol = tw.as_yfinance('台積電')

    # Find stocks similar to a given name or code
    similar = tw.similar_stocks('印度')

    # Get a list of tickers for the TWSE
    twse_tickers = tw.get_twse_tickers()

    # Get a list of tickers for a specified market
    tickers = tw.get_tickers('TWSE')
"""
__version__ = "2.1"
__author__ = "York <york.jong@gmail.com>"
__date__ = "2023/02/19 (initial version) ~ 2024/09/14 (last revision)"

__all__ = [
    'is_taiwan_stock',
    'stock_name',
    'stock_price',
    'as_yfinance',
    'similar_stocks',
    'get_twse_tickers',
    'get_tpex_tickers',
    'get_esb_tickers',
    'get_tickers',
]

import functools
import unicodedata
import time

import requests
from urllib3.exceptions import IncompleteRead
from bs4 import BeautifulSoup


#------------------------------------------------------------------------------
# Utility Functions
#------------------------------------------------------------------------------

def is_chinese(string):
    """
    Check if the first character of given string is a Chinese character.

    This function checks if the first character in the input string is a
    Chinese character. It does not handle empty strings and will raise an
    `IndexError` if the input is an empty string.

    Args:
        string (str): The string to be checked.

    Returns:
        bool: True if the character is Chinese, False otherwise.

    Examples:
        >>> is_chinese('A')
        False
        >>> is_chinese('中')
        True
    """
    return unicodedata.category(string[0]) == 'Lo'


[docs] def is_taiwan_stock(ticker): """ Check if the given ticker represents a Taiwan stock. Args: ticker (str): Stock ticker symbol. Returns: bool: True if it's a Taiwan stock, False otherwise. Examples: >>> is_taiwan_stock('2330.TW') True >>> is_taiwan_stock('NVDA') False """ return ticker.endswith(('TW', 'TWO')) or ticker.isdigit()
#------------------------------------------------------------------------------ # Crawler #------------------------------------------------------------------------------ class Crawler: """ A class for crawling stock data from Taiwan stock market websites. This class provides methods to fetch stock information and convert stock symbols to yfinance compatible format. stock data is from 'https://isin.twse.com.tw/isin/C_public.jsp'. """ @staticmethod def _get_name_code_pair(symbol, str_mode): """Get (name, code) pair from a given symbol. Here the symbol may be a stock name or a stock code. Args: symbol (str): stock name or stock code. str_mode (int): a parameter of GET requests. Returns: (str, str): a name-code pair of first found stock. """ url = 'https://isin.twse.com.tw/isin/C_public.jsp' params = {'strMode': str_mode} response = requests.get(url, params=params) soup = BeautifulSoup(response.text, 'html5lib') #table = soup.find('tbody') #rows = table.find_all('tr') rows = soup.find_all('tr') for row in rows: #for row in rows[2:]: col0 = row.find('td') code_name = col0.text.split() if len(code_name) == 2: code, name = code_name if symbol == name or symbol == code: return name, code return None, None @staticmethod def as_yfinance(symbol): """ Convert a given stock symbol into yfinance compatible stock symbol. Args: symbol (str): the input symbol Returns: str: the yfinance compatible stock symbol. Examples: #>>> Crawler.as_yfinance("台積電") #'2330.TW' #>>> Crawler.as_yfinance("2330") #'2330.TW' #>>> Crawler.as_yfinance("元太") #'8069.TWO' """ # for a listed stock _, code = Crawler._get_name_code_pair(symbol, 2) if code: return f'{code}.TW' # for an OTC stock _, code = Crawler._get_name_code_pair(symbol, 4) if code: return f'{code}.TWO' # for an ESB stock. ESB stands for Emerging Stock Board (also called # Emerging Stock Market, or Emerging OTC) _, code = Crawler._get_name_code_pair(symbol, 5) if code: return f'{code}.TWO' return symbol #------------------------------------------------------------------------------ # Open API #------------------------------------------------------------------------------ class OpenAPI: """ A class for interacting with Taiwan stock market Open APIs. This class provides methods to fetch stock data, convert symbols, and retrieve stock information using various Open APIs. """ _lookup_cache = {} @classmethod def clear_cache(cls): cls._lookup_cache.clear() @classmethod def value_from_key(cls, key, url, key_field, value_field): """Get the value of a given key that is looked-up from an Open API response table. Args: key (str): a key to look-up url (str): the URL of an Open API request. key_field (str): the name of a key field. value_field (str): the name of a value field. Returns: str: the got value for success, None otherwise. """ cache_key = (url, key_field, value_field) if cache_key not in cls._lookup_cache: cols = cls.get_columns(url, [key_field, value_field]) cls._lookup_cache[cache_key] = dict(zip(*cols)) cache = cls._lookup_cache[cache_key] if key in cache: return cache[key] return None @classmethod def similar_keys(cls, key, url, key_field, value_field): """Get (key, value) pairs with similar keys that are looked-up from an Open API response table. Args: key (str): the key to look-up url (str): the URL of an Open API request. key_field (str): the name of a key field. value_field (str): the name of a value field. Returns: [(str, str)]: a list of key-value pairs representing the similar stocks. """ cache_key = (url, key_field, value_field) if cache_key not in cls._lookup_cache: cols = cls.get_columns(url, [key_field, value_field]) cls._lookup_cache[cache_key] = dict(zip(*cols)) cache = cls._lookup_cache[cache_key] pairs = [] for k, v in cache.items(): if key in k: pairs += [(k, v)] return pairs @staticmethod def get_columns(url, column_names): """ Fetch JSON data from the specified URL and extracts the specified columns. Args: url (str): The URL of the JSON data. column_names (list): A list of column names to extract. Returns: list: A list of the extracted columns, each column being a list. Returns a list of empty lists if an error occurs. """ for i in range(5): try: response = requests.get(url, timeout=60) response.raise_for_status() json_rows = response.json() except IncompleteRead as e: print(f"Attempt {i+1} failed, retrying...") time.sleep(2) # Wait a few seconds before retrying except Exception as e: print(f"{e}: {url}") return ([] for _ in column_names) columns = [] for col in column_names: columns.append([row[col] for row in json_rows]) return columns @classmethod def yfinance_symbol_from_name(cls, name): """Get yfinance compatible symbol from a Taiwan stock name. Args: name (str): the Taiwan stock name. Returns: str: the yfinance compatible symbol. Examples: >>> OpenAPI.yfinance_symbol_from_name("台積電") '2330.TW' >>> OpenAPI.yfinance_symbol_from_name("元太") '8069.TWO' >>> OpenAPI.yfinance_symbol_from_name("星宇航空") '2646.TWO' """ # for a listed stock listed_stock_code = functools.partial( cls.value_from_key, url='https://openapi.twse.com.tw/v1/' 'exchangeReport/STOCK_DAY_AVG_ALL', key_field='Name', value_field='Code' ) # for an OTC stock OTC_stock_code = functools.partial( cls.value_from_key, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_mainboard_daily_close_quotes', key_field='CompanyName', value_field='SecuritiesCompanyCode' ) # for an ESB stock. ESB stands for Emerging Stock Board (also called # Emerging Stock Market, or Emerging OTC) emerging_stock_code = functools.partial( cls.value_from_key, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_esb_latest_statistics', key_field='CompanyName', value_field='SecuritiesCompanyCode' ) code = listed_stock_code(name) if code: return f'{code}.TW' code = OTC_stock_code(name) if code: return f'{code}.TWO' code = emerging_stock_code(name) if code: return f'{code}.TWO' return name @classmethod def stock_name(cls, code): """Get stock name from its code. Args: code (str): a Taiwan stock code. Returns: str: the Taiwan stock name. Examples: >>> OpenAPI.stock_name('2330') '台積電' >>> OpenAPI.stock_name('8069') '元太' >>> OpenAPI.stock_name('2646') '星宇航空' """ code = code.replace('.TWO', '').replace('.TW', '') name = cls.listed_stock_name(code) if name: return name name = cls.OTC_stock_name(code) if name: return name name = cls.emerging_stock_name(code) if name: return name return code @classmethod def stock_price(cls, code): """Get stock price from its code. Args: code (str): a Taiwan stock code. Returns: str: the price of given code. Examples: >>> OpenAPI.stock_price("2330") > 900 True >>> OpenAPI.stock_price('8069') > 200 True >>> OpenAPI.stock_price('2646') > 10 True """ # for a listed stock listed_stock_price = functools.partial( cls.value_from_key, url='https://openapi.twse.com.tw/v1/' 'exchangeReport/STOCK_DAY_AVG_ALL', key_field='Code', value_field='ClosingPrice' ) # for an OTC stock OTC_stock_price = functools.partial( cls.value_from_key, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_mainboard_daily_close_quotes', key_field='SecuritiesCompanyCode', value_field='Close' ) # for an ESB stock. ESB stands for Emerging Stock Board (also called # Emerging Stock Market, or Emerging OTC) emerging_stock_price = functools.partial( cls.value_from_key, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_esb_latest_statistics', key_field='SecuritiesCompanyCode', value_field='LatestPrice' ) price = listed_stock_price(code) if price: return float(price) price = OTC_stock_price(code) if price: return float(price) price = emerging_stock_price(code) if price: return float(price) return "" @classmethod def yfinance_symbol_from_code(cls, code): """Get yfinance compatible symbol from a Taiwan stock code. Args: code (str): the Taiwan stock code. Returns: str: the yfinance compatible symbol. Examples: >>> OpenAPI.yfinance_symbol_from_code("2330") '2330.TW' >>> OpenAPI.yfinance_symbol_from_code("8069") '8069.TWO' >>> OpenAPI.yfinance_symbol_from_code("2646") '2646.TWO' """ name = cls.listed_stock_name(code) if name: return f'{code}.TW' name = cls.OTC_stock_name(code) if name: return f'{code}.TWO' name = cls.emerging_stock_name(code) if name: return f'{code}.TWO' return code # for a listed stock OpenAPI.listed_stock_name = functools.partial( OpenAPI.value_from_key, url='https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_AVG_ALL', key_field='Code', value_field='Name' ) # for an OTC stock OpenAPI.OTC_stock_name = functools.partial( OpenAPI.value_from_key, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_mainboard_daily_close_quotes', key_field='SecuritiesCompanyCode', value_field='CompanyName' ) # for an emerging stock OpenAPI.emerging_stock_name = functools.partial( OpenAPI.value_from_key, url='https://www.tpex.org.tw/openapi/v1/tpex_esb_latest_statistics', key_field='SecuritiesCompanyCode', value_field='CompanyName' ) #------------------------------------------------------------------------------ # Public Functions #------------------------------------------------------------------------------
[docs] def stock_name(code): """Get stock name from its code. Args: code (str): a Taiwan stock code. Returns: str: the Taiwan stock name. Examples: >>> stock_name('2330.TW') '台積電' >>> stock_name('8069.TWO') '元太' >>> stock_name('2646.TWO') '星宇航空' >>> stock_name('2330') '台積電' >>> stock_name('8069') '元太' >>> stock_name('2646') '星宇航空' """ return OpenAPI.stock_name(code)
[docs] def stock_price(code): """Get stock price from its code. Args: code (str): a Taiwan stock code. Returns: str: the price of given code. Examples: >>> stock_price("2330") > 900 True """ return OpenAPI.stock_price(code)
[docs] def as_yfinance(symbol): """ Convert a given stock symbol into yfinance compatible stock symbol. This function handles different types of input: - If the input is already in yfinance format (ends with .TW or .TWO), it's returned as is. - If the input is in Chinese, it's treated as a stock name and converted to a code. - If the input starts with a digit, it's treated as a Taiwan stock code and the appropriate suffix (.TW or .TWO) is added. - For other inputs (e.g., non-Taiwan stocks), the symbol is returned unchanged. Args: symbol (str): the input symbol. Returns: str: the yfinance compatible stock symbol. Examples: >>> as_yfinance('TSLA') 'TSLA' >>> as_yfinance('台積電') '2330.TW' >>> as_yfinance('2330') '2330.TW' >>> as_yfinance('元太') '8069.TWO' >>> as_yfinance('星宇航空') '2646.TWO' """ if symbol.endswith('.TW') or symbol.endswith('.TWO'): return symbol if is_chinese(symbol): #return Crawler.as_yfinance(symbol) return OpenAPI.yfinance_symbol_from_name(symbol) if symbol[0].isdigit(): #return Crawler.as_yfinance(symbol) return OpenAPI.yfinance_symbol_from_code(symbol) return symbol
[docs] def similar_stocks(symbol): """Get similar stock of a given stock. Args: symbol (str): a stock name or a stock code. Returns: [(str, str)]: a list of stock name-code pairs. Examples: >>> similar_stocks('印度') [('富邦印度', '00652'), ('富邦印度正2', '00653L'), ('富邦印度反1', '00654R')] >>> similar_stocks('永豐美國') [('永豐美國500大', '00858'), ('永豐美國科技', '00886')] """ # for listed stocks similar_listed_stocks = functools.partial( OpenAPI.similar_keys, url='https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_AVG_ALL', key_field='Name', value_field='Code' ) # for OTC stocks similar_OTC_stocks = functools.partial( OpenAPI.similar_keys, url='https://www.tpex.org.tw/openapi/v1/' 'tpex_mainboard_daily_close_quotes', key_field='CompanyName', value_field='SecuritiesCompanyCode' ) # for emerging stocks similar_emerging_stocks = functools.partial( OpenAPI.similar_keys, url='https://www.tpex.org.tw/openapi/v1/tpex_esb_latest_statistics', key_field='CompanyName', value_field='SecuritiesCompanyCode' ) name = OpenAPI.stock_name(symbol) stocks = similar_listed_stocks(name) if stocks: return stocks stocks = similar_OTC_stocks(name) if stocks: return stocks return similar_emerging_stocks(name)
#------------------------------------------------------------------------------
[docs] def get_twse_tickers(): """ Fetch the list of tickers for companies listed on the Taiwan Stock Exchange (TWSE). Retrieves ticker symbols from the TWSE Open API. Returns: list: A list of TWSE ticker symbols. Examples: >>> tickers = get_twse_tickers() >>> print(tickers[:5]) ['0050.TW', '0051.TW', '0052.TW', '0053.TW', '0055.TW'] """ url = "https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_ALL" (codes,) = OpenAPI.get_columns(url, ["Code"]) return [f'{c}.TW' for c in codes]
[docs] def get_tpex_tickers(): """ Fetch the list of tickers for companies listed on the Taipei Exchange (TPEx). Retrieves ticker symbols from the TPEx Open API. Returns: list: A list of TPEx ticker symbols. Examples: >>> tickers = get_tpex_tickers() >>> '8069.TWO' in tickers True """ url = "https://www.tpex.org.tw/openapi/v1/tpex_daily_market_value" (codes,) = OpenAPI.get_columns(url, ["SecuritiesCompanyCode"]) return [f'{c}.TWO' for c in codes]
[docs] def get_esb_tickers(): """ Fetch the list of tickers for companies listed on the Emerging Stock Board (ESB) of the Taipei Exchange. Retrieves ticker symbols from the TPEx Open API. Returns: list: A list of ESB ticker symbols. Examples: >>> tickers = get_esb_tickers() >>> '2646.TWO' in tickers True >>> '6987.TWO' in tickers True """ url = "https://www.tpex.org.tw/openapi/v1/tpex_esb_capitals_rank" (codes,) = OpenAPI.get_columns(url, ["SecuritiesCompanyCode"]) return [f'{c}.TWO' for c in codes]
[docs] def get_tickers(source): """ Retrieve a list of tickers for the specified exchange(s) in Taiwan, removing duplicates. Args: source (str): The common abbreviation(s) for the exchange or market sector. Possible values include: - 'TWSE': Taiwan Stock Exchange - 'TPEX': Taipei Exchange - 'ESB': Emerging Stock Board Can also be combined with '+' (e.g., 'TWSE+TPEX', 'TWSE+TPEX+ESB'). Returns: list: A list of unique tickers for the given exchange(s) or market sector(s). Raises: KeyError: If any of the provided exchange abbreviations are not recognized. Examples: >>> len(get_tickers('TWSE+TPEX')) >= 2000 True >>> len(get_tickers('ESB')) >= 300 True >>> len(get_tickers('TWSE+TPEX+ESB')) >= (1200 + 800 + 300) True >>> get_tickers('^UNKNOWN') Traceback (most recent call last): ... KeyError: "Exchange abbreviation '^UNKNOWN' not found." """ dic = { 'TWSE': get_twse_tickers, 'TPEX': get_tpex_tickers, 'ESB': get_esb_tickers, } sources = [s.strip().upper() for s in source.split('+')] tickers = set() for s in sources: if s in dic: tickers.update(dic[s]()) else: raise KeyError(f"Exchange abbreviation '{s}' not found.") return list(tickers)
#------------------------------------------------------------------------------ # Test #------------------------------------------------------------------------------ def use_requests_cache(): try: import requests_cache cache_installed = True except ImportError: cache_installed = False if cache_installed: requests_cache.install_cache('test_cache', expire_after=3600) if __name__ == '__main__': import doctest, time start_time = time.time() use_requests_cache() doctest.testmod() print(f"Execution time: {time.time() - start_time:.4f} seconds")