diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..bed64da --- /dev/null +++ b/render.yaml @@ -0,0 +1,27 @@ +services: + - type: worker + name: allocation-engine-2.0 + runtime: python + region: oregon + plan: starter + buildCommand: pip install -r requirements.txt + startCommand: python -m trading_system.main --publish --dashboard --continuous --interval 5 --ticker BTC SPY QQQ AMZN + envVars: + - key: NETLIFY_API_TOKEN + sync: false + - key: NETLIFY_SITE_ID + sync: false + - key: TWELVE_DATA_API_KEY + sync: false + - key: RH_USERNAME + sync: false + - key: RH_PASSWORD + sync: false + - key: RH_TOTP_CODE + sync: false + - key: RH_AUTOMATED_ACCOUNT_NUMBER + sync: false + - key: SLACK_WEBHOOK_URL + sync: false + - key: PYTHON_VERSION + value: "3.11" diff --git a/trading_system/main.py b/trading_system/main.py index 4865dca..bbff066 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -35,7 +35,8 @@ def __init__(self, twelve_data_api_key: str, symbols: List[str], strategy_name: str = 'momentum_dca', verbose: bool = False, dashboard: bool = False, - recent_days: int = None): + recent_days: int = None, + publish: bool = False): """ Initialize trading system @@ -48,6 +49,7 @@ def __init__(self, twelve_data_api_key: str, symbols: List[str], verbose: If True, show detailed output (metrics, portfolio, etc.) dashboard: If True, fetch market indicators and write dashboard data each cycle recent_days: If set, limit backtest to last N daily bars + publish: If True, upload blobs even in dry-run mode (no trades, but site data updates) """ self.symbols = symbols self.dry_run = dry_run @@ -55,43 +57,50 @@ def __init__(self, twelve_data_api_key: str, symbols: List[str], self.verbose = verbose self.dashboard = dashboard self.recent_days = recent_days + self.publish = publish # Initialize components self.data_provider = TwelveDataProvider(twelve_data_api_key) self.metrics_calculator = MetricsCalculator() self.state_manager = StateManager() - self.trading_bot = SafeCashBot() - # Initialize execution quality layer + # Skip broker initialization in publish-only mode (no trading needed) + self.trading_bot = None self.fill_logger = None - try: - from trading_system.execution.fill_auditor import FillAuditor - from trading_system.execution.spread_checker import SpreadChecker - from trading_system.execution.price_optimizer import PriceOptimizer - from trading_system.execution.pdt_gate import PDTGate - from trading_system.execution.fill_logger import FillLogger - - fill_auditor = FillAuditor( - alpaca_key=os.getenv('ALPACA_API_KEY', ''), - alpaca_secret=os.getenv('ALPACA_SECRET_KEY', ''), - twelve_data_provider=self.data_provider, - ) - spread_checker = SpreadChecker(fill_auditor=fill_auditor) - price_optimizer = PriceOptimizer() - pdt_gate = PDTGate(trading_bot=self.trading_bot) - fill_logger = FillLogger() - - self.trading_bot.init_execution_layer( - fill_auditor=fill_auditor, - spread_checker=spread_checker, - price_optimizer=price_optimizer, - pdt_gate=pdt_gate, - fill_logger=fill_logger, - ) + if not (self.publish and self.dry_run): + self.trading_bot = SafeCashBot() - self.fill_logger = fill_logger - except Exception as e: - print(f" [exec-layer] Execution quality layer init failed (proceeding without): {e}") + # Initialize execution quality layer + try: + from trading_system.execution.fill_auditor import FillAuditor + from trading_system.execution.spread_checker import SpreadChecker + from trading_system.execution.price_optimizer import PriceOptimizer + from trading_system.execution.pdt_gate import PDTGate + from trading_system.execution.fill_logger import FillLogger + + fill_auditor = FillAuditor( + alpaca_key=os.getenv('ALPACA_API_KEY', ''), + alpaca_secret=os.getenv('ALPACA_SECRET_KEY', ''), + twelve_data_provider=self.data_provider, + ) + spread_checker = SpreadChecker(fill_auditor=fill_auditor) + price_optimizer = PriceOptimizer() + pdt_gate = PDTGate(trading_bot=self.trading_bot) + fill_logger = FillLogger() + + self.trading_bot.init_execution_layer( + fill_auditor=fill_auditor, + spread_checker=spread_checker, + price_optimizer=price_optimizer, + pdt_gate=pdt_gate, + fill_logger=fill_logger, + ) + + self.fill_logger = fill_logger + except Exception as e: + print(f" [exec-layer] Execution quality layer init failed (proceeding without): {e}") + else: + print(" [publish-only] Skipping broker init — publish-only mode (no trades)") if strategy_name == 'momentum_dca_long': self.strategy = MomentumDcaLongStrategy(symbols) @@ -607,15 +616,18 @@ def run_once(self): print(f"{'='*70}\n") # Print initial portfolio allocation - self.print_portfolio_allocation() + if self.trading_bot: + self.print_portfolio_allocation() # Fetch open orders once (used by momentum_dca) open_orders = [] - if self.strategy_name == 'momentum_dca_long': - open_orders = self.trading_bot.get_open_orders() - - recent_orders = self.trading_bot.get_recent_orders(days=7) - recent_option_orders = self.trading_bot.get_recent_option_orders(days=7) + recent_orders = [] + recent_option_orders = [] + if self.trading_bot: + if self.strategy_name == 'momentum_dca_long': + open_orders = self.trading_bot.get_open_orders() + recent_orders = self.trading_bot.get_recent_orders(days=7) + recent_option_orders = self.trading_bot.get_recent_option_orders(days=7) # Print order book before processing through state manager if open_orders: @@ -668,11 +680,12 @@ def run_once(self): if self.verbose: print(self.metrics_calculator.format_metrics(symbol, metrics)) - # 3. Execute strategy - signal = self.execute_strategy(symbol, metrics, open_orders) + # 3. Execute strategy (requires broker for positions) + if self.trading_bot: + signal = self.execute_strategy(symbol, metrics, open_orders) - # 4. Process signal - self.process_signal(symbol, signal, open_orders) + # 4. Process signal + self.process_signal(symbol, signal, open_orders) except Exception as e: print(f"Error processing {symbol}: {e}") @@ -683,14 +696,14 @@ def run_once(self): # Log state: local file in dry-run, Netlify Blobs when live symbols_filter = None if self.verbose else self.symbols - portfolio_data = self.trading_bot.get_portfolio_summary(symbols=symbols_filter) + portfolio_data = self.trading_bot.get_portfolio_summary(symbols=symbols_filter) if self.trading_bot else None # Compute drift metrics from cached daily bars (if available) drift_metrics = self._compute_drift_metrics() log_state_to_blob( self.state_manager, - live=not self.dry_run, + live=not self.dry_run or self.publish, order_book=open_orders, portfolio=portfolio_data, drift_metrics=drift_metrics, @@ -737,8 +750,9 @@ def run_once(self): self.state_manager.print_state_summary() # Print final portfolio allocation - print("\nFinal Portfolio Allocation:") - self.print_portfolio_allocation() + if self.trading_bot: + print("\nFinal Portfolio Allocation:") + self.print_portfolio_allocation() # Print fill quality stats if hasattr(self, 'fill_logger') and self.fill_logger: @@ -848,7 +862,7 @@ def _send_oncall_summary(self, open_orders: List[Dict], portfolio_data: Dict): lines.append("=" * 40) # PDT status - pdt_info = self.trading_bot.get_pdt_status() + pdt_info = self.trading_bot.get_pdt_status() if self.trading_bot else None if pdt_info is not None: count = pdt_info.get('day_trade_count', 0) if pdt_info.get('flagged'): @@ -1137,6 +1151,11 @@ def main(): action='store_true', help='Fetch market indicators and write dashboard/market_data.json each cycle' ) + parser.add_argument( + '--publish', + action='store_true', + help='Upload blobs in dry-run mode (no trades placed, but site data updates)' + ) parser.add_argument( '--backtest', action='store_true', @@ -1179,10 +1198,11 @@ def main(): verbose=args.verbose, dashboard=args.dashboard, recent_days=args.recent, + publish=args.publish, ) # Notify Slack on deployment startup - mode = "LIVE" if args.live else "DRY RUN" + mode = "LIVE" if args.live else ("PUBLISH" if args.publish else "DRY RUN") send_slack_alert( f":rocket: Engine deployed — mode={mode}, strategy={args.strategy}, symbols={', '.join(symbols)}", emoji=":rocket:",