diff --git a/.github/workflows/release-mcp.yml b/.github/workflows/release-mcp.yml deleted file mode 100644 index 1c39482a4..000000000 --- a/.github/workflows/release-mcp.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Release MCP - -on: - push: - branches: - - main - - build-test - -jobs: - build: - strategy: - fail-fast: true - matrix: - settings: - - host: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - cross: true - bin: longbridge-mcp - - host: ubuntu-22.04 - target: aarch64-unknown-linux-gnu - cross: true - bin: longbridge-mcp - - host: windows-latest - target: x86_64-pc-windows-msvc - bin: longbridge-mcp - bin_suffix: .exe - - host: macos-latest - target: x86_64-apple-darwin - bin: longbridge-mcp - - host: macos-latest - target: aarch64-apple-darwin - bin: longbridge-mcp - runs-on: ${{ matrix.settings.host }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: rustfmt, clippy - targets: ${{ matrix.settings.target }} - - - name: Build - if: ${{ !matrix.settings.cross }} - run: | - cargo build --release -p longbridge-mcp --target ${{ matrix.settings.target }} - - - name: Install latest cross binary - if: ${{ matrix.settings.cross }} - uses: st3iny/install-cross-binary@v3 - - - name: Build with Cross - if: ${{ matrix.settings.cross }} - run: | - cross build --release -p longbridge-mcp --target ${{ matrix.settings.target }} - - - name: Archive artifact - if: ${{ !contains(matrix.settings.target, 'windows') }} - run: | - mkdir dist/ - cd target/${{ matrix.settings.target }}/release - tar czvf longbridge-mcp-${{ matrix.settings.target }}.tar.gz longbridge-mcp - cd ../../.. - mv target/${{ matrix.settings.target }}/release/longbridge-mcp-${{ matrix.settings.target }}.tar.gz dist/ - - - name: Archive artifact (Windows) - if: ${{ contains(matrix.settings.target, 'windows') }} - run: | - mkdir dist/ - cd target/${{ matrix.settings.target }}/release - Compress-Archive -Path longbridge-mcp.exe -DestinationPath longbridge-mcp-${{ matrix.settings.target }}.zip - cd ../../.. - mv target/${{ matrix.settings.target }}/release/longbridge-mcp-${{ matrix.settings.target }}.zip dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: longbridge-mcp-${{ matrix.settings.target }} - path: dist/ diff --git a/Cargo.toml b/Cargo.toml index 3db89307b..716371a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["rust", "python", "nodejs", "java", "c", "mcp"] +members = ["rust", "python", "nodejs", "java", "c"] [workspace.package] version = "4.0.5" @@ -59,12 +59,7 @@ napi = { version = "3.8.3", default-features = false } napi-derive = "3.5.2" napi-build = "2.3.1" chrono = "0.4.41" -poem-mcpserver = "0.3.1" -poem-mcpserver-macros = "0.3.1" poem = "3.1.12" -schemars = "1.0.4" -clap = "4.5.45" -dotenvy = "0.15.7" jni = "0.21.1" proc-macro2 = "1.0.101" quote = "1.0.40" diff --git a/mcp/Cargo.toml b/mcp/Cargo.toml deleted file mode 100644 index cbdf98cf8..000000000 --- a/mcp/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "longbridge-mcp" -version.workspace = true -edition.workspace = true - -[dependencies] -longbridge.workspace = true - -poem-mcpserver = { workspace = true, features = ["streamable-http"] } -poem = { workspace = true, features = ["sse"] } -serde = { workspace = true, features = ["derive"] } -schemars = { workspace = true, features = ["rust_decimal1"] } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] } -clap = { workspace = true, features = ["derive"] } -dotenvy.workspace = true -time = { workspace = true, features = ["formatting", "parsing"] } -tracing-subscriber.workspace = true -serde_json.workspace = true -tracing-appender.workspace = true -tracing.workspace = true diff --git a/mcp/README.md b/mcp/README.md deleted file mode 100644 index e4d16d647..000000000 --- a/mcp/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Longbridge MCP - -A [MCP](https://modelcontextprotocol.io/introduction) server implementation for [Longbridge OpenAPI](https://open.longbridge.com), provides real-time stock market data, provides AI access analysis and trading capabilities through MCP. - -## Documentation - -- Longbridge OpenAPI: https://open.longbridge.com/en/ -- SDK docs: https://longbridge.github.io/openapi - -## Features - -- Trading - Create, amend, cancel orders, query today’s/past orders and transaction details, etc. -- Quotes - Real-time quotes, acquisition of historical quotes, etc. -- Portfolio - Real-time query of the account assets, positions, funds - -## Installation - -### macOS or Linux - -Run script to install: - -```bash -curl -sSL https://raw.githubusercontent.com/longbridge/openapi/refs/heads/main/mcp/install | bash -``` - -### Windows - -Download the latest binary from the [Releases](https://github.com/longbridge/openapi/releases/tag/longbridge-mcp-0.1.0) page. - -## Example Prompts - -Once you done server setup, and connected, you can talk with AI: - -- What's the current price of AAPL and TSLA stock? -- How has Tesla performed over the past month? -- Show me the current values of major market indices. -- What's the stock price history for TSLA, AAPL over the last year? -- Compare the performance of TSLA, AAPL and NVDA over the past 3 months. -- Generate a portfolio performance chart for my holding stocks, and return me with data table and pie chart (Just return result no code). -- Check the price of the stocks I hold today, and if they fall/rise by more than 3%, sell(If fall, buy if rise) 1/3 at the market price. - -## Usage - -### Use in Cursor - -To configure Longbridge MCP in Cursor: - -- Open Cursor Settings -- Go to Features > MCP Servers -- Click `+ Add New MCP Server` -- Enter the following: - - Name: `longbridge-mcp` (or your preferred name) - - Type: `command` - - Command: `env LONGBRIDGE_APP_KEY=your-app-key LONGBRIDGE_APP_SECRET=your-app-secret LONGBRIDGE_ACCESS_TOKEN=your-access-token longbridge-mcp` - -If you are using Windows, replace command with `cmd /c "set LONGBRIDGE_APP_KEY=your-app-key && set LONGBRIDGE_APP_SECRET=your-app-secret && set LONGBRIDGE_ACCESS_TOKEN=your-access-token && longbridge-mcp"` - -Or use this config: - -```json -{ - "mcpServers": { - "longbridge-mcp": { - "command": "/usr/local/bin/longbridge-mcp", - "env": { - "LONGBRIDGE_APP_KEY": "your-app-key", - "LONGBRIDGE_APP_SECRET": "your-app-secret", - "LONGBRIDGE_ACCESS_TOKEN": "your-access-token" - } - } - } -} -``` - -### Use in Cherry Studio - -To configure Longbridge MCP in Cherry Studio: - -- Go to Settings > MCP Servers -- Click `+ Add Server` -- Enter the following: - - Name: `longbridge-mcp` (or your preferred name) - - Type: `STDIO` - - Command: `env LONGBRIDGE_APP_KEY=your-app-key LONGBRIDGE_APP_SECRET=your-app-secret LONGBRIDGE_ACCESS_TOKEN=your-access-token longbridge-mcp` - -If you are using Windows, replace command with `cmd /c "set LONGBRIDGE_APP_KEY=your-app-key && set LONGBRIDGE_APP_SECRET=your-app-secret && set LONGBRIDGE_ACCESS_TOKEN=your-access-token && longbridge-mcp"` - -## Running as a SSE server - -```bash -env LONGBRIDGE_APP_KEY=your-app-key LONGBRIDGE_APP_SECRET=your-app-secret LONGBRIDGE_ACCESS_TOKEN=your-access-token longbridge-mcp --sse -``` - -Default bind address is `127.0.0.1:8000`, you can change it by using the `--bind` flag: - -```bash -longbridge-mcp --sse --bind 127.0.0.1:3000 -``` - -## Configuration - -### Readonly mode - -To run the server in read-only mode, set the flag `--readonly`: - -```bash -longbridge-mcp --readonly -``` - -This will prevent the server from submitting orders to the exchange. - -### Enable logging - -To enable logging, set the flag `--log-dir` to the directory where you want to store the logs: - -```bash -longbridge-mcp --log-dir /path/to/log/dir -``` diff --git a/mcp/install b/mcp/install deleted file mode 100755 index f5542833d..000000000 --- a/mcp/install +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -set -u - -repo='longbridge/openapi' -app_name='Longbridge MCP' -bin_name='longbridge-mcp' -tmpdir=.tmp_install - -if [[ ${OS:-} = Windows_NT ]]; then - echo 'error: Please install using Windows Subsystem for Linux' - exit 1 -fi - -# Check if unzip is installed -type unzip > /dev/null || { echo "unzip: not found"; exit 1; } - -type curl > /dev/null || { echo "curl: not found"; exit 1; } - -# Reset -Color_Off='' -Color_Red='' -Color_Green='' -Color_Dim='' - -if [[ -t 1 ]]; then - # Reset - Color_Off='\033[0m' # Text Reset - - # Regular Colors - Color_Red='\033[0;31m' # Red - Color_Green='\033[0;32m' # Green - Color_Dim='\033[0;2m' # White -fi - -error() { - echo -e "${Color_Red}$@ ${Color_Off}" >&2 - exit 1 -} - -info() { - echo -e "${Color_Dim}$@ ${Color_Off}" -} - -success() { - echo -e "${Color_Green}$@ ${Color_Off}" -} - -# request github api, and check if the response is ok -fetch_github_api() { - url="$1" - body=$(curl "$url") - # Show API Rate limit error if body contains "API rate limit exceeded" - if echo "$body" | grep -q "API rate limit exceeded"; then - error "$url\nGitHub API rate limit exceeded.\n----------------------------------\n$body\n" - exit 1 - fi - echo "$body" -} - -get_mcp_release() { - fetch_github_api "https://api.github.com/repos/$repo/releases" | # Get latest release from GitHub api - grep '"tag_name":' | # Get tag line - sed -E 's/.*"([^"]+)".*/\1/' | # Pluck JSON value - grep "longbridge-mcp" | head -n 1 -} - -get_version() { - version="latest" - - # if version is empty, exit - if test -z "$version"; then - error "Fetch version failed, please check your network." - exit 1 - fi -} - -get_platform() { - platform="$(uname | tr "[A-Z]" "[a-z]")" # Linux => linux - platform_suffix="" - if [ "$platform" = "darwin" ]; then - platform_suffix="apple-darwin" - fi - - if [ "$platform" = "linux" ]; then - platform_suffix="unknown-linux-gnu" - fi - - if [ "$platform" = "windows" ]; then - platform_suffix="pc-windows-msvc" - fi -} - -get_arch() { - arch="$(uname -m)" - - if [ "$arch" = "x86_64" ]; then - arch="x86_64" - elif [ "$arch" = "arm64" ]; then - arch="aarch64" - fi -} - -install() { - name_suffix="$arch-$platform_suffix" - info "Downloading $bin_name@$version ($name_suffix)..." - if [ "$version" = "latest" ]; then - download_url=https://github.com/$repo/releases/latest/download/$bin_name-$name_suffix.tar.gz - else - download_url=https://github.com/$repo/releases/download/$version/$bin_name-$name_suffix.tar.gz - fi - - mkdir -p $tmpdir && cd $tmpdir - if ! curl --fail --progress-bar -Lo $bin_name.tar.gz $download_url; then - error "${Color_Red}Download failed, please check your network.\n${download_url}${Color_Off}" - exit 1 - fi - - mkdir -p out - tar -xzf $bin_name.tar.gz -C ./out && rm $bin_name.tar.gz - ls -lh out - sudo cp ./out/$bin_name /usr/local/bin/ - cd .. && rm -Rf $tmpdir -} - - -get_version $@ -get_platform $@ -get_arch $@ -install $@ - -# Test install -if [ -x "$(command -v $bin_name)" ]; then - info "" - success "$app_name was installed successfully to \`/usr/local/bin/$bin_name\`." - info "Run \`$bin_name -h\` to get help." - info "" -else - error "${Color_Red}$bin_name is not installed${Color_Off}" -fi diff --git a/mcp/src/main.rs b/mcp/src/main.rs deleted file mode 100644 index ff843dd6a..000000000 --- a/mcp/src/main.rs +++ /dev/null @@ -1,99 +0,0 @@ -mod server; - -use std::{path::PathBuf, sync::Arc}; - -use clap::Parser; -use longbridge::{Config, QuoteContext, TradeContext, content::ContentContext}; -use poem::{EndpointExt, Route, Server, listener::TcpListener, middleware::Cors}; -use poem_mcpserver::{McpServer, stdio::stdio, streamable_http}; -use server::Longbridge; -use tracing_appender::rolling::{RollingFileAppender, Rotation}; - -#[derive(Parser)] -struct Cli { - /// Use Streamable-HTTP transport - #[clap(long)] - http: bool, - /// Bind address for the SSE server. - #[clap(long, default_value = "127.0.0.1:8000")] - bind: String, - /// Log directory - #[clap(long)] - log_dir: Option, - /// Read-only mode - /// - /// This mode is used to prevent submitting orders to the exchange. - #[clap(long, default_value_t = false)] - readonly: bool, -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - dotenvy::dotenv().ok(); - - let cli = Cli::parse(); - - if let Some(log_dir) = cli.log_dir { - let file_appender = - RollingFileAppender::new(Rotation::DAILY, log_dir, "longbridge-mcp.log"); - tracing_subscriber::fmt() - .with_writer(file_appender) - .with_ansi(false) - .init(); - } - - let config = Arc::new( - Config::from_apikey_env() - .inspect_err(|err| tracing::error!(error = %err, "failed to load config"))? - .dont_print_quote_packages(), - ); - let (quote_context, _) = QuoteContext::new(config.clone()); - let (trade_context, _) = TradeContext::new(config.clone()); - let content_context = ContentContext::new(config.clone()); - let readonly = cli.readonly; - - if !cli.http { - tracing::info!("Starting MCP server with stdio transport"); - let server = create_mcp_server(quote_context, trade_context, content_context, readonly); - stdio(server).await?; - } else { - tracing::info!( - "Starting MCP server with Streamable-HTTP transport, listening on {}", - cli.bind - ); - let listener = TcpListener::bind(&cli.bind); - let app = Route::new() - .at( - "/", - streamable_http::endpoint(move |_| { - create_mcp_server( - quote_context.clone(), - trade_context.clone(), - content_context.clone(), - readonly, - ) - }), - ) - .with(Cors::new()); - Server::new(listener).run(app).await?; - } - - Ok(()) -} - -fn create_mcp_server( - quote_context: QuoteContext, - trade_context: TradeContext, - content_context: ContentContext, - readonly: bool, -) -> McpServer { - let mut server = McpServer::new().tools(Longbridge::new( - quote_context, - trade_context, - content_context, - )); - if readonly { - server = server.disable_tools(["submit_order"]); - } - server -} diff --git a/mcp/src/server.rs b/mcp/src/server.rs deleted file mode 100644 index 44d50d2a1..000000000 --- a/mcp/src/server.rs +++ /dev/null @@ -1,597 +0,0 @@ -use longbridge::{ - Decimal, Error, Market, QuoteContext, TradeContext, - content::{ContentContext, NewsItem, TopicItem}, - quote::{ - AdjustType, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, - HistoryMarketTemperatureResponse, MarketTemperature, MarketTradingDays, OptionQuote, - ParticipantInfo, Period, SecurityBrokers, SecurityDepth, SecurityQuote, SecurityStaticInfo, - StrikePriceInfo, Trade, TradeSessions, - }, - trade::{ - AccountBalance, FundPositionChannel, GetHistoryOrdersOptions, MarginRatio, Order, - OrderDetail, OrderSide, OrderType, OutsideRTH, StockPositionChannel, SubmitOrderOptions, - SubmitOrderResponse, TimeInForceType, - }, -}; -use poem_mcpserver::{ - Tools, - content::{Json, Text}, -}; -use time::{ - Date, OffsetDateTime, format_description::BorrowedFormatItem, macros::format_description, -}; - -const DATE_FORMAT: &[BorrowedFormatItem] = format_description!("[year]-[month]-[day]"); - -pub(crate) struct Longbridge { - quote_context: QuoteContext, - trade_context: TradeContext, - content_context: ContentContext, -} - -impl Longbridge { - #[inline] - pub(crate) fn new( - quote_context: QuoteContext, - trade_context: TradeContext, - content_context: ContentContext, - ) -> Self { - Self { - quote_context, - trade_context, - content_context, - } - } -} - -/// Longbridge OpenAPI SDK. -#[Tools] -impl Longbridge { - /// Get current time. - async fn now(&self) -> Text { - Text( - OffsetDateTime::now_utc() - .format(&time::format_description::well_known::Rfc3339) - .unwrap(), - ) - } - - /// Get basic information of the securities. - async fn static_info( - &self, - /// A list of security symbols. (e.g. ["700.HK", "AAPL.US", "000001.SH", - /// "D05.SG"]) - symbols: Vec, - ) -> Result>, Error> { - Ok(self - .quote_context - .static_info(symbols) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the latest price of the securities. - async fn quote(&self, symbols: Vec) -> Result>, Error> { - Ok(self - .quote_context - .quote(symbols) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the latest price of option securities. - async fn option_quote( - &self, - /// A list of option symbols. (e.g. ["AAPL230317P160000.US", - /// "AAPL230317C160000.US"]) Maximum 500 symbols per request. - symbols: Vec, - ) -> Result>, Error> { - Ok(self - .quote_context - .option_quote(symbols) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the latest depth of the securities. - async fn depth(&self, symbol: String) -> Result, Error> { - Ok(Json(self.quote_context.depth(symbol).await?)) - } - - /// Get the latest trades of the securities. - async fn trades( - &self, - symbol: String, - /// max 1000 - count: usize, - ) -> Result>, Error> { - Ok(self - .quote_context - .trades(symbol, count) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the latest n candlesticks of the security. - async fn candlesticks( - &self, - symbol: String, - /// 1m, 2m, 3m, 5m, 10m, 15m, 20m, 30m, 45m, 60m, 120m, 180m, 240m, day, - /// week, month, quarter, year - period: String, - /// last n candlesticks (max: 1000) - count: usize, - /// whether to adjust the historical data for splits, dividends, etc. - /// (required) - forward_adjust: bool, - /// trade sessions (required) - /// - intraday: regular trading hours - /// - all: all trading hours (intraday, pre, post, overnight) - trade_sessions: String, - ) -> Result>, Error> { - let period = match period.as_str() { - "1m" => Period::OneMinute, - "2m" => Period::TwoMinute, - "3m" => Period::ThreeMinute, - "5m" => Period::FiveMinute, - "10m" => Period::TenMinute, - "15m" => Period::FifteenMinute, - "20m" => Period::TwentyMinute, - "30m" => Period::ThirtyMinute, - "45m" => Period::FortyFiveMinute, - "60m" => Period::SixtyMinute, - "120m" => Period::TwoHour, - "180m" => Period::ThreeHour, - "240m" => Period::FourHour, - "day" => Period::Day, - "week" => Period::Week, - "month" => Period::Month, - "quarter" => Period::Quarter, - "year" => Period::Year, - _ => { - return Err(Error::ParseField { - name: "market", - error: "invalid period".to_string(), - }); - } - }; - let trade_sessions = match trade_sessions.as_str() { - "intraday" => TradeSessions::Intraday, - "all" => TradeSessions::All, - _ => { - return Err(Error::ParseField { - name: "market", - error: "invalid trade_sessions".to_string(), - }); - } - }; - - Ok(self - .quote_context - .candlesticks( - symbol, - period, - count, - if forward_adjust { - AdjustType::ForwardAdjust - } else { - AdjustType::NoAdjust - }, - trade_sessions, - ) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the trading days between the specified dates. - /// - /// The results include the `start_date` and `end_date`. - async fn trading_days( - &self, - /// Market code. (e.g. "HK", "US", "CN", "SG") - market: String, - /// Start date of the trading days. (Format: "yyyy-mm-dd") - start_date: String, - /// End date of the trading days. (Format: "yyyy-mm-dd") - end_date: String, - ) -> Result, Error> { - let market = market.parse::().map_err(|err| Error::ParseField { - name: "market", - error: err.to_string(), - })?; - let start_date = - Date::parse(&start_date, DATE_FORMAT).map_err(|err| Error::ParseField { - name: "start_date", - error: err.to_string(), - })?; - let end_date = Date::parse(&end_date, DATE_FORMAT).map_err(|err| Error::ParseField { - name: "end_date", - error: err.to_string(), - })?; - - Ok(Json( - self.quote_context - .trading_days(market, start_date, end_date) - .await?, - )) - } - - /// Returns the real-time broker queue data of security. - async fn broker_queue(&self, symbol: String) -> Result, Error> { - Ok(Json(self.quote_context.brokers(symbol).await?)) - } - - /// Returns the participants information. - async fn broker_info(&self) -> Result>, Error> { - Ok(self - .quote_context - .participants() - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Returns the option chain list of the security. - async fn option_chain_list(&self, symbol: String) -> Result>, Error> { - Ok(self - .quote_context - .option_chain_expiry_date_list(symbol) - .await? - .into_iter() - .map(|date| { - Text( - date.format(format_description!("[year]-[month]-[day]")) - .unwrap(), - ) - }) - .collect::>()) - } - - /// Returns the option chain information of the security. - async fn option_chain_info( - &self, - symbol: String, - /// format: "yyyy-mm-dd" - expiry_date: String, - ) -> Result>, Error> { - let expiry_date = Date::parse(&expiry_date, format_description!("[year]-[month]-[day]")) - .map_err(|err| Error::ParseField { - name: "expiry_date", - error: err.to_string(), - })?; - Ok(self - .quote_context - .option_chain_info_by_date(symbol, expiry_date) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - // Returns the capital flow of the security. - async fn capital_flow(&self, symbol: String) -> Result>, Error> { - Ok(self - .quote_context - .capital_flow(symbol) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Returns the capital distribution of the security. - async fn capital_distribution( - &self, - symbol: String, - ) -> Result, Error> { - Ok(Json(self.quote_context.capital_distribution(symbol).await?)) - } - - /// Returns the market temperature of the specified market. - async fn current_market_temperature( - &self, - /// Market code. (e.g. "HK", "US", "CN", "SG") - market: String, - ) -> Result, Error> { - let market = market.parse::().map_err(|err| Error::ParseField { - name: "market", - error: err.to_string(), - })?; - Ok(Json(self.quote_context.market_temperature(market).await?)) - } - - /// Returns the historical market temperature of the specified market. - /// - /// includes the `start` and `end` dates. - async fn history_market_temperature( - &self, - /// Market code. (e.g. "HK", "US", "CN", "SG") - market: String, - /// format: "yyyy-mm-dd" - start: String, - /// format: "yyyy-mm-dd" - end: String, - ) -> Result, Error> { - let market = market.parse::().map_err(|err| Error::ParseField { - name: "market", - error: err.to_string(), - })?; - let start = Date::parse(&start, DATE_FORMAT).map_err(|err| Error::ParseField { - name: "start", - error: err.to_string(), - })?; - let end = Date::parse(&end, DATE_FORMAT).map_err(|err| Error::ParseField { - name: "end", - error: err.to_string(), - })?; - Ok(Json( - self.quote_context - .history_market_temperature(market, start, end) - .await?, - )) - } - - /// Get the account balance. - async fn account_balance(&self) -> Result>, Error> { - Ok(self - .trade_context - .account_balance(None) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Returns the stock positions. - async fn stock_positions(&self) -> Result>, Error> { - Ok(self - .trade_context - .stock_positions(None) - .await? - .channels - .into_iter() - .map(Json) - .collect::>()) - } - - /// Returns the fund positions. - async fn fund_positions(&self) -> Result>, Error> { - Ok(self - .trade_context - .fund_positions(None) - .await? - .channels - .into_iter() - .map(Json) - .collect::>()) - } - - /// Returns the initial margin ratio, maintain the margin ratio and - /// strengthen the margin ratio of stocks. - async fn magin_ratio(&self, symbol: String) -> Result, Error> { - Ok(Json(self.trade_context.margin_ratio(symbol).await?)) - } - - /// Submit an order. - #[allow(clippy::too_many_arguments)] - async fn submit_order( - &self, - symbol: String, - /// Order type - /// LO: Limit Order - /// ELO: Enhanced Limit Order - /// MO: Market Order - /// AO: At-auction Order - /// ALO: At-auction Limit Order - /// ODD: Odd Lots Order - /// LIT: Limit If Touched - /// MIT: Market If Touched - /// TSLPAMT: Trailing Limit If Touched (Trailing Amount) - /// TSLPPCT: Trailing Limit If Touched (Trailing Percent) - /// SLO: Special Limit Order. Not Support Replace Order. - order_type: String, - /// for LO, ELO, ALO, ODD, LIT - submitted_price: Option, - submitted_quantity: Decimal, - /// for LIT, MIT - trigger_price: Option, - /// for TSLPAMT, TSLPPCT - limit_offset: Option, - /// for TSLPAMT - trailing_amount: Option, - /// for TSLPPCT (0-1) - trailing_percent: Option, - /// format: "yyyy-mm-dd" - expire_date: Option, - /// Side of the order (Buy or Sell) - side: String, - /// - RTH_ONLY: regular trading hour only - /// - ANY_TIME: any time - /// - OVERNIGHT: overnight - outside_rth: Option, - /// - Day: Day Order - /// - GTC: Good Till Cancel - /// - GTD: Good Till Date - time_in_force: String, - /// Limit depth level - limit_depth_level: Option, - /// Trigger count - trigger_count: Option, - /// Monitor price - monitor_price: Option, - ) -> Result, Error> { - let mut opts = SubmitOrderOptions::new( - symbol, - order_type - .parse::() - .map_err(|err| Error::ParseField { - name: "order_type", - error: err.to_string(), - })?, - side.parse::().map_err(|err| Error::ParseField { - name: "side", - error: err.to_string(), - })?, - submitted_quantity, - time_in_force - .parse::() - .map_err(|err| Error::ParseField { - name: "time_in_force", - error: err.to_string(), - })?, - ); - - if let Some(submitted_price) = submitted_price { - opts = opts.submitted_price(submitted_price); - } - if let Some(trigger_price) = trigger_price { - opts = opts.trigger_price(trigger_price); - } - if let Some(limit_offset) = limit_offset { - opts = opts.limit_offset(limit_offset); - } - if let Some(trailing_amount) = trailing_amount { - opts = opts.trailing_amount(trailing_amount); - } - if let Some(trailing_percent) = trailing_percent { - opts = opts.trailing_percent(trailing_percent); - } - - if let Some(expire_date) = expire_date { - opts = opts.expire_date( - Date::parse(&expire_date, format_description!("[year]-[month]-[day]")).map_err( - |err| Error::ParseField { - name: "expire_date", - error: err.to_string(), - }, - )?, - ); - } - - if let Some(outside_rth) = outside_rth { - opts = opts.outside_rth(outside_rth.parse::().map_err(|err| { - Error::ParseField { - name: "outside_rth", - error: err.to_string(), - } - })?); - } - - if let Some(limit_depth_level) = limit_depth_level { - opts = opts.limit_depth_level(limit_depth_level); - } - if let Some(trigger_count) = trigger_count { - opts = opts.trigger_count(trigger_count); - } - if let Some(monitor_price) = monitor_price { - opts = opts.monitor_price(monitor_price); - } - - self.trade_context.submit_order(opts).await.map(Json) - } - - async fn cancel_order(&self, order_id: String) -> Result<(), Error> { - self.trade_context.cancel_order(order_id).await - } - - /// Get the order detail. - async fn order_detail(&self, order_id: String) -> Result, Error> { - Ok(Json(self.trade_context.order_detail(order_id).await?)) - } - - /// Get the current account's orders for the day. - async fn today_orders(&self) -> Result>, Error> { - Ok(self - .trade_context - .today_orders(None) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get the historical orders of the current account. - /// - /// does not include today's orders - async fn history_orders( - &self, - /// if not provided, default to all symbols - symbol: Option, - /// format: RFC3339 - start_at: String, - /// format: RFC3339 - end_at: String, - ) -> Result>, Error> { - let mut opts = GetHistoryOrdersOptions::new() - .start_at( - OffsetDateTime::parse(&start_at, &time::format_description::well_known::Rfc3339) - .map_err(|err| Error::ParseField { - name: "start_at", - error: err.to_string(), - })?, - ) - .end_at( - OffsetDateTime::parse(&end_at, &time::format_description::well_known::Rfc3339) - .map_err(|err| Error::ParseField { - name: "end_at", - error: err.to_string(), - })?, - ); - - if let Some(symbol) = symbol { - opts = opts.symbol(symbol); - } - - Ok(self - .trade_context - .history_orders(opts) - .await? - .into_iter() - .map(Json) - .collect::>()) - } - - /// Get news list for a stock symbol. - async fn news(&self, symbol: String) -> Result>, Error> { - Ok(self - .content_context - .news(symbol) - .await? - .into_iter() - .map(Json) - .collect()) - } - - /// Get discussion topics list for a stock symbol. - async fn topics(&self, symbol: String) -> Result>, Error> { - Ok(self - .content_context - .topics(symbol) - .await? - .into_iter() - .map(Json) - .collect()) - } - - /// Get filings list for a stock symbol. - async fn filings(&self, symbol: String) -> Result>, Error> { - Ok(self - .quote_context - .filings(symbol) - .await? - .into_iter() - .map(Json) - .collect()) - } -}