hypertrader 0.2.0__tar.gz → 0.2.1__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 (49) hide show
  1. {hypertrader-0.2.0 → hypertrader-0.2.1}/PKG-INFO +22 -1
  2. {hypertrader-0.2.0 → hypertrader-0.2.1}/README.md +21 -0
  3. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/PKG-INFO +22 -1
  4. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/SOURCES.txt +4 -0
  5. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.py +14 -0
  6. {hypertrader-0.2.0 → hypertrader-0.2.1}/modes/auto_trader.py +152 -39
  7. {hypertrader-0.2.0 → hypertrader-0.2.1}/modes/position_management.py +85 -24
  8. {hypertrader-0.2.0 → hypertrader-0.2.1}/setup.py +1 -1
  9. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/base.py +31 -1
  10. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/fib_reversal.py +6 -0
  11. hypertrader-0.2.1/tests/test_session_recovery.py +224 -0
  12. hypertrader-0.2.1/tests/test_trade_journal.py +202 -0
  13. hypertrader-0.2.1/utils/session_state.py +51 -0
  14. hypertrader-0.2.1/utils/trade_journal.py +186 -0
  15. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/dependency_links.txt +0 -0
  16. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/entry_points.txt +0 -0
  17. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/requires.txt +0 -0
  18. {hypertrader-0.2.0 → hypertrader-0.2.1}/hypertrader.egg-info/top_level.txt +0 -0
  19. {hypertrader-0.2.0 → hypertrader-0.2.1}/modes/market_maker.py +0 -0
  20. {hypertrader-0.2.0 → hypertrader-0.2.1}/modes/position_watcher.py +0 -0
  21. {hypertrader-0.2.0 → hypertrader-0.2.1}/modes/trailing_stop.py +0 -0
  22. {hypertrader-0.2.0 → hypertrader-0.2.1}/setup.cfg +0 -0
  23. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/__init__.py +0 -0
  24. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/default.py +0 -0
  25. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/orderflow_metrics.py +0 -0
  26. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/orderflow_models.py +0 -0
  27. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/orderflow_pullback.py +0 -0
  28. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/registry.py +0 -0
  29. {hypertrader-0.2.0 → hypertrader-0.2.1}/strategies/reversal.py +0 -0
  30. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_api_retry.py +0 -0
  31. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_async_hyperliquid.py +0 -0
  32. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_auto_dry_run.py +0 -0
  33. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_auto_strategy_registry.py +0 -0
  34. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_auto_trader_logging.py +0 -0
  35. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_cli_shutdown.py +0 -0
  36. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_fib_reversal_strategy.py +0 -0
  37. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_market_ids.py +0 -0
  38. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_orderflow_metrics.py +0 -0
  39. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_orderflow_pullback_lifecycle.py +0 -0
  40. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_reversal_strategy.py +0 -0
  41. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_simulated_logging.py +0 -0
  42. {hypertrader-0.2.0 → hypertrader-0.2.1}/tests/test_trailing_stop.py +0 -0
  43. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/constants.py +0 -0
  44. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/helpers.py +0 -0
  45. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/retry.py +0 -0
  46. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/simulated_logging.py +0 -0
  47. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/style.py +0 -0
  48. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/timer.py +0 -0
  49. {hypertrader-0.2.0 → hypertrader-0.2.1}/utils/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -324,6 +324,7 @@ Key options:
324
324
  - `--scalp`
325
325
  - `--use-live-candle`
326
326
  - `--take-profit-pct`: override Bollinger-derived TP
327
+ - `--take-profit-scale`: multiply target distances from entry (default `1.0`; `0.5` halves them)
327
328
  - `--min-take-profit-pct`
328
329
  - `--max-take-profit-pct`
329
330
  - `--stop-loss-pct`
@@ -364,6 +365,8 @@ Key options:
364
365
 
365
366
  `auto` derives TP from Bollinger Bands unless `--take-profit-pct` overrides it. If TP is set and SL is omitted, SL defaults to half TP. Executions route through the same bracket-entry path used by `enter`.
366
367
 
368
+ `--take-profit-scale 0.5` moves each planned target halfway toward entry, for both longs and shorts: `new_target = entry + 0.5 * (target - entry)`. For example, an entry of 100 with targets 110, 120, and 130 becomes 105, 110, and 115. Scaling applies after strategy signal selection and before exchange price rounding, including Fibonacci targets and dry runs. Stops and allocation percentages stay the same. The multiplier must be finite and positive; `1.0` preserves the original distances.
369
+
367
370
  Fibonacci pullback strategy:
368
371
 
369
372
  ```bash
@@ -567,6 +570,24 @@ The websocket cache is used as the fast path for market data such as mids, best
567
570
 
568
571
  ## Logging
569
572
 
573
+ Each new `auto` session (live or dry-run) creates a unique folder under `logs/sessions/` and prints its location. Restarting an unfinished session with the same account and trading settings automatically resumes that folder and appends to its journal. Use `--session-log-dir PATH` to choose another parent directory. Each folder contains:
574
+
575
+ - `session.json`: the original CLI arguments, parsed CLI settings including defaults, and the arguments passed to the auto trader. Environment variables and exchange clients are not serialized.
576
+ - `closed_trades.csv`: one row per completed position cycle, with UTC start/close times, direction, strategy, requested and filled sizes, size-weighted entry/exit prices, PnL, fees, and links to trade details. Rows are saved as trades complete; cancelled or failed tasks are not counted as closed trades.
577
+ - A JSON file per trade with the signal snapshot and raw exchange fills. Missing fill/PnL data produces blank CSV values with an explanation in `pnl_status`. Live PnL subtracts fill fees but excludes funding; simulated PnL excludes fees, funding, and slippage. Start/close times describe the managed cycle, including entry attempts and closure detection.
578
+
579
+ Atomic `.active-*.json` checkpoints in the parent directory retain session PnL and peaks, completed-trade counts, per-coin streaks and cooldown deadlines, pending trades, and Fibonacci impulses already traded. Live monitors restore saved hidden targets, order IDs, and trailing-stop state; interrupted entry attempts reconcile the existing position without submitting a new entry. Simulations restore entry price, remaining size, partial profits, and the next target. Completed results awaiting accounting are replayed without duplicating CSV rows.
580
+
581
+ Keep the same session directory and trading settings when restarting after an error. Accounts, networks, strategies, settings, and live/dry-run modes have separate checkpoints; simultaneous processes cannot share one checkpoint. `--new-session` deliberately resets recovery state and starts a new journal. A session that exits normally after its configured trade or risk limit is complete, so the next invocation starts a new one. Recovery begins with sessions created by this version; older logs alone cannot restore open-trade state. Local stops and simulated fills only run while the bot is running, and resume from the last saved state using current market prices.
582
+
583
+ Add `--trade-charts` to save an SVG price chart for each completed trade:
584
+
585
+ ```bash
586
+ python3 hypertrader.py auto SOL --size 10 --dry-run --trade-charts
587
+ ```
588
+
589
+ Charts plot candle prices across the trade with size-weighted entry/exit markers. The candle interval adapts to the trade duration, and the latest candle may be incomplete. Charts require no extra plotting packages. The CSV is written before chart generation; the per-trade JSON records chart success, failure, or interruption. `chart_file` is the intended relative path and may not exist if chart generation fails. Charting is off by default and adds no candle requests when disabled.
590
+
570
591
  Dry-run console output, runtime messages, risk events, and simulated trades are written to `logs/simulated.log`. Simulation fills at observed market mid prices. Plans with explicit take-profit allocations, including `fib_reversal`, close the allocated portion of the original size at each reached target (40% at TP1, 35% at TP2, and 25% at TP3 for Fibonacci). Each fill logs a `simulated_take_profit_filled` event with its size and PnL. Position records track remaining size, cumulative realized PnL, and unrealized PnL on the remaining position; a stop closes only that remainder. Completed-trade PnL includes all partial exits. Plans without explicit allocations exit the full position at the stop or final target. Fees, funding, slippage, and dynamic stop adjustments are not modeled. Open positions at shutdown are logged with realized partial profits and their remaining unrealized PnL.
571
592
 
572
593
  Runtime logs for live runs are written to [`logs/hypertrader.log`](/media/anon/development/anon/PycharmProjects/hypertrader/logs/hypertrader.log).
@@ -293,6 +293,7 @@ Key options:
293
293
  - `--scalp`
294
294
  - `--use-live-candle`
295
295
  - `--take-profit-pct`: override Bollinger-derived TP
296
+ - `--take-profit-scale`: multiply target distances from entry (default `1.0`; `0.5` halves them)
296
297
  - `--min-take-profit-pct`
297
298
  - `--max-take-profit-pct`
298
299
  - `--stop-loss-pct`
@@ -333,6 +334,8 @@ Key options:
333
334
 
334
335
  `auto` derives TP from Bollinger Bands unless `--take-profit-pct` overrides it. If TP is set and SL is omitted, SL defaults to half TP. Executions route through the same bracket-entry path used by `enter`.
335
336
 
337
+ `--take-profit-scale 0.5` moves each planned target halfway toward entry, for both longs and shorts: `new_target = entry + 0.5 * (target - entry)`. For example, an entry of 100 with targets 110, 120, and 130 becomes 105, 110, and 115. Scaling applies after strategy signal selection and before exchange price rounding, including Fibonacci targets and dry runs. Stops and allocation percentages stay the same. The multiplier must be finite and positive; `1.0` preserves the original distances.
338
+
336
339
  Fibonacci pullback strategy:
337
340
 
338
341
  ```bash
@@ -536,6 +539,24 @@ The websocket cache is used as the fast path for market data such as mids, best
536
539
 
537
540
  ## Logging
538
541
 
542
+ Each new `auto` session (live or dry-run) creates a unique folder under `logs/sessions/` and prints its location. Restarting an unfinished session with the same account and trading settings automatically resumes that folder and appends to its journal. Use `--session-log-dir PATH` to choose another parent directory. Each folder contains:
543
+
544
+ - `session.json`: the original CLI arguments, parsed CLI settings including defaults, and the arguments passed to the auto trader. Environment variables and exchange clients are not serialized.
545
+ - `closed_trades.csv`: one row per completed position cycle, with UTC start/close times, direction, strategy, requested and filled sizes, size-weighted entry/exit prices, PnL, fees, and links to trade details. Rows are saved as trades complete; cancelled or failed tasks are not counted as closed trades.
546
+ - A JSON file per trade with the signal snapshot and raw exchange fills. Missing fill/PnL data produces blank CSV values with an explanation in `pnl_status`. Live PnL subtracts fill fees but excludes funding; simulated PnL excludes fees, funding, and slippage. Start/close times describe the managed cycle, including entry attempts and closure detection.
547
+
548
+ Atomic `.active-*.json` checkpoints in the parent directory retain session PnL and peaks, completed-trade counts, per-coin streaks and cooldown deadlines, pending trades, and Fibonacci impulses already traded. Live monitors restore saved hidden targets, order IDs, and trailing-stop state; interrupted entry attempts reconcile the existing position without submitting a new entry. Simulations restore entry price, remaining size, partial profits, and the next target. Completed results awaiting accounting are replayed without duplicating CSV rows.
549
+
550
+ Keep the same session directory and trading settings when restarting after an error. Accounts, networks, strategies, settings, and live/dry-run modes have separate checkpoints; simultaneous processes cannot share one checkpoint. `--new-session` deliberately resets recovery state and starts a new journal. A session that exits normally after its configured trade or risk limit is complete, so the next invocation starts a new one. Recovery begins with sessions created by this version; older logs alone cannot restore open-trade state. Local stops and simulated fills only run while the bot is running, and resume from the last saved state using current market prices.
551
+
552
+ Add `--trade-charts` to save an SVG price chart for each completed trade:
553
+
554
+ ```bash
555
+ python3 hypertrader.py auto SOL --size 10 --dry-run --trade-charts
556
+ ```
557
+
558
+ Charts plot candle prices across the trade with size-weighted entry/exit markers. The candle interval adapts to the trade duration, and the latest candle may be incomplete. Charts require no extra plotting packages. The CSV is written before chart generation; the per-trade JSON records chart success, failure, or interruption. `chart_file` is the intended relative path and may not exist if chart generation fails. Charting is off by default and adds no candle requests when disabled.
559
+
539
560
  Dry-run console output, runtime messages, risk events, and simulated trades are written to `logs/simulated.log`. Simulation fills at observed market mid prices. Plans with explicit take-profit allocations, including `fib_reversal`, close the allocated portion of the original size at each reached target (40% at TP1, 35% at TP2, and 25% at TP3 for Fibonacci). Each fill logs a `simulated_take_profit_filled` event with its size and PnL. Position records track remaining size, cumulative realized PnL, and unrealized PnL on the remaining position; a stop closes only that remainder. Completed-trade PnL includes all partial exits. Plans without explicit allocations exit the full position at the stop or final target. Fees, funding, slippage, and dynamic stop adjustments are not modeled. Open positions at shutdown are logged with realized partial profits and their remaining unrealized PnL.
540
561
 
541
562
  Runtime logs for live runs are written to [`logs/hypertrader.log`](/media/anon/development/anon/PycharmProjects/hypertrader/logs/hypertrader.log).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -324,6 +324,7 @@ Key options:
324
324
  - `--scalp`
325
325
  - `--use-live-candle`
326
326
  - `--take-profit-pct`: override Bollinger-derived TP
327
+ - `--take-profit-scale`: multiply target distances from entry (default `1.0`; `0.5` halves them)
327
328
  - `--min-take-profit-pct`
328
329
  - `--max-take-profit-pct`
329
330
  - `--stop-loss-pct`
@@ -364,6 +365,8 @@ Key options:
364
365
 
365
366
  `auto` derives TP from Bollinger Bands unless `--take-profit-pct` overrides it. If TP is set and SL is omitted, SL defaults to half TP. Executions route through the same bracket-entry path used by `enter`.
366
367
 
368
+ `--take-profit-scale 0.5` moves each planned target halfway toward entry, for both longs and shorts: `new_target = entry + 0.5 * (target - entry)`. For example, an entry of 100 with targets 110, 120, and 130 becomes 105, 110, and 115. Scaling applies after strategy signal selection and before exchange price rounding, including Fibonacci targets and dry runs. Stops and allocation percentages stay the same. The multiplier must be finite and positive; `1.0` preserves the original distances.
369
+
367
370
  Fibonacci pullback strategy:
368
371
 
369
372
  ```bash
@@ -567,6 +570,24 @@ The websocket cache is used as the fast path for market data such as mids, best
567
570
 
568
571
  ## Logging
569
572
 
573
+ Each new `auto` session (live or dry-run) creates a unique folder under `logs/sessions/` and prints its location. Restarting an unfinished session with the same account and trading settings automatically resumes that folder and appends to its journal. Use `--session-log-dir PATH` to choose another parent directory. Each folder contains:
574
+
575
+ - `session.json`: the original CLI arguments, parsed CLI settings including defaults, and the arguments passed to the auto trader. Environment variables and exchange clients are not serialized.
576
+ - `closed_trades.csv`: one row per completed position cycle, with UTC start/close times, direction, strategy, requested and filled sizes, size-weighted entry/exit prices, PnL, fees, and links to trade details. Rows are saved as trades complete; cancelled or failed tasks are not counted as closed trades.
577
+ - A JSON file per trade with the signal snapshot and raw exchange fills. Missing fill/PnL data produces blank CSV values with an explanation in `pnl_status`. Live PnL subtracts fill fees but excludes funding; simulated PnL excludes fees, funding, and slippage. Start/close times describe the managed cycle, including entry attempts and closure detection.
578
+
579
+ Atomic `.active-*.json` checkpoints in the parent directory retain session PnL and peaks, completed-trade counts, per-coin streaks and cooldown deadlines, pending trades, and Fibonacci impulses already traded. Live monitors restore saved hidden targets, order IDs, and trailing-stop state; interrupted entry attempts reconcile the existing position without submitting a new entry. Simulations restore entry price, remaining size, partial profits, and the next target. Completed results awaiting accounting are replayed without duplicating CSV rows.
580
+
581
+ Keep the same session directory and trading settings when restarting after an error. Accounts, networks, strategies, settings, and live/dry-run modes have separate checkpoints; simultaneous processes cannot share one checkpoint. `--new-session` deliberately resets recovery state and starts a new journal. A session that exits normally after its configured trade or risk limit is complete, so the next invocation starts a new one. Recovery begins with sessions created by this version; older logs alone cannot restore open-trade state. Local stops and simulated fills only run while the bot is running, and resume from the last saved state using current market prices.
582
+
583
+ Add `--trade-charts` to save an SVG price chart for each completed trade:
584
+
585
+ ```bash
586
+ python3 hypertrader.py auto SOL --size 10 --dry-run --trade-charts
587
+ ```
588
+
589
+ Charts plot candle prices across the trade with size-weighted entry/exit markers. The candle interval adapts to the trade duration, and the latest candle may be incomplete. Charts require no extra plotting packages. The CSV is written before chart generation; the per-trade JSON records chart success, failure, or interruption. `chart_file` is the intended relative path and may not exist if chart generation fails. Charting is off by default and adds no candle requests when disabled.
590
+
570
591
  Dry-run console output, runtime messages, risk events, and simulated trades are written to `logs/simulated.log`. Simulation fills at observed market mid prices. Plans with explicit take-profit allocations, including `fib_reversal`, close the allocated portion of the original size at each reached target (40% at TP1, 35% at TP2, and 25% at TP3 for Fibonacci). Each fill logs a `simulated_take_profit_filled` event with its size and PnL. Position records track remaining size, cumulative realized PnL, and unrealized PnL on the remaining position; a stop closes only that remainder. Completed-trade PnL includes all partial exits. Plans without explicit allocations exit the full position at the stop or final target. Fees, funding, slippage, and dynamic stop adjustments are not modeled. Open positions at shutdown are logged with realized partial profits and their remaining unrealized PnL.
571
592
 
572
593
  Runtime logs for live runs are written to [`logs/hypertrader.log`](/media/anon/development/anon/PycharmProjects/hypertrader/logs/hypertrader.log).
@@ -32,12 +32,16 @@ tests/test_market_ids.py
32
32
  tests/test_orderflow_metrics.py
33
33
  tests/test_orderflow_pullback_lifecycle.py
34
34
  tests/test_reversal_strategy.py
35
+ tests/test_session_recovery.py
35
36
  tests/test_simulated_logging.py
37
+ tests/test_trade_journal.py
36
38
  tests/test_trailing_stop.py
37
39
  utils/constants.py
38
40
  utils/helpers.py
39
41
  utils/retry.py
42
+ utils/session_state.py
40
43
  utils/simulated_logging.py
41
44
  utils/style.py
42
45
  utils/timer.py
46
+ utils/trade_journal.py
43
47
  utils/worker.py
@@ -300,6 +300,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
300
300
  help="Use the latest candle even if it may still be forming. Default uses last closed candle.")
301
301
  auto_parser.add_argument("--take-profit-pct", type=float, default=None,
302
302
  help="Override Bollinger-derived TP fraction, e.g. 0.01 = 1pct.")
303
+ auto_parser.add_argument("--take-profit-scale", type=float, default=1.0,
304
+ help="Multiply each target's distance from entry; 0.5 halves it. Default: 1.")
303
305
  auto_parser.add_argument("--min-take-profit-pct", type=float, default=0.002,
304
306
  help="Minimum Bollinger-derived TP fraction. Default: 0.002.")
305
307
  auto_parser.add_argument("--max-take-profit-pct", type=float, default=0.03,
@@ -337,6 +339,12 @@ def build_arg_parser() -> argparse.ArgumentParser:
337
339
  auto_parser.add_argument("--keep-existing-tpsl", action="store_true",
338
340
  help="Do not cancel existing reduce-only TP/SL orders before entry.")
339
341
  auto_parser.add_argument("--dry-run", action="store_true", help="Simulate positions using 10,000 cUSDC assumed collateral and log to logs/simulated.log.")
342
+ auto_parser.add_argument("--session-log-dir", default="logs/sessions",
343
+ help="Parent directory for per-run settings and closed-trade CSV files. Default: logs/sessions.")
344
+ auto_parser.add_argument("--new-session", action="store_true",
345
+ help="Start fresh instead of resuming the matching unfinished auto session.")
346
+ auto_parser.add_argument("--trade-charts", action="store_true",
347
+ help="Save an SVG price chart when each auto position closes.")
340
348
  auto_parser.add_argument("--max-trades", type=int, default=0,
341
349
  help="Maximum completed auto trades before exit. 0 means unlimited. Default: 0.")
342
350
  auto_parser.add_argument("--cooldown-after-trade", type=float, default=60.0,
@@ -877,6 +885,12 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
877
885
  use_last_closed_candle=(not args.use_live_candle),
878
886
  use_sar_stop_on_shortest_interval=args.auto_sar_stop_on_shortest_interval,
879
887
  dry_run=args.dry_run,
888
+ session_log_dir=args.session_log_dir,
889
+ new_session=args.new_session,
890
+ take_profit_scale=args.take_profit_scale,
891
+ trade_charts=args.trade_charts,
892
+ session_argv=list(sys.argv[1:] if argv is None else argv),
893
+ session_cli_settings=vars(args).copy(),
880
894
  max_trades=args.max_trades,
881
895
  cooldown_after_trade=args.cooldown_after_trade,
882
896
  loop_after_trade=args.loop_after_trade,
@@ -6,7 +6,7 @@ import math
6
6
  import os
7
7
  import sys
8
8
  import time
9
- from dataclasses import dataclass, field
9
+ from dataclasses import asdict, dataclass, field
10
10
  from types import SimpleNamespace
11
11
  from typing import Optional, List, Dict, Any, Tuple
12
12
 
@@ -14,11 +14,13 @@ from hyperliquid.exchange import Exchange
14
14
  from hyperliquid.info import Info
15
15
 
16
16
  from modes.position_management import run_bracket_entry
17
- from strategies.base import AutoStrategy, StrategyContext, StrategySignal
17
+ from strategies.base import AutoStrategy, StrategyContext, StrategySignal, StrategyExecutionPlan, scale_take_profit_signal
18
18
  from strategies.registry import create_strategy
19
19
  from strategies.reversal import normalize_signal_prices
20
20
  from utils.constants import AUTO_TRADES_LOG_FILE, SIMULATED_LOG_FILE, INTERVAL_TO_MS, PRICE_EPS, cp
21
21
  from utils.retry import retry_api_read
22
+ from utils.trade_journal import TradeJournal
23
+ from utils.session_state import SessionState
22
24
  from utils.helpers import parse_interval_list, init_clients, _try_float, parse_fractional_pct, \
23
25
  extract_account_balance_from_user_state, round_size_for_hyperliquid, get_user_state_with_retry, get_all_mids, \
24
26
  fetch_recent_candles, compute_position_unrealized_pnl, close_clients, compute_default_stop_loss_pct, \
@@ -248,7 +250,7 @@ class AutoManagedTradeResult:
248
250
  simulated_pnl: Optional[float] = None
249
251
 
250
252
 
251
- async def run_simulated_trade(info, plan, auto_logger, poll_interval):
253
+ async def run_simulated_trade(info, plan, auto_logger, poll_interval, *, state=None, checkpoint=None):
252
254
  """Paper fill at observed mids, honoring explicit TP allocations without orders.
253
255
 
254
256
  PnL is gross quote-currency PnL: fees, funding, and slippage are not modeled.
@@ -271,12 +273,21 @@ async def run_simulated_trade(info, plan, auto_logger, poll_interval):
271
273
  targets = list(enumerate(zip(plan.take_profit_prices, allocations), start=1))
272
274
  else:
273
275
  targets = [(len(plan.take_profit_prices), (plan.take_profit_prices[-1], 100.0))]
274
- started_ms = now_ms()
275
- entry = None
276
- last_price = None
277
- remaining_size = plan.size
278
- realized_pnl = 0.0
279
- next_target = 0
276
+ state = state if state is not None else {}
277
+ if "result" in state:
278
+ return AutoManagedTradeResult(**state["result"])
279
+ started_ms = state.get("started_ms", now_ms())
280
+ entry = state.get("entry")
281
+ last_price = state.get("last_price")
282
+ remaining_size = state.get("remaining_size", plan.size)
283
+ realized_pnl = state.get("realized_pnl", 0.0)
284
+ next_target = state.get("next_target", 0)
285
+
286
+ def save():
287
+ state.update(started_ms=started_ms, entry=entry, last_price=last_price,
288
+ remaining_size=remaining_size, realized_pnl=realized_pnl, next_target=next_target)
289
+ if checkpoint is not None:
290
+ checkpoint()
280
291
 
281
292
  def record(event, **payload):
282
293
  log_auto_trade_event(
@@ -324,14 +335,23 @@ async def run_simulated_trade(info, plan, auto_logger, poll_interval):
324
335
  closed_ms = now_ms()
325
336
  record("simulated_position_exited", exit_price=price,
326
337
  reason="stop_loss" if stop_hit else "take_profit", closed_ms=closed_ms)
327
- return AutoManagedTradeResult(
338
+ result = AutoManagedTradeResult(
328
339
  coin=plan.coin, direction=plan.direction, size=plan.size,
329
340
  take_profit_pct=abs(plan.take_profit_prices[-1] - entry) / entry,
330
341
  stop_loss_pct=abs(plan.stop_price - entry) / entry,
331
342
  started_ms=started_ms, closed_ms=closed_ms, simulated_pnl=realized_pnl,
332
343
  log_context={"simulated": True, "entry_price": entry,
333
- "exit_price": price, "realized_pnl": realized_pnl},
344
+ "exit_price": price, "realized_pnl": realized_pnl,
345
+ "average_exit_price": entry + direction * realized_pnl / plan.size,
346
+ "planned_entry_price": plan.expected_entry,
347
+ "stop_price": plan.stop_price,
348
+ "take_profit_prices": plan.take_profit_prices,
349
+ "exit_reason": "stop_loss" if stop_hit else "take_profit"},
334
350
  )
351
+ state["result"] = asdict(result)
352
+ save()
353
+ return result
354
+ save()
335
355
  record("simulated_position_mark", mark_price=price,
336
356
  unrealized_pnl=direction * (price - entry) * remaining_size)
337
357
  await asyncio.sleep(poll_interval)
@@ -763,7 +783,8 @@ async def fetch_trade_cycle_net_pnl(
763
783
  if str(fill.get("coin", "")).upper() != coin_upper:
764
784
  continue
765
785
  fill_time_ms = int(_try_float(fill.get("time")) or _try_float(fill.get("timestamp")) or 0.0)
766
- if fill_time_ms < request_start_ms or fill_time_ms > request_end_ms:
786
+ # The request has a buffer, but adjacent trade cycles must not share fills.
787
+ if fill_time_ms < start_time_ms or fill_time_ms > end_time_ms:
767
788
  continue
768
789
  closed_pnl = _try_float(fill.get("closedPnl"))
769
790
  if closed_pnl is None:
@@ -2277,10 +2298,22 @@ async def run_auto_trader(
2277
2298
  account_address: Optional[str] = None,
2278
2299
  info: Optional[Info] = None,
2279
2300
  exchange: Optional[Exchange] = None,
2301
+ session_log_dir: str = "logs/sessions",
2302
+ trade_charts: bool = False,
2303
+ session_argv: Optional[List[str]] = None,
2304
+ session_cli_settings: Optional[Dict[str, Any]] = None,
2305
+ take_profit_scale: float = 1.0,
2306
+ new_session: bool = False,
2280
2307
  ) -> None:
2281
2308
  """Automatically scan TA-Lib signals, enter positions, and hand off to bracket management."""
2282
2309
 
2310
+ session_settings = {key: value for key, value in locals().items()
2311
+ if key not in {"info", "exchange", "account_address", "session_argv", "session_cli_settings"}}
2312
+ if session_cli_settings is not None:
2313
+ session_settings["cli_settings"] = dict(session_cli_settings)
2283
2314
  require_talib_available()
2315
+ if not math.isfinite(take_profit_scale) or take_profit_scale <= 0:
2316
+ raise RuntimeError("--take-profit-scale must be finite and > 0.")
2284
2317
  coin = normalize_hyperliquid_market_id(coin) if coin is not None else None
2285
2318
  intervals = parse_interval_list(intervals_value)
2286
2319
  if (size is None) == (size_pct is None):
@@ -2493,13 +2526,34 @@ async def run_auto_trader(
2493
2526
  try:
2494
2527
  if owns_clients:
2495
2528
  account_address, info, exchange = await init_clients(use_testnet, use_websocket=use_websocket)
2529
+ session_store = SessionState(session_log_dir, session_settings, account_address, new_session=new_session)
2530
+ saved = session_store.data
2531
+ if isinstance(strategy, AutoStrategy):
2532
+ strategy.restore_session_state(saved.get("strategy_state", {}))
2533
+ journal = TradeJournal(session_log_dir, session_settings, argv=session_argv, charts=trade_charts,
2534
+ resume_directory=saved.get("journal_directory"))
2535
+ print(f"[AUTO] {'Resumed' if saved else 'New'} session journal: {journal.directory}")
2496
2536
  await strategy.start(info=info, account_address=account_address)
2497
- metrics_start_time_ms = int(time.time() * 1000)
2498
- risk_session = AutoRiskSessionState(started_ms=metrics_start_time_ms)
2499
- coin_sessions: Dict[str, CoinTradeSessionState] = {}
2537
+ risk_session = AutoRiskSessionState(**saved.get("risk_session", {"started_ms": now_ms()}))
2538
+ metrics_start_time_ms = risk_session.started_ms
2539
+ coin_sessions = {key: CoinTradeSessionState(**value) for key, value in saved.get("coin_sessions", {}).items()}
2540
+ completed_trades = saved.get("completed_trades", 0)
2541
+ pending_trades = saved.get("pending_trades", {})
2500
2542
  active_trade_tasks: Dict[str, asyncio.Task[AutoManagedTradeResult]] = {}
2501
- stop_requested = False
2502
- stop_reason = ""
2543
+ shutting_down = False
2544
+ stop_requested = risk_session.stopped or (0 < max_trades <= completed_trades) or (completed_trades > 0 and not loop_after_trade)
2545
+ stop_reason = risk_session.stop_reason or "saved session trade limit reached"
2546
+
2547
+ def checkpoint():
2548
+ session_store.data.update(
2549
+ journal_directory=str(journal.directory), risk_session=asdict(risk_session),
2550
+ coin_sessions={key: asdict(value) for key, value in coin_sessions.items()},
2551
+ completed_trades=completed_trades, pending_trades=pending_trades,
2552
+ strategy_state=strategy.session_state() if isinstance(strategy, AutoStrategy) else {},
2553
+ )
2554
+ session_store.save()
2555
+
2556
+ checkpoint()
2503
2557
  print("============================================================")
2504
2558
  print(" Hyperliquid Async Auto Trader")
2505
2559
  print("============================================================")
@@ -2559,8 +2613,13 @@ async def run_auto_trader(
2559
2613
  managed_market_fallback: Optional[bool] = None,
2560
2614
  managed_tp_prices: Optional[tuple[float, ...]] = None,
2561
2615
  managed_tp_allocations: Optional[tuple[float, ...]] = None,
2616
+ recovery_state=None,
2562
2617
  ) -> AutoManagedTradeResult:
2563
- trade_cycle_started_ms = now_ms()
2618
+ recovery_state = recovery_state if recovery_state is not None else pending_trades[managed_coin]["state"]
2619
+ if "result" in recovery_state:
2620
+ return AutoManagedTradeResult(**recovery_state["result"])
2621
+ trade_cycle_started_ms = recovery_state.setdefault("started_ms", now_ms())
2622
+ checkpoint()
2564
2623
  await run_bracket_entry(
2565
2624
  coin=managed_coin,
2566
2625
  direction=managed_direction,
@@ -2600,8 +2659,10 @@ async def run_auto_trader(
2600
2659
  account_address=account_address,
2601
2660
  info=info,
2602
2661
  exchange=exchange,
2662
+ recovery_state=recovery_state,
2663
+ checkpoint=checkpoint,
2603
2664
  )
2604
- return AutoManagedTradeResult(
2665
+ result = AutoManagedTradeResult(
2605
2666
  coin=managed_coin,
2606
2667
  direction=managed_direction,
2607
2668
  size=managed_size,
@@ -2611,23 +2672,52 @@ async def run_auto_trader(
2611
2672
  closed_ms=now_ms(),
2612
2673
  log_context=dict(managed_log_context or {}),
2613
2674
  )
2675
+ recovery_state["result"] = asdict(result)
2676
+ checkpoint()
2677
+ return result
2678
+
2679
+ def start_live_trade(**kwargs):
2680
+ trade_coin = kwargs["managed_coin"]
2681
+ # Store JSON data before the task can submit any orders.
2682
+ kwargs = dict(kwargs)
2683
+ snapshot = kwargs.pop("managed_shortest_snapshot")
2684
+ pending_trades[trade_coin] = {"kwargs": kwargs, "shortest_interval": getattr(snapshot, "interval", None), "state": {}}
2685
+ checkpoint()
2686
+ active_trade_tasks[trade_coin] = asyncio.create_task(
2687
+ _run_managed_auto_trade(**kwargs, managed_shortest_snapshot=snapshot))
2688
+
2689
+ for saved_coin, pending in pending_trades.items():
2690
+ if dry_run:
2691
+ active_trade_tasks[saved_coin] = asyncio.create_task(run_simulated_trade(
2692
+ info, StrategyExecutionPlan(**pending["plan"]), auto_trades_logger, poll_interval,
2693
+ state=pending["state"], checkpoint=checkpoint))
2694
+ else:
2695
+ pending["state"]["resuming"] = True
2696
+ active_trade_tasks[saved_coin] = asyncio.create_task(_run_managed_auto_trade(
2697
+ **pending["kwargs"], recovery_state=pending["state"],
2698
+ managed_shortest_snapshot=(SimpleNamespace(interval=pending["shortest_interval"])
2699
+ if pending["shortest_interval"] else None)))
2614
2700
 
2615
2701
  async def _drain_completed_trade_tasks() -> None:
2616
2702
  nonlocal completed_trades, stop_requested, stop_reason
2617
2703
  finished_coins = [task_coin for task_coin, task in active_trade_tasks.items() if task.done()]
2618
2704
  for finished_coin in finished_coins:
2619
- task = active_trade_tasks.pop(finished_coin)
2705
+ task = active_trade_tasks[finished_coin]
2620
2706
  try:
2621
2707
  trade_result = task.result()
2622
2708
  except asyncio.CancelledError:
2709
+ active_trade_tasks.pop(finished_coin)
2623
2710
  print(f"[AUTO] Managed trade task for {finished_coin} was cancelled.")
2624
2711
  log_auto_trade_event(
2625
2712
  auto_trades_logger,
2626
2713
  "auto_trade_task_cancelled",
2627
2714
  coin=finished_coin,
2628
2715
  )
2716
+ if not shutting_down:
2717
+ raise
2629
2718
  continue
2630
2719
  except Exception as exc:
2720
+ active_trade_tasks.pop(finished_coin)
2631
2721
  print(f"[AUTO-WARN] Managed trade task failed for {finished_coin}: {exc}")
2632
2722
  logger.exception("[AUTO] Managed trade task failed for %s", finished_coin)
2633
2723
  log_auto_trade_event(
@@ -2636,9 +2726,10 @@ async def run_auto_trader(
2636
2726
  coin=finished_coin,
2637
2727
  error=str(exc),
2638
2728
  )
2639
- continue
2729
+ # Preserve the checkpoint and stop; a restart must recover this trade
2730
+ # before scanning can place another entry for the same coin.
2731
+ raise RuntimeError(f"Managed trade failed for {finished_coin}; session saved for recovery") from exc
2640
2732
 
2641
- completed_trades += 1
2642
2733
  log_auto_trade_event(
2643
2734
  auto_trades_logger,
2644
2735
  "auto_trade_complete",
@@ -2647,7 +2738,7 @@ async def run_auto_trader(
2647
2738
  size=trade_result.size,
2648
2739
  take_profit_pct=trade_result.take_profit_pct,
2649
2740
  stop_loss_pct=trade_result.stop_loss_pct,
2650
- completed_trades=completed_trades,
2741
+ completed_trades=completed_trades + 1,
2651
2742
  started_ms=trade_result.started_ms,
2652
2743
  closed_ms=trade_result.closed_ms,
2653
2744
  signal_snapshot=trade_result.log_context,
@@ -2666,6 +2757,9 @@ async def run_auto_trader(
2666
2757
  start_time_ms=trade_result.started_ms,
2667
2758
  end_time_ms=trade_result.closed_ms,
2668
2759
  )
2760
+ await journal.record(trade_result, cycle_pnl, cycle_fills, pnl_reason, info, deduplicate=True)
2761
+ active_trade_tasks.pop(finished_coin)
2762
+ completed_trades += 1
2669
2763
  if pnl_reason:
2670
2764
  print(
2671
2765
  f"[AUTO-WARN] {trade_result.coin} post-trade PnL unavailable; "
@@ -2775,6 +2869,8 @@ async def run_auto_trader(
2775
2869
  elif not loop_after_trade:
2776
2870
  stop_requested = True
2777
2871
  stop_reason = f"completed {completed_trades} trade(s) and auto looping is disabled"
2872
+ pending_trades.pop(finished_coin, None)
2873
+ checkpoint()
2778
2874
 
2779
2875
  async def _sleep_until_next_scan_batch() -> None:
2780
2876
  if startup_backfill_state.enabled and startup_backfill_state.pause_between_batches and not startup_backfill_state.complete:
@@ -2789,6 +2885,7 @@ async def run_auto_trader(
2789
2885
  while True:
2790
2886
  _iter +=1
2791
2887
  await _drain_completed_trade_tasks()
2888
+ checkpoint()
2792
2889
  if stop_requested:
2793
2890
  if active_trade_tasks:
2794
2891
  print(
@@ -2804,6 +2901,8 @@ async def run_auto_trader(
2804
2901
  await asyncio.sleep(min(scan_interval, 1.0))
2805
2902
  continue
2806
2903
  print(f"[AUTO] {stop_reason}; exiting auto mode.")
2904
+ session_store.data["complete"] = True
2905
+ checkpoint()
2807
2906
  return
2808
2907
 
2809
2908
  if coin is not None:
@@ -3046,12 +3145,16 @@ async def run_auto_trader(
3046
3145
 
3047
3146
  for launch_coin, launch_signal, launch_context, launch_size in launch_specs:
3048
3147
  try:
3148
+ launch_signal = scale_take_profit_signal(launch_signal, take_profit_scale)
3049
3149
  if launch_signal.strategy in ("reversal", "fib_reversal"):
3050
3150
  launch_signal = await normalize_signal_prices(info, launch_signal)
3051
3151
  plan = strategy.build_execution_plan(launch_context, launch_signal, launch_size)
3052
3152
  if dry_run:
3153
+ pending_trades[launch_coin] = {"plan": asdict(plan), "state": {}}
3154
+ checkpoint()
3053
3155
  active_trade_tasks[launch_coin] = asyncio.create_task(
3054
- run_simulated_trade(info, plan, auto_trades_logger, poll_interval)
3156
+ run_simulated_trade(info, plan, auto_trades_logger, poll_interval,
3157
+ state=pending_trades[launch_coin]["state"], checkpoint=checkpoint)
3055
3158
  )
3056
3159
  continue
3057
3160
  if plan.kind == "bracket_entry":
@@ -3082,8 +3185,7 @@ async def run_auto_trader(
3082
3185
  size=launch_size,
3083
3186
  execution_plan=log_context,
3084
3187
  )
3085
- active_trade_tasks[launch_coin] = asyncio.create_task(
3086
- _run_managed_auto_trade(
3188
+ start_live_trade(
3087
3189
  managed_coin=launch_coin,
3088
3190
  managed_direction=plan.direction,
3089
3191
  managed_size=launch_size,
@@ -3099,7 +3201,6 @@ async def run_auto_trader(
3099
3201
  managed_entry_retries=plan_entry_retries,
3100
3202
  managed_market_fallback=plan_market_fallback,
3101
3203
  )
3102
- )
3103
3204
  else:
3104
3205
  if len(plan.take_profit_prices) < 3:
3105
3206
  raise RuntimeError(
@@ -3135,8 +3236,7 @@ async def run_auto_trader(
3135
3236
  size=launch_size,
3136
3237
  execution_plan=log_context,
3137
3238
  )
3138
- active_trade_tasks[launch_coin] = asyncio.create_task(
3139
- _run_managed_auto_trade(
3239
+ start_live_trade(
3140
3240
  managed_coin=launch_coin,
3141
3241
  managed_direction=plan.direction,
3142
3242
  managed_size=launch_size,
@@ -3148,8 +3248,9 @@ async def run_auto_trader(
3148
3248
  managed_shortest_snapshot=None,
3149
3249
  managed_log_context=log_context,
3150
3250
  )
3151
- )
3152
3251
  except Exception as exc:
3252
+ if isinstance(exc, OSError):
3253
+ raise
3153
3254
  print(f"[AUTO-WARN] Failed to launch {launch_signal.strategy} signal for {launch_coin}: {exc}")
3154
3255
  logger.exception("[AUTO] Failed to launch %s signal for %s", launch_signal.strategy, launch_coin)
3155
3256
  log_auto_trade_event(
@@ -3176,16 +3277,28 @@ async def run_auto_trader(
3176
3277
  )
3177
3278
  continue
3178
3279
 
3179
- metrics_start_time_ms = int(time.time() * 1000)
3180
3280
  await _sleep_until_next_scan_batch()
3181
3281
  except KeyboardInterrupt:
3182
3282
  print("\n[!] Caught Ctrl+C, stopping auto trader.")
3183
3283
  finally:
3184
- if dry_run:
3185
- tasks = list(locals().get("active_trade_tasks", {}).values())
3186
- for task in tasks:
3187
- task.cancel()
3188
- await asyncio.gather(*tasks, return_exceptions=True)
3189
- await strategy.shutdown()
3190
- if owns_clients:
3191
- await close_clients(info, exchange)
3284
+ shutting_down = True
3285
+ tasks = list(locals().get("active_trade_tasks", {}).values())
3286
+ for task in tasks:
3287
+ task.cancel()
3288
+ await asyncio.gather(*tasks, return_exceptions=True)
3289
+ try:
3290
+ # Completed tasks can otherwise be lost when shutdown interrupts a scan sleep.
3291
+ if "_drain_completed_trade_tasks" in locals():
3292
+ await _drain_completed_trade_tasks()
3293
+ finally:
3294
+ try:
3295
+ if "checkpoint" in locals():
3296
+ checkpoint()
3297
+ finally:
3298
+ if "session_store" in locals():
3299
+ session_store.close()
3300
+ try:
3301
+ await strategy.shutdown()
3302
+ finally:
3303
+ if owns_clients:
3304
+ await close_clients(info, exchange)