# Mean-Variance Optimization

MPT solves for the optimal portfolio weights to minimize volatility for a given expected return, or maximize returns for a given level of volatility. The key requisite input are expected asset returns, standard deviations, and the covariance matrix. 

Diversification works because the variance of portfolio returns depends on the covariance of the assets and can be reduced below the weighted average of the asset variances by including assets with less than perfect correlation. In particular, given a vector, ω, of portfolio weights and the covariance matrix, $\Sigma$, the portfolio variance, $\sigma_{\text{PF}}$ is defined as:
$$\sigma_{\text{PF}}=\omega^T\Sigma\omega$$

Markowitz showed that the problem of maximizing the expected portfolio return subject to a target risk has an equivalent dual representation of minimizing portfolio risk subject to a target expected return level, $μ_{PF}$. Hence, the optimization problem becomes:
$$
\begin{align}
\min_\omega & \quad\quad\sigma^2_{\text{PF}}= \omega^T\Sigma\omega\\
\text{s.t.} &\quad\quad \mu_{\text{PF}}= \omega^T\mu\\ 
&\quad\quad \lVert\omega\rVert =1
\end{align}
$$

We can calculate an efficient frontier using `scipy.optimize.minimize` and the historical estimates for asset returns, standard deviations, and the covariance matrix. 

## Imports & Settings


```python
import warnings
warnings.filterwarnings('ignore')
```


```python
%matplotlib inline

import pandas as pd
import numpy as np
from numpy.random import random, uniform, dirichlet, choice
from numpy.linalg import inv

from scipy.optimize import minimize

import pandas_datareader.data as web
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import seaborn as sns
```


```python
sns.set_style('whitegrid')
np.random.seed(42)
```


```python
cmap = sns.diverging_palette(10, 240, n=9, as_cmap=True)
```

## Prepare Data

We select historical data for tickers included in the S&P500 (according to Wikipedia) from 1998-2017.


```python
with pd.HDFStore('../data/assets.h5') as store:
    sp500_stocks = store['sp500/stocks']
```


```python
sp500_stocks.head()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>name</th>
      <th>gics_sector</th>
      <th>gics_sub_industry</th>
      <th>location</th>
      <th>first_added</th>
      <th>cik</th>
      <th>founded</th>
    </tr>
    <tr>
      <th>ticker</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>MMM</th>
      <td>3M Company</td>
      <td>Industrials</td>
      <td>Industrial Conglomerates</td>
      <td>St. Paul, Minnesota</td>
      <td>1976-08-09</td>
      <td>66740</td>
      <td>1902</td>
    </tr>
    <tr>
      <th>ABT</th>
      <td>Abbott Laboratories</td>
      <td>Health Care</td>
      <td>Health Care Equipment</td>
      <td>North Chicago, Illinois</td>
      <td>1964-03-31</td>
      <td>1800</td>
      <td>1888</td>
    </tr>
    <tr>
      <th>ABBV</th>
      <td>AbbVie Inc.</td>
      <td>Health Care</td>
      <td>Pharmaceuticals</td>
      <td>North Chicago, Illinois</td>
      <td>2012-12-31</td>
      <td>1551152</td>
      <td>2013 (1888)</td>
    </tr>
    <tr>
      <th>ABMD</th>
      <td>ABIOMED Inc</td>
      <td>Health Care</td>
      <td>Health Care Equipment</td>
      <td>Danvers, Massachusetts</td>
      <td>2018-05-31</td>
      <td>815094</td>
      <td>1981</td>
    </tr>
    <tr>
      <th>ACN</th>
      <td>Accenture plc</td>
      <td>Information Technology</td>
      <td>IT Consulting &amp; Other Services</td>
      <td>Dublin, Ireland</td>
      <td>2011-07-06</td>
      <td>1467373</td>
      <td>1989</td>
    </tr>
  </tbody>
</table>
</div>




```python
with pd.HDFStore('../data/assets.h5') as store:
    prices = (store['quandl/wiki/prices']
              .adj_close
              .unstack('ticker')
              .filter(sp500_stocks.index)
              .sample(n=30, axis=1))
```

## Compute Inputs

### Compute Returns


```python
start = 2008
end = 2017
```

Create month-end monthly returns and drop dates that have no observations:


```python
weekly_returns = prices.loc[f'{start}':f'{end}'].resample('W').last().pct_change().dropna(how='all')
weekly_returns = weekly_returns.dropna(axis=1)
weekly_returns.info()
```

    <class 'pandas.core.frame.DataFrame'>
    DatetimeIndex: 521 entries, 2008-01-13 to 2017-12-31
    Freq: W-SUN
    Data columns (total 25 columns):
     #   Column  Non-Null Count  Dtype  
    ---  ------  --------------  -----  
     0   WYNN    521 non-null    float64
     1   FIS     521 non-null    float64
     2   VLO     521 non-null    float64
     3   MRK     521 non-null    float64
     4   DTE     521 non-null    float64
     5   REGN    521 non-null    float64
     6   TEL     521 non-null    float64
     7   YUM     521 non-null    float64
     8   HST     521 non-null    float64
     9   AES     521 non-null    float64
     10  XOM     521 non-null    float64
     11  NEE     521 non-null    float64
     12  HAL     521 non-null    float64
     13  TJX     521 non-null    float64
     14  AXP     521 non-null    float64
     15  IR      521 non-null    float64
     16  CERN    521 non-null    float64
     17  IVZ     521 non-null    float64
     18  XLNX    521 non-null    float64
     19  LOW     521 non-null    float64
     20  WEC     521 non-null    float64
     21  ETFC    521 non-null    float64
     22  XEL     521 non-null    float64
     23  CDNS    521 non-null    float64
     24  BXP     521 non-null    float64
    dtypes: float64(25)
    memory usage: 105.8 KB


### Set  Parameters


```python
stocks = weekly_returns.columns
```


```python
n_obs, n_assets = weekly_returns.shape
n_assets, n_obs
```




    (25, 521)




```python
NUM_PF = 100000 # no of portfolios to simulate
```


```python
x0 = uniform(0, 1, n_assets)
x0 /= np.sum(np.abs(x0))
```

### Annualization Factor


```python
periods_per_year = round(weekly_returns.resample('A').size().mean())
periods_per_year
```




    52



### Compute Mean Returns, Covariance and Precision Matrix


```python
mean_returns = weekly_returns.mean()
cov_matrix = weekly_returns.cov()
```

The precision matrix is the inverse of the covariance matrix:


```python
precision_matrix = pd.DataFrame(inv(cov_matrix), index=stocks, columns=stocks)
```

### Risk-Free Rate

Load historical 10-year Treasury rate:


```python
treasury_10yr_monthly = (web.DataReader('DGS10', 'fred', start, end)
                         .resample('M')
                         .last()
                         .div(periods_per_year)
                         .div(100)
                         .squeeze())
```


```python
rf_rate = treasury_10yr_monthly.mean()
```

## Simulate Random Portfolios

The simulation generates random weights using the Dirichlet distribution, and computes the mean, standard deviation, and SR for each sample portfolio using the historical return data:


```python
def simulate_portfolios(mean_ret, cov, rf_rate=rf_rate, short=True):
    alpha = np.full(shape=n_assets, fill_value=.05)
    weights = dirichlet(alpha=alpha, size=NUM_PF)
    if short:
        weights *= choice([-1, 1], size=weights.shape)

    returns = weights @ mean_ret.values + 1
    returns = returns ** periods_per_year - 1
    std = (weights @ weekly_returns.T).std(1)
    std *= np.sqrt(periods_per_year)
    sharpe = (returns - rf_rate) / std
    return pd.DataFrame({'Annualized Standard Deviation': std,
                         'Annualized Returns': returns,
                         'Sharpe Ratio': sharpe}), weights
```


```python
simul_perf, simul_wt = simulate_portfolios(mean_returns, cov_matrix, short=False)
```


```python
df = pd.DataFrame(simul_wt)
df.describe()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>0</th>
      <th>1</th>
      <th>2</th>
      <th>3</th>
      <th>4</th>
      <th>5</th>
      <th>6</th>
      <th>7</th>
      <th>8</th>
      <th>9</th>
      <th>...</th>
      <th>15</th>
      <th>16</th>
      <th>17</th>
      <th>18</th>
      <th>19</th>
      <th>20</th>
      <th>21</th>
      <th>22</th>
      <th>23</th>
      <th>24</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>count</th>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>...</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
      <td>1.000000e+05</td>
    </tr>
    <tr>
      <th>mean</th>
      <td>4.014051e-02</td>
      <td>4.021212e-02</td>
      <td>3.990815e-02</td>
      <td>3.998121e-02</td>
      <td>4.091333e-02</td>
      <td>4.010946e-02</td>
      <td>3.994396e-02</td>
      <td>4.008751e-02</td>
      <td>4.040184e-02</td>
      <td>3.962200e-02</td>
      <td>...</td>
      <td>3.931306e-02</td>
      <td>3.911847e-02</td>
      <td>3.958634e-02</td>
      <td>4.031997e-02</td>
      <td>4.065440e-02</td>
      <td>3.994213e-02</td>
      <td>4.017737e-02</td>
      <td>4.007840e-02</td>
      <td>4.026256e-02</td>
      <td>3.993907e-02</td>
    </tr>
    <tr>
      <th>std</th>
      <td>1.312552e-01</td>
      <td>1.311073e-01</td>
      <td>1.303379e-01</td>
      <td>1.306601e-01</td>
      <td>1.329938e-01</td>
      <td>1.307786e-01</td>
      <td>1.301776e-01</td>
      <td>1.307051e-01</td>
      <td>1.321454e-01</td>
      <td>1.293530e-01</td>
      <td>...</td>
      <td>1.296803e-01</td>
      <td>1.282788e-01</td>
      <td>1.301828e-01</td>
      <td>1.316890e-01</td>
      <td>1.317836e-01</td>
      <td>1.309682e-01</td>
      <td>1.313345e-01</td>
      <td>1.307637e-01</td>
      <td>1.315626e-01</td>
      <td>1.302771e-01</td>
    </tr>
    <tr>
      <th>min</th>
      <td>5.487810e-97</td>
      <td>6.391219e-103</td>
      <td>2.537639e-96</td>
      <td>3.103047e-92</td>
      <td>1.394197e-95</td>
      <td>2.796315e-119</td>
      <td>2.366047e-102</td>
      <td>8.830544e-107</td>
      <td>4.507542e-97</td>
      <td>1.620766e-93</td>
      <td>...</td>
      <td>1.696634e-103</td>
      <td>1.842655e-111</td>
      <td>2.948403e-110</td>
      <td>3.165806e-102</td>
      <td>1.177348e-109</td>
      <td>2.898130e-93</td>
      <td>3.038647e-99</td>
      <td>8.175665e-100</td>
      <td>1.990480e-122</td>
      <td>3.521496e-97</td>
    </tr>
    <tr>
      <th>25%</th>
      <td>6.548585e-13</td>
      <td>7.526395e-13</td>
      <td>7.532093e-13</td>
      <td>7.098138e-13</td>
      <td>6.967077e-13</td>
      <td>8.502324e-13</td>
      <td>6.950384e-13</td>
      <td>6.402276e-13</td>
      <td>6.214023e-13</td>
      <td>6.271992e-13</td>
      <td>...</td>
      <td>5.260179e-13</td>
      <td>6.504203e-13</td>
      <td>6.463332e-13</td>
      <td>5.624521e-13</td>
      <td>6.814244e-13</td>
      <td>8.556783e-13</td>
      <td>8.633643e-13</td>
      <td>8.498675e-13</td>
      <td>6.467348e-13</td>
      <td>6.834165e-13</td>
    </tr>
    <tr>
      <th>50%</th>
      <td>6.616167e-07</td>
      <td>7.502422e-07</td>
      <td>7.797426e-07</td>
      <td>7.198818e-07</td>
      <td>7.358429e-07</td>
      <td>7.421117e-07</td>
      <td>6.911534e-07</td>
      <td>6.738274e-07</td>
      <td>6.997784e-07</td>
      <td>7.061397e-07</td>
      <td>...</td>
      <td>6.020512e-07</td>
      <td>7.166774e-07</td>
      <td>7.237275e-07</td>
      <td>7.098441e-07</td>
      <td>8.050248e-07</td>
      <td>8.248063e-07</td>
      <td>7.306568e-07</td>
      <td>7.889467e-07</td>
      <td>6.695123e-07</td>
      <td>7.623604e-07</td>
    </tr>
    <tr>
      <th>75%</th>
      <td>2.277156e-03</td>
      <td>2.395975e-03</td>
      <td>2.408796e-03</td>
      <td>2.264291e-03</td>
      <td>2.452694e-03</td>
      <td>2.467333e-03</td>
      <td>2.558821e-03</td>
      <td>2.387084e-03</td>
      <td>2.386800e-03</td>
      <td>2.457362e-03</td>
      <td>...</td>
      <td>2.144058e-03</td>
      <td>2.313919e-03</td>
      <td>2.412220e-03</td>
      <td>2.323442e-03</td>
      <td>2.513800e-03</td>
      <td>2.377078e-03</td>
      <td>2.345158e-03</td>
      <td>2.410579e-03</td>
      <td>2.369465e-03</td>
      <td>2.459960e-03</td>
    </tr>
    <tr>
      <th>max</th>
      <td>9.977419e-01</td>
      <td>9.979261e-01</td>
      <td>9.993984e-01</td>
      <td>9.998279e-01</td>
      <td>9.997830e-01</td>
      <td>9.990782e-01</td>
      <td>9.984043e-01</td>
      <td>9.987972e-01</td>
      <td>9.992743e-01</td>
      <td>9.999805e-01</td>
      <td>...</td>
      <td>9.995104e-01</td>
      <td>9.997510e-01</td>
      <td>9.979907e-01</td>
      <td>9.991659e-01</td>
      <td>9.982628e-01</td>
      <td>9.984974e-01</td>
      <td>9.992195e-01</td>
      <td>9.986744e-01</td>
      <td>9.990703e-01</td>
      <td>9.993066e-01</td>
    </tr>
  </tbody>
</table>
<p>8 rows × 25 columns</p>
</div>



### Plot Simulated Portfolios


```python
ax = simul_perf.plot.scatter(x=0, y=1, c=2, cmap='Blues',
                             alpha=0.5, figsize=(14, 9), colorbar=True,
                             title=f'{NUM_PF:,d} Simulated Portfolios')

max_sharpe_idx = simul_perf.iloc[:, 2].idxmax()
sd, r = simul_perf.iloc[max_sharpe_idx, :2].values
print(f'Max Sharpe: {sd:.2%}, {r:.2%}')
ax.scatter(sd, r, marker='*', color='darkblue', s=500, label='Max. Sharpe Ratio')

min_vol_idx = simul_perf.iloc[:, 0].idxmin()
sd, r = simul_perf.iloc[min_vol_idx, :2].values
ax.scatter(sd, r, marker='*', color='green', s=500, label='Min Volatility')
plt.legend(labelspacing=1, loc='upper left')
plt.tight_layout()
```

## Compute Annualize PF Performance

Now we'll set up the quadratic optimization problem to solve for the minimum standard deviation for a given return or the maximum SR. 

To this end, define the functions that measure the key metrics:


```python
def portfolio_std(wt, rt=None, cov=None):
    """Annualized PF standard deviation"""
    return np.sqrt(wt @ cov @ wt * periods_per_year)
```


```python
def portfolio_returns(wt, rt=None, cov=None):
    """Annualized PF returns"""
    return (wt @ rt + 1) ** periods_per_year - 1
```


```python
def portfolio_performance(wt, rt, cov):
    """Annualized PF returns & standard deviation"""
    r = portfolio_returns(wt, rt=rt)
    sd = portfolio_std(wt, cov=cov)
    return r, sd
```

## Max Sharpe PF

Define a target function that represents the negative SR for scipy's minimize function to optimize, given the constraints that the weights are bounded by [-1, 1], if short trading is permitted, and [0, 1] otherwise, and sum to one in absolute terms.


```python
def neg_sharpe_ratio(weights, mean_ret, cov):
    r, sd = portfolio_performance(weights, mean_ret, cov)
    return -(r - rf_rate) / sd
```


```python
weight_constraint = {'type': 'eq', 
                     'fun': lambda x: np.sum(np.abs(x))-1}
```


```python
def max_sharpe_ratio(mean_ret, cov, short=False):
    return minimize(fun=neg_sharpe_ratio,
                    x0=x0,
                    args=(mean_ret, cov),
                    method='SLSQP',
                    bounds=((-1 if short else 0, 1),) * n_assets,
                    constraints=weight_constraint,
                    options={'tol':1e-10, 'maxiter':1e4})
```

## Compute Efficient Frontier

The solution requires iterating over ranges of acceptable values to identify optimal risk-return combinations


```python
def min_vol_target(mean_ret, cov, target, short=False):

    def ret_(wt):
        return portfolio_returns(wt, mean_ret)

    constraints = [{'type': 'eq',
                    'fun': lambda x: ret_(x) - target},
                   weight_constraint]

    bounds = ((-1 if short else 0, 1),) * n_assets
    return minimize(portfolio_std,
                    x0=x0,
                    args=(mean_ret, cov),
                    method='SLSQP',
                    bounds=bounds,
                    constraints=constraints,
                    options={'tol': 1e-10, 'maxiter': 1e4})
```

The mean-variance frontier relies on in-sample, backward-looking optimization. In practice, portfolio optimization requires forward-looking input. Unfortunately, expected returns are notoriously difficult to estimate accurately.

The covariance matrix can be estimated somewhat more reliably, which has given rise to several alternative approaches. However, covariance matrices with correlated assets pose computational challenges since the optimization problem requires inverting the matrix. The high condition number induces numerical instability, which in turn gives rise to Markovitz curse: the more diversification is required (by correlated investment opportunities), the more unreliable the weights produced by the algorithm. 

## Min Volatility Portfolio


```python
def min_vol(mean_ret, cov, short=False):
    bounds = ((-1 if short else 0, 1),) * n_assets

    return minimize(fun=portfolio_std,
                    x0=x0,
                    args=(mean_ret, cov),
                    method='SLSQP',
                    bounds=bounds,
                    constraints=weight_constraint,
                    options={'tol': 1e-10, 'maxiter': 1e4})
```


```python
def efficient_frontier(mean_ret, cov, ret_range, short=False):
    return [min_vol_target(mean_ret, cov, ret) for ret in ret_range]
```

## Run Calculation

### Get random PF


```python
simul_perf, simul_wt = simulate_portfolios(mean_returns, cov_matrix, short=False)
```


```python
print(simul_perf.describe())
```

           Annualized Standard Deviation  Annualized Returns   Sharpe Ratio
    count                  100000.000000       100000.000000  100000.000000
    mean                        0.272889            0.177186       0.675622
    std                         0.076975            0.055340       0.213748
    min                         0.139966            0.019720       0.055767
    25%                         0.217795            0.148334       0.511378
    50%                         0.260579            0.173843       0.665910
    75%                         0.314480            0.202062       0.834405
    max                         0.654543            0.466036       1.332866



```python
simul_max_sharpe = simul_perf.iloc[:, 2].idxmax()
simul_perf.iloc[simul_max_sharpe]
```




    Annualized Standard Deviation    0.166130
    Annualized Returns               0.221928
    Sharpe Ratio                     1.332866
    Name: 67110, dtype: float64



### Get Max Sharpe PF


```python
max_sharpe_pf = max_sharpe_ratio(mean_returns, cov_matrix, short=False)
max_sharpe_perf = portfolio_performance(max_sharpe_pf.x, mean_returns, cov_matrix)
```


```python
r, sd = max_sharpe_perf
pd.Series({'ret': r, 'sd': sd, 'sr': (r-rf_rate)/sd})
```




    ret    0.228716
    sd     0.166360
    sr     1.371822
    dtype: float64



From simulated pf data

### Get Min Vol PF


```python
min_vol_pf = min_vol(mean_returns, cov_matrix, short=False)
min_vol_perf = portfolio_performance(min_vol_pf.x, mean_returns, cov_matrix)
```

### Get Efficent PFs


```python
ret_range = np.linspace(simul_perf.iloc[:, 1].min(), simul_perf.iloc[:, 1].max(), 50)
eff_pf = efficient_frontier(mean_returns, cov_matrix, ret_range, short=True)
eff_pf = pd.Series(dict(zip([p['fun'] for p in eff_pf], ret_range)))
```

### Plot Result

The simulation yields a subset of the feasible portfolios, and the efficient frontier identifies the optimal in-sample return-risk combinations that were achievable given historic data. 

The below figure shows the result including the minimum variance portfolio and the portfolio that maximizes the SR and several portfolios produce by alternative optimization strategies. The efficient frontier 


```python
fig, ax = plt.subplots()
simul_perf.plot.scatter(x=0, y=1, c=2, ax=ax, cmap='Blues',alpha=0.25, 
                        figsize=(14, 9), colorbar=True)

eff_pf[eff_pf.index.min():].plot(linestyle='--', lw=2, ax=ax, c='k',
                                 label='Efficient Frontier')

r, sd = max_sharpe_perf
ax.scatter(sd, r, marker='*', color='k', s=500, label='Max Sharpe Ratio PF')

r, sd = min_vol_perf
ax.scatter(sd, r, marker='v', color='k', s=200, label='Min Volatility PF')

kelly_wt = precision_matrix.dot(mean_returns).clip(lower=0).values
kelly_wt /= np.sum(np.abs(kelly_wt))
r, sd = portfolio_performance(kelly_wt, mean_returns, cov_matrix)
ax.scatter(sd, r, marker='D', color='k', s=150, label='Kelly PF')

std = weekly_returns.std()
std /= std.sum()
r, sd = portfolio_performance(std, mean_returns, cov_matrix)
ax.scatter(sd, r, marker='X', color='k', s=250, label='Risk Parity PF')

r, sd = portfolio_performance(np.full(n_assets, 1/n_assets), mean_returns, cov_matrix)
ax.scatter(sd, r, marker='o', color='k', s=200, label='1/n PF')


ax.legend(labelspacing=0.8)
ax.set_xlim(0, eff_pf.max()+.4)
ax.set_title('Mean-Variance Efficient Frontier', fontsize=16)
ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
ax.xaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
sns.despine()
fig.tight_layout();
```

0f034d33b54098c69487809e83040871a2117788
ipynb
Jupyter Notebook
ml4trading-2ed/05_strategy_evaluation/04_mean_variance_optimization.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
ml4trading-2ed/05_strategy_evaluation/04_mean_variance_optimization.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
2020-10-27T19:44:15.000Z
2020-11-03T23:55:36.000Z
ml4trading-2ed/05_strategy_evaluation/04_mean_variance_optimization.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
Qwen/Qwen-72B
1. YES
2. YES