|
1 | 1 | """ |
2 | 2 | Improved Trading Bot - Enhanced Risk Management |
3 | 3 | """ |
4 | | -import json |
5 | | -import time |
6 | | -import pandas as pd |
7 | 4 | import argparse |
| 5 | +import json |
8 | 6 | import logging |
| 7 | +import os |
| 8 | +import secrets |
| 9 | +import shutil |
| 10 | +import time |
9 | 11 | from datetime import datetime |
10 | | -from typing import Dict, Optional |
11 | 12 | from pathlib import Path |
| 13 | +from typing import Dict, Optional |
| 14 | + |
| 15 | +import pandas as pd |
| 16 | +import requests |
12 | 17 |
|
13 | 18 | from exchanges.factory import get_exchange_client |
14 | 19 | from strategies.strategies import get_strategy |
@@ -85,19 +90,50 @@ def _load_config(self, config_path: str) -> Dict: |
85 | 90 | with path.open('r') as f: |
86 | 91 | config = json.load(f) |
87 | 92 |
|
| 93 | + # Resolve secrets that may be provided via environment variables (e.g., "$PRIVATE_KEY") |
| 94 | + def resolve_env_or_literal(value: Optional[str], field: str) -> Optional[str]: |
| 95 | + if value and isinstance(value, str) and value.startswith('$'): |
| 96 | + env_name = value[1:] |
| 97 | + env_val = os.getenv(env_name) |
| 98 | + if env_val: |
| 99 | + logger.info(f"Loaded {field} from env: {env_name}") |
| 100 | + return env_val |
| 101 | + logger.warning(f"{field} references env {env_name} but it is not set") |
| 102 | + return None |
| 103 | + return value |
| 104 | + |
| 105 | + config['private_key'] = resolve_env_or_literal(config.get('private_key'), 'private_key') |
| 106 | + |
88 | 107 | # Auto-provision nostr keys and relays if missing to improve UX |
89 | 108 | nostr_cfg = config.setdefault('nostr', {}) |
90 | 109 | updated = False |
91 | 110 |
|
92 | 111 | nsec = nostr_cfg.get('nsec') |
93 | 112 | relays = nostr_cfg.get('relays', []) |
94 | | - if not nsec: |
| 113 | + |
| 114 | + def generate_keys(): |
95 | 115 | priv = PrivateKey() |
96 | 116 | nostr_cfg['nsec'] = priv.bech32() |
97 | 117 | nostr_cfg['npub'] = priv.public_key.bech32() |
98 | | - updated = True |
99 | 118 | logger.info("Generated nostr keypair and wrote to config.json") |
100 | 119 |
|
| 120 | + # Treat placeholder or invalid nsec as missing and auto-generate |
| 121 | + if not nsec or nsec == "nsec1yourprivatekey": |
| 122 | + generate_keys() |
| 123 | + updated = True |
| 124 | + else: |
| 125 | + try: |
| 126 | + PrivateKey.from_nsec(nsec) |
| 127 | + # populate npub if absent |
| 128 | + if not nostr_cfg.get('npub'): |
| 129 | + priv = PrivateKey.from_nsec(nsec) |
| 130 | + nostr_cfg['npub'] = priv.public_key.bech32() |
| 131 | + updated = True |
| 132 | + except Exception: |
| 133 | + logger.warning("Invalid nsec in config; generating a new keypair") |
| 134 | + generate_keys() |
| 135 | + updated = True |
| 136 | + |
101 | 137 | if not relays: |
102 | 138 | nostr_cfg['relays'] = ["wss://nostr.parallel.hetu.org:8443"] |
103 | 139 | updated = True |
@@ -614,16 +650,158 @@ def shutdown(self): |
614 | 650 | logger.info("👋 Bot safely shutdown") |
615 | 651 |
|
616 | 652 |
|
| 653 | +def run_init(config_path: Path) -> None: |
| 654 | + print("===============================================") |
| 655 | + print(" Moltrade Trader Init (no trading will run)") |
| 656 | + print("===============================================") |
| 657 | + |
| 658 | + example = config_path.parent / "config.example.json" |
| 659 | + if not config_path.exists(): |
| 660 | + if example.exists(): |
| 661 | + shutil.copyfile(example, config_path) |
| 662 | + print(f"Copied template to {config_path} for initialization") |
| 663 | + else: |
| 664 | + print("config.example.json not found; cannot bootstrap config") |
| 665 | + return |
| 666 | + |
| 667 | + with config_path.open('r') as f: |
| 668 | + config = json.load(f) |
| 669 | + |
| 670 | + def prompt(msg: str, default: Optional[str] = None, allow_empty: bool = False) -> str: |
| 671 | + suffix = f" [{default}]" if default is not None else "" |
| 672 | + while True: |
| 673 | + val = input(f"{msg}{suffix}: ").strip() |
| 674 | + if val == '' and default is not None: |
| 675 | + return default |
| 676 | + if val == '' and allow_empty: |
| 677 | + return '' |
| 678 | + if val: |
| 679 | + return val |
| 680 | + |
| 681 | + # Base URL for relayer API |
| 682 | + print("\n[1/5] Relayer setup") |
| 683 | + base_url = prompt("Relayer base URL (for bot registration)", config.get('relayer_api', 'http://localhost:8080')) |
| 684 | + config['relayer_api'] = base_url.rstrip('/') |
| 685 | + |
| 686 | + # Wallet setup |
| 687 | + print("\n[2/5] Wallet setup: choose to generate a new private key or use your own wallet.") |
| 688 | + print("1) Generate new private key (recommended for testing)\n2) Use existing wallet") |
| 689 | + choice = prompt("Select option", "2") |
| 690 | + |
| 691 | + wallet_address = config.get('wallet_address', '') |
| 692 | + private_key = config.get('private_key') |
| 693 | + |
| 694 | + if choice == '1': |
| 695 | + generated_pk = secrets.token_hex(32) |
| 696 | + print("Generated 32-byte hex private key for you.") |
| 697 | + wallet_address = prompt("Enter wallet_address (must correspond to the private key)", wallet_address or "0x...") |
| 698 | + store_env = prompt("Store private key as env reference? (y/N)", "y") |
| 699 | + if store_env.lower().startswith('y'): |
| 700 | + env_name = prompt("Env var name for private key", "PRIVATE_KEY") |
| 701 | + private_key = f"${env_name}" |
| 702 | + print(f"Remember to export {env_name}={generated_pk}") |
| 703 | + else: |
| 704 | + private_key = generated_pk |
| 705 | + else: |
| 706 | + wallet_address = prompt("Enter wallet_address", wallet_address or "0x...") |
| 707 | + print("Private key is sensitive; using an env var is recommended (export PRIVATE_KEY=yourkey before running the bot)") |
| 708 | + store_env = prompt("Use env var for private_key? (y/N)", "y") |
| 709 | + if store_env.lower().startswith('y'): |
| 710 | + env_name = prompt("Env var name for private key", "PRIVATE_KEY") |
| 711 | + private_key = f"${env_name}" |
| 712 | + print(f"\033[91mAfter init, run: export {env_name}=<your_private_key> before starting the bot\033[0m") |
| 713 | + else: |
| 714 | + private_key = prompt("Enter private_key (will be stored in config)", private_key or "") |
| 715 | + |
| 716 | + config['wallet_address'] = wallet_address |
| 717 | + config['private_key'] = private_key |
| 718 | + |
| 719 | + # Trading essentials |
| 720 | + print("\n[3/5] Trading basics") |
| 721 | + trading = config.setdefault('trading', {}) |
| 722 | + trading['exchange'] = prompt("Trading.exchange", trading.get('exchange', 'hyperliquid')) |
| 723 | + trading['default_symbol'] = prompt("Trading.default_symbol", trading.get('default_symbol', 'HYPE')) |
| 724 | + trading['default_strategy'] = prompt("Trading.default_strategy", trading.get('default_strategy', 'test')) |
| 725 | + print("Other trading/risk fields can be adjusted later in config.json.") |
| 726 | + |
| 727 | + # Nostr keys: generate if missing/placeholder |
| 728 | + print("\n[4/5] Nostr keys") |
| 729 | + nostr_cfg = config.setdefault('nostr', {}) |
| 730 | + nsec = nostr_cfg.get('nsec') |
| 731 | + if not nsec or nsec == "nsec1yourprivatekey": |
| 732 | + priv = PrivateKey() |
| 733 | + nostr_cfg['nsec'] = priv.bech32() |
| 734 | + nostr_cfg['npub'] = priv.public_key.bech32() |
| 735 | + print("Generated Nostr nsec/npub and wrote to config.") |
| 736 | + else: |
| 737 | + try: |
| 738 | + priv = PrivateKey.from_nsec(nsec) |
| 739 | + if not nostr_cfg.get('npub'): |
| 740 | + nostr_cfg['npub'] = priv.public_key.bech32() |
| 741 | + except Exception: |
| 742 | + priv = PrivateKey() |
| 743 | + nostr_cfg['nsec'] = priv.bech32() |
| 744 | + nostr_cfg['npub'] = priv.public_key.bech32() |
| 745 | + print("Invalid nsec; generated a new Nostr keypair.") |
| 746 | + |
| 747 | + # Save config after prompts |
| 748 | + with config_path.open('w') as f: |
| 749 | + json.dump(config, f, indent=2, ensure_ascii=False) |
| 750 | + f.write('\n') |
| 751 | + print(f"Config saved to {config_path}.") |
| 752 | + |
| 753 | + # Bot registration |
| 754 | + print("\n[5/5] Bot registration") |
| 755 | + try_register = prompt("Register bot with relayer now? (y/N)", "y") |
| 756 | + if try_register.lower().startswith('y'): |
| 757 | + bot_name = prompt("Bot name", "my-bot-1") |
| 758 | + register_url = f"{config['relayer_api']}/api/bots/register" |
| 759 | + payload = { |
| 760 | + "bot_pubkey": wallet_address, |
| 761 | + "nostr_pubkey": nostr_cfg.get('npub'), |
| 762 | + "eth_address": wallet_address, |
| 763 | + "name": bot_name, |
| 764 | + } |
| 765 | + try: |
| 766 | + resp = requests.post(register_url, json=payload, timeout=10) |
| 767 | + resp.raise_for_status() |
| 768 | + data = resp.json() |
| 769 | + print(f"Bot registration response: {data}") |
| 770 | + platform_pubkey = data.get('platform_pubkey') |
| 771 | + if platform_pubkey: |
| 772 | + nostr_cfg['platform_shared_key'] = platform_pubkey |
| 773 | + with config_path.open('w') as f: |
| 774 | + json.dump(config, f, indent=2, ensure_ascii=False) |
| 775 | + f.write('\n') |
| 776 | + print("Saved platform_pubkey into nostr.platform_shared_key") |
| 777 | + except Exception as exc: |
| 778 | + print(f"Bot registration failed: {exc}") |
| 779 | + else: |
| 780 | + print("Skipped bot registration.") |
| 781 | + |
| 782 | + print("\nInit complete. You can now run the bot normally.") |
| 783 | + |
| 784 | + |
| 785 | +# --------------------------------------------------------------------------- |
| 786 | +# CLI entrypoint |
| 787 | +# --------------------------------------------------------------------------- |
| 788 | + |
| 789 | + |
617 | 790 | def main(): |
618 | 791 | parser = argparse.ArgumentParser(description='Improved Hyperliquid Trading Bot') |
619 | 792 | parser.add_argument('--config', type=str, default='config.json', help='Config file path') |
620 | 793 | parser.add_argument('--test', action='store_true', help='Test mode') |
621 | 794 | parser.add_argument('--strategy', type=str, help='Strategy name') |
622 | 795 | parser.add_argument('--symbol', type=str, help='Trading pair') |
623 | 796 | parser.add_argument('--interval', type=int, default=300, help='Refresh interval (seconds)') |
| 797 | + parser.add_argument('--init', action='store_true', help='Initialize config interactively (no trading)') |
624 | 798 |
|
625 | 799 | args = parser.parse_args() |
626 | 800 |
|
| 801 | + if args.init: |
| 802 | + run_init(Path(args.config)) |
| 803 | + return |
| 804 | + |
627 | 805 | strategy_name = args.strategy |
628 | 806 | if not strategy_name: |
629 | 807 | with open(args.config) as f: |
|
0 commit comments