Skip to main content
Glama
alpacahq

alpaca-mcp-server

Official
by alpacahq
helpers.py20.6 kB
import re import time from datetime import datetime, date, timedelta, timezone from typing import Any, Dict, List, Optional, Union, Tuple from zoneinfo import ZoneInfo from alpaca.data.timeframe import TimeFrame, TimeFrameUnit from alpaca.trading.enums import OrderClass, OrderSide, TimeInForce from alpaca.trading.models import Order from alpaca.trading.requests import MarketOrderRequest, OptionLegRequest from alpaca.trading.enums import ( AssetStatus, ContractType, OrderClass, OrderSide, OrderType, QueryOrderStatus, TimeInForce, ) def _validate_amount(amount: int, unit: TimeFrameUnit) -> bool: if amount <= 0: return False if unit == TimeFrameUnit.Minute and amount > 59: return False if unit == TimeFrameUnit.Hour and amount > 23: return False if unit in [TimeFrameUnit.Day, TimeFrameUnit.Week, TimeFrameUnit.Month] and amount > 365: return False return True def parse_timeframe_with_enums(timeframe_str: str) -> Optional[TimeFrame]: try: if not timeframe_str or not isinstance(timeframe_str, str): return None timeframe_str = timeframe_str.strip() if not timeframe_str: return None predefined_timeframes = { "1Min": TimeFrame.Minute, "1Hour": TimeFrame.Hour, "1Day": TimeFrame.Day, "1Week": TimeFrame.Week, "1Month": TimeFrame.Month, } if timeframe_str in predefined_timeframes: return predefined_timeframes[timeframe_str] normalized = re.sub(r"\s+", " ", timeframe_str.lower().strip()) direct_mappings = { "half hour": (30, TimeFrameUnit.Minute), "quarter hour": (15, TimeFrameUnit.Minute), "hourly": (1, TimeFrameUnit.Hour), "daily": (1, TimeFrameUnit.Day), "weekly": (1, TimeFrameUnit.Week), "monthly": (1, TimeFrameUnit.Month), } if normalized in direct_mappings: amount, unit = direct_mappings[normalized] return TimeFrame(amount, unit) pattern = r"^(\d+)\s*[-\s]*\s*(min|minute|minutes|hr|hour|hours|day|days|week|weeks|month|months)s?$" match = re.match(pattern, normalized) if match: amount = int(match.group(1)) unit_str = match.group(2) unit_mapping = { "min": TimeFrameUnit.Minute, "minute": TimeFrameUnit.Minute, "minutes": TimeFrameUnit.Minute, "hr": TimeFrameUnit.Hour, "hour": TimeFrameUnit.Hour, "hours": TimeFrameUnit.Hour, "day": TimeFrameUnit.Day, "days": TimeFrameUnit.Day, "week": TimeFrameUnit.Week, "weeks": TimeFrameUnit.Week, "month": TimeFrameUnit.Month, "months": TimeFrameUnit.Month, } unit = unit_mapping.get(unit_str) if unit and _validate_amount(amount, unit): return TimeFrame(amount, unit) alpaca_pattern = r"^(\d+)(min|hour|day|week|month)s?$" match = re.match(alpaca_pattern, normalized) if match: amount = int(match.group(1)) unit_str = match.group(2) unit_mapping = { "min": TimeFrameUnit.Minute, "hour": TimeFrameUnit.Hour, "day": TimeFrameUnit.Day, "week": TimeFrameUnit.Week, "month": TimeFrameUnit.Month, } unit = unit_mapping.get(unit_str) if unit and _validate_amount(amount, unit): return TimeFrame(amount, unit) return None except (ValueError, AttributeError, TypeError): return None def _parse_iso_datetime( value: Optional[str], default_timezone: str = "America/New_York" ) -> Optional[datetime]: """ Parse ISO datetime string with timezone handling. Args: value: ISO datetime string (e.g., "2025-11-14T09:30:00") default_timezone: Timezone for naive datetimes. Supported: "UTC", "ET", "EST", "EDT", "America/New_York" Returns: Timezone-aware datetime object """ if not value: return None s = value.strip() if not s: return None if re.match(r"^\d{4}-\d{2}-\d{2}$", s): s = s + "T00:00:00" s = s.replace("Z", "+00:00") try: dt = datetime.fromisoformat(s) # If timezone is explicit in the string, use it if dt.tzinfo is not None: return dt # Apply default timezone (only UTC or ET supported) tz_upper = default_timezone.upper() if tz_upper == "UTC": dt = dt.replace(tzinfo=timezone.utc) elif tz_upper in ("ET", "EST", "EDT") or default_timezone == "America/New_York": dt = dt.replace(tzinfo=ZoneInfo("America/New_York")) else: raise ValueError( f"Unsupported timezone '{default_timezone}'. " "Supported: 'UTC', 'ET', 'EST', 'EDT', 'America/New_York'" ) return dt except ValueError as e: if "Unsupported timezone" in str(e): raise raise ValueError(f"Invalid ISO datetime format: {value}") from e def _parse_date_ymd(value: str) -> date: return datetime.strptime(value, "%Y-%m-%d").date() def _month_name_to_number(name: str) -> int: try: return datetime.strptime(name.title(), "%B").month except ValueError: return datetime.strptime(name.title(), "%b").month def _format_ohlcv_bar(bar, bar_type: str, include_time: bool = True) -> str: if not bar: return "" time_format = "%Y-%m-%d %H:%M:%S %Z" if include_time else "%Y-%m-%d" time_label = "Timestamp" if include_time else "Date" return f"""{bar_type}: Open: ${bar.open:.2f}, High: ${bar.high:.2f}, Low: ${bar.low:.2f}, Close: ${bar.close:.2f} Volume: {bar.volume:,}, {time_label}: {bar.timestamp.strftime(time_format)} """ def _format_quote_data(quote) -> str: if not quote: return "" return f"""Latest Quote: Bid: ${quote.bid_price:.2f} x {quote.bid_size}, Ask: ${quote.ask_price:.2f} x {quote.ask_size} Timestamp: {quote.timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')} """ def _format_trade_data(trade) -> str: if not trade: return "" optional_fields: List[str] = [] if hasattr(trade, "exchange") and trade.exchange: optional_fields.append(f"Exchange: {trade.exchange}") if hasattr(trade, "conditions") and trade.conditions: optional_fields.append(f"Conditions: {trade.conditions}") if hasattr(trade, "id") and trade.id: optional_fields.append(f"ID: {trade.id}") optional_str = f", {', '.join(optional_fields)}" if optional_fields else "" return f"""Latest Trade: Price: ${trade.price:.2f}, Size: {trade.size}{optional_str} Timestamp: {trade.timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')} """ def _parse_expiration_expression(expression: str) -> Dict[str, Any]: expression = expression.strip().lower() week_pattern = r"week\s+of\s+(\w+)\s+(\d{1,2}),?\s+(\d{4})" week_match = re.search(week_pattern, expression) if week_match: month_name, day_str, year_str = week_match.groups() try: month_num = _month_name_to_number(month_name) day = int(day_str) year = int(year_str) anchor_date = datetime(year, month_num, day).date() days_since_monday = anchor_date.weekday() week_start = anchor_date - timedelta(days=days_since_monday) week_end = week_start + timedelta(days=4) return { "expiration_date_gte": week_start, "expiration_date_lte": week_end, "description": f"week of {month_name.title()} {day}, {year}", } except (ValueError, AttributeError) as e: return {"error": f"Invalid date in expression: {str(e)}"} month_pattern = r"month\s+of\s+(\w+)\s+(\d{4})" month_match = re.search(month_pattern, expression) if month_match: month_name, year_str = month_match.groups() try: month_num = _month_name_to_number(month_name) year = int(year_str) start_date = datetime(year, month_num, 1).date() end_date = ( datetime(year + 1, 1, 1).date() - timedelta(days=1) if month_num == 12 else datetime(year, month_num + 1, 1).date() - timedelta(days=1) ) return { "expiration_date_gte": start_date, "expiration_date_lte": end_date, "description": f"month of {month_name.title()} {year}", } except (ValueError, AttributeError) as e: return {"error": f"Invalid month/year in expression: {str(e)}"} date_pattern = r"(\w+)\s+(\d{1,2}),?\s+(\d{4})" date_match = re.search(date_pattern, expression) if date_match: month_name, day_str, year_str = date_match.groups() try: month_num = _month_name_to_number(month_name) day = int(day_str) year = int(year_str) specific_date = datetime(year, month_num, day).date() return { "expiration_date": specific_date, "description": f"{month_name.title()} {day}, {year}", } except (ValueError, AttributeError) as e: return {"error": f"Invalid date in expression: {str(e)}"} return { "error": "Unable to parse expression '{expression}'. Supported formats: 'week of September 7, 2025', 'month of December 2025', 'September 7, 2025'", } def _validate_option_order_inputs(legs: List[Dict[str, Any]], quantity: int, time_in_force: Union[str, TimeInForce]) -> Optional[str]: if not legs: return "Error: No option legs provided" if len(legs) > 4: return "Error: Maximum of 4 legs allowed for option orders" if quantity <= 0: return "Error: Quantity must be positive" if isinstance(time_in_force, str): if time_in_force.lower() != "day": return "Error: Only 'day' time_in_force is supported for options trading" elif isinstance(time_in_force, TimeInForce): if time_in_force != TimeInForce.DAY: return "Error: Only DAY time_in_force is supported for options trading" else: return f"Error: Invalid time_in_force type: {type(time_in_force)}. Must be string or TimeInForce enum." return None def _convert_order_class_string(order_class: Optional[Union[str, OrderClass]]) -> Union[OrderClass, str]: if order_class is None: return order_class if isinstance(order_class, OrderClass): return order_class if isinstance(order_class, str): order_class_upper = order_class.upper() class_mapping = { "SIMPLE": OrderClass.SIMPLE, "BRACKET": OrderClass.BRACKET, "OCO": OrderClass.OCO, "OTO": OrderClass.OTO, "MLEG": OrderClass.MLEG, } if order_class_upper in class_mapping: return class_mapping[order_class_upper] else: return f"Invalid order class: {order_class}. Must be one of: simple, bracket, oco, oto, mleg" else: return f"Invalid order class type: {type(order_class)}. Must be string or OrderClass enum." def _process_option_legs(legs: List[Dict[str, Any]]) -> Union[List[OptionLegRequest], str]: order_legs: List[OptionLegRequest] = [] for leg in legs: if not isinstance(leg["ratio_qty"], int) or leg["ratio_qty"] <= 0: return f"Error: Invalid ratio_qty for leg {leg['symbol']}. Must be positive integer." if leg["side"].lower() == "buy": order_side = OrderSide.BUY elif leg["side"].lower() == "sell": order_side = OrderSide.SELL else: return f"Invalid order side: {leg['side']}. Must be 'buy' or 'sell'." order_legs.append(OptionLegRequest(symbol=leg["symbol"], side=order_side, ratio_qty=leg["ratio_qty"])) return order_legs def _create_option_market_order_request( order_legs: List[OptionLegRequest], order_class: OrderClass, quantity: int, time_in_force: TimeInForce, extended_hours: bool, ) -> MarketOrderRequest: if order_class == OrderClass.MLEG: return MarketOrderRequest( qty=quantity, order_class=order_class, time_in_force=time_in_force, extended_hours=extended_hours, client_order_id=f"mcp_opt_{int(time.time())}", type=OrderType.MARKET, legs=order_legs, ) else: return MarketOrderRequest( symbol=order_legs[0].symbol, qty=quantity, side=order_legs[0].side, order_class=order_class, time_in_force=time_in_force, extended_hours=extended_hours, client_order_id=f"mcp_opt_{int(time.time())}", type=OrderType.MARKET, ) def _format_option_order_response(order: Order, order_class: OrderClass, order_legs: List[OptionLegRequest]) -> str: result = f""" Option Market Order Placed Successfully: -------------------------------------- Order ID: {order.id} Client Order ID: {order.client_order_id} Order Class: {order.order_class} Order Type: {order.type} Time In Force: {order.time_in_force} Status: {order.status} Quantity: {order.qty} Created At: {order.created_at} Updated At: {order.updated_at} """ if order_class == OrderClass.MLEG and order.legs: result += "\nLegs:\n" for leg in order.legs: result += f""" Symbol: {leg.symbol} Side: {leg.side} Ratio Quantity: {leg.ratio_qty} Status: {leg.status} Asset Class: {leg.asset_class} Created At: {leg.created_at} Updated At: {leg.updated_at} Filled Price: {leg.filled_avg_price if hasattr(leg, 'filled_avg_price') else 'Not filled'} Filled Time: {leg.filled_at if hasattr(leg, 'filled_at') else 'Not filled'} ------------------------- """ else: result += f""" Symbol: {order.symbol} Side: {order_legs[0].side} Filled Price: {order.filled_avg_price if hasattr(order, 'filled_avg_price') else 'Not filled'} Filled Time: {order.filled_at if hasattr(order, 'filled_at') else 'Not filled'} ------------------------- """ return result def _analyze_option_strategy_type(order_legs: List[OptionLegRequest], order_class: OrderClass) -> Tuple[bool, bool, bool]: is_short_straddle = False is_short_strangle = False is_short_calendar = False if order_class == OrderClass.MLEG and len(order_legs) == 2: both_short = order_legs[0].side == OrderSide.SELL and order_legs[1].side == OrderSide.SELL if both_short: if order_legs[0].symbol.split("C")[0] == order_legs[1].symbol.split("P")[0]: is_short_straddle = True else: is_short_strangle = True leg1_type = "C" if "C" in order_legs[0].symbol else "P" leg2_type = "C" if "C" in order_legs[1].symbol else "P" if leg1_type == "C" and leg2_type == "C": leg1_exp = order_legs[0].symbol.split(leg1_type)[1][:6] leg2_exp = order_legs[1].symbol.split(leg2_type)[1][:6] if leg1_exp != leg2_exp: is_short_calendar = True is_short_strangle = False return is_short_straddle, is_short_strangle, is_short_calendar def _get_short_straddle_error_message() -> str: return """ Error: Account not eligible to trade short straddles. This error occurs because short straddles require Level 4 options trading permission. A short straddle involves: - Selling a call option - Selling a put option - Both options have the same strike price and expiration Required Account Level: - Level 4 options trading permission is required - Please contact your broker to upgrade your account level if needed Alternative Strategies: - Consider using a long straddle instead - Use a debit spread strategy - Implement a covered call or cash-secured put """ def _get_short_strangle_error_message() -> str: return """ Error: Account not eligible to trade short strangles. This error occurs because short strangles require Level 4 options trading permission. A short strangle involves: - Selling an out-of-the-money call option - Selling an out-of-the-money put option - Both options have the same expiration Required Account Level: - Level 4 options trading permission is required - Please contact your broker to upgrade your account level if needed Alternative Strategies: - Consider using a long strangle instead - Use a debit spread strategy - Implement a covered call or cash-secured put """ def _get_short_calendar_error_message() -> str: return """ Error: Account not eligible to trade short calendar spreads. This error occurs because short calendar spreads require Level 4 options trading permission. A short calendar spread involves: - Selling a longer-term option - Selling a shorter-term option - Both options have the same strike price Required Account Level: - Level 4 options trading permission is required - Please contact your broker to upgrade your account level if needed Alternative Strategies: - Consider using a long calendar spread instead - Use a debit spread strategy - Implement a covered call or cash-secured put """ def _get_uncovered_options_error_message() -> str: return """ Error: Account not eligible to trade uncovered option contracts. This error occurs when attempting to place an order that could result in an uncovered position. Common scenarios include: 1. Selling naked calls 2. Calendar spreads where the short leg expires after the long leg 3. Other strategies that could leave uncovered positions Required Account Level: - Level 4 options trading permission is required for uncovered options - Please contact your broker to upgrade your account level if needed Alternative Strategies: - Consider using covered calls instead of naked calls - Use debit spreads instead of calendar spreads - Ensure all positions are properly hedged """ def _handle_option_api_error(error_message: str, order_legs: List[OptionLegRequest], order_class: OrderClass) -> str: if "40310000" in error_message and "not eligible to trade uncovered option contracts" in error_message: is_short_straddle, is_short_strangle, is_short_calendar = _analyze_option_strategy_type(order_legs, order_class) if is_short_straddle: return _get_short_straddle_error_message() elif is_short_strangle: return _get_short_strangle_error_message() elif is_short_calendar: return _get_short_calendar_error_message() else: return _get_uncovered_options_error_message() elif "403" in error_message: return f""" Error: Permission denied for option trading. Possible reasons: 1. Insufficient account level for the requested strategy 2. Account restrictions on option trading 3. Missing required permissions Please check: 1. Your account's option trading level 2. Any specific restrictions on your account 3. Required permissions for the strategy you're trying to implement Original error: {error_message} """ else: return f""" Error placing option order: {error_message} Please check: 1. All option symbols are valid 2. Your account has sufficient buying power 3. The market is open for trading 4. Your account has the required permissions """

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alpacahq/alpaca-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server