[Python] 計算股票最低不虧損賣價

這是我的第一隻python程式,就想寫些有點用的小工具,
主要分成selling_price.py(主程式)跟selling_price_gui.py(GUI)兩隻,
Mac執行:
python3 selling_price_gui.py
python3 selling_price.py 買入價格
Windows執行:
py -3 selling_price_gui.py
py -3 selling_price.py 買入價格

主要遇到的問題應該是GUI輸入浮點數跟主程式處理浮點數的問題,
主程式用round函式取近似值抓精度來解決,GUI則一輸入就丟去檢查

GUI的code是參考程式語言教學誌跟良葛格的python專欄:
http://www.kaiching.org/2014/07/python-guide.html
https://openhome.cc/Gossip/CodeData/PythonTutorial/index.html


selling_price.py 程式碼&註解:


# -*- coding:utf-8 -*-
# file: selling_price.py
#

class SellingPrice():

    # 常數定義
    # TAX: 證券交易稅 3/1000
    # CHARGE: 手續費 1.425/1000
    TAX = 0.003
    CHARGE = 0.001425
    # ticks_table 欄位定義
    # MINIMUM: tick range 最小邊界
    # MAXIMUM: tick range 最大邊界
    # TICK: 這個 range 的 tick 大小
    MINIMUM = 0
    MAXIMUM = 1
    TICK = 2
    # 使用 round 函式解決浮點數精確度的問題
    # 取到小數點後兩位數
    ACCURATE = 2

    # 不同的股價區間有不同的 tick 數值
    ticks_table = [
        [0, 10, 0.01],
        [10, 50, 0.05],
        [50, 100, 0.10],
        [100, 500, 0.50],
        [500, 1000, 1],
        [1000, 0, 5]
    ]

    # 藉由輸入的數值 cost (購買一張1000股的價格)
    # 去 ticks_table 裡面找到對應的 tick 值
    def get_tick(self, cost):
        for ticks in self.ticks_table:
            if cost < ticks[self.MAXIMUM]:
                return ticks[self.TICK]
        return ticks[self.TICK]

    # 藉由輸入的數值 cost (購買一張1000股的價格)
    # 算出所需的購入金額 ($NT dollars)
    def calculate_cost_nt(self, cost):
        return int(cost * 1000 * (1 + self.CHARGE))

    # 藉由輸入的數值 sell_price (賣出一張1000股的價格)
    # 算出最終賣出的金額 ($NT dollars)
    def calculate_sell_nt(self, sell_price):
        return int(sell_price * 1000) - int(sell_price * 1000 * (self.CHARGE + self.TAX))

    # 檢查買入賣出的金額不能為 0
    # 另外也根據買入賣出價格所處的 range 來抓出 tick
    # 在那個 range 中的最小移動單位應該要為 tick
    # 假如沒辦法被 tick 除盡,代表是不正確的輸入值
    def input_check(self, cost, tick):
        if cost <= 0:
            return False
        if round(cost * 100, self.ACCURATE) % round(tick * 100, self.ACCURATE) != 0:
            return False
        return True

    # 先檢查輸入值是否合法,再算出損益
    # (賣出價格 - 購入成本) = 實際損益
    def calculate_gain_loss(self, cost, sell):
        if (self.input_check(cost, self.get_tick(cost)) == False) | \
           (self.input_check(sell, self.get_tick(sell)) == False):
            return 0
        return (self.calculate_sell_nt(sell) - self.calculate_cost_nt(cost))

    # 先檢查輸入值,算出購入成本
    # 再找出滿足條件的 n: "買入價格 + (n * tick)" >= "購入成本"
    # 根據 n 算出 "至少不虧本的賣價 = 買入價格 + (n * tick)"
    def calculate_least_price(self, cost):
        tick = self.get_tick(cost)
        if self.input_check(cost, tick) == False:
            return 0
        cost_nt = self.calculate_cost_nt(cost)
        sell_price = round(cost + tick, self.ACCURATE)
        while True:
            sell_nt = self.calculate_sell_nt(sell_price)
            if sell_nt >= cost_nt:
                return sell_price
            sell_price = round(sell_price + tick, self.ACCURATE)

import sys
if __name__ == '__main__':
    Selling = SellingPrice()
    sell_price = Selling.calculate_least_price(float(sys.argv[1]))
    if sell_price == 0:
        print("Input valure error!")
    else:
        print(sell_price)

selling_price_gui.py 程式碼&註解:


# -*- coding:utf-8 -*-
# file: selling_price_gui.py
#

import tkinter
from selling_price import SellingPrice

class SellingPriceGUI(tkinter.Frame):

    ACTION_INSERT = '1'
    # input 最大輸入的長度
    INPUT_MAX_LENGTH = 10
    # 設定函式 check_imput 檢查的類型,可以是 int 或 float
    check_type = float
    Selling = SellingPrice()

    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        # tkinter 有 pack、grid、place三種佈局管理
        # grid(row=列, column=行, rowspan=佔用列數, columnspan=佔用行數)
        self.grid()
        self.create_widgets()

    def create_widgets(self):
        # (action)'%d', (index)'%i', (value_if_allowed)'%P'
        # 代表傳入 check_imput 的三個參數的型態
        validate_command = (self.register(self.check_imput), '%d', '%i', '%P')

        self.buy_lable = tkinter.Label(self)
        self.buy_lable["text"] = "買入價格:"
        self.buy_lable.grid(row=0, column=0)

        self.buy_entry = tkinter.Entry(self, validate = 'key', validatecommand = validate_command)
        self.buy_entry["width"] = 20
        self.buy_entry.grid(row=0, column=1)

        self.sell_lable = tkinter.Label(self)
        self.sell_lable["text"] = "賣出價格:"
        self.sell_lable.grid(row=1, column=0)

        self.sell_entry = tkinter.Entry(self, validate = 'key', validatecommand = validate_command)
        self.sell_entry["width"] = 20
        self.sell_entry.grid(row=1, column=1)

        self.least_lable = tkinter.Label(self)
        self.least_lable["text"] = "不虧損價:"
        self.least_lable.grid(row=2, column=0)

        self.least_sell_result = tkinter.Label(self)
        self.least_sell_result["text"] = ""
        self.least_sell_result.grid(row=2, column=1)

        self.profit_lable = tkinter.Label(self)
        self.profit_lable["text"] = "實際盈虧:"
        self.profit_lable.grid(row=3, column=0)

        self.profit_loss_result = tkinter.Label(self)
        self.profit_loss_result["text"] = ""
        self.profit_loss_result.grid(row=3, column=1)

        self.clear_button = tkinter.Button(self)
        self.clear_button["text"] = "清除"
        self.clear_button.grid(row=4, column=0)
        self.clear_button["command"] = self.clear

        self.count_button = tkinter.Button(self)
        self.count_button["text"] = "計算"
        self.count_button["width"] = 20
        self.count_button.grid(row=4, column=1)
        self.count_button["command"] = self.calculate

    # 限制輸入的長度,以及內容必須是整數或是浮點數
    # 檢查的類型則是由 self.check_type 決定
    def check_imput(self, action, index, value_if_allowed):
        if action != self.ACTION_INSERT:
            return True
        if int(index) > self.INPUT_MAX_LENGTH:
            return False
        try:
            return self.check_type(value_if_allowed)
        except ValueError:
            return False

    def calculate(self):
        # 計算最低不虧損價格
        cost_input = self.buy_entry.get()
        if cost_input != "":
            least_sell_price = self.Selling.calculate_least_price(float(cost_input))
            if least_sell_price == 0:
                self.least_sell_result["text"] = "輸入錯誤!"
            else:
                self.least_sell_result["text"] = least_sell_price
        # 計算實際損益
        sell_input = self.sell_entry.get()
        if sell_input != "":
            gain_loss = self.Selling.calculate_gain_loss(float(cost_input), float(sell_input))
            if gain_loss == 0:
                self.profit_loss_result["text"] = "輸入錯誤!"
            else:
                self.profit_loss_result["text"] = str(gain_loss) + "元"

    # 清除輸入輸出值
    def clear(self):
        self.buy_entry.delete(0, self.buy_entry["width"])
        self.sell_entry.delete(0, self.sell_entry["width"])
        self.least_sell_result["text"] = ""
        self.profit_loss_result["text"] = ""

if __name__ == '__main__':
    root = tkinter.Tk()
    root.title("賣價計算")
    app = SellingPriceGUI(master=root)
    app.mainloop()