Repository navigation
Expand file tree
/
Copy pathcli.py
More file actions
157 lines (130 loc) · 5.35 KB
/
Copy pathcli.py
File metadata and controls
157 lines (130 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import argparse
import asyncio
import json
import logging
import pathlib
import sys
import typing
import liquidity_audit.application.workflows.daily_run as daily_run_workflow
import liquidity_audit.application.workflows.identify_selected_only as identify_selected_only_workflow
import liquidity_audit.application.workflows.re_evaluate_data as re_evaluate_data_workflow
import liquidity_audit.application.workflows.resolve_websites as resolve_websites_workflow
import liquidity_audit.application.workflows.standalone_fetch as standalone_fetch_workflow
import liquidity_audit.config as app_config
import liquidity_audit.infrastructure.dotenv_loader as dotenv_loader
import liquidity_audit.infrastructure.mattermost_notifier as mattermost_notifier
_LOGGER = logging.getLogger(pathlib.Path(__file__).stem)
class _ShortNameFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
original_name = record.name
record.name = original_name.rsplit(".", 1)[-1]
try:
return super().format(record)
finally:
record.name = original_name
def _configure_logging(*, log_stream: typing.Optional[typing.TextIO] = None) -> None:
log_handler = logging.StreamHandler(log_stream)
log_handler.setFormatter(_ShortNameFormatter(
"%(asctime)s %(levelname)s [%(name)s] %(message)s",
))
logging.basicConfig(level=logging.INFO, handlers=[log_handler], force=True)
logging.getLogger("ccxt").setLevel(logging.WARNING)
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Liquidity Audit sync")
parser.add_argument("--config", required=True, help="Path to JSON config file")
subparsers = parser.add_subparsers(dest="command", required=True)
run_parser = subparsers.add_parser(
"run",
help="Full daily pipeline: discover listings, analyze health, select projects",
)
run_parser.add_argument(
"--identify-only",
action="store_true",
help="Discover and save new listings only; skip analysis and selection",
)
subparsers.add_parser(
"re-evaluate-data",
help="Refresh health metrics from CSV and rebuild analysis JSON from cached raw blocks",
)
subparsers.add_parser(
"identify-selected-only",
help="Select daily projects from listings.csv and append selected_history.csv",
)
subparsers.add_parser(
"update-websites",
help="Resolve CoinGecko websites for selection candidates and save to listings.csv",
)
standalone_parser = subparsers.add_parser(
"standalone",
help="Fetch and analyze specific pairs; print JSON to stdout (exchange data only)",
)
standalone_parser.add_argument(
"pairs",
nargs="+",
metavar="BASE/QUOTE:exchange",
help="One or more pair specs, e.g. ULX/USDT:bitmart",
)
return parser
def _parse_args(argv: list[str]) -> argparse.Namespace:
return _build_parser().parse_args(argv)
async def _dispatch(command: str, args: argparse.Namespace, config: app_config.AppConfig) -> typing.Any:
if command == "run":
if args.identify_only:
_LOGGER.info("Identify-only mode enabled")
run_result = await daily_run_workflow.run(config, identify_only=args.identify_only)
if run_result is not None:
await mattermost_notifier.send_run_notifications(
run_result.run_summary,
run_result.daily_selections,
)
return
if command == "re-evaluate-data":
await re_evaluate_data_workflow.run(config)
return
if command == "identify-selected-only":
run_result = await identify_selected_only_workflow.run(config)
await mattermost_notifier.send_run_notifications(
run_result.run_summary,
run_result.daily_selections,
post=False,
)
_LOGGER.info(
"Identify-selected-only complete: selections_proposed=%s",
run_result.run_summary.selections_proposed_total,
)
return
if command == "update-websites":
resolved_count = await resolve_websites_workflow.run(config)
_LOGGER.info("Update-websites complete: %s listing(s) updated", resolved_count)
return
if command == "standalone":
return await standalone_fetch_workflow.run(config, args.pairs)
raise ValueError(f"Unknown command: {command}")
def main(argv: list[str] | None = None) -> int:
dotenv_loader.load_project_dotenv()
try:
args = _parse_args(argv or sys.argv[1:])
except SystemExit as error:
return int(error.code or 2)
_configure_logging(
log_stream=sys.stderr if args.command == "standalone" else None,
)
config = app_config.load_config(args.config)
_LOGGER.info(
"Config loaded: exchanges=%s, listings_csv=%s",
config.exchanges,
config.listings_csv_path,
)
try:
dispatch_result = asyncio.run(_dispatch(args.command, args, config))
if args.command == "standalone":
print(json.dumps(dispatch_result, ensure_ascii=False), flush=True)
if any("error" in item for item in dispatch_result):
return 1
return 0
except ValueError as error:
_LOGGER.error("%s", error)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())