Thực hiện một chiến lược giao dịch định lượng tiền kỹ thuật số Dual Thrust bằng Python

Tác giả:Tốt, Tạo: 2019-08-13 14:52:58, Cập nhật: 2023-10-19 21:10:01

img

Đánh giá về thuật toán giao dịch Dual Thrust

Các thuật toán giao dịch Dual Thrust là một chiến lược giao dịch định lượng nổi tiếng được phát triển bởi Michael Chalek. Nó thường được sử dụng trong thị trường tương lai, ngoại hối và chứng khoán. Khái niệm của Dual Thrust là một hệ thống giao dịch đột phá điển hình, nó sử dụng hệ thống đà đà đà đà đà đà đà để xây dựng thời gian ngược mới dựa trên giá lịch sử, điều này về lý thuyết làm cho nó ổn định hơn trong bất kỳ khoảng thời gian nào.

Trong bài viết này, chúng tôi đưa ra chi tiết logic chi tiết về chiến lược này và cho thấy cách thực hiện thuật toán này trên nền tảng định lượng của nhà phát minh. Đầu tiên, chúng ta sẽ chọn giá lịch sử được giao dịch, dựa trên giá đóng cửa gần nhất N ngày, tính toán giá cao nhất và giá thấp nhất.

Chúng tôi đã thử nghiệm chiến lược này với một cặp giao dịch duy nhất trong hai trạng thái thị trường phổ biến, thị trường xu hướng và thị trường lung lay. Kết quả cho thấy hệ thống giao dịch động này hoạt động tốt hơn trong thị trường xu hướng, kích hoạt một số tín hiệu mua bán không hiệu quả trong thị trường biến động hơn. Trong thị trường phân đoạn, chúng tôi có thể điều chỉnh các tham số để có lợi nhuận tốt hơn.

DT chiến lược

Mô hình logic của nó là một chiến lược giao dịch trong ngày phổ biến. Chiến lược phá vỡ khoảng mở cửa dựa trên giá mở cửa hôm nay cộng với hoặc trừ một tỷ lệ phần trăm nhất định của chiều rộng ngày hôm qua để xác định đường dẫn tăng xuống. Khi giá phá vỡ đường dẫn trên, nó sẽ mua, và khi nó phá vỡ đường dẫn dưới, nó sẽ mở đường trống.

Nguyên tắc chiến lược

  • Sau khi đóng cửa, tính toán hai giá trị: giá cao nhất - giá đóng cửa, giá đóng cửa - giá thấp nhất. Sau đó lấy giá trị lớn hơn trong hai giá trị này và nhân giá trị đó bằng 0.7.

  • Sau khi bắt đầu giao dịch vào ngày hôm sau, ghi lại giá mở cửa, sau đó mua ngay lập tức khi giá vượt quá (giá mở cửa + giá kích hoạt) hoặc bán ngắn khi giá thấp hơn (giá mở cửa - giá kích hoạt).

  • Chiến lược này không có tổn thất dừng rõ ràng. Hệ thống này là một hệ thống ngược, nghĩa là nếu có một lệnh đặt vị trí trống khi giá vượt quá giá mở + kích hoạt, thì nó sẽ gửi hai thanh toán (một đóng vị trí sai, một mở vị trí đúng hướng). Vì cùng một lý do, nếu có một lệnh bán nhiều vị trí với giá thấp hơn giá mở - kích hoạt, thì nó sẽ gửi hai lệnh bán.

img

Các biểu thức toán học cho chiến lược DT

Phạm vi = giá trị tối đa (HH-LC, HC-LL)

Các phương pháp tính toán cho tín hiệu đa đầu là:

cap = mở + K1 × Rangecap = mở + K1 × Range

Cách tính cho tín hiệu ngắn không đầu là:

sàn = mở K2 × Rangefloor = mở K2 × Range

Trong đó K1 và K2 là các tham số. Khi K1 lớn hơn K2, kích hoạt tín hiệu đa đầu, và ngược lại. Để chứng minh, chúng ta chọn K1 = K2 = 0.5. Trong giao dịch thực tế, chúng ta vẫn có thể sử dụng dữ liệu lịch sử để tối ưu hóa các tham số này hoặc điều chỉnh các tham số theo xu hướng thị trường.

img

Hệ thống này là một hệ thống đảo ngược, vì vậy nếu nhà đầu tư nắm giữ một vị trí trống khi giá phá vỡ đường cao, họ sẽ phải làm sạch vị trí trống trước khi mở nhiều vị trí. Nếu nhà đầu tư nắm giữ nhiều vị trí trống khi giá phá vỡ đường thấp, họ nên làm sạch nhiều vị trí trống trước khi mở một vị trí trống mới.

Những cải tiến trong chiến lược DT:

Trong thiết lập phạm vi, bốn điểm giá của N ngày trước (độ cao, mở, thấp, đóng) được giới thiệu, giúp cho phạm vi tương đối ổn định trong một khoảng thời gian nhất định và có thể được sử dụng để theo dõi xu hướng ngày.

Các điều kiện kích hoạt của chiến lược này là nhiều và trống, xem xét mức độ không đối xứng, phạm vi tham chiếu giao dịch đa không gian nên được chọn với số lần lặp khác nhau, cũng có thể được xác định bằng các tham số K1 và K2. Khi K1 < K2, tín hiệu đa đầu dễ dàng được kích hoạt, và khi K1 > K2, tín hiệu trống dễ dàng được kích hoạt.

Do đó, khi sử dụng chiến lược này, bạn có thể tham khảo các tham số tốt nhất để xem xét lại dữ liệu lịch sử. Mặt khác, bạn có thể điều chỉnh K1 và K2 theo giai đoạn dựa trên đánh giá của bạn về hậu trường hoặc các chỉ số kỹ thuật chu kỳ chính khác.

Đây là một cách giao dịch điển hình để chờ tín hiệu, đi vào thị trường, lấy lợi nhuận, và sau đó rời khỏi thị trường, nhưng hiệu quả rất tốt.

triển khai chiến lược DT trên nền tảng định lượng nhà phát minh

Chúng tôi mở cửa.FMZ.COM, đăng nhập tài khoản, nhấp vào Control Center, triển khai quản trị viên và robot.

Để biết cách triển khai người quản lý và robot, hãy tham khảo bài viết trước của tôi:https://www.fmz.com/bbs-topic/4140

Người đọc muốn mua một nhà quản lý triển khai máy chủ điện toán đám mây của riêng họ có thể tham khảo bài viết này:https://www.fmz.com/bbs-topic/2848

Sau đó, chúng ta nhấp vào thư viện chính sách ở menu bên trái và nhấp vào tạo chính sách.

Trong góc trên bên phải của trang viết chính sách, hãy nhớ chọn ngôn ngữ lập trình là Python, như sau:

img

Tiếp theo, chúng tôi viết mã Python vào trang chỉnh sửa mã, dưới đó có mã, có lời giải thích từng dòng rất chi tiết, mà người đọc có thể dần dần hiểu và cảm nhận.

Chúng tôi đã thử nghiệm chiến lược này với OKCoin tương lai:

import time # 这里需要引入python自带的时间库,后边的程序会用到

class Error_noSupport(BaseException): # 我们定义一个名为ChartCfg的全局class,用来初始化策略图表设置。对象有很多关于图表功能的属性。图表库为:HighCharts
    def __init__(self): # log出提示信息
        Log("只支持OKCoin期货!#FF0000")

class Error_AtBeginHasPosition(BaseException):
    def __init__(self):
        Log("启动时有期货持仓! #FF0000")

ChartCfg = {
    '__isStock': True, # 该属性用于控制是否显示为单独控制数据序列(可以在图表上取消单独一个数据序列的显示),如果指定__isStock: false, 则显示为普通图表
    'title': { # title为图表的主要标题
        'text': 'Dual Thrust 上下轨图' # title的一个属性text为标题的文本,这里设置为'Dual Thrust 上下轨图'该文本就会显示在标题位置
    },
    'yAxis': { # 图表坐标Y轴的相关设置
        'plotLines': [{ # Y轴上的水平线(和Y轴垂直),该属性的值是一个数组,即多条水平线的设置
            'value': 0, # 水平线在Y轴上的坐标值
            'color': 'red', # 水平线的颜色
            'width': 2, # 水平线的线宽
            'label': {  # 水平线上的标签
                'text': '上轨', # 标签的文本
                'align': 'center' # 标签的显示位置,这里设置为居中(即 :'center')
            }, 
        }, {       # 第二条水平线([{...},{...}]数组中的第二个元素)
            'value': 0, # 水平线在Y轴上的坐标值
            'color': 'green', # 水平线的颜色
            'width': 2,  # 水平线的线宽
            'label': { # 标签
                'text': '下轨',
                'align': 'center'
            },
        }]
    },
    'series': [{ # 数据序列,即用来在图表上显示数据线、K线、标记等等内容的数据。也是一个数组第一个索引为0。
        'type': 'candlestick', # 索引为0数据序列的类型:'candlestick' 表示为K线图
        'name': '当前周期',  # 数据序列的名称
        'id': 'primary', # 数据序列的ID,用于下一个数据序列相关设置。
        'data': []  # 数据序列的数组,用于储存具体的K线数据
    }, {
        'type': 'flags',  # 数据序列,类型:'flags',在图表上显示标签,表示做多和做空。索引为1。
        'onSeries': 'primary',  # 这个属性表示标签显示在id为'primary'上。
        'data': []    # 保存标签数据的数组。
    }] 
}

STATE_IDLE = 0  # 状态常量,表示空闲
STATE_LONG = 1 # 状态常量,表示持多仓
STATE_SHORT = 2 # 状态常量,表示持空仓
State = STATE_IDLE # 表示当前程序状态 ,初始赋值为空闲

LastBarTime = 0  # K线最后一柱的时间戳(单位为毫秒,1000毫秒等于1秒,时间戳是1970年1月1日到现在时刻的毫秒数是一个很大的正整数)
UpTrack = 0   # 上轨值
BottomTrack = 0 # 下轨值
chart = None # 用于接受Chart这个API函数返回的图表控制对象。用该对象(chart)可以调用其成员函数向图表内写入数据。
InitAccount = None # 初始账户情况
LastAccount = None # 最新账户情况
Counter = { # 计数器,用于记录盈亏次数
    'w': 0, # 赢次数
    'l': 0  # 亏次数
}

def GetPosition(posType):  # 定义一个函数,用来存储账户持仓信息
    positions = exchange.GetPosition() # exchange.GetPosition()是发明者量化的官方API,关于它的用法,请参考我的官方API文档:https://www.fmz.com/api
    return [{'Price': position['Price'], 'Amount': position['Amount']} for position in positions if position['Type'] == posType] # 返回各种持仓信息

def CancelPendingOrders(): # 定义一个函数,专门用来撤单
    while True: # 循环检查
        orders = exchange.GetOrders() # 如果有持仓
        [exchange.CancelOrder(order['Id']) for order in orders if not Sleep(500)] # 撤单语句
        if len(orders) == 0: # 逻辑判断
            break 

def Trade(currentState,nextState): # 定义一个函数,用来判断下单逻辑
    global InitAccount,LastAccount,OpenPrice,ClosePrice # 定义全局作用域
    ticker = _C(exchange.GetTicker) # 关于_C的用法,请参考:https://www.fmz.com/api
    slidePrice = 1 # 定义滑点值
    pfn = exchange.Buy if nextState == STATE_LONG else exchange.Sell # 买卖判断逻辑
    if currentState != STATE_IDLE: # 循环开始
        Log(_C(exchange.GetPosition)) # 日志信息 
        exchange.SetDirection("closebuy" if currentState == STATE_LONG else "closesell") # 调整下单方向,特别是下过单后
        while True:
            ID = pfn( (ticker['Last'] - slidePrice) if currentState == STATE_LONG else (ticker['Last'] + slidePrice), AmountOP) # 限价单,ID = pfn(-1, AmountOP)为市价单,ID = pfn(AmountOP)为市价单
            Sleep(Interval) # 休息一阵,防止API访问频率过快,账户被封。
            Log(exchange.GetOrder(ID)) # Log信息
            ClosePrice = (exchange.GetOrder(ID))['AvgPrice'] # 设置收盘价
            CancelPendingOrders() # 调用撤单函数
            if len(GetPosition(PD_LONG if currentState == STATE_LONG else PD_SHORT)) == 0: # 撤单逻辑
                break 
        account = exchange.GetAccount() # 获取账户信息
        if account['Stocks'] > LastAccount['Stocks']: # 如果当前账户币值大于之前账户币值
            Counter['w'] += 1 # 盈亏计数器中,盈利次数加一
        else:
            Counter['l'] += 1 # 否者亏损次数加一
        Log(account) # log信息
        LogProfit((account['Stocks'] - InitAccount['Stocks']),"收益率:", ((account['Stocks'] - InitAccount['Stocks']) * 100 / InitAccount['Stocks']),'%')
        Cal(OpenPrice,ClosePrice)
        LastAccount = account 
    
    exchange.SetDirection("buy" if nextState == STATE_LONG else "sell") # 这一段的逻辑同上,不再详述
    Log(_C(exchange.GetAccount))
    while True:
        ID = pfn( (ticker['Last'] + slidePrice) if nextState == STATE_LONG else (ticker['Last'] - slidePrice), AmountOP) 
        Sleep(Interval)
        Log(exchange.GetOrder(ID)) 
        CancelPendingOrders()
        pos = GetPosition(PD_LONG if nextState == STATE_LONG else PD_SHORT)
        if len(pos) != 0:
            Log("持仓均价",pos[0]['Price'],"数量:",pos[0]['Amount'])
            OpenPrice = (exchange.GetOrder(ID))['AvgPrice']
            Log("now account:",exchange.GetAccount())
            break 

def onTick(exchange): # 程序主要函数,程序主要逻辑都是在该函数内处理。
    global LastBarTime,chart,State,UpTrack,DownTrack,LastAccount # 定义全局作用域
    records = exchange.GetRecords() # 关于exchange.GetRecords()的用法,请参见:https://www.fmz.com/api
    if not records or len(records) <= NPeriod: # 防止发生意外的判断语句
        return 
    Bar = records[-1] # 取records K线数据的倒数第一个元素,也就是最后一个bar
    if LastBarTime != Bar['Time']:
        HH = TA.Highest(records, NPeriod, 'High')  # 声明HH变量,调用TA.Highest函数计算当前K线数据NPeriod周期内最高价的最大值赋值给HH。
        HC = TA.Highest(records, NPeriod, 'Close') # 声明HC变量,获取NPeriod周期内的收盘价的最大值。
        LL = TA.Lowest(records, NPeriod, 'Low') # 声明LL变量,获取NPeriod周期内的最低价的最小值。
        LC = TA.Lowest(records, NPeriod, 'Close') # 声明LC变量,获取NPeriod周期内的收盘价的最小值。具体TA相关的应用,请参见官方API文档。
        
        Range = max(HH - LC, HC - LL)  # 计算出范围 
        UpTrack = _N(Bar['Open'] + (Ks * Range))  # 根据界面参数的上轨系数Ks最新K线柱的开盘价等,计算出上轨值。
        DownTrack = _N(Bar['Open'] - (Kx * Range)) # 计算下轨值
        if LastBarTime > 0: # 由于LastBarTime该变量初始化设置的值为0,所以第一次运行到此处LastBarTime > 0必定是false,不会执行if块内的代码,而是会执行else块内的代码
            PreBar = records[-2] # 声明一个变量含义是“前一个Bar”把当前K线的倒数第二Bar赋值给它。
            chart.add(0, [PreBar['Time'], PreBar['Open'], PreBar['High'], PreBar['Low'], PreBar['Close']], -1) # 调用chart图标控制类的add函数更新K线数据(用获取的K线数据的倒数第二Bar去更新图标的倒数第一个Bar,因为有新的K线Bar生成)
        else:  # chart.add函数的具体用法请参见API文档,和论坛里的文章。程序第一次运行到此必定执行else块内代码,主要作用是把第一次获取的K线一次性全部添加到图表上。
            for i in range(len(records) - min(len(records), NPeriod * 3), len(records)): # 此处执行一个for循环,循环次数使用K线长度和NPeriod的3倍二者中最小的值,可以保证初始的K线不会画的太多太长。索引是从大到小的。
                b = records[i] # 声明一个临时变量b用来取每次循环索引为records.length - i的K线柱数据。
                chart.add(0,[b['Time'], b['Open'], b['High'], b['Low'], b['Close']]) # 调用chart.add函数向图表添加K线柱,注意add函数最后一个参数如果传入-1就是更新图表上最后一个Bar(柱),如果没传参数,就是向最后添加Bar。执行完i等于2这次循环后(i-- 了已经,此时为1了),就会触发i > 1为false停止循环,可见此处代码只处理到records.length - 2这个Bar,最后一个Bar没有处理。                
        chart.add(0,[Bar['Time'], Bar['Open'], Bar['High'], Bar['Low'], Bar['Close']]) # 由于以上if的2个分支都没处理records.length - 1这个Bar,所以此处处理。添加最新出现的Bar到图表中。
        ChartCfg['yAxis']['plotLines'][0]['value'] = UpTrack  # 把计算出来的上轨值赋值给图表对象(区别于图表控制对象chart),用于稍后显示。
        ChartCfg['yAxis']['plotLines'][1]['value'] = DownTrack # 赋值下轨值
        ChartCfg['subtitle'] = { # 设置副标题
            'text': '上轨' + str(UpTrack) + '下轨' + str(DownTrack) # 副标题文本设置,在副标题上显示出上轨下轨值。
        }
        chart.update(ChartCfg) # 用图表类ChartCfg更新图表
        chart.reset(PeriodShow) # 刷新根据界面参数设置的PeriodShow变量,只保留PeriodShow的值数量的K线柱。
        
        LastBarTime = Bar['Time'] # 此次新产生的Bar的时间戳更新,给LastBarTime用于判断下次循环获取的K线数据最后一个Bar,是否是新产生的。
    else: # 如果LastBarTime等于Bar.Time即:没有新的K线Bar产生。则执行一下{..}内代码
        chart.add(0,[Bar['Time'], Bar['Open'], Bar['High'], Bar['Low'], Bar['Close']], -1) # 用当前K线数据的最后一个Bar(K线的最后一个Bar即当前周期的Bar是不断在变化的),更新图表上的最后一个K线柱。        
    LogStatus("Price:", Bar["Close"], "up:", UpTrack, "down:", DownTrack, "wins:", Counter['w'], "losses:", Counter['l'], "Date:", time.time()) # 调用LogStatus函数显示当前策略的数据在状态栏上。
    msg = "" # 定义一个变量msg。
    if State == STATE_IDLE or State == STATE_SHORT: # 判断当前状态变量State是否等于空闲或者State是否等于持空仓,在空闲状态下可以触发做多,在持空仓状态下可以触发平多仓,并反手。
        if Bar['Close'] >= UpTrack: # 如果当前K线的收盘价大于上轨值,执行if块内代码。
            msg = "做多,触发价:" + str(Bar['Close']) + "上轨" + str(UpTrack) # 给msg赋值,把需要显示的数值组合成字符串。
            Log(msg) # 信息
            Trade(State, STATE_LONG) # 调用上边的Trade函数进行交易
            State = STATE_LONG # 无论开多仓还是反手,此刻程序状态要更新为持多仓。
            chart.add(1,{'x': Bar['Time'], 'color': 'red', 'shape': 'flag', 'title': '多', 'text': msg}) # 在K线相应的位置添加一个标记显示开多。 
    
    if State == STATE_IDLE or State == STATE_LONG: # 做空方向与以上同理,不在赘述。代码完全一致。
        if Bar['Close'] <= DownTrack:
            msg = "做空,触发价:" + str(Bar['Close']) + "下轨" + str(DownTrack)
            Log(msg)
            Trade(State, STATE_SHORT)
            State = STATE_SHORT
            chart.add(1,{'x': Bar['Time'], 'color': 'green', 'shape': 'circlepin', 'title': '空', 'text': msg})

OpenPrice = 0 # 初始化OpenPrice和ClosePrice
ClosePrice = 0
def Cal(OpenPrice, ClosePrice): # 定义一个Cal函数,用来计算策略运行后的盈亏情况
    global AmountOP,State
    if State == STATE_SHORT:
        Log(AmountOP,OpenPrice,ClosePrice,"策略盈亏:", (AmountOP * 100) / ClosePrice - (AmountOP * 100) / OpenPrice, "个币,  手续费:", - (100 * AmountOP * 0.0003), "美元,折合:", _N( - 100 * AmountOP * 0.0003/OpenPrice,8), "个币")
        Log(((AmountOP * 100) / ClosePrice - (AmountOP * 100) / OpenPrice) + (- 100 * AmountOP * 0.0003/OpenPrice))
    if State == STATE_LONG:
        Log(AmountOP,OpenPrice,ClosePrice,"策略盈亏:", (AmountOP * 100) / OpenPrice - (AmountOP * 100) / ClosePrice, "个币,  手续费:", - (100 * AmountOP * 0.0003), "美元,折合:", _N( - 100 * AmountOP * 0.0003/OpenPrice,8), "个币")
        Log(((AmountOP * 100) / OpenPrice - (AmountOP * 100) / ClosePrice) + (- 100 * AmountOP * 0.0003/OpenPrice))

def main(): # 策略程序的主函数。(入口函数)
    global LoopInterval,chart,LastAccount,InitAccount # 定义全局作用域
    if exchange.GetName() != 'Futures_OKCoin':  # 判断添加的交易所对象的名称(通过exchange.GetName函数获取)如果不等于'Futures_OKCoin'即:添加的不是OKCoin期货交易所对象。
        raise Error_noSupport # 抛出异常
    exchange.SetRate(1) # 设置交易所的各种参数
    exchange.SetContractType(["this_week","next_week","quarter"][ContractTypeIdx])  # 确定要交易的哪种具体合约。
    exchange.SetMarginLevel([10,20][MarginLevelIdx]) # 设置保证金率,也就是杠杆。
    
    if len(exchange.GetPosition()) > 0: # 设置容错机制
        raise Error_AtBeginHasPosition
    CancelPendingOrders()
    InitAccount = LastAccount = exchange.GetAccount()
    LoopInterval = min(1,LoopInterval)
    Log("交易平台:",exchange.GetName(), InitAccount)
    LogStatus("Ready...")
    
    LogProfitReset()
    chart = Chart(ChartCfg)
    chart.reset()
    
    LoopInterval = max(LoopInterval, 1)
    while True: # 循环整个交易逻辑,调用onTick函数
        onTick(exchange)
        Sleep(LoopInterval * 1000) # 休息一阵,防止API访问频率过快,账户被封。

Sau khi viết mã, hãy lưu ý rằng chúng ta chưa hoàn thành phần viết toàn bộ chính sách, tiếp theo chúng ta cần thêm các tham số được sử dụng trong chính sách vào trang viết chính sách, cách thêm rất đơn giản, chỉ cần nhấp vào thêm một số bên dưới hộp thoại viết chính sách.

img

Những điều cần thêm:

img

Khi đó, chúng ta đã hoàn thành phần viết của chiến lược, và sau đó, chúng ta hãy bắt đầu kiểm tra lại chiến lược.

Chiến lược kiểm tra lại

Sau khi viết chiến lược, điều đầu tiên chúng ta phải làm là xem nó hoạt động như thế nào trong dữ liệu lịch sử, nhưng xin lưu ý cho độc giả rằng kết quả của việc xem lại không đồng nghĩa với việc dự đoán về tương lai, xem lại chỉ là một thông tin tham khảo để xem xét hiệu quả của chiến lược của chúng ta. Một khi thị trường thay đổi và chiến lược bắt đầu có lỗ lớn, chúng ta nên phát hiện vấn đề kịp thời và sau đó thay đổi chiến lược để phù hợp với môi trường thị trường mới, chẳng hạn như ngưỡng nói trên.

Nhấp vào các chính sách chỉnh sửa trong trang sửa đổi, trong trang chỉnh sửa, các tham số có thể được điều chỉnh theo nhu cầu khác nhau, để điều chỉnh dễ dàng và nhanh chóng, đặc biệt là đối với các chính sách logic phức tạp, nhiều tham số, không cần phải quay lại mã nguồn, thay đổi từng lần.

Chúng tôi chọn nửa năm gần đây, nhấp vào thêm giao dịch tương lai OKCoin và chọn chỉ số giao dịch BTC.

img

Như bạn có thể thấy, trong 6 tháng gần đây, chiến lược này đã thu được rất nhiều lợi nhuận vì xu hướng đơn phương của BTC rất tốt.

img img

Bạn bè có vấn đề có thể đếnhttps://www.fmz.com/bbsNhững lời nhắn tin, bất kể là về chiến lược hay công nghệ của nền tảng, các nhà phát minh định lượng nền tảng có nhân viên chuyên nghiệp luôn sẵn sàng trả lời cho bạn.


Có liên quan

Thêm nữa

MAIDOVECó phải chiến lược này sẽ không mang lại sự dừng lỗ và giảm lợi nhuận?