跳轉到

finlab.online

finlab.online.base_account.Account

Bases: ABC

股票帳戶的 abstract class 可以繼承此 Account,來實做券商的帳戶買賣動作,目前已經實做 SinopacAccount (永豐證券) 以及 FugleAccount (玉山富果),來進行交易。可以用以下方式建構物件並用來交易:

永豐證券

import os
from finlab.online.sinopac_account import SinopacAccount


# 舊版請使用
# shioaji < 1.0.0 and finlab < 0.3.18
os.environ['SHIOAJI_ACCOUNT']= '永豐證券帳號'
os.environ['SHIOAJI_PASSWORD']= '永豐證券密碼'

# 新版請使用
# shioaji >= 1.0.0 and finlab >= 0.3.18
os.environ['SHIOAJI_API_KEY'] = '永豐證券API_KEY'
os.environ['SHIOAJI_SECRET_KEY'] = '永豐證券SECRET_KEY'
os.environ['SHIOAJI_CERT_PERSON_ID']= '身份證字號'

# shioaji
os.environ['SHIOAJI_CERT_PATH']= '永豐證券憑證路徑'
os.environ['SHIOAJI_CERT_PASSWORD'] = '永豐證券憑證密碼' # 預設與身份證字號

acc = SinopacAccount()
玉山富果:
from finlab.online.fugle_account import FugleAccount
import os
os.environ['FUGLE_CONFIG_PATH'] = '玉山富果交易設定檔(config.ini.example)路徑'
os.environ['FUGLE_MARKET_API_KEY'] = '玉山富果的行情API Token'

acc = FugleAccount()

cancel_order abstractmethod

cancel_order(order_id)

刪除委託單

建議使用 刪除委託單此功能前,先使用 update_order() 來更新委託單的狀況!如下

acc.update_order()
acc.cancel_order('ORDER_ID')

ATTRIBUTE DESCRIPTION
order_id

券商所提供的委託單 ID

TYPE: str

RETURNS DESCRIPTION
None

代表成功更新委託單

Source code in finlab/online/base_account.py
@abstractmethod
def cancel_order(self, order_id):
    """刪除委託單

    建議使用 刪除委託單此功能前,先使用 update_order() 來更新委託單的狀況!如下
    ```py
    acc.update_order()
    acc.cancel_order('ORDER_ID')
    ```

    Attributes:
        order_id (str): 券商所提供的委託單 ID

    Returns:
        (None): 代表成功更新委託單
    """
    pass

create_order abstractmethod

create_order(action, stock_id, quantity, price=None, force=False, wait_for_best_price=False)

產生新的委託單

PARAMETER DESCRIPTION
action

買賣方向,通常為 'BUY' 或是 'SELL'

TYPE: Action

stock_id

股票代號 ex: '2330'

TYPE: str

quantity

委託股票的總數量(張數),允許小數點

TYPE: Number

price

股票買賣的價格(限價單)

TYPE: Number DEFAULT: None

force

是否用最差之價格(長跌停)強制成交? 當成交量足夠時,可以比較快成交,然而當成交量低時,容易有大的滑價

TYPE: bool DEFAULT: False

wait_for_best_price

是否用最佳之價格(長跌停),無限時間等待?當今天要出場時,可以開啟等漲停價來購買,當今天要買入時,可以掛跌停價等待買入時機。

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

order id 券商提供的委託單編號

Source code in finlab/online/base_account.py
@abstractmethod
def create_order(self, action, stock_id, quantity, price=None, force=False, wait_for_best_price=False):
    """產生新的委託單

    Args:
        action (Action): 買賣方向,通常為 'BUY' 或是 'SELL'

        stock_id (str): 股票代號 ex: '2330'

        quantity (numbers.Number): 委託股票的總數量(張數),允許小數點

        price (numbers.Number, optional): 股票買賣的價格(限價單)

        force (bool): 是否用最差之價格(長跌停)強制成交? 當成交量足夠時,可以比較快成交,然而當成交量低時,容易有大的滑價

        wait_for_best_price (bool): 是否用最佳之價格(長跌停),無限時間等待?當今天要出場時,可以開啟等漲停價來購買,當今天要買入時,可以掛跌停價等待買入時機。

    Returns:
        (str): order id 券商提供的委託單編號
    """
    pass

get_orders abstractmethod

get_orders()

拿到現在所有委託單

RETURNS DESCRIPTION
Dict[str, Order]

所有委託單 id 與委託單資料

Example

{'12345A': Order(order_id='12345A', stock_id='5410',...),...}

Source code in finlab/online/base_account.py
@abstractmethod
def get_orders(self):
    """拿到現在所有委託單

    Returns:
        (Dict[str, Order]): 所有委託單 id 與委託單資料
            !!! example
                `{'12345A': Order(order_id='12345A', stock_id='5410',...),...}`
    """
    pass

get_position abstractmethod

get_position()

拿到當前帳戶的股票部位

RETURNS DESCRIPTION
Position

當前股票部位

Source code in finlab/online/base_account.py
@abstractmethod
def get_position(self):
    """拿到當前帳戶的股票部位

    Returns:
        (Position): 當前股票部位
    """
    pass

get_stocks abstractmethod

get_stocks(stock_ids)

拿到現在股票報價

ATTRIBUTE DESCRIPTION
stock_ids

一次拿取所有股票的報價,ex: ['1101', '2330']

TYPE: `list` of `str`

RETURNS DESCRIPTION
dict

報價資料,

Example

{'1101': Stock(stock_id='1101', open=31.15, high=31.85, low=31.1, close=31.65, bid_price=31.6, bid_volume=728.0, ask_price=31.65, ask_volume=202)}

Source code in finlab/online/base_account.py
@abstractmethod
def get_stocks(self, stock_ids):
    """拿到現在股票報價

    Attributes:
        stock_ids (`list` of `str`): 一次拿取所有股票的報價,ex: ['1101', '2330']

    Returns:
        (dict): 報價資料,
            !!! example
                `{'1101': Stock(stock_id='1101', open=31.15, high=31.85, low=31.1, close=31.65, bid_price=31.6, bid_volume=728.0, ask_price=31.65, ask_volume=202)}`
    """
    pass

get_total_balance abstractmethod

get_total_balance()

拿到當前帳戶的股票部位淨值

Source code in finlab/online/base_account.py
@abstractmethod
def get_total_balance(self):
    """拿到當前帳戶的股票部位淨值"""
    pass

update_order abstractmethod

update_order(order_id, price=None, quantity=None)

產生新的委託單

ATTRIBUTE DESCRIPTION
order_id

券商所提供的委託單 ID

TYPE: str

price

更新的限價

TYPE: Number

quantity

更新的待成交量

TYPE: Number

RETURNS DESCRIPTION
None

無跳出 erorr 代表成功更新委託單

Source code in finlab/online/base_account.py
@abstractmethod
def update_order(self, order_id, price=None, quantity=None):
    """產生新的委託單

    Attributes:
        order_id (str): 券商所提供的委託單 ID
        price (numbers.Number, optional): 更新的限價
        quantity (numbers.Number, optional): 更新的待成交量

    Returns:
        (None): 無跳出 erorr 代表成功更新委託單
    """
    pass

finlab.online.order_executor.Position

Position(stocks, margin_trading=False, short_selling=False, day_trading_long=False, day_trading_short=False)

使用者可以利用 Position 輕鬆建構股票的部位,並且利用 OrderExecuter 將此部位同步於實際的股票帳戶。

建構股票部位

ATTRIBUTE DESCRIPTION
stocks

number.Number): 股票代號與張數 ex: {'1101': 1} 是指持有一張 1101 台泥,可以接受負數,代表做空。

TYPE: `dict` of `str`

margin_trading

做多部位是否使用融資

TYPE: bool

short_selling

做空部位是否使用融券

TYPE: bool

day_trading_long

做多部位為當沖先做多

TYPE: bool

day_trading_short

做空部位為當沖先做空

TYPE: bool

Examples:

設計部位,持有一張和 100 股 1101

from finlab.online.order_executor import Position

Position({'1101': 1.1})
output
[
    {'stock_id': '1101',
     'quantity': 1.1,
     'order_condition': <OrderCondition.CASH: 1>
    }
]

將兩個部位相加

from finlab.online.order_executor import Position

p1 = Position({'1101': 1})
p2 = Position({'2330': 1})
p1 + p2
output
[
    {'stock_id': '1101', 'quantity': 1.0, 'order_condition': <OrderCondition.CASH: 1>},
    {'stock_id': '2330', 'quantity': 1.0, 'order_condition': <OrderCondition.CASH: 1>}
]

Source code in finlab/online/order_executor.py
def __init__(self, stocks, margin_trading=False, short_selling=False, day_trading_long=False, day_trading_short=False):
    """建構股票部位

    Attributes:
        stocks (`dict` of `str`:`number.Number`): 股票代號與張數 ex: {'1101': 1} 是指持有一張 1101 台泥,可以接受負數,代表做空。
        margin_trading (bool): 做多部位是否使用融資
        short_selling (bool): 做空部位是否使用融券
        day_trading_long (bool): 做多部位為當沖先做多
        day_trading_short (bool): 做空部位為當沖先做空

    Examples:
        設計部位,持有一張和 100 股 1101
        ```py
        from finlab.online.order_executor import Position

        Position({'1101': 1.1})
        ```
        output
        ```json
        [
            {'stock_id': '1101',
             'quantity': 1.1,
             'order_condition': <OrderCondition.CASH: 1>
            }
        ]
        ```

        將兩個部位相加
        ```py
        from finlab.online.order_executor import Position

        p1 = Position({'1101': 1})
        p2 = Position({'2330': 1})
        p1 + p2
        ```
        output
        ```json
        [
            {'stock_id': '1101', 'quantity': 1.0, 'order_condition': <OrderCondition.CASH: 1>},
            {'stock_id': '2330', 'quantity': 1.0, 'order_condition': <OrderCondition.CASH: 1>}
        ]
        ```
    """
    assert margin_trading + day_trading_long <= 1
    assert short_selling + day_trading_short <= 1

    long_order_condition = OrderCondition.CASH
    short_order_condition = OrderCondition.CASH

    if margin_trading:
        long_order_condition = OrderCondition.MARGIN_TRADING
    elif day_trading_long:
        long_order_condition = OrderCondition.DAY_TRADING_LONG

    if short_selling:
        short_order_condition = OrderCondition.SHORT_SELLING
    elif day_trading_short:
        short_order_condition = OrderCondition.DAY_TRADING_SHORT

    self.position = []
    for s, a in stocks.items():
        if a != 0:
            self.position.append(
                {'stock_id': s, 'quantity': a, 'order_condition': long_order_condition if a > 0 else short_order_condition})

from_json classmethod

from_json(path)

Load a JSON file from the given path and convert it to a list of positions.

PARAMETER DESCRIPTION
path

The path to the JSON file.

TYPE: str

RETURNS DESCRIPTION

None

Source code in finlab/online/order_executor.py
@classmethod
def from_json(self, path):
    """
    Load a JSON file from the given path and convert it to a list of positions.

    Args:
        path (str): The path to the JSON file.

    Returns:
        None
    """

    with open(path, 'r') as f:
        ret = json.load(f)
        ret = self._format_quantity(ret)

    return Position.from_list(ret)

from_list classmethod

from_list(position)

利用 dict 建構股票部位

ATTRIBUTE DESCRIPTION
position

股票詳細部位

from finlab.online.enums import OrderCondition
from finlab.online.order_executor import Position

Position.from_list(
[{
  'stock_id': '1101', # 股票代號
  'quantity': 1.1, # 張數
  'order_condition': OrderCondition.CASH # 現股融資融券、先買後賣
}])

其中 OrderCondition 除了 CASH 外,還有 MARGIN_TRADINGDAY_TRADING_LONGSHORT_SELLINGDAY_TRADING_SHORT

TYPE: `list` of `dict`

Source code in finlab/online/order_executor.py
@classmethod
def from_list(cls, position):
    """利用 `dict` 建構股票部位


    Attributes:
        position (`list` of `dict`): 股票詳細部位
          ```py
          from finlab.online.enums import OrderCondition
          from finlab.online.order_executor import Position

          Position.from_list(
          [{
              'stock_id': '1101', # 股票代號
              'quantity': 1.1, # 張數
              'order_condition': OrderCondition.CASH # 現股融資融券、先買後賣
          }])

          ```

          其中 OrderCondition 除了 `CASH` 外,還有 `MARGIN_TRADING`、`DAY_TRADING_LONG`、`SHORT_SELLING`、`DAY_TRADING_SHORT`。

    """
    ret = cls({})
    ret.position = ret._format_quantity(position)
    return ret

from_report classmethod

from_report(report, fund, **kwargs)

利用回測完的報告 finlab.report.Report 建構股票部位。

ATTRIBUTE DESCRIPTION
report

回測完的結果報告。

TYPE: Report

fund

希望部屬的資金。

TYPE: int

price

股票代號對應到的價格,若無則使用最近個交易日的收盤價。

TYPE: pd.Series or `dict` of `float`

odd_lot

是否考慮零股。預設為 False,只使用整張操作。

TYPE: bool

board_lot_size

一張股票等於幾股。預設為1000,一張等於1000股。

TYPE: int

allocation

資產配置演算法選定,預設為finlab.online.utils.greedy_allocation(最大資金部屬貪婪法)。

TYPE: func

Example

from finlab import backtest
from finlab.online.order_executor import Position

report1 = backtest.sim(...)
report2 = backtest.sim(...)

position1 = Position.from_report(report1, 1000000) # 策略操作金額一百萬
position2 = Position.from_report(report2, 1000000) # 策略操作金額一百萬

total_position = position1 + position2
Source code in finlab/online/order_executor.py
@classmethod
def from_report(cls, report, fund, **kwargs):
    """利用回測完的報告 `finlab.report.Report` 建構股票部位。

    Attributes:
        report (finlab.report.Report): 回測完的結果報告。
        fund (int): 希望部屬的資金。
        price (pd.Series or `dict` of `float`): 股票代號對應到的價格,若無則使用最近個交易日的收盤價。
        odd_lot (bool): 是否考慮零股。預設為 False,只使用整張操作。
        board_lot_size (int): 一張股票等於幾股。預設為1000,一張等於1000股。
        allocation (func): 資產配置演算法選定,預設為`finlab.online.utils.greedy_allocation`(最大資金部屬貪婪法)。
    !!! example
        ```py
        from finlab import backtest
        from finlab.online.order_executor import Position

        report1 = backtest.sim(...)
        report2 = backtest.sim(...)

        position1 = Position.from_report(report1, 1000000) # 策略操作金額一百萬
        position2 = Position.from_report(report2, 1000000) # 策略操作金額一百萬

        total_position = position1 + position2
        ```
    """

    # next trading date arrived

    if hasattr(report.market_info, 'market_close_at_timestamp'):
        next_trading_time = report.market_info.market_close_at_timestamp(report.next_trading_date)
    else:
        # tw stock only
        tz = datetime.timezone(datetime.timedelta(hours=8))
        next_trading_time = report.next_trading_date.tz_localize(tz) + datetime.timedelta(hours=16)

    now = datetime.datetime.now(tz=datetime.timezone.utc)

    if now >= next_trading_time:
        w = report.next_weights.copy()
    else:
        w = report.weights.copy()

    ###################################
    # handle stoploss and takeprofit
    ###################################

    is_sl_tp = report.actions.isin(['sl_', 'tp_','sl', 'tp'])

    if sum(is_sl_tp):
        exit_stocks = report.actions[is_sl_tp].index.intersection(w.index)
        w.loc[exit_stocks] = 0

    ######################################################
    # handle exit now and enter in next trading date
    ######################################################

    is_exit_enter = report.actions.isin(['sl_enter', 'tp_enter'])
    if sum(is_exit_enter) and now < next_trading_time:
        exit_stocks = report.actions[is_exit_enter].index.intersection(w.index)
        w.loc[exit_stocks] = 0

    # todo: check if w.index is unique and remove this line if possible
    w = w.groupby(w.index).last()

    if 'price' not in kwargs:
        if hasattr(report.market_info, 'get_reference_price'):
            price = report.market_info.get_reference_price()

        else:
            price = report.market_info.get_price('close', adj=False).iloc[-1].to_dict()

        kwargs['price'] = price


    # find w.index not in price.keys()
    for s in w.index:
        if s.split(' ')[0] not in kwargs['price']:
            w = w.drop(s)
            logger.warning(f"Stock {s} is not in price data. It is dropped from the position.")

    return cls.from_weight(w, fund, **kwargs)

from_weight classmethod

from_weight(weights, fund, price=None, odd_lot=False, board_lot_size=1000, allocation=greedy_allocation, precision=None, **kwargs)

利用 weight 建構股票部位

ATTRIBUTE DESCRIPTION
weights

股票詳細部位

TYPE: `dict` of `float`

fund

資金大小

TYPE: Number

price

股票代號對應到的價格,若無則使用最近個交易日的收盤價。

TYPE: pd.Series or `dict` of `float`

odd_lot

是否考慮零股

TYPE: bool

board_lot_size

一張股票等於幾股

TYPE: int

precision

計算張數時的精度,預設為 None 代表依照 board_lot_size 而定,而 1 代表 0.1 張,2 代表 0.01 張,以此類推。

TYPE: int or None

allocation

資產配置演算法選定,預設為預設為finlab.online.utils.greedy_allocation(最大資金部屬貪婪法)

TYPE: func

margin_trading

做多部位是否使用融資

TYPE: bool

short_selling

做空部位是否使用融券

TYPE: bool

day_trading_long

做多部位為當沖先做多

TYPE: bool

day_trading_short

做空部位為當沖先做空

TYPE: bool

Examples:

例如,用 100 萬的資金,全部投入,持有 1101 和 2330 各一半:

from finlab.online.order_executor import Position

Position.from_weight({
    '1101': 0.5,
    '2330': 0.5,
}, fund=1000000)
output
[
  {'stock_id': '1101', 'quantity': 13, 'order_condition': <OrderCondition.CASH: 1>},
  {'stock_id': '2330', 'quantity': 1, 'order_condition': <OrderCondition.CASH: 1>}
]

Source code in finlab/online/order_executor.py
@classmethod
def from_weight(cls, weights, fund, price=None, odd_lot=False, board_lot_size=1000, allocation=greedy_allocation, precision=None, **kwargs):
    """利用 `weight` 建構股票部位

    Attributes:
        weights (`dict` of `float`): 股票詳細部位
        fund (number.Number): 資金大小
        price (pd.Series or `dict` of `float`): 股票代號對應到的價格,若無則使用最近個交易日的收盤價。
        odd_lot (bool): 是否考慮零股
        board_lot_size (int): 一張股票等於幾股
        precision (int or None): 計算張數時的精度,預設為 None 代表依照 board_lot_size 而定,而 1 代表 0.1 張,2 代表 0.01 張,以此類推。
        allocation (func): 資產配置演算法選定,預設為預設為`finlab.online.utils.greedy_allocation`(最大資金部屬貪婪法)
        margin_trading (bool): 做多部位是否使用融資
        short_selling (bool): 做空部位是否使用融券
        day_trading_long (bool): 做多部位為當沖先做多
        day_trading_short (bool): 做空部位為當沖先做空

    Examples:
          例如,用 100 萬的資金,全部投入,持有 1101 和 2330 各一半:
          ```py
          from finlab.online.order_executor import Position

          Position.from_weight({
              '1101': 0.5,
              '2330': 0.5,
          }, fund=1000000)

          ```
          output
          ```
          [
            {'stock_id': '1101', 'quantity': 13, 'order_condition': <OrderCondition.CASH: 1>},
            {'stock_id': '2330', 'quantity': 1, 'order_condition': <OrderCondition.CASH: 1>}
          ]
          ```
    """

    if precision != None and precision < 0:
        raise ValueError("The precision parameter is out of the valid range >= 0")

    if price is None:
        price = data.get('reference_price').set_index('stock_id')['收盤價'].to_dict()

    if isinstance(price, dict):
        price = pd.Series(price)

    if isinstance(weights, dict):
        weights = pd.Series(weights)

    if precision is not None and board_lot_size != 1:
        logger.warning(
            "The precision parameter is ignored when board_lot_size is not 1.")

    if precision is None:
        precision = 0

    if odd_lot:
        if board_lot_size == 1000:
            precision = max(3, precision)
        elif board_lot_size == 100:
            precision = max(2, precision)
        elif board_lot_size == 10:
            precision = max(1, precision)
        elif board_lot_size == 1:
            precision = max(0, precision)
        else:
            raise ValueError(
                "The board_lot_size parameter is out of the valid range 1, 10, 100, 1000")

    multiple = 10**precision

    allocation = greedy_allocation(
        weights, price*board_lot_size, fund*multiple)[0]

    for s, q in allocation.items():
        allocation[s] = Decimal(q) / multiple

    if not odd_lot:
        for s, q in allocation.items():
            allocation[s] = round(q)

    return cls(allocation, **kwargs)

to_json

to_json(path)

Converts the position dictionary to a JSON file and saves it to the specified path.

PARAMETER DESCRIPTION
path

The path where the JSON file will be saved.

TYPE: str

RETURNS DESCRIPTION

None

Source code in finlab/online/order_executor.py
def to_json(self, path):
    """
    Converts the position dictionary to a JSON file and saves it to the specified path.

    Args:
        path (str): The path where the JSON file will be saved.

    Returns:
        None
    """

    # Custom JSON Encoder that handles Decimal objects
    class DecimalEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, Decimal):
                return str(obj)  # Convert Decimal to string
            # Let the base class default method raise the TypeError
            return json.JSONEncoder.default(self, obj)

    with open(path, 'w') as f:
        json.dump(self.position, f, cls=DecimalEncoder)

finlab.online.order_executor.OrderExecutor

OrderExecutor(target_position, account)

對比實際帳戶與欲部屬的股票部位,進行同步 Arguments: target_position (Position): 想要部屬的股票部位。 account (Account): 目前支援永豐與富果帳戶,請參考 Account 來實做。

Source code in finlab/online/order_executor.py
def __init__(
        self, target_position, account):
    """對比實際帳戶與欲部屬的股票部位,進行同步
        Arguments:
            target_position (Position): 想要部屬的股票部位。
            account (Account): 目前支援永豐與富果帳戶,請參考 Account 來實做。
    """

    if isinstance(target_position, dict):
        target_position = Position(target_position)

    self.account = account
    self.target_position = target_position

cancel_orders

cancel_orders()

刪除所有未實現委託單

Source code in finlab/online/order_executor.py
def cancel_orders(self):
    """刪除所有未實現委託單"""
    orders = self.account.get_orders()
    for oid, o in orders.items():
        if o.status == OrderStatus.NEW or o.status == OrderStatus.PARTIALLY_FILLED:
            self.account.cancel_order(oid)

create_orders

create_orders(market_order=False, best_price_limit=False, view_only=False, extra_bid_pct=0)

產生委託單,將部位同步成 self.target_position 預設以該商品最後一筆成交價設定為限價來下單

ATTRIBUTE DESCRIPTION
market_order

以類市價盡量即刻成交:所有買單掛漲停價,所有賣單掛跌停價

TYPE: bool

best_price_limit

掛芭樂價:所有買單掛跌停價,所有賣單掛漲停價

TYPE: bool

view_only

預設為 False,會實際下單。若設為 True,不會下單,只會回傳欲執行的委託單資料(dict)

TYPE: bool

extra_bid_pct

以該百分比值乘以價格進行追價下單,如設定為 0.05 時,將以當前價的 +(-)5% 的限價進買入(賣出),也就是更有機會可以成交,但是成交價格可能不理想; 假如設定為 -0.05 時,將以當前價的 -(+)5% 進行買入賣出,也就是限價單將不會立即成交,然而假如成交後,價格比較理想。參數有效範圍為 -0.1 到 0.1 內。

TYPE: float

Source code in finlab/online/order_executor.py
def create_orders(self, market_order=False, best_price_limit=False, view_only=False, extra_bid_pct=0):
    """產生委託單,將部位同步成 self.target_position
    預設以該商品最後一筆成交價設定為限價來下單

    Attributes:
        market_order (bool): 以類市價盡量即刻成交:所有買單掛漲停價,所有賣單掛跌停價
        best_price_limit (bool): 掛芭樂價:所有買單掛跌停價,所有賣單掛漲停價
        view_only (bool): 預設為 False,會實際下單。若設為 True,不會下單,只會回傳欲執行的委託單資料(dict)
        extra_bid_pct (float): 以該百分比值乘以價格進行追價下單,如設定為 0.05 時,將以當前價的 +(-)5% 的限價進買入(賣出),也就是更有機會可以成交,但是成交價格可能不理想;
            假如設定為 -0.05 時,將以當前價的 -(+)5% 進行買入賣出,也就是限價單將不會立即成交,然而假如成交後,價格比較理想。參數有效範圍為 -0.1 到 0.1 內。
    """

    orders = self.generate_orders()
    return self.execute_orders(orders, market_order, best_price_limit, view_only, extra_bid_pct)

    if [market_order, best_price_limit, bool(extra_bid_pct)].count(True) > 1:
        raise ValueError("Only one of 'market_order', 'best_price_limit', or 'extra_bid_pct' can be set.")
    if extra_bid_pct < 0 or extra_bid_pct > 0.1:
        raise ValueError("The extra_bid_pct parameter is out of the valid range 0 to 0.1")

    present_position = self.account.get_position()
    orders = (self.target_position - present_position).position

    if view_only:
        return orders

    self.cancel_orders()
    stocks = self.account.get_stocks(list({o['stock_id'] for o in orders}))

    # make orders
    for o in orders:

        if o['quantity'] == 0:
            continue

        stock = stocks[o['stock_id']]
        action = Action.BUY if o['quantity'] > 0 else Action.SELL
        price = stock.close if isinstance(stock.close, numbers.Number) else (
                stock.bid_price if action == Action.BUY else stock.ask_price
                )

        if best_price_limit:
            price_string = 'LOWEST' if action == Action.BUY else 'HIGHEST'
        elif market_order:
            price_string = 'HIGHEST' if action == Action.BUY else 'LOWEST'
        else:
            price_string = str(price)

        extra_bid_text = ''
        if extra_bid_pct > 0:
            extra_bid_text = f'with extra bid {extra_bid_pct*100}%'

        print('execute', action, o['stock_id'], 'X', abs(
            o['quantity']), '@', price_string, extra_bid_text, o['order_condition'])

        quantity = abs(o['quantity'])
        board_lot_quantity = int(abs(quantity // 1))
        odd_lot_quantity = int(abs(round(1000 * (quantity % 1))))

        if self.account.sep_odd_lot_order():
            if odd_lot_quantity != 0:
                self.account.create_order(action=action,
                                          stock_id=o['stock_id'],
                                          quantity=odd_lot_quantity,
                                          price=price, market_order=market_order,
                                          order_cond=o['order_condition'],
                                          odd_lot=True,
                                          best_price_limit=best_price_limit,
                                          extra_bid_pct=extra_bid_pct)

            if board_lot_quantity != 0:
                self.account.create_order(action=action,
                                          stock_id=o['stock_id'],
                                          quantity=board_lot_quantity,
                                          price=price, market_order=market_order,
                                          order_cond=o['order_condition'],
                                          best_price_limit=best_price_limit,
                                          extra_bid_pct=extra_bid_pct)
        else:
            self.account.create_order(action=action,
                                      stock_id=o['stock_id'],
                                      quantity=quantity,
                                      price=price, market_order=market_order,
                                      order_cond=o['order_condition'],
                                      best_price_limit=best_price_limit,
                                      extra_bid_pct=extra_bid_pct)

    return orders

execute_orders

execute_orders(orders, market_order=False, best_price_limit=False, view_only=False, extra_bid_pct=0)

產生委託單,將部位同步成 self.target_position 預設以該商品最後一筆成交價設定為限價來下單

ATTRIBUTE DESCRIPTION
orders

欲下單的部位,通常是由 self.generate_orders 產生。

TYPE: list

market_order

以類市價盡量即刻成交:所有買單掛漲停價,所有賣單掛跌停價

TYPE: bool

best_price_limit

掛芭樂價:所有買單掛跌停價,所有賣單掛漲停價

TYPE: bool

view_only

預設為 False,會實際下單。若設為 True,不會下單,只會回傳欲執行的委託單資料(dict)

TYPE: bool

extra_bid_pct

以該百分比值乘以價格進行追價下單,如設定為 0.05 時,將以當前價的 +(-)5% 的限價進買入(賣出),也就是更有機會可以成交,但是成交價格可能不理想; 假如設定為 -0.05 時,將以當前價的 -(+)5% 進行買入賣出,也就是限價單將不會立即成交,然而假如成交後,價格比較理想。參數有效範圍為 -0.1 到 0.1 內。

TYPE: float

Source code in finlab/online/order_executor.py
def execute_orders(self, orders, market_order=False, best_price_limit=False, view_only=False, extra_bid_pct=0):
    """產生委託單,將部位同步成 self.target_position
    預設以該商品最後一筆成交價設定為限價來下單

    Attributes:
        orders (list): 欲下單的部位,通常是由 `self.generate_orders` 產生。
        market_order (bool): 以類市價盡量即刻成交:所有買單掛漲停價,所有賣單掛跌停價
        best_price_limit (bool): 掛芭樂價:所有買單掛跌停價,所有賣單掛漲停價
        view_only (bool): 預設為 False,會實際下單。若設為 True,不會下單,只會回傳欲執行的委託單資料(dict)
        extra_bid_pct (float): 以該百分比值乘以價格進行追價下單,如設定為 0.05 時,將以當前價的 +(-)5% 的限價進買入(賣出),也就是更有機會可以成交,但是成交價格可能不理想;
            假如設定為 -0.05 時,將以當前價的 -(+)5% 進行買入賣出,也就是限價單將不會立即成交,然而假如成交後,價格比較理想。參數有效範圍為 -0.1 到 0.1 內。
    """

    if [market_order, best_price_limit, bool(extra_bid_pct)].count(True) > 1:
        raise ValueError("Only one of 'market_order', 'best_price_limit', or 'extra_bid_pct' can be set.")
    if extra_bid_pct < -0.1 or extra_bid_pct > 0.1:
        raise ValueError("The extra_bid_pct parameter is out of the valid range 0 to 0.1")

    self.cancel_orders()
    stocks = self.account.get_stocks(list({o['stock_id'] for o in orders}))

    pinfo = None
    if hasattr(self.account, 'get_price_info'):
        pinfo = self.account.get_price_info()

    # make orders
    for o in orders:

        if o['quantity'] == 0:
            continue

        if o['stock_id'] not in stocks:
            logging.warning(o['stock_id'] + 'not in stocks... skipped!')
            continue

        stock = stocks[o['stock_id']]
        action = Action.BUY if o['quantity'] > 0 else Action.SELL
        price = stock.close if isinstance(stock.close, numbers.Number) else (
                stock.bid_price if action == Action.BUY else stock.ask_price
                )

        if extra_bid_pct != 0:
            price = calculate_price_with_extra_bid(price, extra_bid_pct if action == Action.BUY else -extra_bid_pct)

        if pinfo and o['stock_id'] in pinfo:
            limitup = float(pinfo[o['stock_id']]['漲停價'])
            limitdn = float(pinfo[o['stock_id']]['跌停價'])
            price = max(price, limitdn)
            price = min(price, limitup)
        else:
            logger.warning('No price info for stock %s', o['stock_id'])

        if isinstance(price, Decimal):
            price = format(price, 'f')

        if best_price_limit:
            price_string = 'LOWEST' if action == Action.BUY else 'HIGHEST'
        elif market_order:
            price_string = 'HIGHEST' if action == Action.BUY else 'LOWEST'
        else:
            price_string = str(price)

        extra_bid_text = ''
        if extra_bid_pct > 0:
            extra_bid_text = f'with extra bid {extra_bid_pct*100}%'

        logger.warning('%-11s %-6s X %-10s @ %-11s %s %s', action, o['stock_id'], abs(o['quantity']), price_string, extra_bid_text, o['order_condition'])

        quantity = abs(o['quantity'])
        board_lot_quantity = int(abs(quantity // 1))
        odd_lot_quantity = int(abs(round(1000 * (quantity % 1))))

        if view_only:
            continue

        if self.account.sep_odd_lot_order():
            if odd_lot_quantity != 0:
                self.account.create_order(action=action,
                                          stock_id=o['stock_id'],
                                          quantity=odd_lot_quantity,
                                          price=price, market_order=market_order,
                                          order_cond=o['order_condition'],
                                          odd_lot=True,
                                          best_price_limit=best_price_limit,
                                          )

            if board_lot_quantity != 0:
                self.account.create_order(action=action,
                                          stock_id=o['stock_id'],
                                          quantity=board_lot_quantity,
                                          price=price, market_order=market_order,
                                          order_cond=o['order_condition'],
                                          best_price_limit=best_price_limit,
                                          )
        else:
            self.account.create_order(action=action,
                                      stock_id=o['stock_id'],
                                      quantity=quantity,
                                      price=price, market_order=market_order,
                                      order_cond=o['order_condition'],
                                      best_price_limit=best_price_limit,
                                      )

    return orders

generate_orders

generate_orders()

Generate orders based on the difference between target position and present position.

Returns: orders (dict): Orders to be executed.

Source code in finlab/online/order_executor.py
def generate_orders(self):
    """
    Generate orders based on the difference between target position and present position.

    Returns:
    orders (dict): Orders to be executed.
    """

    target_position = Position.from_list(copy.copy(self.target_position.position))

    if hasattr(self.account, 'base_currency'):
        base_currency = self.account.base_currency
        for pp in target_position.position:
            if pp['stock_id'][-len(base_currency):] == base_currency:
                pp['stock_id'] = pp['stock_id'][:-len(base_currency)]
            else:
                raise ValueError(f"Stock ID {pp['stock_id']} does not end with {base_currency}")

    present_position = self.account.get_position()
    orders = (target_position - present_position).position
    return orders

show_alerting_stocks

show_alerting_stocks()

產生下單部位是否有警示股,以及相關資訊

Source code in finlab/online/order_executor.py
def show_alerting_stocks(self):
    """產生下單部位是否有警示股,以及相關資訊"""

    present_position = self.account.get_position()
    new_orders = (self.target_position - present_position).position

    stock_ids = [o['stock_id'] for o in new_orders]
    quantity = {o['stock_id']: o['quantity'] for o in new_orders}

    res = requests.get('https://www.sinotrade.com.tw/Stock/Stock_3_8_3')
    dfs = pd.read_html(res.text)
    credit_sids = dfs[0][dfs[0]['股票代碼'].astype(str).isin(stock_ids)]['股票代碼']

    res = requests.get('https://www.sinotrade.com.tw/Stock/Stock_3_8_1')
    dfs = pd.read_html(res.text)
    credit_sids = pd.concat(
        [credit_sids, dfs[0][dfs[0]['股票代碼'].astype(str).isin(stock_ids)]['股票代碼'].astype(str)])
    credit_sids.name = None

    if credit_sids.any():
        close = data.get('price:收盤價').ffill().iloc[-1]
        for sid in list(credit_sids.values):
            if quantity[sid] > 0:
                total_amount = quantity[sid]*close[sid]*1000*1.1
                print(
                    f"買入 {sid} {quantity[sid]:>5} 張 - 總價約 {total_amount:>15.2f}")
            else:
                total_amount = quantity[sid]*close[sid]*1000*0.9
                print(
                    f"賣出 {sid} {quantity[sid]:>5} 張 - 總價約 {total_amount:>15.2f}")

update_order_price

update_order_price(extra_bid_pct=0)

更新委託單,將委託單的限價調整成當天最後一筆價格。 (讓沒成交的限價單去追價) Attributes: extra_bid_pct (float): 以該百分比值乘以價格進行追價下單,如設定為 0.1 時,將以超出(低於)現價之10%價格下單,以漲停(跌停)價為限。參數有效範圍為 0 到 0.1 內

Source code in finlab/online/order_executor.py
def update_order_price(self, extra_bid_pct=0):
    """更新委託單,將委託單的限價調整成當天最後一筆價格。
    (讓沒成交的限價單去追價)
    Attributes:
        extra_bid_pct (float): 以該百分比值乘以價格進行追價下單,如設定為 0.1 時,將以超出(低於)現價之10%價格下單,以漲停(跌停)價為限。參數有效範圍為 0 到 0.1 內
        """
    if extra_bid_pct < -0.1 or extra_bid_pct > 0.1:
        raise ValueError("The extra_bid_pct parameter is out of the valid range 0 to 0.1")
    orders = self.account.get_orders()
    sids = set([o.stock_id for i, o in orders.items()])
    stocks = self.account.get_stocks(sids)

    pinfo = None
    if hasattr(self.account, 'get_price_info'):
        pinfo = self.account.get_price_info()

    for i, o in orders.items():
        if o.status == OrderStatus.NEW or o.status == OrderStatus.PARTIALLY_FILLED:

            price = stocks[o.stock_id].close
            if extra_bid_pct > 0:

                price = calculate_price_with_extra_bid(price, extra_bid_pct if o.action == Action.BUY else -extra_bid_pct)

            if pinfo and o.stock_id in pinfo:
                up_limit = float(pinfo[o.stock_id]['漲停價'])
                dn_limit = float(pinfo[o.stock_id]['跌停價'])
                price = max(price, dn_limit)
                price = min(price, up_limit)
            else:
                logger.warning('No price info for stock %s', o.stock_id)

            self.account.update_order(i, price=price)

finlab.online.base_account.Order dataclass

Order status

委託單的狀態

ATTRIBUTE DESCRIPTION
order_id

委託單的 id,與券商 API 所提供的 id 一致

TYPE: str

stock_id

股票代號 ex: '2330'

TYPE: str

action

買賣方向,通常為 'BUY' 或是 'SELL'

TYPE: Action

price

股票買賣的價格(限價單)

TYPE: Number

quantity

委託股票的總數量(張數),允許小數點

TYPE: Number

filled_quantity

以成交股票的數量(張數),允許小數點

TYPE: Number

status

委託狀態,可以設定為:'NEW', 'PARTIALLY_FILLED', 'FILLED', 'CANCEL'

TYPE: OrderStatus

time

委託時間

TYPE: datetime

org_order

券商所提供的委託物件格式

TYPE: Any = None

finlab.online.panel.order_panel

order_panel(account)

下單 GUI 介面 Arguments: account (Account): 請參考 Account 針對不同券商來建構相對應的操作帳戶

Source code in finlab/online/panel.py
def order_panel(account):
    """下單 GUI 介面
        Arguments:
            account (Account): 請參考 Account 針對不同券商來建構相對應的操作帳戶
    """

    strategies = data.get_strategies()

    def calc_position(allocations, odd_lot=False):

        total_position = Position({})

        for (strategy, allocation) in allocations:
            p = strategies[strategy]['positions']
            if 'position' in p:
                p = p['position']

            weights = {pname.split(' ')[0]: pp['next_weight']
                       for pname, pp in p.items() if isinstance(pp, dict)}

            price = account.get_price([s.split(' ')[0] for s in weights])

            # s = account.get_stocks([s.split(' ')[0] for s in weights])
            # price = {pname: s[pname].close for pname in weights}

            # for sid, p in price.items():
            #     if p == 0:
            #         bid_price = s[sid].bid_price if s[sid].bid_price != 0 else s[sid].ask_price
            #         ask_price = s[sid].ask_price if s[sid].ask_price != 0 else s[sid].bid_price
            #         price[sid] = (bid_price + ask_price)/2

            #     if price[sid] == 0:
            #         raise Exception(
            #             f"Stock {sid} has no price to reference. Use latest close of previous trading day")

            position = Position.from_weight(
                weights, allocation, price=price, odd_lot=odd_lot)
            total_position += position

        return total_position

    ss = StrategySelector(strategies)
    op = OrderPanel()

    def position_check(strategy_selector,  odd_lot=False):
        with strategy_selector.strategy_out:
            pos = calc_position(strategy_selector.strategy_allocation, odd_lot)
            strategy_stocks = {s: [i.split(' ')[0] for i in strategies[s]['positions'].keys()
                                   if isinstance(strategies[s]['positions'][i], dict)]
                               for (s, a) in strategy_selector.strategy_allocation}
            op.set_strategy_stocks(strategy_stocks)
            op.set_position(pos, account)

    ss.set_callback(position_check)

    display(ss.out)
    display(op.out)
    display(op.out2)
    return {'strategy_selector': ss, 'order_panel': op}