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.
- LTtx/__init__.py +2 -0
- LTtx/tx/Config.txt +8 -0
- LTtx/tx/LTtx_server.py +1177 -0
- LTtx/tx/__init__.py +5 -0
- LTtx/tx/tx.py +1935 -0
- cfquant/__init__.py +26 -0
- cfquant/account_routing.py +119 -0
- cfquant/channels.py +76 -0
- cfquant/cli.py +266 -0
- cfquant/client.py +365 -0
- cfquant/config.py +86 -0
- cfquant/hub.py +304 -0
- cfquant/logging_i18n.py +348 -0
- cfquant/normal_bridge.py +676 -0
- cfquant/pipe_bridge.py +249 -0
- cfquant/pipe_client.py +223 -0
- cfquant/pipe_hub.py +602 -0
- cfquant/pipe_transport.py +493 -0
- cfquant/protocol.py +238 -0
- cfquant/qmt_bridge.py +1081 -0
- cfquant/runtime_report.py +334 -0
- cfquant/tx.py +1898 -0
- cfquant/tx_trade_bridge.py +1905 -0
- cfquant/version.py +5 -0
- cfquant/xtconstant.py +262 -0
- cfquant/xtdata.py +462 -0
- cfquant/xttrader.py +955 -0
- cfquant/xttype.py +586 -0
- cfquant-0.2.0.dist-info/LICENSE +21 -0
- cfquant-0.2.0.dist-info/METADATA +144 -0
- cfquant-0.2.0.dist-info/RECORD +90 -0
- cfquant-0.2.0.dist-info/WHEEL +5 -0
- cfquant-0.2.0.dist-info/entry_points.txt +4 -0
- cfquant-0.2.0.dist-info/top_level.txt +6 -0
- cfquant_pipe_hub.py +45 -0
- cfquant_web_server.py +12137 -0
- qmt_scripts/CFQUANT.py +507 -0
- qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
- qmt_scripts/CFQUANT_LITE.py +4928 -0
- qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
- qmt_scripts/__init__.py +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/__init__.py +1 -0
- web_dashboard/app.js +9847 -0
- web_dashboard/avatars/amber-pulse.svg +15 -0
- web_dashboard/avatars/copper-grid.svg +15 -0
- web_dashboard/avatars/market-blue.svg +15 -0
- web_dashboard/avatars/rose-circuit.svg +15 -0
- web_dashboard/avatars/signal-green.svg +15 -0
- web_dashboard/avatars/slate-wave.svg +15 -0
- web_dashboard/avatars/teal-orbit.svg +16 -0
- web_dashboard/avatars/violet-node.svg +15 -0
- web_dashboard/favicon.svg +5 -0
- web_dashboard/image/qmt_deploy/image-10.png +0 -0
- web_dashboard/image/qmt_deploy/image-11.png +0 -0
- web_dashboard/image/qmt_deploy/image-12.png +0 -0
- web_dashboard/image/qmt_deploy/image-13.png +0 -0
- web_dashboard/image/qmt_deploy/image-14.png +0 -0
- web_dashboard/image/qmt_deploy/image-15.png +0 -0
- web_dashboard/image/qmt_deploy/image-16.png +0 -0
- web_dashboard/image/qmt_deploy/image-17.png +0 -0
- web_dashboard/image/qmt_deploy/image-18.png +0 -0
- web_dashboard/image/qmt_deploy/image-19.png +0 -0
- web_dashboard/image/qmt_deploy/image-20.png +0 -0
- web_dashboard/image/qmt_deploy/image-21.png +0 -0
- web_dashboard/image/qmt_deploy/image-4.png +0 -0
- web_dashboard/image/qmt_deploy/image-5.png +0 -0
- web_dashboard/image/qmt_deploy/image-6.png +0 -0
- web_dashboard/image/qmt_deploy/image-7.png +0 -0
- web_dashboard/image/qmt_deploy/image-8.png +0 -0
- web_dashboard/image/qmt_deploy/image-9.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/index.html +2317 -0
- web_dashboard/styles.css +7817 -0
- 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())
|