cfquant 0.2.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.
Files changed (90) hide show
  1. LTtx/__init__.py +2 -0
  2. LTtx/tx/Config.txt +8 -0
  3. LTtx/tx/LTtx_server.py +1177 -0
  4. LTtx/tx/__init__.py +5 -0
  5. LTtx/tx/tx.py +1935 -0
  6. cfquant/__init__.py +26 -0
  7. cfquant/account_routing.py +119 -0
  8. cfquant/channels.py +76 -0
  9. cfquant/cli.py +266 -0
  10. cfquant/client.py +365 -0
  11. cfquant/config.py +86 -0
  12. cfquant/hub.py +304 -0
  13. cfquant/logging_i18n.py +348 -0
  14. cfquant/normal_bridge.py +676 -0
  15. cfquant/pipe_bridge.py +249 -0
  16. cfquant/pipe_client.py +223 -0
  17. cfquant/pipe_hub.py +602 -0
  18. cfquant/pipe_transport.py +493 -0
  19. cfquant/protocol.py +238 -0
  20. cfquant/qmt_bridge.py +1081 -0
  21. cfquant/runtime_report.py +334 -0
  22. cfquant/tx.py +1898 -0
  23. cfquant/tx_trade_bridge.py +1905 -0
  24. cfquant/version.py +5 -0
  25. cfquant/xtconstant.py +262 -0
  26. cfquant/xtdata.py +462 -0
  27. cfquant/xttrader.py +955 -0
  28. cfquant/xttype.py +586 -0
  29. cfquant-0.2.0.dist-info/LICENSE +21 -0
  30. cfquant-0.2.0.dist-info/METADATA +144 -0
  31. cfquant-0.2.0.dist-info/RECORD +90 -0
  32. cfquant-0.2.0.dist-info/WHEEL +5 -0
  33. cfquant-0.2.0.dist-info/entry_points.txt +4 -0
  34. cfquant-0.2.0.dist-info/top_level.txt +6 -0
  35. cfquant_pipe_hub.py +45 -0
  36. cfquant_web_server.py +12137 -0
  37. qmt_scripts/CFQUANT.py +507 -0
  38. qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
  39. qmt_scripts/CFQUANT_LITE.py +4928 -0
  40. qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
  41. qmt_scripts/__init__.py +1 -0
  42. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
  43. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
  44. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
  45. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
  46. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
  47. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
  48. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
  49. web_dashboard/__init__.py +1 -0
  50. web_dashboard/app.js +9847 -0
  51. web_dashboard/avatars/amber-pulse.svg +15 -0
  52. web_dashboard/avatars/copper-grid.svg +15 -0
  53. web_dashboard/avatars/market-blue.svg +15 -0
  54. web_dashboard/avatars/rose-circuit.svg +15 -0
  55. web_dashboard/avatars/signal-green.svg +15 -0
  56. web_dashboard/avatars/slate-wave.svg +15 -0
  57. web_dashboard/avatars/teal-orbit.svg +16 -0
  58. web_dashboard/avatars/violet-node.svg +15 -0
  59. web_dashboard/favicon.svg +5 -0
  60. web_dashboard/image/qmt_deploy/image-10.png +0 -0
  61. web_dashboard/image/qmt_deploy/image-11.png +0 -0
  62. web_dashboard/image/qmt_deploy/image-12.png +0 -0
  63. web_dashboard/image/qmt_deploy/image-13.png +0 -0
  64. web_dashboard/image/qmt_deploy/image-14.png +0 -0
  65. web_dashboard/image/qmt_deploy/image-15.png +0 -0
  66. web_dashboard/image/qmt_deploy/image-16.png +0 -0
  67. web_dashboard/image/qmt_deploy/image-17.png +0 -0
  68. web_dashboard/image/qmt_deploy/image-18.png +0 -0
  69. web_dashboard/image/qmt_deploy/image-19.png +0 -0
  70. web_dashboard/image/qmt_deploy/image-20.png +0 -0
  71. web_dashboard/image/qmt_deploy/image-21.png +0 -0
  72. web_dashboard/image/qmt_deploy/image-4.png +0 -0
  73. web_dashboard/image/qmt_deploy/image-5.png +0 -0
  74. web_dashboard/image/qmt_deploy/image-6.png +0 -0
  75. web_dashboard/image/qmt_deploy/image-7.png +0 -0
  76. web_dashboard/image/qmt_deploy/image-8.png +0 -0
  77. web_dashboard/image/qmt_deploy/image-9.png +0 -0
  78. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
  79. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
  80. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
  81. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
  82. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
  83. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
  84. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
  85. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
  86. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
  87. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
  88. web_dashboard/index.html +2317 -0
  89. web_dashboard/styles.css +7817 -0
  90. web_dashboard/vendor/mermaid.min.js +2029 -0
cfquant/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ cfquant: 面向 QMT 的 xtquant 兼容层。
4
+
5
+ 外部程序导入本包后,可以按常见 xtquant 方式使用:
6
+
7
+ from cfquant import xtdata, xtconstant
8
+ from cfquant.xttrader import XtQuantTrader
9
+ from cfquant.xttype import StockAccount
10
+ """
11
+
12
+ from . import xtconstant, xtdata, xttrader, xttype
13
+ from .client import CfquantError, CfquantTimeout, configure, get_client
14
+ from .version import __version__
15
+
16
+ __all__ = [
17
+ "xtconstant",
18
+ "xtdata",
19
+ "xttrader",
20
+ "xttype",
21
+ "configure",
22
+ "get_client",
23
+ "CfquantError",
24
+ "CfquantTimeout",
25
+ "__version__",
26
+ ]
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ import threading
3
+
4
+
5
+ _lock = threading.RLock()
6
+ _subscribers = {}
7
+ _client_accounts = {}
8
+
9
+
10
+ def _account_type(account_type):
11
+ value = str(account_type or "STOCK").strip().upper()
12
+ mapping = {
13
+ "2": "STOCK",
14
+ "SECURITY": "STOCK",
15
+ "SECURITY_ACCOUNT": "STOCK",
16
+ "STOCK_ACCOUNT": "STOCK",
17
+ "3": "CREDIT",
18
+ "CREDIT_ACCOUNT": "CREDIT",
19
+ "MARGIN": "CREDIT",
20
+ }
21
+ return mapping.get(value, value or "STOCK")
22
+
23
+
24
+ def _key(bridge_id, account_id, account_type=None):
25
+ return (
26
+ str(bridge_id or "default").strip(),
27
+ _account_type(account_type),
28
+ str(account_id or "").strip(),
29
+ )
30
+
31
+
32
+ def subscribe(bridge_id, account_id, client_id, account_type=None):
33
+ bridge_id, account_type, account_id = _key(bridge_id, account_id, account_type)
34
+ client_id = str(client_id or "").strip()
35
+ if not account_id or not client_id:
36
+ return
37
+ with _lock:
38
+ _subscribers.setdefault((bridge_id, account_type, account_id), set()).add(client_id)
39
+ _client_accounts.setdefault((bridge_id, client_id), set()).add((account_type, account_id))
40
+
41
+
42
+ def unsubscribe(bridge_id, account_id=None, client_id=None, account_type=None):
43
+ bridge_id = str(bridge_id or "default").strip()
44
+ account_type = _account_type(account_type) if account_type not in (None, "") else ""
45
+ account_id = str(account_id or "").strip()
46
+ client_id = str(client_id or "").strip()
47
+ with _lock:
48
+ if account_id and client_id:
49
+ if account_type:
50
+ _remove_pair(bridge_id, account_id, client_id, account_type)
51
+ else:
52
+ for item_type in _account_types_for_account_locked(bridge_id, account_id):
53
+ _remove_pair(bridge_id, account_id, client_id, item_type)
54
+ return
55
+ if client_id:
56
+ accounts = _client_accounts.pop((bridge_id, client_id), set())
57
+ for item in accounts:
58
+ if isinstance(item, tuple):
59
+ item_type, item_account_id = item
60
+ else:
61
+ item_type, item_account_id = "STOCK", item
62
+ subscribers = _subscribers.get((bridge_id, item_type, item_account_id))
63
+ if subscribers:
64
+ subscribers.discard(client_id)
65
+ if not subscribers:
66
+ _subscribers.pop((bridge_id, item_type, item_account_id), None)
67
+ return
68
+ if account_id:
69
+ types = [account_type] if account_type else _account_types_for_account_locked(bridge_id, account_id)
70
+ for item_type in types:
71
+ subscribers = _subscribers.pop((bridge_id, item_type, account_id), set())
72
+ for item in subscribers:
73
+ accounts = _client_accounts.get((bridge_id, item))
74
+ if accounts:
75
+ accounts.discard((item_type, account_id))
76
+ accounts.discard(account_id)
77
+ if not accounts:
78
+ _client_accounts.pop((bridge_id, item), None)
79
+
80
+
81
+ def client_ids(bridge_id, account_id, account_type=None):
82
+ bridge_id, account_type, account_id = _key(bridge_id, account_id, account_type)
83
+ if not account_id:
84
+ return []
85
+ with _lock:
86
+ return sorted(_subscribers.get((bridge_id, account_type, account_id), set()))
87
+
88
+
89
+ def status(bridge_id):
90
+ bridge_id = str(bridge_id or "default").strip()
91
+ with _lock:
92
+ return dict(
93
+ ("%s:%s" % (account_type, account_id), len(client_ids))
94
+ for (item_bridge_id, account_type, account_id), client_ids in _subscribers.items()
95
+ if item_bridge_id == bridge_id
96
+ )
97
+
98
+
99
+ def _account_types_for_account_locked(bridge_id, account_id):
100
+ return [
101
+ account_type
102
+ for item_bridge_id, account_type, item_account_id in _subscribers.keys()
103
+ if item_bridge_id == bridge_id and item_account_id == account_id
104
+ ] or ["STOCK"]
105
+
106
+
107
+ def _remove_pair(bridge_id, account_id, client_id, account_type="STOCK"):
108
+ account_type = _account_type(account_type)
109
+ subscribers = _subscribers.get((bridge_id, account_type, account_id))
110
+ if subscribers:
111
+ subscribers.discard(client_id)
112
+ if not subscribers:
113
+ _subscribers.pop((bridge_id, account_type, account_id), None)
114
+ accounts = _client_accounts.get((bridge_id, client_id))
115
+ if accounts:
116
+ accounts.discard((account_type, account_id))
117
+ accounts.discard(account_id)
118
+ if not accounts:
119
+ _client_accounts.pop((bridge_id, client_id), None)
cfquant/channels.py ADDED
@@ -0,0 +1,76 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import re
4
+
5
+
6
+ LEGACY_NORMAL_REQUEST_CHANNEL = "cfquant.normal.request"
7
+ LEGACY_TRADE_REQUEST_CHANNEL = "cfquant.trade.request"
8
+ LEGACY_CALLBACK_EVENT_CHANNEL = "cfquant.callback.event"
9
+
10
+
11
+ def normalize_bridge_id(value=None):
12
+ value = str(value or "").strip()
13
+ if not value:
14
+ return "default"
15
+ value = re.sub(r"[^0-9A-Za-z_.-]+", "_", value)
16
+ return value or "default"
17
+
18
+
19
+ def bridge_id_from_env(default="default"):
20
+ return normalize_bridge_id(os.environ.get("CFQUANT_BRIDGE_ID") or default)
21
+
22
+
23
+ def bridge_env_prefix(bridge_id):
24
+ safe = re.sub(r"[^0-9A-Za-z]+", "_", normalize_bridge_id(bridge_id)).upper()
25
+ return "CFQUANT_BRIDGE_%s_" % safe
26
+
27
+
28
+ def channels_for_bridge(bridge_id=None):
29
+ bridge_id = normalize_bridge_id(bridge_id or bridge_id_from_env())
30
+ prefix = bridge_env_prefix(bridge_id)
31
+ if bridge_id == "default":
32
+ default_normal = os.environ.get("CFQUANT_NORMAL_REQUEST_CHANNEL", LEGACY_NORMAL_REQUEST_CHANNEL)
33
+ default_trade = os.environ.get("CFQUANT_TRADE_REQUEST_CHANNEL", LEGACY_TRADE_REQUEST_CHANNEL)
34
+ default_callback = os.environ.get("CFQUANT_CALLBACK_EVENT_CHANNEL", LEGACY_CALLBACK_EVENT_CHANNEL)
35
+ else:
36
+ default_normal = "cfquant.%s.normal.request" % bridge_id
37
+ default_trade = "cfquant.%s.trade.request" % bridge_id
38
+ default_callback = "cfquant.%s.callback.event" % bridge_id
39
+ return {
40
+ "normal": os.environ.get(prefix + "NORMAL_REQUEST_CHANNEL", default_normal),
41
+ "trade": os.environ.get(prefix + "TRADE_REQUEST_CHANNEL", default_trade),
42
+ "callback": os.environ.get(prefix + "CALLBACK_EVENT_CHANNEL", default_callback),
43
+ }
44
+
45
+
46
+ def bridge_name(bridge_id):
47
+ bridge_id = normalize_bridge_id(bridge_id)
48
+ prefix = bridge_env_prefix(bridge_id)
49
+ return os.environ.get(prefix + "NAME", bridge_id)
50
+
51
+
52
+ def configured_bridge_ids():
53
+ raw = os.environ.get("CFQUANT_BRIDGE_IDS")
54
+ if raw:
55
+ ids = [normalize_bridge_id(item) for item in raw.split(",") if item.strip()]
56
+ else:
57
+ ids = [bridge_id_from_env()]
58
+ seen = set()
59
+ result = []
60
+ for bridge_id in ids:
61
+ if bridge_id in seen:
62
+ continue
63
+ seen.add(bridge_id)
64
+ result.append(bridge_id)
65
+ return result or ["default"]
66
+
67
+
68
+ def configured_bridges():
69
+ result = {}
70
+ for bridge_id in configured_bridge_ids():
71
+ result[bridge_id] = {
72
+ "id": bridge_id,
73
+ "name": bridge_name(bridge_id),
74
+ "channels": channels_for_bridge(bridge_id),
75
+ }
76
+ return result
cfquant/cli.py ADDED
@@ -0,0 +1,266 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Command-line entry points for the installed cfquant package."""
3
+
4
+ from __future__ import print_function
5
+
6
+ import argparse
7
+ import os
8
+ import shutil
9
+ import socket
10
+ import sys
11
+ import threading
12
+ import time
13
+ import webbrowser
14
+ from pathlib import Path
15
+
16
+ from .version import WEB_VERSION, __version__ as CORE_VERSION
17
+
18
+
19
+ QMT_SCRIPT_FILES = (
20
+ "CFQUANT.py",
21
+ "CFQUANT_CTYPE_ALL_LOWLAT.py",
22
+ "CFQUANT_LITE.py",
23
+ "CFQUANT_TRADE_LOWLAT.py",
24
+ "同账号独立市场/CFQUANT_CTYPE_ALL_LOWLAT_SH.py",
25
+ "同账号独立市场/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py",
26
+ "同账号独立市场/CFQUANT_LITE_SH.py",
27
+ "同账号独立市场/CFQUANT_LITE_SZ.py",
28
+ "同账号独立市场/CFQUANT_TRADE_LOWLAT_SH.py",
29
+ "同账号独立市场/CFQUANT_TRADE_LOWLAT_SZ.py",
30
+ "同账号独立市场/readme.md",
31
+ )
32
+
33
+
34
+ def _package_version():
35
+ try:
36
+ from importlib import metadata
37
+
38
+ return metadata.version("cfquant")
39
+ except Exception:
40
+ return "unknown"
41
+
42
+
43
+ def _port(value):
44
+ try:
45
+ port = int(value)
46
+ except (TypeError, ValueError):
47
+ raise argparse.ArgumentTypeError("port must be an integer")
48
+ if port < 1 or port > 65535:
49
+ raise argparse.ArgumentTypeError("port must be between 1 and 65535")
50
+ return port
51
+
52
+
53
+ def _path(value):
54
+ value = str(value or "").strip()
55
+ if not value:
56
+ return ""
57
+ return os.path.abspath(os.path.expandvars(os.path.expanduser(value)))
58
+
59
+
60
+ def _apply_web_environment(args):
61
+ mappings = (
62
+ ("home", "CFQUANT_HOME"),
63
+ ("runtime_dir", "CFQUANT_RUNTIME_DIR"),
64
+ ("log_dir", "CFQUANT_LOG_DIR"),
65
+ ("config", "CFQUANT_WEB_CONFIG_FILE"),
66
+ ("pipe_name", "CFQUANT_PIPE_NAME"),
67
+ ("lttx_host", "CFQUANT_LTTX_HOST"),
68
+ ("account_id", "CFQUANT_ACCOUNT_ID"),
69
+ )
70
+ for attribute, variable in mappings:
71
+ value = getattr(args, attribute, None)
72
+ if value:
73
+ os.environ[variable] = _path(value) if variable.endswith(("_DIR", "_FILE", "_HOME")) else str(value)
74
+ if getattr(args, "lttx_port", None):
75
+ os.environ["CFQUANT_LTTX_PORT"] = str(args.lttx_port)
76
+
77
+
78
+ def _serve_parser(prog="cfquant"):
79
+ help_command = "%s serve --help" % prog if prog == "cfquant" else "%s --help" % prog
80
+ if prog == "cfquant":
81
+ epilog = (
82
+ "Commands:\n"
83
+ " cfquant pipe-hub [options] Start only the named-pipe hub.\n"
84
+ " cfquant qmt-scripts --output DIRECTORY Export bundled QMT entry scripts.\n"
85
+ " cfquant version Show installed package details.\n"
86
+ "Use '%s' for dashboard options." % help_command
87
+ )
88
+ else:
89
+ epilog = "Use '%s' for dashboard options." % help_command
90
+ parser = argparse.ArgumentParser(
91
+ prog=prog,
92
+ description="Start the cfquant local web dashboard.",
93
+ epilog=epilog,
94
+ formatter_class=argparse.RawDescriptionHelpFormatter,
95
+ )
96
+ parser.add_argument("--host", help="Web bind address. Defaults to localhost or saved configuration.")
97
+ parser.add_argument("--port", type=_port, help="Web listen port. Defaults to saved configuration or 8765.")
98
+ parser.add_argument(
99
+ "--home",
100
+ "--state-dir",
101
+ dest="home",
102
+ help="Writable cfquant state directory for runtime data, configuration, and logs.",
103
+ )
104
+ parser.add_argument("--runtime-dir", help="Override the runtime subdirectory.")
105
+ parser.add_argument("--log-dir", help="Override the log directory.")
106
+ parser.add_argument("--config", help="Path to the web configuration JSON file.")
107
+ parser.add_argument("--pipe-name", help=r"Override the QMT named pipe, e.g. \\.\pipe\cfquant_pipe_hub.")
108
+ parser.add_argument("--lttx-host", help="LTtx host. Defaults to 127.0.0.1.")
109
+ parser.add_argument("--lttx-port", type=_port, help="LTtx port. Defaults to 2049.")
110
+ parser.add_argument("--account-id", help="Default account ID before the web setup wizard is completed.")
111
+ parser.add_argument("--open-browser", action="store_true", help="Open the dashboard in the default browser after it is ready.")
112
+ parser.add_argument("--dry-run", action="store_true", help="Print the effective startup settings without starting services.")
113
+ parser.add_argument("--version", action="version", version=_version_text())
114
+ return parser
115
+
116
+
117
+ def _version_text():
118
+ return "cfquant package %s\ncore %s\nweb %s" % (_package_version(), CORE_VERSION, WEB_VERSION)
119
+
120
+
121
+ def _browser_url(host, port):
122
+ host = str(host or "").strip()
123
+ if not host or host in ("0.0.0.0", "::"):
124
+ host = "127.0.0.1"
125
+ if ":" in host and not host.startswith("["):
126
+ host = "[%s]" % host
127
+ return "http://%s:%s/" % (host, port)
128
+
129
+
130
+ def _wait_and_open_browser(host, port):
131
+ connect_host = str(host or "").strip()
132
+ if not connect_host or connect_host in ("0.0.0.0", "::"):
133
+ connect_host = "127.0.0.1"
134
+ url = _browser_url(connect_host, port)
135
+ deadline = time.monotonic() + 30.0
136
+ while time.monotonic() < deadline:
137
+ try:
138
+ with socket.create_connection((connect_host.strip("[]"), int(port)), timeout=0.4):
139
+ webbrowser.open(url)
140
+ return
141
+ except OSError:
142
+ time.sleep(0.25)
143
+
144
+
145
+ def _serve(argv, prog="cfquant"):
146
+ parser = _serve_parser(prog)
147
+ args = parser.parse_args(argv)
148
+ _apply_web_environment(args)
149
+ if args.dry_run:
150
+ print("cfquant package=%s core=%s web=%s" % (_package_version(), CORE_VERSION, WEB_VERSION))
151
+ for key in (
152
+ "CFQUANT_HOME",
153
+ "CFQUANT_RUNTIME_DIR",
154
+ "CFQUANT_LOG_DIR",
155
+ "CFQUANT_WEB_CONFIG_FILE",
156
+ "CFQUANT_PIPE_NAME",
157
+ "CFQUANT_LTTX_HOST",
158
+ "CFQUANT_LTTX_PORT",
159
+ "CFQUANT_ACCOUNT_ID",
160
+ ):
161
+ if os.environ.get(key):
162
+ print("%s=%s" % (key, os.environ[key]))
163
+ return 0
164
+
165
+ from cfquant_web_server import main as web_main
166
+
167
+ server_args = []
168
+ if args.host:
169
+ server_args.extend(["--host", args.host])
170
+ if args.port:
171
+ server_args.extend(["--port", str(args.port)])
172
+ if args.open_browser:
173
+ from cfquant_web_server import WEB_CONFIG, normalize_web_port
174
+
175
+ host = args.host or os.environ.get("CFQUANT_WEB_HOST")
176
+ if not host:
177
+ host = "0.0.0.0" if WEB_CONFIG.allow_remote() else "127.0.0.1"
178
+ port = args.port
179
+ if port is None:
180
+ port = normalize_web_port(
181
+ os.environ.get("CFQUANT_WEB_PORT"),
182
+ default=WEB_CONFIG.web_port(),
183
+ )
184
+ thread = threading.Thread(target=_wait_and_open_browser, args=(host, port))
185
+ thread.daemon = True
186
+ thread.start()
187
+ return web_main(server_args)
188
+
189
+
190
+ def _qmt_scripts_source_dir():
191
+ import qmt_scripts
192
+
193
+ return Path(qmt_scripts.__file__).resolve().parent
194
+
195
+
196
+ def _qmt_scripts_parser():
197
+ parser = argparse.ArgumentParser(
198
+ prog="cfquant qmt-scripts",
199
+ description="Export the QMT entry scripts bundled with this cfquant installation.",
200
+ )
201
+ parser.add_argument("--output", "-o", required=True, help="Destination directory for QMT scripts.")
202
+ parser.add_argument("--force", action="store_true", help="Overwrite existing files in the destination.")
203
+ return parser
204
+
205
+
206
+ def _export_qmt_scripts(argv):
207
+ parser = _qmt_scripts_parser()
208
+ args = parser.parse_args(argv)
209
+ output_dir = Path(_path(args.output))
210
+ if output_dir.exists() and not output_dir.is_dir():
211
+ parser.error("output path is not a directory: %s" % output_dir)
212
+
213
+ source_dir = _qmt_scripts_source_dir()
214
+ missing = [name for name in QMT_SCRIPT_FILES if not (source_dir / name).is_file()]
215
+ if missing:
216
+ raise RuntimeError("installed cfquant package is missing QMT scripts: %s" % ", ".join(missing))
217
+ existing = [name for name in QMT_SCRIPT_FILES if (output_dir / name).exists()]
218
+ if existing and not args.force:
219
+ parser.error("destination already has files; use --force to replace: %s" % ", ".join(existing))
220
+
221
+ output_dir.mkdir(parents=True, exist_ok=True)
222
+ for relative_name in QMT_SCRIPT_FILES:
223
+ source = source_dir / relative_name
224
+ target = output_dir / relative_name
225
+ target.parent.mkdir(parents=True, exist_ok=True)
226
+ shutil.copy2(str(source), str(target))
227
+ print("Exported %s QMT files to %s" % (len(QMT_SCRIPT_FILES), output_dir))
228
+ return 0
229
+
230
+
231
+ def _print_version():
232
+ package_root = Path(__file__).resolve().parent
233
+ qmt_scripts_dir = _qmt_scripts_source_dir()
234
+ print(_version_text())
235
+ print("package_root %s" % package_root)
236
+ print("qmt_scripts %s" % qmt_scripts_dir)
237
+ return 0
238
+
239
+
240
+ def web_main(argv=None):
241
+ """Backward-compatible entry point for the ``cfquant-web`` command."""
242
+ return _serve(list(sys.argv[1:] if argv is None else argv), prog="cfquant-web")
243
+
244
+
245
+ def main(argv=None):
246
+ argv = list(sys.argv[1:] if argv is None else argv)
247
+ if not argv:
248
+ return _serve([])
249
+ command = argv[0]
250
+ if command in ("serve", "web"):
251
+ return _serve(argv[1:])
252
+ if command == "pipe-hub":
253
+ from cfquant_pipe_hub import main as pipe_hub_main
254
+
255
+ return pipe_hub_main(argv[1:])
256
+ if command in ("qmt-scripts", "qmt"):
257
+ return _export_qmt_scripts(argv[1:])
258
+ if command in ("version", "info"):
259
+ return _print_version()
260
+ if command in ("help", "-h", "--help"):
261
+ return _serve(["--help"])
262
+ return _serve(argv)
263
+
264
+
265
+ if __name__ == "__main__":
266
+ raise SystemExit(main())