-
Notifications
You must be signed in to change notification settings - Fork 1
Reduce maximum order size to 50 shares #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
28c2235
cfdf627
660ccd2
aa88355
de3c5f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| """Read-only: list open stock orders across ALL accounts (cash + margin) as a table. | ||
|
|
||
| Places nothing. Account-aware: logs in via RobinhoodAuth, discovers each account on | ||
| the login, queries open stock orders per account, and labels every row with the | ||
| account it lives on (so margin orders are included, not hidden behind a cash filter). | ||
|
|
||
| Self-locating: chdir/sys.path to its own dir so it runs from any cwd | ||
| (the Telegram bot runs Claude with cwd=/). | ||
| """ | ||
| import os | ||
| import sys | ||
| from datetime import datetime | ||
|
|
||
| HERE = os.path.dirname(os.path.abspath(__file__)) | ||
| os.chdir(HERE) # so load_dotenv() finds .env | ||
| sys.path.insert(0, HERE) # so `from utils...` resolves | ||
|
|
||
| import robin_stocks.robinhood as r # noqa: E402 | ||
| from utils.rh_auth import RobinhoodAuth # noqa: E402 | ||
|
|
||
|
|
||
| def _fmt_price(v): | ||
| return f"${float(v):,.2f}" if v not in (None, "", "N/A") else "—" | ||
|
|
||
|
|
||
| def _fmt_ts(ts): | ||
| try: | ||
| return datetime.fromisoformat(str(ts).replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M") | ||
| except Exception: | ||
| return str(ts)[:16] | ||
|
|
||
|
|
||
| def discover_accounts(): | ||
| """Return {account_number: type} for every account on this login. | ||
|
|
||
| Seeds with the configured trade account (RH_AUTOMATED_ACCOUNT_NUMBER, which the | ||
| /accounts/ endpoint may omit), then merges anything the endpoint reports. | ||
| """ | ||
| accts = {} | ||
| cfg = os.getenv("RH_AUTOMATED_ACCOUNT_NUMBER") | ||
| if cfg: | ||
| accts[cfg] = None | ||
| try: | ||
| data = r.helper.request_get("https://api.robinhood.com/accounts/", "regular") | ||
| results = (data or {}).get("results", []) if isinstance(data, dict) else (data or []) | ||
| for a in results: | ||
| num = a.get("account_number") | ||
| if num: | ||
| accts[num] = a.get("type") | ||
| except Exception: | ||
| pass | ||
| for num in list(accts): | ||
| if accts[num] is None: | ||
| try: | ||
| prof = r.profiles.load_account_profile(account_number=num) | ||
| accts[num] = (prof or {}).get("type", "?") | ||
| except Exception: | ||
| accts[num] = "?" | ||
| return accts | ||
|
|
||
|
|
||
| def fetch_open_orders(account_number, acct_label): | ||
| """Return parsed open stock orders for one account, each tagged with acct_label.""" | ||
| raw = r.orders.get_all_open_stock_orders(account_number=account_number) or [] | ||
| out = [] | ||
| for o in raw: | ||
| symbol = o.get("symbol", "N/A") | ||
| if symbol == "N/A" and o.get("instrument_id"): | ||
| try: | ||
| inst = r.stocks.get_instrument_by_url( | ||
| f"https://api.robinhood.com/instruments/{o['instrument_id']}/" | ||
| ) | ||
| symbol = (inst or {}).get("symbol", "N/A") | ||
| except Exception: | ||
| pass | ||
| order_type = o.get("type", "N/A") | ||
| trigger = o.get("trigger", "immediate") | ||
| if trigger == "stop" and order_type == "limit": | ||
| desc = "Stop Limit" | ||
| elif trigger == "stop": | ||
| desc = "Stop Loss" | ||
| elif order_type == "limit": | ||
| desc = "Limit" | ||
| else: | ||
| desc = "Market" | ||
| side = o.get("side", "N/A") | ||
| out.append({ | ||
| "account": acct_label, | ||
| "symbol": symbol, | ||
| "side": side.upper() if side != "N/A" else "N/A", | ||
| "order_type": desc, | ||
| "quantity": float(o.get("quantity", 0)), | ||
| "limit_price": o.get("price"), | ||
| "stop_price": o.get("stop_price"), | ||
| "created_at": _fmt_ts(o.get("created_at", "N/A")), | ||
| }) | ||
| return out | ||
|
|
||
|
|
||
| def main(): | ||
| RobinhoodAuth().login() # cached session | ||
| accounts = discover_accounts() | ||
|
|
||
| orders = [] | ||
| for num, acct_type in accounts.items(): | ||
| label = f"{num}/{acct_type}" | ||
| orders.extend(fetch_open_orders(num, label)) | ||
|
|
||
| scanned = ", ".join(f"{n}/{t}" for n, t in accounts.items()) | ||
| print(f"\nOpen orders — accounts: {scanned} — DRY RUN (read-only)\n") | ||
| if not orders: | ||
| print(" (no open orders)") | ||
| return | ||
|
|
||
| rows = [( | ||
| o["account"], o["symbol"], o["side"], o["order_type"], f"{o['quantity']:g}", | ||
| _fmt_price(o["limit_price"]), _fmt_price(o["stop_price"]), o["created_at"], | ||
| ) for o in orders] | ||
|
|
||
| headers = ("ACCOUNT", "SYMBOL", "SIDE", "TYPE", "QTY", "LIMIT", "STOP", "PLACED") | ||
| widths = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(headers)] | ||
|
|
||
| def line(cells): | ||
| return " ".join(c.ljust(widths[i]) for i, c in enumerate(cells)) | ||
|
|
||
| print(line(headers)) | ||
| print(line(["-" * w for w in widths])) | ||
| for row in rows: | ||
| print(line(row)) | ||
| print(f"\n{len(orders)} open order(s) across {len(accounts)} account(s).") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| """ | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a real pytest test — it's a manual script. This file requires live Robinhood credentials, uses
Also: this exact file already exists in PR #45. The PDT gate changes in this PR are identical to #45 — one of them should be closed to avoid duplicate work. Generated by Claude Code |
||
| Test PDT Gate with current live positions and fills | ||
| """ | ||
|
|
||
| import sys | ||
| import os | ||
| from datetime import date | ||
|
|
||
| # Add parent directory to path | ||
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from utils.safe_cash_bot import SafeCashBot | ||
| from trading_system.execution.pdt_gate import PDTGate | ||
|
|
||
|
|
||
| def test_pdt_guard(): | ||
| """Test PDT guard against current live positions and fills.""" | ||
| print("\n" + "="*70) | ||
| print("PDT GATE TEST - LIVE DATA") | ||
| print("="*70 + "\n") | ||
|
|
||
| # Initialize bot and PDT gate | ||
| bot = SafeCashBot() | ||
| pdt_gate = PDTGate(trading_bot=bot) | ||
|
|
||
| # Get recent fills (stock and options) | ||
| print("Fetching recent fills (last 1 day)...\n") | ||
| recent_stock_orders = bot.get_recent_orders(days=1) | ||
| recent_option_orders = bot.get_recent_option_orders(days=1) | ||
|
|
||
| stock_fills = [o for o in recent_stock_orders if o['state'] in ('filled', 'confirmed')] | ||
| option_fills = [o for o in recent_option_orders if o['state'] == 'filled'] | ||
|
|
||
| print(f"Stock fills today: {len(stock_fills)}") | ||
| print(f"Option fills today: {len(option_fills)}\n") | ||
|
|
||
| # Display stock fills | ||
| if stock_fills: | ||
| print("STOCK FILLS TODAY:") | ||
| for fill in stock_fills: | ||
| print(f" {fill['side']} {fill['symbol']} x{fill['quantity']:.0f} @ ${fill.get('average_price', 0):.2f}") | ||
| print(f" Filled: {fill.get('last_transaction_at', 'N/A')}") | ||
| print() | ||
|
|
||
| # Display option fills | ||
| if option_fills: | ||
| print("OPTION FILLS TODAY:") | ||
| for fill in option_fills: | ||
| for leg in fill.get('legs', []): | ||
| print(f" {leg['side']} {leg['position_effect']} {leg['chain_symbol']} " | ||
| f"${leg['strike']:.2f} {leg['option_type'].upper()} exp {leg['expiration']}") | ||
| print(f" Premium: ${fill['processed_premium']:.2f}") | ||
| print(f" Updated: {fill['updated_at']}") | ||
| print() | ||
|
|
||
| # Test PDT checks for various scenarios | ||
| print("="*70) | ||
| print("PDT CHECK SCENARIOS") | ||
| print("="*70 + "\n") | ||
|
|
||
| # Collect unique symbols from fills | ||
| test_symbols = set() | ||
| for fill in stock_fills: | ||
| test_symbols.add(fill['symbol']) | ||
| for fill in option_fills: | ||
| for leg in fill.get('legs', []): | ||
| test_symbols.add(leg['chain_symbol']) | ||
|
|
||
| if not test_symbols: | ||
| print("No fills today - testing with common symbols: BTC, SPY, QQQ\n") | ||
| test_symbols = {'BTC', 'SPY', 'QQQ'} | ||
|
|
||
| # Test each symbol for both buy and sell | ||
| for symbol in sorted(test_symbols): | ||
| print(f"Testing {symbol}:") | ||
|
|
||
| # Test BUY | ||
| can_buy, buy_reason = pdt_gate.can_place_order(symbol, 'buy') | ||
| status = "✅ ALLOWED" if can_buy else "🚫 BLOCKED" | ||
| print(f" BUY: {status}") | ||
| print(f" {buy_reason}") | ||
|
|
||
| # Test SELL | ||
| can_sell, sell_reason = pdt_gate.can_place_order(symbol, 'sell') | ||
| status = "✅ ALLOWED" if can_sell else "🚫 BLOCKED" | ||
| print(f" SELL: {status}") | ||
| print(f" {sell_reason}") | ||
| print() | ||
|
|
||
| # Summary | ||
| print("="*70) | ||
| print("PDT GUARD STATUS") | ||
| print("="*70 + "\n") | ||
|
|
||
| pdt_info = bot.get_pdt_status() | ||
| if pdt_info: | ||
| count = pdt_info.get('day_trade_count', 0) | ||
| flagged = pdt_info.get('flagged', False) | ||
|
|
||
| print(f"Day Trades Used: {count}/3") | ||
| print(f"PDT Flagged: {'YES ⚠️' if flagged else 'NO ✅'}") | ||
| print(f"Remaining Day Trades: {3 - count}") | ||
|
|
||
| if flagged: | ||
| print("\n⚠️ ACCOUNT IS PDT FLAGGED") | ||
| print(" Only position-closing sells are allowed") | ||
| elif count >= 2: | ||
| print("\n⚠️ WARNING: Only 1 day trade remaining") | ||
| else: | ||
| print(f"\n✅ Safe to trade ({3 - count} day trades remaining)") | ||
| else: | ||
| print("❌ Could not fetch PDT status") | ||
|
|
||
| print("\n" + "="*70 + "\n") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| test_pdt_guard() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| """Central configuration constants for the trading system.""" | ||
|
|
||
| # Maximum shares per paired order (used by live strategy, backtests, and simulations) | ||
| DEFAULT_LOT_SIZE = 250 | ||
| DEFAULT_LOT_SIZE = 50 | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one-liner is the only change that matches the PR title. The rest of the diff (PDT gate rewrite,
Generated by Claude Code |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to lot size change — move or remove.
This 134-line script is a standalone Robinhood order viewer that has nothing to do with reducing
DEFAULT_LOT_SIZE. It also lives at the repo root instead ofscripts/. If it's needed, it should be a separate PR underscripts/(and drop thesys.path.insert/os.chdirhacks in favor of running as a module).Generated by Claude Code