From 25659b3ab33eece2b21b5e2e2ea31ef547fe46f8 Mon Sep 17 00:00:00 2001 From: Jason Bian Date: Mon, 23 Mar 2026 19:04:40 -0400 Subject: [PATCH 1/3] Add --publish flag to decouple blob uploads from live trading Allows running the engine in dry-run mode (no Robinhood orders) while still uploading state to Netlify Blobs, enabling the dashboard at 5thstreetcapital.org/trade. Includes render.yaml for Render deployment. --- render.yaml | 27 +++++++++++++++++++++++++++ trading_system/main.py | 15 ++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 render.yaml 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..a4267e5 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,6 +57,7 @@ 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) @@ -690,7 +693,7 @@ def run_once(self): 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, @@ -1137,6 +1140,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 +1187,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:", From 3948fb0215c20895a9841f9996c957c669ad1895 Mon Sep 17 00:00:00 2001 From: Jason Bian Date: Mon, 23 Mar 2026 21:33:30 -0400 Subject: [PATCH 2/3] Skip broker init in publish-only mode for headless Render deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When --publish is used without --live, the engine only needs market data and blob uploads — no Robinhood credentials required. Guards all trading_bot calls in run_once() so the service can run without RH_AUTOMATED_ACCOUNT_NUMBER. --- trading_system/main.py | 95 +++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/trading_system/main.py b/trading_system/main.py index a4267e5..b73668b 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -63,38 +63,44 @@ def __init__(self, twelve_data_api_key: str, symbols: List[str], 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) @@ -610,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: @@ -671,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}") @@ -686,7 +696,7 @@ 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() @@ -740,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: From 1d701311079142cce03cacc62ee9af3129578f85 Mon Sep 17 00:00:00 2001 From: Jason Bian Date: Thu, 7 May 2026 17:10:32 -0700 Subject: [PATCH 3/3] Guard _send_oncall_summary against None trading_bot (#57) In publish-only/headless Render deploys, trading_bot is None when RH credentials are absent. _send_oncall_summary called .get_pdt_status() unconditionally, raising AttributeError after every successful blob upload. The existing 'PDT: unavailable' branch already handles None, so the fix is a single ternary guard. Co-authored-by: jasonzb --- trading_system/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trading_system/main.py b/trading_system/main.py index b73668b..bbff066 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -862,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'):