Skip to content

Commit e0f8775

Browse files
committed
feat: update configuration handling to support environment variables and improve initialization process
1 parent 17d1db5 commit e0f8775

File tree

3 files changed

+189
-17
lines changed

3 files changed

+189
-17
lines changed

trader/config.example.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
2+
"relayer_api": "http://localhost:8080",
23
"wallet_address": "0x...",
3-
"private_key": "your_private_key_here",
4+
"private_key": "$PRIVATE_KEY",
45
"telegram": {
56
"enabled": false,
67
"bot_token": "your_bot_token_here",

trader/main.py

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"""
22
Improved Trading Bot - Enhanced Risk Management
33
"""
4-
import json
5-
import time
6-
import pandas as pd
74
import argparse
5+
import json
86
import logging
7+
import os
8+
import secrets
9+
import shutil
10+
import time
911
from datetime import datetime
10-
from typing import Dict, Optional
1112
from pathlib import Path
13+
from typing import Dict, Optional
14+
15+
import pandas as pd
16+
import requests
1217

1318
from exchanges.factory import get_exchange_client
1419
from strategies.strategies import get_strategy
@@ -85,19 +90,50 @@ def _load_config(self, config_path: str) -> Dict:
8590
with path.open('r') as f:
8691
config = json.load(f)
8792

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+
88107
# Auto-provision nostr keys and relays if missing to improve UX
89108
nostr_cfg = config.setdefault('nostr', {})
90109
updated = False
91110

92111
nsec = nostr_cfg.get('nsec')
93112
relays = nostr_cfg.get('relays', [])
94-
if not nsec:
113+
114+
def generate_keys():
95115
priv = PrivateKey()
96116
nostr_cfg['nsec'] = priv.bech32()
97117
nostr_cfg['npub'] = priv.public_key.bech32()
98-
updated = True
99118
logger.info("Generated nostr keypair and wrote to config.json")
100119

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+
101137
if not relays:
102138
nostr_cfg['relays'] = ["wss://nostr.parallel.hetu.org:8443"]
103139
updated = True
@@ -614,16 +650,158 @@ def shutdown(self):
614650
logger.info("👋 Bot safely shutdown")
615651

616652

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+
617790
def main():
618791
parser = argparse.ArgumentParser(description='Improved Hyperliquid Trading Bot')
619792
parser.add_argument('--config', type=str, default='config.json', help='Config file path')
620793
parser.add_argument('--test', action='store_true', help='Test mode')
621794
parser.add_argument('--strategy', type=str, help='Strategy name')
622795
parser.add_argument('--symbol', type=str, help='Trading pair')
623796
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)')
624798

625799
args = parser.parse_args()
626800

801+
if args.init:
802+
run_init(Path(args.config))
803+
return
804+
627805
strategy_name = args.strategy
628806
if not strategy_name:
629807
with open(args.config) as f:

trader/strategies/strategies.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from typing import Dict, Optional, List
1010
import time
1111

12+
from strategies.trend_follow_strategies import TrendFollowingStrategy
13+
from strategies.test_strategy import TestStrategy
14+
1215
class BaseStrategy:
1316
"""Base class for strategies"""
1417
def __init__(self, config: Dict):
@@ -225,16 +228,6 @@ def analyze(self, df: pd.DataFrame) -> Dict:
225228

226229
def get_strategy(strategy_name: str, config: Dict) -> BaseStrategy:
227230
"""Get strategy instance"""
228-
try:
229-
from trader.strategies.trend_follow_strategies import TrendFollowingStrategy
230-
except ImportError:
231-
pass
232-
233-
try:
234-
from trader.strategies.test_strategy import TestStrategy
235-
except ImportError:
236-
pass
237-
238231
strategies = {
239232
'momentum': MomentumStrategy,
240233
'mean_reversion': MeanReversionStrategy,

0 commit comments

Comments
 (0)