aipa-cli 0.1.26__tar.gz → 0.1.28__tar.gz

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 (46) hide show
  1. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/CHANGELOG.md +15 -0
  2. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/PKG-INFO +83 -6
  3. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/README.md +81 -4
  4. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/pyproject.toml +1 -1
  5. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/__init__.py +1 -1
  6. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/tools.py +178 -0
  7. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/cli.py +30 -0
  8. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/cli_commands.py +145 -0
  9. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/.gitignore +0 -0
  10. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/LICENSE +0 -0
  11. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/__main__.py +0 -0
  12. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/actions.py +0 -0
  13. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/__init__.py +0 -0
  14. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/agent.py +0 -0
  15. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/callbacks.py +0 -0
  16. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/config.py +0 -0
  17. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/agents/personas.py +0 -0
  18. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/analyze.py +0 -0
  19. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/app.py +0 -0
  20. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/bindings.py +0 -0
  21. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/chart.py +0 -0
  22. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/chat.py +0 -0
  23. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/cli_setup.py +0 -0
  24. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/deep_research.py +0 -0
  25. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/session.py +0 -0
  26. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/settings_tab.py +0 -0
  27. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/theme.py +0 -0
  28. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/ticker_data.py +0 -0
  29. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/user_settings.py +0 -0
  30. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/utils.py +0 -0
  31. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/widgets/__init__.py +0 -0
  32. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/widgets/chat_input.py +0 -0
  33. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/widgets/safe_rich_log.py +0 -0
  34. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/widgets/ticker_select.py +0 -0
  35. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/src/aipriceaction_terminal/workflows.py +0 -0
  36. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/conftest.py +0 -0
  37. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/openrouter_responses.py +0 -0
  38. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_app.py +0 -0
  39. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_chat.py +0 -0
  40. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_integration.py +0 -0
  41. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_settings_api.py +0 -0
  42. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_thinking.py +0 -0
  43. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_tool_call_streaming.py +0 -0
  44. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_tools.py +0 -0
  45. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_utils.py +0 -0
  46. {aipa_cli-0.1.26 → aipa_cli-0.1.28}/tests/test_workflows.py +0 -0
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.28] - 2026-05-11
9
+
10
+ ### Added
11
+ - `--group` sector filter for `aipa performers` and `get_performers` agent tool (e.g. `--group NGAN_HANG`)
12
+
13
+ ## [0.1.27] - 2026-05-11
14
+
15
+ ### Added
16
+ - `aipa performers` CLI command: rank top/worst performers by price change, volume, value (close × volume), MA scores, or money flow (defaults to VN stocks)
17
+ - `aipa volume-profile TICKER` CLI command: volume-by-price histogram analysis with POC, value area, and statistics
18
+ - `get_performers` and `get_volume_profile` agent tools for AI-powered market analysis
19
+
20
+ ### Changed
21
+ - Require `aipriceaction>=0.1.13` for performers and volume_profile modules
22
+
8
23
  ## [0.1.26] - 2026-05-11
9
24
 
10
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipa-cli
3
- Version: 0.1.26
3
+ Version: 0.1.28
4
4
  Summary: Terminal TUI for AI-powered ticker analysis
5
5
  Project-URL: Homepage, https://github.com/quanhua92/aipriceaction
6
6
  Project-URL: Repository, https://github.com/quanhua92/aipriceaction
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Office/Business :: Financial
17
17
  Requires-Python: >=3.13
18
- Requires-Dist: aipriceaction>=0.1.12
18
+ Requires-Dist: aipriceaction>=0.1.13
19
19
  Requires-Dist: langchain-core
20
20
  Requires-Dist: langchain-openai
21
21
  Requires-Dist: langgraph
@@ -134,6 +134,21 @@ aipa ticker-list --source vn --group NGAN_HANG
134
134
  # Compact output (symbols only)
135
135
  aipa ticker-list --source crypto --compact
136
136
 
137
+ # Top 10 VN performers by price change (default)
138
+ aipa performers
139
+
140
+ # Top 5 by volume
141
+ aipa performers --sort-by volume --limit 5
142
+
143
+ # Top 10 by trading value
144
+ aipa performers --sort-by value --limit 10
145
+
146
+ # Volume profile for VCB today
147
+ aipa volume-profile VCB
148
+
149
+ # Volume profile for BTCUSDT
150
+ aipa volume-profile BTCUSDT --source crypto --bins 30
151
+
137
152
  # List saved chat sessions
138
153
  aipa resume
139
154
 
@@ -175,7 +190,7 @@ aipa analyze VCB --lang en
175
190
  | `--question TEXT` | Custom analysis question |
176
191
  | `--questions` | List available question templates and exit |
177
192
  | `--context-only` | Dump raw context without LLM (no API key needed) |
178
- | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W` (default: `1D`) |
193
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
179
194
  | `--limit N` | Number of bars (default: 20) |
180
195
  | `--source` | Filter by source: `vn` or `crypto` |
181
196
  | `--start-date` / `--end-date` | Date range (e.g. `2026-04-01`) |
@@ -241,7 +256,7 @@ aipa get-ohlcv-data BTCUSDT --interval 1D --limit 30
241
256
 
242
257
  | Flag | Description |
243
258
  |---|---|
244
- | `--interval` | Time interval (default: `1D`) |
259
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
245
260
  | `--limit N` | Number of bars |
246
261
  | `--start-date` / `--end-date` | Date range |
247
262
  | `--source` | Filter by source: `vn` or `crypto` |
@@ -272,7 +287,7 @@ aipa live-data VCB TCB MBB
272
287
  |---|---|
273
288
  | `TICKERS...` | Optional ticker symbols (omit for top N) |
274
289
  | `--top N` | Number of top tickers to show (default: 50) |
275
- | `--interval` | Time interval: `1D`, `1h`, `1m` (default: `1D`) |
290
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
276
291
  | `--source` | Filter by source: `vn`, `crypto`, `global`, `sjc` |
277
292
 
278
293
  ### `aipa ticker-list`
@@ -299,6 +314,66 @@ aipa ticker-list --source crypto --compact
299
314
  | `--group` | Filter by group (e.g. `NGAN_HANG`, `CHUNG_KHOAN`, `BAT_DONG_SAN`) |
300
315
  | `--compact` | Output symbols only, comma-separated |
301
316
 
317
+ ### `aipa performers`
318
+
319
+ Rank top and worst performers by any metric. Fetches live daily data with MA indicators — no API key needed. Defaults to VN stocks.
320
+
321
+ ```
322
+ # Top 10 VN stocks by price change (default)
323
+ aipa performers
324
+
325
+ # Top 5 by volume, ascending
326
+ aipa performers --sort-by volume --direction asc --limit 5
327
+
328
+ # Top 20 by MA50 score
329
+ aipa performers --sort-by ma50_score --limit 20
330
+
331
+ # Crypto performers
332
+ aipa performers --source crypto --limit 5
333
+
334
+ # Banking sector only
335
+ aipa performers --group NGAN_HANG --sort-by value
336
+
337
+ # Securities sector top gainers
338
+ aipa performers --group CHUNG_KHOAN --sort-by close_changed --limit 5
339
+ ```
340
+
341
+ | Flag | Description |
342
+ |---|---|
343
+ | `--sort-by` | Metric: `close_changed` (default), `volume`, `value`, `volume_changed`, `ma10_score`, `ma20_score`, `ma50_score`, `ma100_score`, `ma200_score`, `total_money_changed` |
344
+ | `--direction` | `desc` (default) or `asc` |
345
+ | `--limit N` | Entries per list (default: 10) |
346
+ | `--min-volume N` | Min volume filter for VN tickers (default: 10000) |
347
+ | `--source` | Data source: `vn` (default), `crypto`, `global`, `yahoo`, `sjc` |
348
+ | `--group` | Filter by sector (e.g. `NGAN_HANG`, `CHUNG_KHOAN`, `BAT_DONG_SAN`) |
349
+
350
+ ### `aipa volume-profile`
351
+
352
+ Volume-by-price histogram analysis from 1-minute data. Shows POC, value area, volume-weighted statistics, and a visual bar chart — no API key needed.
353
+
354
+ ```
355
+ # Today's profile for VCB
356
+ aipa volume-profile VCB
357
+
358
+ # Specific date
359
+ aipa volume-profile VCB --date 2026-05-09
360
+
361
+ # Crypto with fewer bins
362
+ aipa volume-profile BTCUSDT --source crypto --bins 30
363
+
364
+ # Date range with custom value area
365
+ aipa volume-profile FPT --start-date 2026-05-05 --end-date 2026-05-09 --value-area-pct 80
366
+ ```
367
+
368
+ | Flag | Description |
369
+ |---|---|
370
+ | `TICKER` | Ticker symbol (required) |
371
+ | `--date` | Single date (YYYY-MM-DD), defaults to today |
372
+ | `--start-date` / `--end-date` | Date range |
373
+ | `--source` | Source for tick size logic: `vn` (default), `crypto`, `global`, `yahoo`, `sjc` |
374
+ | `--bins N` | Number of price bins (default: 50, range: 2–200) |
375
+ | `--value-area-pct` | Value area target % (default: 70, range: 60–90) |
376
+
302
377
  ### `aipa setup`
303
378
 
304
379
  Interactive first-run configuration. Prompts for language, reference ticker, API key, base URL, and model. Settings are saved to `~/.aipriceaction/settings.json`. Re-running shows current values as defaults.
@@ -336,6 +411,8 @@ Commands that require an API key will auto-run `aipa setup` on first use if not
336
411
  | `aipa get-ohlcv-data` | No setup needed |
337
412
  | `aipa live-data` | No setup needed |
338
413
  | `aipa ticker-list` | No setup needed |
414
+ | `aipa performers` | No setup needed |
415
+ | `aipa volume-profile VCB` | No setup needed |
339
416
  | `aipa analyze VCB --context-only` | No setup needed |
340
417
  | `aipa analyze VCB --questions` | No setup needed |
341
418
  | `aipa resume` | No setup needed |
@@ -350,10 +427,10 @@ Commands that require an API key will auto-run `aipa setup` on first use if not
350
427
  Launch the TUI with `aipa`. The interface has six tabs:
351
428
 
352
429
  - **Chat** — AI-powered chat with streaming responses, thinking/reasoning display, slash commands, and arrow-key history navigation
430
+ - **Workflows** — Structured analysis forms with question bank dropdown for ticker analysis and deep research
353
431
  - **Vietnam** — Browse and search Vietnamese stock tickers
354
432
  - **Crypto** — Browse and search cryptocurrency tickers
355
433
  - **Global** — Browse and search global/Yahoo tickers
356
- - **Workflows** — Structured analysis forms with question bank dropdown for ticker analysis and deep research
357
434
  - **Settings** — Configure API key, model, base URL, and other preferences
358
435
 
359
436
  ### Slash Commands (Chat tab)
@@ -108,6 +108,21 @@ aipa ticker-list --source vn --group NGAN_HANG
108
108
  # Compact output (symbols only)
109
109
  aipa ticker-list --source crypto --compact
110
110
 
111
+ # Top 10 VN performers by price change (default)
112
+ aipa performers
113
+
114
+ # Top 5 by volume
115
+ aipa performers --sort-by volume --limit 5
116
+
117
+ # Top 10 by trading value
118
+ aipa performers --sort-by value --limit 10
119
+
120
+ # Volume profile for VCB today
121
+ aipa volume-profile VCB
122
+
123
+ # Volume profile for BTCUSDT
124
+ aipa volume-profile BTCUSDT --source crypto --bins 30
125
+
111
126
  # List saved chat sessions
112
127
  aipa resume
113
128
 
@@ -149,7 +164,7 @@ aipa analyze VCB --lang en
149
164
  | `--question TEXT` | Custom analysis question |
150
165
  | `--questions` | List available question templates and exit |
151
166
  | `--context-only` | Dump raw context without LLM (no API key needed) |
152
- | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W` (default: `1D`) |
167
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
153
168
  | `--limit N` | Number of bars (default: 20) |
154
169
  | `--source` | Filter by source: `vn` or `crypto` |
155
170
  | `--start-date` / `--end-date` | Date range (e.g. `2026-04-01`) |
@@ -215,7 +230,7 @@ aipa get-ohlcv-data BTCUSDT --interval 1D --limit 30
215
230
 
216
231
  | Flag | Description |
217
232
  |---|---|
218
- | `--interval` | Time interval (default: `1D`) |
233
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
219
234
  | `--limit N` | Number of bars |
220
235
  | `--start-date` / `--end-date` | Date range |
221
236
  | `--source` | Filter by source: `vn` or `crypto` |
@@ -246,7 +261,7 @@ aipa live-data VCB TCB MBB
246
261
  |---|---|
247
262
  | `TICKERS...` | Optional ticker symbols (omit for top N) |
248
263
  | `--top N` | Number of top tickers to show (default: 50) |
249
- | `--interval` | Time interval: `1D`, `1h`, `1m` (default: `1D`) |
264
+ | `--interval` | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1D`, `1W`, `2W` (default: `1D`) |
250
265
  | `--source` | Filter by source: `vn`, `crypto`, `global`, `sjc` |
251
266
 
252
267
  ### `aipa ticker-list`
@@ -273,6 +288,66 @@ aipa ticker-list --source crypto --compact
273
288
  | `--group` | Filter by group (e.g. `NGAN_HANG`, `CHUNG_KHOAN`, `BAT_DONG_SAN`) |
274
289
  | `--compact` | Output symbols only, comma-separated |
275
290
 
291
+ ### `aipa performers`
292
+
293
+ Rank top and worst performers by any metric. Fetches live daily data with MA indicators — no API key needed. Defaults to VN stocks.
294
+
295
+ ```
296
+ # Top 10 VN stocks by price change (default)
297
+ aipa performers
298
+
299
+ # Top 5 by volume, ascending
300
+ aipa performers --sort-by volume --direction asc --limit 5
301
+
302
+ # Top 20 by MA50 score
303
+ aipa performers --sort-by ma50_score --limit 20
304
+
305
+ # Crypto performers
306
+ aipa performers --source crypto --limit 5
307
+
308
+ # Banking sector only
309
+ aipa performers --group NGAN_HANG --sort-by value
310
+
311
+ # Securities sector top gainers
312
+ aipa performers --group CHUNG_KHOAN --sort-by close_changed --limit 5
313
+ ```
314
+
315
+ | Flag | Description |
316
+ |---|---|
317
+ | `--sort-by` | Metric: `close_changed` (default), `volume`, `value`, `volume_changed`, `ma10_score`, `ma20_score`, `ma50_score`, `ma100_score`, `ma200_score`, `total_money_changed` |
318
+ | `--direction` | `desc` (default) or `asc` |
319
+ | `--limit N` | Entries per list (default: 10) |
320
+ | `--min-volume N` | Min volume filter for VN tickers (default: 10000) |
321
+ | `--source` | Data source: `vn` (default), `crypto`, `global`, `yahoo`, `sjc` |
322
+ | `--group` | Filter by sector (e.g. `NGAN_HANG`, `CHUNG_KHOAN`, `BAT_DONG_SAN`) |
323
+
324
+ ### `aipa volume-profile`
325
+
326
+ Volume-by-price histogram analysis from 1-minute data. Shows POC, value area, volume-weighted statistics, and a visual bar chart — no API key needed.
327
+
328
+ ```
329
+ # Today's profile for VCB
330
+ aipa volume-profile VCB
331
+
332
+ # Specific date
333
+ aipa volume-profile VCB --date 2026-05-09
334
+
335
+ # Crypto with fewer bins
336
+ aipa volume-profile BTCUSDT --source crypto --bins 30
337
+
338
+ # Date range with custom value area
339
+ aipa volume-profile FPT --start-date 2026-05-05 --end-date 2026-05-09 --value-area-pct 80
340
+ ```
341
+
342
+ | Flag | Description |
343
+ |---|---|
344
+ | `TICKER` | Ticker symbol (required) |
345
+ | `--date` | Single date (YYYY-MM-DD), defaults to today |
346
+ | `--start-date` / `--end-date` | Date range |
347
+ | `--source` | Source for tick size logic: `vn` (default), `crypto`, `global`, `yahoo`, `sjc` |
348
+ | `--bins N` | Number of price bins (default: 50, range: 2–200) |
349
+ | `--value-area-pct` | Value area target % (default: 70, range: 60–90) |
350
+
276
351
  ### `aipa setup`
277
352
 
278
353
  Interactive first-run configuration. Prompts for language, reference ticker, API key, base URL, and model. Settings are saved to `~/.aipriceaction/settings.json`. Re-running shows current values as defaults.
@@ -310,6 +385,8 @@ Commands that require an API key will auto-run `aipa setup` on first use if not
310
385
  | `aipa get-ohlcv-data` | No setup needed |
311
386
  | `aipa live-data` | No setup needed |
312
387
  | `aipa ticker-list` | No setup needed |
388
+ | `aipa performers` | No setup needed |
389
+ | `aipa volume-profile VCB` | No setup needed |
313
390
  | `aipa analyze VCB --context-only` | No setup needed |
314
391
  | `aipa analyze VCB --questions` | No setup needed |
315
392
  | `aipa resume` | No setup needed |
@@ -324,10 +401,10 @@ Commands that require an API key will auto-run `aipa setup` on first use if not
324
401
  Launch the TUI with `aipa`. The interface has six tabs:
325
402
 
326
403
  - **Chat** — AI-powered chat with streaming responses, thinking/reasoning display, slash commands, and arrow-key history navigation
404
+ - **Workflows** — Structured analysis forms with question bank dropdown for ticker analysis and deep research
327
405
  - **Vietnam** — Browse and search Vietnamese stock tickers
328
406
  - **Crypto** — Browse and search cryptocurrency tickers
329
407
  - **Global** — Browse and search global/Yahoo tickers
330
- - **Workflows** — Structured analysis forms with question bank dropdown for ticker analysis and deep research
331
408
  - **Settings** — Configure API key, model, base URL, and other preferences
332
409
 
333
410
  ### Slash Commands (Chat tab)
@@ -9,7 +9,7 @@ license = "MIT"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
11
11
  dependencies = [
12
- "aipriceaction>=0.1.12",
12
+ "aipriceaction>=0.1.13",
13
13
  "textual>=3.0.0",
14
14
  "textual-autocomplete>=4.0.6",
15
15
  "textual-plotext>=0.4.1",
@@ -1,4 +1,4 @@
1
1
  """AIPriceAction Terminal - TUI chat interface for ticker analysis."""
2
2
 
3
- __version__ = "0.1.26"
3
+ __version__ = "0.1.28"
4
4
 
@@ -238,10 +238,188 @@ def create_live_data_tool(lang: str = "en") -> ToolDef:
238
238
  )
239
239
 
240
240
 
241
+ def create_performers_tool(lang: str = "en") -> ToolDef:
242
+ """Factory: creates the get_performers tool."""
243
+
244
+ @tool
245
+ def get_performers(
246
+ sort_by: str = "close_changed",
247
+ direction: str = "desc",
248
+ limit: int = 10,
249
+ source: str | None = None,
250
+ group: str | None = None,
251
+ ) -> str:
252
+ """Rank top and worst performers from live daily data by a chosen metric.
253
+
254
+ Returns two ranked lists (top and worst) of performers. Useful for
255
+ identifying market leaders, laggards, and sector trends.
256
+
257
+ Args:
258
+ sort_by: Metric to rank by — "close_changed" (default), "volume",
259
+ "value" (close × volume), "volume_changed",
260
+ "ma10_score", "ma20_score", "ma50_score",
261
+ "ma100_score", "ma200_score", "total_money_changed".
262
+ direction: Sort direction — "desc" (default, strongest first in top)
263
+ or "asc" (weakest first in top).
264
+ limit: Number of entries per list (default 10, max 100).
265
+ source: Filter by source — "vn" (default), "crypto", "yahoo". None = vn.
266
+ group: Filter by group/sector — e.g. "NGAN_HANG", "CHUNG_KHOAN",
267
+ "BAT_DONG_SAN", "CONG_NGHE", "DAU_KHI". None = all sectors.
268
+ """
269
+ from aipriceaction.performers import build_performers
270
+
271
+ client, _ = _ensure_clients(lang)
272
+ try:
273
+ data = client.fetch_live_data("1D", ma=True)
274
+ except Exception as e:
275
+ return f"Error fetching live data: {e}"
276
+ if not data:
277
+ return "No live data available."
278
+
279
+ sector_map: dict[str, str] = {}
280
+ if source:
281
+ tickers_meta = client.get_tickers(source=source)
282
+ sector_map = {t.ticker: t.group for t in tickers_meta if t.group}
283
+ source_symbols = {t.ticker for t in tickers_meta}
284
+ data = {k: v for k, v in data.items() if k in source_symbols}
285
+
286
+ # Filter by group/sector if specified
287
+ if group:
288
+ group_upper = group.upper()
289
+ data = {k: v for k, v in data.items() if sector_map.get(k, "").upper() == group_upper}
290
+
291
+ top, worst = build_performers(
292
+ data, sector_map,
293
+ sort_by=sort_by,
294
+ direction=direction,
295
+ limit=limit,
296
+ source=source,
297
+ )
298
+
299
+ lines = [f"Top {len(top)} performers (by {sort_by}, {direction}):"]
300
+ for i, p in enumerate(top, 1):
301
+ chg = f"{p.close_changed:+.2f}%" if p.close_changed is not None else "N/A"
302
+ sector = f" [{p.sector}]" if p.sector else ""
303
+ lines.append(f" {i}. {p.symbol}: close={p.close:.2f} change={chg} vol={p.volume:,} value={p.value:,.0f}{sector}")
304
+
305
+ lines.append(f"\nWorst {len(worst)} performers (by {sort_by}):")
306
+ for i, p in enumerate(worst, 1):
307
+ chg = f"{p.close_changed:+.2f}%" if p.close_changed is not None else "N/A"
308
+ sector = f" [{p.sector}]" if p.sector else ""
309
+ lines.append(f" {i}. {p.symbol}: close={p.close:.2f} change={chg} vol={p.volume:,} value={p.value:,.0f}{sector}")
310
+
311
+ return "\n".join(lines)
312
+
313
+ return ToolDef(
314
+ tool=get_performers,
315
+ name="get_performers",
316
+ description="Rank top and worst performers by price change, volume, or MA scores.",
317
+ category="market_data",
318
+ )
319
+
320
+
321
+ def create_volume_profile_tool(lang: str = "en") -> ToolDef:
322
+ """Factory: creates the get_volume_profile tool."""
323
+
324
+ @tool
325
+ def get_volume_profile(
326
+ ticker: str,
327
+ date: str | None = None,
328
+ start_date: str | None = None,
329
+ end_date: str | None = None,
330
+ bins: int = 50,
331
+ value_area_pct: float = 70.0,
332
+ ) -> str:
333
+ """Compute volume-by-price histogram for a ticker using 1-minute data.
334
+
335
+ Returns the Point of Control (POC), Value Area, volume-weighted statistics,
336
+ and the binned profile. Useful for identifying key support/resistance levels
337
+ based on where the most volume traded.
338
+
339
+ Args:
340
+ ticker: Ticker symbol (e.g. "VCB", "BTCUSDT").
341
+ date: Single date in YYYY-MM-DD format. Defaults to today.
342
+ start_date: Start date (YYYY-MM-DD). Alternative to --date.
343
+ end_date: End date (YYYY-MM-DD). Defaults to start_date.
344
+ bins: Number of price bins (default 50, range 2-200).
345
+ value_area_pct: Value area target percentage (default 70, range 60-90).
346
+ """
347
+ from datetime import date as date_type
348
+
349
+ from aipriceaction.volume_profile import compute_volume_profile
350
+
351
+ client, _ = _ensure_clients(lang)
352
+ ticker = ticker.upper()
353
+
354
+ # Resolve date range
355
+ if date:
356
+ sd = ed = date
357
+ elif start_date:
358
+ sd = start_date
359
+ ed = end_date or start_date
360
+ else:
361
+ today = date_type.today().isoformat()
362
+ sd = ed = today
363
+
364
+ # Resolve source for tick size
365
+ source = "vn"
366
+ try:
367
+ tickers_meta = client.get_tickers()
368
+ for t in tickers_meta:
369
+ if t.ticker == ticker:
370
+ source = t.source or "vn"
371
+ break
372
+ except Exception:
373
+ pass
374
+
375
+ try:
376
+ df = client.get_ohlcv(ticker, interval="1m", start_date=sd, end_date=ed, ma=False)
377
+ except Exception as e:
378
+ return f"Error fetching 1m data for {ticker}: {e}"
379
+
380
+ if df is None or df.empty:
381
+ return f"No 1m data found for {ticker} on {sd}."
382
+
383
+ result = compute_volume_profile(df, ticker, source=source, bins=bins, value_area_pct=value_area_pct)
384
+
385
+ lines = [
386
+ f"Volume Profile: {result.symbol} ({sd})",
387
+ f"Volume: {result.total_volume:,} Minutes: {result.total_minutes}",
388
+ f"Range: {result.price_range.low:.2f} - {result.price_range.high:.2f}",
389
+ f"POC: {result.poc.price:.2f} ({result.poc.percentage:.1f}%)",
390
+ f"Value Area: {result.value_area.low:.2f} - {result.value_area.high:.2f} ({result.value_area.percentage:.1f}%)",
391
+ ]
392
+
393
+ if result.statistics:
394
+ s = result.statistics
395
+ lines.append(f"Mean: {s.mean_price:.2f} Median: {s.median_price:.2f} "
396
+ f"StdDev: {s.std_deviation:.2f} Skew: {s.skewness:.4f}")
397
+
398
+ if result.profile:
399
+ lines.append(f"\nProfile ({len(result.profile)} bins):")
400
+ max_vol = max(p.volume for p in result.profile)
401
+ for level in result.profile:
402
+ bar_len = int(level.volume / max_vol * 25) if max_vol > 0 else 0
403
+ bar = "\u2588" * bar_len
404
+ lines.append(f" {level.price:>10.2f} vol={level.volume:>10,.0f} "
405
+ f"{level.percentage:>5.1f}% {bar}")
406
+
407
+ return "\n".join(lines)
408
+
409
+ return ToolDef(
410
+ tool=get_volume_profile,
411
+ name="get_volume_profile",
412
+ description="Volume-by-price histogram with POC, value area, and statistics.",
413
+ category="market_data",
414
+ )
415
+
416
+
241
417
  def get_default_tools(lang: str = "en") -> ToolRegistry:
242
418
  """Return a ToolRegistry pre-loaded with the built-in market data tools."""
243
419
  registry = ToolRegistry()
244
420
  registry.register(create_ohlcv_tool(lang))
245
421
  registry.register(create_ticker_list_tool(lang))
246
422
  registry.register(create_live_data_tool(lang))
423
+ registry.register(create_performers_tool(lang))
424
+ registry.register(create_volume_profile_tool(lang))
247
425
  return registry
@@ -74,6 +74,30 @@ def run():
74
74
  p_tlist.add_argument("--group", default=None, help="Filter by group (e.g. NGAN_HANG, CHUNG_KHOAN)")
75
75
  p_tlist.add_argument("--compact", action="store_true", help="Output symbols only, comma-separated")
76
76
 
77
+ # aipa performers [--sort-by close_changed] [--direction desc] [--limit 10]
78
+ # [--min-volume 10000] [--source vn] [--group NGAN_HANG]
79
+ p_perf = sub.add_parser("performers", help="Top/worst performers ranked by a chosen metric")
80
+ p_perf.add_argument("--sort-by", default="close_changed",
81
+ choices=["close_changed", "volume", "value", "volume_changed",
82
+ "ma10_score", "ma20_score", "ma50_score", "ma100_score", "ma200_score",
83
+ "total_money_changed"])
84
+ p_perf.add_argument("--direction", default="desc", choices=["desc", "asc"])
85
+ p_perf.add_argument("--limit", type=int, default=10)
86
+ p_perf.add_argument("--min-volume", type=int, default=10000)
87
+ p_perf.add_argument("--source", default="vn", choices=["vn", "crypto", "global", "yahoo", "sjc"])
88
+ p_perf.add_argument("--group", default=None, help="Filter by group/sector (e.g. NGAN_HANG, CHUNG_KHOAN, BAT_DONG_SAN)")
89
+
90
+ # aipa volume-profile TICKER [--date YYYY-MM-DD] [--start-date] [--end-date]
91
+ # [--source vn] [--bins 50] [--value-area-pct 70]
92
+ p_vp = sub.add_parser("volume-profile", help="Volume-by-price histogram analysis")
93
+ p_vp.add_argument("ticker", help="Ticker symbol")
94
+ p_vp.add_argument("--date", default=None, help="Single date (YYYY-MM-DD)")
95
+ p_vp.add_argument("--start-date", default=None, help="Start date (YYYY-MM-DD)")
96
+ p_vp.add_argument("--end-date", default=None, help="End date (YYYY-MM-DD)")
97
+ p_vp.add_argument("--source", default=None, choices=["vn", "crypto", "global", "yahoo", "sjc"])
98
+ p_vp.add_argument("--bins", type=int, default=50, help="Number of price bins (2-200)")
99
+ p_vp.add_argument("--value-area-pct", type=float, default=70.0, help="Value area target percentage (60-90)")
100
+
77
101
  # aipa setup
78
102
  sub.add_parser("setup", help="Interactive first-run setup")
79
103
 
@@ -100,6 +124,12 @@ def run():
100
124
  elif args.command == "ticker-list":
101
125
  from .cli_commands import cmd_ticker_list
102
126
  cmd_ticker_list(args)
127
+ elif args.command == "performers":
128
+ from .cli_commands import cmd_performers
129
+ cmd_performers(args)
130
+ elif args.command == "volume-profile":
131
+ from .cli_commands import cmd_volume_profile
132
+ cmd_volume_profile(args)
103
133
  elif args.command == "deep-research":
104
134
  if getattr(args, "run", False):
105
135
  _ensure_setup()
@@ -396,3 +396,148 @@ def _resolve_cli_question(builder, args) -> str:
396
396
  return templates[0]["question"]
397
397
 
398
398
  return ""
399
+
400
+
401
+ def cmd_performers(args) -> None:
402
+ """CLI handler: aipa performers."""
403
+ from aipriceaction import AIPriceAction
404
+ from aipriceaction.performers import build_performers
405
+
406
+ client = AIPriceAction()
407
+ source = _resolve_source(args.source)
408
+
409
+ # Fetch live 1D data with MA indicators
410
+ data = client.fetch_live_data("1D", ma=True)
411
+ if not data:
412
+ print("No live data available.", file=sys.stderr)
413
+ sys.exit(1)
414
+
415
+ # Build sector map from ticker metadata
416
+ sector_map: dict[str, str] = {}
417
+ if source:
418
+ tickers_meta = client.get_tickers(source=source)
419
+ sector_map = {t.ticker: t.group for t in tickers_meta if t.group}
420
+ # Filter live data to source tickers
421
+ source_symbols = {t.ticker for t in tickers_meta}
422
+ data = {k: v for k, v in data.items() if k in source_symbols}
423
+
424
+ # Filter by group/sector if specified
425
+ if args.group:
426
+ group_upper = args.group.upper()
427
+ data = {k: v for k, v in data.items() if sector_map.get(k, "").upper() == group_upper}
428
+
429
+ top, worst = build_performers(
430
+ data, sector_map,
431
+ sort_by=args.sort_by,
432
+ direction=args.direction,
433
+ limit=args.limit,
434
+ min_volume=args.min_volume,
435
+ source=source,
436
+ )
437
+
438
+ import pandas as pd
439
+
440
+ if top:
441
+ print(f"\n=== Top {len(top)} Performers (by {args.sort_by}, {args.direction}) ===")
442
+ df = pd.DataFrame([_performer_to_dict(p) for p in top])
443
+ print(df.to_string(index=False))
444
+
445
+ if worst:
446
+ print(f"\n=== Worst {len(worst)} Performers (by {args.sort_by}, {args.direction}) ===")
447
+ df = pd.DataFrame([_performer_to_dict(p) for p in worst])
448
+ print(df.to_string(index=False))
449
+
450
+
451
+ def _performer_to_dict(p) -> dict:
452
+ """Convert PerformerInfo to a flat dict for display."""
453
+ return {
454
+ "ticker": p.symbol,
455
+ "close": p.close,
456
+ "volume": p.volume,
457
+ "value": f"{p.value:,.0f}",
458
+ "close_changed": f"{p.close_changed:+.2f}" if p.close_changed is not None else "",
459
+ "volume_changed": f"{p.volume_changed:+.2f}" if p.volume_changed is not None else "",
460
+ "ma10_score": f"{p.ma10_score:.1f}" if p.ma10_score is not None else "",
461
+ "ma50_score": f"{p.ma50_score:.1f}" if p.ma50_score is not None else "",
462
+ "ma200_score": f"{p.ma200_score:.1f}" if p.ma200_score is not None else "",
463
+ "sector": p.sector or "",
464
+ }
465
+
466
+
467
+ def cmd_volume_profile(args) -> None:
468
+ """CLI handler: aipa volume-profile."""
469
+ from datetime import date
470
+
471
+ from aipriceaction import AIPriceAction
472
+ from aipriceaction.volume_profile import compute_volume_profile
473
+
474
+ client = AIPriceAction()
475
+ ticker = args.ticker.upper()
476
+
477
+ # Resolve date range
478
+ if args.date:
479
+ start_date = end_date = args.date
480
+ elif args.start_date:
481
+ start_date = args.start_date
482
+ end_date = args.end_date or args.start_date
483
+ else:
484
+ today = date.today().isoformat()
485
+ start_date = end_date = today
486
+
487
+ # Resolve source for tick size
488
+ source = _resolve_source(args.source) or "vn"
489
+ # Auto-detect from ticker metadata if not specified
490
+ if args.source is None:
491
+ try:
492
+ tickers_meta = client.get_tickers()
493
+ for t in tickers_meta:
494
+ if t.ticker == ticker:
495
+ source = t.source or "vn"
496
+ break
497
+ except Exception:
498
+ pass
499
+
500
+ # Fetch 1m data
501
+ df = client.get_ohlcv(
502
+ ticker,
503
+ interval="1m",
504
+ start_date=start_date,
505
+ end_date=end_date,
506
+ ma=False,
507
+ )
508
+
509
+ if df is None or df.empty:
510
+ print(f"No 1m data found for {ticker} on {start_date}.", file=sys.stderr)
511
+ sys.exit(1)
512
+
513
+ result = compute_volume_profile(
514
+ df, ticker,
515
+ source=source,
516
+ bins=args.bins,
517
+ value_area_pct=args.value_area_pct,
518
+ )
519
+
520
+ print(f"\n=== Volume Profile: {result.symbol} ({start_date}) ===")
521
+ print(f"Total Volume: {result.total_volume:,} | Minutes: {result.total_minutes}")
522
+ print(f"Price Range: {result.price_range.low:.2f} - {result.price_range.high:.2f} "
523
+ f"(spread: {result.price_range.spread:.2f})")
524
+ print(f"\nPOC: {result.poc.price:.2f} volume={result.poc.volume:,.0f} "
525
+ f"({result.poc.percentage:.1f}%)")
526
+ print(f"Value Area: {result.value_area.low:.2f} - {result.value_area.high:.2f} "
527
+ f"volume={result.value_area.volume:,.0f} ({result.value_area.percentage:.1f}%)")
528
+
529
+ if result.statistics:
530
+ s = result.statistics
531
+ print("\nStatistics:")
532
+ print(f" Mean: {s.mean_price:.2f} Median: {s.median_price:.2f} "
533
+ f"StdDev: {s.std_deviation:.2f} Skewness: {s.skewness:.4f}")
534
+
535
+ if result.profile:
536
+ print(f"\n{'Price':>12} {'Volume':>12} {'%':>6} {'Cum%':>6} Bar")
537
+ print("-" * 60)
538
+ max_vol = max(p.volume for p in result.profile) if result.profile else 1.0
539
+ for level in result.profile:
540
+ bar_len = int(level.volume / max_vol * 30) if max_vol > 0 else 0
541
+ bar = "\u2588" * bar_len
542
+ print(f"{level.price:>12.2f} {level.volume:>12,.0f} {level.percentage:>5.1f}% "
543
+ f"{level.cumulative_percentage:>5.1f}% {bar}")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes