alpha-strike 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ """alpha-strike — TradingView Webhook を moomoo / OANDA へルーティングする FastAPI サーバー."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("alpha-strike")
7
+ except PackageNotFoundError: # editable install / source 直接実行時
8
+ __version__ = "0.0.0+local"
9
+
10
+ __all__ = ["__version__"]
alpha_strike/cli.py ADDED
@@ -0,0 +1,68 @@
1
+ """alpha-strike CLI エントリポイント.
2
+
3
+ PyPI からインストール後、以下のコマンドで Webhook サーバーを起動できる:
4
+
5
+ alpha-strike # 既定: 0.0.0.0:8080
6
+ alpha-strike --host 127.0.0.1 # ホスト指定
7
+ alpha-strike --port 9000 # ポート指定
8
+ alpha-strike --version # バージョン表示
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import sys
16
+
17
+ import uvicorn
18
+
19
+ from alpha_strike import __version__
20
+
21
+
22
+ def _build_parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(
24
+ prog="alpha-strike",
25
+ description=(
26
+ "alpha-strike — TradingView Webhook を moomoo / OANDA へ"
27
+ "ルーティングする FastAPI サーバー"
28
+ ),
29
+ )
30
+ parser.add_argument(
31
+ "--host",
32
+ default=os.getenv("ALPHA_STRIKE_HOST", "0.0.0.0"),
33
+ help="バインドするホスト (既定: 0.0.0.0、環境変数 ALPHA_STRIKE_HOST でも上書き可)",
34
+ )
35
+ parser.add_argument(
36
+ "--port",
37
+ type=int,
38
+ default=int(os.getenv("ALPHA_STRIKE_PORT", "8080")),
39
+ help="バインドするポート (既定: 8080、環境変数 ALPHA_STRIKE_PORT でも上書き可)",
40
+ )
41
+ parser.add_argument(
42
+ "--reload",
43
+ action="store_true",
44
+ help="開発時のホットリロードを有効化",
45
+ )
46
+ parser.add_argument(
47
+ "--version",
48
+ action="version",
49
+ version=f"%(prog)s {__version__}",
50
+ )
51
+ return parser
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> int:
55
+ """CLI エントリポイント. uvicorn でサーバーを起動する。"""
56
+ parser = _build_parser()
57
+ args = parser.parse_args(argv)
58
+ uvicorn.run(
59
+ "alpha_strike.webhook_server:app",
60
+ host=args.host,
61
+ port=args.port,
62
+ reload=args.reload,
63
+ )
64
+ return 0
65
+
66
+
67
+ if __name__ == "__main__": # pragma: no cover - python -m alpha_strike 実行時
68
+ sys.exit(main())
@@ -0,0 +1,74 @@
1
+ """JSON Lines 形式で live trading イベントを保存する。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from pydantic import BaseModel
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class JsonlEventLogger:
17
+ """JSONL ファイルにイベントを書き出す軽量ロガー。"""
18
+
19
+ def __init__(self, base_path: str | Path | None = None) -> None:
20
+ self.base_path = Path(base_path) if base_path is not None else None
21
+
22
+ def _resolve_base_path(self) -> Path:
23
+ env_path = os.getenv("LIVE_EVENTS_PATH")
24
+ return self.base_path or Path(env_path or "./data/live/events")
25
+
26
+ def append(self, event: BaseModel) -> None:
27
+ """イベントを日次・ブローカー別 JSONL に追記する。失敗時は警告ログのみ残す。"""
28
+ try:
29
+ occurred_at = getattr(event, "occurred_at", datetime.now())
30
+ broker = getattr(event, "broker", "unknown")
31
+ day = occurred_at.strftime("%Y-%m-%d")
32
+ path = self._resolve_base_path() / f"{day}.{broker}.jsonl"
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ with path.open("a", encoding="utf-8") as fp:
35
+ fp.write(event.model_dump_json())
36
+ fp.write("\n")
37
+ except Exception as exc:
38
+ logger.warning("イベントログ保存に失敗しました: %s", exc)
39
+
40
+ def load_events(
41
+ self,
42
+ *,
43
+ broker: str | None = None,
44
+ event_type: str | None = None,
45
+ ticker: str | None = None,
46
+ strategy_id: str | None = None,
47
+ limit: int | None = 200,
48
+ ) -> list[dict]:
49
+ """保存済み JSONL event を新しい順で読む。"""
50
+ events: list[dict] = []
51
+ try:
52
+ base_path = self._resolve_base_path()
53
+ if not base_path.exists():
54
+ return []
55
+ files = sorted(base_path.glob("*.jsonl"), reverse=True)
56
+ for path in files:
57
+ if broker and not path.name.endswith(f".{broker}.jsonl"):
58
+ continue
59
+ for line in reversed(path.read_text(encoding="utf-8").splitlines()):
60
+ if not line.strip():
61
+ continue
62
+ payload = json.loads(line)
63
+ if event_type and payload.get("event_type") != event_type:
64
+ continue
65
+ if ticker and payload.get("ticker") != ticker:
66
+ continue
67
+ if strategy_id and payload.get("strategy_id") != strategy_id:
68
+ continue
69
+ events.append(payload)
70
+ if limit is not None and len(events) >= limit:
71
+ return events
72
+ except Exception as exc:
73
+ logger.warning("イベントログ読込に失敗しました: %s", exc)
74
+ return events
@@ -0,0 +1,5 @@
1
+ from .base import BrokerHandler
2
+ from .oanda_handler import OandaHandler
3
+ from .moomoo_handler import MoomooHandler
4
+
5
+ __all__ = ["BrokerHandler", "OandaHandler", "MoomooHandler"]
@@ -0,0 +1,22 @@
1
+ """ブローカーハンドラーの抽象インターフェース"""
2
+ from typing import Protocol
3
+
4
+ from alpha_strike.models import WebhookPayload
5
+
6
+
7
+ class BrokerHandler(Protocol):
8
+ """ブローカー注文ハンドラーの共通インターフェース。
9
+
10
+ 新しいブローカーを追加するには、このProtocolを満たすクラスを
11
+ `handlers/` に追加し `build_default_router()` に登録するだけでよい。
12
+ """
13
+
14
+ def execute(self, payload: WebhookPayload) -> dict:
15
+ """注文を実行し、結果dictを返す。
16
+
17
+ Raises:
18
+ ImportError: 必要なライブラリが未インストールの場合
19
+ ValueError: 環境変数が不足または不正な場合
20
+ RuntimeError: APIがエラーを返した場合
21
+ """
22
+ ...
@@ -0,0 +1,195 @@
1
+ """moomoo証券(Futu OpenAPI)アダプター
2
+
3
+ ローカルで稼働するOpenDゲートウェイ経由で注文を実行します。
4
+
5
+ テスト時は MOOMOO_TRD_ENV=SIMULATE(デモ環境)を使用してください。
6
+ OpenD を先に起動してからサーバーを起動してください。
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import socket
12
+ from typing import TYPE_CHECKING
13
+
14
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
15
+
16
+ from alpha_strike.models import WebhookPayload
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ if TYPE_CHECKING:
21
+ import futu
22
+
23
+ try:
24
+ import futu
25
+
26
+ FUTU_AVAILABLE = True
27
+ except ImportError:
28
+ FUTU_AVAILABLE = False
29
+ logger.warning(
30
+ "futu-api がインポートできません。moomoo注文は実行時に失敗します。OpenDが起動しているか確認してください。"
31
+ )
32
+
33
+
34
+ @retry(
35
+ retry=retry_if_exception_type((OSError, socket.timeout)),
36
+ wait=wait_fixed(2),
37
+ stop=stop_after_attempt(3),
38
+ reraise=True,
39
+ )
40
+ def _check_opend_connection(host: str, port: int) -> None:
41
+ """OpenD への TCP 接続確認。一時障害に対して最大3回リトライする。"""
42
+ with socket.create_connection((host, port), timeout=3):
43
+ pass # 接続確認のみ。コンテキスト終了時に自動クローズ
44
+
45
+
46
+ _MARKET_MAP: "dict[str, str]" = {
47
+ "HK": "HK",
48
+ "CRYPTO": "CRYPTO",
49
+ }
50
+
51
+
52
+ def _get_trade_context(
53
+ asset_class: str,
54
+ host: str,
55
+ port: int,
56
+ ) -> "futu.OpenSecTradeContext":
57
+ """asset_class に基づいて統一トレードコンテキスト OpenSecTradeContext を返す。
58
+
59
+ futu/moomoo SDK 10.5.6508 以降は OpenUSTradeContext / OpenHKTradeContext が
60
+ 廃止され、OpenSecTradeContext + filter_trdmarket に統一された。
61
+
62
+ 対応:
63
+ - "HK" → filter_trdmarket=TrdMarket.HK
64
+ - "CRYPTO" → filter_trdmarket=TrdMarket.CRYPTO
65
+ - その他 → filter_trdmarket=TrdMarket.US (US / INDEX / COMMODITY / FX)
66
+
67
+ security_firm はすべての市場で SecurityFirm.NONE をデフォルト指定する。
68
+ REAL 取引で broker 固有の firm が必要な場合は呼び出し側で上書きする。
69
+ """
70
+ ac = asset_class.upper()
71
+ market_name = _MARKET_MAP.get(ac, "US")
72
+ market = getattr(futu.TrdMarket, market_name)
73
+ return futu.OpenSecTradeContext(
74
+ filter_trdmarket=market,
75
+ host=host,
76
+ port=port,
77
+ security_firm=futu.SecurityFirm.NONE,
78
+ )
79
+
80
+
81
+ class MoomooHandler:
82
+ """moomoo証券(Futu OpenAPI)への注文を実行するハンドラー。"""
83
+
84
+ def execute(self, payload: WebhookPayload) -> dict:
85
+ """moomoo証券(Futu OpenAPI)へ注文を送信する。
86
+
87
+ Raises:
88
+ ImportError: futu-api が利用不可の場合
89
+ ValueError: 必須設定が不足している場合
90
+ RuntimeError: 注文APIがエラーを返した場合
91
+ """
92
+ if not FUTU_AVAILABLE:
93
+ raise ImportError(
94
+ "futu-api が利用できません。`uv add futu-api` でインストール後、OpenDを起動してください。"
95
+ )
96
+
97
+ host = os.getenv("MOOMOO_HOST", "127.0.0.1")
98
+ try:
99
+ port = int(os.getenv("MOOMOO_PORT", "11111"))
100
+ except ValueError as e:
101
+ raise ValueError("MOOMOO_PORT に不正な値が設定されています") from e
102
+ trd_env_str = os.getenv("MOOMOO_TRD_ENV", "SIMULATE").upper()
103
+
104
+ # TrdEnv マッピング(デフォルトはSIMULATEで安全側)
105
+ trd_env_map = {
106
+ "SIMULATE": futu.TrdEnv.SIMULATE,
107
+ "REAL": futu.TrdEnv.REAL,
108
+ }
109
+ if trd_env_str not in trd_env_map:
110
+ raise ValueError(
111
+ f"MOOMOO_TRD_ENV は SIMULATE または REAL を指定してください(現在: {trd_env_str})"
112
+ )
113
+
114
+ trd_env = trd_env_map[trd_env_str]
115
+ trd_side = futu.TrdSide.BUY if payload.action == "buy" else futu.TrdSide.SELL
116
+
117
+ logger.info(
118
+ "moomoo注文開始: trd_env=%s ticker=%s action=%s qty=%s",
119
+ trd_env_str,
120
+ payload.ticker,
121
+ payload.action,
122
+ payload.quantity,
123
+ )
124
+
125
+ # OpenD への接続可否を事前確認(タイムアウト3秒、最大3回リトライ)
126
+ try:
127
+ _check_opend_connection(host, port)
128
+ except (OSError, socket.timeout) as e:
129
+ logger.error("OpenD に接続できません (%s:%s): %s", host, port, e)
130
+ raise RuntimeError(
131
+ f"OpenD ({host}:{port}) が起動していません。先にOpenDを起動してください。"
132
+ ) from e
133
+
134
+ try:
135
+ ctx = _get_trade_context(payload.asset_class, host, port)
136
+ with ctx:
137
+ ret_code, data = ctx.place_order(
138
+ price=0,
139
+ qty=payload.quantity,
140
+ code=payload.ticker,
141
+ trd_side=trd_side,
142
+ order_type=futu.OrderType.MARKET,
143
+ trd_env=trd_env,
144
+ )
145
+
146
+ if ret_code != futu.RET_OK:
147
+ logger.error(
148
+ "moomoo注文失敗: ticker=%s ret_code=%s data=%s",
149
+ payload.ticker,
150
+ ret_code,
151
+ data,
152
+ )
153
+ raise RuntimeError(f"moomoo注文エラー: {data}")
154
+
155
+ try:
156
+ if (
157
+ hasattr(data, "empty")
158
+ and not data.empty
159
+ and "order_id" in data.columns
160
+ ):
161
+ order_id = str(data["order_id"].iloc[0])
162
+ else:
163
+ order_id = str(data)
164
+ logger.warning(
165
+ "order_idの取得に失敗。レスポンス全体を使用: %s", data
166
+ )
167
+ except (AttributeError, KeyError, IndexError) as e:
168
+ logger.warning("order_idのパース失敗: %s。レスポンス全体を使用。", e)
169
+ order_id = str(data)
170
+
171
+ filled_qty = None
172
+ filled_price = None
173
+ if hasattr(data, "empty") and not data.empty:
174
+ if "dealt_qty" in data.columns:
175
+ filled_qty = float(data["dealt_qty"].iloc[0])
176
+ if "dealt_avg_price" in data.columns:
177
+ filled_price = float(data["dealt_avg_price"].iloc[0])
178
+
179
+ logger.info("moomoo注文成功: order_id=%s", order_id)
180
+ return {
181
+ "order_id": order_id,
182
+ "ret_code": ret_code,
183
+ "filled_qty": filled_qty,
184
+ "filled_price": filled_price,
185
+ }
186
+
187
+ except Exception as e:
188
+ if isinstance(e, (ImportError, ValueError, RuntimeError)):
189
+ raise
190
+ logger.error(
191
+ "moomoo注文で予期しないエラー: ticker=%s error=%s",
192
+ payload.ticker,
193
+ e,
194
+ )
195
+ raise RuntimeError(f"moomoo注文失敗: {e}") from e
@@ -0,0 +1,122 @@
1
+ """OANDA証券 REST API v20 を使用した注文ハンドラー"""
2
+
3
+ import logging
4
+ import os
5
+
6
+ import requests
7
+ from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
8
+
9
+ from alpha_strike.models import WebhookPayload
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ OANDA_PRACTICE_URL = "https://api-fxpractice.oanda.com"
14
+ OANDA_LIVE_URL = "https://api-fxtrade.oanda.com"
15
+
16
+
17
+ def _is_retryable_oanda_error(exc: Exception) -> bool:
18
+ """5xx エラーおよびネットワーク一時障害のみリトライ対象とする。4xx(設定ミス等)はリトライしない。"""
19
+ if isinstance(exc, requests.HTTPError):
20
+ return exc.response is not None and exc.response.status_code >= 500
21
+ return isinstance(exc, (requests.ConnectionError, requests.Timeout))
22
+
23
+
24
+ @retry(
25
+ retry=retry_if_exception(_is_retryable_oanda_error),
26
+ wait=wait_exponential(multiplier=1, min=1, max=10),
27
+ stop=stop_after_attempt(3),
28
+ reraise=True,
29
+ )
30
+ def _call_oanda_api(url: str, body: dict, headers: dict) -> dict:
31
+ """OANDA REST API を呼び出す。一時障害(5xx、接続エラー)は最大3回リトライする。"""
32
+ response = requests.post(url, json=body, headers=headers, timeout=10)
33
+ response.raise_for_status()
34
+ return response.json()
35
+
36
+
37
+ def _to_oanda_instrument(ticker: str, asset_class: str) -> str:
38
+ """TradingViewのティッカーをOANDA instrument形式に変換する。
39
+
40
+ asset_class に応じた変換ルール:
41
+ - "FX" / "COMMODITY": 6文字の場合 "USDJPY" → "USD_JPY"
42
+ - "US" / "INDEX": アンダースコアなしの場合 "AAPL" → "AAPL_USD"
43
+ - その他: そのまま使用(OANDA形式で直接指定)
44
+ """
45
+ if asset_class in ("FX", "COMMODITY") and len(ticker) == 6 and "_" not in ticker:
46
+ return f"{ticker[:3]}_{ticker[3:]}"
47
+ if asset_class in ("US", "INDEX") and "_" not in ticker:
48
+ return f"{ticker}_USD"
49
+ if asset_class in ("FX", "COMMODITY") and len(ticker) != 6:
50
+ logger.warning(
51
+ "FX/COMMODITYの ticker が6文字ではありません。変換せずに送信します: %s",
52
+ ticker,
53
+ )
54
+ return ticker
55
+
56
+
57
+ class OandaHandler:
58
+ """OANDA証券への注文を実行するハンドラー。"""
59
+
60
+ def execute(self, payload: WebhookPayload) -> dict:
61
+ """OANDA証券に成行注文を送信する。
62
+
63
+ Returns:
64
+ {"order_id": str, "instrument": str, ...}
65
+
66
+ Raises:
67
+ ValueError: 環境変数が不足または不正な場合
68
+ requests.RequestException: API呼び出しに失敗した場合
69
+ """
70
+ api_key = os.getenv("OANDA_API_KEY", "")
71
+ account_id = os.getenv("OANDA_ACCOUNT_ID", "")
72
+ oanda_env = os.getenv("OANDA_ENV", "PRACTICE").upper()
73
+
74
+ if not api_key:
75
+ raise ValueError("環境変数 OANDA_API_KEY が設定されていません")
76
+ if not account_id:
77
+ raise ValueError("環境変数 OANDA_ACCOUNT_ID が設定されていません")
78
+ if oanda_env not in ("PRACTICE", "LIVE"):
79
+ raise ValueError(
80
+ f"OANDA_ENV は PRACTICE または LIVE である必要があります: {oanda_env!r}"
81
+ )
82
+
83
+ base_url = OANDA_PRACTICE_URL if oanda_env == "PRACTICE" else OANDA_LIVE_URL
84
+ instrument = _to_oanda_instrument(payload.ticker, payload.asset_class)
85
+
86
+ # SELL は負の units で表現する
87
+ units = payload.quantity if payload.action == "buy" else -payload.quantity
88
+
89
+ headers = {
90
+ "Authorization": f"Bearer {api_key}",
91
+ "Content-Type": "application/json",
92
+ }
93
+ body = {
94
+ "order": {
95
+ "type": "MARKET",
96
+ "instrument": instrument,
97
+ "units": str(units),
98
+ }
99
+ }
100
+
101
+ url = f"{base_url}/v3/accounts/{account_id}/orders"
102
+ logger.info(
103
+ "OANDA注文送信: instrument=%s units=%s env=%s", instrument, units, oanda_env
104
+ )
105
+
106
+ data = _call_oanda_api(url, body, headers)
107
+ order_id = data.get("orderCreateTransaction", {}).get("id", "unknown")
108
+ fill_tx = data.get("orderFillTransaction", {})
109
+ filled_qty = abs(float(fill_tx["units"])) if fill_tx.get("units") is not None else None
110
+ filled_price = (
111
+ float(fill_tx["price"]) if fill_tx.get("price") is not None else None
112
+ )
113
+ fill_id = str(fill_tx["id"]) if fill_tx.get("id") is not None else None
114
+
115
+ logger.info("OANDA注文成功: order_id=%s instrument=%s", order_id, instrument)
116
+ return {
117
+ "order_id": order_id,
118
+ "instrument": instrument,
119
+ "fill_id": fill_id,
120
+ "filled_qty": filled_qty,
121
+ "filled_price": filled_price,
122
+ }
alpha_strike/models.py ADDED
@@ -0,0 +1,186 @@
1
+ from datetime import datetime
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class WebhookPayload(BaseModel):
8
+ passphrase: str = Field(repr=False)
9
+ broker: Literal["oanda", "moomoo"]
10
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
11
+ action: Literal["buy", "sell"]
12
+ ticker: str = Field(
13
+ pattern=r"^[A-Z0-9_.]{1,20}$",
14
+ description="ティッカーシンボル(英大文字・数字・ドット・アンダースコアのみ、20文字以内)",
15
+ )
16
+ quantity: float = Field(gt=0, description="注文数量(株数またはロット数)")
17
+ strategy_id: str | None = Field(
18
+ default=None,
19
+ pattern=r"^[a-zA-Z0-9_.-]{1,64}$",
20
+ description="alpha-forge 側の strategy_id",
21
+ )
22
+ strategy_version: str | None = Field(
23
+ default=None,
24
+ max_length=32,
25
+ description="戦略バージョン",
26
+ )
27
+ snapshot_id: str | None = Field(
28
+ default=None,
29
+ pattern=r"^snap_[0-9]{20}$",
30
+ description="alpha-forge journal snapshot_id",
31
+ )
32
+ signal_id: str | None = Field(
33
+ default=None,
34
+ max_length=80,
35
+ description="シグナル一意ID。未指定なら alpha-strike 側で採番",
36
+ )
37
+ timeframe: str | None = Field(
38
+ default=None,
39
+ max_length=16,
40
+ description="例: 1m, 5m, 1h, 1d",
41
+ )
42
+ alert_timestamp: datetime | None = Field(
43
+ default=None,
44
+ description="TradingView 側でシグナルが発火した時刻",
45
+ )
46
+ run_mode: Literal["paper", "live"] = Field(
47
+ default="live",
48
+ description="paper は模擬運用・ログ用途",
49
+ )
50
+ alert_name: str | None = Field(
51
+ default=None,
52
+ max_length=128,
53
+ description="TradingView アラート名",
54
+ )
55
+ order_comment: str | None = Field(
56
+ default=None,
57
+ max_length=256,
58
+ description="任意メモ",
59
+ )
60
+
61
+
62
+ class OrderResult(BaseModel):
63
+ status: Literal["success", "error"]
64
+ broker: Literal["oanda", "moomoo"]
65
+ ticker: str
66
+ message: str
67
+ signal_id: str | None = None
68
+ order_id: str | None = None
69
+ broker_order_id: str | None = None
70
+ event_id: str | None = None
71
+
72
+
73
+ class EventIngestResult(BaseModel):
74
+ status: str
75
+ event_id: str
76
+ message: str
77
+
78
+
79
+ class SignalEvent(BaseModel):
80
+ event_type: Literal["signal_received"] = "signal_received"
81
+ event_id: str
82
+ signal_id: str
83
+ occurred_at: datetime
84
+ broker: Literal["oanda", "moomoo"]
85
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
86
+ action: Literal["buy", "sell"]
87
+ ticker: str
88
+ quantity: float
89
+ strategy_id: str | None = None
90
+ strategy_version: str | None = None
91
+ snapshot_id: str | None = None
92
+ timeframe: str | None = None
93
+ alert_timestamp: datetime | None = None
94
+ run_mode: Literal["paper", "live"] = "live"
95
+ alert_name: str | None = None
96
+
97
+
98
+ class OrderEvent(BaseModel):
99
+ event_type: Literal["order_recorded"] = "order_recorded"
100
+ event_id: str
101
+ signal_id: str
102
+ order_id: str
103
+ occurred_at: datetime
104
+ broker: Literal["oanda", "moomoo"]
105
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
106
+ action: Literal["buy", "sell"]
107
+ ticker: str
108
+ quantity: float
109
+ status: Literal["accepted", "failed"]
110
+ request_latency_ms: int | None = None
111
+ broker_order_id: str | None = None
112
+ strategy_id: str | None = None
113
+ strategy_version: str | None = None
114
+ snapshot_id: str | None = None
115
+ run_mode: Literal["paper", "live"] = "live"
116
+ error_type: str | None = None
117
+
118
+
119
+ class FillEvent(BaseModel):
120
+ event_type: Literal["fill_received"] = "fill_received"
121
+ event_id: str
122
+ signal_id: str
123
+ order_id: str
124
+ fill_id: str
125
+ occurred_at: datetime
126
+ broker: Literal["oanda", "moomoo"]
127
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
128
+ action: Literal["buy", "sell"]
129
+ ticker: str
130
+ quantity: float
131
+ filled_qty: float
132
+ filled_price: float
133
+ broker_order_id: str | None = None
134
+ trade_id: str | None = None
135
+ strategy_id: str | None = None
136
+ strategy_version: str | None = None
137
+ snapshot_id: str | None = None
138
+ run_mode: Literal["paper", "live"] = "live"
139
+ commission: float | None = None
140
+ slippage_bps: float | None = None
141
+
142
+
143
+ class TradeClosedEvent(BaseModel):
144
+ event_type: Literal["trade_closed"] = "trade_closed"
145
+ event_id: str
146
+ signal_id: str
147
+ trade_id: str
148
+ occurred_at: datetime
149
+ closed_at: datetime
150
+ broker: Literal["oanda", "moomoo"]
151
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
152
+ action: Literal["buy", "sell"]
153
+ ticker: str
154
+ quantity: float
155
+ entry_price: float
156
+ exit_price: float
157
+ gross_pnl: float
158
+ net_pnl: float
159
+ strategy_id: str | None = None
160
+ strategy_version: str | None = None
161
+ snapshot_id: str | None = None
162
+ run_mode: Literal["paper", "live"] = "live"
163
+ commission: float | None = None
164
+ exit_reason: str | None = None
165
+
166
+
167
+ class TradeClosedPayload(BaseModel):
168
+ passphrase: str = Field(repr=False)
169
+ signal_id: str
170
+ trade_id: str
171
+ closed_at: datetime
172
+ broker: Literal["oanda", "moomoo"]
173
+ asset_class: Literal["FX", "COMMODITY", "US", "HK", "INDEX", "CRYPTO"]
174
+ action: Literal["buy", "sell"]
175
+ ticker: str
176
+ quantity: float = Field(gt=0)
177
+ entry_price: float = Field(gt=0)
178
+ exit_price: float = Field(gt=0)
179
+ gross_pnl: float
180
+ net_pnl: float
181
+ strategy_id: str | None = None
182
+ strategy_version: str | None = None
183
+ snapshot_id: str | None = None
184
+ run_mode: Literal["paper", "live"] = "live"
185
+ commission: float | None = None
186
+ exit_reason: str | None = None
File without changes