@@ -215,34 +215,86 @@ def format_account_balance(data: dict | None) -> str:
215215
216216def format_positions (data : list [dict ] | None ) -> str :
217217 """Format account positions response.
218-
219- API returns: [{position_id, currency, quantity, symbol, option_strategy,
220- instrument_type, last_price, cost_price, unrealized_profit_loss, legs: [...]}]
218+
219+ Top-level core (all regions): position_id, currency, quantity, symbol,
220+ option_strategy, instrument_type, last_price, cost_price,
221+ unrealized_profit_loss, legs: [...].
222+ Top-level region-specific:
223+ - US: event_outcome (event orders only)
224+ - JP: available_quantity, market_value, symbol_name, exchange_code,
225+ instrument_id, account_tax_type, base_currency, fx_rate,
226+ base_currency_market_value
227+
228+ Leg fields differ by region:
229+ - US: side, quantity, market, instrument_type, symbol, strike_price,
230+ option_expire_date, option_type (no leg_id)
231+ - HK: symbol, quantity, option_type, option_expire_date,
232+ option_exercise_price, option_contract_multiplier,
233+ option_contract_deliverable, expiration_type (no leg_id)
234+ - JP: same as HK plus leg_id
235+
236+ The formatter reads whichever fields are present so it works across regions.
221237 """
222238 if not data :
223239 return _NO_DATA
224240 lines : list [str ] = ["=== Positions ===" ]
225241 for pos in data :
226- lines .append (
227- f" { _get (pos , 'symbol' ):>8s} "
228- f"Qty: { _get (pos , 'quantity' ):>8s} "
229- f"Type: { _get (pos , 'instrument_type' ):>6s} "
230- f"Cost: { _get (pos , 'cost_price' ):>10s} "
231- f"Last: { _get (pos , 'last_price' ):>10s} "
232- f"Unrealized P&L: { _get (pos , 'unrealized_profit_loss' ):>10s} "
233- f"Currency: { _get (pos , 'currency' )} "
234- )
235- # Option legs
242+ # Top-level fields — show only present fields (varies by region).
243+ head_fields : list [tuple [str , str ]] = [
244+ ("Symbol" , "symbol" ),
245+ ("Position ID" , "position_id" ),
246+ ("Qty" , "quantity" ),
247+ ("Available Qty" , "available_quantity" ),
248+ ("Type" , "instrument_type" ),
249+ ("Strategy" , "option_strategy" ),
250+ ("Name" , "symbol_name" ),
251+ ("Exchange" , "exchange_code" ),
252+ ("Instrument ID" , "instrument_id" ),
253+ ("Cost" , "cost_price" ),
254+ ("Last" , "last_price" ),
255+ ("Market Value" , "market_value" ),
256+ ("Unrealized P&L" , "unrealized_profit_loss" ),
257+ ("Currency" , "currency" ),
258+ ("Event Outcome" , "event_outcome" ),
259+ ("Tax Type" , "account_tax_type" ),
260+ ("Base Currency" , "base_currency" ),
261+ ("FX Rate" , "fx_rate" ),
262+ ("Base Market Value" , "base_currency_market_value" ),
263+ ]
264+ head_parts = [
265+ f"{ label } : { pos [key ]} "
266+ for label , key in head_fields
267+ if pos .get (key ) is not None
268+ ]
269+ lines .append (" " + " " .join (head_parts ))
270+
271+ # Option legs — field set varies by region; show only present fields.
236272 legs = pos .get ("legs" , [])
237273 for leg in legs :
238- lines .append (
239- f"{ '' :>10s} "
240- f"Leg: { _get (leg , 'symbol' )} "
241- f"Qty: { _get (leg , 'quantity' )} "
242- f"Type: { _get (leg , 'option_type' )} "
243- f"Strike: { _get (leg , 'option_exercise_price' )} "
244- f"Exp: { _get (leg , 'option_expire_date' )} "
245- )
274+ parts : list [str ] = [f"Leg: { _get (leg , 'symbol' )} " ]
275+ if leg .get ("leg_id" ) is not None :
276+ parts .append (f"Leg ID: { leg ['leg_id' ]} " )
277+ if leg .get ("side" ) is not None :
278+ parts .append (f"Side: { leg ['side' ]} " )
279+ parts .append (f"Qty: { _get (leg , 'quantity' )} " )
280+ if leg .get ("instrument_type" ) is not None :
281+ parts .append (f"Type: { leg ['instrument_type' ]} " )
282+ if leg .get ("option_type" ) is not None :
283+ parts .append (f"Option: { leg ['option_type' ]} " )
284+ # Strike: US uses strike_price, HK/JP use option_exercise_price
285+ strike = leg .get ("strike_price" ) or leg .get ("option_exercise_price" )
286+ if strike is not None :
287+ parts .append (f"Strike: { strike } " )
288+ if leg .get ("option_expire_date" ) is not None :
289+ parts .append (f"Exp: { leg ['option_expire_date' ]} " )
290+ # HK/JP-only option contract fields
291+ if leg .get ("option_contract_multiplier" ) is not None :
292+ parts .append (f"Multiplier: { leg ['option_contract_multiplier' ]} " )
293+ if leg .get ("option_contract_deliverable" ) is not None :
294+ parts .append (f"Deliverable: { leg ['option_contract_deliverable' ]} " )
295+ if leg .get ("expiration_type" ) is not None :
296+ parts .append (f"ExpType: { leg ['expiration_type' ]} " )
297+ lines .append (f"{ '' :>10s} " + " " .join (parts ))
246298 return "\n " .join (lines )
247299
248300
@@ -262,45 +314,38 @@ def format_position_details(data: list[dict] | dict | None) -> str:
262314 if not isinstance (data , list ) or not data :
263315 return _NO_DATA
264316
317+ # Field labels in display order; only present fields are shown.
318+ fields : list [tuple [str , str ]] = [
319+ ("Position ID" , "id" ),
320+ ("Symbol" , "symbol" ),
321+ ("Name" , "symbol_name" ),
322+ ("Exchange" , "exchange_code" ),
323+ ("Instrument ID" , "instrument_id" ),
324+ ("Contract ID" , "contract_id" ),
325+ ("Qty" , "quantity" ),
326+ ("Hold Type" , "hold_type" ),
327+ ("Margin Type" , "margin_type" ),
328+ ("Average Price" , "average_price" ),
329+ ("Market Value" , "market_value" ),
330+ ("Unrealized P&L" , "unrealized_pl" ),
331+ ("Currency" , "currency" ),
332+ ("Account Tax Type" , "account_tax_type" ),
333+ ("Base Currency" , "base_currency" ),
334+ ("FX Rate" , "fx_rate" ),
335+ ("Base Currency Market Value" , "base_currency_market_value" ),
336+ ]
337+
265338 lines : list [str ] = ["=== Position Details ===" ]
266339 for i , detail in enumerate (data , 1 ):
267340 if not isinstance (detail , dict ):
268341 continue
269342 lines .append (f"\n [Position Detail { i } ]" )
270- lines .append (
271- f" { _get (detail , 'symbol' ):>8s} "
272- f"Qty: { _get (detail , 'quantity' ):>8s} "
273- f"Hold: { _get (detail , 'hold_type' ):>6s} "
274- f"Market Value: { _get (detail , 'market_value' ):>10s} "
275- f"Currency: { _get (detail , 'currency' )} "
276- )
277- lines .append (
278- f"{ '' :>10s} "
279- f"Name: { _get (detail , 'symbol_name' )} "
280- f"Exchange: { _get (detail , 'exchange_code' )} "
281- )
282- lines .append (
283- f"{ '' :>10s} "
284- f"Average Price: { _get (detail , 'average_price' ):>10s} "
285- f"Unrealized P&L: { _get (detail , 'unrealized_pl' ):>10s} "
286- )
287- lines .append (
288- f"{ '' :>10s} "
289- f"Account Tax Type: { _get (detail , 'account_tax_type' )} "
290- f"Margin Type: { _get (detail , 'margin_type' )} "
291- )
292- lines .append (
293- f"{ '' :>10s} "
294- f"Instrument ID: { _get (detail , 'instrument_id' )} "
295- f"Contract ID: { _get (detail , 'contract_id' )} "
296- f"Position ID: { _get (detail , 'id' )} "
297- )
298- lines .append (
299- f"{ '' :>10s} "
300- f"Base Currency: { _get (detail , 'base_currency' )} "
301- f"FX Rate: { _get (detail , 'fx_rate' )} "
302- f"Base Currency Market Value: { _get (detail , 'base_currency_market_value' )} "
303- )
343+ parts = [
344+ f"{ label } : { detail [key ]} "
345+ for label , key in fields
346+ if detail .get (key ) is not None
347+ ]
348+ lines .append (" " + " " .join (parts ))
304349
305350 if len (lines ) == 1 :
306351 return _NO_DATA
0 commit comments