axdata 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- axdata/__init__.py +16 -0
- axdata/cache.py +19 -0
- axdata/client.py +1223 -0
- axdata-0.1.0.dist-info/METADATA +238 -0
- axdata-0.1.0.dist-info/RECORD +427 -0
- axdata-0.1.0.dist-info/WHEEL +5 -0
- axdata-0.1.0.dist-info/entry_points.txt +8 -0
- axdata-0.1.0.dist-info/licenses/LICENSE +162 -0
- axdata-0.1.0.dist-info/top_level.txt +6 -0
- axdata_core/__init__.py +221 -0
- axdata_core/_tdx_wire/__init__.py +35 -0
- axdata_core/_tdx_wire/_shim.py +140 -0
- axdata_core/_tdx_wire/api/__init__.py +51 -0
- axdata_core/_tdx_wire/api/auction.py +24 -0
- axdata_core/_tdx_wire/api/bars.py +24 -0
- axdata_core/_tdx_wire/api/base.py +26 -0
- axdata_core/_tdx_wire/api/codes.py +25 -0
- axdata_core/_tdx_wire/api/corporate.py +24 -0
- axdata_core/_tdx_wire/api/finance.py +24 -0
- axdata_core/_tdx_wire/api/intraday.py +24 -0
- axdata_core/_tdx_wire/api/quotes.py +26 -0
- axdata_core/_tdx_wire/api/resources.py +25 -0
- axdata_core/_tdx_wire/api/session.py +24 -0
- axdata_core/_tdx_wire/api/trades.py +24 -0
- axdata_core/_tdx_wire/client.py +33 -0
- axdata_core/_tdx_wire/exceptions.py +27 -0
- axdata_core/_tdx_wire/hosts.py +106 -0
- axdata_core/_tdx_wire/models/__init__.py +93 -0
- axdata_core/_tdx_wire/models/auction.py +26 -0
- axdata_core/_tdx_wire/models/corporate.py +27 -0
- axdata_core/_tdx_wire/models/finance.py +26 -0
- axdata_core/_tdx_wire/models/intraday.py +31 -0
- axdata_core/_tdx_wire/models/kline.py +27 -0
- axdata_core/_tdx_wire/models/quote.py +32 -0
- axdata_core/_tdx_wire/models/resource.py +24 -0
- axdata_core/_tdx_wire/models/security.py +24 -0
- axdata_core/_tdx_wire/models/session.py +27 -0
- axdata_core/_tdx_wire/models/subchart.py +25 -0
- axdata_core/_tdx_wire/models/trade.py +28 -0
- axdata_core/_tdx_wire/protocol/__init__.py +49 -0
- axdata_core/_tdx_wire/protocol/commands/__init__.py +43 -0
- axdata_core/_tdx_wire/protocol/commands/auction.py +110 -0
- axdata_core/_tdx_wire/protocol/commands/codec.py +43 -0
- axdata_core/_tdx_wire/protocol/commands/corporate.py +98 -0
- axdata_core/_tdx_wire/protocol/commands/finance.py +96 -0
- axdata_core/_tdx_wire/protocol/commands/intraday.py +122 -0
- axdata_core/_tdx_wire/protocol/commands/klines.py +102 -0
- axdata_core/_tdx_wire/protocol/commands/lookup.py +36 -0
- axdata_core/_tdx_wire/protocol/commands/price_limits.py +89 -0
- axdata_core/_tdx_wire/protocol/commands/quotes.py +102 -0
- axdata_core/_tdx_wire/protocol/commands/registry.py +36 -0
- axdata_core/_tdx_wire/protocol/commands/resources.py +99 -0
- axdata_core/_tdx_wire/protocol/commands/security.py +103 -0
- axdata_core/_tdx_wire/protocol/commands/session.py +82 -0
- axdata_core/_tdx_wire/protocol/commands/subchart.py +97 -0
- axdata_core/_tdx_wire/protocol/commands/trades.py +107 -0
- axdata_core/_tdx_wire/protocol/constants.py +124 -0
- axdata_core/_tdx_wire/protocol/frame.py +68 -0
- axdata_core/_tdx_wire/protocol/unit.py +85 -0
- axdata_core/_tdx_wire/py.typed +1 -0
- axdata_core/_tdx_wire/transport/__init__.py +34 -0
- axdata_core/_tdx_wire/transport/base.py +25 -0
- axdata_core/_tdx_wire/transport/memory.py +24 -0
- axdata_core/_tdx_wire/transport/pool.py +32 -0
- axdata_core/_tdx_wire/transport/socket.py +39 -0
- axdata_core/adapters/__init__.py +0 -0
- axdata_core/adapters/cls/__init__.py +17 -0
- axdata_core/adapters/cls/provider_bridge.py +23 -0
- axdata_core/adapters/cls/request.py +632 -0
- axdata_core/adapters/cls/source_adapter_registry.py +31 -0
- axdata_core/adapters/cninfo/__init__.py +17 -0
- axdata_core/adapters/cninfo/provider_bridge.py +25 -0
- axdata_core/adapters/cninfo/request.py +2425 -0
- axdata_core/adapters/cninfo/source_adapter_registry.py +42 -0
- axdata_core/adapters/eastmoney/__init__.py +17 -0
- axdata_core/adapters/eastmoney/provider_bridge.py +25 -0
- axdata_core/adapters/eastmoney/request.py +1128 -0
- axdata_core/adapters/eastmoney/source_adapter_registry.py +42 -0
- axdata_core/adapters/exchange/__init__.py +17 -0
- axdata_core/adapters/exchange/provider_bridge.py +25 -0
- axdata_core/adapters/exchange/request.py +1214 -0
- axdata_core/adapters/exchange/source_adapter_registry.py +42 -0
- axdata_core/adapters/kph/__init__.py +17 -0
- axdata_core/adapters/kph/provider_bridge.py +23 -0
- axdata_core/adapters/kph/request.py +509 -0
- axdata_core/adapters/kph/source_adapter_registry.py +31 -0
- axdata_core/adapters/sina/__init__.py +17 -0
- axdata_core/adapters/sina/provider_bridge.py +25 -0
- axdata_core/adapters/sina/request.py +5155 -0
- axdata_core/adapters/sina/source_adapter_registry.py +42 -0
- axdata_core/adapters/tdx/__init__.py +32 -0
- axdata_core/adapters/tdx/adjustment.py +50 -0
- axdata_core/adapters/tdx/adjustment_fetch.py +51 -0
- axdata_core/adapters/tdx/auction_fetch.py +51 -0
- axdata_core/adapters/tdx/client_factory.py +110 -0
- axdata_core/adapters/tdx/code_fetch.py +62 -0
- axdata_core/adapters/tdx/codes.py +54 -0
- axdata_core/adapters/tdx/derived_rows.py +69 -0
- axdata_core/adapters/tdx/downloader.py +193 -0
- axdata_core/adapters/tdx/downloader_interface_sets.py +50 -0
- axdata_core/adapters/tdx/downloader_profiles.py +43 -0
- axdata_core/adapters/tdx/downloader_registry.py +115 -0
- axdata_core/adapters/tdx/execution_utils.py +55 -0
- axdata_core/adapters/tdx/f10_executor.py +57 -0
- axdata_core/adapters/tdx/f10_normalize.py +61 -0
- axdata_core/adapters/tdx/f10_params.py +57 -0
- axdata_core/adapters/tdx/f10_postprocess.py +67 -0
- axdata_core/adapters/tdx/f10_render.py +48 -0
- axdata_core/adapters/tdx/f10_request.py +62 -0
- axdata_core/adapters/tdx/finance_fetch.py +61 -0
- axdata_core/adapters/tdx/finance_maps.py +54 -0
- axdata_core/adapters/tdx/finance_normalize.py +57 -0
- axdata_core/adapters/tdx/host_config.py +115 -0
- axdata_core/adapters/tdx/interface_sets.py +50 -0
- axdata_core/adapters/tdx/intraday_fetch.py +68 -0
- axdata_core/adapters/tdx/kline_helpers.py +58 -0
- axdata_core/adapters/tdx/limit_ladder_fetch.py +60 -0
- axdata_core/adapters/tdx/limit_ladder_topics.py +53 -0
- axdata_core/adapters/tdx/normalize_utils.py +75 -0
- axdata_core/adapters/tdx/options.py +61 -0
- axdata_core/adapters/tdx/price_limit_calendar.py +49 -0
- axdata_core/adapters/tdx/price_limit_fetch.py +53 -0
- axdata_core/adapters/tdx/price_limit_history.py +50 -0
- axdata_core/adapters/tdx/price_limits.py +58 -0
- axdata_core/adapters/tdx/provider_bridge.py +68 -0
- axdata_core/adapters/tdx/quote_fetch.py +61 -0
- axdata_core/adapters/tdx/quote_identity.py +49 -0
- axdata_core/adapters/tdx/rank_fetch.py +55 -0
- axdata_core/adapters/tdx/rank_params.py +52 -0
- axdata_core/adapters/tdx/realtime_refresh.py +45 -0
- axdata_core/adapters/tdx/request.py +11 -0
- axdata_core/adapters/tdx/request_adapter_runtime.py +41 -0
- axdata_core/adapters/tdx/request_client.py +51 -0
- axdata_core/adapters/tdx/request_compat.py +89 -0
- axdata_core/adapters/tdx/request_dispatch.py +50 -0
- axdata_core/adapters/tdx/request_filters.py +60 -0
- axdata_core/adapters/tdx/request_host_config.py +110 -0
- axdata_core/adapters/tdx/request_limits.py +59 -0
- axdata_core/adapters/tdx/request_methods.py +41 -0
- axdata_core/adapters/tdx/request_params.py +62 -0
- axdata_core/adapters/tdx/request_seams.py +41 -0
- axdata_core/adapters/tdx/resources/__init__.py +0 -0
- axdata_core/adapters/tdx/resources/finance_maps/__init__.py +0 -0
- axdata_core/adapters/tdx/resources/finance_maps/incon.dat +3702 -0
- axdata_core/adapters/tdx/resources/finance_maps/tdxhy.cfg +5620 -0
- axdata_core/adapters/tdx/resources/finance_maps/tdxzs.cfg +605 -0
- axdata_core/adapters/tdx/security_codes.py +54 -0
- axdata_core/adapters/tdx/series_history.py +57 -0
- axdata_core/adapters/tdx/server_cache.py +58 -0
- axdata_core/adapters/tdx/snapshot_normalize.py +56 -0
- axdata_core/adapters/tdx/source_adapter_registry.py +69 -0
- axdata_core/adapters/tdx/source_execution.py +44 -0
- axdata_core/adapters/tdx/source_execution_registry.py +70 -0
- axdata_core/adapters/tdx/stats_cache.py +184 -0
- axdata_core/adapters/tdx/stats_models.py +159 -0
- axdata_core/adapters/tdx/stats_resource.py +304 -0
- axdata_core/adapters/tdx/status_fetch.py +55 -0
- axdata_core/adapters/tdx/time_series_normalize.py +67 -0
- axdata_core/adapters/tdx/tqlex.py +50 -0
- axdata_core/adapters/tdx/wire_requests.py +60 -0
- axdata_core/adapters/tdx_ext/__init__.py +66 -0
- axdata_core/adapters/tdx_ext/client.py +688 -0
- axdata_core/adapters/tdx_ext/exceptions.py +51 -0
- axdata_core/adapters/tdx_ext/host_config.py +40 -0
- axdata_core/adapters/tdx_ext/interface_sets.py +65 -0
- axdata_core/adapters/tdx_ext/local_cache.py +649 -0
- axdata_core/adapters/tdx_ext/models.py +96 -0
- axdata_core/adapters/tdx_ext/options.py +36 -0
- axdata_core/adapters/tdx_ext/pool.py +153 -0
- axdata_core/adapters/tdx_ext/provider_bridge.py +35 -0
- axdata_core/adapters/tdx_ext/request.py +498 -0
- axdata_core/adapters/tdx_ext/request_execution.py +118 -0
- axdata_core/adapters/tdx_ext/request_instruments.py +221 -0
- axdata_core/adapters/tdx_ext/request_normalize.py +356 -0
- axdata_core/adapters/tdx_ext/request_params.py +157 -0
- axdata_core/adapters/tdx_ext/request_series.py +98 -0
- axdata_core/adapters/tdx_ext/server_cache.py +13 -0
- axdata_core/adapters/tdx_ext/servers.py +35 -0
- axdata_core/adapters/tdx_ext/source_adapter_registry.py +42 -0
- axdata_core/adapters/tdx_ext/source_execution.py +19 -0
- axdata_core/adapters/tdx_ext/source_execution_registry.py +43 -0
- axdata_core/adapters/tencent/__init__.py +17 -0
- axdata_core/adapters/tencent/provider_bridge.py +25 -0
- axdata_core/adapters/tencent/request.py +729 -0
- axdata_core/adapters/tencent/source_adapter_registry.py +42 -0
- axdata_core/axp.py +1843 -0
- axdata_core/builtin_providers.py +586 -0
- axdata_core/builtin_source_declarations.py +156 -0
- axdata_core/cli.py +1818 -0
- axdata_core/collector_registry.py +227 -0
- axdata_core/collector_runner.py +1016 -0
- axdata_core/collector_scheduler.py +3155 -0
- axdata_core/collector_templates.py +339 -0
- axdata_core/data_browser.py +1696 -0
- axdata_core/diagnostics.py +734 -0
- axdata_core/downloader_engine.py +1480 -0
- axdata_core/downloader_registry.py +62 -0
- axdata_core/downloaders.py +1619 -0
- axdata_core/legacy_provider_adapter.py +68 -0
- axdata_core/paths.py +51 -0
- axdata_core/plugin_config.py +272 -0
- axdata_core/plugin_status.py +419 -0
- axdata_core/plugins.py +1363 -0
- axdata_core/provider_catalog.py +462 -0
- axdata_core/provider_registry.py +833 -0
- axdata_core/quality.py +86 -0
- axdata_core/query.py +242 -0
- axdata_core/resources/__init__.py +2 -0
- axdata_core/resources/tdx_extended_servers.json +22 -0
- axdata_core/resources/tdx_quote_servers.json +49 -0
- axdata_core/sample_collectors.py +42 -0
- axdata_core/schema.py +385 -0
- axdata_core/source_adapter_factory.py +95 -0
- axdata_core/source_adapter_options.py +18 -0
- axdata_core/source_errors.py +43 -0
- axdata_core/source_execution_options.py +37 -0
- axdata_core/source_execution_registry.py +27 -0
- axdata_core/source_projection.py +21 -0
- axdata_core/source_request.py +441 -0
- axdata_core/source_session.py +355 -0
- axdata_core/sources/__init__.py +40 -0
- axdata_core/sources/base.py +112 -0
- axdata_core/sources/catalog.py +60 -0
- axdata_core/sources/cls/__init__.py +5 -0
- axdata_core/sources/cls/catalog.py +231 -0
- axdata_core/sources/cninfo/__init__.py +5 -0
- axdata_core/sources/cninfo/catalog.py +1994 -0
- axdata_core/sources/display_docs.py +139 -0
- axdata_core/sources/eastmoney/__init__.py +5 -0
- axdata_core/sources/eastmoney/catalog.py +509 -0
- axdata_core/sources/exchange/__init__.py +5 -0
- axdata_core/sources/exchange/catalog.py +361 -0
- axdata_core/sources/kph/__init__.py +5 -0
- axdata_core/sources/kph/catalog.py +255 -0
- axdata_core/sources/sina/__init__.py +5 -0
- axdata_core/sources/sina/catalog.py +2951 -0
- axdata_core/sources/tdx/__init__.py +5 -0
- axdata_core/sources/tdx/catalog.py +4546 -0
- axdata_core/sources/tdx_ext/__init__.py +5 -0
- axdata_core/sources/tdx_ext/catalog.py +1518 -0
- axdata_core/sources/tencent/__init__.py +5 -0
- axdata_core/sources/tencent/catalog.py +370 -0
- axdata_core/storage.py +186 -0
- axdata_core/tdx_f10_catalog.py +52 -0
- axdata_core/tdx_f10_models.py +50 -0
- axdata_core/tdx_f10_names.py +47 -0
- axdata_core/tdx_f10_specs.py +56 -0
- axdata_core/tdx_plugin_required.py +43 -0
- axdata_core/tdx_server_config.py +65 -0
- axdata_core/trade_calendar_cache.py +397 -0
- axdata_source_cninfo/__init__.py +15 -0
- axdata_source_cninfo/adapter.py +31 -0
- axdata_source_cninfo/axdata-provider.json +384 -0
- axdata_source_cninfo/catalog.py +209 -0
- axdata_source_cninfo/metadata.py +8 -0
- axdata_source_cninfo/provider.py +41 -0
- axdata_source_tdx/__init__.py +15 -0
- axdata_source_tdx/_tdx_wire/__init__.py +26 -0
- axdata_source_tdx/_tdx_wire/_binary.py +218 -0
- axdata_source_tdx/_tdx_wire/_code_utils.py +42 -0
- axdata_source_tdx/_tdx_wire/_command_codec.py +186 -0
- axdata_source_tdx/_tdx_wire/_command_codes.py +74 -0
- axdata_source_tdx/_tdx_wire/_command_defaults.py +15 -0
- axdata_source_tdx/_tdx_wire/_command_dispatch.py +96 -0
- axdata_source_tdx/_tdx_wire/_command_layouts.py +96 -0
- axdata_source_tdx/_tdx_wire/_command_lookup.py +25 -0
- axdata_source_tdx/_tdx_wire/_command_metadata.py +32 -0
- axdata_source_tdx/_tdx_wire/_command_registry.py +61 -0
- axdata_source_tdx/_tdx_wire/_connection_defaults.py +13 -0
- axdata_source_tdx/_tdx_wire/_host_defaults.py +51 -0
- axdata_source_tdx/_tdx_wire/_host_probe.py +81 -0
- axdata_source_tdx/_tdx_wire/_host_resource.py +70 -0
- axdata_source_tdx/_tdx_wire/_host_utils.py +31 -0
- axdata_source_tdx/_tdx_wire/_market.py +66 -0
- axdata_source_tdx/_tdx_wire/_request_defaults.py +11 -0
- axdata_source_tdx/_tdx_wire/_security_classification.py +61 -0
- axdata_source_tdx/_tdx_wire/_time_utils.py +7 -0
- axdata_source_tdx/_tdx_wire/api/__init__.py +45 -0
- axdata_source_tdx/_tdx_wire/api/auction.py +25 -0
- axdata_source_tdx/_tdx_wire/api/bars.py +31 -0
- axdata_source_tdx/_tdx_wire/api/base.py +39 -0
- axdata_source_tdx/_tdx_wire/api/codes.py +25 -0
- axdata_source_tdx/_tdx_wire/api/corporate.py +11 -0
- axdata_source_tdx/_tdx_wire/api/finance.py +10 -0
- axdata_source_tdx/_tdx_wire/api/intraday.py +38 -0
- axdata_source_tdx/_tdx_wire/api/quotes.py +78 -0
- axdata_source_tdx/_tdx_wire/api/resources.py +34 -0
- axdata_source_tdx/_tdx_wire/api/session.py +16 -0
- axdata_source_tdx/_tdx_wire/api/trades.py +34 -0
- axdata_source_tdx/_tdx_wire/client.py +470 -0
- axdata_source_tdx/_tdx_wire/exceptions.py +25 -0
- axdata_source_tdx/_tdx_wire/hosts.py +27 -0
- axdata_source_tdx/_tdx_wire/models/__init__.py +87 -0
- axdata_source_tdx/_tdx_wire/models/auction.py +58 -0
- axdata_source_tdx/_tdx_wire/models/corporate.py +59 -0
- axdata_source_tdx/_tdx_wire/models/finance.py +80 -0
- axdata_source_tdx/_tdx_wire/models/intraday.py +112 -0
- axdata_source_tdx/_tdx_wire/models/kline.py +58 -0
- axdata_source_tdx/_tdx_wire/models/quote.py +262 -0
- axdata_source_tdx/_tdx_wire/models/resource.py +18 -0
- axdata_source_tdx/_tdx_wire/models/security.py +28 -0
- axdata_source_tdx/_tdx_wire/models/session.py +30 -0
- axdata_source_tdx/_tdx_wire/models/subchart.py +39 -0
- axdata_source_tdx/_tdx_wire/models/trade.py +46 -0
- axdata_source_tdx/_tdx_wire/protocol/__init__.py +43 -0
- axdata_source_tdx/_tdx_wire/protocol/_frame_constants.py +5 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/__init__.py +37 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/auction.py +165 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/codec.py +37 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/corporate.py +163 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/finance.py +180 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/intraday.py +382 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/klines.py +396 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/lookup.py +31 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/price_limits.py +110 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/quotes.py +804 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/registry.py +30 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/resources.py +107 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/security.py +163 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/session.py +129 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/subchart.py +182 -0
- axdata_source_tdx/_tdx_wire/protocol/commands/trades.py +275 -0
- axdata_source_tdx/_tdx_wire/protocol/constants.py +70 -0
- axdata_source_tdx/_tdx_wire/protocol/frame.py +141 -0
- axdata_source_tdx/_tdx_wire/protocol/unit.py +98 -0
- axdata_source_tdx/_tdx_wire/py.typed +1 -0
- axdata_source_tdx/_tdx_wire/transport/__init__.py +28 -0
- axdata_source_tdx/_tdx_wire/transport/base.py +21 -0
- axdata_source_tdx/_tdx_wire/transport/memory.py +42 -0
- axdata_source_tdx/_tdx_wire/transport/pool.py +148 -0
- axdata_source_tdx/_tdx_wire/transport/socket.py +373 -0
- axdata_source_tdx/adapter.py +31 -0
- axdata_source_tdx/adjustment.py +163 -0
- axdata_source_tdx/adjustment_fetch.py +148 -0
- axdata_source_tdx/auction_fetch.py +162 -0
- axdata_source_tdx/axdata-provider.json +23680 -0
- axdata_source_tdx/catalog.py +39 -0
- axdata_source_tdx/client_factory.py +61 -0
- axdata_source_tdx/code_fetch.py +579 -0
- axdata_source_tdx/codes.py +69 -0
- axdata_source_tdx/collectors.py +525 -0
- axdata_source_tdx/derived_rows.py +500 -0
- axdata_source_tdx/downloader.py +139 -0
- axdata_source_tdx/downloader_interface_sets.py +29 -0
- axdata_source_tdx/downloader_profiles.py +731 -0
- axdata_source_tdx/downloader_registry.py +85 -0
- axdata_source_tdx/execution_utils.py +141 -0
- axdata_source_tdx/f10_executor.py +414 -0
- axdata_source_tdx/f10_normalize.py +169 -0
- axdata_source_tdx/f10_params.py +221 -0
- axdata_source_tdx/f10_postprocess.py +405 -0
- axdata_source_tdx/f10_render.py +29 -0
- axdata_source_tdx/f10_request.py +365 -0
- axdata_source_tdx/finance_fetch.py +514 -0
- axdata_source_tdx/finance_maps.py +377 -0
- axdata_source_tdx/finance_normalize.py +273 -0
- axdata_source_tdx/host_config.py +51 -0
- axdata_source_tdx/interface_sets.py +180 -0
- axdata_source_tdx/intraday_fetch.py +600 -0
- axdata_source_tdx/kline_helpers.py +333 -0
- axdata_source_tdx/limit_ladder_fetch.py +614 -0
- axdata_source_tdx/limit_ladder_topics.py +280 -0
- axdata_source_tdx/metadata.py +8 -0
- axdata_source_tdx/normalize_utils.py +264 -0
- axdata_source_tdx/options.py +195 -0
- axdata_source_tdx/price_limit_calendar.py +106 -0
- axdata_source_tdx/price_limit_fetch.py +264 -0
- axdata_source_tdx/price_limit_history.py +100 -0
- axdata_source_tdx/price_limits.py +142 -0
- axdata_source_tdx/provider.py +44 -0
- axdata_source_tdx/provider_bridge.py +43 -0
- axdata_source_tdx/quote_fetch.py +485 -0
- axdata_source_tdx/quote_identity.py +50 -0
- axdata_source_tdx/rank_fetch.py +316 -0
- axdata_source_tdx/rank_params.py +181 -0
- axdata_source_tdx/realtime_refresh.py +51 -0
- axdata_source_tdx/request_adapter.py +533 -0
- axdata_source_tdx/request_adapter_runtime.py +302 -0
- axdata_source_tdx/request_client.py +98 -0
- axdata_source_tdx/request_compat.py +432 -0
- axdata_source_tdx/request_dispatch.py +138 -0
- axdata_source_tdx/request_entrypoints.py +146 -0
- axdata_source_tdx/request_filters.py +154 -0
- axdata_source_tdx/request_host_config.py +103 -0
- axdata_source_tdx/request_limits.py +15 -0
- axdata_source_tdx/request_methods.py +1910 -0
- axdata_source_tdx/request_params.py +225 -0
- axdata_source_tdx/request_seams.py +534 -0
- axdata_source_tdx/resources/__init__.py +0 -0
- axdata_source_tdx/resources/finance_maps/__init__.py +1 -0
- axdata_source_tdx/resources/finance_maps/incon.dat +3702 -0
- axdata_source_tdx/resources/finance_maps/tdxhy.cfg +5620 -0
- axdata_source_tdx/resources/finance_maps/tdxzs.cfg +605 -0
- axdata_source_tdx/resources/tdx_extended_servers.json +22 -0
- axdata_source_tdx/resources/tdx_quote_servers.json +49 -0
- axdata_source_tdx/security_codes.py +127 -0
- axdata_source_tdx/series_history.py +348 -0
- axdata_source_tdx/server_cache.py +21 -0
- axdata_source_tdx/snapshot_normalize.py +232 -0
- axdata_source_tdx/source_adapter_registry.py +42 -0
- axdata_source_tdx/source_execution.py +22 -0
- axdata_source_tdx/source_execution_registry.py +43 -0
- axdata_source_tdx/stats_cache.py +150 -0
- axdata_source_tdx/stats_models.py +161 -0
- axdata_source_tdx/stats_resource.py +242 -0
- axdata_source_tdx/status_fetch.py +209 -0
- axdata_source_tdx/tdx_f10_catalog.py +957 -0
- axdata_source_tdx/tdx_f10_models.py +47 -0
- axdata_source_tdx/tdx_f10_names.py +48 -0
- axdata_source_tdx/tdx_f10_specs.py +1142 -0
- axdata_source_tdx/tdx_server_config.py +389 -0
- axdata_source_tdx/time_series_normalize.py +370 -0
- axdata_source_tdx/tqlex.py +138 -0
- axdata_source_tdx/wire.py +32 -0
- axdata_source_tdx/wire_requests.py +190 -0
- axdata_source_tdx_ext/__init__.py +15 -0
- axdata_source_tdx_ext/adapter.py +31 -0
- axdata_source_tdx_ext/axdata-provider.json +9424 -0
- axdata_source_tdx_ext/catalog.py +33 -0
- axdata_source_tdx_ext/metadata.py +8 -0
- axdata_source_tdx_ext/provider.py +44 -0
- axdata_source_tencent/__init__.py +15 -0
- axdata_source_tencent/adapter.py +31 -0
- axdata_source_tencent/axdata-provider.json +291 -0
- axdata_source_tencent/catalog.py +190 -0
- axdata_source_tencent/metadata.py +8 -0
- axdata_source_tencent/provider.py +41 -0
axdata/client.py
ADDED
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
"""Python client for AxData local and remote data access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Iterator, Mapping
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urlencode, urlparse, urlunparse
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_API_BASE = "http://127.0.0.1:8666"
|
|
20
|
+
BackendMode = str
|
|
21
|
+
DEFAULT_LIMIT = 1000
|
|
22
|
+
LOCAL_STREAM_NAME = "stock_quote_refresh_tdx"
|
|
23
|
+
LOCAL_STREAM_DEFAULT_INTERVAL_MS = 3000
|
|
24
|
+
LOCAL_STREAM_MIN_INTERVAL_MS = 500
|
|
25
|
+
LOCAL_STREAM_MAX_CODES = 100
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AxDataError(RuntimeError):
|
|
29
|
+
"""Raised when AxData returns an invalid or failed response."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AxDataClient:
|
|
33
|
+
"""User-facing AxData client.
|
|
34
|
+
|
|
35
|
+
The default backend is local: it reads the current machine's AxData data
|
|
36
|
+
directory through axdata_core and does not require an API service. Pass
|
|
37
|
+
api_base, set AXDATA_API_BASE, or use mode="api" to read a LAN/server AxData
|
|
38
|
+
service through HTTP.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
token: str | None = None,
|
|
44
|
+
api_base: str | None = None,
|
|
45
|
+
data_root: str | Path | None = None,
|
|
46
|
+
data_dir: str | Path | None = None,
|
|
47
|
+
data_home: str | Path | None = None,
|
|
48
|
+
mode: BackendMode = "auto",
|
|
49
|
+
backend: BackendMode | None = None,
|
|
50
|
+
session: requests.Session | None = None,
|
|
51
|
+
timeout: float = 30.0,
|
|
52
|
+
) -> None:
|
|
53
|
+
if backend is not None and mode != "auto":
|
|
54
|
+
raise ValueError("Use either mode or backend, not both.")
|
|
55
|
+
if backend is None and mode == "auto" and api_base is None:
|
|
56
|
+
backend = os.getenv("AXDATA_BACKEND")
|
|
57
|
+
if backend is not None:
|
|
58
|
+
mode = backend
|
|
59
|
+
if mode not in {"auto", "local", "api"}:
|
|
60
|
+
raise ValueError("backend/mode must be one of: auto, local, api")
|
|
61
|
+
|
|
62
|
+
self.token = token if token is not None else os.getenv("AXDATA_TOKEN")
|
|
63
|
+
env_api_base = os.getenv("AXDATA_API_BASE")
|
|
64
|
+
resolved_api_base = api_base or env_api_base
|
|
65
|
+
if mode == "api" and not resolved_api_base:
|
|
66
|
+
resolved_api_base = DEFAULT_API_BASE
|
|
67
|
+
|
|
68
|
+
if mode == "auto":
|
|
69
|
+
self.mode = "api" if resolved_api_base else "local"
|
|
70
|
+
else:
|
|
71
|
+
self.mode = mode
|
|
72
|
+
self.backend = self.mode
|
|
73
|
+
|
|
74
|
+
self.api_base = resolved_api_base.rstrip("/") if resolved_api_base else None
|
|
75
|
+
self.data_root = self._resolve_data_root(
|
|
76
|
+
data_root=data_root,
|
|
77
|
+
data_dir=data_dir,
|
|
78
|
+
data_home=data_home,
|
|
79
|
+
)
|
|
80
|
+
self._http_session = session or requests.Session()
|
|
81
|
+
self.timeout = timeout
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def local(
|
|
85
|
+
cls,
|
|
86
|
+
*,
|
|
87
|
+
data_root: str | Path | None = None,
|
|
88
|
+
data_dir: str | Path | None = None,
|
|
89
|
+
data_home: str | Path | None = None,
|
|
90
|
+
) -> "AxDataClient":
|
|
91
|
+
"""Create a client that reads the current machine's AxData data directory."""
|
|
92
|
+
|
|
93
|
+
return cls(mode="local", data_root=data_root, data_dir=data_dir, data_home=data_home)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def api(
|
|
97
|
+
cls,
|
|
98
|
+
api_base: str = DEFAULT_API_BASE,
|
|
99
|
+
*,
|
|
100
|
+
token: str | None = None,
|
|
101
|
+
session: requests.Session | None = None,
|
|
102
|
+
timeout: float = 30.0,
|
|
103
|
+
) -> "AxDataClient":
|
|
104
|
+
"""Create a client that reads a local/LAN/server AxData API service."""
|
|
105
|
+
|
|
106
|
+
return cls(
|
|
107
|
+
token=token,
|
|
108
|
+
api_base=api_base,
|
|
109
|
+
mode="api",
|
|
110
|
+
session=session,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def query(
|
|
115
|
+
self,
|
|
116
|
+
api_name: str,
|
|
117
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
118
|
+
**params: Any,
|
|
119
|
+
):
|
|
120
|
+
"""Query an AxData table and return records as a pandas.DataFrame."""
|
|
121
|
+
|
|
122
|
+
if not api_name:
|
|
123
|
+
raise ValueError("api_name is required")
|
|
124
|
+
|
|
125
|
+
if self.mode == "local":
|
|
126
|
+
return self._query_local(api_name, fields=fields, **params)
|
|
127
|
+
return self._query_api(api_name, fields=fields, **params)
|
|
128
|
+
|
|
129
|
+
def call(
|
|
130
|
+
self,
|
|
131
|
+
interface: str,
|
|
132
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
133
|
+
options: Mapping[str, Any] | None = None,
|
|
134
|
+
**params: Any,
|
|
135
|
+
):
|
|
136
|
+
"""Call a provider-style source interface and return a pandas.DataFrame."""
|
|
137
|
+
|
|
138
|
+
if not interface:
|
|
139
|
+
raise ValueError("interface is required")
|
|
140
|
+
|
|
141
|
+
if self.mode == "local":
|
|
142
|
+
return self._call_local(interface, fields=fields, options=options, **params)
|
|
143
|
+
return self._call_api(interface, fields=fields, options=options, **params)
|
|
144
|
+
|
|
145
|
+
def download(
|
|
146
|
+
self,
|
|
147
|
+
interface: str,
|
|
148
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
149
|
+
output_root: str | Path | None = None,
|
|
150
|
+
output_dir: str | Path | None = None,
|
|
151
|
+
formats: str | list[str] | tuple[str, ...] | None = None,
|
|
152
|
+
collect_mode: str | None = None,
|
|
153
|
+
connection_mode: str | None = None,
|
|
154
|
+
connection_count: int | None = None,
|
|
155
|
+
source_server_count: int | None = None,
|
|
156
|
+
connections_per_server: int | None = None,
|
|
157
|
+
max_concurrent_tasks: int | None = None,
|
|
158
|
+
batch_size: int | None = None,
|
|
159
|
+
request_interval_ms: int | None = None,
|
|
160
|
+
retry_count: int | None = None,
|
|
161
|
+
timeout_ms: int | None = None,
|
|
162
|
+
**params: Any,
|
|
163
|
+
) -> SimpleNamespace:
|
|
164
|
+
"""Run a configured downloader and return job metadata."""
|
|
165
|
+
|
|
166
|
+
if not interface:
|
|
167
|
+
raise ValueError("interface is required")
|
|
168
|
+
|
|
169
|
+
if self.mode == "local":
|
|
170
|
+
return self._download_local(
|
|
171
|
+
interface,
|
|
172
|
+
fields=fields,
|
|
173
|
+
output_root=output_root,
|
|
174
|
+
output_dir=output_dir,
|
|
175
|
+
formats=formats,
|
|
176
|
+
collect_mode=collect_mode,
|
|
177
|
+
connection_mode=connection_mode,
|
|
178
|
+
connection_count=connection_count,
|
|
179
|
+
source_server_count=source_server_count,
|
|
180
|
+
connections_per_server=connections_per_server,
|
|
181
|
+
max_concurrent_tasks=max_concurrent_tasks,
|
|
182
|
+
batch_size=batch_size,
|
|
183
|
+
request_interval_ms=request_interval_ms,
|
|
184
|
+
retry_count=retry_count,
|
|
185
|
+
timeout_ms=timeout_ms,
|
|
186
|
+
**params,
|
|
187
|
+
)
|
|
188
|
+
return self._download_api(
|
|
189
|
+
interface,
|
|
190
|
+
fields=fields,
|
|
191
|
+
output_root=output_root,
|
|
192
|
+
output_dir=output_dir,
|
|
193
|
+
formats=formats,
|
|
194
|
+
collect_mode=collect_mode,
|
|
195
|
+
connection_mode=connection_mode,
|
|
196
|
+
connection_count=connection_count,
|
|
197
|
+
source_server_count=source_server_count,
|
|
198
|
+
connections_per_server=connections_per_server,
|
|
199
|
+
max_concurrent_tasks=max_concurrent_tasks,
|
|
200
|
+
batch_size=batch_size,
|
|
201
|
+
request_interval_ms=request_interval_ms,
|
|
202
|
+
retry_count=retry_count,
|
|
203
|
+
timeout_ms=timeout_ms,
|
|
204
|
+
**params,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def stream(
|
|
208
|
+
self,
|
|
209
|
+
stream: str,
|
|
210
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
211
|
+
**params: Any,
|
|
212
|
+
) -> "AxDataStream | LocalAxDataStream":
|
|
213
|
+
"""Open an AxData realtime stream.
|
|
214
|
+
|
|
215
|
+
Local clients run supported streams in-process without requiring the
|
|
216
|
+
AxData API service. API clients keep using the `/v1/stream/{stream}`
|
|
217
|
+
WebSocket route.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
if not stream:
|
|
221
|
+
raise ValueError("stream is required")
|
|
222
|
+
stream_params = {
|
|
223
|
+
str(key): self._normalize_param_value(value)
|
|
224
|
+
for key, value in params.items()
|
|
225
|
+
if value is not None
|
|
226
|
+
}
|
|
227
|
+
selected_fields = self._normalize_fields(fields) if fields is not None else None
|
|
228
|
+
if selected_fields is not None:
|
|
229
|
+
stream_params["fields"] = selected_fields
|
|
230
|
+
if self.mode == "local":
|
|
231
|
+
return LocalAxDataStream(
|
|
232
|
+
stream=stream,
|
|
233
|
+
params=stream_params,
|
|
234
|
+
data_root=self.data_root,
|
|
235
|
+
timeout=self.timeout,
|
|
236
|
+
)
|
|
237
|
+
return AxDataStream(
|
|
238
|
+
api_base=self.api_base or DEFAULT_API_BASE,
|
|
239
|
+
stream=stream,
|
|
240
|
+
params=stream_params,
|
|
241
|
+
token=self.token,
|
|
242
|
+
timeout=self.timeout,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def session(
|
|
246
|
+
self,
|
|
247
|
+
source: str,
|
|
248
|
+
options: Mapping[str, Any] | None = None,
|
|
249
|
+
**source_options: Any,
|
|
250
|
+
) -> "AxDataLocalSession":
|
|
251
|
+
"""Open a local high-frequency source session for TDX/TDX_EXT."""
|
|
252
|
+
|
|
253
|
+
if not source:
|
|
254
|
+
raise ValueError("source is required")
|
|
255
|
+
if self.mode != "local":
|
|
256
|
+
raise AxDataError(
|
|
257
|
+
"client.session(...) is local-only. Create AxDataClient() without api_base "
|
|
258
|
+
"to reuse local TDX/TDX_EXT connections; API clients should use call(...) or stream(...)."
|
|
259
|
+
)
|
|
260
|
+
resolved_options = self._normalize_options(options or {})
|
|
261
|
+
resolved_options.update(
|
|
262
|
+
{
|
|
263
|
+
str(key): self._normalize_param_value(value)
|
|
264
|
+
for key, value in source_options.items()
|
|
265
|
+
if value is not None
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
return AxDataLocalSession(
|
|
269
|
+
client=self,
|
|
270
|
+
source=source,
|
|
271
|
+
options=resolved_options,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def stock_quote_refresh_tdx(
|
|
275
|
+
self,
|
|
276
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
277
|
+
**params: Any,
|
|
278
|
+
) -> "AxDataStream":
|
|
279
|
+
"""Subscribe to TDX stock quote refresh events."""
|
|
280
|
+
|
|
281
|
+
return self.stream("stock_quote_refresh_tdx", fields=fields, **params)
|
|
282
|
+
|
|
283
|
+
def request_interface(
|
|
284
|
+
self,
|
|
285
|
+
interface: str,
|
|
286
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
287
|
+
options: Mapping[str, Any] | None = None,
|
|
288
|
+
**params: Any,
|
|
289
|
+
):
|
|
290
|
+
"""Compatibility helper for :meth:`call`."""
|
|
291
|
+
|
|
292
|
+
return self.call(interface, fields=fields, options=options, **params)
|
|
293
|
+
|
|
294
|
+
def _query_api(
|
|
295
|
+
self,
|
|
296
|
+
api_name: str,
|
|
297
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
298
|
+
**params: Any,
|
|
299
|
+
):
|
|
300
|
+
if not self.api_base:
|
|
301
|
+
raise AxDataError("api_base is required for API mode")
|
|
302
|
+
|
|
303
|
+
query_parts = self._split_query_params(params)
|
|
304
|
+
payload: dict[str, Any] = {
|
|
305
|
+
"table": api_name,
|
|
306
|
+
"params": query_parts["params"],
|
|
307
|
+
}
|
|
308
|
+
if fields is not None:
|
|
309
|
+
payload["fields"] = self._normalize_fields(fields)
|
|
310
|
+
if query_parts["filters"]:
|
|
311
|
+
payload["filters"] = query_parts["filters"]
|
|
312
|
+
if query_parts["start_date"] is not None:
|
|
313
|
+
payload["start_date"] = query_parts["start_date"]
|
|
314
|
+
if query_parts["end_date"] is not None:
|
|
315
|
+
payload["end_date"] = query_parts["end_date"]
|
|
316
|
+
if query_parts["limit"] is not None:
|
|
317
|
+
payload["limit"] = query_parts["limit"]
|
|
318
|
+
|
|
319
|
+
response = self._http_session.post(
|
|
320
|
+
f"{self.api_base}/v1/query",
|
|
321
|
+
json=payload,
|
|
322
|
+
headers=self._headers(),
|
|
323
|
+
timeout=self.timeout,
|
|
324
|
+
)
|
|
325
|
+
self._raise_for_status(response)
|
|
326
|
+
data = response.json()
|
|
327
|
+
records = self._extract_records(data)
|
|
328
|
+
return self._to_dataframe(records)
|
|
329
|
+
|
|
330
|
+
def _call_api(
|
|
331
|
+
self,
|
|
332
|
+
interface: str,
|
|
333
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
334
|
+
options: Mapping[str, Any] | None = None,
|
|
335
|
+
**params: Any,
|
|
336
|
+
):
|
|
337
|
+
if not self.api_base:
|
|
338
|
+
raise AxDataError("api_base is required for API mode")
|
|
339
|
+
|
|
340
|
+
payload: dict[str, Any] = {
|
|
341
|
+
"params": {
|
|
342
|
+
str(key): self._normalize_param_value(value)
|
|
343
|
+
for key, value in params.items()
|
|
344
|
+
if value is not None
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
if fields is not None:
|
|
348
|
+
payload["fields"] = self._normalize_fields(fields)
|
|
349
|
+
if options:
|
|
350
|
+
payload["options"] = self._normalize_options(options)
|
|
351
|
+
|
|
352
|
+
response = self._http_session.post(
|
|
353
|
+
f"{self.api_base}/v1/request/{interface}",
|
|
354
|
+
json=payload,
|
|
355
|
+
headers=self._headers(),
|
|
356
|
+
timeout=self.timeout,
|
|
357
|
+
)
|
|
358
|
+
self._raise_for_status(response)
|
|
359
|
+
data = response.json()
|
|
360
|
+
records = self._extract_records(data)
|
|
361
|
+
return self._to_dataframe(records)
|
|
362
|
+
|
|
363
|
+
def _download_api(
|
|
364
|
+
self,
|
|
365
|
+
interface: str,
|
|
366
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
367
|
+
output_root: str | Path | None = None,
|
|
368
|
+
output_dir: str | Path | None = None,
|
|
369
|
+
formats: str | list[str] | tuple[str, ...] | None = None,
|
|
370
|
+
collect_mode: str | None = None,
|
|
371
|
+
connection_mode: str | None = None,
|
|
372
|
+
connection_count: int | None = None,
|
|
373
|
+
source_server_count: int | None = None,
|
|
374
|
+
connections_per_server: int | None = None,
|
|
375
|
+
max_concurrent_tasks: int | None = None,
|
|
376
|
+
batch_size: int | None = None,
|
|
377
|
+
request_interval_ms: int | None = None,
|
|
378
|
+
retry_count: int | None = None,
|
|
379
|
+
timeout_ms: int | None = None,
|
|
380
|
+
**params: Any,
|
|
381
|
+
) -> SimpleNamespace:
|
|
382
|
+
if not self.api_base:
|
|
383
|
+
raise AxDataError("api_base is required for API mode")
|
|
384
|
+
|
|
385
|
+
payload: dict[str, Any] = {
|
|
386
|
+
"params": {
|
|
387
|
+
str(key): self._normalize_param_value(value)
|
|
388
|
+
for key, value in params.items()
|
|
389
|
+
if value is not None
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
if fields is not None:
|
|
393
|
+
payload["fields"] = self._normalize_fields(fields)
|
|
394
|
+
if output_root is not None:
|
|
395
|
+
payload["output_root"] = str(output_root)
|
|
396
|
+
if output_dir is not None:
|
|
397
|
+
payload["output_dir"] = str(output_dir)
|
|
398
|
+
if formats is not None:
|
|
399
|
+
payload["formats"] = self._normalize_fields(formats)
|
|
400
|
+
if collect_mode is not None:
|
|
401
|
+
payload["collect_mode"] = collect_mode
|
|
402
|
+
if connection_mode is not None:
|
|
403
|
+
payload["connection_mode"] = connection_mode
|
|
404
|
+
if connection_count is not None:
|
|
405
|
+
payload["connection_count"] = int(connection_count)
|
|
406
|
+
if source_server_count is not None:
|
|
407
|
+
payload["source_server_count"] = int(source_server_count)
|
|
408
|
+
if connections_per_server is not None:
|
|
409
|
+
payload["connections_per_server"] = int(connections_per_server)
|
|
410
|
+
if max_concurrent_tasks is not None:
|
|
411
|
+
payload["max_concurrent_tasks"] = int(max_concurrent_tasks)
|
|
412
|
+
if batch_size is not None:
|
|
413
|
+
payload["batch_size"] = int(batch_size)
|
|
414
|
+
if request_interval_ms is not None:
|
|
415
|
+
payload["request_interval_ms"] = int(request_interval_ms)
|
|
416
|
+
if retry_count is not None:
|
|
417
|
+
payload["retry_count"] = int(retry_count)
|
|
418
|
+
if timeout_ms is not None:
|
|
419
|
+
payload["timeout_ms"] = int(timeout_ms)
|
|
420
|
+
|
|
421
|
+
response = self._http_session.post(
|
|
422
|
+
f"{self.api_base}/v1/download/{interface}",
|
|
423
|
+
json=payload,
|
|
424
|
+
headers=self._headers(),
|
|
425
|
+
timeout=self.timeout,
|
|
426
|
+
)
|
|
427
|
+
self._raise_for_status(response)
|
|
428
|
+
data = response.json()
|
|
429
|
+
result = self._extract_data_object(data)
|
|
430
|
+
return SimpleNamespace(**result)
|
|
431
|
+
|
|
432
|
+
def _query_local(
|
|
433
|
+
self,
|
|
434
|
+
table: str,
|
|
435
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
436
|
+
**params: Any,
|
|
437
|
+
):
|
|
438
|
+
try:
|
|
439
|
+
from axdata_core import query_table
|
|
440
|
+
except ImportError as exc:
|
|
441
|
+
raise ImportError(
|
|
442
|
+
"AxData local mode requires axdata_core and DuckDB. Install the workspace "
|
|
443
|
+
"or use `pip install axdata[local]`."
|
|
444
|
+
) from exc
|
|
445
|
+
|
|
446
|
+
query_parts = self._split_query_params(params)
|
|
447
|
+
filters = dict(query_parts["filters"])
|
|
448
|
+
filters.update(query_parts["params"])
|
|
449
|
+
selected_fields = self._normalize_fields(fields) if fields is not None else None
|
|
450
|
+
return query_table(
|
|
451
|
+
table,
|
|
452
|
+
root=self.data_root,
|
|
453
|
+
filters=filters,
|
|
454
|
+
fields=selected_fields,
|
|
455
|
+
start_date=query_parts["start_date"],
|
|
456
|
+
end_date=query_parts["end_date"],
|
|
457
|
+
limit=query_parts["limit"],
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def _call_local(
|
|
461
|
+
self,
|
|
462
|
+
interface: str,
|
|
463
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
464
|
+
options: Mapping[str, Any] | None = None,
|
|
465
|
+
**params: Any,
|
|
466
|
+
):
|
|
467
|
+
try:
|
|
468
|
+
from axdata_core import request_interface
|
|
469
|
+
except ImportError as exc:
|
|
470
|
+
raise ImportError(
|
|
471
|
+
"AxData local source requests require axdata_core. Install the workspace "
|
|
472
|
+
"or use API mode with api_base."
|
|
473
|
+
) from exc
|
|
474
|
+
|
|
475
|
+
request_params = {
|
|
476
|
+
str(key): self._normalize_param_value(value)
|
|
477
|
+
for key, value in params.items()
|
|
478
|
+
if value is not None
|
|
479
|
+
}
|
|
480
|
+
selected_fields = self._normalize_fields(fields) if fields is not None else None
|
|
481
|
+
request_kwargs = {
|
|
482
|
+
"params": request_params,
|
|
483
|
+
"fields": selected_fields,
|
|
484
|
+
"persist": False,
|
|
485
|
+
"data_root": self.data_root,
|
|
486
|
+
}
|
|
487
|
+
if options:
|
|
488
|
+
request_kwargs["options"] = self._normalize_options(options)
|
|
489
|
+
result = request_interface(interface, **request_kwargs)
|
|
490
|
+
return self._to_dataframe(result.records)
|
|
491
|
+
|
|
492
|
+
def _download_local(
|
|
493
|
+
self,
|
|
494
|
+
interface: str,
|
|
495
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
496
|
+
output_root: str | Path | None = None,
|
|
497
|
+
output_dir: str | Path | None = None,
|
|
498
|
+
formats: str | list[str] | tuple[str, ...] | None = None,
|
|
499
|
+
collect_mode: str | None = None,
|
|
500
|
+
connection_mode: str | None = None,
|
|
501
|
+
connection_count: int | None = None,
|
|
502
|
+
source_server_count: int | None = None,
|
|
503
|
+
connections_per_server: int | None = None,
|
|
504
|
+
max_concurrent_tasks: int | None = None,
|
|
505
|
+
batch_size: int | None = None,
|
|
506
|
+
request_interval_ms: int | None = None,
|
|
507
|
+
retry_count: int | None = None,
|
|
508
|
+
timeout_ms: int | None = None,
|
|
509
|
+
**params: Any,
|
|
510
|
+
) -> SimpleNamespace:
|
|
511
|
+
try:
|
|
512
|
+
from axdata_core import run_downloader
|
|
513
|
+
except ImportError as exc:
|
|
514
|
+
raise ImportError(
|
|
515
|
+
"AxData local downloads require axdata_core. Install the workspace "
|
|
516
|
+
"or use API mode with api_base."
|
|
517
|
+
) from exc
|
|
518
|
+
|
|
519
|
+
request_params = {
|
|
520
|
+
str(key): self._normalize_param_value(value)
|
|
521
|
+
for key, value in params.items()
|
|
522
|
+
if value is not None
|
|
523
|
+
}
|
|
524
|
+
selected_fields = self._normalize_fields(fields) if fields is not None else None
|
|
525
|
+
result = run_downloader(
|
|
526
|
+
interface,
|
|
527
|
+
params=request_params,
|
|
528
|
+
fields=selected_fields,
|
|
529
|
+
data_root=self.data_root,
|
|
530
|
+
output_root=output_root,
|
|
531
|
+
output_dir=output_dir,
|
|
532
|
+
formats=self._normalize_fields(formats) if formats is not None else None,
|
|
533
|
+
collect_mode=collect_mode,
|
|
534
|
+
connection_mode=connection_mode,
|
|
535
|
+
connection_count=connection_count,
|
|
536
|
+
source_server_count=source_server_count,
|
|
537
|
+
connections_per_server=connections_per_server,
|
|
538
|
+
max_concurrent_tasks=max_concurrent_tasks,
|
|
539
|
+
batch_size=batch_size,
|
|
540
|
+
request_interval_ms=request_interval_ms,
|
|
541
|
+
retry_count=retry_count,
|
|
542
|
+
timeout_ms=timeout_ms,
|
|
543
|
+
)
|
|
544
|
+
return SimpleNamespace(**result)
|
|
545
|
+
|
|
546
|
+
def daily(
|
|
547
|
+
self,
|
|
548
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
549
|
+
**params: Any,
|
|
550
|
+
):
|
|
551
|
+
"""Query daily market data."""
|
|
552
|
+
|
|
553
|
+
return self.query("daily", fields=fields, **params)
|
|
554
|
+
|
|
555
|
+
def adj_factor(
|
|
556
|
+
self,
|
|
557
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
558
|
+
**params: Any,
|
|
559
|
+
):
|
|
560
|
+
"""Query daily adjustment factors."""
|
|
561
|
+
|
|
562
|
+
return self.query("adj_factor", fields=fields, **params)
|
|
563
|
+
|
|
564
|
+
def trade_cal(
|
|
565
|
+
self,
|
|
566
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
567
|
+
**params: Any,
|
|
568
|
+
):
|
|
569
|
+
"""Query the trading calendar."""
|
|
570
|
+
|
|
571
|
+
return self.query("trade_cal", fields=fields, **params)
|
|
572
|
+
|
|
573
|
+
def stock_basic(
|
|
574
|
+
self,
|
|
575
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
576
|
+
**params: Any,
|
|
577
|
+
):
|
|
578
|
+
"""Query stock basic information.
|
|
579
|
+
|
|
580
|
+
Compatibility alias for :meth:`stock_basic_exchange`.
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
return self.stock_basic_exchange(fields=fields, **params)
|
|
584
|
+
|
|
585
|
+
def stock_basic_exchange(
|
|
586
|
+
self,
|
|
587
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
588
|
+
**params: Any,
|
|
589
|
+
):
|
|
590
|
+
"""Query stock list using the official exchange interface口径."""
|
|
591
|
+
|
|
592
|
+
return self.query("stock_basic_exchange", fields=fields, **params)
|
|
593
|
+
|
|
594
|
+
@staticmethod
|
|
595
|
+
def _resolve_data_root(
|
|
596
|
+
*,
|
|
597
|
+
data_root: str | Path | None = None,
|
|
598
|
+
data_dir: str | Path | None = None,
|
|
599
|
+
data_home: str | Path | None = None,
|
|
600
|
+
) -> Path:
|
|
601
|
+
if data_root is not None and data_dir is not None:
|
|
602
|
+
raise ValueError("Use either data_root or data_dir, not both.")
|
|
603
|
+
if data_root is not None:
|
|
604
|
+
return Path(data_root).expanduser().resolve()
|
|
605
|
+
if data_dir is not None:
|
|
606
|
+
return Path(data_dir).expanduser().resolve()
|
|
607
|
+
|
|
608
|
+
env_data_root = os.getenv("AXDATA_DATA_DIR")
|
|
609
|
+
if env_data_root:
|
|
610
|
+
return Path(env_data_root).expanduser().resolve()
|
|
611
|
+
|
|
612
|
+
resolved_data_home = data_home or os.getenv("AXDATA_DATA_HOME")
|
|
613
|
+
if resolved_data_home:
|
|
614
|
+
return (Path(resolved_data_home).expanduser().resolve() / "data")
|
|
615
|
+
|
|
616
|
+
return (Path.cwd() / "data").resolve()
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
def _normalize_date_text(value: Any) -> Any:
|
|
620
|
+
if isinstance(value, str) and len(value) == 10 and value[4] == "-" and value[7] == "-":
|
|
621
|
+
return value.replace("-", "")
|
|
622
|
+
return value
|
|
623
|
+
|
|
624
|
+
@classmethod
|
|
625
|
+
def _split_query_params(cls, params: Mapping[str, Any]) -> dict[str, Any]:
|
|
626
|
+
query_params = dict(params)
|
|
627
|
+
explicit_filters = query_params.pop("filters", None)
|
|
628
|
+
start_date = cls._normalize_date_text(
|
|
629
|
+
query_params.pop("start_date", None) or query_params.pop("start", None)
|
|
630
|
+
)
|
|
631
|
+
end_date = cls._normalize_date_text(
|
|
632
|
+
query_params.pop("end_date", None) or query_params.pop("end", None)
|
|
633
|
+
)
|
|
634
|
+
limit = cls._normalize_limit(query_params.pop("limit", DEFAULT_LIMIT))
|
|
635
|
+
filters = {
|
|
636
|
+
str(key): cls._normalize_param_value(value)
|
|
637
|
+
for key, value in dict(explicit_filters or {}).items()
|
|
638
|
+
if value is not None
|
|
639
|
+
}
|
|
640
|
+
params = {
|
|
641
|
+
str(key): cls._normalize_param_value(value)
|
|
642
|
+
for key, value in query_params.items()
|
|
643
|
+
if value is not None
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
"params": params,
|
|
647
|
+
"filters": filters,
|
|
648
|
+
"start_date": start_date,
|
|
649
|
+
"end_date": end_date,
|
|
650
|
+
"limit": limit,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
@classmethod
|
|
654
|
+
def _normalize_param_value(cls, value: Any) -> Any:
|
|
655
|
+
if isinstance(value, str):
|
|
656
|
+
return cls._normalize_date_text(value)
|
|
657
|
+
if isinstance(value, tuple):
|
|
658
|
+
return tuple(cls._normalize_param_value(item) for item in value)
|
|
659
|
+
if isinstance(value, list):
|
|
660
|
+
return [cls._normalize_param_value(item) for item in value]
|
|
661
|
+
if isinstance(value, set):
|
|
662
|
+
return [cls._normalize_param_value(item) for item in value]
|
|
663
|
+
if isinstance(value, frozenset):
|
|
664
|
+
return [cls._normalize_param_value(item) for item in value]
|
|
665
|
+
return value
|
|
666
|
+
|
|
667
|
+
@classmethod
|
|
668
|
+
def _normalize_options(cls, options: Mapping[str, Any]) -> dict[str, Any]:
|
|
669
|
+
return {
|
|
670
|
+
str(key): cls._normalize_param_value(value)
|
|
671
|
+
for key, value in dict(options or {}).items()
|
|
672
|
+
if value is not None
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
@staticmethod
|
|
676
|
+
def _normalize_limit(value: Any) -> int | None:
|
|
677
|
+
if value is None:
|
|
678
|
+
return None
|
|
679
|
+
return int(value)
|
|
680
|
+
|
|
681
|
+
def _headers(self) -> dict[str, str] | None:
|
|
682
|
+
if not self.token:
|
|
683
|
+
return None
|
|
684
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _raise_for_status(response: Any) -> None:
|
|
688
|
+
status_code = getattr(response, "status_code", 200)
|
|
689
|
+
if status_code is not None and int(status_code) >= 400:
|
|
690
|
+
try:
|
|
691
|
+
payload = response.json()
|
|
692
|
+
except Exception:
|
|
693
|
+
payload = None
|
|
694
|
+
if isinstance(payload, Mapping):
|
|
695
|
+
error = payload.get("error")
|
|
696
|
+
if isinstance(error, Mapping) and error.get("message"):
|
|
697
|
+
raise AxDataError(str(error["message"]))
|
|
698
|
+
if payload.get("message") or payload.get("msg"):
|
|
699
|
+
raise AxDataError(str(payload.get("message") or payload.get("msg")))
|
|
700
|
+
response.raise_for_status()
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def _normalize_fields(fields: str | list[str] | tuple[str, ...]) -> list[str]:
|
|
704
|
+
if isinstance(fields, str):
|
|
705
|
+
return [field.strip() for field in fields.split(",") if field.strip()]
|
|
706
|
+
return [str(field).strip() for field in fields if str(field).strip()]
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def _extract_records(data: Any) -> list[Mapping[str, Any]]:
|
|
710
|
+
if not isinstance(data, Mapping):
|
|
711
|
+
raise AxDataError("AxData response must be a JSON object")
|
|
712
|
+
|
|
713
|
+
if data.get("success") is False:
|
|
714
|
+
error = data.get("error")
|
|
715
|
+
if isinstance(error, Mapping):
|
|
716
|
+
message = error.get("message") or error.get("code")
|
|
717
|
+
else:
|
|
718
|
+
message = data.get("message") or data.get("msg")
|
|
719
|
+
message = message or "AxData query failed"
|
|
720
|
+
raise AxDataError(str(message))
|
|
721
|
+
|
|
722
|
+
if isinstance(data.get("data"), list):
|
|
723
|
+
records = data["data"]
|
|
724
|
+
elif "records" in data:
|
|
725
|
+
records = data["records"]
|
|
726
|
+
elif isinstance(data.get("data"), Mapping) and "records" in data["data"]:
|
|
727
|
+
records = data["data"]["records"]
|
|
728
|
+
else:
|
|
729
|
+
raise AxDataError("AxData response is missing a data record list")
|
|
730
|
+
|
|
731
|
+
if records is None:
|
|
732
|
+
return []
|
|
733
|
+
if not isinstance(records, list):
|
|
734
|
+
raise AxDataError("AxData response 'records' must be a list")
|
|
735
|
+
|
|
736
|
+
return records
|
|
737
|
+
|
|
738
|
+
@staticmethod
|
|
739
|
+
def _extract_data_object(data: Any) -> dict[str, Any]:
|
|
740
|
+
if not isinstance(data, Mapping):
|
|
741
|
+
raise AxDataError("AxData response must be a JSON object")
|
|
742
|
+
|
|
743
|
+
if data.get("success") is False:
|
|
744
|
+
error = data.get("error")
|
|
745
|
+
if isinstance(error, Mapping):
|
|
746
|
+
message = error.get("message") or error.get("code")
|
|
747
|
+
else:
|
|
748
|
+
message = data.get("message") or data.get("msg")
|
|
749
|
+
raise AxDataError(str(message or "AxData request failed"))
|
|
750
|
+
|
|
751
|
+
payload = data.get("data")
|
|
752
|
+
if not isinstance(payload, Mapping):
|
|
753
|
+
raise AxDataError("AxData response is missing a data object")
|
|
754
|
+
return dict(payload)
|
|
755
|
+
|
|
756
|
+
@staticmethod
|
|
757
|
+
def _to_dataframe(records: list[Mapping[str, Any]]):
|
|
758
|
+
try:
|
|
759
|
+
import pandas as pd
|
|
760
|
+
except ImportError as exc:
|
|
761
|
+
raise ImportError(
|
|
762
|
+
"AxData SDK requires pandas to convert API records to a DataFrame. "
|
|
763
|
+
"Install it with `pip install pandas` or `pip install axdata[pandas]`."
|
|
764
|
+
) from exc
|
|
765
|
+
|
|
766
|
+
return pd.DataFrame.from_records(records)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class AxDataStream:
|
|
770
|
+
"""Synchronous iterator wrapper for AxData WebSocket streams."""
|
|
771
|
+
|
|
772
|
+
def __init__(
|
|
773
|
+
self,
|
|
774
|
+
*,
|
|
775
|
+
api_base: str,
|
|
776
|
+
stream: str,
|
|
777
|
+
params: Mapping[str, Any],
|
|
778
|
+
token: str | None,
|
|
779
|
+
timeout: float,
|
|
780
|
+
) -> None:
|
|
781
|
+
self.api_base = api_base.rstrip("/")
|
|
782
|
+
self.stream = stream
|
|
783
|
+
self.params = dict(params)
|
|
784
|
+
self.token = token
|
|
785
|
+
self.timeout = timeout
|
|
786
|
+
self._ws = None
|
|
787
|
+
|
|
788
|
+
def __enter__(self) -> "AxDataStream":
|
|
789
|
+
self.open()
|
|
790
|
+
return self
|
|
791
|
+
|
|
792
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
793
|
+
self.close()
|
|
794
|
+
|
|
795
|
+
def __iter__(self) -> Iterator[SimpleNamespace]:
|
|
796
|
+
return self
|
|
797
|
+
|
|
798
|
+
def __next__(self) -> SimpleNamespace:
|
|
799
|
+
if self._ws is None:
|
|
800
|
+
self.open()
|
|
801
|
+
assert self._ws is not None
|
|
802
|
+
raw = self._ws.recv()
|
|
803
|
+
if raw in (None, ""):
|
|
804
|
+
raise StopIteration
|
|
805
|
+
payload = json.loads(raw)
|
|
806
|
+
if isinstance(payload, Mapping) and payload.get("type") == "error":
|
|
807
|
+
error = payload.get("error")
|
|
808
|
+
if isinstance(error, Mapping) and error.get("message"):
|
|
809
|
+
raise AxDataError(str(error["message"]))
|
|
810
|
+
return SimpleNamespace(**payload)
|
|
811
|
+
|
|
812
|
+
@property
|
|
813
|
+
def url(self) -> str:
|
|
814
|
+
parsed = urlparse(self.api_base)
|
|
815
|
+
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
816
|
+
query = parsed.query
|
|
817
|
+
if self.token:
|
|
818
|
+
token_query = urlencode({"token": self.token})
|
|
819
|
+
query = f"{query}&{token_query}" if query else token_query
|
|
820
|
+
return urlunparse((scheme, parsed.netloc, f"/v1/stream/{self.stream}", "", query, ""))
|
|
821
|
+
|
|
822
|
+
def open(self) -> None:
|
|
823
|
+
if self._ws is not None:
|
|
824
|
+
return
|
|
825
|
+
try:
|
|
826
|
+
import websocket
|
|
827
|
+
except ImportError as exc:
|
|
828
|
+
raise ImportError(
|
|
829
|
+
"AxData realtime streams require websocket-client. Install axdata with "
|
|
830
|
+
"`pip install axdata` from this workspace or install `websocket-client`."
|
|
831
|
+
) from exc
|
|
832
|
+
|
|
833
|
+
headers = []
|
|
834
|
+
if self.token:
|
|
835
|
+
headers.append(f"Authorization: Bearer {self.token}")
|
|
836
|
+
self._ws = websocket.create_connection(
|
|
837
|
+
self.url,
|
|
838
|
+
timeout=self.timeout,
|
|
839
|
+
header=headers or None,
|
|
840
|
+
)
|
|
841
|
+
self._ws.send(
|
|
842
|
+
json.dumps(
|
|
843
|
+
{
|
|
844
|
+
"op": "subscribe",
|
|
845
|
+
"stream": self.stream,
|
|
846
|
+
"params": self.params,
|
|
847
|
+
}
|
|
848
|
+
)
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
def close(self) -> None:
|
|
852
|
+
if self._ws is None:
|
|
853
|
+
return
|
|
854
|
+
try:
|
|
855
|
+
self._ws.close()
|
|
856
|
+
finally:
|
|
857
|
+
self._ws = None
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
class AxDataLocalSession:
|
|
861
|
+
"""SDK wrapper around an in-process local source session."""
|
|
862
|
+
|
|
863
|
+
def __init__(
|
|
864
|
+
self,
|
|
865
|
+
*,
|
|
866
|
+
client: AxDataClient,
|
|
867
|
+
source: str,
|
|
868
|
+
options: Mapping[str, Any],
|
|
869
|
+
) -> None:
|
|
870
|
+
self._client = client
|
|
871
|
+
self.source = source
|
|
872
|
+
self.options = dict(options)
|
|
873
|
+
self._session = None
|
|
874
|
+
|
|
875
|
+
def __enter__(self) -> "AxDataLocalSession":
|
|
876
|
+
self.open()
|
|
877
|
+
return self
|
|
878
|
+
|
|
879
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
880
|
+
self.close()
|
|
881
|
+
|
|
882
|
+
def open(self) -> None:
|
|
883
|
+
if self._session is not None:
|
|
884
|
+
return
|
|
885
|
+
try:
|
|
886
|
+
from axdata_core import create_source_session
|
|
887
|
+
except ImportError as exc:
|
|
888
|
+
raise ImportError(
|
|
889
|
+
"AxData local source sessions require axdata_core. Install the workspace "
|
|
890
|
+
"or use API mode with api_base."
|
|
891
|
+
) from exc
|
|
892
|
+
try:
|
|
893
|
+
session = create_source_session(
|
|
894
|
+
source=self.source,
|
|
895
|
+
data_root=self._client.data_root,
|
|
896
|
+
options=self.options,
|
|
897
|
+
)
|
|
898
|
+
session.open()
|
|
899
|
+
except Exception as exc:
|
|
900
|
+
raise AxDataError(f"Failed to open local source session for {self.source!r}: {exc}") from exc
|
|
901
|
+
self._session = session
|
|
902
|
+
|
|
903
|
+
def close(self) -> None:
|
|
904
|
+
if self._session is None:
|
|
905
|
+
return
|
|
906
|
+
try:
|
|
907
|
+
self._session.close()
|
|
908
|
+
finally:
|
|
909
|
+
self._session = None
|
|
910
|
+
|
|
911
|
+
def call(
|
|
912
|
+
self,
|
|
913
|
+
interface: str,
|
|
914
|
+
fields: str | list[str] | tuple[str, ...] | None = None,
|
|
915
|
+
options: Mapping[str, Any] | None = None,
|
|
916
|
+
**params: Any,
|
|
917
|
+
):
|
|
918
|
+
"""Call a source request interface through the session-owned adapter."""
|
|
919
|
+
|
|
920
|
+
if not interface:
|
|
921
|
+
raise ValueError("interface is required")
|
|
922
|
+
if self._session is None:
|
|
923
|
+
self.open()
|
|
924
|
+
assert self._session is not None
|
|
925
|
+
request_params = {
|
|
926
|
+
str(key): self._client._normalize_param_value(value)
|
|
927
|
+
for key, value in params.items()
|
|
928
|
+
if value is not None
|
|
929
|
+
}
|
|
930
|
+
selected_fields = self._client._normalize_fields(fields) if fields is not None else None
|
|
931
|
+
request_options = self._client._normalize_options(options) if options else None
|
|
932
|
+
try:
|
|
933
|
+
result = self._session.call(
|
|
934
|
+
interface,
|
|
935
|
+
params=request_params,
|
|
936
|
+
fields=selected_fields,
|
|
937
|
+
options=request_options,
|
|
938
|
+
)
|
|
939
|
+
except AxDataError:
|
|
940
|
+
raise
|
|
941
|
+
except Exception as exc:
|
|
942
|
+
raise AxDataError(f"Local source session call {interface!r} failed: {exc}") from exc
|
|
943
|
+
return self._client._to_dataframe(result.records)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class LocalAxDataStream:
|
|
947
|
+
"""Synchronous iterator for local in-process TDX streams."""
|
|
948
|
+
|
|
949
|
+
def __init__(
|
|
950
|
+
self,
|
|
951
|
+
*,
|
|
952
|
+
stream: str,
|
|
953
|
+
params: Mapping[str, Any],
|
|
954
|
+
data_root: Path,
|
|
955
|
+
timeout: float,
|
|
956
|
+
) -> None:
|
|
957
|
+
self.stream = stream
|
|
958
|
+
self.params = dict(params)
|
|
959
|
+
self.data_root = data_root
|
|
960
|
+
self.timeout = timeout
|
|
961
|
+
self._source_session = None
|
|
962
|
+
self._subscription_id: str | None = None
|
|
963
|
+
self._code: list[str] = []
|
|
964
|
+
self._fields: list[str] | None = None
|
|
965
|
+
self._interval_ms = LOCAL_STREAM_DEFAULT_INTERVAL_MS
|
|
966
|
+
self._initial_snapshot = True
|
|
967
|
+
self._pending: list[SimpleNamespace] = []
|
|
968
|
+
self._next_step = "closed"
|
|
969
|
+
self._refresh_cursors: dict[str, int] = {}
|
|
970
|
+
|
|
971
|
+
def __enter__(self) -> "LocalAxDataStream":
|
|
972
|
+
self.open()
|
|
973
|
+
return self
|
|
974
|
+
|
|
975
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
976
|
+
self.close()
|
|
977
|
+
|
|
978
|
+
def __iter__(self) -> Iterator[SimpleNamespace]:
|
|
979
|
+
return self
|
|
980
|
+
|
|
981
|
+
def __next__(self) -> SimpleNamespace:
|
|
982
|
+
if self._source_session is None:
|
|
983
|
+
self.open()
|
|
984
|
+
if self._pending:
|
|
985
|
+
return self._pending.pop(0)
|
|
986
|
+
if self._next_step == "snapshot":
|
|
987
|
+
self._next_step = "update"
|
|
988
|
+
return self._snapshot_event()
|
|
989
|
+
if self._next_step == "update":
|
|
990
|
+
time.sleep(self._interval_ms / 1000.0)
|
|
991
|
+
return self._update_event()
|
|
992
|
+
raise StopIteration
|
|
993
|
+
|
|
994
|
+
def open(self) -> None:
|
|
995
|
+
if self._source_session is not None:
|
|
996
|
+
return
|
|
997
|
+
if self.stream != LOCAL_STREAM_NAME:
|
|
998
|
+
raise AxDataError(
|
|
999
|
+
f"Local streams currently support only {LOCAL_STREAM_NAME!r}; got {self.stream!r}. "
|
|
1000
|
+
"Pass api_base to AxDataClient for remote WebSocket streams."
|
|
1001
|
+
)
|
|
1002
|
+
code = _normalize_stream_codes(self.params.get("code"))
|
|
1003
|
+
if not code:
|
|
1004
|
+
raise AxDataError("code must include at least one security code for local TDX stream.")
|
|
1005
|
+
if len(code) > LOCAL_STREAM_MAX_CODES:
|
|
1006
|
+
raise AxDataError(f"Local TDX stream supports at most {LOCAL_STREAM_MAX_CODES} codes per subscription.")
|
|
1007
|
+
fields = self.params.get("fields")
|
|
1008
|
+
selected_fields = AxDataClient._normalize_fields(fields) if fields is not None else None
|
|
1009
|
+
interval_ms = _normalize_stream_interval_ms(self.params.get("interval_ms"))
|
|
1010
|
+
initial_snapshot = _normalize_stream_bool(
|
|
1011
|
+
self.params.get("initial_snapshot", self.params.get("snapshot")),
|
|
1012
|
+
True,
|
|
1013
|
+
)
|
|
1014
|
+
try:
|
|
1015
|
+
from axdata_core import create_source_session
|
|
1016
|
+
except ImportError as exc:
|
|
1017
|
+
raise ImportError(
|
|
1018
|
+
"AxData local streams require axdata_core and the TDX source plugin. "
|
|
1019
|
+
"Install the workspace or create AxDataClient(api_base=...) for remote streams."
|
|
1020
|
+
) from exc
|
|
1021
|
+
|
|
1022
|
+
session_options = _local_stream_session_options(self.params)
|
|
1023
|
+
try:
|
|
1024
|
+
source_session = create_source_session(
|
|
1025
|
+
source="tdx",
|
|
1026
|
+
data_root=self.data_root,
|
|
1027
|
+
options=session_options,
|
|
1028
|
+
)
|
|
1029
|
+
source_session.open()
|
|
1030
|
+
except Exception as exc:
|
|
1031
|
+
raise AxDataError(f"Failed to open local TDX stream session: {exc}") from exc
|
|
1032
|
+
|
|
1033
|
+
self._source_session = source_session
|
|
1034
|
+
self._subscription_id = f"sub_{uuid4().hex[:12]}"
|
|
1035
|
+
self._code = code
|
|
1036
|
+
self._fields = selected_fields
|
|
1037
|
+
self._interval_ms = interval_ms
|
|
1038
|
+
self._initial_snapshot = initial_snapshot
|
|
1039
|
+
self._next_step = "snapshot" if initial_snapshot else "update"
|
|
1040
|
+
self._pending.append(
|
|
1041
|
+
self._event(
|
|
1042
|
+
"subscribed",
|
|
1043
|
+
{
|
|
1044
|
+
"code": code,
|
|
1045
|
+
"fields": selected_fields,
|
|
1046
|
+
"interval_ms": interval_ms,
|
|
1047
|
+
"initial_snapshot": initial_snapshot,
|
|
1048
|
+
},
|
|
1049
|
+
)
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
def close(self) -> None:
|
|
1053
|
+
if self._source_session is None:
|
|
1054
|
+
return
|
|
1055
|
+
try:
|
|
1056
|
+
self._source_session.close()
|
|
1057
|
+
finally:
|
|
1058
|
+
self._source_session = None
|
|
1059
|
+
self._subscription_id = None
|
|
1060
|
+
self._pending.clear()
|
|
1061
|
+
self._refresh_cursors.clear()
|
|
1062
|
+
self._next_step = "closed"
|
|
1063
|
+
|
|
1064
|
+
def _snapshot_event(self) -> SimpleNamespace:
|
|
1065
|
+
assert self._source_session is not None
|
|
1066
|
+
try:
|
|
1067
|
+
result = self._source_session.call(
|
|
1068
|
+
"stock_realtime_snapshot_tdx",
|
|
1069
|
+
params={"code": self._code},
|
|
1070
|
+
fields=self._fields,
|
|
1071
|
+
)
|
|
1072
|
+
except Exception as exc:
|
|
1073
|
+
raise AxDataError(f"Local TDX stream snapshot failed: {exc}") from exc
|
|
1074
|
+
return self._event("snapshot", list(result.records))
|
|
1075
|
+
|
|
1076
|
+
def _update_event(self) -> SimpleNamespace:
|
|
1077
|
+
assert self._source_session is not None
|
|
1078
|
+
try:
|
|
1079
|
+
from axdata_core.adapters.tdx.realtime_refresh import request_realtime_refresh_rows
|
|
1080
|
+
|
|
1081
|
+
rows = request_realtime_refresh_rows(
|
|
1082
|
+
code=self._code,
|
|
1083
|
+
fields=self._fields,
|
|
1084
|
+
cursors=self._refresh_cursors,
|
|
1085
|
+
include_internal=True,
|
|
1086
|
+
client=getattr(self._source_session, "tdx_client", None),
|
|
1087
|
+
)
|
|
1088
|
+
except Exception as exc:
|
|
1089
|
+
raise AxDataError(f"Local TDX stream update failed: {exc}") from exc
|
|
1090
|
+
_update_refresh_cursors(self._refresh_cursors, rows)
|
|
1091
|
+
return self._event("update", _strip_internal_fields(rows))
|
|
1092
|
+
|
|
1093
|
+
def _event(self, event_type: str, data: Any) -> SimpleNamespace:
|
|
1094
|
+
return SimpleNamespace(
|
|
1095
|
+
type=event_type,
|
|
1096
|
+
stream=self.stream,
|
|
1097
|
+
subscription_id=self._subscription_id,
|
|
1098
|
+
server_time=_stream_time(),
|
|
1099
|
+
data=data,
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _stream_time() -> str:
|
|
1104
|
+
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _normalize_stream_codes(value: Any) -> list[str]:
|
|
1108
|
+
if value is None:
|
|
1109
|
+
return []
|
|
1110
|
+
if isinstance(value, str):
|
|
1111
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
1112
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
1113
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
1114
|
+
return [str(value).strip()] if str(value).strip() else []
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _normalize_stream_interval_ms(value: Any) -> int:
|
|
1118
|
+
if value is None:
|
|
1119
|
+
return LOCAL_STREAM_DEFAULT_INTERVAL_MS
|
|
1120
|
+
try:
|
|
1121
|
+
parsed = int(value)
|
|
1122
|
+
except (TypeError, ValueError):
|
|
1123
|
+
return LOCAL_STREAM_DEFAULT_INTERVAL_MS
|
|
1124
|
+
return max(LOCAL_STREAM_MIN_INTERVAL_MS, parsed)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _normalize_stream_bool(value: Any, default: bool) -> bool:
|
|
1128
|
+
if value is None:
|
|
1129
|
+
return default
|
|
1130
|
+
if isinstance(value, bool):
|
|
1131
|
+
return value
|
|
1132
|
+
if isinstance(value, str):
|
|
1133
|
+
normalized = value.strip().lower()
|
|
1134
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
1135
|
+
return True
|
|
1136
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
1137
|
+
return False
|
|
1138
|
+
return bool(value)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _local_stream_session_options(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
1142
|
+
option_keys = {
|
|
1143
|
+
"connections_per_server",
|
|
1144
|
+
"hosts",
|
|
1145
|
+
"pool_size",
|
|
1146
|
+
"source_server_count",
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
key: value
|
|
1150
|
+
for key, value in params.items()
|
|
1151
|
+
if key in option_keys and value is not None
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _update_refresh_cursors(cursors: dict[str, int], rows: list[dict[str, Any]]) -> None:
|
|
1156
|
+
for row in rows:
|
|
1157
|
+
instrument_id = row.get("_tdx_instrument_id") or row.get("instrument_id")
|
|
1158
|
+
update_time_raw = row.get("_tdx_update_time_raw")
|
|
1159
|
+
if not instrument_id or update_time_raw in (None, ""):
|
|
1160
|
+
continue
|
|
1161
|
+
try:
|
|
1162
|
+
cursors[str(instrument_id).upper()] = int(update_time_raw)
|
|
1163
|
+
except (TypeError, ValueError):
|
|
1164
|
+
continue
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _strip_internal_fields(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1168
|
+
return [
|
|
1169
|
+
{key: value for key, value in dict(row).items() if not str(key).startswith("_tdx_")}
|
|
1170
|
+
for row in rows
|
|
1171
|
+
]
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def pro_api(
|
|
1175
|
+
token: str | None = None,
|
|
1176
|
+
api_base: str | None = None,
|
|
1177
|
+
data_root: str | Path | None = None,
|
|
1178
|
+
data_dir: str | Path | None = None,
|
|
1179
|
+
data_home: str | Path | None = None,
|
|
1180
|
+
mode: BackendMode = "auto",
|
|
1181
|
+
backend: BackendMode | None = None,
|
|
1182
|
+
) -> AxDataClient:
|
|
1183
|
+
"""Create an AxData client.
|
|
1184
|
+
|
|
1185
|
+
If api_base or AXDATA_API_BASE is set, the client reads that AxData API
|
|
1186
|
+
service. Otherwise it reads the local AxData data directory directly.
|
|
1187
|
+
"""
|
|
1188
|
+
|
|
1189
|
+
return AxDataClient(
|
|
1190
|
+
token=token,
|
|
1191
|
+
api_base=api_base,
|
|
1192
|
+
data_root=data_root,
|
|
1193
|
+
data_dir=data_dir,
|
|
1194
|
+
data_home=data_home,
|
|
1195
|
+
mode=mode,
|
|
1196
|
+
backend=backend,
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def connect(
|
|
1201
|
+
*,
|
|
1202
|
+
api_base: str | None = None,
|
|
1203
|
+
data_root: str | Path | None = None,
|
|
1204
|
+
data_dir: str | Path | None = None,
|
|
1205
|
+
data_home: str | Path | None = None,
|
|
1206
|
+
token: str | None = None,
|
|
1207
|
+
mode: BackendMode = "auto",
|
|
1208
|
+
backend: BackendMode | None = None,
|
|
1209
|
+
) -> AxDataClient:
|
|
1210
|
+
"""Connect to local AxData data or a remote AxData API service."""
|
|
1211
|
+
|
|
1212
|
+
return AxDataClient(
|
|
1213
|
+
token=token,
|
|
1214
|
+
api_base=api_base,
|
|
1215
|
+
data_root=data_root,
|
|
1216
|
+
data_dir=data_dir,
|
|
1217
|
+
data_home=data_home,
|
|
1218
|
+
mode=mode,
|
|
1219
|
+
backend=backend,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
Client = AxDataClient
|