#
# Copyright 2016 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pandas as pd


def map_transaction(txn):
    """
    Maps a single transaction row to a dictionary.

    Parameters
    ----------
    txn : pd.DataFrame
        A single transaction object to convert to a dictionary.

    Returns
    -------
    dict
        Mapped transaction.
    """

    if isinstance(txn["sid"], dict):
        sid = txn["sid"]["sid"]
        symbol = txn["sid"]["symbol"]
    else:
        sid = txn["sid"]
        symbol = txn["sid"]

    return {
        "sid": sid,
        "symbol": symbol,
        "price": txn["price"],
        "order_id": txn["order_id"],
        "amount": txn["amount"],
        "commission": txn["commission"],
        "dt": txn["dt"],
    }


def make_transaction_frame(transactions):
    """
    Formats a transaction DataFrame.

    Parameters
    ----------
    transactions : pd.DataFrame
        Contains improperly formatted transactional data.

    Returns
    -------
    df : pd.DataFrame
        Daily transaction volume and dollar ammount.
         - See full explanation in tears.create_full_tear_sheet.
    """

    transaction_list = []
    for dt in transactions.index:
        txns = transactions.loc[dt]
        if len(txns) == 0:
            continue

        for txn in txns:
            txn = map_transaction(txn)
            transaction_list.append(txn)
    df = pd.DataFrame(sorted(transaction_list, key=lambda x: x["dt"]))
    df["txn_dollars"] = -df["amount"] * df["price"]

    df.index = list(map(pd.Timestamp, df.dt.values))
    return df


def get_txn_vol(transactions):
    """
    Extract daily transaction data from set of transaction objects.

    Parameters
    ----------
    transactions : pd.DataFrame
        Time series containing one row per symbol (and potentially
        duplicate datetime indices) and columns for amount and
        price.

    Returns
    -------
    pd.DataFrame
        Daily transaction volume and number of shares.
         - See full explanation in tears.create_full_tear_sheet.
    """

    txn_norm = transactions.copy()
    txn_norm.index = txn_norm.index.normalize()
    amounts = txn_norm.amount.abs()
    prices = txn_norm.price
    values = amounts * prices
    daily_amounts = amounts.groupby(amounts.index).sum()
    daily_values = values.groupby(values.index).sum()
    daily_amounts.name = "txn_shares"
    daily_values.name = "txn_volume"
    return pd.concat([daily_values, daily_amounts], axis=1)


def adjust_returns_for_slippage(
    returns, positions, transactions, slippage_bps
):
    """
    Apply a slippage penalty for every dollar traded.

    Parameters
    ----------
    returns : pd.Series
        Daily returns of the strategy, noncumulative.
         - See full explanation in create_full_tear_sheet.
    positions : pd.DataFrame
        Daily net position values.
         - See full explanation in create_full_tear_sheet.
    transactions : pd.DataFrame
        Prices and amounts of executed trades. One row per trade.
         - See full explanation in create_full_tear_sheet.
    slippage_bps: int/float
        Basis points of slippage to apply.

    Returns
    -------
    pd.Series
        Time series of daily returns, adjusted for slippage.
    """

    slippage = 0.0001 * slippage_bps
    portfolio_value = positions.sum(axis=1)

    '''
    **修改pnl算法** 20230725
    pnl（profit and loss）應該要是前一天portfolio_value * 當天returns，
    原先程式碼是`pnl = portfolio_value * returns`，
    代表是當天的portfolio_value * 當天的returns。所以做以下修改：
    1.將當天portfolio_value/(1 + 當天的returns)得到前一天的portfolio_value。
    2.接著再把步驟1的結果乘上當天的returns就得到當天的pnl
    
    此外，當adjusted_pnl=pnl=0時，skew及Kurtosis算不出來，因nan_policy='propagate'，
    所以adjusted_returns先fillna(0)。
    
    註：
    1.這邊的pnl與run_algorithm()裡的pnl不同。這邊的pnl包含收取股利的部分，因portfolio_value
    包含股利（減資退還）的部分。然而run_algorithm()裡的pnl不包含股利（減資退還）。

    2.這樣修改至少達到原本預期的功能，但是還是不太嚴謹。如果多付了slippage，這樣其實每天的portfolio_value
    都會異動，只調整有交易日子的returns，不太對。待後續版本優化。

    3.所以最嚴謹方式還是再跑一次回測，這個僅供快速比較使用。
    '''
    #pnl = portfolio_value * returns                                     # 20230725 (by MRC) 
    pnl = portfolio_value * returns  / (1 + returns)                     # 20230725 (by MRC) 
    
    traded_value = get_txn_vol(transactions).txn_volume
    slippage_dollars = traded_value * slippage
    adjusted_pnl = pnl.add(-slippage_dollars, fill_value=0)
    adjusted_returns = returns * adjusted_pnl / pnl
    
    return adjusted_returns.fillna(0) # 20230725 (by MRC)


def get_turnover(positions, transactions, denominator="AGB"):
    """
     - Value of purchases and sales divided by either the actual
     gross book or the portfolio value for the time step.

    Parameters
    ----------
    positions : pd.DataFrame
        Contains daily position values including cash.
        - See full explanation in tears.create_full_tear_sheet
    transactions : pd.DataFrame
        Prices and amounts of executed trades. One row per trade.
        - See full explanation in tears.create_full_tear_sheet
    denominator : str, optional
        Either 'AGB' or 'portfolio_value', default AGB.
        - AGB (Actual gross book) is the gross market
        value (GMV) of the specific algo being analyzed.
        Swapping out an entire portfolio of stocks for
        another will yield 200% turnover, not 100%, since
        transactions are being made for both sides.
        - We use average of the previous and the current end-of-period
        AGB to avoid singularities when trading only into or
        out of an entire book in one trading period.
        - portfolio_value is the total value of the algo's
        positions end-of-period, including cash.

    Returns
    -------
    turnover_rate : pd.Series
        timeseries of portfolio turnover rates.
    """

    txn_vol = get_txn_vol(transactions)
    traded_value = txn_vol.txn_volume

    if denominator == "AGB":
        # Actual gross book is the same thing as the algo's GMV
        # We want our denom to be avg(AGB previous, AGB current)
        AGB = positions.drop("cash", axis=1).abs().sum(axis=1)
        denom = AGB.rolling(2).mean()

        # Since the first value of pd.rolling returns NaN, we
        # set our "day 0" AGB to 0.
        denom.iloc[0] = AGB.iloc[0] / 2
    elif denominator == "portfolio_value":
        denom = positions.sum(axis=1)
    else:
        raise ValueError(
            "Unexpected value for denominator '{}'. The "
            "denominator parameter must be either 'AGB'"
            " or 'portfolio_value'.".format(denominator)
        )

    denom.index = denom.index.normalize()
    turnover = traded_value.div(denom, axis="index")
    turnover = turnover.fillna(0)
    return turnover
