├── tests ├── __init__.py └── test_cli.py ├── koapy ├── utils │ ├── __init__.py │ ├── messaging │ │ ├── __init__.py │ │ ├── Messenger.py │ │ └── DiscordMessenger.py │ ├── logging │ │ ├── tqdm │ │ │ ├── __init__.py │ │ │ └── TqdmStreamHandler.py │ │ ├── pyside2 │ │ │ ├── __init__.py │ │ │ ├── QObjectLogging.py │ │ │ ├── QThreadLogging.py │ │ │ ├── QWidgetLogging.py │ │ │ └── QAbstractSocketLogging.py │ │ └── __init__.py │ ├── rate_limiting │ │ ├── __init__.py │ │ └── RateLimiter.py │ ├── store │ │ ├── misc │ │ │ ├── __init__.py │ │ │ └── VersionedItem.py │ │ ├── sqlalchemy │ │ │ ├── __init__.py │ │ │ ├── Session.py │ │ │ ├── Base.py │ │ │ ├── SnapshotAssociation.py │ │ │ ├── Timestamp.py │ │ │ ├── Version.py │ │ │ └── Snapshot.py │ │ ├── __init__.py │ │ └── SQLiteStore.py │ ├── builtin.py │ ├── ctypes.py │ ├── datetime.py │ ├── platform.py │ ├── notimplemented.py │ ├── itertools.py │ ├── data │ │ ├── __init__.py │ │ └── YahooFinanceKrxHistoricalDailyPriceDataDownloader.py │ ├── threading.py │ ├── exchange_calendars.py │ ├── grpc.py │ ├── pywinauto.py │ ├── networking.py │ ├── serialization.py │ └── recursion.py ├── backend │ ├── __init__.py │ ├── kiwoom_open_api_w │ │ ├── __init__.py │ │ └── core │ │ │ ├── __init__.py │ │ │ ├── KiwoomOpenApiWEntrypoint.py │ │ │ ├── KiwoomOpenApiWQAxWidgetMixin.py │ │ │ ├── KiwoomOpenApiWEventHandlerFunctions.py │ │ │ ├── KiwoomOpenApiWLoggingEventHandler.py │ │ │ ├── KiwoomOpenApiWDynamicCallable.py │ │ │ └── KiwoomOpenApiWSignalConnector.py │ ├── daishin_cybos_plus │ │ ├── __init__.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── CybosPlusDispatch.py │ │ │ ├── CybosPlusEntrypoint.py │ │ │ ├── CybosPlusTypeLibSpec.py │ │ │ └── CybosPlusError.py │ │ ├── proxy │ │ │ ├── __init__.py │ │ │ ├── CybosPlusDispatchProxyServiceServicer.py │ │ │ ├── CybosPlusEntrypointProxy.py │ │ │ └── CybosPlusDispatchProxyService.py │ │ ├── stub │ │ │ └── __init__.py │ │ └── tools │ │ │ ├── __init__.py │ │ │ └── generate_python_stubs.py │ └── kiwoom_open_api_plus │ │ ├── __init__.py │ │ ├── core │ │ ├── __init__.py │ │ ├── tools │ │ │ └── __init__.py │ │ ├── KiwoomOpenApiPlusEntrypointMixin.py │ │ ├── KiwoomOpenApiPlusTypeLib.py │ │ ├── KiwoomOpenApiPlusTypeLibSpec.py │ │ ├── KiwoomOpenApiPlusEventHandlerFunctions.py │ │ ├── KiwoomOpenApiPlusEventHandlerSignature.py │ │ ├── KiwoomOpenApiPlusDispatchSignature.py │ │ ├── KiwoomOpenApiPlusSignature.py │ │ └── KiwoomOpenApiPlusDynamicCallable.py │ │ ├── grpc │ │ ├── __init__.py │ │ ├── event │ │ │ ├── __init__.py │ │ │ ├── KiwoomOpenApiPlusEventHandlerForGrpc.py │ │ │ ├── KiwoomOpenApiPlusLoadConditionEventHandler.py │ │ │ └── KiwoomOpenApiPlusLoginEventHandler.py │ │ ├── tools │ │ │ ├── __init__.py │ │ │ └── compile_proto.py │ │ ├── KiwoomOpenApiPlusServiceMessageUtils.py │ │ └── KiwoomOpenApiPlusServiceClientSideDynamicCallable.py │ │ ├── utils │ │ ├── __init__.py │ │ ├── grpc │ │ │ ├── __init__.py │ │ │ └── PipeableMultiThreadedRendezvous.py │ │ ├── queue │ │ │ ├── __init__.py │ │ │ ├── QueueBasedBufferedIterator.py │ │ │ ├── QueueBasedIterableObserver.py │ │ │ └── QueueIterator.py │ │ ├── pyside2 │ │ │ └── __init__.py │ │ ├── module_path.py │ │ └── list_conversion.py │ │ ├── pyside2 │ │ ├── __init__.py │ │ └── KiwoomOpenApiPlusSignalHandler.py │ │ └── data │ │ ├── metadata │ │ └── fid.xlsx │ │ ├── scripts │ │ ├── install.iss │ │ ├── uninstall.iss │ │ └── setup.ps1 │ │ └── icon │ │ ├── external │ │ ├── github.png │ │ └── readthedocs.png │ │ ├── server │ │ ├── active │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── site.webmanifest │ │ │ └── about.txt │ │ ├── normal │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── site.webmanifest │ │ │ └── about.txt │ │ └── disabled │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── site.webmanifest │ │ │ └── about.txt │ │ └── manager │ │ ├── active │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── about.txt │ │ ├── disabled │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── about.txt │ │ └── normal │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── about.txt ├── backtrader │ ├── __init__.py │ ├── examples │ │ ├── __init__.py │ │ └── kiwoom_data_integration.py │ ├── SQLAlchemySQLiteData.py │ └── KrxTradingCalendar.py ├── cli │ ├── utils │ │ ├── __init__.py │ │ ├── cybos_plus.py │ │ ├── verbose_option.py │ │ ├── fail_with_usage.py │ │ ├── pywin32.py │ │ └── openapi.py │ ├── commands │ │ ├── __init__.py │ │ ├── get │ │ │ ├── account_data │ │ │ │ ├── __init__.py │ │ │ │ ├── deposit.py │ │ │ │ ├── userinfo.py │ │ │ │ ├── evaluation.py │ │ │ │ └── orders.py │ │ │ ├── chart_data │ │ │ │ ├── __init__.py │ │ │ │ └── daily.py │ │ │ ├── openapi_meta │ │ │ │ ├── __init__.py │ │ │ │ ├── modulepath.py │ │ │ │ ├── errmsg.py │ │ │ │ ├── realinfo.py │ │ │ │ └── trinfo.py │ │ │ ├── stock_meta │ │ │ │ ├── __init__.py │ │ │ │ ├── stockname.py │ │ │ │ ├── codelist.py │ │ │ │ └── stockinfo.py │ │ │ └── __init__.py │ │ ├── disable │ │ │ ├── __init__.py │ │ │ └── auto_login.py │ │ ├── enable │ │ │ ├── __init__.py │ │ │ └── auto_login.py │ │ ├── show │ │ │ ├── __init__.py │ │ │ └── account_window.py │ │ ├── generate │ │ │ ├── grpc │ │ │ │ └── __init__.py │ │ │ ├── openapi │ │ │ │ ├── __init__.py │ │ │ │ └── python_stubs.py │ │ │ └── __init__.py │ │ ├── install │ │ │ ├── __init__.py │ │ │ ├── pywin32.py │ │ │ └── openapi.py │ │ ├── uninstall │ │ │ ├── __init__.py │ │ │ ├── pywin32.py │ │ │ └── openapi.py │ │ ├── serve │ │ │ └── __init__.py │ │ └── update │ │ │ └── __init__.py │ ├── extensions │ │ ├── __init__.py │ │ └── functools.py │ └── __main__.py ├── compat │ ├── __init__.py │ ├── pyside2 │ │ ├── QtGui.py │ │ ├── QtCore.py │ │ ├── QtNetwork.py │ │ ├── QtWidgets.py │ │ ├── QtAxContainer.py │ │ └── __init__.py │ └── pywinauto │ │ ├── timings.py │ │ ├── findwindows.py │ │ ├── __init__.py │ │ └── importlib.py ├── examples │ ├── __init__.py │ ├── 04_koapy_tray_application.py │ ├── 01_roll_your_own_koapy.py │ ├── 00_roll_your_own_pyqt5.py │ ├── 00_roll_your_own_pyside2.py │ ├── 11_compare_codes.py │ ├── 12_grpc_client_auth.py │ ├── 05_koapy_entrypoint.py │ ├── 07_transaction_event.py │ ├── 08_check_account_status.py │ ├── 10_condition.py │ ├── 12_grpc_server_auth.py │ └── 09_get_historical_data.py ├── common │ ├── tools │ │ ├── __init__.py │ │ └── compile_proto.py │ ├── __init__.py │ ├── EventInstance.py │ ├── DispatchProxyServiceMessageUtils.py │ └── DispatchProxyService.proto └── config.conf ├── .pyup.yml ├── .github ├── ISSUE_TEMPLATE.md ├── actions │ └── check-version │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ └── action.yml ├── release-drafter.yml ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── release-drafter.yml │ ├── documentation.yml │ └── ci.yml ├── docs ├── source │ ├── _templates │ │ ├── .gitkeep │ │ └── layout.html │ ├── authors.rst │ ├── history.rst │ ├── notebooks_ipynb │ │ └── .gitignore │ ├── readme.rst │ ├── contributing.rst │ ├── notebooks │ │ ├── index.rst │ │ ├── koapy-tutorial.rst │ │ └── getting-historical-stock-price-data.rst │ ├── index.rst │ └── _static │ │ └── css │ │ └── style.css └── Makefile ├── .coveragerc ├── .gitattributes ├── .mypy.ini ├── pytest.ini ├── HISTORY.rst ├── .isort.cfg ├── .flake8 ├── .editorconfig ├── MANIFEST.in ├── SECURITY.md ├── AUTHORS.rst ├── .readthedocs.yaml ├── LICENSE ├── LICENSE.MIT ├── .bumpversion.cfg └── .pre-commit-config.yaml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | update: false 2 | -------------------------------------------------------------------------------- /koapy/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backtrader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/compat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/source/_templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/common/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/messaging/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backtrader/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/logging/tqdm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/rate_limiting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/store/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = koapy 3 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/logging/pyside2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/utils/store/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/account_data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/chart_data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/openapi_meta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/stock_meta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/**/*.ipynb linguist-documentation -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/stub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /docs/source/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/source/notebooks_ipynb/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | *.conf -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/core/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/event/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/pyside2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/queue/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/QtGui.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtGui import * 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = koapy tests docs 3 | -------------------------------------------------------------------------------- /.github/actions/check-version/.dockerignore: -------------------------------------------------------------------------------- 1 | /action.yml 2 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/pyside2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/QtCore.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtCore import * 2 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWEntrypoint.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/QtNetwork.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtNetwork import * 2 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/QtWidgets.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtWidgets import * 2 | -------------------------------------------------------------------------------- /koapy/compat/pywinauto/timings.py: -------------------------------------------------------------------------------- 1 | from pywinauto.timings import * 2 | -------------------------------------------------------------------------------- /koapy/utils/store/__init__.py: -------------------------------------------------------------------------------- 1 | from .SQLiteStore import SQLiteStore 2 | -------------------------------------------------------------------------------- /koapy/compat/pywinauto/findwindows.py: -------------------------------------------------------------------------------- 1 | from pywinauto.findwindows import * 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | # What's Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /koapy/cli/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /koapy/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .Dispatch import Dispatch 2 | from .EventInstance import EventInstance 3 | -------------------------------------------------------------------------------- /koapy/utils/store/sqlalchemy/Session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import sessionmaker 2 | 3 | Session = sessionmaker() 4 | -------------------------------------------------------------------------------- /koapy/utils/builtin.py: -------------------------------------------------------------------------------- 1 | def dir_public(*args): 2 | return [name for name in dir(*args) if not name.startswith("_")] 3 | -------------------------------------------------------------------------------- /koapy/utils/store/sqlalchemy/Base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWQAxWidgetMixin.py: -------------------------------------------------------------------------------- 1 | class KiwoomOpenApiWQAxWidgetMixin: 2 | 3 | pass 4 | -------------------------------------------------------------------------------- /koapy/utils/ctypes.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | 4 | def is_admin(): 5 | return ctypes.windll.shell32.IsUserAnAdmin() != 0 6 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.7.1 (2022-02-07) 6 | ------------------ 7 | 8 | * Latest release on PyPI. 9 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusEntrypointMixin.py: -------------------------------------------------------------------------------- 1 | class KiwoomOpenApiPlusEntrypointMixin: 2 | pass 3 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile = black 3 | multi_line_output = 3 4 | lines_between_types = 1 5 | extend_skip_glob = *_pb2.py,*_pb2_grpc.py 6 | -------------------------------------------------------------------------------- /koapy/compat/pywinauto/__init__.py: -------------------------------------------------------------------------------- 1 | from .importlib import PyWinAutoFinder 2 | 3 | PyWinAutoFinder.register() 4 | 5 | from pywinauto import * 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: elbakramer 4 | custom: ["https://toon.at/donate/637521884081401532"] 5 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/core/CybosPlusDispatch.py: -------------------------------------------------------------------------------- 1 | from koapy.common import Dispatch 2 | 3 | 4 | class CybosPlusDispatch(Dispatch): 5 | pass 6 | -------------------------------------------------------------------------------- /docs/source/notebooks/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Notebooks 3 | ========= 4 | 5 | .. toctree:: 6 | 7 | koapy-tutorial 8 | getting-historical-stock-price-data 9 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/metadata/fid.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/metadata/fid.xlsx -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/scripts/install.iss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/scripts/install.iss -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/scripts/uninstall.iss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/scripts/uninstall.iss -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-exclude = docs,*_pb2.py,*_pb2_grpc.py 3 | max-line-length = 88 4 | extend-ignore = E203 5 | count = true 6 | statistics = true 7 | show-source = true 8 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/external/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/external/github.png -------------------------------------------------------------------------------- /koapy/examples/04_koapy_tray_application.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from koapy import KiwoomOpenApiPlusManagerApplication 4 | 5 | KiwoomOpenApiPlusManagerApplication.main(sys.argv) 6 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/external/readthedocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/external/readthedocs.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon.ico -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/apple-touch-icon.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon-16x16.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/favicon-32x32.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/apple-touch-icon.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/apple-touch-icon.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/apple-touch-icon.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/apple-touch-icon.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/source/notebooks/koapy-tutorial.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | KOAPY Tutorial 3 | =================================== 4 | 5 | .. raw:: html 6 | :file: ../_static/html/notebooks/koapy-tutorial.html 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/active/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/android-chrome-192x192.png -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbakramer/koapy/HEAD/koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/android-chrome-512x512.png -------------------------------------------------------------------------------- /koapy/utils/messaging/Messenger.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Messenger(ABC): 5 | """ """ 6 | 7 | @abstractmethod 8 | def send_message(self, content): 9 | raise NotImplementedError 10 | -------------------------------------------------------------------------------- /koapy/cli/commands/disable/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .auto_login import auto_login 4 | 5 | 6 | @click.group(short_help="Disable things, including auto login.") 7 | def disable(): 8 | pass 9 | 10 | 11 | disable.add_command(auto_login) 12 | -------------------------------------------------------------------------------- /koapy/cli/commands/enable/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .auto_login import auto_login 4 | 5 | 6 | @click.group(short_help="Enable things, including auto login.") 7 | def enable(): 8 | pass 9 | 10 | 11 | enable.add_command(auto_login) 12 | -------------------------------------------------------------------------------- /koapy/cli/commands/show/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .account_window import account_window 4 | 5 | 6 | @click.group(short_help="Show some configuration windows.") 7 | def show(): 8 | pass 9 | 10 | 11 | show.add_command(account_window) 12 | -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {%- block extrahead %} 3 | {%- if google_site_verification %} 4 | 5 | {%- endif %} 6 | {% endblock %} -------------------------------------------------------------------------------- /koapy/cli/commands/generate/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .ssl_credentials import ssl_credentials 4 | 5 | 6 | @click.group(short_help="Generate grpc related files.") 7 | def grpc(): 8 | pass 9 | 10 | 11 | grpc.add_command(ssl_credentials) 12 | -------------------------------------------------------------------------------- /koapy/cli/commands/generate/openapi/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .python_stubs import python_stubs 4 | 5 | 6 | @click.group(short_help="Generate openapi related files.") 7 | def openapi(): 8 | pass 9 | 10 | 11 | openapi.add_command(python_stubs) 12 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/QtAxContainer.py: -------------------------------------------------------------------------------- 1 | from . import PYQT5, PYSIDE2, PythonQtError 2 | 3 | if PYQT5: 4 | from PyQt5.QAxContainer import * 5 | elif PYSIDE2: 6 | from PySide2.QtAxContainer import * 7 | else: 8 | raise PythonQtError("No Qt bindings could be found") 9 | -------------------------------------------------------------------------------- /koapy/cli/utils/cybos_plus.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def download_cybos_plus_installer(filepath): 5 | url = "https://www.daishin.com/install/CYBOS5.exe" 6 | response = requests.get(url) 7 | with open(filepath, "wb") as f: 8 | f.write(response.content) 9 | -------------------------------------------------------------------------------- /docs/source/notebooks/getting-historical-stock-price-data.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Getting historical stock price data 3 | =================================== 4 | 5 | .. raw:: html 6 | :file: ../_static/html/notebooks/getting-historical-stock-price-data.html 7 | -------------------------------------------------------------------------------- /koapy/utils/datetime.py: -------------------------------------------------------------------------------- 1 | def is_naive(dt): 2 | return dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None 3 | 4 | 5 | def localize(dt, tz): 6 | if is_naive(dt): 7 | dt = tz.localize(dt) 8 | else: 9 | dt = dt.astimezone(tz) 10 | return dt 11 | -------------------------------------------------------------------------------- /koapy/utils/platform.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | 4 | 5 | def is_windows(): 6 | return sys.platform == "win32" 7 | 8 | 9 | def is_32bit(): 10 | return platform.architecture()[0] == "32bit" 11 | 12 | 13 | def is_64bit(): 14 | return platform.architecture()[0] == "64bit" 15 | -------------------------------------------------------------------------------- /koapy/utils/notimplemented.py: -------------------------------------------------------------------------------- 1 | def notimplemented(func): 2 | func.__isnotimplemented__ = True 3 | return func 4 | 5 | 6 | def isnotimplemented(func): 7 | return getattr(func, "__isnotimplemented__", False) 8 | 9 | 10 | def isimplemented(func): 11 | return not isnotimplemented(func) 12 | -------------------------------------------------------------------------------- /koapy/cli/commands/generate/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .grpc import grpc 4 | from .openapi import openapi 5 | 6 | 7 | @click.group(short_help="Generate files related to grpc, openapi.") 8 | def generate(): 9 | pass 10 | 11 | 12 | generate.add_command(grpc) 13 | generate.add_command(openapi) 14 | -------------------------------------------------------------------------------- /koapy/cli/commands/install/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .openapi import openapi 4 | from .pywin32 import pywin32 5 | 6 | 7 | @click.group(short_help="Install openapi module and others.") 8 | def install(): 9 | pass 10 | 11 | 12 | install.add_command(openapi) 13 | install.add_command(pywin32) 14 | -------------------------------------------------------------------------------- /koapy/utils/itertools.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | 4 | def chunk(iterable, n): 5 | fillvalue = object() 6 | args = [iter(iterable)] * n 7 | it = itertools.zip_longest(fillvalue=fillvalue, *args) 8 | it = map(lambda items: [item for item in items if item is not fillvalue], it) 9 | return it 10 | -------------------------------------------------------------------------------- /.github/actions/check-version/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | RUN apt-get update && apt-get install -y git && apt-get clean 4 | RUN pip install click packaging dunamai actions-toolkit 5 | COPY docker-entrypoint.py /usr/local/bin/docker-entrypoint.py 6 | 7 | ENTRYPOINT ["python", "/usr/local/bin/docker-entrypoint.py"] 8 | -------------------------------------------------------------------------------- /koapy/utils/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .KrxHistoricalDailyPriceDataDownloader import KrxHistoricalDailyPriceDataDownloader 2 | from .KrxHistoricalDailyPriceDataForBacktestLoader import ( 3 | KrxHistoricalDailyPriceDataForBacktestLoader, 4 | ) 5 | from .KrxHistoricalDailyPriceDataLoader import KrxHistoricalDailyPriceDataLoader 6 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/cli/commands/uninstall/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .openapi import openapi 4 | from .pywin32 import pywin32 5 | 6 | 7 | @click.group(short_help="Uninstall openapi module and others.") 8 | def uninstall(): 9 | pass 10 | 11 | 12 | uninstall.add_command(openapi) 13 | uninstall.add_command(pywin32) 14 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /koapy/examples/01_roll_your_own_koapy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from koapy import KiwoomOpenApiPlusQAxWidget 4 | from koapy.compat.pyside2.QtWidgets import QApplication 5 | 6 | app = QApplication(sys.argv) 7 | control = KiwoomOpenApiPlusQAxWidget() 8 | 9 | APIModulePath = control.GetAPIModulePath() 10 | 11 | print(APIModulePath) 12 | -------------------------------------------------------------------------------- /koapy/examples/00_roll_your_own_pyqt5.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QAxContainer import QAxWidget 4 | from PyQt5.QtWidgets import QApplication 5 | 6 | app = QApplication(sys.argv) 7 | control = QAxWidget("{A1574A0D-6BFA-4BD7-9020-DED88711818D}") 8 | 9 | APIModulePath = control.dynamicCall("GetAPIModulePath()") 10 | 11 | print(APIModulePath) 12 | -------------------------------------------------------------------------------- /koapy/cli/commands/uninstall/pywin32.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.pywin32 import uninstall_pywin32 4 | from koapy.cli.utils.verbose_option import verbose_option 5 | 6 | 7 | @click.command(short_help="Uninstall pywin32 and run post uninstall script.") 8 | @verbose_option(default=5, show_default=True) 9 | def pywin32(): 10 | uninstall_pywin32() 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [LICENSE] 17 | insert_final_newline = false 18 | 19 | [*.bat] 20 | indent_style = tab 21 | end_of_line = crlf 22 | -------------------------------------------------------------------------------- /koapy/utils/logging/tqdm/TqdmStreamHandler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from logging import StreamHandler 4 | 5 | from tqdm.contrib import DummyTqdmFile 6 | 7 | 8 | class TqdmStreamHandler(StreamHandler): 9 | def __init__(self, stream=None): 10 | if stream is None: 11 | stream = sys.stderr 12 | stream = DummyTqdmFile(stream) 13 | StreamHandler.__init__(self, stream) 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include LICENSE.MIT 6 | include LICENSE.APACHE-2.0 7 | include LICENSE.GPL-3.0-OR-LATER 8 | include README.rst 9 | 10 | recursive-include tests * 11 | recursive-exclude * __pycache__ 12 | recursive-exclude * *.py[co] 13 | 14 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 15 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/openapi_meta/modulepath.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.verbose_option import verbose_option 4 | 5 | 6 | @click.command(short_help="Get OpenApi module installation path.") 7 | @verbose_option() 8 | def modulepath(): 9 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLibSpec import ( 10 | API_MODULE_PATH, 11 | ) 12 | 13 | click.echo(API_MODULE_PATH) 14 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/active/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/normal/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/active/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/disabled/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/server/normal/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/data/icon/manager/disabled/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to KOAPY's documentation! 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | contributing 12 | authors 13 | history 14 | 15 | notebooks/index 16 | autoapi/index 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /koapy/utils/logging/pyside2/QObjectLogging.py: -------------------------------------------------------------------------------- 1 | from koapy.compat.pyside2.QtCore import QObject 2 | from koapy.utils.logging.Logging import Logging 3 | 4 | 5 | class QObjectLoggingMeta(type(Logging), type(QObject)): 6 | pass 7 | 8 | 9 | class QObjectLogging(QObject, Logging, metaclass=QObjectLoggingMeta): 10 | def __init__(self, *args, **kwargs): 11 | QObject.__init__(self, *args, **kwargs) 12 | Logging.__init__(self) 13 | -------------------------------------------------------------------------------- /koapy/utils/logging/pyside2/QThreadLogging.py: -------------------------------------------------------------------------------- 1 | from koapy.compat.pyside2.QtCore import QThread 2 | from koapy.utils.logging.Logging import Logging 3 | 4 | 5 | class QThreadLoggingMeta(type(Logging), type(QThread)): 6 | pass 7 | 8 | 9 | class QThreadLogging(QThread, Logging, metaclass=QThreadLoggingMeta): 10 | def __init__(self, *args, **kwargs): 11 | QThread.__init__(self, *args, **kwargs) 12 | Logging.__init__(self) 13 | -------------------------------------------------------------------------------- /koapy/utils/logging/pyside2/QWidgetLogging.py: -------------------------------------------------------------------------------- 1 | from koapy.compat.pyside2.QtWidgets import QWidget 2 | from koapy.utils.logging.Logging import Logging 3 | 4 | 5 | class QWidgetLoggingMeta(type(Logging), type(QWidget)): 6 | pass 7 | 8 | 9 | class QWidgetLogging(QWidget, Logging, metaclass=QWidgetLoggingMeta): 10 | def __init__(self, *args, **kwargs): 11 | QWidget.__init__(self, *args, **kwargs) 12 | Logging.__init__(self) 13 | -------------------------------------------------------------------------------- /koapy/cli/commands/install/pywin32.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.pywin32 import install_pywin32 4 | from koapy.cli.utils.verbose_option import verbose_option 5 | 6 | 7 | @click.command(short_help="Install pywin32 and run post install script.") 8 | @click.option("--version", metavar="VERSION", help="Version of pywin32 to install.") 9 | @verbose_option(default=5, show_default=True) 10 | def pywin32(version): 11 | install_pywin32(version) 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | While this project is currently in alpha, 6 | no major security update supports are guaranteed. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | < 1.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Go to Issues and create a new issue about the vulnerability to report. 15 | Add `security` tag to the issue while creating the issue. 16 | -------------------------------------------------------------------------------- /koapy/backtrader/SQLAlchemySQLiteData.py: -------------------------------------------------------------------------------- 1 | from .SQLAlchemyData import SQLAlchemyData 2 | 3 | 4 | class SQLAlchemySQLiteData(SQLAlchemyData): 5 | 6 | # pylint: disable=no-member 7 | 8 | params = (("filename", None),) 9 | 10 | def __init__(self): 11 | if not self.p.url: 12 | self.p.url = "sqlite://" 13 | 14 | if self.p.filename: 15 | self.p.url += "/" + self.p.filename 16 | 17 | super().__init__() 18 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/module_path.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from pathlib import Path 4 | 5 | from deprecated import deprecated 6 | 7 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLibSpec import ( 8 | API_MODULE_PATH, 9 | ) 10 | 11 | 12 | @deprecated 13 | @functools.lru_cache() 14 | def GetAPIModulePath() -> Path: 15 | return API_MODULE_PATH 16 | 17 | 18 | if __name__ == "__main__": 19 | print(GetAPIModulePath()) 20 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Yunseong Hwang as `elbakramer`_ 9 | 10 | .. _`elbakramer`: https://github.com/elbakramer 11 | 12 | Contributors 13 | ------------ 14 | 15 | * Dongyoon Han as `dani-han`_ 16 | * `resoliwan`_ 17 | * Taehoon Lee as `taehoon1`_ 18 | 19 | .. _`dani-han`: https://github.com/dani-han 20 | .. _`resoliwan`: https://github.com/resoliwan 21 | .. _`taehoon1`: https://github.com/taehoon1 22 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/proxy/CybosPlusDispatchProxyServiceServicer.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pywintypes import IID 4 | 5 | from koapy.backend.daishin_cybos_plus.core.CybosPlusDispatch import CybosPlusDispatch 6 | from koapy.common.DispatchProxyServiceServicer import DispatchProxyServiceServicer 7 | 8 | 9 | class CybosPlusDispatchProxyServiceServicer(DispatchProxyServiceServicer): 10 | def _GetDispatch(self, iid: Union[IID, str]) -> CybosPlusDispatch: 11 | return CybosPlusDispatch(iid) 12 | -------------------------------------------------------------------------------- /koapy/utils/logging/pyside2/QAbstractSocketLogging.py: -------------------------------------------------------------------------------- 1 | from koapy.compat.pyside2.QtNetwork import QAbstractSocket 2 | from koapy.utils.logging.Logging import Logging 3 | 4 | 5 | class QAbstractSocketLoggingMeta(type(Logging), type(QAbstractSocket)): 6 | pass 7 | 8 | 9 | class QAbstractSocketLogging( 10 | QAbstractSocket, Logging, metaclass=QAbstractSocketLoggingMeta 11 | ): 12 | def __init__(self, *args, **kwargs): 13 | QAbstractSocket.__init__(self, *args, **kwargs) 14 | Logging.__init__(self) 15 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusTypeLib.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLibSpec import ( 2 | DISPATCH_CLSID, 3 | EVENT_CLSID, 4 | TYPELIB_SPEC, 5 | ) 6 | from koapy.utils.pywin32 import BuildOleItems, LoadTypeLib 7 | 8 | TYPELIB = LoadTypeLib(TYPELIB_SPEC) 9 | 10 | OLE_ITEMS, ENUM_ITEMS, RECORD_ITEMS, VTABLE_ITEMS = BuildOleItems(TYPELIB_SPEC, TYPELIB) 11 | 12 | DISPATCH_OLE_ITEM = OLE_ITEMS.get(DISPATCH_CLSID) 13 | EVENT_OLE_ITEM = OLE_ITEMS.get(EVENT_CLSID) 14 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/event/KiwoomOpenApiPlusEventHandlerForGrpc.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEventHandler import ( 2 | KiwoomOpenApiPlusEventHandler, 3 | ) 4 | 5 | 6 | class KiwoomOpenApiPlusEventHandlerForGrpc(KiwoomOpenApiPlusEventHandler): 7 | def __init__(self, control, context): 8 | super().__init__(control) 9 | 10 | self._context = context 11 | self._context.add_callback(self.stop) 12 | 13 | @property 14 | def context(self): 15 | return self._context 16 | -------------------------------------------------------------------------------- /koapy/cli/extensions/functools.py: -------------------------------------------------------------------------------- 1 | from functools import update_wrapper 2 | 3 | 4 | def update_wrapper_with_click_params(wrapper, wrapped, *args, **kwargs): 5 | click_params_wrapper = getattr(wrapper, "__click_params__", []) 6 | click_params_wrapped = getattr(wrapped, "__click_params__", []) 7 | click_params = click_params_wrapped + click_params_wrapper 8 | wrapper.__click_params__ = click_params 9 | wrapped.__click_params__ = click_params 10 | updated_wrapper = update_wrapper(wrapper, wrapped, *args, **kwargs) 11 | return updated_wrapper 12 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from click.testing import CliRunner 4 | 5 | from koapy import cli 6 | 7 | 8 | def test_cli(): 9 | runner = CliRunner() 10 | result = runner.invoke(cli.cli) 11 | assert result.exit_code == 0 12 | assert "Usage" in result.output 13 | help_result = runner.invoke(cli.cli, ["--help"]) 14 | assert help_result.exit_code == 0 15 | assert ( 16 | re.search(r"-h, --help[ ]* Show this message and exit.", help_result.output) 17 | is not None 18 | ) 19 | assert "serve" in help_result.output 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /koapy/utils/threading.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class Singleton: 5 | 6 | _instance = None 7 | _lock = threading.RLock() 8 | 9 | @classmethod 10 | def _get_instance_without_check(cls, *args, **kwargs): 11 | return cls._instance 12 | 13 | @classmethod 14 | def get_instance(cls, *args, **kwargs): 15 | with cls._lock: 16 | if not isinstance(cls._instance, cls): 17 | cls._instance = cls(*args, **kwargs) 18 | cls.get_instance = cls._get_instance_without_check 19 | return cls._instance 20 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/pyside2/KiwoomOpenApiPlusSignalHandler.py: -------------------------------------------------------------------------------- 1 | import signal as signal_module 2 | 3 | from koapy.backend.kiwoom_open_api_plus.utils.pyside2.QSignalHandler import ( 4 | QSignalHandler, 5 | ) 6 | 7 | 8 | class KiwoomOpenApiPlusSignalHandler(QSignalHandler): 9 | def __init__(self, app, parent=None): 10 | self._app = app 11 | self._parent = parent 12 | 13 | self._signals = [ 14 | signal_module.SIGINT, 15 | signal_module.SIGTERM, 16 | ] 17 | super().__init__(self._signals, self._parent) 18 | -------------------------------------------------------------------------------- /koapy/utils/store/sqlalchemy/SnapshotAssociation.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, ForeignKey, Integer 2 | from sqlalchemy.orm import relationship 3 | 4 | from .Base import Base 5 | 6 | 7 | class SnapshotAssociation(Base): 8 | __tablename__ = "snapshot_associations" 9 | 10 | snapshot_id = Column(Integer, ForeignKey("snapshots.id"), primary_key=True) 11 | version_id = Column(Integer, ForeignKey("versions.id"), primary_key=True) 12 | 13 | snapshot = relationship("Snapshot", back_populates="versions") 14 | version = relationship("Version", back_populates="snapshots") 15 | -------------------------------------------------------------------------------- /koapy/examples/00_roll_your_own_pyside2.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import PySide2 4 | 5 | QT_QPA_PLATFORM_PLUGIN_PATH = os.path.join( 6 | os.path.dirname(PySide2.__file__), "plugins", "platforms" 7 | ) 8 | os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QT_QPA_PLATFORM_PLUGIN_PATH 9 | 10 | import sys 11 | 12 | from PySide2.QtAxContainer import QAxWidget 13 | from PySide2.QtWidgets import QApplication 14 | 15 | app = QApplication(sys.argv) 16 | control = QAxWidget("{A1574A0D-6BFA-4BD7-9020-DED88711818D}") 17 | 18 | APIModulePath = control.dynamicCall("GetAPIModulePath()") 19 | 20 | print(APIModulePath) 21 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/core/CybosPlusEntrypoint.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.daishin_cybos_plus.core.CybosPlusDispatch import CybosPlusDispatch 2 | from koapy.backend.daishin_cybos_plus.core.CybosPlusEntrypointMixin import ( 3 | CybosPlusEntrypointMixin, 4 | ) 5 | from koapy.utils.platform import is_32bit 6 | 7 | 8 | class CybosPlusEntrypoint(CybosPlusEntrypointMixin): 9 | 10 | """ 11 | http://cybosplus.github.io/ 12 | """ 13 | 14 | def __init__(self): 15 | assert is_32bit(), "Control object should be created in 32bit environment" 16 | 17 | def __getitem__(self, name): 18 | return CybosPlusDispatch(name) 19 | -------------------------------------------------------------------------------- /koapy/cli/commands/show/account_window.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.verbose_option import verbose_option 4 | 5 | 6 | @click.command(short_help="Show configuration window for auto login.") 7 | @click.option( 8 | "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)." 9 | ) 10 | @verbose_option() 11 | def account_window(port): 12 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import ( 13 | KiwoomOpenApiPlusEntrypoint, 14 | ) 15 | 16 | with KiwoomOpenApiPlusEntrypoint(port=port) as context: 17 | context.EnsureConnected() 18 | context.ShowAccountWindow() 19 | -------------------------------------------------------------------------------- /koapy/cli/commands/enable/auto_login.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.verbose_option import verbose_option 4 | 5 | 6 | @click.command(short_help="Enable auto login.") 7 | @click.option( 8 | "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)." 9 | ) 10 | @verbose_option(default=5, show_default=True) 11 | def auto_login(port): 12 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import ( 13 | KiwoomOpenApiPlusEntrypoint, 14 | ) 15 | 16 | with KiwoomOpenApiPlusEntrypoint(port=port) as context: 17 | context.EnsureConnected() 18 | context.EnsureAutoLoginEnabled() 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: docs/source/conf.py 12 | fail_on_warning: false 13 | 14 | # Optionally build your docs in additional formats such as PDF 15 | formats: 16 | - htmlzip 17 | 18 | # Optionally set the version of Python and requirements required to build your docs 19 | python: 20 | version: 3.8 21 | install: 22 | - method: pip 23 | path: . 24 | - requirements: requirements_dev.txt 25 | system_packages: true 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/openapi_meta/errmsg.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.fail_with_usage import fail_with_usage 4 | from koapy.cli.utils.verbose_option import verbose_option 5 | 6 | 7 | @click.command(short_help="Get error message for error code.") 8 | @click.option("-e", "--err-code", metavar="ERR", type=int, help="Error code to check.") 9 | @verbose_option() 10 | def errmsg(err_code): 11 | if err_code is None: 12 | fail_with_usage() 13 | 14 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusError import ( 15 | KiwoomOpenApiPlusError, 16 | ) 17 | 18 | err_msg = KiwoomOpenApiPlusError.get_error_message_by_code(err_code) 19 | click.echo("[%d] %s" % (err_code, err_msg)) 20 | -------------------------------------------------------------------------------- /koapy/examples/11_compare_codes.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | from koapy import CybosPlusEntrypoint, KiwoomOpenApiPlusEntrypoint 4 | 5 | kiwoom = KiwoomOpenApiPlusEntrypoint() 6 | cybos = CybosPlusEntrypoint() 7 | 8 | kiwoom.EnsureConnected() 9 | cybos.EnsureConnected() 10 | 11 | kiwoom_codes = kiwoom.GetGeneralCodeList() 12 | cybos_codes = cybos.GetGeneralCodeList() 13 | 14 | cybos_codes = [code[1:] for code in cybos_codes] 15 | 16 | kiwoom_codes = pd.DataFrame(kiwoom_codes, columns=["code"]) 17 | kiwoom_codes["kiwoom"] = "TRUE" 18 | 19 | cybos_codes = pd.DataFrame(cybos_codes, columns=["code"]) 20 | cybos_codes["cybos"] = "TRUE" 21 | 22 | df = pd.merge(kiwoom_codes, cybos_codes, how="outer", on="code") 23 | 24 | df.to_excel("output.xlsx") 25 | -------------------------------------------------------------------------------- /koapy/examples/12_grpc_client_auth.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | import grpc 3 | 4 | from koapy.backend.kiwoom_open_api_plus.grpc.KiwoomOpenApiPlusServiceClient import ( 5 | KiwoomOpenApiPlusServiceClient, 6 | ) 7 | 8 | host = "localhost" 9 | port = 5943 10 | 11 | with open("server.crt", "rb") as f: 12 | server_crt = f.read() 13 | 14 | credentials = grpc.ssl_channel_credentials( 15 | root_certificates=server_crt, 16 | private_key=None, 17 | certificate_chain=None, 18 | ) 19 | 20 | client = KiwoomOpenApiPlusServiceClient( 21 | host=host, port=port, credentials=credentials 22 | ) 23 | client.EnsureConnected() 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /koapy/utils/store/misc/VersionedItem.py: -------------------------------------------------------------------------------- 1 | class VersionedItem: 2 | def __init__(self, library, symbol, version, timestamp, data, metadata): 3 | self.library = library 4 | self.symbol = symbol 5 | self.version = version 6 | self.timestamp = timestamp 7 | self.data = data 8 | self.metadata = metadata 9 | 10 | def __repr__(self): 11 | return ", metadata={!r})>".format( 12 | self.library, 13 | self.symbol, 14 | self.version, 15 | self.timestamp, 16 | type(self.data).__module__, 17 | type(self.data).__name__, 18 | hex(id(self.data)), 19 | self.metadata, 20 | ) 21 | -------------------------------------------------------------------------------- /koapy/utils/exchange_calendars.py: -------------------------------------------------------------------------------- 1 | from exchange_calendars import get_calendar 2 | from pandas import Timestamp 3 | 4 | name = "XKRX" 5 | calendar = get_calendar(name) 6 | day = calendar.day 7 | 8 | 9 | def is_currently_in_session() -> bool: 10 | now = Timestamp.now(calendar.tz).floor("T") 11 | previous_open = calendar.previous_open(now).astimezone(calendar.tz) 12 | next_close = calendar.next_close(previous_open).astimezone(calendar.tz) 13 | return previous_open <= now <= next_close 14 | 15 | 16 | def get_last_session_date() -> Timestamp: 17 | now = Timestamp.now(calendar.tz).floor("T") 18 | previous_close = calendar.previous_close(now).astimezone(calendar.tz) 19 | previous_open = calendar.previous_open(previous_close).astimezone(calendar.tz) 20 | return previous_open.normalize() 21 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWEventHandlerFunctions.py: -------------------------------------------------------------------------------- 1 | from koapy.utils.notimplemented import notimplemented 2 | 3 | 4 | class KiwoomOpenApiWEventHandlerFunctions: 5 | @notimplemented 6 | def OnEventConnect(self, errcode): 7 | raise NotImplementedError 8 | 9 | @notimplemented 10 | def OnReceiveMsg(self, scrnno, rqname, trcode, msg): 11 | raise NotImplementedError 12 | 13 | @notimplemented 14 | def OnReceiveTrData(self, scrnno, rqname, trcode, recordname, prevnext): 15 | raise NotImplementedError 16 | 17 | @notimplemented 18 | def OnReceiveRealData(self, code, realtype, realdata): 19 | raise NotImplementedError 20 | 21 | @notimplemented 22 | def OnReceiveChejanData(self, gubun, itemcnt, fidlist): 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /koapy/cli/utils/verbose_option.py: -------------------------------------------------------------------------------- 1 | from koapy.cli.extensions.verbose_option import ( 2 | verbose_option as extensions_verbose_option, 3 | ) 4 | from koapy.utils.logging import set_verbosity 5 | 6 | full_verbosity = 5 7 | 8 | 9 | def verbose_option_callback(ctx, param, value): 10 | set_verbosity(value) 11 | 12 | 13 | def verbose_option(*args, **kwargs): 14 | if "callback" not in kwargs: 15 | kwargs["callback"] = verbose_option_callback 16 | return extensions_verbose_option(*args, **kwargs) 17 | 18 | 19 | def full_verbose_option(*args, **kwargs): 20 | default_kwargs = dict( 21 | default=full_verbosity, 22 | show_default=True, 23 | expose_value=True, 24 | ) 25 | kwargs = dict(kwargs) 26 | kwargs.update(default_kwargs) 27 | return verbose_option(*args, **kwargs) 28 | -------------------------------------------------------------------------------- /koapy/examples/05_koapy_entrypoint.py: -------------------------------------------------------------------------------- 1 | # Test this example script with two different scenarios 2 | # 3 | # 1. External-Server-Process scenario 4 | # 1. Run 04_koapy_tray_application.py script in 32bit environment 5 | # => Server will start with tray application 6 | # 2. Open another console and run 05_koapy_entrypoint.py script (this one), in 32bit or 64bit environment 7 | # => Client will connect to the existing server 8 | # 9 | # 2. Server-In-Subprocess scenario 10 | # 1. Just run 05_koapy_entrypoint.py script (this one) in 32bit environment 11 | # => Server will start in subprocess and Client will connect to it 12 | 13 | from koapy import KiwoomOpenApiPlusEntrypoint 14 | 15 | entrypoint = KiwoomOpenApiPlusEntrypoint() 16 | 17 | APIModulePath = entrypoint.GetAPIModulePath() 18 | 19 | print(APIModulePath) 20 | -------------------------------------------------------------------------------- /koapy/utils/grpc.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import threading 3 | import warnings 4 | 5 | from pprint import PrettyPrinter 6 | 7 | import grpc 8 | 9 | from google.protobuf.json_format import MessageToDict 10 | 11 | 12 | @contextlib.contextmanager 13 | def warn_on_rpc_error_context(): 14 | try: 15 | yield 16 | except grpc.RpcError as e: 17 | warnings.warn(str(e)) 18 | 19 | 20 | def warn_on_rpc_error(stream): 21 | with warn_on_rpc_error_context(): 22 | for event in stream: 23 | yield event 24 | 25 | 26 | def cancel_after(stream, after): 27 | timer = threading.Timer(after, stream.cancel) 28 | timer.start() 29 | return warn_on_rpc_error(stream) 30 | 31 | 32 | def pprint_message(message): 33 | pp = PrettyPrinter() 34 | pp.pprint(MessageToDict(message, preserving_proto_field_name=True)) 35 | -------------------------------------------------------------------------------- /koapy/cli/utils/fail_with_usage.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | def get_help(ctx): 5 | return ctx.get_help() 6 | 7 | 8 | def get_help_without_usage(ctx): 9 | formatter = ctx.make_formatter() 10 | command = ctx.command 11 | command.format_help_text(ctx, formatter) 12 | command.format_options(ctx, formatter) 13 | command.format_epilog(ctx, formatter) 14 | help_without_usage = formatter.getvalue().rstrip("\n") 15 | return help_without_usage 16 | 17 | 18 | def fail_with_usage(message=None, ctx=None): 19 | if ctx is None: 20 | ctx = click.get_current_context() 21 | if message is not None: 22 | click.UsageError(message, ctx).show() 23 | click.echo() 24 | click.echo(get_help_without_usage(ctx)) 25 | else: 26 | click.echo(get_help(ctx)) 27 | ctx.exit(click.UsageError.exit_code) 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | static_html_notebooks: 16 | jupyter nbconvert --to html --output-dir=source/_static/html/notebooks/ source/notebooks_ipynb/*.ipynb 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /koapy/common/tools/compile_proto.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from pathlib import Path 5 | 6 | 7 | def main(): 8 | script_dir = Path(__file__).parent.resolve() 9 | proto_filedir = script_dir.parent.resolve() 10 | proto_filename = "DispatchProxyService.proto" 11 | project_dir = proto_filedir.parent.parent.resolve() 12 | proto_path = project_dir 13 | python_out = project_dir 14 | grpc_python_out = python_out 15 | proto_filepath = proto_filedir / proto_filename 16 | cmd = [ 17 | sys.executable, 18 | "-m", 19 | "grpc_tools.protoc", 20 | f"--proto_path={proto_path}", 21 | f"--python_out={python_out}", 22 | f"--grpc_python_out={grpc_python_out}", 23 | f"{proto_filepath}", 24 | ] 25 | return subprocess.check_call(cmd, cwd=project_dir) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /koapy/utils/pywinauto.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from typing import Callable, Optional, Sequence, TypeVar 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | def wait_any( 9 | waits: Sequence[Callable[[], T]], 10 | timeout: Optional[int] = None, 11 | retry_interval: Optional[int] = None, 12 | ) -> T: 13 | import pywinauto.timings 14 | 15 | if timeout is None: 16 | timeout = pywinauto.timings.Timings.window_find_timeout 17 | if retry_interval is None: 18 | retry_interval = pywinauto.timings.Timings.window_find_retry 19 | 20 | start_time = time.time() 21 | should_stop = False 22 | 23 | while not should_stop: 24 | for wait in waits: 25 | try: 26 | return wait() 27 | except pywinauto.timings.TimeoutError: 28 | elapsed_seconds = time.time() - start_time 29 | if elapsed_seconds > timeout: 30 | raise 31 | time.sleep(retry_interval) 32 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusTypeLibSpec.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pywintypes import IID 4 | 5 | from koapy.utils.pywin32 import GetLatestTypelibSpec, GetTypelibSpecs 6 | 7 | CONTROL_PROGID = "KHOPENAPI.KHOpenApiCtrl.1" 8 | 9 | CONTROL_CLSID = IID("{A1574A0D-6BFA-4BD7-9020-DED88711818D}") 10 | DISPATCH_CLSID = IID("{CF20FBB6-EDD4-4BE5-A473-FEF91977DEB6}") 11 | EVENT_CLSID = IID("{7335F12D-8973-4BD5-B7F0-12DF03D175B7}") 12 | MODULE_CLSID = IID("{6D8C2B4D-EF41-4750-8AD4-C299033833FB}") 13 | 14 | TYPELIB_SPECS = GetTypelibSpecs(MODULE_CLSID) 15 | TYPELIB_SPEC = GetLatestTypelibSpec(TYPELIB_SPECS) 16 | 17 | TYPELIB_DLL_PATH = TYPELIB_SPEC and TYPELIB_SPEC.dll and Path(TYPELIB_SPEC.dll) 18 | 19 | API_MODULE_PATH_FROM_DEFAULT_INSTALLATION = Path("C:\\OpenAPI") 20 | API_MODULE_PATH_FROM_TYPELIB_DLL = TYPELIB_DLL_PATH and TYPELIB_DLL_PATH.parent 21 | API_MODULE_PATH = ( 22 | API_MODULE_PATH_FROM_TYPELIB_DLL or API_MODULE_PATH_FROM_DEFAULT_INSTALLATION 23 | ) 24 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/grpc/PipeableMultiThreadedRendezvous.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | 3 | from grpc._channel import _MultiThreadedRendezvous as MultiThreadedRendezvous 4 | 5 | 6 | class PipeableMultiThreadedRendezvous(Iterator): 7 | def __init__(self, rendezvous, iterator=None): 8 | super().__init__() 9 | self._rendezvous = rendezvous 10 | self._iterator = iterator or self._rendezvous 11 | assert isinstance(self._rendezvous, MultiThreadedRendezvous) 12 | assert isinstance(self._iterator, Iterator) 13 | 14 | def __next__(self): 15 | return self._iterator.__next__() 16 | 17 | def pipe(self, func): 18 | rendezvous = self._rendezvous 19 | iterator = func(self._iterator) 20 | return type(self)(rendezvous, iterator) 21 | 22 | def __getattr__(self, name): 23 | try: 24 | return getattr(self._rendezvous, name) 25 | except AttributeError: 26 | return getattr(self._iterator, name) 27 | -------------------------------------------------------------------------------- /koapy/utils/networking.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import socket 3 | 4 | from contextlib import closing 5 | 6 | from wrapt import synchronized 7 | 8 | 9 | @synchronized 10 | def find_free_port_for_host(host): 11 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 12 | with closing(sock) as sock: 13 | sock.bind((host, 0)) 14 | host, port = sock.getsockname() 15 | return port 16 | 17 | 18 | def get_free_localhost_port(): 19 | return find_free_port_for_host("localhost") 20 | 21 | 22 | def is_in_private_network(host): 23 | host = socket.gethostbyname(host) 24 | ip_address = ipaddress.ip_address(host) 25 | private_networks = [ 26 | "10.0.0.0/8", 27 | "127.0.0.0/8", 28 | "172.16.0.0/12", 29 | "192.168.0.0/16", 30 | ] 31 | private_networks = [ipaddress.ip_network(network) for network in private_networks] 32 | for private_network in private_networks: 33 | if ip_address in private_network: 34 | return True 35 | return False 36 | -------------------------------------------------------------------------------- /koapy/utils/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from .Logging import Logging 2 | 3 | 4 | def get_logger(name=None): 5 | return Logging.get_logger(name) 6 | 7 | 8 | def verbosity_to_loglevel(verbosity): 9 | return Logging.verbosity_to_loglevel(verbosity) 10 | 11 | 12 | def loglevel_to_verbosity(loglevel): 13 | return Logging.loglevel_to_verbosity(loglevel) 14 | 15 | 16 | def get_package_logger(): 17 | root_package_name = __name__.split(".", maxsplit=1)[0] 18 | logger = get_logger(root_package_name) 19 | return logger 20 | 21 | 22 | def set_loglevel(loglevel): 23 | logger = get_package_logger() 24 | logger.setLevel(loglevel) 25 | 26 | 27 | def set_verbosity(verbosity): 28 | loglevel = verbosity_to_loglevel(verbosity) 29 | set_loglevel(loglevel) 30 | 31 | 32 | def get_loglevel(): 33 | logger = get_package_logger() 34 | loglevel = logger.level 35 | return loglevel 36 | 37 | 38 | def get_verbosity(): 39 | loglevel = get_loglevel() 40 | verbosity = loglevel_to_verbosity(loglevel) 41 | return verbosity 42 | -------------------------------------------------------------------------------- /koapy/compat/pyside2/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from koapy.config import config, debug 4 | from koapy.utils.logging import get_logger 5 | 6 | logger = get_logger(__name__) 7 | 8 | # Set QT_API environment variable for correct Qt backend usage 9 | os.environ["QT_API"] = config.get("koapy.qtpy.qt_api", "pyside2") 10 | 11 | # Import proper Qt binding using qtpy 12 | from qtpy import * 13 | from qtpy import PYQT5, PYSIDE2, PythonQtError 14 | 15 | # Check Qt backend 16 | if PYQT5: 17 | if debug: 18 | logger.debug("Using PyQt5 as Qt backend") 19 | elif PYSIDE2: 20 | if debug: 21 | logger.debug("Using PySide2 as Qt backend") 22 | else: 23 | raise PythonQtError("No Qt bindings could be found") 24 | 25 | # PySide2 patch 26 | if PYSIDE2: 27 | import PySide2 28 | 29 | if hasattr(PySide2, "__file__"): 30 | QT_QPA_PLATFORM_PLUGIN_PATH = os.path.join( 31 | os.path.dirname(PySide2.__file__), "plugins", "platforms" 32 | ) 33 | os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QT_QPA_PLATFORM_PLUGIN_PATH 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | If applicable, add commands, codes, logs to help explain what you've done. 21 | ``` 22 | Paste the command(s) you ran and the output. 23 | If there was a crash, please include the traceback here. 24 | ``` 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Screenshots** 30 | If applicable, add screenshots to help explain your problem. 31 | 32 | **Environment (please complete the following information):** 33 | - OS: [e.g. Windows 10] 34 | - Python Version: [e.g. Python 3.8 64Bit] 35 | - KOAPY Version: [e.g. 0.1.12] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/tools/compile_proto.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from pathlib import Path 4 | 5 | 6 | def compile_proto(): 7 | proto_filename = "KiwoomOpenApiPlusService.proto" 8 | file_path = Path(__file__) 9 | file_dir = file_path.parent 10 | project_dir = file_dir.parent.parent.parent.parent.parent 11 | proto_path = project_dir 12 | proto_filepath = ( 13 | proto_path 14 | / "koapy" 15 | / "backend" 16 | / "kiwoom_open_api_plus" 17 | / "grpc" 18 | / proto_filename 19 | ) 20 | python_out = project_dir 21 | grpc_python_out = python_out 22 | cmd = [ 23 | "python", 24 | "-m", 25 | "grpc_tools.protoc", 26 | "--proto_path=%s" % str(proto_path), 27 | "--python_out=%s" % str(python_out), 28 | "--grpc_python_out=%s" % str(grpc_python_out), 29 | str(proto_filepath), 30 | ] 31 | print(" ".join(cmd)) 32 | subprocess.check_call(cmd, cwd=project_dir) 33 | 34 | 35 | if __name__ == "__main__": 36 | compile_proto() 37 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/account_data/deposit.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.verbose_option import verbose_option 4 | from koapy.utils.logging import get_logger 5 | 6 | logger = get_logger(__name__) 7 | 8 | 9 | @click.command(short_help="Get account deposit.") 10 | @click.option("-a", "--account", metavar="ACCNO", help="Account number.") 11 | @click.option( 12 | "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)." 13 | ) 14 | @verbose_option() 15 | def deposit(account, port): 16 | if account is None: 17 | logger.info("Account not given. Using first account available.") 18 | 19 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import ( 20 | KiwoomOpenApiPlusEntrypoint, 21 | ) 22 | 23 | with KiwoomOpenApiPlusEntrypoint(port=port) as context: 24 | context.EnsureConnected() 25 | 26 | if account is None: 27 | account = context.GetAccountList()[0] 28 | 29 | result = context.GetDepositInfo(account) 30 | click.echo(result.to_markdown(floatfmt=".2f")) 31 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/KiwoomOpenApiPlusServiceMessageUtils.py: -------------------------------------------------------------------------------- 1 | def convert_arguments_from_protobuf_to_python(arguments): 2 | args = [] 3 | for argument in arguments: 4 | if argument.HasField("string_value"): 5 | args.append(argument.string_value) 6 | elif argument.HasField("bool_value"): 7 | args.append(argument.bool_value) 8 | elif argument.HasField("long_value"): 9 | args.append(argument.long_value) 10 | else: 11 | raise ValueError("No expecting field found.") 12 | return args 13 | 14 | 15 | def convert_arguments_from_python_to_protobuf(arguments, arguments_message): 16 | for i, arg in enumerate(arguments): 17 | if isinstance(arg, str): 18 | arguments_message.add().string_value = arg 19 | elif isinstance(arg, bool): 20 | arguments_message.add().bool_value = arg 21 | elif isinstance(arg, int): 22 | arguments_message.add().long_value = arg 23 | else: 24 | raise TypeError("Unexpected type for argument %d: %s" % (i, type(arg))) 25 | return arguments_message 26 | -------------------------------------------------------------------------------- /koapy/examples/07_transaction_event.py: -------------------------------------------------------------------------------- 1 | from koapy import KiwoomOpenApiPlusEntrypoint 2 | 3 | with KiwoomOpenApiPlusEntrypoint() as context: 4 | # 로그인 처리 5 | context.EnsureConnected() 6 | 7 | # 이벤트를 알아서 처리하고 결과물만 제공하는 상위 함수 사용 예시 8 | code = "005930" 9 | info = context.GetStockBasicInfoAsDict(code) 10 | print(info) 11 | price = info["현재가"] 12 | print(price) 13 | 14 | # 이벤트를 스트림으로 반환하는 하위 함수 직접 사용 예시 (위의 상위 함수 내부에서 실제로 처리되는 내용에 해당) 15 | rqname = "주식기본정보요청" 16 | trcode = "opt10001" 17 | screenno = "0001" 18 | inputs = {"종목코드": code} 19 | output = {} 20 | 21 | # 아래의 함수는 gRPC 서비스의 rpc 함수를 직접 호출함 22 | # 따라서 event 메시지의 구조는 koapy/backend/kiwoom_open_api_plus/grpc/KiwoomOpenApiPlusService.proto 파일 참조 23 | for event in context.TransactionCall(rqname, trcode, screenno, inputs): 24 | names = event.single_data.names 25 | values = event.single_data.values 26 | for name, value in zip(names, values): 27 | output[name] = value 28 | 29 | # 전체 결과 출력 (싱글데이터) 30 | print(output) 31 | 32 | # 현재가 값만 출력 33 | price = output["현재가"] 34 | print(price) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yunseong Hwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yunseong Hwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/core/CybosPlusTypeLibSpec.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pywintypes import IID 4 | 5 | from koapy.utils.pywin32 import GetLatestTypelibSpec, GetTypelibSpecs 6 | 7 | CPFOREDIB_CLSID = IID("{ABA39D6F-5AF4-4D10-8389-031055C13A75}") 8 | CPFORETRADE_CLSID = IID("{1E3BC2CB-4AC7-46BB-AF63-11DEA8628E3C}") 9 | 10 | CPSYSDIB_CLSID = IID("{9C31B76A-7189-49A3-9781-3C6DD6ED5AD3}") 11 | CPTRADE_CLSID = IID("{1F7D5E5A-05AB-4236-B6F3-3D383B09203A}") 12 | 13 | CPUTIL_CLSID = IID("{2DA9C35C-FE59-4A32-A942-325EE8A6F659}") 14 | DSCBO1_CLSID = IID("{859343F1-08FD-11D4-8231-00105A7C4F8C}") 15 | 16 | CPUTIL_TYPELIB_SPECS = GetTypelibSpecs(CPUTIL_CLSID) 17 | CPUTIL_TYPELIB_SPEC = GetLatestTypelibSpec(CPUTIL_TYPELIB_SPECS) 18 | 19 | CPUTIL_TYPELIB_DLL_PATH = ( 20 | CPUTIL_TYPELIB_SPEC and CPUTIL_TYPELIB_SPEC.dll and Path(CPUTIL_TYPELIB_SPEC.dll) 21 | ) 22 | 23 | INSTALLATION_PATH_FROM_DEFAULT_INSTALLATION = Path("C:\\DAISHIN") 24 | INSTALLATION_PATH_FROM_TYPELIB_DLL = ( 25 | CPUTIL_TYPELIB_DLL_PATH and CPUTIL_TYPELIB_DLL_PATH.parent.parent 26 | ) 27 | INSTALLATION_PATH = ( 28 | INSTALLATION_PATH_FROM_TYPELIB_DLL or INSTALLATION_PATH_FROM_DEFAULT_INSTALLATION 29 | ) 30 | -------------------------------------------------------------------------------- /koapy/common/EventInstance.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from threading import RLock 4 | from typing import Any, Callable, Generic, List, Optional 5 | 6 | try: 7 | from typing import ParamSpec 8 | except ImportError: 9 | from typing_extensions import ParamSpec 10 | 11 | P = ParamSpec("P") 12 | 13 | 14 | class EventInstance(Generic[P]): 15 | def __init__(self): 16 | self._lock = RLock() 17 | self._slots: List[Callable[P, Any]] = [] 18 | 19 | def connect(self, slot: Callable[P, Any]): 20 | with self._lock: 21 | if slot not in self._slots: 22 | self._slots.append(slot) 23 | 24 | def disconnect(self, slot: Optional[Callable[P, Any]] = None): 25 | with self._lock: 26 | if slot is None: 27 | self._slots.clear() 28 | elif slot in self._slots: 29 | self._slots.remove(slot) 30 | else: 31 | warnings.warn("Tried to disconnect a slot that doesn't exist") 32 | 33 | def __call__(self, *args: P.args, **kwargs: P.kwargs) -> None: 34 | with self._lock: 35 | slots = list(self._slots) 36 | for slot in slots: 37 | slot(*args, **kwargs) 38 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWLoggingEventHandler.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.kiwoom_open_api_w.core.KiwoomOpenApiWEventHandler import ( 2 | KiwoomOpenApiWEventHandler, 3 | ) 4 | from koapy.utils.logging.Logging import Logging 5 | 6 | 7 | class KiwoomOpenApiWLoggingEventHandler(KiwoomOpenApiWEventHandler, Logging): 8 | def OnReceiveTrData(self, scrnno, rqname, trcode, recordname, prevnext): 9 | self.logger.debug( 10 | "OnReceiveTrData(%r, %r, %r, %r, %r)", 11 | scrnno, 12 | rqname, 13 | trcode, 14 | recordname, 15 | prevnext, 16 | ) 17 | 18 | def OnReceiveRealData(self, code, realtype, realdata): 19 | self.logger.debug("OnReceiveRealData(%r, %r, %r)", code, realtype, realdata) 20 | 21 | def OnReceiveMsg(self, scrnno, rqname, trcode, msg): 22 | self.logger.debug("OnReceiveMsg(%r, %r, %r, %r)", scrnno, rqname, trcode, msg) 23 | 24 | def OnReceiveChejanData(self, gubun, itemcnt, fidlist): 25 | self.logger.debug("OnReceiveChejanData(%r, %r, %r)", gubun, itemcnt, fidlist) 26 | 27 | def OnEventConnect(self, errcode): 28 | self.logger.debug("OnEventConnect(%r)", errcode) 29 | -------------------------------------------------------------------------------- /koapy/cli/commands/get/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from .account_data.deposit import deposit 4 | from .account_data.evaluation import evaluation 5 | from .account_data.orders import orders 6 | from .account_data.userinfo import userinfo 7 | from .chart_data.daily import daily 8 | from .chart_data.minute import minute 9 | from .openapi_meta.errmsg import errmsg 10 | from .openapi_meta.modulepath import modulepath 11 | from .openapi_meta.realinfo import realinfo 12 | from .openapi_meta.trinfo import trinfo 13 | from .stock_meta.codelist import codelist 14 | from .stock_meta.stockcode import stockcode 15 | from .stock_meta.stockinfo import stockinfo 16 | from .stock_meta.stockname import stockname 17 | 18 | 19 | @click.group(short_help="Get various types of data.") 20 | def get(): 21 | pass 22 | 23 | 24 | get.add_command(deposit) 25 | get.add_command(evaluation) 26 | get.add_command(orders) 27 | get.add_command(userinfo) 28 | 29 | get.add_command(daily) 30 | get.add_command(minute) 31 | 32 | get.add_command(errmsg) 33 | get.add_command(modulepath) 34 | get.add_command(realinfo) 35 | get.add_command(trinfo) 36 | 37 | get.add_command(codelist) 38 | get.add_command(stockcode) 39 | get.add_command(stockinfo) 40 | get.add_command(stockname) 41 | -------------------------------------------------------------------------------- /koapy/backend/daishin_cybos_plus/tools/generate_python_stubs.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from pathlib import Path 4 | 5 | try: 6 | from ast import unparse 7 | except ImportError: 8 | from astunparse import unparse 9 | 10 | from koapy.backend.daishin_cybos_plus.core.CybosPlusTypeLibSpec import ( 11 | CPFOREDIB_CLSID, 12 | CPFORETRADE_CLSID, 13 | CPSYSDIB_CLSID, 14 | CPTRADE_CLSID, 15 | CPUTIL_CLSID, 16 | DSCBO1_CLSID, 17 | ) 18 | from koapy.common.StubGenerator import make_stub_module 19 | 20 | 21 | def main(): 22 | script_dir = Path(__file__).parent 23 | stub_dir = script_dir / ".." / "stub" 24 | modules = { 25 | "CpForeDib": CPFOREDIB_CLSID, 26 | "CpForeTrade": CPFORETRADE_CLSID, 27 | "CpSysDib": CPSYSDIB_CLSID, 28 | "CpTrade": CPTRADE_CLSID, 29 | "CpUtil": CPUTIL_CLSID, 30 | "DsCbo1": DSCBO1_CLSID, 31 | } 32 | for name, clsid in modules.items(): 33 | stub_filename = f"{name}.py" 34 | stub_filepath = stub_dir / stub_filename 35 | mod = make_stub_module(clsid) 36 | mod = ast.fix_missing_locations(mod) 37 | code = unparse(mod) 38 | with open(stub_filepath, "w", encoding="utf-8") as f: 39 | f.write(code) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Draft release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | create: 8 | branches: 9 | - master 10 | tags: 11 | - 'v[0-9]**' 12 | workflow_dispatch: 13 | inputs: 14 | is_postrelease: 15 | description: Whether this release is postrelease (default is prerelease) 16 | required: true 17 | default: false 18 | 19 | jobs: 20 | draft-release: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | ref: ${{ github.event.create.ref }} 26 | fetch-depth: 0 27 | - name: Check version 28 | id: check-version 29 | uses: ./.github/actions/check-version 30 | with: 31 | is_postrelease: ${{ github.event.inputs.is_postrelease }} 32 | - name: Draft release 33 | id: draft-release 34 | uses: release-drafter/release-drafter@v5 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | name: v${{ steps.check-version.outputs.version }} 39 | tag: v${{ steps.check-version.outputs.version }} 40 | version: ${{ steps.check-version.outputs.version }} 41 | publish: ${{ steps.check-version.outputs.is_finalrelease }} 42 | prerelease: ${{ steps.check-version.outputs.is_prerelease }} 43 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/event/KiwoomOpenApiPlusLoadConditionEventHandler.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusError import ( 2 | KiwoomOpenApiPlusError, 3 | ) 4 | from koapy.backend.kiwoom_open_api_plus.grpc import KiwoomOpenApiPlusService_pb2 5 | from koapy.backend.kiwoom_open_api_plus.grpc.event.KiwoomOpenApiPlusEventHandlerForGrpc import ( 6 | KiwoomOpenApiPlusEventHandlerForGrpc, 7 | ) 8 | 9 | 10 | class KiwoomOpenApiPlusLoadConditionEventHandler(KiwoomOpenApiPlusEventHandlerForGrpc): 11 | def __init__(self, control, context, request): 12 | super().__init__(control, context) 13 | self._request = request 14 | 15 | def on_enter(self): 16 | KiwoomOpenApiPlusError.try_or_raise_boolean( 17 | self.control.GetConditionLoad(), "Failed to load condition" 18 | ) 19 | 20 | def OnReceiveConditionVer(self, ret, msg): 21 | if ret != 1: 22 | error = KiwoomOpenApiPlusError(msg) 23 | self.observer.on_error(error) 24 | response = KiwoomOpenApiPlusService_pb2.ListenResponse() 25 | response.name = "OnReceiveConditionVer" 26 | response.arguments.add().long_value = ret 27 | response.arguments.add().string_value = msg 28 | self.observer.on_next(response) 29 | self.observer.on_completed() 30 | -------------------------------------------------------------------------------- /koapy/cli/commands/serve/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from koapy.cli.utils.grpc_options import grpc_server_and_client_options 6 | from koapy.cli.utils.verbose_option import full_verbose_option 7 | 8 | 9 | @click.command( 10 | context_settings=dict( 11 | ignore_unknown_options=True, 12 | ), 13 | short_help="Start grpc server with tray application.", 14 | ) 15 | @click.pass_context 16 | @grpc_server_and_client_options() 17 | @full_verbose_option() 18 | @click.argument("args", nargs=-1, type=click.UNPROCESSED) 19 | def serve( 20 | ctx, 21 | verbose, 22 | **kwargs, 23 | ): 24 | # reconstruct args so that only the first argument is program 25 | # and others are real arguments 26 | context_depth = 0 27 | c = ctx 28 | while c is not None: 29 | context_depth += 1 30 | c = c.parent 31 | args = sys.argv[:1] + sys.argv[context_depth:] 32 | 33 | # force verbosity of created applications to follow the cli option 34 | args.append("--verbose={}".format(verbose)) 35 | 36 | # call main function of manager application with the prepared args 37 | from koapy.backend.kiwoom_open_api_plus.pyside2.KiwoomOpenApiPlusManagerApplication import ( 38 | KiwoomOpenApiPlusManagerApplication, 39 | ) 40 | 41 | KiwoomOpenApiPlusManagerApplication.main(args) 42 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/utils/list_conversion.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from functools import wraps 4 | from inspect import signature 5 | 6 | 7 | def string_to_list(s, sep=";"): 8 | if s is None: 9 | return s 10 | if not isinstance(s, str) and isinstance(s, collections.abc.Iterable): 11 | return s 12 | return s.rstrip(sep).split(sep) if s else [] 13 | 14 | 15 | def list_to_string(l, sep=";"): 16 | if l is None: 17 | return l 18 | if isinstance(l, str): 19 | return l 20 | if not isinstance(l, collections.abc.Iterable): 21 | return l 22 | return sep.join(l) if l else "" 23 | 24 | 25 | def convert_list_arguments(*indices): 26 | def decorator(f): 27 | sig = signature(f) 28 | names = tuple(sig.parameters.keys()) 29 | 30 | @wraps(f) 31 | def wrapper(*args, **kwargs): 32 | ba = sig.bind(*args, **kwargs) 33 | ba.apply_defaults() 34 | for i in indices: 35 | if isinstance(i, int): 36 | i = names[i] 37 | if isinstance(i, str): 38 | ba.arguments[i] = list_to_string(ba.arguments[i]) 39 | args = ba.args 40 | kwargs = ba.kwargs 41 | return f(*args, **kwargs) 42 | 43 | return wrapper 44 | 45 | return decorator 46 | -------------------------------------------------------------------------------- /koapy/examples/08_check_account_status.py: -------------------------------------------------------------------------------- 1 | from koapy import KiwoomOpenApiPlusEntrypoint 2 | 3 | with KiwoomOpenApiPlusEntrypoint() as context: 4 | context.EnsureConnected() 5 | 6 | account_nos = context.GetAccountList() 7 | print("전체 계좌 목록: %s" % account_nos) 8 | 9 | account_no = account_nos[0] 10 | print("사용할 계좌번호: %s" % account_no) 11 | print() 12 | 13 | series = context.GetDepositInfo(account_no) 14 | print("예수금상세현황요청 : 예수금상세현황") 15 | print(series.to_markdown()) 16 | print() 17 | 18 | df = context.GetAccountRateOfReturnAsDataFrame(account_no) 19 | print("계좌수익률요청 : 계좌수익률") # TR 이름에서 그대로 따왔긴 한데, 정작 수익률은 그 어디에도 없음.. 20 | print(df.to_markdown()) 21 | print() 22 | 23 | summary, foreach = context.GetAccountEvaluationStatusAsSeriesAndDataFrame( 24 | account_no 25 | ) 26 | print("계좌평가현황요청 : 계좌평가현황") 27 | print(summary.to_markdown()) 28 | print("계좌평가현황요청 : 종목별계좌평가현황") 29 | print(foreach.to_markdown()) 30 | print() 31 | 32 | # 위와 아래의 차이는 수수료/세금 영향을 고려하냐 안하냐의 차이인듯, 위는 고려하지 않고 아래는 모두 고려하는것으로 보임 33 | 34 | summary, foreach = context.GetAccountEvaluationBalanceAsSeriesAndDataFrame( 35 | account_no 36 | ) 37 | print("계좌평가잔고내역요청 : 계좌평가결과") 38 | print(summary.to_markdown(disable_numparse=True)) 39 | print("계좌평가잔고내역요청 : 계좌평가잔고개별합산") 40 | print(foreach.to_markdown()) 41 | print() 42 | -------------------------------------------------------------------------------- /koapy/utils/messaging/DiscordMessenger.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from discord import RequestsWebhookAdapter, Webhook 6 | 7 | from koapy.config import config 8 | from koapy.utils.messaging.Messenger import Messenger 9 | 10 | 11 | class DiscordWebhookMessenger(Messenger): 12 | def __init__(self, url=None): 13 | self._url = url or config.get_string( 14 | "koapy.utils.messaging.discord.webhook_url" 15 | ) 16 | assert self._url is not None and len(self._url) > 0 17 | self._webhook = Webhook.from_url(self._url, adapter=RequestsWebhookAdapter()) 18 | 19 | def send_message(self, content): 20 | return self._webhook.send(content) 21 | 22 | 23 | class DoItYourselfDiscordWebhookMessenger(Messenger): 24 | def __init__(self, url=None): 25 | self._url = url or config.get_string( 26 | "koapy.utils.messaging.discord.webhook_url" 27 | ) 28 | assert self._url is not None and len(self._url) > 0 29 | 30 | def send_message(self, content): 31 | headers = { 32 | "Content-Type": "application/json", 33 | } 34 | data = { 35 | "content": content, 36 | } 37 | data = json.dumps(data) 38 | response = requests.post( 39 | self._url, headers=headers, data=data, params={"wait": "true"} 40 | ) 41 | return response 42 | -------------------------------------------------------------------------------- /koapy/common/DispatchProxyServiceMessageUtils.py: -------------------------------------------------------------------------------- 1 | def AssignValue(message, value): 2 | if value is None: 3 | pass 4 | elif isinstance(value, str): 5 | message.string_value = value 6 | elif isinstance(value, int): 7 | message.long_value = value 8 | elif isinstance(value, bool): 9 | message.bool_value = value 10 | elif isinstance(value, float): 11 | message.double_value = value 12 | elif isinstance(value, list): 13 | for item in value: 14 | AssignValue(message.list_value.values.add(), item) 15 | elif isinstance(value, tuple): 16 | for item in value: 17 | AssignValue(message.tuple_value.values.add(), item) 18 | else: 19 | raise TypeError 20 | return message 21 | 22 | 23 | def ExtractValue(message): 24 | if message.HasField("string_value"): 25 | return message.string_value 26 | elif message.HasField("long_value"): 27 | return message.long_value 28 | elif message.HasField("bool_value"): 29 | return message.bool_value 30 | elif message.HasField("double_value"): 31 | return message.double_value 32 | elif message.HasField("list_value"): 33 | return list(ExtractValue(value) for value in message.list_value.values) 34 | elif message.HasField("tuple_value"): 35 | return tuple(ExtractValue(value) for value in message.tuple_value.values) 36 | -------------------------------------------------------------------------------- /koapy/cli/commands/disable/auto_login.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from koapy.cli.utils.verbose_option import verbose_option 4 | 5 | 6 | def disable_auto_login_after_login(port): 7 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import ( 8 | KiwoomOpenApiPlusEntrypoint, 9 | ) 10 | 11 | with KiwoomOpenApiPlusEntrypoint(port=port) as context: 12 | context.EnsureConnected() 13 | context.DisableAutoLogin() 14 | 15 | 16 | def disable_auto_login_without_login(): 17 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusQAxWidgetMixin import ( 18 | KiwoomOpenApiPlusQAxWidgetUniversalMixin, 19 | ) 20 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLibSpec import ( 21 | API_MODULE_PATH, 22 | ) 23 | 24 | class GetAPIModulePathStub: 25 | def GetAPIModulePath(self): 26 | return API_MODULE_PATH 27 | 28 | class DisableAutoLoginStub( 29 | GetAPIModulePathStub, 30 | KiwoomOpenApiPlusQAxWidgetUniversalMixin, 31 | ): 32 | pass 33 | 34 | stub = DisableAutoLoginStub() 35 | stub.DisableAutoLogin() 36 | 37 | 38 | @click.command(short_help="Disable auto login.") 39 | @click.option( 40 | "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)." 41 | ) 42 | @verbose_option(default=5, show_default=True) 43 | def auto_login(port): 44 | disable_auto_login_without_login() 45 | -------------------------------------------------------------------------------- /koapy/backend/kiwoom_open_api_plus/grpc/KiwoomOpenApiPlusServiceClientSideDynamicCallable.py: -------------------------------------------------------------------------------- 1 | from koapy.backend.kiwoom_open_api_plus.grpc import KiwoomOpenApiPlusService_pb2 2 | from koapy.backend.kiwoom_open_api_plus.grpc.KiwoomOpenApiPlusServiceMessageUtils import ( 3 | convert_arguments_from_python_to_protobuf, 4 | ) 5 | 6 | 7 | class KiwoomOpenApiPlusServiceClientSideDynamicCallable: 8 | def __init__(self, stub, name): 9 | self._stub = stub 10 | self._name = name 11 | 12 | @classmethod 13 | def _create_call_request(cls, name, args): 14 | request = KiwoomOpenApiPlusService_pb2.CallRequest() 15 | request.name = name 16 | convert_arguments_from_python_to_protobuf(args, request.arguments) 17 | return request 18 | 19 | @classmethod 20 | def _unpack_response(cls, response): 21 | if response.return_value.HasField("string_value"): 22 | return response.return_value.string_value 23 | elif response.return_value.HasField("bool_value"): 24 | return response.return_value.bool_value 25 | elif response.return_value.HasField("long_value"): 26 | return response.return_value.long_value 27 | else: 28 | return None 29 | 30 | def __call__(self, *args): 31 | request = self._create_call_request(self._name, args) 32 | response = self._stub.Call(request) 33 | result = self._unpack_response(response) 34 | return result 35 | -------------------------------------------------------------------------------- /koapy/utils/serialization.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import os 4 | 5 | 6 | class JsonSerializable: 7 | def to_dict(self): 8 | return dict(self.__dict__) 9 | 10 | @classmethod 11 | def from_dict(cls, dic): 12 | output = cls() 13 | for name in output.__dict__: 14 | setattr(output, name, dic.get(name)) 15 | return output 16 | 17 | def to_json(self, f=None, encoding=None): 18 | if f is None: 19 | return json.dumps(self.to_dict()) 20 | elif isinstance(f, str): 21 | with open(f, "w", encoding=encoding) as f: 22 | return json.dump(self.to_dict(), f) 23 | elif isinstance(f, io.TextIOBase): 24 | return json.dump(self.to_dict(), f) 25 | else: 26 | raise ValueError("Unsupported argument type: %s" % type(f)) 27 | 28 | @classmethod 29 | def from_json(cls, jsn, encoding=None): 30 | if isinstance(jsn, str): 31 | if jsn.startswith("{") and jsn.endswith("}"): 32 | dic = json.loads(jsn) 33 | elif os.path.exists(jsn): 34 | with open(jsn, "r", encoding=encoding) as f: 35 | dic = json.load(f) 36 | else: 37 | dic = json.loads(jsn) 38 | elif isinstance(jsn, io.TextIOBase): 39 | dic = json.load(jsn) 40 | else: 41 | raise ValueError("Unsupported argument type: %s" % type(jsn)) 42 | return cls.from_dict(dic) 43 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.9.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)(\.(?P\d+))?(\.(?P\d+))?((?P(a|b|rc))(?P
\d+))?(\.post(?P\d+))?(\.dev(?P\d+))?(\+(?P.+))?
 6 | serialize = 
 7 | 	{major}.{minor}.{patch}{cycle}{pre}.post{post}.dev{dev}+{local}
 8 | 	{major}.{minor}.{patch}{cycle}{pre}.post{post}.dev{dev}
 9 | 	{major}.{minor}.{patch}{cycle}{pre}.post{post}+{local}
10 | 	{major}.{minor}.{patch}{cycle}{pre}.post{post}
11 | 	{major}.{minor}.{patch}{cycle}{pre}.dev{dev}+{local}
12 | 	{major}.{minor}.{patch}{cycle}{pre}.dev{dev}
13 | 	{major}.{minor}.{patch}{cycle}{pre}+{local}
14 | 	{major}.{minor}.{patch}{cycle}{pre}
15 | 	{major}.{minor}.{patch}.post{post}.dev{dev}+{local}
16 | 	{major}.{minor}.{patch}.post{post}.dev{dev}
17 | 	{major}.{minor}.{patch}.post{post}+{local}
18 | 	{major}.{minor}.{patch}.post{post}
19 | 	{major}.{minor}.{patch}.dev{dev}+{local}
20 | 	{major}.{minor}.{patch}.dev{dev}
21 | 	{major}.{minor}.{patch}+{local}
22 | 	{major}.{minor}.{patch}
23 | 
24 | [bumpversion:part:cycle]
25 | values = 
26 | 	final
27 | 	a
28 | 	b
29 | 	rc
30 | 
31 | [bumpversion:file:pyproject.toml]
32 | search = version = "{current_version}"
33 | replace = version = "{new_version}"
34 | 
35 | [bumpversion:file:koapy/__init__.py]
36 | search = __version__ = "{current_version}"
37 | replace = __version__ = "{new_version}"
38 | 
39 | [bumpversion:file:setup.py]
40 | search = version="{current_version}"
41 | replace = version="{new_version}"
42 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusEventHandlerFunctions.py:
--------------------------------------------------------------------------------
 1 | from koapy.utils.notimplemented import notimplemented
 2 | 
 3 | 
 4 | class KiwoomOpenApiPlusEventHandlerFunctions:
 5 |     @notimplemented
 6 |     def OnEventConnect(self, errcode):
 7 |         raise NotImplementedError
 8 | 
 9 |     @notimplemented
10 |     def OnReceiveMsg(self, scrnno, rqname, trcode, msg):
11 |         raise NotImplementedError
12 | 
13 |     @notimplemented
14 |     def OnReceiveTrData(
15 |         self,
16 |         scrnno,
17 |         rqname,
18 |         trcode,
19 |         recordname,
20 |         prevnext,
21 |         datalength,
22 |         errorcode,
23 |         message,
24 |         splmmsg,
25 |     ):
26 |         raise NotImplementedError
27 | 
28 |     @notimplemented
29 |     def OnReceiveRealData(self, code, realtype, realdata):
30 |         raise NotImplementedError
31 | 
32 |     @notimplemented
33 |     def OnReceiveChejanData(self, gubun, itemcnt, fidlist):
34 |         raise NotImplementedError
35 | 
36 |     @notimplemented
37 |     def OnReceiveConditionVer(self, ret, msg):
38 |         raise NotImplementedError
39 | 
40 |     @notimplemented
41 |     def OnReceiveTrCondition(
42 |         self, scrnno, codelist, condition_name, condition_index, prevnext
43 |     ):
44 |         raise NotImplementedError
45 | 
46 |     @notimplemented
47 |     def OnReceiveRealCondition(
48 |         self, code, condition_type, condition_name, condition_index
49 |     ):
50 |         raise NotImplementedError
51 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWDynamicCallable.py:
--------------------------------------------------------------------------------
 1 | from inspect import Signature
 2 | 
 3 | from koapy.backend.kiwoom_open_api_w.core.KiwoomOpenApiWSignature import (
 4 |     KiwoomOpenApiWDispatchSignature,
 5 | )
 6 | 
 7 | 
 8 | class KiwoomOpenApiWDynamicCallable:
 9 |     def __init__(self, control, name):
10 |         self._control = control
11 |         self._name = name
12 |         self._signature = KiwoomOpenApiWDispatchSignature.from_name(self._name)
13 |         self._function = self._signature.to_pyside2_function_prototype()
14 |         self._should_test_return_type = (
15 |             self._signature.return_annotation is not Signature.empty
16 |         )
17 | 
18 |     def is_valid_return_type(self, result):
19 |         if self._should_test_return_type and not isinstance(
20 |             result, self._signature.return_annotation
21 |         ):
22 |             return False
23 |         return True
24 | 
25 |     def __call__(self, *args, **kwargs):
26 |         ba = self._signature.bind(*args, **kwargs)
27 |         ba.apply_defaults()
28 |         result = self._control.dynamicCall(self._function, list(ba.args))
29 |         if not self.is_valid_return_type(result):
30 |             raise TypeError(
31 |                 "Return type of %s was expected for function call %s(...), but %s was found."
32 |                 % (
33 |                     self._signature.return_annotation,
34 |                     self._name,
35 |                     type(result),
36 |                 )
37 |             )
38 |         return result
39 | 


--------------------------------------------------------------------------------
/koapy/utils/data/YahooFinanceKrxHistoricalDailyPriceDataDownloader.py:
--------------------------------------------------------------------------------
 1 | import io
 2 | 
 3 | import pandas as pd
 4 | import requests
 5 | 
 6 | 
 7 | class YahooFinanceKrxHistoricalDailyPriceDataDownloader:
 8 |     def __init__(self):
 9 |         self._headers = {
10 |             "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
11 |         }
12 | 
13 |     def download(self, symbol, start_date=None, end_date=None):
14 |         if start_date is None:
15 |             start_date = pd.Timestamp(1980, 1, 1)
16 |         elif not isinstance(start_date, pd.Timestamp):
17 |             start_date = pd.Timestamp(start_date)
18 |         if end_date is None:
19 |             end_date = pd.Timestamp.now().normalize() + pd.Timedelta(1, unit="day")
20 |         elif not isinstance(end_date, pd.Timestamp):
21 |             end_date = pd.Timestamp(end_date)
22 | 
23 |         url = (
24 |             "https://query1.finance.yahoo.com/v7/finance/download/%s.KS"
25 |             % symbol.upper()
26 |         )
27 |         params = {
28 |             "period1": int(start_date.timestamp()),
29 |             "period2": int(end_date.timestamp()),
30 |             "interval": "1d",
31 |             "events": "history",
32 |             "includeAdjustedClose": "true",
33 |         }
34 |         response = requests.get(url, params=params, headers=self._headers)
35 |         df = pd.read_csv(
36 |             io.BytesIO(response.content), parse_dates=["Date"], index_col="Date"
37 |         )
38 | 
39 |         return df
40 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/account_data/userinfo.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | 
 5 | 
 6 | @click.command(short_help="Get user information.")
 7 | @click.option(
 8 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
 9 | )
10 | @verbose_option()
11 | def userinfo(port):
12 |     import pandas as pd
13 | 
14 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
15 |         KiwoomOpenApiPlusEntrypoint,
16 |     )
17 | 
18 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
19 |         context.EnsureConnected()
20 | 
21 |         result = {}
22 |         result["보유계좌수"] = context.GetLoginInfo("ACCOUNT_CNT")
23 |         account_numbers = context.GetLoginInfo("ACCLIST").rstrip(";").split(";")
24 |         for i, accno in enumerate(account_numbers):
25 |             result["계좌번호 (%d/%s)" % (i + 1, result["보유계좌수"])] = accno
26 |         result["사용자 ID"] = context.GetLoginInfo("USER_ID")
27 |         result["사용자 명"] = context.GetLoginInfo("USER_NAME")
28 |         result["키보드보안 해지 여부"] = {
29 |             "0": "정상",
30 |             "1": "해지",
31 |         }.get(context.GetLoginInfo("KEY_BSECGB"), "알수없음")
32 |         result["방화벽 설정 여부"] = {
33 |             "0": "미설정",
34 |             "1": "설정",
35 |             "2": "해지",
36 |         }.get(context.GetLoginInfo("FIREW_SECGB"), "알수없음")
37 |         result["접속서버 구분"] = {
38 |             "1": "모의투자",
39 |         }.get(context.GetServerGubun(), "실서버")
40 | 
41 |         click.echo(pd.Series(result).to_markdown())
42 | 


--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
 1 | # See https://pre-commit.com/ for usage and config
 2 | repos:
 3 | - repo: https://github.com/pre-commit/pre-commit-hooks
 4 |   rev: v3.4.0
 5 |   hooks:
 6 |   - id: check-yaml
 7 |   - id: check-toml
 8 | - repo: local
 9 |   hooks:
10 |   - id: pyupgrade
11 |     name: pyupgrade
12 |     stages: [commit]
13 |     language: system
14 |     entry: poetry run pyupgrade --keep-runtime-typing
15 |     types: [python]
16 |   - id: isort
17 |     name: isort
18 |     stages: [commit]
19 |     language: system
20 |     entry: poetry run isort
21 |     types_or: [cython, pyi, python]
22 |     args: [--filter-files]
23 |     require_serial: true
24 |   - id: black
25 |     name: black
26 |     stages: [commit]
27 |     language: system
28 |     entry: poetry run black
29 |     types_or: [python, pyi]
30 |     require_serial: true
31 |   - id: flake8
32 |     name: flake8
33 |     stages: [manual]
34 |     language: system
35 |     entry: poetry run flake8
36 |     types: [python]
37 |     require_serial: true
38 |   - id: pylint
39 |     name: pylint
40 |     stages: [manual]
41 |     language: system
42 |     entry: poetry run pylint
43 |     types: [python]
44 |     args: [--errors-only]
45 |     require_serial: true
46 |   - id: mypy
47 |     name: mypy
48 |     stages: [manual]
49 |     language: system
50 |     entry: poetry run mypy
51 |     types: [python]
52 |     require_serial: true
53 |   - id: pytest
54 |     name: pytest
55 |     stages: [push]
56 |     language: system
57 |     entry: poetry run pytest --cov
58 |     types: [python]
59 |     pass_filenames: false
60 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/stock_meta/stockname.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | 
 5 | 
 6 | @click.command(short_help="Get name for stock codes.")
 7 | @click.option(
 8 |     "-c",
 9 |     "--code",
10 |     "codes",
11 |     metavar="CODE",
12 |     multiple=True,
13 |     help="Stock code to get. Can set multiple times.",
14 | )
15 | @click.option(
16 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
17 | )
18 | @verbose_option()
19 | def stockname(codes, port):
20 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
21 |         KiwoomOpenApiPlusEntrypoint,
22 |     )
23 | 
24 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
25 |         context.EnsureConnected()
26 | 
27 |         def get_codes():
28 |             if codes:
29 |                 if "-" in codes:
30 |                     with click.open_file("-", "r") as f:
31 |                         for code in f:
32 |                             yield code.strip()
33 |                 else:
34 |                     for code in codes:
35 |                         yield code
36 |             else:
37 |                 while True:
38 |                     try:
39 |                         code = click.prompt("code", prompt_suffix=" >>> ")
40 |                         code = code.strip()
41 |                         if code == "exit":
42 |                             break
43 |                         if code:
44 |                             yield code
45 |                     except EOFError:
46 |                         break
47 | 
48 |         for code in get_codes():
49 |             name = context.GetMasterCodeName(code)
50 |             click.echo(name)
51 | 


--------------------------------------------------------------------------------
/koapy/backtrader/examples/kiwoom_data_integration.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | 
 3 | logging.basicConfig(
 4 |     format="%(asctime)s [%(levelname)s] %(message)s - %(filename)s:%(lineno)d",
 5 |     level=logging.DEBUG,
 6 | )
 7 | 
 8 | import backtrader as bt
 9 | 
10 | from koapy.backtrader.examples.vanila_quickstart import OrclStrategy
11 | from koapy.backtrader.KiwoomOpenApiPlusBroker import KiwoomOpenApiPlusCommInfo
12 | from koapy.backtrader.KiwoomOpenApiPlusStore import KiwoomOpenApiPlusStore
13 | 
14 | 
15 | def main():
16 |     logging.info("Creating Cerebro")
17 |     cerebro = bt.Cerebro()  # pylint: disable=unexpected-keyword-arg
18 | 
19 |     logging.info("Initializing Kiwoom Store")
20 |     kiwoomstore = KiwoomOpenApiPlusStore()
21 | 
22 |     logging.info("Getting data")
23 |     historial_data = kiwoomstore.getdata(dataname="005930", historical=True)
24 |     realtime_data = kiwoomstore.getdata(
25 |         dataname="005930",
26 |         backfill_start=False,
27 |         timeframe=bt.TimeFrame.Ticks,
28 |         compression=1,
29 |     )
30 | 
31 |     logging.info("Adding data")
32 |     cerebro.adddata(historial_data)
33 | 
34 |     logging.info("Configuring others")
35 |     cerebro.addsizer(bt.sizers.FixedSize, stake=10)
36 |     cerebro.addtz("Asia/Seoul")
37 | 
38 |     cerebro.broker.setcash(30000000.0)
39 |     cerebro.broker.addcommissioninfo(KiwoomOpenApiPlusCommInfo())
40 | 
41 |     logging.info("Setting strategy")
42 |     cerebro.addstrategy(OrclStrategy, printlog=True)
43 | 
44 |     logging.info("Starting Portfolio Value: %.2f", cerebro.broker.getvalue())
45 |     cerebro.run()
46 |     logging.info("Final Portfolio Value: %.2f", cerebro.broker.getvalue())
47 | 
48 | 
49 | if __name__ == "__main__":
50 |     main()
51 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/update/__init__.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.credentials import get_credentials
 4 | from koapy.cli.utils.verbose_option import verbose_option
 5 | 
 6 | 
 7 | @click.group(short_help="Update openapi module and metadata.")
 8 | def update():
 9 |     pass
10 | 
11 | 
12 | @update.command(short_help="Update openapi TR metadata.")
13 | @verbose_option()
14 | def trinfo():
15 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTrInfo import (
16 |         KiwoomOpenApiPlusTrInfo,
17 |     )
18 | 
19 |     KiwoomOpenApiPlusTrInfo.dump_trinfo_by_code()
20 | 
21 | 
22 | @update.command(short_help="Update openapi realtype metadata.")
23 | @verbose_option()
24 | def realtype():
25 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusRealType import (
26 |         KiwoomOpenApiPlusRealType,
27 |     )
28 | 
29 |     KiwoomOpenApiPlusRealType.dump_realtype_by_desc()
30 | 
31 | 
32 | @update.command(short_help="Update openapi module version.")
33 | @click.option(
34 |     "-i", "--interactive", is_flag=True, help="Put login information with prompts."
35 | )
36 | @verbose_option(default=5, show_default=True)
37 | def openapi(interactive):
38 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusVersionUpdater import (
39 |         KiwoomOpenApiPlusVersionUpdater,
40 |     )
41 | 
42 |     credentials = get_credentials(interactive)
43 |     updater = KiwoomOpenApiPlusVersionUpdater(credentials)
44 |     updater.update_version_if_necessary()
45 | 
46 | 
47 | @update.command(short_help="Update gRPC stub files by compiling proto files.")
48 | def proto():
49 |     from koapy.backend.kiwoom_open_api_plus.grpc.tools.compile_proto import (
50 |         compile_proto,
51 |     )
52 | 
53 |     compile_proto()
54 | 


--------------------------------------------------------------------------------
/koapy/utils/store/sqlalchemy/Timestamp.py:
--------------------------------------------------------------------------------
 1 | import datetime
 2 | import inspect
 3 | 
 4 | import pytz
 5 | 
 6 | from sqlalchemy import DateTime
 7 | from sqlalchemy.types import TypeDecorator
 8 | 
 9 | 
10 | class Timestamp(TypeDecorator):  # pylint: disable=abstract-method
11 | 
12 |     impl = DateTime
13 |     cache_ok = True
14 | 
15 |     signature = inspect.signature(impl)
16 | 
17 |     utc = datetime.timezone.utc
18 |     local_timezone = datetime.datetime.now().astimezone().tzinfo
19 | 
20 |     def __init__(self, *args, **kwargs):
21 |         self._timezone = self.local_timezone
22 |         bound = self.signature.bind(*args, **kwargs)
23 |         bound.apply_defaults()
24 |         if bound.arguments["timezone"]:
25 |             if not isinstance(bound.arguments["timezone"], bool):
26 |                 self._timezone = bound.arguments["timezone"]
27 |                 bound.arguments["timezone"] = True
28 |                 if isinstance(self._timezone, str):
29 |                     self._timezone = pytz.timezone(self._timezone)
30 |         super().__init__(*bound.args, **bound.kwargs)
31 | 
32 |     @classmethod
33 |     def is_naive(cls, value):
34 |         return value.tzinfo is None or value.tzinfo.utcoffset(value) is None
35 | 
36 |     def process_bind_param(self, value, dialect):
37 |         if self.is_naive(value):
38 |             value = value.replace(tzinfo=self.local_timezone)
39 |         return value.astimezone(self.utc)
40 | 
41 |     def process_result_value(self, value, dialect):
42 |         if self.is_naive(value):
43 |             value = value.replace(tzinfo=self.utc)
44 |         value = value.astimezone(self._timezone)
45 |         if not self.timezone:
46 |             value = value.replace(tzinfo=None)
47 |         return value
48 | 


--------------------------------------------------------------------------------
/.github/actions/check-version/action.yml:
--------------------------------------------------------------------------------
 1 | name: Check version
 2 | description: Chceck version status
 3 | inputs:
 4 |   vcs:
 5 |     description: vcs
 6 |     required: true
 7 |     default: any
 8 |   metadata:
 9 |     description: metadata
10 |     required: false
11 |   no_metadata:
12 |     description: no metadata
13 |     required: false
14 |   dirty:
15 |     description: dirty
16 |     required: false
17 |   tagged_metadata:
18 |     description: tagged metadata
19 |     required: false
20 |   pattern:
21 |     description: pattern
22 |     required: false
23 |   format:
24 |     description: format
25 |     required: false
26 |   style:
27 |     description: style
28 |     required: false
29 |   latest_tag:
30 |     description: latest tag
31 |     required: false
32 |   bump:
33 |     description: bump
34 |     required: false
35 |   tag_dir:
36 |     description: tag dir
37 |     required: false
38 |   is_postrelease:
39 |     description: is postrelease
40 |     required: false
41 | outputs:
42 |   version:
43 |     description: version
44 |   is_finalrelease:
45 |     description: is final release
46 |   public:
47 |     description: public
48 |   base_version:
49 |     description: base version
50 |   epoch:
51 |     description: epoch
52 |   release:
53 |     description: release
54 |   major:
55 |     description: major
56 |   minor:
57 |     description: minor
58 |   micro:
59 |     description: micro
60 |   local:
61 |     description: local
62 |   pre:
63 |     description: pre
64 |   is_prerelease:
65 |     description: is_prerelease
66 |   dev:
67 |     description: dev
68 |   is_devrelease:
69 |     description: is_devrelease
70 |   post:
71 |     description: post
72 |   is_postrelease:
73 |     description: is_postrelease
74 | runs:
75 |   using: docker
76 |   image: Dockerfile
77 | 


--------------------------------------------------------------------------------
/koapy/backend/daishin_cybos_plus/proxy/CybosPlusEntrypointProxy.py:
--------------------------------------------------------------------------------
 1 | from threading import RLock
 2 | 
 3 | import grpc
 4 | 
 5 | from koapy.backend.daishin_cybos_plus.core.CybosPlusEntrypointMixin import (
 6 |     CybosPlusEntrypointMixin,
 7 | )
 8 | from koapy.backend.daishin_cybos_plus.proxy.CybosPlusDispatchProxy import (
 9 |     CybosPlusDispatchProxy,
10 | )
11 | from koapy.common import DispatchProxyService_pb2, DispatchProxyService_pb2_grpc
12 | 
13 | 
14 | class CybosPlusEntrypointProxy(CybosPlusEntrypointMixin):
15 |     def __init__(self, host=None, port=None):
16 |         if host is None:
17 |             host = "localhost"
18 |         if port is None:
19 |             port = 3031
20 | 
21 |         self._host = host
22 |         self._port = port
23 | 
24 |         self._address = self._host + ":" + str(self._port)
25 |         self._channel = grpc.insecure_channel(self._address)
26 |         self._stub = DispatchProxyService_pb2_grpc.DispatchProxyServiceStub(
27 |             self._channel
28 |         )
29 | 
30 |         grpc.channel_ready_future(self._channel).result(timeout=5)
31 | 
32 |         self._lock = RLock()
33 |         self._dispatch_proxies = {}
34 | 
35 |     def __getitem__(self, name):
36 |         if name not in self._dispatch_proxies:
37 |             with self._lock:
38 |                 if name not in self._dispatch_proxies:
39 |                     request = DispatchProxyService_pb2.GetDispatchRequest()
40 |                     request.iid = name
41 |                     response = self._stub.GetDispatch(request)
42 |                     iid = response.iid
43 |                     proxy = CybosPlusDispatchProxy(iid, self._stub)
44 |                     self._dispatch_proxies[name] = proxy
45 |         proxy = self._dispatch_proxies[name]
46 |         return proxy
47 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/utils/queue/QueueBasedBufferedIterator.py:
--------------------------------------------------------------------------------
 1 | import sys
 2 | import threading
 3 | 
 4 | from queue import Queue
 5 | 
 6 | from koapy.backend.kiwoom_open_api_plus.utils.queue.QueueIterator import (
 7 |     BufferedQueueIterator,
 8 | )
 9 | from koapy.utils.logging.Logging import Logging
10 | 
11 | 
12 | class QueueBasedBufferedIterator(BufferedQueueIterator, Logging):
13 | 
14 |     _check_timeout = 1
15 |     _default_maxsize = 10
16 | 
17 |     def __init__(self, iterator, queue=None, maxsize=None):
18 |         if queue is None:
19 |             if maxsize is None:
20 |                 maxsize = self._default_maxsize
21 |             queue = Queue(maxsize)
22 | 
23 |         self._iterator = iterator
24 |         self._queue = queue
25 |         self._maxsize = maxsize
26 |         self._exc_info = None
27 | 
28 |         super().__init__(self._queue)
29 | 
30 |         self._thread = threading.Thread(target=self._consume_iterator, daemon=True)
31 |         self._thread.start()
32 | 
33 |     def _consume_iterator(self):
34 |         try:
35 |             for item in self._iterator:
36 |                 self._queue.put(item)
37 |         except Exception:  # pylint: disable=broad-except
38 |             self.logger.exception("Exception raised while consuming iterator")
39 |             self._exc_info = sys.exc_info()
40 | 
41 |     def next(self, block=True, timeout=None):
42 |         try:
43 |             item = super().next(block, timeout)
44 |         except StopIteration as e:
45 |             if self._exc_info is not None:
46 |                 raise self._exc_info[1] from e
47 |             else:
48 |                 raise e
49 |         else:
50 |             if self._exc_info is not None:
51 |                 raise self._exc_info[1]
52 |         return item
53 | 


--------------------------------------------------------------------------------
/koapy/cli/utils/pywin32.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import subprocess
 3 | import sys
 4 | import tempfile
 5 | 
 6 | import requests
 7 | 
 8 | from koapy.utils.logging import get_logger
 9 | 
10 | logger = get_logger(__name__)
11 | 
12 | 
13 | def get_pywin32_postinstall_script(filepath):
14 |     url = (
15 |         "https://raw.githubusercontent.com/mhammond/pywin32/main/pywin32_postinstall.py"
16 |     )
17 |     response = requests.get(url)
18 |     with open(filepath, "wb") as f:
19 |         f.write(response.content)
20 | 
21 | 
22 | def install_pywin32(version=None):
23 |     if version is None:
24 |         version = "304"
25 |     cmd = ["pip", "install", "pywin32>={}".format(version)]
26 |     logger.info("Running command: %s", subprocess.list2cmdline(cmd))
27 |     subprocess.check_call(cmd)
28 |     with tempfile.TemporaryDirectory() as tempdir:
29 |         script_filename = "pywin32_postinstall.py"
30 |         script_filepath = os.path.join(tempdir, script_filename)
31 |         get_pywin32_postinstall_script(script_filepath)
32 |         cmd = [sys.executable, script_filepath, "-install"]
33 |         logger.info("Running command: %s", subprocess.list2cmdline(cmd))
34 |         subprocess.check_call(cmd)
35 | 
36 | 
37 | def uninstall_pywin32():
38 |     with tempfile.TemporaryDirectory() as tempdir:
39 |         script_filename = "pywin32_postinstall.py"
40 |         script_filepath = os.path.join(tempdir, script_filename)
41 |         get_pywin32_postinstall_script(script_filepath)
42 |         cmd = [sys.executable, script_filepath, "-remove"]
43 |         logger.info("Running command: %s", subprocess.list2cmdline(cmd))
44 |         subprocess.check_call(cmd)
45 |     cmd = ["pip", "uninstall", "pywin32"]
46 |     logger.info("Running command: %s", subprocess.list2cmdline(cmd))
47 |     subprocess.check_call(cmd)
48 | 


--------------------------------------------------------------------------------
/koapy/backend/daishin_cybos_plus/proxy/CybosPlusDispatchProxyService.py:
--------------------------------------------------------------------------------
 1 | import sys
 2 | 
 3 | from concurrent.futures import ThreadPoolExecutor
 4 | 
 5 | import grpc
 6 | import pythoncom
 7 | 
 8 | from koapy.backend.daishin_cybos_plus.proxy.CybosPlusDispatchProxyServiceServicer import (
 9 |     CybosPlusDispatchProxyServiceServicer,
10 | )
11 | from koapy.common import DispatchProxyService_pb2_grpc
12 | 
13 | 
14 | class CybosPlusDispatchProxyService:
15 |     def __init__(self, host=None, port=None, max_workers=None):
16 |         if host is None:
17 |             host = "localhost"
18 |         if port is None:
19 |             port = 3031
20 | 
21 |         if max_workers is None:
22 |             max_workers = None
23 | 
24 |         self._host = host
25 |         self._port = port
26 |         self._max_workers = max_workers
27 | 
28 |         self._address = self._host + ":" + str(self._port)
29 |         self._servicer = CybosPlusDispatchProxyServiceServicer()
30 | 
31 |         def initializer():
32 |             flags = getattr(sys, "coinit_flags", pythoncom.COINIT_MULTITHREADED)
33 |             pythoncom.CoInitializeEx(flags)
34 | 
35 |         self._executor = ThreadPoolExecutor(
36 |             max_workers=self._max_workers,
37 |             initializer=initializer,
38 |         )
39 | 
40 |         self._server = grpc.server(self._executor)
41 |         DispatchProxyService_pb2_grpc.add_DispatchProxyServiceServicer_to_server(
42 |             self._servicer, self._server
43 |         )
44 |         self._server.add_insecure_port(self._address)
45 | 
46 |     def __getattr__(self, name):
47 |         return getattr(self._server, name)
48 | 
49 | 
50 | def main():
51 |     service = CybosPlusDispatchProxyService()
52 |     service.start()
53 |     service.wait_for_termination()
54 | 
55 | 
56 | if __name__ == "__main__":
57 |     main()
58 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/openapi_meta/realinfo.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | 
 5 | 
 6 | @click.command(short_help="Get real type info.")
 7 | @click.option(
 8 |     "-t",
 9 |     "--realtype",
10 |     "realtypes",
11 |     metavar="REALTYPE",
12 |     multiple=True,
13 |     help="Real type name to get (like 주식시세).",
14 | )
15 | @verbose_option()
16 | def realinfo(realtypes):
17 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusRealType import (
18 |         KiwoomOpenApiPlusRealType,
19 |     )
20 | 
21 |     def get_realtypes():
22 |         if realtypes:
23 |             if "-" in realtypes:
24 |                 with click.open_file("-", "r") as f:
25 |                     for realtype in f:
26 |                         yield realtype.strip()
27 |             else:
28 |                 for realtype in realtypes:
29 |                     yield realtype
30 |         else:
31 |             while True:
32 |                 try:
33 |                     realtype = click.prompt("realtype", prompt_suffix=" >>> ")
34 |                     realtype = realtype.strip()
35 |                     if realtype == "exit":
36 |                         break
37 |                     if realtype:
38 |                         yield realtype
39 |                 except EOFError:
40 |                     break
41 | 
42 |     for realtype in get_realtypes():
43 |         fids = KiwoomOpenApiPlusRealType.get_fids_by_realtype_name(realtype)
44 |         if fids:
45 |             names = [
46 |                 KiwoomOpenApiPlusRealType.Fid.get_name_by_fid(fid, str(fid))
47 |                 for fid in fids
48 |             ]
49 |             for fid, name in zip(fids, names):
50 |                 click.echo("  [{}] = {}".format(fid, name))
51 |         else:
52 |             click.echo("Given realtype is invalid")
53 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/grpc/event/KiwoomOpenApiPlusLoginEventHandler.py:
--------------------------------------------------------------------------------
 1 | from google.protobuf.json_format import MessageToDict
 2 | 
 3 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusError import (
 4 |     KiwoomOpenApiPlusError,
 5 |     KiwoomOpenApiPlusNegativeReturnCodeError,
 6 | )
 7 | from koapy.backend.kiwoom_open_api_plus.grpc import KiwoomOpenApiPlusService_pb2
 8 | from koapy.backend.kiwoom_open_api_plus.grpc.event.KiwoomOpenApiPlusEventHandlerForGrpc import (
 9 |     KiwoomOpenApiPlusEventHandlerForGrpc,
10 | )
11 | 
12 | 
13 | class KiwoomOpenApiPlusLoginEventHandler(KiwoomOpenApiPlusEventHandlerForGrpc):
14 |     def __init__(self, control, request, context):
15 |         super().__init__(control, context)
16 |         self._request = request
17 | 
18 |         if self._request.HasField("credentials"):
19 |             self._credentials = self._request.credentials
20 |             self._credentials = MessageToDict(
21 |                 self._credentials, preserving_proto_field_name=True
22 |             )
23 |         else:
24 |             self._credentials = None
25 | 
26 |     def on_enter(self):
27 |         if self._credentials is not None:
28 |             self.control.DisableAutoLogin()
29 |             KiwoomOpenApiPlusError.try_or_raise(self.control.CommConnect())
30 |             self.control.LoginUsingPywinauto(self._credentials)
31 |         else:
32 |             KiwoomOpenApiPlusError.try_or_raise(self.control.CommConnect())
33 | 
34 |     def OnEventConnect(self, errcode):
35 |         if errcode < 0:
36 |             error = KiwoomOpenApiPlusNegativeReturnCodeError(errcode)
37 |             self.observer.on_error(error)
38 |         response = KiwoomOpenApiPlusService_pb2.ListenResponse()
39 |         response.name = "OnEventConnect"
40 |         response.arguments.add().long_value = errcode
41 |         self.observer.on_next(response)
42 |         self.observer.on_completed()
43 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/stock_meta/codelist.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.commands.get.stock_meta.codelist_interactive import codelist_interactive
 4 | from koapy.cli.utils.verbose_option import verbose_option
 5 | 
 6 | market_codes = [
 7 |     "0",
 8 |     "10",
 9 |     "3",
10 |     "8",
11 |     "50",
12 |     "4",
13 |     "5",
14 |     "6",
15 |     "9",
16 |     "30",
17 |     "all",
18 | ]
19 | 
20 | 
21 | @click.command(short_help="Get stock codes.")
22 | @click.option(
23 |     "-m",
24 |     "--market",
25 |     "markets",
26 |     metavar="MARKET",
27 |     multiple=True,
28 |     type=click.Choice(market_codes, case_sensitive=False),
29 |     help="Stock market code to get. Can set multiple times.",
30 | )
31 | @click.option(
32 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
33 | )
34 | @verbose_option()
35 | def codelist(markets, port):
36 |     """
37 |     \b
38 |     Possible market codes are:
39 |       0 : 장내
40 |       10 : 코스닥
41 |       3 : ELW
42 |       8 : ETF
43 |       50 : KONEX
44 |       4 : 뮤추얼펀드
45 |       5 : 신주인수권
46 |       6 : 리츠
47 |       9 : 하이얼펀드
48 |       30 : K-OTC
49 |     """
50 | 
51 |     if not markets and click.get_text_stream("stdin").isatty():
52 |         return codelist_interactive()
53 | 
54 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
55 |         KiwoomOpenApiPlusEntrypoint,
56 |     )
57 | 
58 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
59 |         context.EnsureConnected()
60 |         codes = set()
61 |         for market in markets:
62 |             codes = codes.union(set(context.GetCodeListByMarketAsList(market)))
63 |         codes = sorted(list(codes))
64 |         for code in codes:
65 |             click.echo(code)
66 | 
67 | 
68 | def main():
69 |     codelist()  # pylint: disable=no-value-for-parameter
70 | 
71 | 
72 | if __name__ == "__main__":
73 |     main()
74 | 


--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
 1 | name: Documentation
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |     - master
 7 |     paths:
 8 |     - '**.rst'
 9 |     - 'docs/*'
10 |     - '.github/workflows/documentation.yml'
11 |   workflow_dispatch:
12 |     inputs:
13 |       force_orphan:
14 |         description: Force orphan
15 |         required: false
16 |         default: false
17 | 
18 | jobs:
19 |   build-and-publish:
20 |     runs-on: ubuntu-latest
21 |     steps:
22 |     - uses: actions/checkout@v3
23 |     - name: Set up Python
24 |       uses: actions/setup-python@v4
25 |       id: setup-python
26 |       with:
27 |         python-version: 3.8
28 |     - name: Install poetry
29 |       run: |
30 |         python -m pip install --upgrade pip
31 |         pip install pipx
32 |         pipx install poetry
33 |     - name: Get poetry cache dir
34 |       id: poetry-cache
35 |       run: |
36 |         echo "::set-output name=dir::$(poetry config cache-dir)"
37 |     - name: Restore cache
38 |       uses: actions/cache@v3
39 |       with:
40 |         path: ${{ steps.poetry-cache.outputs.dir }}
41 |         key: ${{ runner.os }}-poetry-py${{ steps.setup-python.outputs.python-version }}-x64-${{ hashFiles('poetry.lock') }}
42 |         restore-keys: |
43 |           ${{ runner.os }}-poetry-py${{ steps.setup-python.outputs.python-version }}-x64-
44 |     - name: Install dependencies
45 |       run: |
46 |         poetry install
47 |         sudo apt-get update && sudo apt-get install pandoc
48 |     - name: Build documentation
49 |       run: |
50 |         poetry run make -C docs html
51 |     - name: Publish documentaion to Github Pages
52 |       uses: peaceiris/actions-gh-pages@v3
53 |       with:
54 |         github_token: ${{ secrets.GITHUB_TOKEN }}
55 |         publish_dir: ./docs/build/html/
56 |         force_orphan: ${{ github.event.inputs.force_orphan }}
57 |         commit_message: ${{ github.event.head_commit.message }}
58 | 


--------------------------------------------------------------------------------
/koapy/backtrader/KrxTradingCalendar.py:
--------------------------------------------------------------------------------
 1 | import datetime
 2 | 
 3 | import pytz
 4 | 
 5 | from backtrader.tradingcal import TradingCalendarBase
 6 | from backtrader.utils.py3 import string_types
 7 | from pandas import DataFrame, DatetimeIndex, Timestamp
 8 | 
 9 | 
10 | class ExchangeCalendarsTradingCalendar(TradingCalendarBase):
11 | 
12 |     # pylint: disable=no-member
13 | 
14 |     params = (
15 |         ("calendar", None),
16 |         ("cachesize", 365),
17 |     )
18 | 
19 |     def __init__(self):
20 |         super().__init__()
21 | 
22 |         self._calendar = self.p.calendar
23 | 
24 |         if isinstance(self._calendar, string_types):
25 |             from exchange_calendars import get_calendar
26 | 
27 |             self._calendar = get_calendar(self._calendar)
28 | 
29 |         self.dcache = DatetimeIndex([0.0])
30 |         self.idcache = DataFrame(index=DatetimeIndex([0.0]))
31 |         self.csize = datetime.timedelta(days=self.p.cachesize)
32 | 
33 |     def _nextday(self, day):
34 |         d = day + self._calendar.day
35 |         return d, d.isocalendar()
36 | 
37 |     def schedule(
38 |         self, day, tz=None
39 |     ):  # pylint: disable=arguments-differ,unused-argument
40 |         """
41 |         day: expecting naive datetime.datetime day in utc timezone
42 |         tz: treat/localize internal naive datetimes to this timezone
43 |         returns: (opening, closing) naive datetime.datetime pair in utc timezone
44 |         """
45 |         session = Timestamp(day, tz=pytz.UTC).normalize()
46 |         opening, closing = self._calendar.open_and_close_for_session(session)
47 |         opening = opening.tz.localize(None).to_pydatetime()
48 |         closing = closing.tz.localize(None).to_pydatetime()
49 |         return opening, closing
50 | 
51 | 
52 | class KrxTradingCalendar(ExchangeCalendarsTradingCalendar):
53 | 
54 |     params = (
55 |         ("calendar", "XKRX"),
56 |         ("cachesize", 365),
57 |     )
58 | 


--------------------------------------------------------------------------------
/koapy/utils/recursion.py:
--------------------------------------------------------------------------------
 1 | """
 2 | """
 3 | 
 4 | """
 5 | This is free and unencumbered software released into the public domain.
 6 | 
 7 | Anyone is free to copy, modify, publish, use, compile, sell, or
 8 | distribute this software, either in source code form or as a compiled
 9 | binary, for any purpose, commercial or non-commercial, and by any
10 | means.
11 | 
12 | In jurisdictions that recognize copyright laws, the author or authors
13 | of this software dedicate any and all copyright interest in the
14 | software to the public domain. We make this dedication for the benefit
15 | of the public at large and to the detriment of our heirs and
16 | successors. We intend this dedication to be an overt act of
17 | relinquishment in perpetuity of all present and future rights to this
18 | software under copyright law.
19 | 
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 | 
28 | For more information, please refer to 
29 | """
30 | 
31 | # https://github.com/andrewp-as-is/recursion-detect.py
32 | 
33 | import inspect
34 | 
35 | 
36 | def depth():
37 |     """return recursion depth. 0 if no recursion"""
38 |     counter = 0
39 |     frames = inspect.getouterframes(inspect.currentframe())[1:]
40 |     top_frame = inspect.getframeinfo(frames[0][0])
41 |     for frame, _, _, _, _, _ in frames[1:]:
42 |         (path, line_number, func_name, lines, index) = inspect.getframeinfo(frame)
43 |         if path == top_frame[0] and func_name == top_frame[2]:
44 |             counter += 1
45 |     return counter
46 | 
47 | 
48 | __all__ = ["depth"]
49 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |     - master
 7 |   pull_request:
 8 |     branches:
 9 |     - master
10 |   workflow_dispatch:
11 |     inputs:
12 | 
13 | jobs:
14 |   build-and-test:
15 | 
16 |     runs-on: ${{ matrix.os }}
17 |     strategy:
18 |       matrix:
19 |         os: [ubuntu-latest, windows-latest, macos-latest]
20 |         python-version: [3.8, 3.9]
21 |         architecture: [x86, x64]
22 |         exclude:
23 |         - os: ubuntu-latest
24 |         - os: macos-latest
25 | 
26 |     steps:
27 |     - uses: actions/checkout@v3
28 |     - name: Set up Python
29 |       uses: actions/setup-python@v4
30 |       id: setup-python
31 |       with:
32 |         python-version: ${{ matrix.python-version }}
33 |     - name: Install poetry
34 |       run: |
35 |         python -m pip install --upgrade pip
36 |         pip install pipx
37 |         pipx install poetry
38 |     - name: Get poetry cache dir
39 |       id: poetry-cache
40 |       run: |
41 |         echo "::set-output name=dir::$(poetry config cache-dir)"
42 |     - name: Restore cache
43 |       uses: actions/cache@v3
44 |       with:
45 |         path: ${{ steps.poetry-cache.outputs.dir }}
46 |         key: ${{ runner.os }}-poetry-py${{ steps.setup-python.outputs.python-version }}-${{ matrix.architecture }}-${{ hashFiles('poetry.lock') }}
47 |         restore-keys: |
48 |           ${{ runner.os }}-poetry-py${{ steps.setup-python.outputs.python-version }}-${{ matrix.architecture }}-
49 |     - name: Install dependencies
50 |       run: |
51 |         poetry install
52 |     - name: Test with pytest
53 |       run: |
54 |         poetry run pytest -n auto --dist loadscope --junit-xml=junit/test-results.xml --cov --cov-report=xml --cov-report=html
55 |     - name: Update Codecov coverage report
56 |       uses: codecov/codecov-action@v3.1.1
57 |       with:
58 |         token: ${{ secrets.CODECOV_TOKEN }}
59 |         flags: ${{ matrix.os }}-py${{ matrix.python-version }}
60 | 


--------------------------------------------------------------------------------
/koapy/examples/10_condition.py:
--------------------------------------------------------------------------------
 1 | import threading
 2 | 
 3 | import grpc
 4 | 
 5 | from koapy import KiwoomOpenApiPlusEntrypoint
 6 | 
 7 | with KiwoomOpenApiPlusEntrypoint() as context:
 8 |     # 로그인 처리
 9 |     context.EnsureConnected()
10 | 
11 |     # 조건검색을 사용하기 위해서는 먼저 서버에 저장된 조건들을 불러와야함 (GetConditionLoad)
12 |     # 아래 함수는 조건을 불러온적이 없다면 불러오고 성공 이벤트까지 기다렸다 반환함
13 |     # 이전 호출여부와 상관없이 강제로 다시 조건을 불러오려면 LoadCondition() 호출
14 |     context.EnsureConditionLoaded()
15 | 
16 |     # 불러온 조건 리스트 확인, (조건번호, 조건이름) 쌍 리스트 반환
17 |     conditions = context.GetConditionNameListAsList()
18 |     print(conditions)
19 | 
20 |     # 이후 예시의 정상동작을 위해 아래에서 사용되는 조건들과 같은 이름을 가지는 조건들이 미리 저장되어 있어야함
21 |     # - 대형 저평가 우량주
22 |     # - 중소형 저평가주
23 | 
24 |     # 위의 조건식들은 키움에서 예시로 제공하는 추천식들을 그대로 이름을 똑같이 해서 저장한 것들임
25 |     # 참고로 조건들을 편집하고 저장하는건 영웅문 HTS 내부에서만 가능함
26 | 
27 |     # 조건검색을 실행할 첫 조건명
28 |     condition_name = "대형 저평가 우량주"
29 | 
30 |     # 조건을 만족하는 코드 리스트를 바로 반환 (단순 조건검색)
31 |     codes, info = context.GetCodeListByCondition(condition_name, with_info=True)
32 |     print(codes)
33 |     print(info)
34 | 
35 |     # 조건검색을 다시 실행할 조건명 (같은 조건식은 1분에 1건 제한이므로 예시 실행시 제한을 회피하기 위해서 새로 설정)
36 |     condition_name = "중소형 저평가주"
37 | 
38 |     # 실시간 조건 검색 예시, 편입된/제외된 코드 리스트 쌍을 스트림으로 반환 (실시간 조건 검색)
39 |     stream = context.GetCodeListByConditionAsStream(condition_name)
40 | 
41 |     def stop_listening():
42 |         print()
43 |         print("Stopping to listen events...")
44 |         stream.cancel()
45 | 
46 |     threading.Timer(10.0, stop_listening).start()  # 10초 이후에 gRPC 커넥션 종료하도록 설정
47 | 
48 |     # (디버깅을 위한) 이벤트 메시지 출력 함수
49 |     from pprint import PrettyPrinter
50 | 
51 |     from google.protobuf.json_format import MessageToDict
52 | 
53 |     pp = PrettyPrinter()
54 | 
55 |     def pprint_event(event):
56 |         pp.pprint(MessageToDict(event, preserving_proto_field_name=True))
57 | 
58 |     try:
59 |         for event in stream:
60 |             pprint_event(event)
61 |     except grpc.RpcError as e:
62 |         print(e)
63 | 


--------------------------------------------------------------------------------
/koapy/compat/pywinauto/importlib.py:
--------------------------------------------------------------------------------
 1 | import importlib
 2 | import sys
 3 | import warnings
 4 | 
 5 | from importlib.abc import Loader, MetaPathFinder
 6 | 
 7 | import pythoncom
 8 | 
 9 | 
10 | class PyWinAutoFinder(MetaPathFinder):
11 |     def find_spec(self, fullname, path, target=None):
12 |         if fullname == "pywinauto":
13 |             self.unregister()
14 |             spec = importlib.util.find_spec(fullname)
15 |             spec.loader = PyWinAutoLoader()
16 |             return spec
17 | 
18 |     @classmethod
19 |     def register(cls):
20 |         sys.meta_path = [cls()] + sys.meta_path
21 | 
22 |     @classmethod
23 |     def unregister(cls):
24 |         sys.meta_path = [x for x in sys.meta_path if not isinstance(x, cls)]
25 | 
26 | 
27 | class PyWinAutoLoader(Loader):
28 |     def __init__(self):
29 |         self._original_coinit_flags_defined = False
30 |         self._original_coinit_flags = None
31 | 
32 |     def set_sys_coinit_flags(self):
33 |         self._original_coinit_flags_defined = hasattr(sys, "coinit_flags")
34 |         self._original_coinit_flags = getattr(sys, "coinit_flags", None)
35 |         sys.coinit_flags = pythoncom.COINIT_APARTMENTTHREADED
36 | 
37 |     def reset_sys_coinit_flags(self):
38 |         if not self._original_coinit_flags_defined:
39 |             del sys.coinit_flags
40 |         else:
41 |             sys.coinit_flags = self._original_coinit_flags
42 | 
43 |     def create_module(self, spec):
44 |         # set sys.coinit_flags = 2
45 |         # check https://github.com/pywinauto/pywinauto/issues/472 for more information
46 |         self.set_sys_coinit_flags()
47 | 
48 |         # ensure that qt binding is imported before pywinauto import
49 |         from koapy.compat import pyside2
50 | 
51 |         # import pywinauto
52 |         with warnings.catch_warnings():
53 |             warnings.simplefilter("ignore", UserWarning)
54 |             module = importlib.import_module(spec.name)
55 |         return module
56 | 
57 |     def exec_module(self, module):
58 |         pass
59 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/utils/queue/QueueBasedIterableObserver.py:
--------------------------------------------------------------------------------
 1 | from queue import Empty, Queue
 2 | 
 3 | from rx.core.typing import Observer
 4 | 
 5 | from koapy.backend.kiwoom_open_api_plus.utils.queue.QueueIterator import (
 6 |     BufferedQueueIterator,
 7 | )
 8 | 
 9 | 
10 | class QueueBasedIterableObserverIterator(BufferedQueueIterator):
11 |     def __init__(self, queue, sentinel):
12 |         self._queue = queue
13 |         self._sentinel = sentinel
14 |         super().__init__(self._queue)
15 | 
16 |     def next(self, block=True, timeout=None):
17 |         value, error = super().next(block, timeout)
18 |         if value == self._sentinel:
19 |             raise StopIteration
20 |         elif error is not None and isinstance(error, Exception):
21 |             raise error
22 |         return value
23 | 
24 |     def head(self):
25 |         value, error = super().head()
26 |         if value == self._sentinel:
27 |             raise Empty
28 |         elif error is not None and isinstance(error, Exception):
29 |             raise error
30 |         return value
31 | 
32 | 
33 | class QueueBasedIterableObserver(Observer):
34 | 
35 |     _default_maxsize = 0
36 |     _queue_get_timeout = 2
37 | 
38 |     def __init__(self, queue=None, maxsize=None):
39 |         if queue is None:
40 |             if maxsize is None:
41 |                 maxsize = self._default_maxsize
42 |             queue = Queue(maxsize)
43 | 
44 |         self._queue = queue
45 |         self._maxsize = maxsize
46 |         self._sentinel = object()
47 | 
48 |         self._iterator = QueueBasedIterableObserverIterator(self._queue, self._sentinel)
49 | 
50 |     @property
51 |     def queue(self):
52 |         return self._queue
53 | 
54 |     def on_next(self, value):
55 |         self._queue.put((value, None))
56 | 
57 |     def on_error(self, error):
58 |         self._queue.put((None, error))
59 | 
60 |     def on_completed(self):
61 |         self._queue.put((self._sentinel, None))
62 | 
63 |     def __iter__(self):
64 |         return self._iterator
65 | 
66 |     def stop(self):
67 |         return self._iterator.stop()
68 | 


--------------------------------------------------------------------------------
/koapy/backend/daishin_cybos_plus/core/CybosPlusError.py:
--------------------------------------------------------------------------------
 1 | from functools import wraps
 2 | 
 3 | 
 4 | class CybosPlusError(Exception):
 5 | 
 6 |     pass
 7 | 
 8 | 
 9 | class CybosPlusRequestError(CybosPlusError):
10 | 
11 |     """
12 |     아래 문서에서 [BlockRequest/Blockrequest2/Request의 리턴값] 내용 참조
13 |     http://cybosplus.github.io/cputil_rtf_1_/cybosplus_interface.htm
14 |     """
15 | 
16 |     ERROR_MESSAGE_BY_CODE = {
17 |         0: "정상요청",
18 |         1: "통신요청 실패",
19 |         2: "주문확인창에서 취소",
20 |         3: "그외의 내부 오류",
21 |         4: "주문요청 제한 개수 초과",
22 |     }
23 | 
24 |     @classmethod
25 |     def get_error_message_by_code(cls, code, default=None):
26 |         return cls.ERROR_MESSAGE_BY_CODE.get(code, default)
27 | 
28 |     @classmethod
29 |     def check_code_or_raise(cls, code):
30 |         if code != 0:
31 |             raise cls(code)
32 |         return code
33 | 
34 |     @classmethod
35 |     def wrap_to_check_code_or_raise(cls, func):
36 |         @wraps(func)
37 |         def wrapper(*args, **kwargs):
38 |             return cls.check_code_or_raise(func(*args, **kwargs))
39 | 
40 |         return wrapper
41 | 
42 |     @classmethod
43 |     def try_or_raise(cls, arg):
44 |         if isinstance(arg, int):
45 |             return cls.check_code_or_raise(arg)
46 |         elif callable(arg):
47 |             return cls.wrap_to_check_code_or_raise(arg)
48 |         else:
49 |             raise TypeError("Expected 'int' or 'callable' but %s found" % type(arg))
50 | 
51 |     def __init__(self, code, message=None):
52 |         if message is None:
53 |             message = self.get_error_message_by_code(code)
54 | 
55 |         super().__init__(message)
56 | 
57 |         self._code = code
58 |         self._message = message
59 | 
60 |     def __str__(self):
61 |         return self._message
62 | 
63 |     def __repr__(self):
64 |         return "{}({!r}, {!r})".format(
65 |             self.__class__.__name__, self._code, self._message
66 |         )
67 | 
68 |     @property
69 |     def code(self):
70 |         return self._code
71 | 
72 |     @property
73 |     def message(self):
74 |         return self._message
75 | 


--------------------------------------------------------------------------------
/koapy/common/DispatchProxyService.proto:
--------------------------------------------------------------------------------
 1 | syntax = "proto3";
 2 | 
 3 | service DispatchProxyService {
 4 |     rpc GetDispatch(GetDispatchRequest) returns (GetDispatchResponse) {};
 5 |     rpc GetAttr(GetAttrRequest) returns (GetAttrResponse) {};
 6 |     rpc SetAttr(SetAttrRequest) returns (SetAttrResponse) {};
 7 |     rpc CallMethod(CallMethodRequest) returns (CallMethodResponse) {};
 8 |     rpc ConnectEvent(stream ConnectEventRequest) returns (stream ConnectEventResponse) {};
 9 | }
10 | 
11 | message GetDispatchRequest {
12 |     string iid = 1;
13 | }
14 | 
15 | message GetDispatchResponse {
16 |     string iid = 1;
17 | }
18 | 
19 | message Value {
20 |     oneof value {
21 |         string string_value = 1;
22 |         int32 short_value = 2;
23 |         int32 int_value = 3;
24 |         int64 long_value = 4;
25 |         bool bool_value = 5;
26 |         double double_value = 6;
27 |         ValueArray list_value = 7;
28 |         ValueArray tuple_value = 8;
29 |     }
30 | }
31 | 
32 | message ValueArray {
33 |     repeated Value values = 1;
34 | }
35 | 
36 | message GetAttrRequest {
37 |     string iid = 1;
38 |     string name = 2;
39 | }
40 | 
41 | message GetAttrResponse {
42 |     Value value = 1;
43 | }
44 | 
45 | message SetAttrRequest {
46 |     string iid = 1;
47 |     string name = 2;
48 |     Value value = 3;
49 | }
50 | 
51 | message SetAttrResponse {
52 | }
53 | 
54 | message Argument {
55 |     Value value = 1;
56 | }
57 | 
58 | message CallMethodRequest {
59 |     string iid = 1;
60 |     string name = 2;
61 |     repeated Argument arguments = 3;
62 | }
63 | 
64 | message CallMethodResponse {
65 |     Value return_value = 1;
66 | }
67 | 
68 | message ConnectEventEstablishRequest {
69 |     string iid = 1;
70 |     string name = 2;
71 | }
72 | 
73 | message ConnectEventAckRequest {
74 | }
75 | 
76 | message ConnectEventRequest {
77 |     oneof request {
78 |         ConnectEventEstablishRequest establish_request = 1;
79 |         ConnectEventAckRequest ack_request = 2;
80 |     }
81 | }
82 | 
83 | message ConnectEventResponse {
84 |     string iid = 1;
85 |     string name = 2;
86 |     repeated Argument arguments = 3;
87 | }
88 | 


--------------------------------------------------------------------------------
/koapy/utils/store/sqlalchemy/Version.py:
--------------------------------------------------------------------------------
 1 | from sqlalchemy import (
 2 |     Boolean,
 3 |     Column,
 4 |     ForeignKey,
 5 |     Integer,
 6 |     PickleType,
 7 |     String,
 8 |     UniqueConstraint,
 9 | )
10 | from sqlalchemy.orm import object_session, relationship
11 | from sqlalchemy.schema import DropTable, MetaData, Table
12 | from sqlalchemy.sql.functions import current_timestamp
13 | 
14 | from .Base import Base
15 | from .Timestamp import Timestamp
16 | 
17 | 
18 | class Version(Base):
19 |     __tablename__ = "versions"
20 | 
21 |     id = Column(Integer, primary_key=True)
22 |     version = Column(Integer, nullable=False)
23 | 
24 |     table_name = Column(String)
25 |     user_metadata = Column(PickleType)
26 |     pandas_metadata = Column(PickleType)
27 | 
28 |     deleted = Column(Boolean, default=False)
29 | 
30 |     timestamp = Column(Timestamp(timezone=True), server_default=current_timestamp())
31 | 
32 |     symbol_id = Column(Integer, ForeignKey("symbols.id"))
33 |     symbol = relationship("Symbol", back_populates="versions")
34 | 
35 |     snapshots = relationship("SnapshotAssociation", back_populates="version")
36 | 
37 |     __table_args__ = (UniqueConstraint("symbol_id", "version"),)
38 | 
39 |     def get_snapshots(self):
40 |         # quick fix fir circular dependency
41 |         from .Snapshot import Snapshot
42 |         from .SnapshotAssociation import SnapshotAssociation
43 | 
44 |         session = object_session(self)
45 |         snapshots = session.query(SnapshotAssociation).with_parent(self)
46 |         snapshots = session.query(Snapshot).join(snapshots.subquery())
47 |         snapshots = snapshots.all()
48 |         return snapshots
49 | 
50 |     def delete(self):
51 |         session = object_session(self)
52 |         if self.table_name is not None:
53 |             table_reference_count = (
54 |                 session.query(Version)
55 |                 .filter(Version.table_name == self.table_name)
56 |                 .count()
57 |             )
58 |             if table_reference_count <= 1:
59 |                 session.execute(
60 |                     DropTable(Table(self.table_name, MetaData()), if_exists=True)
61 |                 )
62 |         session.delete(self)
63 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusEventHandlerSignature.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Dict, List
 4 | 
 5 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEventFunctions import (
 6 |     KiwoomOpenApiPlusEventFunctions,
 7 | )
 8 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusSignature import (
 9 |     KiwoomOpenApiPlusSignature,
10 | )
11 | from koapy.utils.builtin import dir_public
12 | 
13 | 
14 | class KiwoomOpenApiPlusEventHandlerSignature(KiwoomOpenApiPlusSignature):
15 | 
16 |     EVENT_HANDLER_SIGNATURES_BY_NAME: Dict[
17 |         str, KiwoomOpenApiPlusEventHandlerSignature
18 |     ] = {}
19 | 
20 |     @classmethod
21 |     def from_name(cls, name: str) -> KiwoomOpenApiPlusEventHandlerSignature:
22 |         signature = cls.EVENT_HANDLER_SIGNATURES_BY_NAME[name]
23 |         return signature
24 | 
25 |     @classmethod
26 |     def names(cls) -> List[str]:
27 |         names = cls.EVENT_HANDLER_SIGNATURES_BY_NAME.keys()
28 |         names = list(names)
29 |         if not names:
30 |             _names = dir_public(KiwoomOpenApiPlusEventFunctions)
31 |         return names
32 | 
33 |     @classmethod
34 |     def _make_event_handler_signatures_by_name(
35 |         cls,
36 |     ) -> Dict[str, KiwoomOpenApiPlusEventHandlerSignature]:
37 |         from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLib import (
38 |             EVENT_OLE_ITEM,
39 |         )
40 | 
41 |         event = EVENT_OLE_ITEM
42 |         event_handler_signatures_by_name: Dict[
43 |             str, KiwoomOpenApiPlusEventHandlerSignature
44 |         ] = {}
45 |         if event:
46 |             event_funcs = event.mapFuncs.items()
47 |             event_handler_signatures_by_name = {}
48 |             for func_name, entry in event_funcs:
49 |                 signature = cls._from_entry(func_name, entry)
50 |                 event_handler_signatures_by_name[func_name] = signature
51 |         return event_handler_signatures_by_name
52 | 
53 |     @classmethod
54 |     def _initialize(cls):
55 |         cls.EVENT_HANDLER_SIGNATURES_BY_NAME = (
56 |             cls._make_event_handler_signatures_by_name()
57 |         )
58 | 
59 | 
60 | KiwoomOpenApiPlusEventHandlerSignature._initialize()  # pylint: disable=protected-access
61 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/openapi_meta/trinfo.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | 
 5 | 
 6 | @click.command(short_help="Get TR info.")
 7 | @click.option(
 8 |     "-t",
 9 |     "--trcode",
10 |     "trcodes",
11 |     metavar="TRCODE",
12 |     multiple=True,
13 |     help="TR code to get (like opt10001).",
14 | )
15 | @verbose_option()
16 | def trinfo(trcodes):
17 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTrInfo import (
18 |         KiwoomOpenApiPlusTrInfo,
19 |     )
20 | 
21 |     def get_codes():
22 |         if trcodes:
23 |             if "-" in trcodes:
24 |                 with click.open_file("-", "r") as f:
25 |                     for code in f:
26 |                         yield code.strip()
27 |             else:
28 |                 for code in trcodes:
29 |                     yield code
30 |         else:
31 |             while True:
32 |                 try:
33 |                     code = click.prompt("trcode", prompt_suffix=" >>> ")
34 |                     code = code.strip()
35 |                     if code == "exit":
36 |                         break
37 |                     if code:
38 |                         yield code
39 |                 except EOFError:
40 |                     break
41 | 
42 |     for trcode in get_codes():
43 |         tr_info = KiwoomOpenApiPlusTrInfo.get_trinfo_by_code(trcode)
44 |         if tr_info is not None:
45 |             click.echo("[{}] : [{}]".format(tr_info.tr_code.upper(), tr_info.name))
46 |             click.echo("  [INPUT]")
47 |             for tr_input in tr_info.inputs:
48 |                 click.echo("    %s" % tr_input.name)
49 |             if tr_info.single_outputs:
50 |                 click.echo(
51 |                     "  [OUTPUT] [SINGLE DATA] : [%s]" % tr_info.single_outputs_name
52 |                 )
53 |                 for output in tr_info.single_outputs:
54 |                     click.echo("    %s" % output.name)
55 |             if tr_info.multi_outputs:
56 |                 click.echo(
57 |                     "  [OUTPUT] [MULTI DATA]  : [%s]" % tr_info.multi_outputs_name
58 |                 )
59 |                 for output in tr_info.multi_outputs:
60 |                     click.echo("    %s" % output.name)
61 |         else:
62 |             click.echo("Given trcode is invalid")
63 | 


--------------------------------------------------------------------------------
/docs/source/_static/css/style.css:
--------------------------------------------------------------------------------
 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@300;400&display=swap');
 2 | @import url("https://cdn.jsdelivr.net/gh/wan2land/d2coding/d2coding-full.css");
 3 | 
 4 | /* General */
 5 | 
 6 | body {
 7 |     padding: 0 !important;
 8 | }
 9 | 
10 | code {
11 |     vertical-align: text-bottom;
12 | }
13 | 
14 | dl dd {
15 |     margin-left: 0;
16 | }
17 | 
18 | div.document {
19 |     width: auto;
20 |     max-width: 1200px;
21 | }
22 | 
23 | /* Jupyter notebook related overrides */
24 | 
25 | :root {
26 |     --jp-ui-font-family: 'Noto Serif KR', Georgia, 'Times New Roman', Times, serif !important;
27 |     --jp-content-font-family: 'Noto Serif KR', Georgia, 'Times New Roman', Times, serif !important;
28 |     --jp-code-font-family: 'D2Coding', 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace !important;
29 |     --jp-content-font-size1: 17px !important;
30 |     --jp-code-font-size: 0.9em !important;
31 | }
32 | 
33 | .jp-Cell .highlight {
34 |     background: transparent;
35 | }
36 | 
37 | .jp-Cell pre {
38 |     background: transparent;
39 | }
40 | 
41 | .jp-Cell .highlight pre {
42 |     margin: 1em 0px;
43 | }
44 | 
45 | .jp-Notebook {
46 |     font-size: var(--jp-content-font-size1);
47 | }
48 | 
49 | .jp-InputPrompt {
50 |     font-size: 0.7em !important;
51 | }
52 | 
53 | .jp-OutputPrompt {
54 |     font-size: 0.7em !important;
55 | }
56 | 
57 | .jp-Notebook {
58 |     letter-spacing: normal !important;
59 |     line-height: normal !important;
60 | }
61 | 
62 | /* Media query overrides */
63 | 
64 | @media screen and (max-width: 875px) {
65 |     div.sphinxsidebar {
66 |         width: 100%;
67 |         margin: 0 !important;
68 |         padding: 0 !important;
69 |     }
70 | 
71 |     div.sphinxsidebarwrapper {
72 |         padding: 28px 30px !important;
73 |     }
74 | 
75 |     div.documentwrapper {
76 |         overflow-x: scroll;
77 |     }
78 | 
79 |     div.body {
80 |         padding: 0 30px 20px;
81 |     }
82 | 
83 |     body.jp-Notebook div.body {
84 |         margin-left: -30px;
85 |     }
86 | 
87 |     body.jp-Notebook div.body > section > h1 {
88 |         margin-left: 30px;
89 |     }
90 | }
91 | 
92 | @media screen and (max-width: 870px) {
93 |     ul, ol {
94 |         margin-left: 30px;
95 |         padding-left: 0 !important;
96 |     }
97 | }


--------------------------------------------------------------------------------
/koapy/cli/commands/generate/openapi/python_stubs.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.backend.kiwoom_open_api_plus.core.tools.generate_python_stubs import (
 4 |     generate_python_stubs,
 5 | )
 6 | from koapy.config import default_encoding
 7 | 
 8 | default_dispatch_class_name = "KiwoomOpenApiPlusDispatchFunctionsGenerated"
 9 | default_dispatch_file_path = default_dispatch_class_name + ".py"
10 | 
11 | default_event_class_name = "KiwoomOpenApiPlusEventFunctionsGenerated"
12 | default_event_file_path = default_event_class_name + ".py"
13 | 
14 | 
15 | @click.command(
16 |     short_help="Generate python stubs for OpenAPI Dispatch and Event.",
17 | )
18 | @click.option(
19 |     "--dispatch-class-name",
20 |     metavar="NAME",
21 |     help="Name for class with Dispatch functions.",
22 |     default=default_dispatch_class_name,
23 |     show_default=True,
24 | )
25 | @click.option(
26 |     "--dispatch-file-path",
27 |     type=click.Path(),
28 |     help="Path for python-stub with Dispatch functions.",
29 |     default=default_dispatch_file_path,
30 |     show_default=True,
31 | )
32 | @click.option(
33 |     "--event-class-name",
34 |     metavar="NAME",
35 |     help="Name for class with Event functions.",
36 |     default=default_event_class_name,
37 |     show_default=True,
38 | )
39 | @click.option(
40 |     "--event-file-path",
41 |     type=click.Path(),
42 |     help="Path for python-stub with Event functions.",
43 |     default=default_event_file_path,
44 |     show_default=True,
45 | )
46 | @click.option(
47 |     "--encoding",
48 |     metavar="ENCODING",
49 |     help="Encoding for stub files.",
50 |     default=default_encoding,
51 |     show_default=True,
52 | )
53 | @click.option(
54 |     "--force-overwrite",
55 |     help="Force overwrite even if target file already exists.",
56 |     is_flag=True,
57 |     show_default=True,
58 | )
59 | def python_stubs(
60 |     dispatch_class_name,
61 |     dispatch_file_path,
62 |     event_class_name,
63 |     event_file_path,
64 |     encoding,
65 |     force_overwrite,
66 | ):
67 |     generate_python_stubs(
68 |         dispatch_class_name=dispatch_class_name,
69 |         dispatch_file_path=dispatch_file_path,
70 |         event_class_name=event_class_name,
71 |         event_file_path=event_file_path,
72 |         encoding=encoding,
73 |         force_overwrite=force_overwrite,
74 |     )
75 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/utils/queue/QueueIterator.py:
--------------------------------------------------------------------------------
 1 | import atexit
 2 | 
 3 | from queue import Empty
 4 | 
 5 | 
 6 | class QueueIterator:
 7 | 
 8 |     _check_timeout = 1
 9 | 
10 |     def __init__(self, queue):
11 |         self._queue = queue
12 |         self._should_stop = False
13 | 
14 |         atexit.register(self.stop)
15 | 
16 |     def __del__(self):
17 |         atexit.unregister(self.stop)
18 | 
19 |     @property
20 |     def queue(self):
21 |         return self._queue
22 | 
23 |     def next(self, block=True, timeout=None):
24 |         if block and timeout is None:
25 |             timeout = self._check_timeout
26 |             while not self._should_stop:
27 |                 try:
28 |                     item = self._queue.get(block=block, timeout=timeout)
29 |                 except Empty:
30 |                     pass
31 |                 else:
32 |                     self._queue.task_done()
33 |                     return item
34 |             raise StopIteration
35 |         else:
36 |             item = self._queue.get(block=block, timeout=timeout)
37 |             self._queue.task_done()
38 |             return item
39 | 
40 |     def next_nowait(self):
41 |         return self.next(block=False)
42 | 
43 |     def has_next(self):
44 |         return not self._queue.empty()
45 | 
46 |     def __iter__(self):
47 |         return self
48 | 
49 |     def __next__(self):
50 |         return self.next()
51 | 
52 |     def stop(self):
53 |         self._should_stop = True
54 | 
55 |     def enable(self):
56 |         self._should_stop = False
57 | 
58 | 
59 | class BufferedQueueIterator(QueueIterator):
60 |     def __init__(self, queue):
61 |         super().__init__(queue)
62 | 
63 |         self._none = object()
64 |         self._head = self._none
65 | 
66 |     def next(self, block=True, timeout=None):
67 |         if self._head is not self._none:
68 |             item = self._head
69 |             self._head = self._none
70 |             return item
71 |         else:
72 |             return super().next(block, timeout)
73 | 
74 |     def has_next(self):
75 |         return self._head is not self._none or super().has_next()
76 | 
77 |     def head(self):
78 |         if self._head is not self._none:
79 |             return self._head
80 |         else:
81 |             item = self.next_nowait()  # raises queue.Empty if queue is empty
82 |             self._head = item
83 |             return self._head
84 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/account_data/evaluation.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | from koapy.utils.logging import get_logger
 5 | 
 6 | logger = get_logger(__name__)
 7 | 
 8 | 
 9 | @click.command(short_help="Get account evaluation.")
10 | @click.option("-a", "--account", metavar="ACCNO", help="Account number.")
11 | @click.option(
12 |     "-d", "--include-delisted", is_flag=True, help="Include delisted.", default=True
13 | )
14 | @click.option("-D", "--exclude-delisted", is_flag=True, help="Exclude delisted.")
15 | @click.option(
16 |     "-e", "--for-each", is_flag=True, help="Show individual evaluation.", default=True
17 | )
18 | @click.option("-E", "--as-summary", is_flag=True, help="Show summarized evaluation.")
19 | @click.option(
20 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
21 | )
22 | @verbose_option()
23 | def evaluation(account, include_delisted, exclude_delisted, for_each, as_summary, port):
24 |     if account is None:
25 |         logger.info("Account not given. Using first account available.")
26 | 
27 |     if exclude_delisted:
28 |         include_delisted = False
29 | 
30 |     if as_summary:
31 |         for_each = False
32 |         lookup_type = "1"
33 |     elif for_each:
34 |         lookup_type = "2"
35 | 
36 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
37 |         KiwoomOpenApiPlusEntrypoint,
38 |     )
39 | 
40 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
41 |         context.EnsureConnected()
42 | 
43 |         if account is None:
44 |             account = context.GetAccountList()[0]
45 | 
46 |         single, multi = context.GetAccountEvaluationStatusAsSeriesAndDataFrame(
47 |             account, include_delisted
48 |         )
49 |         click.echo("[계좌평가현황요청] : [계좌평가현황]")
50 |         click.echo(single.to_markdown(floatfmt=".2f"))
51 |         click.echo()
52 |         click.echo("[계좌평가현황요청] : [종목별계좌평가현황]")
53 |         click.echo(multi.to_markdown())
54 |         click.echo()
55 | 
56 |         single, multi = context.GetAccountEvaluationBalanceAsSeriesAndDataFrame(
57 |             account, lookup_type
58 |         )
59 |         click.echo("[계좌평가잔고내역요청] : [계좌평가결과]")
60 |         click.echo(single.to_markdown(floatfmt=".2f"))
61 |         click.echo()
62 |         click.echo("[계좌평가잔고내역요청] : [계좌평가잔고개별합산]")
63 |         click.echo(multi.to_markdown())
64 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/data/scripts/setup.ps1:
--------------------------------------------------------------------------------
 1 | Function New-TemporaryDirectory {
 2 |     $Parent = [System.IO.Path]::GetTempPath()
 3 |     [string] $Name = [System.Guid]::NewGuid()
 4 |     New-Item -ItemType Directory -Path (Join-Path "$Parent" "$Name")
 5 | }
 6 | 
 7 | Function Invoke-DownloadFile {
 8 |     Param (
 9 |         $Address,
10 |         $FilePath
11 |     )
12 |     (New-Object System.Net.WebClient).DownloadFile("$Address", "$FilePath")
13 | }
14 | 
15 | Function Get-OpenAPISetupFile {
16 |     Param (
17 |         $FilePath
18 |     )
19 |     $SetupFile = "OpenAPISetup.exe"
20 |     $SetupFileURL = "https://download.kiwoom.com/web/openapi/$SetupFile"
21 |     Invoke-DownloadFile -Address "$SetupFileURL" -FilePath "$FilePath"
22 | }
23 | 
24 | Function Start-OpenAPISetupProcess {
25 |     Param (
26 |         $ISSFilePath
27 |     )
28 |     Write-Output "Creating Temporary Directory"
29 |     $TempDir = New-TemporaryDirectory
30 |     Write-Output "Created Temporary Directory: $TempDir"
31 |     Try {
32 |         $SetupFile = "OpenAPISetup.exe"
33 |         $SetupFilePath = "$TempDir\$SetupFile"
34 |         Write-Output "Downloading Installer"
35 |         Get-OpenAPISetupFile -FilePath "$SetupFilePath"
36 |         Write-Output "Downloaded Installer Location: $SetupFilePath"
37 |         If ($ISSFilePath.EndsWith("\install.iss")) {
38 |             Write-Output "Installing OpenAPI"
39 |         }
40 |         ElseIf ($ISSFilePath.EndsWith("\uninstall.iss")) {
41 |             Write-Output "Uninstalling OpenAPI"
42 |         }
43 |         Write-Output "Starting Installer Process with ISSFile: $ISSFilePath"
44 |         Start-Process -WorkingDirectory "$TempDir" -FilePath "$SetupFilePath" -ArgumentList "/s /f1`"$ISSFilePath`"" -Wait
45 |         Write-Output "Installer Process Finished"
46 |         If ($ISSFilePath.EndsWith("\install.iss")) {
47 |             Write-Output "Installed OpenAPI"
48 |         }
49 |         ElseIf ($ISSFilePath.EndsWith("\uninstall.iss")) {
50 |             Write-Output "Uninstalled OpenAPI"
51 |         }
52 |     }
53 |     Finally {
54 |         Remove-Item "$TempDir" -Recurse -ErrorAction Ignore
55 |     }
56 | }
57 | 
58 | Function Install-OpenAPI {
59 |     Start-OpenAPISetupProcess -ISSFilePath "$PSScriptRoot\install.iss"
60 | }
61 | 
62 | Function Uninstall-OpenAPI {
63 |     Start-OpenAPISetupProcess -ISSFilePath "$PSScriptRoot\uninstall.iss"
64 | }
65 | 
66 | Install-OpenAPI
67 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/uninstall/openapi.py:
--------------------------------------------------------------------------------
 1 | import contextlib
 2 | import os
 3 | import shutil
 4 | import subprocess
 5 | import tempfile
 6 | 
 7 | import click
 8 | 
 9 | from koapy.cli.utils.openapi import (
10 |     download_openapi_installer,
11 |     prepare_issfile_for_uninstall,
12 |     run_installer_with_issfile,
13 | )
14 | from koapy.cli.utils.verbose_option import verbose_option
15 | from koapy.utils.logging import get_logger
16 | 
17 | logger = get_logger(__name__)
18 | 
19 | 
20 | @click.command(short_help="Uninstall openapi module.")
21 | @click.option(
22 |     "--reboot/--no-reboot",
23 |     default=False,
24 |     help="Reboot after uninstall. (default: false)",
25 | )
26 | @click.option(
27 |     "--cleanup/--no-cleanup",
28 |     default=True,
29 |     help="Clean up temporary directory after uninstall. (default: true)",
30 | )
31 | @verbose_option(default=5, show_default=True)
32 | def openapi(reboot, cleanup):
33 |     with contextlib.ExitStack() as stack:
34 |         tempdir = tempfile.mkdtemp()
35 |         logger.info("Created temporary directory: %s", tempdir)
36 |         if cleanup:
37 |             stack.callback(shutil.rmtree, tempdir)
38 |             logger.info("Registered to remove the temporary directory after uninstall.")
39 |         installer_filename = "OpenAPISetup.exe"
40 |         installer_filepath = os.path.join(tempdir, installer_filename)
41 |         logger.info("Downloading installer: %s", installer_filepath)
42 |         download_openapi_installer(installer_filepath)
43 |         iss_filename = "setup.iss"
44 |         iss_filepath = os.path.join(tempdir, iss_filename)
45 |         logger.info("Preparing .iss file: %s", iss_filepath)
46 |         prepare_issfile_for_uninstall(iss_filepath, reboot)
47 |         log_filename = "setup.log"
48 |         log_filepath = os.path.join(tempdir, log_filename)
49 |         try:
50 |             _return_code = run_installer_with_issfile(
51 |                 installer_filepath, iss_filepath, log_filepath, cwd=tempdir
52 |             )
53 |         except subprocess.CalledProcessError as e:
54 |             logger.exception(
55 |                 "Failed to uninstall openapi with return code: %d", e.returncode
56 |             )
57 |             raise RuntimeError(
58 |                 "Failed to uninstall openapi with return code: %d" % e.returncode
59 |             ) from e
60 |         else:
61 |             logger.info("Succesfully uninstalled openapi.")
62 | 


--------------------------------------------------------------------------------
/koapy/config.conf:
--------------------------------------------------------------------------------
 1 | {
 2 |     koapy.qtpy.qt_api = "pyside2"
 3 |     koapy.python.executable {
 4 |         32bit = { conda = "x86" }
 5 |         64bit = { path = "python" }
 6 |     }
 7 |     koapy.backend.kiwoom_open_api_plus.grpc {
 8 |         host = "localhost"
 9 |         port = 5943
10 |         server {
11 |             host = "localhost"
12 |             bind_address = "localhost"
13 |             port = 5943
14 |             max_workers = 8
15 |             channel.credentials.ssl {
16 |                 key_file = null
17 |                 cert_file = null
18 |                 root_certs_file = null
19 |                 require_client_auth = false
20 |             }
21 |         }
22 |         client {
23 |             host = "localhost"
24 |             port = 5943
25 |             max_workers = 8
26 |             is_ready.timeout = 10
27 |             channel.credentials.ssl {
28 |                 enable_ssl = false
29 |                 require_server_auth = false
30 |                 key_file = null
31 |                 cert_file = null
32 |                 root_certs_file = null
33 |             }
34 |         }
35 |     }
36 |     koapy.backend.kiwoom_open_api_plus.credentials {
37 |         user_id = ""
38 |         user_password = ""
39 |         cert_password = ""
40 |         is_simulation = true
41 |         account_passwords {}
42 |     }
43 |     koapy.backend.daishin_cybos_plus.credentials {
44 |         user_id = ""
45 |         user_password = ""
46 |         cert_password = ""
47 |         auto_account_password = true
48 |         auto_cert_password = true
49 |         price_check_only = true
50 |         account_passwords {}
51 |     }
52 |     koapy.utils.logging.config {
53 |         version = 1
54 |         formatters {
55 |             default {
56 |                 format = "%(asctime)s [%(levelname)s] %(message)s - %(filename)s:%(lineno)d"
57 |             }
58 |         }
59 |         handlers {
60 |             console {
61 |                 class = "koapy.utils.logging.tqdm.TqdmStreamHandler.TqdmStreamHandler"
62 |                 level = "NOTSET"
63 |                 formatter = "default"
64 |             }
65 |         }
66 |         loggers {
67 |             koapy {
68 |                 level = "DEBUG"
69 |                 propagate = false
70 |                 handlers = ["console"]
71 |             }
72 |         }
73 |         incremental = false
74 |         disable_existing_loggers = false
75 |     }
76 | }


--------------------------------------------------------------------------------
/koapy/examples/12_grpc_server_auth.py:
--------------------------------------------------------------------------------
 1 | def generate_tls_certificate(ou, cn):
 2 |     import random
 3 | 
 4 |     import OpenSSL
 5 | 
 6 |     cert_seconds_to_expiry = 60 * 60 * 24 * 365  # one year
 7 | 
 8 |     key = OpenSSL.crypto.PKey()
 9 |     key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
10 | 
11 |     cert = OpenSSL.crypto.X509()
12 |     cert.get_subject().OU = ou
13 |     cert.get_subject().CN = cn
14 |     cert.gmtime_adj_notBefore(0)
15 |     cert.gmtime_adj_notAfter(cert_seconds_to_expiry)
16 |     cert.set_serial_number(random.getrandbits(64))
17 |     cert.set_issuer(cert.get_subject())
18 |     cert.set_pubkey(key)
19 |     cert.sign(key, "sha256")
20 | 
21 |     with open("server.key", "wb") as f:
22 |         f.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
23 |     with open("server.crt", "wb") as f:
24 |         f.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
25 | 
26 | 
27 | def main():
28 |     import signal
29 |     import sys
30 | 
31 |     import grpc
32 | 
33 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusQAxWidget import (
34 |         KiwoomOpenApiPlusQAxWidget,
35 |     )
36 |     from koapy.backend.kiwoom_open_api_plus.grpc.KiwoomOpenApiPlusServiceServer import (
37 |         KiwoomOpenApiPlusServiceServer,
38 |     )
39 |     from koapy.compat.pyside2.QtWidgets import QApplication
40 | 
41 |     host = "localhost"
42 |     port = 5943
43 | 
44 |     app = QApplication.instance()
45 | 
46 |     if not app:
47 |         app = QApplication(sys.argv)
48 | 
49 |     control = KiwoomOpenApiPlusQAxWidget()
50 | 
51 |     with open("server.key", "rb") as f:
52 |         server_key = f.read()
53 |     with open("server.crt", "rb") as f:
54 |         server_crt = f.read()
55 | 
56 |     private_key_certificate_chain_pairs = [
57 |         (server_key, server_crt),
58 |     ]
59 |     credentials = grpc.ssl_server_credentials(
60 |         private_key_certificate_chain_pairs=private_key_certificate_chain_pairs,
61 |         root_certificates=None,
62 |         require_client_auth=False,
63 |     )
64 | 
65 |     server = KiwoomOpenApiPlusServiceServer(
66 |         control, host=host, port=port, credentials=credentials
67 |     )
68 | 
69 |     # for Ctrl+C to work
70 |     signal.signal(signal.SIGINT, signal.SIG_DFL)
71 | 
72 |     server.start()
73 |     app.exec_()
74 | 
75 | 
76 | if __name__ == "__main__":
77 |     generate_tls_certificate("koapy", "localhost")
78 |     main()
79 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/chart_data/daily.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.fail_with_usage import fail_with_usage
 4 | from koapy.cli.utils.verbose_option import verbose_option
 5 | from koapy.utils.logging import get_logger
 6 | 
 7 | logger = get_logger(__name__)
 8 | 
 9 | 
10 | @click.command(short_help="Get daily OHLCV of stocks.")
11 | @click.option("-c", "--code", metavar="CODE", help="Stock code to get.")
12 | @click.option(
13 |     "-o",
14 |     "--output",
15 |     metavar="FILENAME",
16 |     type=click.Path(),
17 |     help="Output filename for code.",
18 | )
19 | @click.option(
20 |     "-f",
21 |     "--format",
22 |     metavar="FORMAT",
23 |     type=click.Choice(["xlsx", "sqlite3"], case_sensitive=False),
24 |     default="xlsx",
25 |     help="Output format. (default: xlsx)",
26 | )
27 | @click.option(
28 |     "-s",
29 |     "--start-date",
30 |     metavar="YYYY-MM-DD",
31 |     type=click.DateTime(formats=["%Y-%m-%d", "%Y%m%d"]),
32 |     help="Most recent date to get. Defaults to today or yesterday if market is open.",
33 | )
34 | @click.option(
35 |     "-e",
36 |     "--end-date",
37 |     metavar="YYYY-MM-DD",
38 |     type=click.DateTime(formats=["%Y-%m-%d", "%Y%m%d"]),
39 |     help="Stops if reached, not included (optional).",
40 | )
41 | @click.option(
42 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
43 | )
44 | @verbose_option()
45 | def daily(
46 |     code, output, format, start_date, end_date, port
47 | ):  # pylint: disable=redefined-builtin
48 |     if (code, output, start_date, end_date) == (None, None, None, None):
49 |         fail_with_usage()
50 | 
51 |     if output is None:
52 |         output = "{}.{}".format(code, format)
53 | 
54 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
55 |         KiwoomOpenApiPlusEntrypoint,
56 |     )
57 | 
58 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
59 |         context.EnsureConnected()
60 |         df = context.GetDailyStockDataAsDataFrame(code, start_date, end_date)
61 | 
62 |     if format == "xlsx":
63 |         df.to_excel(output)
64 |         logger.info("Saved data to file: %s", output)
65 |     elif format == "sqlite3":
66 |         from sqlalchemy import create_engine
67 | 
68 |         engine = create_engine("sqlite:///" + output)
69 |         tablename = "A" + code
70 |         df.to_sql(tablename, engine)
71 |         logger.info("Saved data to file %s with tablename %s", output, tablename)
72 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/stock_meta/stockinfo.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.fail_with_usage import fail_with_usage
 4 | from koapy.cli.utils.verbose_option import verbose_option
 5 | from koapy.config import default_encoding
 6 | 
 7 | 
 8 | @click.command(short_help="Get basic information of stocks.")
 9 | @click.option("-c", "--code", metavar="CODE", help="Stock code to get.")
10 | @click.option(
11 |     "-o",
12 |     "--output",
13 |     metavar="FILENAME",
14 |     type=click.Path(),
15 |     help="Output filename. Optional for single code (prints to console).",
16 | )
17 | @click.option(
18 |     "-f",
19 |     "--format",
20 |     type=click.Choice(["md", "xlsx", "json"], case_sensitive=False),
21 |     help="Output format. (default: md)",
22 |     default="md",
23 |     show_choices=True,
24 | )
25 | @click.option(
26 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
27 | )
28 | @verbose_option()
29 | def stockinfo(code, output, format, port):  # pylint: disable=redefined-builtin
30 |     if (code, output) == (None, None):
31 |         fail_with_usage()
32 | 
33 |     if output is None:
34 |         if format is None:
35 |             format = "md"
36 |     else:
37 |         if format is None:
38 |             format = "xlsx"
39 |         if format == "xlsx":
40 |             if not output.endswith(".xlsx"):
41 |                 output += ".xlsx"
42 |         elif format == "md":
43 |             if not output.endswith(".md"):
44 |                 output += ".md"
45 |         elif format == "json":
46 |             if not output.endswith(".json"):
47 |                 output += ".json"
48 | 
49 |     import pandas as pd
50 | 
51 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
52 |         KiwoomOpenApiPlusEntrypoint,
53 |     )
54 | 
55 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
56 |         context.EnsureConnected()
57 |         dic = context.GetStockBasicInfoAsDict(code)
58 |         series = pd.Series(dic)
59 | 
60 |         if not output:
61 |             if format == "md":
62 |                 click.echo(series.to_markdown())
63 |             elif format == "json":
64 |                 click.echo(series.to_json())
65 |         else:
66 |             if format == "xlsx":
67 |                 series.to_excel(output, header=False)
68 |             elif format == "json":
69 |                 with open(output, "w", encoding=default_encoding) as f:
70 |                     click.echo(series.to_json(), file=f)
71 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/install/openapi.py:
--------------------------------------------------------------------------------
 1 | import contextlib
 2 | import os
 3 | import shutil
 4 | import subprocess
 5 | import tempfile
 6 | 
 7 | import click
 8 | 
 9 | from koapy.cli.utils.openapi import (
10 |     download_openapi_installer,
11 |     prepare_issfile_for_install,
12 |     run_installer_with_issfile,
13 | )
14 | from koapy.cli.utils.verbose_option import verbose_option
15 | from koapy.utils.logging import get_logger
16 | 
17 | logger = get_logger(__name__)
18 | 
19 | 
20 | @click.command(short_help="Install openapi module.")
21 | @click.option(
22 |     "-t",
23 |     "--target",
24 |     metavar="TARGET",
25 |     type=click.Path(),
26 |     default="C:\\",
27 |     help='Target directory for installation. Will create "OpenAPI" folder under this directory for install. (default: "C:\\")',
28 | )
29 | @click.option(
30 |     "--cleanup/--no-cleanup",
31 |     default=True,
32 |     help="Clean up temporary directory after install. (default: true)",
33 | )
34 | @verbose_option(default=5, show_default=True)
35 | def openapi(target, cleanup):
36 |     with contextlib.ExitStack() as stack:
37 |         tempdir = tempfile.mkdtemp()
38 |         logger.info("Created temporary directory: %s", tempdir)
39 |         if cleanup:
40 |             stack.callback(shutil.rmtree, tempdir)
41 |             logger.info("Registered to remove the temporary directory after install.")
42 |         installer_filename = "OpenAPISetup.exe"
43 |         installer_filepath = os.path.join(tempdir, installer_filename)
44 |         logger.info("Downloading installer: %s", installer_filepath)
45 |         download_openapi_installer(installer_filepath)
46 |         iss_filename = "setup.iss"
47 |         iss_filepath = os.path.join(tempdir, iss_filename)
48 |         logger.info("Preparing .iss file: %s", iss_filepath)
49 |         prepare_issfile_for_install(iss_filepath, target)
50 |         log_filename = "setup.log"
51 |         log_filepath = os.path.join(tempdir, log_filename)
52 |         try:
53 |             _return_code = run_installer_with_issfile(
54 |                 installer_filepath, iss_filepath, log_filepath, cwd=tempdir
55 |             )
56 |         except subprocess.CalledProcessError as e:
57 |             logger.exception(
58 |                 "Failed to install openapi with return code: %d", e.returncode
59 |             )
60 |             raise RuntimeError(
61 |                 "Failed to install openapi with return code: %d" % e.returncode
62 |             ) from e
63 |         else:
64 |             logger.info("Succesfully installed openapi.")
65 | 


--------------------------------------------------------------------------------
/koapy/utils/store/sqlalchemy/Snapshot.py:
--------------------------------------------------------------------------------
 1 | from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint
 2 | from sqlalchemy.orm import object_session, relationship
 3 | from sqlalchemy.sql.functions import current_timestamp
 4 | 
 5 | from .Base import Base
 6 | from .SnapshotAssociation import SnapshotAssociation
 7 | from .Symbol import Symbol
 8 | from .Timestamp import Timestamp
 9 | from .Version import Version
10 | 
11 | 
12 | class Snapshot(Base):
13 |     __tablename__ = "snapshots"
14 | 
15 |     id = Column(Integer, primary_key=True)
16 |     name = Column(String, index=True, nullable=False)
17 | 
18 |     timestamp = Column(Timestamp(timezone=True), server_default=current_timestamp())
19 | 
20 |     library_id = Column(Integer, ForeignKey("libraries.id"), nullable=False)
21 |     library = relationship("Library", back_populates="snapshots")
22 | 
23 |     versions = relationship("SnapshotAssociation", back_populates="snapshot")
24 | 
25 |     __table_args__ = (UniqueConstraint("library_id", "name"),)
26 | 
27 |     def get_symbols(self):
28 |         session = object_session(self)
29 |         associations = session.query(SnapshotAssociation).with_parent(self)
30 |         versions = session.query(Version).join(associations.subquery())
31 |         symbols = session.query(Symbol).join(versions.subquery())
32 |         symbols = symbols.all()
33 |         return symbols
34 | 
35 |     def get_versions(self):
36 |         session = object_session(self)
37 |         associations = session.query(SnapshotAssociation).with_parent(self)
38 |         versions = session.query(Version).join(associations.subquery())
39 |         versions = versions.all()
40 |         return versions
41 | 
42 |     def get_version_of_symbol(self, symbol):
43 |         session = object_session(self)
44 |         if isinstance(symbol, str):
45 |             symbol = (
46 |                 session.query(Symbol)
47 |                 .filter(Symbol.library_id == self.library_id and Symbol.name == symbol)
48 |                 .one()
49 |             )
50 |         associations = session.query(SnapshotAssociation).with_parent(self)
51 |         version = (
52 |             session.query(Version)
53 |             .with_parent(symbol)
54 |             .join(associations.subquery())
55 |             .one()
56 |         )
57 |         return version
58 | 
59 |     def delete(self):
60 |         session = object_session(self)
61 |         associations = session.query(SnapshotAssociation).with_parent(self)
62 |         associations.delete()
63 |         session.delete(self)
64 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_w/core/KiwoomOpenApiWSignalConnector.py:
--------------------------------------------------------------------------------
 1 | import inspect
 2 | import threading
 3 | 
 4 | from koapy.backend.kiwoom_open_api_w.core.KiwoomOpenApiWSignature import (
 5 |     KiwoomOpenApiWEventHandlerSignature,
 6 | )
 7 | from koapy.compat.pyside2 import PYQT5, PYSIDE2, PythonQtError
 8 | from koapy.utils.logging.Logging import Logging
 9 | 
10 | 
11 | class KiwoomOpenApiWSignalConnector(Logging):
12 |     def __init__(self, name=None):
13 |         super().__init__()
14 | 
15 |         self._name = name
16 |         self._lock = threading.RLock()
17 |         self._slots = list()
18 | 
19 |         self._signature = KiwoomOpenApiWEventHandlerSignature.from_name(self._name)
20 |         self._param_types = [
21 |             (p.annotation) for p in self._signature.parameters.values()
22 |         ]
23 |         self._signal = self._signature.to_pyside2_event_signal()
24 | 
25 |     def is_valid_slot(self, slot):
26 |         slot_signature = inspect.signature(slot)
27 |         slot_types = [(p.annotation) for p in slot_signature.parameters.values()]
28 |         condition = len(self._param_types) == len(
29 |             slot_types
30 |         )  # currently only check parameter length
31 |         return condition
32 | 
33 |     def connect_to(self, control):
34 |         if PYSIDE2:
35 |             return control.connect(self._signal, self)
36 |         elif PYQT5:
37 |             return getattr(control, self._name).connect(self)
38 |         else:
39 |             raise PythonQtError("Unsupported Qt bindings")
40 | 
41 |     def connect(self, slot):
42 |         if not self.is_valid_slot(slot):
43 |             raise TypeError("Invalid slot: %s" % slot)
44 |         with self._lock:
45 |             if slot not in self._slots:
46 |                 self._slots.append(slot)
47 | 
48 |     def disconnect(self, slot=None):
49 |         with self._lock:
50 |             if slot is None:
51 |                 self._slots.clear()
52 |             elif slot in self._slots:
53 |                 self._slots.remove(slot)
54 |             else:
55 |                 self.logger.warning("Tried to disconnect a slot that doesn't exist")
56 | 
57 |     def call(self, *args, **kwargs):
58 |         # make a copy in order to prevent modification during iteration problem
59 |         with self._lock:
60 |             slots = list(self._slots)
61 |         # TODO: use Thread with await/join for concurrency?
62 |         for slot in slots:
63 |             slot(*args, **kwargs)
64 | 
65 |     def __call__(self, *args, **kwargs):
66 |         return self.call(*args, **kwargs)
67 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusDispatchSignature.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Dict, List
 4 | 
 5 | import pythoncom
 6 | 
 7 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusDispatchFunctions import (
 8 |     KiwoomOpenApiPlusDispatchFunctions,
 9 | )
10 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusSignature import (
11 |     KiwoomOpenApiPlusSignature,
12 | )
13 | from koapy.utils.builtin import dir_public
14 | 
15 | 
16 | class KiwoomOpenApiPlusDispatchSignature(KiwoomOpenApiPlusSignature):
17 | 
18 |     DISPATCH_SIGNATURES_BY_NAME: Dict[str, KiwoomOpenApiPlusDispatchSignature] = {}
19 | 
20 |     @classmethod
21 |     def from_name(cls, name: str) -> KiwoomOpenApiPlusDispatchSignature:
22 |         signature = cls.DISPATCH_SIGNATURES_BY_NAME[name]
23 |         return signature
24 | 
25 |     @classmethod
26 |     def names(cls) -> List[str]:
27 |         names = cls.DISPATCH_SIGNATURES_BY_NAME.keys()
28 |         names = list(names)
29 |         if not names:
30 |             _names = dir_public(KiwoomOpenApiPlusDispatchFunctions)
31 |         return names
32 | 
33 |     @classmethod
34 |     def _make_dispatch_signatures_by_name(
35 |         cls,
36 |     ) -> Dict[str, KiwoomOpenApiPlusDispatchSignature]:
37 |         from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusTypeLib import (
38 |             DISPATCH_OLE_ITEM,
39 |         )
40 | 
41 |         dispatch = DISPATCH_OLE_ITEM
42 |         dispatch_signatures_by_name: Dict[str, KiwoomOpenApiPlusDispatchSignature] = {}
43 |         if dispatch:
44 |             dispatch_funcs = [
45 |                 (name, entry)
46 |                 for name, entry in dispatch.mapFuncs.items()
47 |                 if not any(
48 |                     [
49 |                         entry.desc[9] & pythoncom.FUNCFLAG_FRESTRICTED
50 |                         and entry.desc[0] != pythoncom.DISPID_NEWENUM,
51 |                         entry.desc[3] != pythoncom.FUNC_DISPATCH,
52 |                         entry.desc[0] == pythoncom.DISPID_NEWENUM,
53 |                     ]
54 |                 )
55 |             ]
56 |             for func_name, entry in dispatch_funcs:
57 |                 signature = cls._from_entry(func_name, entry)
58 |                 dispatch_signatures_by_name[func_name] = signature
59 |         return dispatch_signatures_by_name
60 | 
61 |     @classmethod
62 |     def _initialize(cls):
63 |         cls.DISPATCH_SIGNATURES_BY_NAME = cls._make_dispatch_signatures_by_name()
64 | 
65 | 
66 | KiwoomOpenApiPlusDispatchSignature._initialize()  # pylint: disable=protected-access
67 | 


--------------------------------------------------------------------------------
/koapy/utils/rate_limiting/RateLimiter.py:
--------------------------------------------------------------------------------
 1 | import collections
 2 | import threading
 3 | import time
 4 | 
 5 | from typing import List
 6 | 
 7 | from koapy.utils.logging.Logging import Logging
 8 | 
 9 | 
10 | class RateLimiter:
11 |     def check_sleep_seconds(self, *args, **kwargs):
12 |         return 0
13 | 
14 |     def add_call_history(self, *args, **kwargs):
15 |         pass
16 | 
17 |     def sleep_if_necessary(self, *args, **kwargs):
18 |         sleep_seconds = self.check_sleep_seconds(*args, **kwargs)
19 |         if sleep_seconds > 0:
20 |             time.sleep(sleep_seconds)
21 | 
22 | 
23 | class TimeWindowRateLimiter(RateLimiter, Logging):
24 |     def __init__(self, period, calls):
25 |         super().__init__()
26 | 
27 |         self._period = period
28 |         self._calls = calls
29 | 
30 |         self._lock = threading.RLock()
31 |         self._clock = time.time
32 | 
33 |         if hasattr(time, "monotonic"):
34 |             self._clock = time.monotonic
35 | 
36 |         self._call_history = collections.deque(maxlen=self._calls)
37 | 
38 |     def check_sleep_seconds(self, *args, **kwargs):
39 |         with self._lock:
40 |             if len(self._call_history) < self._calls:
41 |                 return 0
42 |             clock = self._clock()
43 |             remaining = self._call_history[0] + self._period - clock
44 |             return remaining
45 | 
46 |     def add_call_history(self, *args, **kwargs):
47 |         with self._lock:
48 |             return self._call_history.append(self._clock())
49 | 
50 |     def sleep_if_necessary(self, *args, **kwargs):
51 |         with self._lock:
52 |             sleep_seconds = self.check_sleep_seconds(*args, **kwargs)
53 |             if sleep_seconds > 0:
54 |                 time.sleep(sleep_seconds)
55 | 
56 | 
57 | class CompositeTimeWindowRateLimiter(RateLimiter):
58 |     def __init__(self, limiters: List[RateLimiter]):
59 |         super().__init__()
60 | 
61 |         self._lock = threading.RLock()
62 |         self._limiters = limiters
63 | 
64 |     def check_sleep_seconds(self, *args, **kwargs):
65 |         with self._lock:
66 |             return max(
67 |                 limiter.check_sleep_seconds(*args, **kwargs)
68 |                 for limiter in self._limiters
69 |             )
70 | 
71 |     def add_call_history(self, *args, **kwargs):
72 |         with self._lock:
73 |             for limiter in self._limiters:
74 |                 limiter.add_call_history(*args, **kwargs)
75 | 
76 |     def sleep_if_necessary(self, *args, **kwargs):
77 |         with self._lock:
78 |             sleep_seconds = self.check_sleep_seconds(*args, **kwargs)
79 |             if sleep_seconds > 0:
80 |                 time.sleep(sleep_seconds)
81 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusSignature.py:
--------------------------------------------------------------------------------
 1 | from inspect import Parameter, Signature
 2 | from typing import Dict
 3 | 
 4 | import pythoncom
 5 | 
 6 | from koapy.compat.pyside2 import PYSIDE2
 7 | 
 8 | if PYSIDE2:
 9 |     from koapy.compat.pyside2.QtCore import SIGNAL
10 | 
11 | try:
12 |     from types import NoneType
13 | except ImportError:
14 |     NoneType = type(None)
15 | 
16 | 
17 | class KiwoomOpenApiPlusSignature(Signature):
18 | 
19 |     PYTHONTYPE_TO_QTTYPE = {
20 |         int: "int",
21 |         str: "const QString&",
22 |     }
23 | 
24 |     COMTYPE_TO_PYTHONTYPE = {
25 |         pythoncom.VT_I4: int,
26 |         pythoncom.VT_BSTR: str,
27 |         pythoncom.VT_VARIANT: Parameter.empty,
28 |         pythoncom.VT_VOID: NoneType,
29 |     }
30 | 
31 |     def __init__(
32 |         self,
33 |         name: str,
34 |         parameters: Dict[str, Parameter] = None,
35 |         return_annotation=Signature.empty,
36 |     ):
37 |         self._name = name
38 |         super().__init__(parameters, return_annotation=return_annotation)
39 | 
40 |     @property
41 |     def name(self) -> str:
42 |         return self._name
43 | 
44 |     @classmethod
45 |     def _pythontype_to_qttype(cls, typ):
46 |         return cls.PYTHONTYPE_TO_QTTYPE[typ]
47 | 
48 |     def to_pyside2_function_prototype(self) -> str:
49 |         name = self._name
50 |         parameters = self.parameters
51 |         parameters = parameters.values()
52 |         parameters = [self._pythontype_to_qttype(p.annotation) for p in parameters]
53 |         prototype = "{}({})".format(name, ", ".join(parameters))
54 |         return prototype
55 | 
56 |     if PYSIDE2:
57 | 
58 |         def to_pyside2_event_signal(self) -> str:
59 |             return SIGNAL(self.to_pyside2_function_prototype())
60 | 
61 |     @classmethod
62 |     def _comtype_to_pythontype(cls, typ):
63 |         return cls.COMTYPE_TO_PYTHONTYPE.get(typ, Parameter.empty)
64 | 
65 |     @classmethod
66 |     def _from_entry(cls, name, entry):
67 |         arg_names = entry.names[1:]
68 |         arg_types = [typ[0] for typ in entry.desc[2]]
69 |         return_type = entry.desc[8][0]
70 |         parameters = [
71 |             Parameter(
72 |                 name=arg_name,
73 |                 kind=Parameter.POSITIONAL_OR_KEYWORD,
74 |                 annotation=cls._comtype_to_pythontype(arg_type),
75 |             )
76 |             for arg_name, arg_type in zip(arg_names, arg_types)
77 |         ]
78 |         return_annotation = cls._comtype_to_pythontype(return_type)
79 |         signature = cls(
80 |             name=name,
81 |             parameters=parameters,
82 |             return_annotation=return_annotation,
83 |         )
84 |         return signature
85 | 


--------------------------------------------------------------------------------
/koapy/cli/utils/openapi.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import shutil
 3 | import subprocess
 4 | 
 5 | import requests
 6 | 
 7 | from koapy.utils.logging import get_logger
 8 | from koapy.utils.subprocess import quote
 9 | 
10 | logger = get_logger(__name__)
11 | 
12 | iss_file_encoding = "euc-kr"
13 | 
14 | 
15 | def download_openapi_installer(filepath):
16 |     url = "https://download.kiwoom.com/web/openapi/OpenAPISetup.exe"
17 |     response = requests.get(url)
18 |     with open(filepath, "wb") as f:
19 |         f.write(response.content)
20 | 
21 | 
22 | def prepare_issfile_for_install(filepath, target=None):
23 |     iss_filedir = os.path.join(
24 |         os.path.dirname(__file__),
25 |         "../../backend/kiwoom_open_api_plus/data/scripts/",
26 |     )
27 |     iss_filename = "install.iss"
28 |     iss_filepath = os.path.join(iss_filedir, iss_filename)
29 |     shutil.copy(iss_filepath, filepath)
30 |     if target is not None:
31 |         with open(filepath, "r", encoding=iss_file_encoding) as f:
32 |             lines = [line for line in f]
33 |         for i, line in enumerate(lines):
34 |             if line.startswith("szDir="):
35 |                 lines[i] = "szDir={}\n".format(target)
36 |                 break
37 |         with open(filepath, "w", encoding=iss_file_encoding) as f:
38 |             for line in lines:
39 |                 f.write(line)
40 | 
41 | 
42 | def prepare_issfile_for_uninstall(filepath, reboot=False):
43 |     iss_filedir = os.path.join(
44 |         os.path.dirname(__file__),
45 |         "../../backend/kiwoom_open_api_plus/data/scripts/",
46 |     )
47 |     iss_filename = "uninstall.iss"
48 |     iss_filepath = os.path.join(iss_filedir, iss_filename)
49 |     shutil.copy(iss_filepath, filepath)
50 |     if reboot:
51 |         with open(filepath, "r", encoding=iss_file_encoding) as f:
52 |             lines = [line for line in f]
53 |         for i, line in enumerate(lines):
54 |             if line.startswith("BootOption="):
55 |                 boot_option = 3
56 |                 lines[i] = "BootOption={}\n".format(boot_option)
57 |                 break
58 |         with open(filepath, "w", encoding=iss_file_encoding) as f:
59 |             for line in lines:
60 |                 f.write(line)
61 | 
62 | 
63 | def run_installer_with_issfile(installer, issfile, logfile=None, cwd=None):
64 |     cmd = [quote(installer), "/s", "/f1{}".format(quote(issfile))]
65 |     if logfile is not None:
66 |         cmd.extend(["/f2{}".format(quote(logfile))])
67 |     # should use shell=True in order not to escape quotes in args
68 |     logger.info("Running command: %s", " ".join(cmd))
69 |     proc = subprocess.run(" ".join(cmd), shell=True, check=False, cwd=cwd)
70 |     if proc.returncode < 0:
71 |         raise subprocess.CalledProcessError(proc.returncode, " ".join(cmd))
72 |     return proc.returncode
73 | 


--------------------------------------------------------------------------------
/koapy/cli/commands/get/account_data/orders.py:
--------------------------------------------------------------------------------
 1 | import click
 2 | 
 3 | from koapy.cli.utils.verbose_option import verbose_option
 4 | from koapy.utils.logging import get_logger
 5 | 
 6 | logger = get_logger(__name__)
 7 | 
 8 | 
 9 | @click.command(short_help="Get order history of a date.")
10 | @click.option("-a", "--account", metavar="ACCNO", help="Account number.")
11 | @click.option("-d", "--date", metavar="DATE", help="Date to get.")
12 | @click.option("-r", "--reverse", is_flag=True)
13 | @click.option("-e", "--executed-only", is_flag=True)
14 | @click.option("-E", "--not-executed-only", is_flag=True)
15 | @click.option("-S", "--stock-only", is_flag=True)
16 | @click.option("-B", "--bond-only", is_flag=True)
17 | @click.option("-s", "--sell-only", is_flag=True)
18 | @click.option("-b", "--buy-only", is_flag=True)
19 | @click.option("-c", "--code", metavar="CODE", help="Stock code to get.")
20 | @click.option("-o", "--starting-order-no", metavar="ORDERNO", help="Starting order no.")
21 | @click.option(
22 |     "-p", "--port", metavar="PORT", help="Port number of grpc server (optional)."
23 | )
24 | @verbose_option()
25 | def orders(
26 |     account,
27 |     date,
28 |     reverse,
29 |     executed_only,
30 |     not_executed_only,
31 |     stock_only,
32 |     bond_only,
33 |     sell_only,
34 |     buy_only,
35 |     code,
36 |     starting_order_no,
37 |     port,
38 | ):
39 |     if account is None:
40 |         logger.info("Account not given. Using first account available.")
41 | 
42 |     sort_type = "1"
43 |     if reverse:
44 |         sort_type = "2"
45 |     if executed_only:
46 |         sort_type = "3"
47 |     if not_executed_only:
48 |         sort_type = "4"
49 |     asset_type = "0"
50 |     if stock_only:
51 |         asset_type = "1"
52 |     if bond_only:
53 |         asset_type = "2"
54 |     order_type = "0"
55 |     if sell_only:
56 |         order_type = "1"
57 |     if buy_only:
58 |         order_type = "2"
59 | 
60 |     from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusEntrypoint import (
61 |         KiwoomOpenApiPlusEntrypoint,
62 |     )
63 | 
64 |     with KiwoomOpenApiPlusEntrypoint(port=port) as context:
65 |         context.EnsureConnected()
66 |         if account is None:
67 |             account = context.GetFirstAvailableAccount()
68 |         df = context.GetOrderLogAsDataFrame1(account)
69 |         click.echo("[실시간미체결요청]")
70 |         click.echo(df.to_markdown())
71 |         click.echo()
72 |         df = context.GetOrderLogAsDataFrame2(account)
73 |         click.echo("[실시간체결요청]")
74 |         click.echo(df.to_markdown())
75 |         click.echo()
76 |         df = context.GetOrderLogAsDataFrame3(
77 |             account, date, sort_type, asset_type, order_type, code, starting_order_no
78 |         )
79 |         click.echo("[계좌별주문체결내역상세요청]")
80 |         click.echo(df.to_markdown())
81 | 


--------------------------------------------------------------------------------
/koapy/utils/store/SQLiteStore.py:
--------------------------------------------------------------------------------
 1 | from sqlalchemy import create_engine, inspect
 2 | 
 3 | from .sqlalchemy.Base import Base
 4 | from .sqlalchemy.Library import Library
 5 | from .sqlalchemy.Session import Session
 6 | from .SQLiteStoreLibrary import SQLiteStoreLibrary
 7 | 
 8 | 
 9 | class SQLiteStore:
10 |     def __init__(self, filename):
11 |         self._filename = filename
12 |         self._engine = create_engine("sqlite:///" + self._filename)
13 | 
14 |         Session.configure(bind=self._engine)
15 | 
16 |         inspector = inspect(self._engine)
17 |         table_names = inspector.get_table_names()
18 | 
19 |         if len(table_names) == 0:
20 |             Base.metadata.create_all(self._engine)
21 |             inspector = inspect(self._engine)
22 |             table_names = inspector.get_table_names()
23 | 
24 |         assert all(table_name in table_names for table_name in Base.metadata.tables)
25 | 
26 |         self._session = Session()
27 |         self._library_cache = {}
28 | 
29 |     def list_libraries(self):
30 |         libraries = self._session.query(Library).all()
31 |         libraries = [library.name for library in libraries]
32 |         return libraries
33 | 
34 |     def _get_library(self, library):
35 |         library = self._session.query(Library).filter(Library.name == library).one()
36 |         return library
37 | 
38 |     def library_exists(self, library):
39 |         try:
40 |             _library = self._get_library(library)
41 |             return True
42 |         except:
43 |             return False
44 | 
45 |     def initialize_library(self, library):
46 |         library_name = library
47 |         if not self.library_exists(library_name):
48 |             library = Library(name=library_name)
49 |             try:
50 |                 self._session.add(library)
51 |                 self._session.commit()
52 |             except:
53 |                 self._session.rollback()
54 |                 raise
55 | 
56 |     def get_library(self, library):
57 |         library_name = library
58 |         if library_name in self._library_cache:
59 |             return self._library_cache[library_name]
60 |         library = self._get_library(library)
61 |         library = SQLiteStoreLibrary(self, library)
62 |         self._library_cache[library_name] = library
63 |         return library
64 | 
65 |     def get_or_create_library(self, library):
66 |         if not self.library_exists(library):
67 |             self.initialize_library(library)
68 |         library = self.get_library(library)
69 |         return library
70 | 
71 |     def delete_library(self, library):
72 |         library_name = library
73 |         library = self._get_library(library_name)
74 |         try:
75 |             library.delete()
76 |             self._session.commit()
77 |         except:
78 |             self._session.rollback()
79 |             raise
80 | 
81 |     def __getitem__(self, library):
82 |         return self.get_library(library)
83 | 


--------------------------------------------------------------------------------
/koapy/examples/09_get_historical_data.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | 
 3 | logging.basicConfig(
 4 |     format="%(asctime)s [%(levelname)s] %(message)s - %(filename)s:%(lineno)d",
 5 |     level=logging.DEBUG,
 6 | )
 7 | 
 8 | import datetime
 9 | 
10 | from exchange_calendars import get_calendar
11 | 
12 | from koapy import KiwoomOpenApiPlusEntrypoint
13 | 
14 | krx_calendar = get_calendar("XKRX")
15 | 
16 | with KiwoomOpenApiPlusEntrypoint() as context:
17 |     # 로그인 처리
18 |     context.EnsureConnected()
19 | 
20 |     # 종목코드
21 |     code = "005930"
22 | 
23 |     # 가장 최근 날짜
24 |     start_date = datetime.datetime.now()
25 | 
26 |     # 날짜 값은 datetime.datetime 타입도 되고 문자열도 됨
27 |     # 문자열 포맷은 구체적으로, 일봉위로는 YYYYMMDD 포맷, 분봉아래로는 YYYYMMDDhhmmss 포맷 지원
28 | 
29 |     # 가장 오래된 날짜 (거래소 개장일 기준 30일 전)
30 |     end_date = start_date - 30 * krx_calendar.day
31 | 
32 |     # 참고로 end_date 은 주어진 시간보다 큰 (같지 않은) 레코드만 가져오도록 구현되어있기 때문에 설정에 주의
33 |     # 일반적으로 해당 날짜는 결과에서 제외되는 효과 있음, 구체적인 구간은 [start_date, end_date)
34 | 
35 |     # 위와 같은 (end_date = start_date - 30 * krx_calendar.day) 코드는 거래소가 열리는 날짜 기준 30일 전 날짜 계산하는데 사용
36 |     # 이렇게 설정해서 일봉데이터 가져올 경우 당일 포함되면 30일치, 당일 제외되면 29일치 가져오게 됨
37 |     # 참고로 exchange_calendars 는 현재 년도의 말일까지 미리 계산된 휴일정보를 사용하므로 미래시간은 계산이 안됨
38 | 
39 |     # 만약에 미래 시간에 대한 연산 또한 필요하다면 아래처럼 내부 구현체를 대체재로 사용해 볼 수 잇음
40 |     #   from koapy.utils.krx.calendar.KrxHolidayCalendar import KrxBusinessDay
41 |     #   end_date = start_date + KrxBusinessDay(30) # (거래소 개장일 기준 30일 이후)
42 |     # 하지만 계산시 일반적인 휴일들을 제외한 임시공휴일, 선거일등은 별도 추가 데이터가 없는 한 반영되지 않으니 주의
43 | 
44 |     # 일봉 데이터
45 |     logging.info("Requesting daily data from %s to %s", end_date, start_date)
46 |     data = context.GetDailyStockDataAsDataFrame(code, start_date, end_date)
47 |     print(data)
48 | 
49 |     # 15분봉 데이터
50 |     # 분봉과 틱봉은 TR 에서 기준날짜 (기간설정시 최근날짜) 지정이 안되므로
51 |     # 가장 최근부터 가져오고 클라이언트에서 따로 추가로 거르는 식으로 구현되어 있음
52 |     end_date = start_date - 1 * krx_calendar.day
53 |     logging.info("Requesting minute data from %s to %s", end_date, start_date)
54 |     data = context.GetMinuteStockDataAsDataFrame(code, 15, start_date, end_date)
55 |     print(data)
56 | 
57 |     # 주봉 데이터
58 |     # 참고로 주봉은 각 주의 첫 개장일을 기준일으로 함
59 |     end_date = start_date - 30 * krx_calendar.day
60 |     logging.info("Requesting weekly data from %s to %s", end_date, start_date)
61 |     data = context.GetWeeklyStockDataAsDataFrame(code, start_date, end_date)
62 |     print(data)
63 | 
64 |     # 월봉 데이터
65 |     # 참고로 월봉은 각 달의 첫 개장일을 기준일으로 함
66 |     end_date = start_date - 180 * krx_calendar.day
67 |     logging.info("Requesting monthly data from %s to %s", end_date, start_date)
68 |     data = context.GetMonthlyStockDataAsDataFrame(code, start_date, end_date)
69 |     print(data)
70 | 
71 |     # 30틱 데이터
72 |     # 분봉과 틱봉은 TR 에서 기준날짜 (기간설정시 최근날짜) 지정이 안되므로
73 |     # 가장 최근부터 가져오고 클라이언트에서 따로 추가로 거르는 식으로 구현되어 있음
74 |     end_date = start_date - 1 * krx_calendar.day
75 |     logging.info("Requesting tick data from %s to %s", end_date, start_date)
76 |     data = context.GetTickStockDataAsDataFrame(code, 30, start_date, end_date)
77 |     print(data)
78 | 


--------------------------------------------------------------------------------
/koapy/backend/kiwoom_open_api_plus/core/KiwoomOpenApiPlusDynamicCallable.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from inspect import Signature
 4 | from typing import Any, Callable, List, Sequence
 5 | 
 6 | try:
 7 |     from typing import ParamSpec, TypeVar
 8 | except ImportError:
 9 |     from typing_extensions import ParamSpec
10 |     from typing import TypeVar
11 | 
12 | from koapy.backend.kiwoom_open_api_plus.core.KiwoomOpenApiPlusDispatchSignature import (
13 |     KiwoomOpenApiPlusDispatchSignature,
14 | )
15 | from koapy.compat.pyside2.QtAxContainer import QAxWidget
16 | 
17 | P = ParamSpec("P")
18 | R = TypeVar("R")
19 | 
20 | 
21 | class KiwoomOpenApiPlusDynamicCallable(Callable[P, R]):
22 |     def __init__(
23 |         self,
24 |         control: QAxWidget,
25 |         name: str,
26 |     ):
27 |         self._control = control
28 |         self._name = name
29 | 
30 |         self._signature = KiwoomOpenApiPlusDispatchSignature.from_name(self._name)
31 |         self._function = self._signature.to_pyside2_function_prototype()
32 |         self._has_return_value = (
33 |             self._signature.return_annotation is not Signature.empty
34 |         )
35 | 
36 |         self.__name__ = self._name
37 |         self.__signature__ = self._signature
38 | 
39 |     def bind_dynamic_call_args(self, *args, **kwargs) -> List[Any]:
40 |         try:
41 |             ba = self._signature.bind(*args, **kwargs)
42 |         except TypeError as e:
43 |             raise TypeError(
44 |                 "Exception while binding arguments for function: %s" % self._name
45 |             ) from e
46 |         ba.apply_defaults()
47 |         args = list(ba.args)
48 |         return args
49 | 
50 |     def is_valid_return_type(self, result: Any) -> bool:
51 |         is_valid = True
52 |         if self._has_return_value:
53 |             if not isinstance(result, self._signature.return_annotation):
54 |                 is_valid = False
55 |         return is_valid
56 | 
57 |     def check_return_value(self, result: Any):
58 |         if not self.is_valid_return_type(result):
59 |             raise TypeError(
60 |                 "Return type of %s was expected for function call %s(...), but %s was found"
61 |                 % (
62 |                     self._signature.return_annotation,
63 |                     self._name,
64 |                     type(result),
65 |                 )
66 |             )
67 |         return result
68 | 
69 |     def dynamic_call(self, args: Sequence[Any]) -> R:
70 |         return self._control.dynamicCall(self._function, args)
71 | 
72 |     def dynamic_call_and_check(self, args: Sequence[Any]) -> R:
73 |         result = self.dynamic_call(args)
74 |         self.check_return_value(result)
75 |         return result
76 | 
77 |     def call(self, *args: P.args, **kwargs: P.kwargs) -> R:
78 |         args = self.bind_dynamic_call_args(*args, **kwargs)
79 |         result = self.dynamic_call_and_check(args)
80 |         return result
81 | 
82 |     def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
83 |         return self.call(*args, **kwargs)
84 | 


--------------------------------------------------------------------------------