Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 3.5.8 (2026-04-24)
### New
- CLI 新增 `tigeropen quote scanner` 选股命令,支持 `--filter`(数值/累计/财务/标签筛选,预设 `gainers`/`losers`)、`--sort`、`--sort-dir`、`--limit` 参数
- 新增 Windows PowerShell 一键安装脚本 `install.ps1`(`irm .../install.ps1 | iex`);`install.sh` 在 MINGW/MSYS/Cygwin 环境下增加 PowerShell 安装提示
### Fix
- 修复 `_get_props_path()` 传入完整文件路径时文件名被替换为 `tiger_openapi_config.properties` 的问题,现在支持任意文件名
- 修复 `account assets` 命令渲染 `PortfolioAccount` 对象时报错的问题

## 3.5.7 (2026-03-25)
### Fix
- 修复 pyproject.toml 中 build-backend 使用 `setuptools.backends._legacy:_Backend` 导致旧版 pip/setuptools 安装失败的问题,改为标准的 `setuptools.build_meta`
Expand Down
158 changes: 158 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Tiger Open API Python SDK — Windows Installer (PowerShell)
#
# Usage:
# irm https://raw.githubusercontent.com/tigerfintech/openapi-python-sdk/master/install.ps1 | iex
#
# Options (via environment variables):
# $env:TIGEROPEN_INSTALL_METHOD = "uv" -- force install method (uv|pipx|pip)
# $env:TIGEROPEN_NO_MODIFY_PATH = "1" -- skip PATH modification

$ErrorActionPreference = "Stop"

# ─── Colors ──────────────────────────────────────────────────────────────────

function Write-Info { param($msg) Write-Host "info: $msg" -ForegroundColor Green }
function Write-Warn { param($msg) Write-Host "warn: $msg" -ForegroundColor Yellow }
function Write-Err { param($msg) Write-Host "error: $msg" -ForegroundColor Red; exit 1 }

function Has-Command { param($cmd) return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) }

# ─── Banner ──────────────────────────────────────────────────────────────────

Write-Host ""
Write-Host " Tiger Open API Python SDK Installer" -ForegroundColor Cyan
Write-Host " ─────────────────────────────────────"
Write-Host ""

# ─── Python Detection ────────────────────────────────────────────────────────

$PYTHON = $null
foreach ($candidate in @("python", "python3", "py")) {
if (Has-Command $candidate) {
$PYTHON = $candidate
break
}
}

if (-not $PYTHON) {
Write-Err "Python not found. Install Python 3.8+ first:`n https://www.python.org/downloads/`n Or: winget install Python.Python.3"
}

$PY_VERSION = & $PYTHON -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null
$PY_MAJOR = & $PYTHON -c "import sys; print(sys.version_info.major)" 2>$null
$PY_MINOR = & $PYTHON -c "import sys; print(sys.version_info.minor)" 2>$null

if (-not $PY_VERSION) { Write-Err "Could not determine Python version" }
if ([int]$PY_MAJOR -lt 3 -or ([int]$PY_MAJOR -eq 3 -and [int]$PY_MINOR -lt 8)) {
Write-Err "Python 3.8+ required, found $PY_VERSION"
}

Write-Info "Found Python $PY_VERSION ($PYTHON)"

# ─── Choose Install Method ───────────────────────────────────────────────────

$METHOD = $env:TIGEROPEN_INSTALL_METHOD

if (-not $METHOD) {
if (Has-Command "uv") { $METHOD = "uv" }
elseif (Has-Command "pipx") { $METHOD = "pipx" }
else { $METHOD = "pip" }
}

Write-Info "Install method: $METHOD"

# ─── Install ─────────────────────────────────────────────────────────────────

switch ($METHOD) {
"uv" {
if (-not (Has-Command "uv")) { Write-Err "uv not found. Install with: irm https://astral.sh/uv/install.ps1 | iex" }
Write-Info "Installing tigeropen via uv..."
& uv pip install tigeropen --upgrade
}
"pipx" {
if (-not (Has-Command "pipx")) { Write-Err "pipx not found. Install with: pip install pipx" }
Write-Info "Installing tigeropen via pipx..."
& pipx install tigeropen --force
}
"pip" {
Write-Info "Installing tigeropen via pip..."
& $PYTHON -m pip install tigeropen --upgrade
}
default {
Write-Err "Unknown install method: $METHOD (use uv, pipx, or pip)"
}
}

# ─── Verify Installation ─────────────────────────────────────────────────────

$SDK_VERSION = & $PYTHON -c "from tigeropen import __VERSION__; print(__VERSION__)" 2>$null
if (-not $SDK_VERSION) {
Write-Err "Installation failed — could not import tigeropen"
}

Write-Info "tigeropen v$SDK_VERSION installed successfully"

# ─── PATH Setup ──────────────────────────────────────────────────────────────

if ($env:TIGEROPEN_NO_MODIFY_PATH -ne "1") {
$TIGEROPEN_BIN = Get-Command "tigeropen" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source

if (-not $TIGEROPEN_BIN) {
# Check common pip/uv install locations on Windows
$candidates = @(
"$env:APPDATA\Python\Scripts\tigeropen.exe",
"$env:LOCALAPPDATA\Programs\Python\Python$($PY_MAJOR)$($PY_MINOR)\Scripts\tigeropen.exe",
"$env:LOCALAPPDATA\Programs\Python\Python$($PY_MAJOR)$($PY_MINOR.PadLeft(2,'0'))\Scripts\tigeropen.exe"
)
foreach ($path in $candidates) {
if (Test-Path $path) {
$TIGEROPEN_BIN = $path
break
}
}
}

if ($TIGEROPEN_BIN) {
Write-Info "CLI installed at: $TIGEROPEN_BIN"
$BIN_DIR = Split-Path $TIGEROPEN_BIN

# Check if already in PATH
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($currentPath -notlike "*$BIN_DIR*") {
[Environment]::SetEnvironmentVariable("PATH", "$BIN_DIR;$currentPath", "User")
Write-Info "Added $BIN_DIR to user PATH"
Write-Warn "Restart your terminal (or open a new PowerShell window) for PATH changes to take effect"
}
} else {
Write-Warn "tigeropen installed but CLI not found on PATH"
Write-Warn "You may need to add pip's Scripts directory to your PATH manually"
Write-Warn "Run: & $PYTHON -m site --user-site (then look for the Scripts sibling dir)"
}
}

# ─── Success Message ─────────────────────────────────────────────────────────

Write-Host ""
Write-Host " Installation complete!" -ForegroundColor Green
Write-Host ""
Write-Host " Getting started:"
Write-Host ""
Write-Host " # Set up your API credentials" -ForegroundColor Cyan
Write-Host " tigeropen config init"
Write-Host ""
Write-Host " # Or set environment variables" -ForegroundColor Cyan
Write-Host " `$env:TIGEROPEN_TIGER_ID = 'your_tiger_id'"
Write-Host " `$env:TIGEROPEN_PRIVATE_KEY = 'your_private_key'"
Write-Host " `$env:TIGEROPEN_ACCOUNT = 'your_account'"
Write-Host ""
Write-Host " # Query market data" -ForegroundColor Cyan
Write-Host " tigeropen quote briefs AAPL TSLA"
Write-Host " tigeropen quote bars AAPL --period day --limit 10"
Write-Host ""
Write-Host " # Manage orders" -ForegroundColor Cyan
Write-Host " tigeropen trade order list"
Write-Host " tigeropen account assets"
Write-Host ""
Write-Host " Documentation: https://docs.itigerup.com/docs/" -ForegroundColor Cyan
Write-Host " GitHub: https://github.com/tigerfintech/openapi-python-sdk" -ForegroundColor Cyan
Write-Host ""
17 changes: 15 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,18 @@ OS=$(uname -s)
case "$OS" in
Linux*) OS="linux" ;;
Darwin*) OS="macos" ;;
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
MINGW*|MSYS*|CYGWIN*)
OS="windows"
printf '%s\n' ""
printf '%s\n' "${yellow} Windows detected.${reset}"
printf '%s\n' " This shell script works under Git Bash / MSYS2 / Cygwin, but"
printf '%s\n' " the native Windows installer (PowerShell) is recommended:"
printf '%s\n' ""
printf '%s\n' " ${cyan}irm https://raw.githubusercontent.com/tigerfintech/openapi-python-sdk/master/install.ps1 | iex${reset}"
printf '%s\n' ""
printf '%s\n' " Continuing with shell installer..."
printf '%s\n' ""
;;
*) warn "unrecognized OS: $OS — proceeding anyway" ;;
esac

Expand Down Expand Up @@ -142,8 +153,10 @@ if [ -z "$TIGEROPEN_BIN" ]; then
"$HOME/.local/bin" \
"$HOME/Library/Python/${PY_VERSION}/bin" \
"$HOME/.local/share/pipx/venvs/tigeropen/bin" \
"$APPDATA/Python/Scripts" \
"$LOCALAPPDATA/Programs/Python/Python${PY_MAJOR}${PY_MINOR}/Scripts" \
; do
if [ -x "$dir/tigeropen" ]; then
if [ -x "$dir/tigeropen" ] || [ -x "$dir/tigeropen.exe" ]; then
TIGEROPEN_BIN="$dir/tigeropen"
break
fi
Expand Down
4 changes: 1 addition & 3 deletions tests/test_option_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,10 +637,8 @@ def test_real_option_metrics(self):
from tigeropen.tiger_open_config import TigerOpenClientConfig
import os

# This would require actual configuration
current_dir = os.path.dirname(__file__)
client_config = TigerOpenClientConfig(
props_path=os.path.join(current_dir, ".config/prod_2015xxxx/")
props_path=os.path.expanduser("~/.tigeropen/")
)
quote_client = QuoteClient(client_config)
option_util = OptionUtil(quote_client)
Expand Down
3 changes: 1 addition & 2 deletions tests/test_quote_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ class TestQuoteClient(unittest.TestCase):

def setUp(self):
self.is_mock = True
current_dir = os.path.dirname(__file__)
self.client_config = TigerOpenClientConfig(
props_path=os.path.join(current_dir, ".config/prod_xxxx/"))
props_path=os.path.expanduser("~/.tigeropen/"))
self.client: QuoteClient = QuoteClient(self.client_config,
logger=logger,
is_grab_permission=False)
Expand Down
3 changes: 1 addition & 2 deletions tests/test_trade_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ class TestTradeClient(unittest.TestCase):

def setUp(self):
self.is_mock = False
current_dir = os.path.dirname(__file__)
self.client_config = TigerOpenClientConfig(
props_path=os.path.join(current_dir, ".config/prod_2015xxxx/"))
props_path=os.path.expanduser("~/.tigeropen/"))
self.client: TradeClient = TradeClient(self.client_config,
logger=logger)
self.origin_do_request = web_utils.do_request
Expand Down
2 changes: 1 addition & 1 deletion tigeropen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

@author: gaoan
"""
__VERSION__ = '3.5.7'
__VERSION__ = '3.5.8'
29 changes: 23 additions & 6 deletions tigeropen/cli/account_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ def account_info(ctx):
click.echo('No account info available.')


def _segment_to_dict(segment):
"""Serialize a Segment object to a plain dict."""
d = {k: v for k, v in segment.__dict__.items() if not k.startswith('_')}
d['currency_assets'] = {
currency: {k: v for k, v in ca.__dict__.items() if not k.startswith('_')}
for currency, ca in segment.currency_assets.items()
}
return d


def _portfolio_account_to_dict(account):
"""Serialize a PortfolioAccount object to a plain dict."""
return {
'account': account.account,
'update_timestamp': account.update_timestamp,
'segments': {
key: _segment_to_dict(seg)
for key, seg in account._segments.items()
},
}


@account.command('assets')
@click.option('--currency', default=None, help='Currency filter (USD, HKD, etc.).')
@click.pass_context
Expand All @@ -38,12 +60,7 @@ def account_assets(ctx, currency):
kwargs['base_currency'] = currency
result = client.get_prime_assets(**kwargs)
if not is_empty(result):
if hasattr(result, 'to_dict'):
render(result.to_dict(), ctx.obj['format'])
elif hasattr(result, '__dict__'):
render(result.__dict__, ctx.obj['format'])
else:
render(result, ctx.obj['format'])
render(_portfolio_account_to_dict(result), ctx.obj['format'])
else:
click.echo('No asset data available.')

Expand Down
Loading